-
TUCTF2018 lisa WriteupSystem/CTF 2019. 1. 14. 02:37
[ Binary Info ]
32Bit 바이너리이며, shared object으로 PIE가 적용되어 있다.
PIE와 NX를 제외한 제약사항이 없기 때문에
개인적으로 PIE를 처음 접한다면 입문용 문제로 괜찮다고 생각한다.
( 로컬서버에서는 같은 디렉터리에 password와 flag파일을 만들어줘야 합니다. )
문제의 난이도는 PIE를 제대로 이해한 사람에게는 쉬웠을 것이고
나처럼 PIE가 익숙치 않은 사람에게는 어렵게 느껴졌을 것이다.
따라서, 무작정 풀다가 어려움을 느끼고 라이트업을 보러오신 분들은 [PIE 보호기법 정리] 를 먼저 보시고
다시 한번 풀어보는 것을 추천드리고 싶다!
[ Binary 실행 ]
바이너리를 실행하면 주소 하나를 릭해준다.
그리고 두 번의 입력을 받고 프로그램이 종료된다.
대충 이렇게 흐름을 눈에 익혀두고 IDA로 정적분석을 해보도록 한다.
[ 정적분석#1 ]
릭 해주는 주소의 정체는 동적할당된 pass변수의 주소였고,
pass에는 같은 디렉터리에 있는 password내용이 저장되어 있다.
그리고 첫번째 입력 값은 s에 저장된 후 inp포인터 변수에 s의 주소값이 저장된다.
s의 크기는 [EBP-0x34](52Byte)이고, 입력 크기는 0x30(48Byte)이므로 안전한 상태이다.
[ 정적분석#2 ]
check Pass( )에서는 우리가 입력한 값을 pass와 비교해서 분기시키는 함수이다.
이때 fail( )로 분기될 때, buf변수의 주소값을 인자로 넘겨주는 것을 확인할 수 있는데
buf의 크기가 24Byte인 것을 알아두도록 하고, lisa( )부터 분석해보자
[ 정적분석#3 ]
모나리자당중간에 flag값을 출력해준다.
패드워드를 맞추면 flag를 출력해주는 문제인 듯하다.
[ 정적분석#4 ]
패스워드를 맞추지 못하면 fail( )로 분기되고 두번째 입력을 받는다.
이때, buf에 29Byte를 저장하면서 5Byte BoF가 발생하게 된다.
32Bit니까 SFP와 RET 1Byte를 핸들링할 수 있게 된다.
또한, buf는 이전 함수인 checkPass( )의 변수인 점을 기억해야 한다.
우리가 핸들링할 수 있는 덮어쓸 수 있는 곳은 fail( )가 아닌 checkPass( )인 것 이다.
이 점을 포인트로 잡고 익스플로잇 단계로 넘어가도록 한다.
[ Exploit ]
우선 문제에서 우리에게 주는 주소값을 활용을 하는 선에서 생각을 해보자
웬만한 상황 아니고서는 저런 값을 낚시성으로 던져주지는 않으니.
제일 먼저 떠오른 방법은 puts( )를 이용해서 password값을 알아내보자! 였다.
하지만 이 방법은 불가능했다.
우리가 핸들링할 수 있는 RET는 1Byte뿐이고, FakeEBP를 사용하기에는 PIE로 인해서 주소값이
계속해서 바뀌기 때문이다.
GDB로 분석해보면서 자세히 알아보도록 하자
[ GDB#1 ]
우선 checkPass( )의 RET 부분에 BP를 걸고 실행을 해본다.
[ GDB#2 ]
확인해보면 EIP에 [0x56555d22]가 저장되어 있고, 해당 주소는 main( )에 위치한 주소이다.
[ GDB#3 ]
main( )에서 call 명령어로 checkPass( )를 호출했으니 함수가 종료되면
call 인스트럭션 다음 명령어 주소인 [0x56555d22]로 복귀하는 하는 흐름인 것이다.
여기서 하나 알고가야할 것이 있다.
PIE의 주소랜덤화는 Base주소에 offset을 더하는 방식으로 이루어진다.
따라서, 변하는 것은 Base주소이고 offset값은 고정된 값이다.
[ PIE 주소 offset ]
위와 같이 Code영역의 주소값이 상대적인 주소(offset)로 바뀌게 되는데
이 값이 매 번 바뀌는 Base주소에 더해져서 Code의 위치를 찾아가게 되는 것이다.
따라서, 1Byte의 RET를 핸들링할 수 있는 우리는 [0xdxx] 범위안의 Code를 재사용할 수 있게 된다.
범위안에는 read( )가 존재하며, 이 함수를 이용해보도록 한다.
근데 한가지 문제점이 생긴다.
read( )를 재사용하려면 인자를 맞쳐줘야 하는데, 32Bit니까 [ESP],[ESP+4],[ESP+8]에 이쁘게 위치해 있어야 한다.
[ GDB#4 ]
자, 빨간박스 부분을 보면 checkPass( )를 호출하기 전에 ESP에 12Byte만큼 더해준다.
read( )를 호출하기 전에 push로 쌓인 스택을 정리해주는 모습이다. (4Byte*3)
사실 저부분 말고도 모든 함수를 실행한 뒤, 스택을 항상 정리해주고 있었다.
이 말은 즉, 우리가 첫 번째 입력했던 값이 항상 [ESP],[ESP+4],[ESP+8]순으로 정렬되고 있음을 의미한다.
따라서, 첫번째 입력때 인자구성을 [fd/pass의 주소값/size]으로 맞춰서 pass값을 초기화 시키면
checkPass( )에서 fail( )가 아닌 lisa( )가 실행될 것이다.
[ PAYLOAD ]
123456789101112131415161718from pwn import *p = process("./lisa")print p.recvuntil("0x")leak = int(p.recv(8),16)print "leak: "+hex(leak)#read() Arg Settingpayload = p32(0) # Arg0payload += p32(leak) #Arg1payload += p32(1) #Arg2print p.recvuntil("...\n")p.sendline(payload)print p.recvuntil("Ugh! You kiss your mother with that mouth?\n")p.send("\x15"*29)p.send("\x00")p.interactive()cs 'System > CTF' 카테고리의 다른 글
[2019picoCTF] droids0 (0) 2019.10.12 [64bit] FSB Exploit Generator (0) 2019.07.10 Meepwn 2018 White snow, Black shadow Writeup (3) 2019.03.02 2018 CCE Qual Writeup - note [pwn] (0) 2018.10.02 CTF Tools 정리 (0) 2018.09.30 댓글