Pwntools를 사용하기 전에는 socket() 함수로 Pwnable 문제를 풀거나 혹은 한 줄 익스플로잇 코드로 Pwnable문제를 풀었을 것입니다.
이러한 문제점들을 개선하여 시스템 해킹 즉, Pwnable에 필요한 기능들을 탑재한 모듈이 바로 Pwntools라고 할 수 있다.
Pwntools를 사용하면 socket() 함수를 사용하는 것보다 더욱 간결한 코드로 Exploit Code를 작성할 수 있다.
// socket() 함수를 사용하는 경우
from socket import *
s = socket(AF_INET, SOCK_STREA)
s.connect('localhost', 4000)
// Pwntools를 이용하는 경우
from pwn import *
r = remote('localhost', 4000)
process() 함수는 로컬 바이너리에 대해서 익스플로잇을 실험해볼 때, 사용하는 함수이며,
remote() 함수는 원격 서버를 대상으로 실제 익스플로잇을 작동시킬 때 사용하는 함수이고,
ssh() 함수는 ssh를 통해서 접속하는 함수이다.
from pwn import *
p = process('./test') # process(filename)
r = remote('snowcluster.com', 4000) # remote(host, port)
s = ssh("fd". "snowcluster.com", 4000) # ssh(user, port, password)순서
p.close() # 접속 종료
r.close() # 접속 종료
s.close() # 접속 종료
from pwn import *
p = process('./hello')
p.send('A') # ./hello에 'A'입력
p.sendline('A') # ./hello에 'A' 입력 뒤에 "\n"(newline character)까지 입력
p.sendafter('asdf', 'A') # ./hello가 'asdf'를 출력할시에 'A'를 입력한다.
p.sendlineafter('asdf', 'A') # ./hello가 'asdf'를 출력할시에 'A' + "\n"을 입력한다.
프로그램이 출력하는 값을 봐야할 때, recv() 함수를 사용한다.
recv() 함수는 프로그램과의 interaction을 보기 위해 사용한다.
recv()와 recvn()의 차이점은 그냥 무조건 받는 것과 특정 바이트 만큼 받는다는 점에서 차이가 있다.
-> 즉, p.recv()는 최대 n바이크 만큼의 크기를 받는 것이기 때문에, 1024바이트를 모두 다 채워 받지 못하더라도 에러를 발생시키지 않지만,
-> p.recvn()의 경우 정확히 인자만큼의 데이터를 받지 못하면 계속 대기한다는 차이점이 있다.
-> print p.recv(1024)의 방식을 가장 많이 사용하고 잇다.
from pwn import *
p = process("./hello")
data = p.recv(1024) # p가 출력하는 데이터 중 최대 1024바이트의 데이터를 받아서 data에 저장한다.
data = p.recvline() # p가 출력하는 데이터 중 개행문자를 만날 때까지를 data에 저장한다.
data = p.recvn(5) # p가 출력하는 데이터 중 정확히 5바이트를 받아서 data에 저장한다.
print p.recvuntil('asdf') # 'asdf'라는 문자열을 p가 출력할 때까지 받아서 출력한다.
print p.recvall() # 연결이 끊어지거나 프로세스가 종료될 때까지 데이터를 받아서 data에 저장한다.
Dynamically-Linked된 바이너리를 대상으로 문제를 풀 때 ELF를 통하여 정보를 가져올 수 있으며, 가장 대표적으로 GOT, PLT 등이 있는데, Pwntools의 API를 이용하면 이러한 값들을 쉽게 구할 수 있다.
또한, run은 바이너리를 실행시킬 수 있다. (run은 거의 사용되지 않는다.)
from pwn import *
p = process('./hello')
e = ELF('./hello') # e에 hello의 ELF를 불러온다. (PLT)
libc = ELF('./libc.so.6') # libc에 libc.so.6 라이브러리를 불러온다.
# (보통 바이너리와 같이 제공된다.)
sh = p.run('/bin/sh') # hello로 /bin/sh를 바이너리에 실행시킨다.
sh.sendline('whoami') # 권한이 있다는 전제하에 "whoami"를 전송한다.
puts_plt = e.plt['puts'] # ELF ./hello 에서 puts()의 PLT 주소를 찾아서 puts_plt에 넣는다.
read_got = libc.got['read'] # libc base에서 read()의 GOT 주소를 찾아서 read_got에 넣는다.
from pwn import *
data32 = 0x42424344
data64 = 0x4242434445464748
print(p32(data32))
print(p64(data64))
data32 = "ABCD"
data64 = "ABCDEFGH"
print(hex(u32(data32)))
print(hex(u64(data64)))

from pwn import *
p = process('./hello')
gdb.attach(p) # gdb에 attach시킨다.
쉘을 획득한 경우나, 특정한 익스플로잇의 경우에는 직적 입력을 주면서 디버깅을 해줘야 하는 경우가 존재하는데, 이때 사용하는 함수가 바로 interactive이다.
interactive 함수는 익스플로잇 파일과 프로세스와의 연결을 stdin/stdout에서 process로 바꿔준다.
from pwn import *
p = process('./hello')
p.interactive()
from pwn import *
context.log_level = 'error' # 에러만 출력한다.
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력한다.
context.log_level = 'info' # 비교적 중요한 정보들만 출력한다.
Pwntools는 쉘코드를 생성하거나, 코드를 어셈블 / 디스어셈블 하는 기능 등을 가지고 있는데, 이들은 공격 대상의 아키텍처에 영향을 받는다.
그래서 Pwntools는 아키텍처 정보를 프로그래머가 지정할 수 있게 하며, 해당 값에 따라 몇몇 함수들의 동작이 달라진다.
from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386" # x86 아키텍처
context.arch = "arm" # arm 아키텍처
Pwntools에는 자주 사용되는 쉘코드들이 저장되어 있어서, 공격에 필요한 쉘코드를 쉽게 꺼내쓸 수 있게 해준다.
매우 편리한 기능이지만, 정적으로 생성된 쉘코드는 쉘코드가 실행될 때의 메모리를 반영하지 못하며,
또한, 프로그램에 따라 입력할 수 있는 쉘코드의 길이나, 구성 가능한 문자의 종류에 제약이 있을 수 있는데, 이런 조건들을 반영하기 어렵다.
즉, 제약 조건이 존재하는 상황에서는 직접 쉘코드를 작성하는 것이 좋다.
- 아래의 문서에서 x86-64 아키텍처를 대상으로 생성할 수 있는 여러 쉘코드의 종류를 살펴볼 수 있다.
https://docs.pwntools.com/en/stable/shellcraft/amd64.html
# shellcraft 예제
from pwn import *
context.arch = "amd64" # x86-64 아키텍처
code = shellcraft.sh() # 쉘을 실행하는 쉘코드
print(code)

from pwn import *
context.arch = "amd64" # 익스플로잇 대상 아키텍처 x86-64
code = shellcraft.sh() # 쉘을 실행하는 쉘코드
code = asm(code) # 쉘코드를 기계어로 어셈블한다.
print(code)
