Pwnable & Python

Pwnable & Python

2024년 5월 29일
python

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가 입력됐다고 표시되면서 종료된다.

3f05d65f-018a-4b93-8927-480032482f3b

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가 가젯을 사용하기 위한 제약 조건을 의미한다.

be757876-86b8-4eec-9758-31f93c849e15

comments powered by Disqus