c# 溢出抛异常_深入分析GNU Readline中基于堆的缓冲区溢出漏洞

f1a051d27d709f54c963565fdad5714e.png

在上一篇文章中,我们讨论了fuzzer是如何确定崩溃的唯一性的。在本文中,我们将为读者介绍如何通过手动方式对崩溃进行分类,并确定导致漏洞的根本原因。为此,我们将以GNU readline 8.1 rc2中发现的一个基于堆的缓冲区溢出漏洞为例进行演示,另外,该漏洞已经在最新的版本中得到了相应的修复。同时,我们将使用GDB和rr进行时间旅行式调试(time – travel debugging),以确定该漏洞的根本原因。

对于GNU readline的源代码,读者可以从这里下载。

下载之后,请利用如下所示的命令进行编译:

$ ./configure --enable-shared=nomake all

我对其中一个示例进行了相应的修改,以使其更加简单明了。

$ cat examples/rlbasic.c#include #include #include #include #if defined (READLINE_LIBRARY)#include "readline.h"#include "history.h"#else#include #include #endifintmain (int c, char **v){char * buf = readline("");if (buf != 0) {puts(buf);free(buf);}}

接下来,我们还需要编译该示例,具体命令如下所示:

$ cd examples$ make all$ echo test | ./rlbasictesttest

这个设置(加上相应的插桩技术)不仅用于模糊测试,同时,还将用于对各种崩溃进行分类。

经过一段时间的模糊测试之后,honggfuzz报告了第一个崩溃事件。正如上一篇文章中提到的,这时将有许多信息被嵌入到发生崩溃的代码所在的文件名中。下面是honggfuzz创建的一些文件。

'SIGABRT.PC.7ffff7c03615.STACK.19d36d1d13.CODE.-6.ADDR.0.INSTR.mov____0x108(%rsp),%rax.fuzz''SIGABRT.PC.7ffff7c03615.STACK.eb563da6d.CODE.-6.ADDR.0.INSTR.mov____0x108(%rsp),%rax.fuzz''SIGABRT.PC.7ffff7c03615.STACK.ec136d3ea.CODE.-6.ADDR.0.INSTR.mov____0x108(%rsp),%rax.fuzz''SIGABRT.PC.7ffff7c03615.STACK.f43c4c1ee.CODE.-6.ADDR.0.INSTR.mov____0x108(%rsp),%rax.fuzz'

我们可以在文件名的第一部分中看到,这里发送的信号是“abort(异常终止)”。此外,我们还可以看到,所有崩溃的程序计数器的值都是相同的(即0x7ffff7c03615)。这两点都表明,这些都是与堆有关的问题(SIGABRT),并且可能是同一类型的问题,例如基于堆的缓冲区溢出漏洞或Double Free漏洞。

为了收集崩溃发生时的一手信息,我们可以借助于Valgrind看看到底发生了什么事情。

