Pwntools
Gallospled 팀이 개발한 파이썬 익스플로잇 프레임워크로, 익스플로잇을 할 때 유용한 여러 기능들을 제공해 준다
설치
$ apt-get update
$ apt-get install python2.7 python-pip python-dev git libssl-dev libffi-dev build-essential
$ pip install --upgrade pip
$ pip install --upgrade pwntools
사용법
1. Connection
remote
원격 서비에 접속하여 통신할 때 사용되는 클래스
p = remote("127.0.0.1", 5000)
위 코드는 127.0.0.1 주소에 열려있는 5000번 포트에 TCP 연결을 맺는다. 연결이 성공적으로 맺어지면 remote 객체를 리턴한다.
process
로컬 프로세스를 실행하여 통신할 때 사용되는 클래스
p = process("/home/theori/binary")
위 코드는 로컬 파일시스템에 존재하는 /home/theori/binary 바이너리를 실행한다.
process 클래스는 로컬에서 바이너리를 실행할 때 환경 변수를 직접 설정할 수 있고, 프로그램을 실행할 때 인자를 전달해야 할 경우 다음과 같이 전달 할 수 있다.
p = process(["/home/theori/binary","AAAA"], env={"LD_PRELOAD":"./libc.so.6"})
위 코드는 프로그램의 argv[1]에 "AAAA" 문자열을 전달하고 LD_PRELOAD 환경 변수를 ./libc.so.6으로 설정하여 실행한다.
ssh
ssh는 ssh 서버에 접속하여 통신할 때 사용되는 클래스이다.
p = ssh("theori","127.0.0.1", port=22, password="theori")
위 코드는 127.0.0.1 서버에 theori 계정으로 ssh 로그인을 하여 연결을 하는 코드이다.
2. send/recv
send 함수와 recv 메소드를 사용하여 소켓에 연결하거나 프로그램을 실행할 때 데이터를 보내고 읽어들이는 작업을 할 수 있다. 해당 메소드의 경우 연결이 맺어진 객체가 존재해야 사용할 수 있다.
send
send는 연결이 맺어진 객체에 데이터를 보내는 메소드
p = remote("127.0.0.1", 22)
p.send("AAAA")
위 코드는 "127.0.0.1"의 22번 포트에 연결한 후 "AAAA"를 보낸다.
sendline은 연결이 맺어진 객체에 개행을 포함하는 데이터를 보내는 메소드
p = remote("127.0.0.1", 22)
p.sendline("AAAA")
"AAAA\n"을 송신한다.
recv
recv는 연결이 뱆어진 객체로부터 수신한 데이터를 리턴하는 메소드
p = remote("127.0.0.1", 22)
print p.recv(1024)
SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.8
위 코드는 "127.0.0.1"의 22번 포트에 연결했을 때 출력되는 문자열을 1024바이트 만큼 수신하여 출력한다.
recvline은 연결이 맺어진 객체로부터 개행까지 수신하여 리턴하는 메소드
p = remote("127.0.0.1", 22)
print p.recvline()
SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.8
읽을 바이트 수를 지정해주지 않고 개행까지 읽어들였음
pack/unpack
pack과 unpack은 각각의 데이터 크기에 맞게 데이터를 변환할 때 사용한다.
패킹 함수는 정수를 인자로 받아 패킹한 후 문자열 형태로 리턴한다.
언패킹 함수는 문자열을 인자로 받아 언패킹 한 후 정수 형태로 리턴한다.
두 함수는 리틀 엔디언 혹은 빅 엔디언 형태로 지정해줄 수 있고, 지정하지 않는다면 리틀 엔디언 형태로 변환한다. 데이터 크기에 따라 함수가 존재하기 때문에 데이터에 맞게 사용해야 한다.
pack
p8은 1 바이트의 데이터를 패킹하는 함수p16은 2 바이트의 데이터를 패킹하는 함수
p32는 4 바이트의 데이터를 패킹하는 함수
p64는 8 바이트의 데이터를 패킹하는 함수
print p64(0x4142434445464748)
HGFEDCBA
0x4142434445464748을 리틀 엔디언의 형태로 변환하여 "HGFEDCBA"를 리턴
빅 엔디언으로 변환을 하기 위해서는 두 번째 인자로 endian='big'을 명시해주면 된다. print p64(0x4142434445464748)
ABCDEFGH
unpack
u8은 1 바이트 데이터를 언패킹 하는 함수
print u16("AB")
16961
print hex(16961)
0x4241
u16은 2 바이트의 데이터를 언패킹 하는 함수
u32은 4 바이트의 데이터를 언패킹 하는 함수
u64는 8 바이트의 데이터를 언패킹 하는 함수
빅 엔디언으로 변환을 하기 위해서는 두 번째 인자로 endian='big'을 명시해주면 된다.
print u64("ABCDEFGH", endian='big')
4702394921427289928
ELF
익스플로잇 코드를 작성할 때 함수 주소와 문자열 주소 등을 구해야 한다. 이때, ELF를 사용하면 ELF 헤더를 갖고 있는 파일의 경우 파일의 여러 데이터를 가져올 수 있다.
plt는 바이너리에 존재하는 PLT 주소를 가져온다.
print hex(elf.plt['printf'])
0x400480
got는 바이너리에 존재하는 GOT 주소를 가져온다.
print hex(elf.got['printf'])
0x601020
symbols는 바이너리에 존재하는 함수의 주소를 가져온다.
print hex(elf.symbols['giveshell'])
0x4005b6
search는 바이너리에 존재하는 문자열의 주소를 가져온다.
print hex(next(elf.search("Hello World!")))
0x40069c
get_section_by_name은 바이너리에 존재하는 섹션의 주소를 가져온다.
print hex(elf.get_section_by_name('.bss').header.sh_addr)
0x601048
read는 원하는 바이너리 주소의 데이터를 읽어온다.
print `elf.read(0x400000, 4)`
'\x7fELF'
read의 인자로 바이너리의 주소와 읽을 바이트 수를 전달하면 해당하는 주소에 존재하는 값을 읽어온다.
write는 원하는 바이너리 주소에 데이터를 쓴다.
print `elf.read(0x400000, 4)`
'\x7fELF'
elf.write(0x400000, "!!!")
print `elf.read(0x400000, 4)`
'!!!F'
0x400000 주소의 첫 4 바이트는 "\x7fELF" 값을 가지고 있다. write를 사용해서 "!!!" 문자열을 삽입하면 해당하는 주소에 값이 쓰이게 된다. 이는 특정 영역의 코드를 수정하기 위해서 사용할 수 있다.
Assembly/Disassembly
셸코딩을 하거나 특정 바이트의 디스어셈블리 결과를 보기 위해서 사용할 수 있다
Assembly
asm함수는 명령어를 인자로 전달하면 바이트로 변환
asm("ret")
'\xc3'
asm함수는 두 개 이상의 명령어도 변환할 수 있다.
asm 함수는 기본적으로 x86 아키텍처를 지원한다. 시스템의 아키텍처마다 명령어 혹은 레지스터가 다르기 때문에 아키텍처를 따로 지정해야 한다.
context.arch= 'x86_64'
asm("mov rdi,0")
'H\xc7\xc7\x00\x00\x00\x00'
contect.arch를 사용하면 원하는 아키텍처를 지정할 수 있다.
Disassembly
disasm 함수는 특정 바이트를 명령어로 변환
disasm("\x90")
' 0: 90 nop''
disasm 함수는 두 개 이상의 바이트를 명령어로 변환할 수 있다.
disasm 함수는 기본적으로 x86 아키텍처를 지원한다. 시스템의 아키텍처마다 명령어 혹은 레지스터가 다르기 때문에 아키텍처를 따로 지정해야 한다.
context.arch를 통해 x86_64 아키텍처로 지정해주면 해당하는 아키텍처에 맞게 변환한다.
shellcraft
shellcraft는 원하는 시스템 콜과 인자를 지정해주면 셸코드를 작성해주는 기능
shellcraft에서 지원하는 시스템 콜은 다음 명령을 통해 확인할 수 있음
dir(shellcraft)
shellcraft는 스택에 값을 저장하고 레지스터를 인자로 전달하는 셸코드를 작성할 수 있다.
interactive
interactive 함수는 연결이 맺어진 객체와 상호 작용을 할 수 있도록 하는 함수
interactive 함수를 사용하면 사용자가 직접 명령어를 입력하고 출력된 결과를 확인할 수 있음
cyclic
cyclic 함수는 버퍼의 크기를 제대로 계산하지 않고 수많은 데이터를 입력해 리턴 주소가 덮였을 때 버퍼와 리턴 주소의 간격을 정확하게 알아낼 수 있다.
cyclic은 데이터를 바이트마다 다르게 하여 리턴 주소까지의 간격을 계산할 수 있다.
ROP
ROP는 코드 가젯을 연결해서 실행하려는 코드를 작성해주는 기능
ROP는 각각의 아키텍처마다 호출 규약이 다르기 때문에 context.arch를 사용하여 공격하는 바이너리의 아키텍처를 명시해야 한다.
ROP 기능을 사용해서 셸을 획득하기 위해서는 바이너리의 정보를 가져와야 하기 때문에 ELF 함수 또한 필요
fmtstr
fmtstr은 연산이 필요없이 주소와 값을 입력하면 해당 주소를 덮어쓸 수 있도록 코드를 작성할 수 있다