Pwntools 정리

Haruster·2022년 10월 15일

> Pwntools란?

  • Pwntools를 사용하기 전에는 socket() 함수로 Pwnable 문제를 풀거나 혹은 한 줄 익스플로잇 코드로 Pwnable문제를 풀었을 것입니다.

  • 이러한 문제점들을 개선하여 시스템 해킹 즉, Pwnable에 필요한 기능들을 탑재한 모듈이 바로 Pwntools라고 할 수 있다.

  • Pwntools를 사용하면 socket() 함수를 사용하는 것보다 더욱 간결한 코드로 Exploit Code를 작성할 수 있다.

// socket() 함수를 사용하는 경우

from socket import *

s = socket(AF_INET, SOCK_STREA)

s.connect('localhost', 4000)
// Pwntools를 이용하는 경우

from pwn import *

r = remote('localhost', 4000)
  • 이러한 간결한 사항 말고도 다른 편의 사항을 제공하기 때문에 Pwntools를 사용한다고 볼 수 있다.

> Pwntools를 사용하기 위해서는 from pwn import *을 하면 된다.

> Pwntools에서 자주 사용하는 문법

*접속 : process / remote / ssh

  • process() 함수는 로컬 바이너리에 대해서 익스플로잇을 실험해볼 때, 사용하는 함수이며,

  • remote() 함수는 원격 서버를 대상으로 실제 익스플로잇을 작동시킬 때 사용하는 함수이고,

  • ssh() 함수는 ssh를 통해서 접속하는 함수이다.

from pwn import *

p = process('./test')	# process(filename)
r = remote('snowcluster.com', 4000)		# remote(host, port)
s = ssh("fd". "snowcluster.com", 4000)	# ssh(user, port, password)순서

p.close()	# 접속 종료
r.close()	# 접속 종료
s.close()	# 접속 종료

*페이로드 보내기 - send

  • send() 관련 함수들을 보자면, 아래와 같다.
from pwn import *

 p = process('./hello')
 
 p.send('A')	# ./hello에 'A'입력
 
 p.sendline('A')  # ./hello에 'A' 입력 뒤에 "\n"(newline character)까지 입력
 
 p.sendafter('asdf', 'A')	# ./hello가 'asdf'를 출력할시에 'A'를 입력한다.
 
 p.sendlineafter('asdf', 'A')	# ./hello가 'asdf'를 출력할시에 'A' + "\n"을 입력한다.
 
  • 다만 send() 함수를 사용할 때는 꼭 어떠한 프로그램을 process나 remote했는지 붙여야 하기 때문에 해당 경우에는 p.를 붙여주여야 한다.

*데이터 받기 - recv

  • 프로그램이 출력하는 값을 봐야할 때, recv() 함수를 사용한다.

  • recv() 함수는 프로그램과의 interaction을 보기 위해 사용한다.

  • recv()와 recvn()의 차이점은 그냥 무조건 받는 것과 특정 바이트 만큼 받는다는 점에서 차이가 있다.
    -> 즉, p.recv()는 최대 n바이크 만큼의 크기를 받는 것이기 때문에, 1024바이트를 모두 다 채워 받지 못하더라도 에러를 발생시키지 않지만,
    -> p.recvn()의 경우 정확히 인자만큼의 데이터를 받지 못하면 계속 대기한다는 차이점이 있다.
    -> print p.recv(1024)의 방식을 가장 많이 사용하고 잇다.

from pwn import *

p = process("./hello")

data = p.recv(1024)		# p가 출력하는 데이터 중 최대 1024바이트의 데이터를 받아서 data에 저장한다.

data = p.recvline()		# p가 출력하는 데이터 중 개행문자를 만날 때까지를 data에 저장한다.

data = p.recvn(5)		# p가 출력하는 데이터 중 정확히 5바이트를 받아서 data에 저장한다.

print p.recvuntil('asdf')	# 'asdf'라는 문자열을 p가 출력할 때까지 받아서 출력한다.

print p.recvall()	# 연결이 끊어지거나 프로세스가 종료될 때까지 데이터를 받아서 data에 저장한다.

