Pwnable & Python

python

자주 사용하는 문법

byte string

b'Ascii' 와 같은 형식을 byte string이라고 한다. 문자는 그대로 표현할 수 있고, 바이너리 값은 b'\x12\x34\x56\x78' 형식으로 \xff 까지 표현할 수 있다.

# 바이트 스트링 뒤집기
byte_str = b'\x12\x34\x56\x78'
r_str = byte_str[::-1]  # 슬라이싱 구문. 처음부터 끝까지 역순으로 슬라이싱
print(rstr)             # 출력값: b'xV4\x12' == b'\x78\x56\x34\x12'

str().encode()

문자열을 byte string으로 변환하는 방법

>>> str(0x1234).encode()
b'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)
import struct

hex_value = 0x12345678

byte_str = struct.pack('<I', hex_value )
print(byte_str)    # 출력값: b'xV4\x12'

byte_str = struct.pack('>I', hex_value )
print(byte_str)    # 출력값: b'\x124Vx'

byte_str = struct.pack('!I', hex_value )     # 네트워크 (=빅엔디안)
print(byte_str)    # 출력값: b'\x124Vx'

byte_str = struct.pack('@I', hex_value )     # default 값
print(byte_str)    # 출력값: 네이티브 엔디안 (시스템의 엔디안을 따라감)

str = "hello"
print(struct.pack('p', str))   # 출력값: b'\x05hello'

# 여러 데이터를 한꺼번에 처리할수도 있으며, unpack은 항상 튜플형태로 리턴된다.
print(struct.unpack('I', byte_str))   # 출력값: 0x12345678

m_pack = struct.pack('II', 0x11223344, 0x55667788)   # 각각 패킹해서 바이트스트링으로 변환
print(m_pack)     # 출력값: b'D3"\x11\x88wfU'
m_unpack = struct.pack('II', m_pack)
print(m_unpack)   # 출력값: (0x11223344, 0x55667788) 

문자열과 숫자 변환

int를 사용해서 문자열을 정수 형태로 변환할 수 있다.
정수를 문자열로 변환하려면 bin, oct, hex 함수를 사용하면 되고, 문자열로 변하며 접미사가 붙는다.

# 문자열을 16진수로 읽어서 숫자로 변환
print(int("012f3d5e", 16))   # 출력값: 19985886
# 0x 같은 접미사가 포함된 경우에도 변환할 수 있음
print(int("0x012f3d5e", 16)) 

print(hex(0x12345678))       # 출력값: 0x12345678
print(bin(0x12345678))       # 출력값: 0b00010010...01111000

문자열을 바이트 스트링으로 변환

# 이 값은 hex값으로 보이지만, 사실 아스키로 이뤄진 b'\x30\x30...\x31\x63' 이다.
byte_str = b'00a9c71c'

# hex 문자열로 변환 (byte str -> string)
hex_str = byte_str.decode('utf-8')      # 00a9c71c

# hex 문자열을 byte string 으로 변환
real_byte = bytes.fromhex(hex_str)      # b'\x00\xa9\xc7\x1c'

# hex 문자열을 숫자로 변환
real_int = int(hex_str)

pwntools

설치

https://github.com/Gallopsled/pwntools

$ apt-get update
$ apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
$ python3 -m pip install --upgrade pip
$ python3 -m pip install --upgrade pwntools

사용법

process & remote

from pwn import *
p = process('./test')  # 로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com', 31337)  # 'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행
p = process('./rop', env= {"LD_PRELOAD" : "./libc.so.6"})

마지막 라인은 환경변수 LD_PRELOAD에 현재 경로의 libc.so.6를 지정한다는 의미인데, 의존관계들이 있기 때문에 해당 라이브러리 버전지원이 가능한 운영체제에서 사용해야 에러 없이 실행된다.

gdb 붙이기

gdb를 연결하고 pause를 실행시켜두면, 자동으로 gdb가 실행되며 attach 된다.
gdb도 멈춘 상태이고, python 실행도 멈춰있기 때문에 gdb에서 continue하고 pause를 풀어주면 된다.

gdb.attach(p, '''
b main
b *main+91
''')
pause()

send

from pwn import *
p = process('./test')

p.send(b'A')  # ./test에 b'A'를 입력
p.sendline(b'A') # ./test에 b'A' + b'\n'을 입력
p.sendafter(b'hello', b'A')  # ./test가 b'hello'를 출력하면, b'A'를 입력
p.sendlineafter(b'hello', b'A')  # ./test가 b'hello'를 출력하면, b'A' + b'\n'을 입력

recv

recv(n)은 최대 n byte를 받고 종료되지만, recvn(n)은 n byte 받을 때 까지 블럭이 된다.

from pwn import *
p = process('./test')

data = p.recv(1024)  # p가 출력하는 데이터를 최대 1024바이트까지 받아서 data에 저장
data = p.recvline()  # p가 출력하는 데이터를 개행문자를 만날 때까지 받아서 data에 저장
data = p.recvn(5)  # p가 출력하는 데이터를 5바이트만 받아서 data에 저장
data = p.recvuntil(b'hello')  # p가 b'hello'를 출력할 때까지 데이터를 수신하여 data에 저장
data = p.recvall()  # p가 출력하는 데이터를 프로세스가 종료될 때까지 받아서 data에 저장

packing & unpacking

#!/usr/bin/env python3
# Name: pup.py

from pwn import *

