Pwntools Tutorial - ROP

모씨김(mossikim)·2025년 8월 13일

Pwntools

목록 보기
7/8
post-thumbnail

ROP


  • Background
  • Loading an ELF
    • Fixing Addresses
  • Inspecting Gadgets
  • Viewing All Gadgets
    • Really viewing ALL Gadgets
  • Adding Raw Data
  • Dumping ROP Stacks
  • Extracting Raw Bytes
  • Calling Functions Magically
    • Calling Functions by Name
  • Multiple ELFs
  • Getting a Shell


Background

Return-Oriented Programming (ROP) 는 NX ( or DEP) 를 bypassing 하기 위한 기술이다. pwntools는 ROP Exploitation을 단순히 하기 위한 몇몇 기능들이 있지만, i386과 amd64 아키텍처에서만 작동한다.


Loading an ELF

ROP object를 생성하기 위해서는, ELF 파일을 넘겨주면 된다.

elf = ELF('/bin/sh')
rop = ROP(elf)

이것은 자동적으로 바이너리를 로드하고, 가장 단순한 가젯들을 추출한다. 예시로, 만약 rbx register를 로드하고 싶다면 다음과 같이 하면 된다. :

rop.rbx
# Gadget(0x5fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)

Fixing Addresses

여기서 우리는 가젯의 주소, 디스어셈블 내용, 레지스터 값 등을 볼 수 있다.

elf.address = 0xff000000
rop = ROP(elf)
rop.rbx
# Gadget(0xff005fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)

Inspecting Gadgets

ROP object 에 어떻게 레지스터를 로드할지 넘겨줄 수 있다.

rop.rbx
# Gadget(0xff005fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)

만약 레지스터가 로드될 수 없다면, 반환값은 None이다.

rop.rcx
# None

Viewing All Gadgets

pwntools 는 대부분의 사소한 가젯들을 무시하지만, ROP.gadgets 프로퍼티를 확인함으로써 무엇이 로드되었는지 볼 수 있다.

rop.gadgets
# {4278225723: Gadget(0xff008b3b, ['add esp, 0x10', 'pop rbx', 'pop rbp', 'pop r12', 'ret'], ['rbx', 'rbp', 'r12'], 0x20),
#  4278278088: Gadget(0xff0157c8, ['add esp, 0x130', 'pop rbp', 'ret'], ['rbp'], 0x138),
#  4278284789: Gadget(0xff0171f5, ['add esp, 0x138', 'pop rbx', 'pop rbp', 'ret'], ['rbx', 'rbp'], 0x144),
#  4278272966: Gadget(0xff0143c6, ['add esp, 0x18', 'ret'], [], 0x1c),
#  4278239612: Gadget(0xff00c17c, ['add esp, 0x20', 'pop rbx', 'pop rbp', 'pop r12', 'ret'], ['rbx', 'rbp', 'r12'], 0x30),
#  4278259611: Gadget(0xff010f9b, ['add esp, 0x28', 'pop rbp', 'pop r12', 'ret'], ['rbp', 'r12'], 0x34),
# ...
#  4278216828: Gadget(0xff00687c, ['pop rsp', 'pop r13', 'ret'], ['rsp', 'r13'], 0xc),
#  4278214225: Gadget(0xff005e51, ['pop rsp', 'ret'], ['rsp'], 0x8),
#  4278210586: Gadget(0xff00501a, ['ret'], [], 0x4)}

Really Viewing ALL Gadgets

Pwntools의 ROP Filter는 대부분의 사소한 가젯을 필터링하기 때문에, 필요한 가젯이 없다면 위의 ROP.Gadgets 를 활용해 확인해라. ( 오피셜에 딱 이것만 적혀있음;; )


Adding Raw Data

ROP stack의 raw data를 추가하기 위해서는, ROP.raw() 를 호출하면 된다.

rop.raw(0xdeadbeef)
rop.raw(0xcafebabe)
rop.raw('asdf')

Dumping ROP Stacks

dump method를 통해 ROP Stack에 뭐가 있는지 확인할 수 있다.

print(rop.dump())
# 0x0000:       0xdeadbeef
# 0x0004:       0xcafebabe
# 0x0008:          b'asdf' 'asdf'

Extracting the Raw Bytes

bytes method를 통해 ROP stack의 raw bytes를 획득할 수 있다.

print(hexdump(bytes(rop)))
# 00000000  ef be ad de  be ba fe ca  61 73 64 66               │····│····│asdf│
# 0000000c

Calling Functions Magically

Pwntools의 ROP 도구의 장점은 magic anccessors 또는 ROP.call() 를 통해 함수를 호출하는 것이다.

elf = ELF('/bin/sh')
rop = ROP(elf)
rop.call(0xdeadbeef, [0, 1])
print(rop.dump())
# 0x0000:       0xdeadbeef 0xdeadbeef(0, 1, 2, 3)
# 0x0004:          b'baaa' <return address>
# 0x0008:              0x0 arg0
# 0x000c:              0x1 arg1