*라이브러리 / 바이너리 - ELF / run

  • Dynamically-Linked된 바이너리를 대상으로 문제를 풀 때 ELF를 통하여 정보를 가져올 수 있으며, 가장 대표적으로 GOT, PLT 등이 있는데, Pwntools의 API를 이용하면 이러한 값들을 쉽게 구할 수 있다.

  • 또한, run은 바이너리를 실행시킬 수 있다. (run은 거의 사용되지 않는다.)

from pwn import *

p = process('./hello')

e = ELF('./hello')	# e에 hello의 ELF를 불러온다. (PLT)
libc = ELF('./libc.so.6')	# libc에 libc.so.6 라이브러리를 불러온다. 
							# (보통 바이너리와 같이 제공된다.)

sh = p.run('/bin/sh')	# hello로 /bin/sh를 바이너리에 실행시킨다.
sh.sendline('whoami')	# 권한이 있다는 전제하에  "whoami"를 전송한다.

puts_plt = e.plt['puts']	# ELF ./hello 에서 puts()의 PLT 주소를 찾아서 puts_plt에 넣는다.

read_got = libc.got['read']	# libc base에서 read()의 GOT 주소를 찾아서 read_got에 넣는다.

*패킹 - p32, p64 / u32, u64

  • 대부분의 CPU는 little endian(리틀엔디안) 방식을 사용하며, 익스플로잇 값을 작성하다 보면 어떤 값을 Little endian 방식으로 packing(패킹)하거나(p32, p64), unpacking(언패킹)을 해야할 때가 있는데(u32, u64), 이를 쉽게 사용할 수 있도록 pwntools에서는 함수로 제공된다.
from pwn import *

data32 = 0x42424344
data64 = 0x4242434445464748

print(p32(data32))
print(p64(data64))

data32 = "ABCD"
data64 = "ABCDEFGH"

print(hex(u32(data32)))
print(hex(u64(data64)))
  • 위의 코드를 실행시킨 결과를 보자면, 아래와 같다.

*Debug - gdb.attach

  • gdb.attach는 자신이 예상한 페이로드가 터질 때 유용하게 사용되며, 페이로드를 진행하는 도중에 gdb를 실행시킬 수 있는 함수이다.
from pwn import *

p = process('./hello')

gdb.attach(p)	# gdb에 attach시킨다.
  • 위의 코드를 실행하면, 새 창에서 해당 프로그램이 gdb로 보여진다.
    -> 다만, 이것은 서버 접속으로는 불가능하며, process 즉, 파일 실행으로만 가능하다.

*쉘 접속 - interactive

  • 쉘을 획득한 경우나, 특정한 익스플로잇의 경우에는 직적 입력을 주면서 디버깅을 해줘야 하는 경우가 존재하는데, 이때 사용하는 함수가 바로 interactive이다.

  • interactive 함수는 익스플로잇 파일과 프로세스와의 연결을 stdin/stdout에서 process로 바꿔준다.

from pwn import *

p = process('./hello')

p.interactive()

*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

  • Pwntools에는 자주 사용되는 쉘코드들이 저장되어 있어서, 공격에 필요한 쉘코드를 쉽게 꺼내쓸 수 있게 해준다.

  • 매우 편리한 기능이지만, 정적으로 생성된 쉘코드는 쉘코드가 실행될 때의 메모리를 반영하지 못하며,

  • 또한, 프로그램에 따라 입력할 수 있는 쉘코드의 길이나, 구성 가능한 문자의 종류에 제약이 있을 수 있는데, 이런 조건들을 반영하기 어렵다.

  • 즉, 제약 조건이 존재하는 상황에서는 직접 쉘코드를 작성하는 것이 좋다.

# shellcraft 예제

from pwn import *

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

code = shellcraft.sh()		# 쉘을 실행하는 쉘코드

print(code)

*asm

  • Pwntools는 어셈블 기능을 제공하며, 해당 기능도 대상 아키텍처가 중요하므로, 아키텍처를 미리 지정해야 한다.
from pwn import *

context.arch = "amd64"		# 익스플로잇 대상 아키텍처 x86-64

code = shellcraft.sh()		# 쉘을 실행하는 쉘코드

code = asm(code)	# 쉘코드를 기계어로 어셈블한다.

print(code)		

profile
다양한 스택을 공부하는 정보보안 전문가 지망생입니다. (Pwnable, Reversing, Webhacking, ...)

0개의 댓글