$ valgrind --tool=memcheck ./rlbasic > /dev/null < SIGABRT.PC.7ffff7c03615.STACK.f43c4c1ee.CODE.-6.ADDR.0.INSTR.mov____0x108(%rsp),%rax.fuzz==510271== Memcheck, a memory error detector==510271== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.==510271== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info==510271== Command: ./rlbasic==510271====510271== Invalid write of size 1==510271==at 0x483DF68: strncpy (vg_replace_strmem.c:550)==510271==by 0x13221D: rl_insert_text (text.c:99)==510271==by 0x1325F9: _rl_insert_char.part.0 (text.c:902)==510271==by 0x133B8D: _rl_insert_char (text.c:720)==510271==by 0x133B8D: rl_insert (text.c:965)==510271==by 0x112FFA: _rl_dispatch_subseq (readline.c:887)==510271==by 0x1135AF: _rl_dispatch (readline.c:833)==510271==by 0x1135AF: readline_internal_char (readline.c:645)==510271==by 0x113F2C: readline_internal_charloop (readline.c:694)==510271==by 0x113F2C: readline_internal (readline.c:706)==510271==by 0x113F2C: readline (readline.c:385)==510271==by 0x11262F: main (rlbasic.c:17)==510271==Address 0x4bb2c20 is 0 bytes after a block of size 13,568 alloc'd[...]==510271== Invalid read of size 8==510271==at 0x12EBC8: _rl_free_undo_list (undo.c:108)==510271==by 0x12EBC8: rl_free_undo_list (undo.c:124)==510271==by 0x112B4D: readline_internal_teardown (readline.c:498)==510271==by 0x113F43: readline_internal (readline.c:707)==510271==by 0x113F43: readline (readline.c:385)==510271==by 0x11262F: main (rlbasic.c:17)==510271==Address 0x3737373737373737 is not stack'd, malloc'd or (recently) free'd==510271====510271====510271== Process terminating with default action of signal 11 (SIGSEGV): dumping core==510271==General Protection Fault==510271==at 0x12EBC8: _rl_free_undo_list (undo.c:108)==510271==by 0x12EBC8: rl_free_undo_list (undo.c:124)==510271==by 0x112B4D: readline_internal_teardown (readline.c:498)==510271==by 0x113F43: readline_internal (readline.c:707)==510271==by 0x113F43: readline (readline.c:385)==510271==by 0x11262F: main (rlbasic.c:17)==510271====510271== HEAP SUMMARY:==510271==in use at exit: 382,054 bytes in 898 blocks==510271==total heap usage: 4,421 allocs, 3,523 frees, 1,342,095 bytes allocated[…]

上面的内容向我们展示了两个重要的线索。第一部分内容表明,在rl_insert_text.c:99处可能存在一个堆溢出问题;第二部分内容表明,我们控制了可能被释放的数据(0x373737373737或 “7777777”)。需要注意的是,Valgrind的输出可能跟其他工具(如“Dr Memory”)以及glibc错误信息的结果会有所不同,这取决于它们对与堆相关的问题的具体检查方式。

rr(表示记录和重放)是GDB的一个增强工具。它实际上会执行两件事:记录二进制代码的执行过程,并在稍后重放执行过程。需要注意的是,重放将始终是确定性的。此外,重放还可以进行反转。同时,它还支持在执行流程中后退一步,而其他工具通常只能向前走一步;这有时被称为时间旅行式调试。下面,让我们首先来记录代码的执行情况。

