Pwnable & Python
2024년 5월 29일
python #
자주 사용하는 문법 #
byte string #
b'Ascii' 와 같은 형식을 byte string이라고 한다. 문자는 그대로 표현할 수 있고, 바이너리 값은 b'\x12\x34\x56\x78' 형식으로 \xff 까지 표현할 수 있다.
1# 바이트 스트링 뒤집기
2byte_str = b'\x12\x34\x56\x78'
3r_str = byte_str[::-1] # 슬라이싱 구문. 처음부터 끝까지 역순으로 슬라이싱
4print(rstr) # 출력값: b'xV4\x12' == b'\x78\x56\x34\x12'
str().encode() #
문자열을 byte string으로 변환하는 방법
1>>> str(0x1234).encode()
2b'4660'
struct #
숫자를 byte string 으로 변경하거나 반대로 byte string을 숫자로 변경할 때 사용한다.
숫자는 사실 바이트 오더링 개념이 없지만, 시스템(메모리)이나 네트워크와 통신하기 위해 byte string으로 패킹/언패킹하는 작업이 필요하다.
대문자는 부호가 없다는 의미이며, 각 문자는 아래의 의미와 같다.
- b=byte(1) / h=short(2) / i=int(4) / l=long(4) / q=longlong(8)
- f(float) / d(double)
- c(character) / s(string) / P(pascal string)
- P(pointer)
1import struct
2
3hex_value = 0x12345678
4
5byte_str = struct.pack('<I', hex_value )
6print(byte_str) # 출력값: b'xV4\x12'
7
8byte_str = struct.pack('>I', hex_value )
9print(byte_str) # 출력값: b'\x124Vx'
10
11byte_str = struct.pack('!I', hex_value ) # 네트워크 (=빅엔디안)
12print(byte_str) # 출력값: b'\x124Vx'
13
14byte_str = struct.pack('@I', hex_value ) # default 값
15print(byte_str) # 출력값: 네이티브 엔디안 (시스템의 엔디안을 따라감)
16
17str = "hello"
18print(struct.pack('p', str)) # 출력값: b'\x05hello'
19
20# 여러 데이터를 한꺼번에 처리할수도 있으며, unpack은 항상 튜플형태로 리턴된다.
21print(struct.unpack('I', byte_str)) # 출력값: 0x12345678
22
23m_pack = struct.pack('II', 0x11223344, 0x55667788) # 각각 패킹해서 바이트스트링으로 변환
24print(m_pack) # 출력값: b'D3"\x11\x88wfU'
25m_unpack = struct.pack('II', m_pack)
26print(m_unpack) # 출력값: (0x11223344, 0x55667788)
문자열과 숫자 변환 #
int를 사용해서 문자열을 정수 형태로 변환할 수 있다.
정수를 문자열로 변환하려면 bin, oct, hex 함수를 사용하면 되고, 문자열로 변하며 접미사가 붙는다.
1# 문자열을 16진수로 읽어서 숫자로 변환
2print(int("012f3d5e", 16)) # 출력값: 19985886
3# 0x 같은 접미사가 포함된 경우에도 변환할 수 있음
4print(int("0x012f3d5e", 16))
5
6print(hex(0x12345678)) # 출력값: 0x12345678
7print(bin(0x12345678)) # 출력값: 0b00010010...01111000
문자열을 바이트 스트링으로 변환 #
1# 이 값은 hex값으로 보이지만, 사실 아스키로 이뤄진 b'\x30\x30...\x31\x63' 이다.
2byte_str = b'00a9c71c'
3
4# hex 문자열로 변환 (byte str -> string)
5hex_str = byte_str.decode('utf-8') # 00a9c71c
6
7# hex 문자열을 byte string 으로 변환
8real_byte = bytes.fromhex(hex_str) # b'\x00\xa9\xc7\x1c'
9
10# hex 문자열을 숫자로 변환
11real_int = int(hex_str)
pwntools #
설치 #
https://github.com/Gallopsled/pwntools
1$ apt-get update
2$ apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
3$ python3 -m pip install --upgrade pip
4$ python3 -m pip install --upgrade pwntools
사용법 #
process & remote #
1from pwn import *
2p = process('./test') # 로컬 바이너리 'test'를 대상으로 익스플로잇 수행
3p = remote('example.com', 31337) # 'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행
4p = process('./rop', env= {"LD_PRELOAD" : "./libc.so.6"})
마지막 라인은 환경변수 LD_PRELOAD에 현재 경로의 libc.so.6를 지정한다는 의미인데, 의존관계들이 있기 때문에 해당 라이브러리 버전지원이 가능한 운영체제에서 사용해야 에러 없이 실행된다.
gdb 붙이기 #
gdb를 연결하고 pause를 실행시켜두면, 자동으로 gdb가 실행되며 attach 된다.
gdb도 멈춘 상태이고, python 실행도 멈춰있기 때문에 gdb에서 continue하고 pause를 풀어주면 된다.
1gdb.attach(p, '''
2b main
3b *main+91
4''')
5pause()
send #
1from pwn import *
2p = process('./test')
3
4p.send(b'A') # ./test에 b'A'를 입력
5p.sendline(b'A') # ./test에 b'A' + b'\n'을 입력
6p.sendafter(b'hello', b'A') # ./test가 b'hello'를 출력하면, b'A'를 입력
7p.sendlineafter(b'hello', b'A') # ./test가 b'hello'를 출력하면, b'A' + b'\n'을 입력
recv #
recv(n)은 최대 n byte를 받고 종료되지만, recvn(n)은 n byte 받을 때 까지 블럭이 된다.
1from pwn import *
2p = process('./test')
3
4data = p.recv(1024) # p가 출력하는 데이터를 최대 1024바이트까지 받아서 data에 저장
5data = p.recvline() # p가 출력하는 데이터를 개행문자를 만날 때까지 받아서 data에 저장
6data = p.recvn(5) # p가 출력하는 데이터를 5바이트만 받아서 data에 저장
7data = p.recvuntil(b'hello') # p가 b'hello'를 출력할 때까지 데이터를 수신하여 data에 저장
8data = p.recvall() # p가 출력하는 데이터를 프로세스가 종료될 때까지 받아서 data에 저장
packing & unpacking #
1#!/usr/bin/env python3
2# Name: pup.py
3
4from pwn import *
5
6s32 = 0x41424344
7s64 = 0x4142434445464748
8
9print(p32(s32))
10print(p64(s64))
11
12s32 = b"ABCD"
13s64 = b"ABCDEFGH"
14
15print(hex(u32(s32)))
16print(hex(u64(s64)))
17
18import struct
19print(p32(0xdeadbeef) == struct.pack('I', 0xdeadbeef))
20print(u32(b'abcd') == struct.unpack('I', b'abcd')[0])
1$ python3 pup.py
2b'DCBA'
3b'HGFEDCBA'
40x44434241
50x4847464544434241
6True
7True
interactive #
직접 입출력을 키보드로 주고받고 싶을때 사용.
1from pwn import *
2p = process('./test')
3p.interactive()
인터랙티브모드가 실행될 수 없을 때 (쉘코드가 정상실행되지 않은경우) 이 명령을 사용하면 쉘을 얻은것처럼 보이지만, EOF가 입력됐다고 표시되면서 종료된다.
ELF #
ELF 헤더를 파싱해준다
1from pwn import *
2e = ELF('./test')
3puts_plt = e.plt['puts'] # ./test에서 puts()의 PLT주소를 찾아서 puts_plt에 저장
4read_got = e.got['read'] # ./test에서 read()의 GOT주소를 찾아서 read_got에 저장
5
6get_shell = e.symbols['get_shell'] # ./test에서 get_shell 함수의 주소를 찾아준다
7
8bin_sh = list(e.search(b'/bin/sh'))[0] # ./test에서 /bin/sh 문자열을 검색해서 처음 나오는 문자열 오프셋을 저장한다.
context.log #
익스플로잇의 디버깅을 위해 로그레벨을 지정할 수 있다.
1from pwn import *
2context.log_level = 'error' # 에러만 출력
3context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
4context.log_level = 'info' # 비교적 중요한 정보들만 출력
context.arch #
쉘코드 생성이나 어셈블, 디스어셈블 기능을 이용할 때 아키텍쳐에 맞춰야 하기 때문에 아키텍쳐를 지정할 수 있다.
1from pwn import *
2context.arch = "amd64" # x86-64 아키텍처
3context.arch = "i386" # x86 아키텍처
4context.arch = "arm" # arm 아키텍처
shellcraft #
자주 사용하는 쉘코드들을 바로 꺼내올 수 있다.
1#!/usr/bin/env python3
2# Name: shellcraft.py
3
4from pwn import *
5context.arch = 'amd64' # 대상 아키텍처 x86-64
6
7code = shellcraft.sh() # 셸을 실행하는 셸 코드
8print(code)
1$ python3 shellcraft.py
2 /* execve(path='/bin///sh', argv=['sh'], envp=0) */
3 /* push b'/bin///sh\x00' */
4 push 0x68
5 mov rax, 0x732f2f2f6e69622f
6 ...
7 syscall
asm #
어셈블이 가능해서 어셈블리 코드를 기계어로 어셈블할 수 있다. 역시 아키텍쳐가 중요하다.
1#!/usr/bin/env python3
2# Name: asm.py
3
4from pwn import *
5context.arch = 'amd64' # 익스플로잇 대상 아키텍처 'x86-64'
6
7code = shellcraft.sh() # 셸을 실행하는 셸 코드
8code = asm(code) # 셸 코드를 기계어로 어셈블
9print(code)
1$ python3 asm.py
2b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
ljust #
원하는 payload에 패딩을 추가할 때 사용하면 좋다.
1sh = asm(shellcraft.sh())
2payload = sh.ljust(bufsize, b'A') + p64(cnry) + b'B'*0x8 + p64(buf)
쉘코드를 왼쪽정렬(오른쪽에 패딩)하는데, 쉘코드 이후 bufsize 크기까지 패딩 ‘A’를 채워넣는다는 의미이다.
기타 도구 #
strings #
파일에서 문자열을 검색하는 도구. tx를 붙여서 오프셋을 알아올 수 있다.
1$ strings -tx libc-2.27.so | grep "/bin/sh"
2 1b3e1a /bin/sh
file #
checksec #
ROPGodget #
pwntool을 설치하면 같이 설치되는 도구이며, 바이너리에서 리턴 가젯을 검색해주는 도구이다.
1$ python3 -m pip install ROPgadget --user
2
3$ ROPgadget -v
4Version: ROPgadget v7.3
5Author: Jonathan Salwan
6Author page: https://twitter.com/JonathanSalwan
7Project page: http://shell-storm.org/project/ROPgadget/
8
9$ ROPgadget --binary ./rtl --re "pop rdi"
10Gadgets information
11============================================================
120x0000000000400853 : pop rdi ; ret
프로그램이 공유 라이브러리를 사용한다면 라이브러리에서 gadget을 가져와도 된다.
1# ldd로 프로그램에서 사용하고 있는 라이브러리를 확인
2$ ldd ./rop
3 linux-vdso.so.1 (0x00007ffc0b9ce000)
4 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f941f53f000)
5 /lib64/ld-linux-x86-64.so.2 (0x00007f941f73d000)
6
7# 심볼릭 링크된 실제 파일 경로를 찾아옴
8$ readlink -f /lib/x86_64-linux-gnu/libc.so.6
9/usr/lib/x86_64-linux-gnu/libc-2.31.so
10
11# 라이브러리를 실행해보면 버전을 알 수 있음
12$ /usr/lib/x86_64-linux-gnu/libc-2.31.so
13GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.16) stable release version 2.31.
14
15# --binary로 바이너리 경로를 지정하고, --re로 검색할 가젯의 패턴을 정규표현식으로 전달한다.
16$ ROPgadget --binary /usr/lib/x86_64-linux-gnu/libc-2.31.so --re "pop rdi"
17... gadget list ...
one_gadget #
libc에서 단일 가젯으로 쉘을 획득할 수 있는 강력한 가젯을 찾아오는 도구이다. libc 버전이 올라갈수록 찾거나 사용하기 어려워진다. constraints가 가젯을 사용하기 위한 제약 조건을 의미한다.