
- 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
Return-Oriented Programming (ROP) 는 NX ( or DEP) 를 bypassing 하기 위한 기술이다. pwntools는 ROP Exploitation을 단순히 하기 위한 몇몇 기능들이 있지만, i386과 amd64 아키텍처에서만 작동한다.
ROP object를 생성하기 위해서는, ELF 파일을 넘겨주면 된다.
elf = ELF('/bin/sh')
rop = ROP(elf)
이것은 자동적으로 바이너리를 로드하고, 가장 단순한 가젯들을 추출한다. 예시로, 만약 rbx register를 로드하고 싶다면 다음과 같이 하면 된다. :
rop.rbx
# Gadget(0x5fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)
여기서 우리는 가젯의 주소, 디스어셈블 내용, 레지스터 값 등을 볼 수 있다.
elf.address = 0xff000000
rop = ROP(elf)
rop.rbx
# Gadget(0xff005fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)
ROP object 에 어떻게 레지스터를 로드할지 넘겨줄 수 있다.
rop.rbx
# Gadget(0xff005fd5, ['pop rbx', 'ret'], ['rbx'], 0x8)
만약 레지스터가 로드될 수 없다면, 반환값은 None이다.
rop.rcx
# None
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)}
Pwntools의 ROP Filter는 대부분의 사소한 가젯을 필터링하기 때문에, 필요한 가젯이 없다면 위의 ROP.Gadgets 를 활용해 확인해라. ( 오피셜에 딱 이것만 적혀있음;; )
ROP stack의 raw data를 추가하기 위해서는, ROP.raw() 를 호출하면 된다.
rop.raw(0xdeadbeef)
rop.raw(0xcafebabe)
rop.raw('asdf')
dump method를 통해 ROP Stack에 뭐가 있는지 확인할 수 있다.
print(rop.dump())
# 0x0000: 0xdeadbeef
# 0x0004: 0xcafebabe
# 0x0008: b'asdf' 'asdf'
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
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
만약 라이브러리가 호출해야 하는 함수를 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
한번에 하나의 주소 공간에서 하나 이상의 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
가끔은 쉘을 얻는 것은 꽤 쉬워질 수도 있다. 메모리 안 어딘가에서 첫번째 인자를 넘기기 위해, 우선 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