이제 마지막 문제인 20번째 문제이다.
전에 풀어보려다가 실패를 해서...
bof 공부를 다시 하고
마지막으로 풀어보았다
20번 문제
문제 :
Download 링크를 누르면, 'reverseme.zip'이라는 파일이 받아지고,
압축을 풀면, reverseme라는 파일이 나온다.
해당 파일을 [ Exeinfo PE ] 와 [ HxD ] 에서 열면 아래와 같은 정보가 나온다.
ELF 파일이다.
윈도우에 EXE 파일이 있듯이, ELF 파일은 리눅스에서 돌아가는 파일이다. 그래서 Ollydbg, 32xdbg 같은 디버깅 프로그램으로는 열리지 않는다.
리눅스에서 reverseme 파일을 실행하면 아무런 반응이 없다.
gdb 와 IDA 프로그램으로 해당 파일을 분석해 보았다.
main 함수 구조
auth 함수 구조
correct 함수 구조
IDA freeware 버전이어서 elf 파일은 디컴파일은 불가하고,
어셈블리어로는 한계가 있어스 'Ghidra(기드라)' 라는 elf도 디컴파일 해주는 프로그램이 있어서 이용해 보았다.
https://ghidra-sre.org/
참고 : 문제를 풀면서 알아야 하는 함수
strmcp(string1, string2) : strcmp() 함수는 string1 및 string2를 비교합니다.
memset(void* ptr, int value, size_t num) : memset함수는 어떤 메모리의 시작점부터 연속된 범위를 어떤 값으로(바이트 단위) 모두 지정하고 싶을 때 사용하는 함수이다.
memcpy(데이터 복사될 곳 가르키는 포인터, 복사할 데이터가 있는 위치를 가리키는 포인터, 복사할 바이트 수) : 메모리의 값을 복사하는 기능을 하는 함수
main 함수
조건문 안에
strcmp(*param_2,"./suninatas")
을 통해 파일명을 확인한다는 것을 알 수 있다.
strcmp 함수로 두 문자열을 비교하는 함수인데 *param_2는 argv 인자
※ 참고 : 2023.01.31 - [시스템/시스템 해킹] - main 함수 인자
위 조건을 만족시키기 위해서 'reverseme'라는 파일 이름을 'suninatas'로 바꿔서 실행해 보았다.
이제, 'Authenticate : '라는 글자가 나오고 입력을 받기 시작한다.
입력값에 1을 넣고 [enter]를 누르니까 hash값이 나온다.
auth함수로 넘어갈 때 사용되는 인자 local_lc을 확인해 보면
__isoc99_scanf(&DAT_080d9dd9,local_3a);
scanf를 통해서 입력받은 값으로,
local_1c = Base64Decode(local_3a,&local_40);
Base64Decode로 디코딩한 것이 auth함수의 인자이다.
→ 이전에 base64로 인코딩이 되어 있어야 함!!!
이후에 auth 함수의 결과 값이 1이 되면 correct() 함수가 실행된다.
auth 함수
memcpy(auStack_c,input,param_1);
local_10 = (char *)calc_md5(local_18,12);
printf("hash : %s\n",local_10);
iVar1 = strcmp("f87cd601aa7fedca99018a8be88eda34",local_10)
return iVar1==0;
입력한 값을 calc_md5 함수로 해시한 결과값을 strcmp 함수로 "f87cd601aa7fedca99018a8be88eda34"라는 값과 비교한다. 해당 해시값이 일치하면 '0' , 일치하지 않으면 '1'이 된다.
즉, 일치해야 함수가 결과가 return이 되면서 correct 함수가 실행된다.
하지만, 입력값을 동일하게 넣어도 해시 값이 다르게 출력되기 때문에
이 부분을 뛰어 넘겨야 한다.
또한,
undefined local_18 [8];
undefined auStack_c [8];
memcpy(auStack_c,input,param_1);
입력값(auStack_c)은 기본 변수 크기가 8bytes이지만,
길이 제한이 없어 취약한 함수인 memcpy에서 8bytes 이상 입력이 가능하나,
local_10 = (char *)calc_md5(local_18,12);
calc_md5 함수에 의해 입력값이 최대 12자리(12bytes)까지만 가능하다.
그 이상 입력하게 되면 segmentation fault가 발생하기 때문이다.
이부분을 조작해서 hash값 비교부분을 넘겨야 할거 같다.
correct 함수
코드를 확인해 보면,
input이 0xdeadbeef일때 "Congratulation! ~"하면서 성공하게 된다.
이때 input._0_4_ 의 뒤에 _0_4_에 대해 찾아보았는데 input 에서 앞의 4bytes에 해당한다고 한다.
The first digit (0) being the offset of the byte to start from, and the second digit (4) being the number of bytes being accessed.
아마 나머지 뒤 부분은 어떻든지 상관없는 거 같다.
이제 앞에서 분석한 것을 정리해 보면,
1. 파일명을 suninatas로 바꿔야 한다.
2. Buffer overflow 공격을 이용해야 한다.
- 취약한 함수 : memcpy (문자열 길이 제한 없이 복사한다.)
3. correct 함수는 앞의 input 값의 4bytes가 0xdeadbeaf가 돼야 한다.
하지만, 여기서 제한사항이
1) input의 변수는 기본 크기가 8bytes이나 최대 12bytes까지 입력가능하다.
→ 4bytes overflow가능하다.
→ 4bytes면 스택프레임에서 ret는 변조가 불가능하고 SFP까지만 변조가 가능하다.
→ 버퍼오버플로우 공격을 이용하는 것으로 에필로그(leave-ret)를 조작해 EIP로 원하는 부분을 실행되도록 해야 한다.
(FPO(Frame Pointer Overwrite)공격)
함수 에필로그는 leave와 ret로 구성되어 있다.
leave
mov esp ebp
pop ebp
ret
pop eip
jmp eip
위와 같은 auth 함수의 stack 구조에서
input에 12bytes를 넣어야 하는데 앞의 4bytes는 correct 함수 만족시키기 위해 0xdeadbeef가 들어가야 하고,
sfp만 조작이 가능하고 auth 함수에서 hash값을 만족시킬 수 없기에
sfp 경로를 input으로 조작을 하고, input안에 correct 함수 주소를 넣는다.
두번의 에필로그로 correct함수가 실행되도록한다.
정리를 하면 아래와 같다.
auth함수의 에필로그에서
[leave]
move esp ebp 명령어에 의해 둘 다 auth SFP를 가리킨다.
그다음,
pop ebp 명령어에 인해 input주소가 ebp로 들어가고 esp는 esp+4 되어 auth RET를 가리키게 된다.
[ret] 여기는 그대로 진행된다.
pop eip 명령어에 의해 esp에 들어 있는 값인 auth RET가 eip에 들어가고, esp는 esp+4가 된다.
그다음,
jmp eip로 인해 다음 명령어를 실행한다.
이후 main함수의 에필로그가 실행되면
[leave]
push esp ebp 명령어에 의해서 둘다 input함수를 가리키게 된다.
pop ebp에 의해 ebp 값은 0xdeadbeef가 되고 esp는 esp+4가 되어 correct함수를 가리키게 된다.
[ret]
pop eip 명령어에 의해서 esp에 들어 있는 값인 correct함수 주소가 eip에 들어간다, esp는 esp+4가 된다.
jmp eip로 인해서 eip에 들어있는 correct주소로 해당 함수가 실행된다.
correct 함수에서는 입력값(input)의 첫 4bytes가 0xdeadbeef인지 확인한다.
4. 입력값은 base64로 인코딩 필요
위의 모든 걸 반영해 pwntools을 이용해서 코드를 작성해 보았다.
base64로 인코딩 (0xdeadbeef+correct주소+input주소)
from pwn import *
import base64
correct_input=p32(0xdeadbeef)
correct_addr=p32(0x0804925f)
input_addr=p32(0x0811c9ec)
payload=correct_input+correct_addr+input_addr
payload=base64.b64encode(payload)
p=process('./suninatas')
p.sendline(payload)
print(payload)
print(p.recvall())
이때 입력값으로 사용되었던 payload가 20번 문제의 답이 된다!
성공!
이 문제를 풀면서 pwnable 쪽이 많이 부족하다는 걸 느꼈다.
전에 배웠던 책이랑 인터넷 검색하면서 다시 기억을 되살려보았고
아직도 모르고 모르는 게 많이 있어서 계속 보완하면서 공부해야겠다.
[ 참고 ]
suninatas 20번 문제풀이
https://hackchang.tistory.com/89
https://je0n-je.tistory.com/142
https://m.blog.naver.com/lstarrlodyl/221729885577
pwnable.kr simple login write-up
https://myste.tistory.com/34
https://snwo.tistory.com/107
https://koyo.kr/post/pwnable-kr-simple-login/
문제 풀이에 오류가 있으면 반영해서 수정하겠습니다!!
문제풀이에 오류가 있어서 수정(23.2.9.)