위 예제는 32bit를 사용한다. 우리는 또한 64bit에서 ROP를 할 수 있지만, 우선 context.arch를 적절히 설정해야한다. context.binary를 통해 자동으로 세팅할 수 있다.

context.binary = elf = ELF('/bin/sh')
rop = ROP(elf)
rop.call(0xdeadbeef, [0, 1])
print(rop.dump())
# 0x0000:           0x61aa pop rdi; ret
# 0x0008:              0x0 [arg0] rdi = 0
# 0x0010:           0x5f73 pop rsi; ret
# 0x0018:              0x1 [arg1] rsi = 1
# 0x0020:       0xdeadbeef

Calling Functions by Name

만약 라이브러리가 호출해야 하는 함수를 GOT/PLT에 보유하고 있다면, 또는 바이너리의 심볼이 존재한다면, 너는 함수의 이름을 통해 직접적으로 호출할 수 있다.

context.binary = elf = ELF('/bin/sh')
rop = ROP(elf)
rop.execve(0xdeadbeef)
print(rop.dump())
# 0x0000:           0x61aa pop rdi; ret
# 0x0008:       0xdeadbeef [arg0] rdi = 3735928559
# 0x0010:           0x5824 execve

Multiple ELFs

한번에 하나의 주소 공간에서 하나 이상의 ELF를 사용할 수 있다.

다음은 /bin/sh과 그것의 libc를 동시에 사용하는 예시이다. :

context.binary = elf = ELF('/bin/sh')
libc = elf.libc

elf.address = 0xAA000000
libc.address = 0xBB000000

rop.rax
# Gadget(0xaa00eb87, ['pop rax', 'ret'], ['rax'], 0x10)
rop.rbx
# Gadget(0xaa005fd5, ['pop rbx', 'ret'], ['rbx'], 0x10)
rop.rcx
# Gadget(0xbb09f822, ['pop rcx', 'ret'], ['rcx'], 0x10)
rop.rdx
# Gadget(0xbb117960, ['pop rdx', 'add rsp, 0x38', 'ret'], ['rdx'], 0x48)

rax와 rbx는 메인 바이너리 안에 있는 반면에, rcx와 rdx는 libc에 있는 것을 알 수 있다.

더 복잡한 예시는 다음과 같다 :

rop.memcpy(0xaaaaaaaa, 0xbbbbbbbb, 0xcccccccc)
print(rop.dump())
# 0x0000:       0xbb11c1e1 pop rdx; pop r12; ret
# 0x0008:       0xcccccccc [arg2] rdx = 3435973836
# 0x0010:      b'eaaafaaa' <pad r12>
# 0x0018:       0xaa0061aa pop rdi; ret
# 0x0020:       0xaaaaaaaa [arg0] rdi = 2863311530
# 0x0028:       0xaa005f73 pop rsi; ret
# 0x0030:       0xbbbbbbbb [arg1] rsi = 3149642683
# 0x0038:       0xaa0058a4 memcpy

Getting a shell

가끔은 쉘을 얻는 것은 꽤 쉬워질 수도 있다. 메모리 안 어딘가에서 첫번째 인자를 넘기기 위해, 우선 execve를 직접 호출하고, “/bin/sh\x00”의 instance를 찾아라.

context.binary = elf = ELF('/bin/sh')
libc = elf.libc

elf.address = 0xAA000000
libc.address = 0xBB000000

rop = ROP([elf, libc])

binsh = next(libc.search(b"/bin/sh\x00"))
rop.execve(binsh, 0, 0)

ROP stack을 출력해라

print(rop.dump())
# 0x0000:       0xbb11c1e1 pop rdx; pop r12; ret
# 0x0008:              0x0 [arg2] rdx = 0
# 0x0010:      b'eaaafaaa' <pad r12>
# 0x0018:       0xaa0061aa pop rdi; ret
# 0x0020:       0xbb1b75aa [arg0] rdi = 3139138986
# 0x0028:       0xaa005f73 pop rsi; ret
# 0x0030:              0x0 [arg1] rsi = 0
# 0x0038:       0xaa005824 execve

ROP에서 raw bytes를 추출해라

print(hexdump(bytes(rop)))
# 00000000  e1 c1 11 bb  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
# 00000010  65 61 61 61  66 61 61 61  aa 61 00 aa  00 00 00 00  │eaaa│faaa│·a··│····│
# 00000020  aa 75 1b bb  00 00 00 00  73 5f 00 aa  00 00 00 00  │·u··│····│s_··│····│
# 00000030  00 00 00 00  00 00 00 00  24 58 00 aa  00 00 00 00  │····│····│$X··│····│
# 00000040

Pwntools - official tutorial
Full Reference

0개의 댓글