s32 = 0x41424344
s64 = 0x4142434445464748

print(p32(s32))
print(p64(s64))

s32 = b"ABCD"
s64 = b"ABCDEFGH"

print(hex(u32(s32)))
print(hex(u64(s64)))

import struct
print(p32(0xdeadbeef) == struct.pack('I', 0xdeadbeef))
print(u32(b'abcd') == struct.unpack('I', b'abcd')[0])
$ python3 pup.py
b'DCBA'
b'HGFEDCBA'
0x44434241
0x4847464544434241
True
True

interactive

직접 입출력을 키보드로 주고받고 싶을때 사용.

from pwn import *
p = process('./test')
p.interactive()

인터랙티브모드가 실행될 수 없을 때 (쉘코드가 정상실행되지 않은경우) 이 명령을 사용하면 쉘을 얻은것처럼 보이지만, EOF가 입력됐다고 표시되면서 종료된다.

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

ELF

ELF 헤더를 파싱해준다

from pwn import *
e = ELF('./test')
puts_plt = e.plt['puts'] # ./test에서 puts()의 PLT주소를 찾아서 puts_plt에 저장
read_got = e.got['read'] # ./test에서 read()의 GOT주소를 찾아서 read_got에 저장

get_shell = e.symbols['get_shell']  # ./test에서 get_shell 함수의 주소를 찾아준다

bin_sh = list(e.search(b'/bin/sh'))[0]   # ./test에서 /bin/sh 문자열을 검색해서 처음 나오는 문자열 오프셋을 저장한다. 

context.log

익스플로잇의 디버깅을 위해 로그레벨을 지정할 수 있다.

from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info'  # 비교적 중요한 정보들만 출력

context.arch

쉘코드 생성이나 어셈블, 디스어셈블 기능을 이용할 때 아키텍쳐에 맞춰야 하기 때문에 아키텍쳐를 지정할 수 있다.

from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386"  # x86 아키텍처
context.arch = "arm"   # arm 아키텍처

shellcraft

자주 사용하는 쉘코드들을 바로 꺼내올 수 있다.

#!/usr/bin/env python3
# Name: shellcraft.py

from pwn import *
context.arch = 'amd64' # 대상 아키텍처 x86-64

code = shellcraft.sh() # 셸을 실행하는 셸 코드 
print(code)
$ python3 shellcraft.py
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00' */
    push 0x68
    mov rax, 0x732f2f2f6e69622f
    ...
    syscall

asm

어셈블이 가능해서 어셈블리 코드를 기계어로 어셈블할 수 있다. 역시 아키텍쳐가 중요하다.

#!/usr/bin/env python3
# Name: asm.py

from pwn import *
context.arch = 'amd64' # 익스플로잇 대상 아키텍처 'x86-64'

code = shellcraft.sh() # 셸을 실행하는 셸 코드
code = asm(code)       # 셸 코드를 기계어로 어셈블
print(code)
$ python3 asm.py
b'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에 패딩을 추가할 때 사용하면 좋다.

sh = asm(shellcraft.sh())
payload = sh.ljust(bufsize, b'A') + p64(cnry) + b'B'*0x8 + p64(buf)

쉘코드를 왼쪽정렬(오른쪽에 패딩)하는데, 쉘코드 이후 bufsize 크기까지 패딩 ‘A’를 채워넣는다는 의미이다.

기타 도구

strings

파일에서 문자열을 검색하는 도구. tx를 붙여서 오프셋을 알아올 수 있다.

$ strings -tx libc-2.27.so | grep "/bin/sh"
 1b3e1a /bin/sh

file

checksec

ROPGodget

pwntool을 설치하면 같이 설치되는 도구이며, 바이너리에서 리턴 가젯을 검색해주는 도구이다.

$ python3 -m pip install ROPgadget --user

$ ROPgadget -v
Version:        ROPgadget v7.3
Author:         Jonathan Salwan
Author page:    https://twitter.com/JonathanSalwan
Project page:   http://shell-storm.org/project/ROPgadget/

$ ROPgadget --binary ./rtl --re "pop rdi"
Gadgets information
============================================================
0x0000000000400853 : pop rdi ; ret

프로그램이 공유 라이브러리를 사용한다면 라이브러리에서 gadget을 가져와도 된다.

# ldd로 프로그램에서 사용하고 있는 라이브러리를 확인
$ ldd ./rop
        linux-vdso.so.1 (0x00007ffc0b9ce000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f941f53f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f941f73d000)

# 심볼릭 링크된 실제 파일 경로를 찾아옴
$ readlink -f /lib/x86_64-linux-gnu/libc.so.6
/usr/lib/x86_64-linux-gnu/libc-2.31.so

# 라이브러리를 실행해보면 버전을 알 수 있음 
$ /usr/lib/x86_64-linux-gnu/libc-2.31.so
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.16) stable release version 2.31.

# --binary로 바이너리 경로를 지정하고, --re로 검색할 가젯의 패턴을 정규표현식으로 전달한다.
$ ROPgadget --binary /usr/lib/x86_64-linux-gnu/libc-2.31.so --re "pop rdi"
... gadget list ...

one_gadget

libc에서 단일 가젯으로 쉘을 획득할 수 있는 강력한 가젯을 찾아오는 도구이다. libc 버전이 올라갈수록 찾거나 사용하기 어려워진다.
constraints가 가젯을 사용하기 위한 제약 조건을 의미한다.

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

Comments

ESC
Type to search...