Pwntools란 파이썬에서 지원하는 라이브러리 중 하나로 익스플로잇을 쉽게 할 수 있도록 지원해주는 기능들이 포함되어 있다. 파이썬 2.x버전에서만 사용 가능하다고 알고 있었는데, python 3.x 에서 사용해보니 문제 없이 사용이 가능하길래 3.x 버전을 기준으로 작성하였다.
설치
pwntools는 파이썬의 라이브러리이기 때문에 파이썬의 라이브러리를 관리해주는 pip를 먼저 설치한 다음 pip를 이용해 pwntools를 설치하면 된다. 참고로 파이썬 3의 경우, pip가 아닌 pip3를 사용해야 한다.
$ pip3 install pwntools
이후 pip3 list를 통해 설치된 패키지 목록을 출력하여 pwntools가 정상적으로 설치되었는지 확인하면 된다.
사용법
기본적인 메소드는 다음과 같다.
process("파일 이름") #로컬 상의 실행 파일을 불러온다.
recv(int) #표준 출력에서 int 만큼의 문자열을 읽어서 반환한다.
recvuntil("str") #표준 출력에서 "str"이라는 문자열까지 읽어서 반환한다.
recvline() # 표준 출력에서 한 줄을 읽어서 반환한다. recvuntil("\n")으로도 동일한 동작 구현 가능.
send("str") # 표준 입력에 "str"이라는 문자열을 넣어준다.
sendline("str") # 표준 입력에 "str\n"이라는 문자열을 넣어준다.
interactive() # 유저가 화면에 직접 입출력 할 수 있도록 해준다.
예시를 통해 사용법을 익히도록 한다.
//hello.c
//gcc -o hello hello.c
#include <stdio.h>
int main(){
printf("Hello \n");
printf("World! \n");
return 0;
}
위 코드는 표준 출력 함수인 printf를 두 번 사용해서 각각 "Hello,", "World!"를 출력하는 코드이다. 이를 파이썬의 pwn 라이브러리를 이용해서 한 줄씩 출력해보면 다음과 같다.
>>> from pwn import *
>>> r = process("./hello")
[x] Starting local process './hello'
[+] Starting local process './hello': pid 40491
>>> tmp = r.recvline()
>>> print(tmp)
b'Hello \n'
>>> tmp = r.recvline()
>>> print(tmp)
b'World! \n
표준 출력을 recvline 메소드를 이용해 한 줄씩 받아와 tmp에 저장한 후에 이를 출력해주는 동작이다. 이번엔 반대로 표준 입력을 받고, 이를 출력하는 코드를 pwntools를 이용해 동작시키는 과정이다. 먼저 소스 코드는 다음과 같다.
//buffer.c
//gcc -o buffer buffer.c
#include <stdio.h>
int main(){
char buffer[20];
gets(buffer);
printf("%s \n", buffer);
return 0;
}
gets를 통해 buffer에 입력을 받은 후 이를 출력해주는 코드이다. pwntools의 사용 예시는 다음과 같다.
>>> from pwn import *
>>> r = process("./buffer")
[x] Starting local process './buffer'
[+] Starting local process './buffer': pid 41020
>>> r.sendline("ABCD")
>>> r.recvline()
b'ABCD \n'
코드의 실행 과정을 보면 gets를 통한 표준 입력을 받는 것이 먼저이고 그 후에 printf 표준 출력 함수로 buffer를 출력해주기 때문에 pwntools에서도 sendline 메소드를 통해 입력을 넘기고 recvline 메소드를 통해 출력 값을 받아온 것을 알 수 있다. 이 때, sendline은 문자열 + '\n'을 넘겨주기 때문에 recvline 으로 받아온 문자열 뒤에 \n이 추가된 것을 확인할 수 있다.
위의 경우는 로컬, 즉 사용자의 서버에 있는 파일들을 가지고 pwntools를 활용하는 예시였지만, 실제로 공격은 상대방의 서버에 접속한 후에 이루어진다. pwntools를 이용해 서버에 연결하는 방법은 아래와 같다.
NC
r = remote(IP, PORT)
SSH
r = ssh(USERNAME, IP, PORT, PASSWORD)
주로 쓰이는 방식은 NC이지만 특수한 경우 SSH를 사용해야 할 때도 있다. 해당 메소드를 이용해서 원격 접속한 후에 공격하고자 하는 파일을 실행시켜서 공격하는 방식으로 이루어진다.
활용
pwntools는 보통 CTF(Capture The Flag) 문제를 풀거나, wargame 문제를 풀 때 주로 사용된다. 아래는 실제 문제를 pwntools를 이용해 해결하는 과정이다. 문제는 pwnable.kr의 첫 번째 문제인 fd이다. pwnable.kr은 ssh 통신을 이용해 접속해서 문제를 푸는 방식이기에 ssh 접속을 이용한다. fd에 접속하고 나면 fd, fd.c, flag 가 보이는데 fd.c의 내용은 다음과 같다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buf[32];
int main(int argc, char* argv[], char* envp[]){
if(argc<2){
printf("pass argv[1] a number\n");
return 0;
}
int fd = atoi( argv[1] ) - 0x1234;
int len = 0;
len = read(fd, buf, 32);
if(!strcmp("LETMEWIN\n", buf)){
printf("good job :)\n");
system("/bin/cat flag");
exit(0);
}
printf("learn about Linux file IO\n");
return 0;
}
argv를 정수로 바꾼 후에 16진수 0x1234를 빼준 값을 fd로 사용한다. len = read(fd, buf, 32)는 다음의 동작을 수행한다.
fd : 파일 디스크럽터. 해당 값은 0, 1, 2가 들어갈 수 있는데 각각 표준 입력, 표준 출력, 표준 에러를 의미한다.
해당 코드에선 buf에 값을 입력해야 하기 때문에 fd가 0이 되어야 함.
buf : 읽어들일 공간.
len : 읽어 올 문자 길이.
read 함수는 내용을 정상적으로 읽어온다면 읽어 온 바이트 수를 반환하고, 그렇지 않다면 0을 반환한다.
공격에 성공해서 flag를 읽기 위해선 다음의 과정을 거쳐야 한다.
- argv[1]의 값을 0x1234로 만들어 주어야 함.
- read 함수의 fd가 0이 되면 표준 입력 대기 상태가 되고, 입력한 값을 buf에 저장하게 됨.
- buf에 "LETMEWIN\n"을 저장해서 if문의 조건을 만족시키고, /bin/cat flag를 실행시켜야 함.
이를 pwntools를 이용해 작성하면 다음과 같다. 우선 pwnable.kr은 id, ip, port, password가 모두 주어지고 ssh를 통해 사용자가 로그인 한 후에 문제를 푸는 방식이기 때문에, ssh를 이용해 접속한다.
r = ssh("fd", "pwnable.kr", 2222, "guest")
로그인 하면 홈 디렉토리에 fd 실행파일이 바로 존재하기 때문에 이를 실행시켜 주면 된다. 이 때에는 process를 r.process로 사용해야 하며, 파일을 실행할 때 인자(argv)를 넘겨주고 싶다면, 넘겨주고자 하는 값을 process의 두 번째 인자로 작성해주면 된다.
argv = 0x1234
fd = p.process(["./fd", str(argv)])
0x1234에 대응하는 10진수는 계산기를 이용해서도 쉽게 구할 수 있지만, 파이썬에서 0x1234를 입력하면 자동으로 10진수로 출력이 되기 때문에 10진수의 값을 직접 구하지 않고도 사용할 수 있다.
>>> a = 0x1234
>>> a
4660
argv는 문자열 형태여야 하기 때문에 인자로 넘겨줄 때 str(argv)로 넘겨준다.
또 주의할 점이 process는 실행파일 경로와 argv를 둘 다 보낼 땐 하나로 패킹해주어야 하기 때문에 위에선 리스트의 형태로 넘겨주었다. 아래처럼 리스트 변수를 하나 만든 다음, 해당 리스트 변수를 인자로 넘겨주어도 무방하다.
argv = 0x1234
file = ["./fd", str(argv)]
fd = p.process(file)
argc가 2이기 때문에 프로그램이 종료되지 않고 read 함수를 만나게 되는데 이 때 fd가 0이므로 표준 입력 상태가 된다. buf에 입력해야 하는 값을 sendline을 통해 넘겨준다.
fd.sendline('LETMEWIN')
sendline으로 보내야 하는 이유는 비교하는 문자열이 "LETMEWIN"이 아닌 "LETMEWIN\n"이기 때문에 문자열 뒤에 '\n'을 자동으로 붙여주는 sendline을 이용해야 한다. 만약 send를 사용하고 싶다면 입력하는 문자열 뒤에 '\n'을 직접 붙여서 사용해도 동일하게 작동한다.
fd.send("LETMEWIN\n")
문자열을 넘겨준 이후에 printf 출력문이 실행되고 /bin/cat flag가 실행되기 때문에 여려 줄의 출력을 받아와야 한다. 이들을 한번에 읽어들이기 위해 recvall 메소드를 사용한다.
print(fd.recvall())
받아 온 문자열을 출력하면 flag의 내용이 출력된다.
아래는 전체 코드와 실제로 동작하는 화면이다.
#fd.py
from pwn import *
p = ssh("fd","pwnable.kr",2222,"guest")
argv = 0x1234
file = ["./fd", str(argv)]
fd = p.process(file)
fd.sendline('LETMEWIN')
print(fd.recvall())
'Pwnable' 카테고리의 다른 글
[ProjectH4C] [Write-up] 우리 집에 GDB 있는데... 메모리 보고 갈래? (0) | 2020.07.30 |
---|