$ rr record -n ./rlbasic < SIGABRT.PC.7ffff7c03615.STACK.f43c4c1ee.CODE.-6.ADDR.0.INSTR.mov____0x108(%rsp),%rax.fuzzrr: Saving execution to trace directory `/[…]/rlbasic-6

现在,我们就可以重放执行过程了。在这里,我们将使用GEF作为gdbinit脚本。它在两方面对GDB进行了加强:提供了更多的命令,提高了易用性。当然,即使在不借助gdbinit脚本的情况下,我们也可以顺利使用rr。

rr replay /home/till/.local/share/rr/rlbasic-6gef➤ continue[…]───────────────────────────────────────────────────────────── threads ────[#0] Id 1, stopped 0x7f0e82eaa615 in raise (), reason: SIGABRT─────────────────────────────────────────────────────────────── trace ────[#0] 0x7f0e82eaa615 → raise()[#1] 0x7f0e82e93862 → abort()[#2] 0x7f0e82eec5e8 → __libc_message()[#3] 0x7f0e82ef427a → malloc_printerr()[#4] 0x7f0e82ef47ec → mremap_chunk()[#5] 0x7f0e82ef9670 → realloc()[#6] 0x5584b8275f2e → xrealloc(pointer=, bytes=0x8000)[#7] 0x5584b826017c → realloc_line(minsize=)[#8] 0x5584b82651ab → invis_addc(face=0x30, c=0x5e, outp=)[#9] 0x5584b82651ab → rl_redisplay()

我们可以通过GDB考察SIGABRT。回溯表明,代码调用了realloc函数。现在,我们可以在realloc处放置一个断点,然后使用reverse-continue命令以相反的顺序继续执行(也就是所谓的时间旅行)。

gef➤break reallocBreakpoint 1 at 0x7f0e82ef9580gef➤ reverse-continue

很快,我们就到了断点处。现在,让我们回顾一下realloc函数的原型及其参数:

void *realloc(void *ptr, size_t size);

接下来,我们可以通过查看调用约定来了解参数是如何传递的。实际上,Ptr将被保存到RDI寄存器中,同时,参数size则是通过RSI寄存器进行传递的。

gef➤info registers[…]rdi0x5584b91541f00x5584b91541f0

现在,我们知道了传递的是哪个分块。这样,我们就可以用GEF来研究它了。这正是我喜欢使用gdbinit脚本的众多原因之一。

gef➤heap chunk 0x5584b91541f0Chunk(addr=0x5584b91541f0, size=0x3737373737373730, flags=PREV_INUSE|IS_MMAPPED|NON_MAIN_ARENA)Chunk size: 3978709506094217008 (0x3737373737373730)Usable size: 3978709506094216992 (0x3737373737373720)Previous chunk size: 3978709506094217015 (0x3737373737373737)PREV_INUSE flag: OnIS_MMAPPED flag: OnNON_MAIN_ARENA flag: On

但是,我们也可以通过手动方式来调查这个分块。此外,读者也可以在此处找到堆块布局的图示。

此外,prev_size将位于实际的数据之前,这也是我们的指针所指向的位置。

gef➤x/2x 0x00005584b91541f0 - 80x5584b91541e8:0x373737370x37373737

这些内容看起来与通过GEF收集到的prev_size信息非常相似。由于写入的值是0x3737373737373737或“77777777”,这表明前一个分块中发生了基于堆的缓冲区溢出。现在,我们想找出这些数据是写到哪里的。为此,我们可以在prev_size处放置一个观察点,然后继续利用“时间旅行”查找数据的写入位置。

gef➤watch *0x5584b91541e8Hardware watchpoint 2: *0x5584b91541e8gef➤info breakpointsNumTypeDisp Enb AddressWhat1breakpointkeep y0x00007f0e82ef9580 breakpoint already hit 2 times2hw watchpointkeep y*0x5584b91541e8gef➤disable 1gef➤ reverse-continue[…][#0] Id 1, stopped 0x7f0e82fd1933 in __strncpy_avx2 (), reason: BREAKPOINT─────────────────────────────────────────────────────────────── trace ────[#0] 0x7f0e82fd1933 → __strncpy_avx2()[#1] 0x5584b826d21e → rl_insert_text(string=0x7ffc188192a0 "7")[#2] 0x5584b826d5fa → _rl_insert_char(count=0x1, c=0x37)[#3] 0x5584b826eb8e → _rl_insert_char(c=, count=0x1)[#4] 0x5584b826eb8e → rl_insert(count=, c=)[#5] 0x5584b824dffb → _rl_dispatch_subseq(key=, map=0x5584b8287420 , got_subseq=)[#6] 0x5584b824e5b0 → _rl_dispatch(map=, key=)[#7] 0x5584b824e5b0 → readline_internal_char()[#8] 0x5584b824ef2d → readline_internal_charloop()[#9] 0x5584b824ef2d → readline_internal()──────────────────────────────────────────────────────────────────────────gef➤x/x 0x5584b91541f0-80x5584b91541e8:0x00373737

为了保险起见,让我们再重复一次这个过程。

gef➤ reverse-continue[…]───────────────────────────────────────────────────────────── threads ────[#0] Id 1, stopped 0x7f0e82fd1933 in __strncpy_avx2 (), reason: BREAKPOINT─────────────────────────────────────────────────────────────── trace ────[#0] 0x7f0e82fd1933 → __strncpy_avx2()[#1] 0x5584b826d21e → rl_insert_text(string=0x7ffc188192a0 "7")[#2] 0x5584b826d5fa → _rl_insert_char(count=0x1, c=0x37)[#3] 0x5584b826eb8e → _rl_insert_char(c=, count=0x1)[#4] 0x5584b826eb8e → rl_insert(count=, c=)[#5] 0x5584b824dffb → _rl_dispatch_subseq(key=, map=0x5584b8287420 , got_subseq=)[#6] 0x5584b824e5b0 → _rl_dispatch(map=, key=)[#7] 0x5584b824e5b0 → readline_internal_char()[#8] 0x5584b824ef2d → readline_internal_charloop()[#9] 0x5584b824ef2d → readline_internal()──────────────────────────────────────────────────────────────────────────gef➤x/x 0x5584b91541f0-80x5584b91541e8:0x00003737

这看起来确实像是一步一步地写入数据,但就本例来说,更像是未写入数据。因为如果重复调用同一个函数,将有越来越多的数据按字节写入。

现在,我们有了足够的信息来考察源代码,下面看看到底发生了什么事情。实际上,函数rl_insert_text可以在text.c:85中找到,下面的展示的是我们感兴趣的部分:

if (rl_end + l >= rl_line_buffer_len)rl_extend_line_buffer (rl_end + l);for (i = rl_end; i >= rl_point; i--)rl_line_buffer[i + l] = rl_line_buffer[i];strncpy (rl_line_buffer + rl_point, string, l);[…]rl_point += l;rl_end += l;

由此可见,溢出发生在第99行,其中strncpy溢出到了下一个分块中。这种情况是如何发生的呢?

首先,让我们先看看相关变量的值,然后再来讨论前面的源代码。

gef➤print rl_end$3 = 0xb4gef➤print rl_point$4 = 0x350agef➤print rl_line_buffer_len$5 = 0x3500gef➤print rl_line_buffer$6 = 0x5584b9150ce0 "377200"377p|pp"pppppppp"gef➤heap chunk 0x5584b9150ce0Chunk(addr=0x5584b9150ce0, size=0x3510, flags=PREV_INUSE)Chunk size: 13584 (0x3510)Usable size: 13576 (0x3508)Previous chunk size: 0 (0x0)PREV_INUSE flag: OnIS_MMAPPED flag: OffNON_MAIN_ARENA flag: Off

我们从头分析一下这段代码。如果rl_end(一个全局变量)+l大于rl_line_buffer_len的话,那么缓冲区就会被扩展,其中rl_extend_line_buffer是realloc函数的一个封装函数。同时,它还负责调整rl_line_buffer_len的大小。在text.c:99中(我们的代码段中的第6行),发生了溢出现象。其中,rl_line_buffer是一个指向堆分配缓冲区的指针,注意我们加上了rl_point的值。然而,这里从来没有检查过这个算术运算后,它是否仍然指向我们分配的缓冲区。实际上,关于缓冲区及其大小的检查只有一次,并且在检查过程中,大小是与rl_end和l进行比较的。通过源代码我们可以了解到,rl_end应该指向缓冲区的末端,而rl_point应该指向缓冲区中的某个位置。

See Readline.h:545/* The location of point, and end. */extern int rl_point;extern int rl_end;

因此,通过检查rl_point + l和rl_line_buffer_len的比较结果应该能够防止这种内存损坏,但就这里来说没有任何意义。因此,我们需要找到状态被破坏的地方,以至于rl_point > rl_end。

我们可以再次使用具有反向调试功能的GDB会话。现在,我们将禁用现有的观察点。同时,我们将使用Python脚本,因为GDB Python API允许对断点类进行子类化并编写自定义停止函数。根据自定义stop函数的返回值,程序将中断,我们可以对其进行分析,或者断点将以静默方式步进。我们将用GDB脚本比较rl_point和rl_end的值,如果rl_point值较大,程序就会中断。

然后,我们将把这个自定义的断点用作rl_point和rl_end的观察点。按照Gist的描述应用该脚本后,我们得到了四个断点:

gef➤info breakpointsNumTypeDisp Enb AddressWhat1breakpointkeep n0x00007f0e82ef9580 breakpoint already hit 2 times2hw watchpointkeep n*0x5584b91541e8breakpoint already hit 2 times5hw watchpointkeep yrl_end6hw watchpointkeep yrl_point

现在,我们反向执行。这需要一些时间,因为观察点会被频繁触发,而且条件在相当长的时间内并不会发生改变。

gef➤ reverse-continueContinuing.We found the condition were everything was okay:──────────────────────────────────────────────── source:isearch.c+616 ────611612break;613614case -4:/* C-G, abort */615rl_replace_line (cxt->lines[cxt->save_line], 0);→616rl_point = cxt->save_point;617rl_mark = cxt->save_mark;618rl_deactivate_mark ();619rl_restore_prompt();620rl_clear_message ();621───────────────────────────────────────────────────────────── threads ────[#0] Id 1, stopped 0x5584b825f171 in _rl_isearch_dispatch (), reason: BREAKPOINT─────────────────────────────────────────────────────────────── trace ────[#0] 0x5584b825f171 → _rl_isearch_dispatch(cxt=0x5584b915b750, c=)[#1] 0x5584b825ffef → _rl_isearch_dispatch(c=, cxt=0x5584b915b750)[#2] 0x5584b825ffef → rl_search_history(direction=, invoking_key=)[#3] 0x5584b824dffb → _rl_dispatch_subseq(key=, map=0x5584b8287420 , got_subseq=)[#4] 0x5584b824e5b0 → _rl_dispatch(map=, key=)[#5] 0x5584b824e5b0 → readline_internal_char()[#6] 0x5584b824ef2d → readline_internal_charloop()[#7] 0x5584b824ef2d → readline_internal()[#8] 0x5584b824ef2d → readline(prompt=0x5584b827842b "")[#9] 0x5584b824d630 → main(c=, v=)──────────────────────────────────────────────────────────────────────────gef➤print rl_point$7 = 0x11gef➤print rl_end$8 = 0x11gef➤print cxt->save_point$9 = 0x3468

此时,一个“旧”的上下文被恢复了。但是,这并不会恢复完整的上下文。上面的源代码清单中缺少的就是return语句。这个程序还原了rl_point,但没有还原rl_end。这样的话,我们就能观察到缓冲区溢出的状态了。

下面我们来看看该漏洞的修复代码:

diff --git a/isearch.c b/isearch.cindex ef65e5f..080ba3c 100644--- a/isearch.c+++ b/isearch.c@@ -619,6 +619,7 @@ opcode_dispatch:rl_restore_prompt();rl_clear_message ();+_rl_fix_point (1);/* in case save_line and save_point are out of sync */return -1;case -5:/* C-W */

如果rl_line和rl_point不同步,则通过_rl_fix_point进行同步处理。通过这次修正,所有由honggfuzz报告的Abort都被修正了。所以,虽然honggfuzz进行了一些初步的筛选和分类,但造成的崩溃的一个根本原因,用时间旅行式调试是无法发现的。本文中,我们是通过rr来分析这个漏洞的根本原因的,从而节省了很多时间和脑细胞,因为我们可以轻松地将执行流程逆转到之前的时间点。同时,这也让本文的撰写和步骤记录变得异常简单。如果您意识到自己之前搞砸了什么,并报告了错误的信息,那么,您可以直接穿越到出问题的时间点,直接修复相关数据即可,哈哈哈!

在此,我们感谢GNU readline的维护者Chet Ramey修复了这个漏洞,并指出了我的最初漏洞分析中的一处谬误。最初,我在反向调试时,检查了rl_point大于rl_end的情况,但后来,我忽略了这些值是通过_rl_fix_point同步的。在写这篇文章的时候,我注意到,如果在问题出现之前进行相应的检查,事情会变得更加简单。

现在,我们知道了该漏洞的相关细节,如果我们能在其他地方检测到这个问题就更好了。为此,我们可以借助于相关的静态工具,比如CodeQL或Joern。我之所以选择使用动态的方法,是因为通过之前和这次模糊测试活动,我已经得到了一个corpus。在检测过程中,我使用Frida的Stalker在每个返回语句处获得控制权,并检查rl_point是否大于rl_end。最后,我没有在其他地方发现这个漏洞。如果您对这个Frida脚本感兴趣的话,可以从这里找到它。

时间线

2020-11-10 向GNU readline维护者提交漏洞报告。

2020-11-10维护者确认了这个漏洞。

2020-11-18 在GNU readline邮件列表中公布了一个修正了该漏洞的新版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值