有调试,也有调试。这是一个关于后者的故事。在我们开始这次旅行之前,我想补充说,我写这篇文章是为了模仿我们是如何真正得出结论的。如果你对奇怪的东西有经验,你可能会看到一条更快的路线,或者使用不同的工具。有不止一种方法可以做到这一点,这就是我需要的时候手头的东西。
首先是一些背景知识。我目前在一个生物信息学实验室工作,致力于蛋白质的功能鉴定。我们的一个项目要求我们以一个执行类似功能的旧工具为基准。我不会把这个特定的工具扔在公交车下,但不用说,它以一种只有学术软件才能达到的方式很有魅力。
它与perl、awk、tsch(是的,还有Fortran)的邪恶融合在一起。
其中一些版权标题来自我上小学的时候-其他的甚至在那之前。
所以…。“很有魅力。”我们提供了基于网络的版本,但限制了您可以提交的蛋白质数量(通过cgi-bin!)。在24小时的窗口内(这是一个很低的数字-比如说~100),我们需要处理远远超过100k的数据。因此,开始这个问题的问题很天真,很天真:
毕竟,所有的代码都可以下载,所以看起来很简单,不是吗?您可能已经猜到了答案,因为您正在阅读一篇名为“极端调试”的文章。
我将集中在一个更大的叫做netNGly的拼图中的一个小片段上。这个方便、花哨的小程序被用来预测蛋白质中的N-糖基化位点。它是以焦油球的形式分布的。足够容易了!
我们可以将其解压缩,然后通过指定解压缩目录的路径来编辑其主文件netNglc(以TSCH编写)来执行设置。然后,我们运行快速测试和…
该程序的入口点是一个调用大量附加awk和tsch文件的tsch脚本,我们有几个选项可以尝试。
如果启用了内核日志记录,则dmesg日志可能会包括攻击者,如下所示。
假设dmesg不包括这个或没有帮助,第二个选择是使用strace。如果您没有太多使用strace的经验,并且经常在Linux上工作,那么非常值得您花时间学习这个工具。我们将把它与-f一起使用来“跟随进程派生”,这样我们就可以看到最终被调用的所有东西。我们还使用-o调用它,这样我们就可以将输出写入一个文件并遍历它。
查找SIGSEGV将向我们显示处理了段故障信号的所有地方。我们希望看到如下所示的行,其中最左边的整数是进程的PID。
现在,我们可以对PID和execve syscall1执行grep,以查看启动了哪个程序。
$file how98_Linuxhow98_LINUX:ELF 32位lsb可执行文件,英特尔80386,版本1(SYSV),静态链接,适用于GNU/LINUX 2.0.0,已剥离。
哦,所以崩溃的是一个旧的静态链接的二进制文件,它已经被完全剥离了。
快速运行nm将确认可怕的事实并产生相同的结果。
现在的问题是:这个古老的程序有问题吗?还是我们的环境有问题?毕竟,它已经使用了很多年了(对吗??)。检查二进制文件需要一些努力,所以最好的方法可能是确保输入文件是正确的。以前,由于shell之间或gawk和awk之间的细微差异,我们在其他类似的程序中遇到了问题。
从exec的strace输出中,我们可以看到程序实际上没有接受任何参数,程序名后面有方括号表示参数,如果我编造一个类似“asdf”的参数并将其传递给程序,您可以看到它。
如果说它一定是从stdin读取,这将是一个相当合乎逻辑的推论。实际上,strace输出包含一个read(0)。
在Linux上,文件描述符0始终是stdin 3。这看起来似乎我们无法摆脱对所有tsch和尴尬的挖掘。第二,那行星号“*”实际上是进程读取内容的预览,我们可以在上面找到匹配的调用来编写。
所有的东西都是通过管道重定向的,不幸的是,重构通过管道的数据移动是非常不容易的。在strace输出的某个地方,我们会发现一些open()调用,这将显示我们正在查找的特定文件的路径。不幸的是,如果它们与chdir调用交织在一起,我们可能只有相对路径。还有一个问题是,只有大量数据。快速运行strace跟踪只跟踪open()调用会产生数千个结果。
嗯。即使我们过滤掉所有对库文件的打开调用,我们仍然只剩下2k以上的结果。可能需要另一种方法。
是时候换个方式来处理一些其他事情了。如何在我的主目录中制作一份netNGlyc的副本,以便我稍后可以做一些更多的分析。让我们试一试,以确保错误是可重现的。
不知何故,将文件复制到我的主目录可以修复所有问题。在此过程中没有修改任何权限。工作副本和损坏副本之间的唯一区别是它们在哪个文件系统上运行。突然间,似乎这个段故障与文件系统有关。
但是这是无稽之谈,它到底在做什么会干扰底层文件系统呢?
查找错误输入的部分问题是netNGlyc脚本在其临时目录上通过rm-rf进行清理。请注意,我们现在有一个包含子目录的tmp目录。我们可以将strace输出中的星号序列与grep相结合,以最终找到有问题的文件。
输入文件的名称都类似于tmp.dat.123456。文件名中的尾随数字(-H到grep)很可能是最初创建它的进程的PID。
$../../how/how98_linux<;tmp.dat.1576192open:Can';t stat文件表观状态:名为test的单元3最近读取顺序格式化的外部IO分段错误(核心转储)。
太棒了!我们终于有了一小块可迭代的工作可以使用,我主目录中的副本的工作方式与此相同,当然只是它输出了正确的结果。
也许我们可以从这个二进制文件中学到一些东西,在我的职业生涯中,每次我不得不求助于strace和gdb来修复第三方代码时,都是一个悲惨的故事。
还记得那个剥离的符号表吗?通常我们可以在gdb内的什么地方运行,它会返回堆栈上的所有帧。
(Gdb)运行<;tmp.dat.1576058启动程序:/mnt/ceph/users/cchandler/Programs/netNglyc-1.0-broken/tmp/netNglyc-1576032/../../how/how98_Linux<;tmp.dat.1576058open:CAN';T STAT文件表观状态:名为TEST.How的单元3最近读取顺序格式化的外部IOProgram接收信号SIGSEGV,分段故障?()(Gdb)其中#0 0x080744f7 in?()#1 0x080612e3 in?()#2 0x08061379 in?()#3 0x08064f3b in?()#4 0x08063e1b in?()#5 0x08063e1b in?()#6 0x080643c3 in?()#7 0x080453c7a in?()#8 0x0804b68b in?()#10 0x0806906 in?()#11 0x080643c3 in?()#7 0x0804b68b in?()#8 0x0804b68b in?()#10 0x0806906 in?()#11 0x080643c3 in?()#7 0x0804b68b in?()#8 0x0804b68b in?()#10 0x0806906 in?()#11 0x080643c3 in?()#7 0x0804b68b in?()#10 0x0806906 in?
这就是我们想要符号表的原因。Gdb不知道我们在哪里,只有一堆未命名的、无法解析的堆栈框架。
也许我们可以从另一个角度来解决这个问题。与其从段错误开始并向后工作,不如让我们尝试从头开始,解决开放的:Cant统计文件。
Strace-f-estat../../how/how98_linux<;tmp.dat.1576058strace:[进程PID=1787025在32位模式下运行。]stat(";test.how";,0xffd81afc)=-1 EOVERFLOW(值对于定义的数据类型来说太大)*输出被省略*。
这是一个有趣的结果。我们创建了一个Stat Syscall,然后内核返回EOVERFLOW。它甚至用一条关于值太大的消息做出了有益的响应。但是这个Stat调用…实际上还有一些非常奇怪的地方。那就是它调用的是stat,而不是stat64。
近20年来,stat64一直是这方面的首选调用(实际上也是glibc中的默认调用),甚至在32位模式下也是如此,这意味着该二进制文件实际上是在GCC/glibc默认开始使用64位调用之前编译的。
为了保证完整性,让我们快速检查一下。GCC将编译器的版本包含在ELF标头的.Comment部分中,而且使用objdump读取ELF标头非常容易。
$objdump-s--SECTION.COMMENT../../how/how98_linux|head../../how/how98_linux:文件格式elf32-i386节的内容注释:0000 00474343 3a202847 4e552920 322e3935.gcc:(GNU)2.95 0010 2e322031 39393931 30323420 2872656c.2.19991024(版本0020 65617365 29000047 4GN43433a20 28474e55 E55)..GCC:(GNU 0030 2920322e 39352e32 20313939 313032)2.95.2 9102 000040 3428656c204743 4(Release)..GCC:(Rel 0020 65617365 29000047 4GN43433a20 28474e55 E55)..GCC:(GNU 0030 2920322e 39352e39352e32 20313939 313032)2.95.2 9102 0040 3428656 c20443 4 4(Release)..GCC:(Release)..GCC。
GCC 2.95.2是在1999年10月发布的,我不知道GCC/glibc默认改用stat64的确切日期,但我敢打赌是在1999年之后。
事情开始拼凑起来了,我打赌如果我们自己运行stat命令5,我们会发现这个旧的32位结构无法处理的值。
最后,这个愚蠢的bug开始有意义了!当临时文件位于庞大的共享Cep文件系统上时,它有一个inode值,内核无法将其强制为32位。当它在我的home目录中时,它恰好有一个足够低的inode值,以至于对stat的32位实现的调用成功了!尽管我的home目录也在64位文件系统上。这纯粹是侥幸。
让我们下载内核3.10的源代码(因为我们运行的是CentOS 7.7)并进行快速验证。这个特定的syscall处理程序在fs/stat.c中实现。
如果类型不同并且整数不匹配,stat.c的第138行上的条件才会生成EOVERFLOW,所以这里有强制检查,它可以在我的主目录中工作。
在这一点上,如果你举起手说“版权标题是1989年的”,那是完全情有可原的。我们既没有来源,也没有符号。互联网早已忘记了如何做到这一点。这就是突破口。“。
但这不是那种帖子,我们也不是在冒险。
让我们假设一下,回到90年代初(甚至80年代末!)。我们必须对文件系统做出不同的假设。可能原始作者正在通过STAT检查文件是否存在?今天,我们几乎肯定会使用此调用来检查权限、时间戳或信息节点号…。但这些都与我们正在进行的实际计算没有任何关系,我们知道文件是存在的。
我们所知道的是,有一个对stat的调用,然后它会向控制台打印一条消息,说明它如何无法统计文件。让我们再次尝试gdb。这一次观察syscall和堆栈。GDB在这方面有一个非常方便的功能,即catch syscall,它会在任何时候发出特定的syscall时中断。
(Gdb)捕获syscall写入(Gdb)捕获syscall statCatchpoint 1(syscall';write';[4])(Gdb)catch syscall statCatchpoint 2(syscall';stat&39;[106])(Gdb)运行<;tmp.dat.1576058启动程序:/mnt/ceph/users/cchandler/Programs/netNglyc-1.0-broken/tmp/netNglyc-1576032/../../how/how98_Linux<;Tmp.dat.1576058Catchpoint 2(调用syscall stat),0x080775be in??()(Gdb)其中#0 0x080775be in??()#1 0x08064a88 be in??()#2 0x08064385 in?()#3 0x08053c7a in?()#4 0x0804b68b in?()#5 0x08064e85 in?()#6 0x080690a6 in?()#7 0x08048111 in?()(Gdb)Catchpoint 2(从sycall stat返回),0x080775be in?()(Gdb)Continuing.Catchpoint 1(对sysCall的调用),#6 0x080690a6 in?()#7 0x08048111 in?()(Gdb)#6 0x080690a6 in?()#7 0x08048111 in?()(Gdb)ContinuingCatchpoint 2(从sycall统计返回),0x080775be in?()(Gdb)#6 0x080690a6 in?()(Gdb)#6 0x080690a6。0x080779f4 in?()(Gdb)其中#0 0x080779f4 in?()#1 0x0807136f in?()#2 0x08071be8 in?()#3 0x080853cf in?()#4 0x08081175 in?()#5 0x0806fc7a in?()#6 0x0806183c in?()#7 0x0806e1b in?()#8 0x080643c3 in?()#8 0x080643c3 in?()#10 0x0804b68b in?()#11 0x0806fc7a in?()#8 0x080643c3 in?()#8 0x080643c3 in?()#10 0x0804b68b in?()#5 0x0806fc7a in?()#6 0x0806183c in?()#8 0x080643c3 in?()#8 0x080643c3 in?()#10 0x0804b68b in?%12 0x080690a6 in??()#13 0x08048111 in??()(Gdb)。
我们感兴趣的是这两组栈帧之间的差异。第一组中的帧3-7与第二组中的帧9-13相匹配。因此,可以合理地得出结论,地址为0x08053c7a的帧是用于检查stat和相关调用的逻辑结果的地方。多亏了catch syscall stat,我们还知道stat调用发生在0x080775be。
我们需要一些逆向工程工具。在这一点上,我启动Cutter,它使用Radare2内核。让我们通过Cutter运行我们的二进制代码,并分析调用图+指令。
上面是包含STAT的函数体,位于0x080775be-您可以在Box#1中看到它。如果您不习惯读取x86程序集,它是int 0x80 6。Box#2也调用Stat,但我们不采用该路径,所以我们将忽略它。有趣的是,寄存器$eax将具有syscall的返回值。假设它为0/成功,我们将直接转到Box#4。从#4开始,我们返回…。我的屏幕抓取中没有空间,但是在4号框之后我们就返回了。要说程序对stat调用完全没有任何作用还为时过早,但是它在调用点附近没有做任何花哨的事情。
一旦我们从这个特定的帧返回,我们就会找到一个函数,它确实节省了统计数据缓冲区的一部分。
所以…。程序似乎对文件调用STAT,从STAT缓冲区获取设备ID(并且只有设备ID),将其保存到内存…中。
我们怎么知道它只是设备ID呢?首先,因为它只移动一个我们可以在指令中看到的双字(32位);其次,因为我们可以直接检查内存并查看stat结构中有什么。
$gd../../HOW/HOW 98_LinuxGNU gdb(Gdb)Red Hat Enterprise Linux 7.6.1-115.el7版权所有(C)2013 Free Software Foundation,Inc.许可GPLv3+:GNU GPL版本3或更高版本<;http://gnu.org/licenses/gpl.html>;This是自由软件:您可以自由更改和重新分发它。在法律允许的范围内,不提供任何担保。键入";show copy";和";show warty";了解详细信息。此gdb配置为";x86_64-redhat-linux-gnu&34;。有关错误报告说明,请访问see:<;http://www.gnu.org/software/gdb/bugs/>;...正在从/mnt/home/cchandler/netNglyc-1.0-broken/how/how98_Linux...(no读取符号找到调试符号)...完成。(Gdb)在0x80775be(Gdb)处中断*0x080775be断点1(Gdb)运行<;tmp.dat.1603546启动程序:/mnt/home/cchandler/netNglyc-1.0-broken/tmp/netNglyc-1603521/../../how/how98_Linux<;Tmp.dat.1603546断点1,0x080775be in??()(Gdb)x/26xw$ecx0xffbc2c:0x0000002c 0x03651d33 0x000181b4 0x061106110xffbc3c:0x00000000 0x0000038d 0x00100000 0x0000000 0x000000010xffffbc4c:0x5f99ee5b 0x0a023368 0x5fffb5b 0x0b0517380xffb032 0x00000
第一个字节包含0x2c,它与/usr/include/bits/stat.h中的设备字段匹配。如果我们统计文件test.how(这是它实际统计的第一个文件)。
$stattest.how文件:‘test.how’大小:909个数据块:1个IO数据块:1048576个普通文件设备:2ch/44d索引节点:56958259个链接:1Access:(0664/-rw-rw-r--)uid:(1553/cchandler)gid:(1553/cchandler)访问:2020-10-28 18:19:07.176305000-0400修改:2020-10-28 18:19:07.184883000-0400更改:2020-10-28 18:19:07.184803378-0400。
但是无论哪种方式,这都是我们最后一次听到它。我们可以通过使用gdb并在该地址设置好后对其设置rwatch来说服自己。无论何时访问特定的内存,gdb都会触发一个硬件辅助断点。如果我们到达程序的末尾并在没有触发断点的情况下得到结果,我们就知道这个内存/设备id永远不会被读取。
(Base)[cchandler@ccblin053netNglc-1603521]$gd../../HOW 98_LinuxGNU gdb(Gdb)Red Hat Enterprise Linux 7.6.1-115.el7版权所有(C)2013 Free Software Foundation,Inc.License GPLv3+:GNU GPL Version 3或更高版本<;http://gnu.org/licenses/gpl.html>;This是自由软件:您可以自由更改和重新分发它。在法律允许的范围内,不提供任何担保。键入";show copy";和";show warty";了解详细信息。此gdb配置为";x86_64-redhat-linux-gnu&34;。有关错误报告说明,请访问see:<;http://www.gnu.org/software/gdb/bugs/>;...正在从/mnt/home/cchandler/netNglyc-1.0-broken/how/how98_Linux...(no读取符号找到调试符号)...完成。(Gdb)在0x8064a92(Gdb)处中断*0x08064a92断点1(Gdb)运行<;tmp.dat.1603546启动程序:/mnt/home/cchandler/netNglyc-1.0-broken/tmp/netNglyc-1603521/../../how/how98_Linux<;Tmp.dat.1603546断点1,0x08064a92 in??()(Gdb)p$eax$1=175498268(Gdb)p/x$eax$2=0xa75e41c(Gdb)rwatch*0xa75e41c硬件读取观察点2:*0xa75e41c(Gdb)cContinuing。##忽略的输出束T*样本*长度:378OK:161%:42.593错误:124.46321分布:n:0 0.0C:316842.6相关:n:0.0000 C:0.0000[下级1(进程1853441)正常退出](Gdb)。
我们现在有了修复此错误的潜在策略。似乎是错误处理代码试图处理失败的stat调用,而实际上最终触发了段故障。因此,一种可能是我们修改了二进制文件以忽略stat语句的结果。我们已经验证了这个小程序在我们的任何用例中都没有使用stat调用函数的结果。让我们再次使用radare2来直接检查和修改二进制文件。
在0x0806438e处是指令jne或JUMP-NOT-EQUAL。您可以在上图的主框底部看到它。如果我们将其从JNE更改为无条件JMP,它将始终走左边的路径,并且完全忽略STAT的结果。
(BASE)[cchandler@ccblin053netNglc-1576032]$pwd/mnt/home/cchandler/ceph/Programs/netNglyc-1.0-broken/tmp/netNglyc-1576032(base)[cchandler@ccblin053netNglc-1576032]$../../HOW 98_LINUX<;Tmp.dat.1576058****。*神经网络分子结构预研*HOW*(C)1989年至1995年***。*****网络架构:NET=1层1:171信元层2:2信元层3:2信元ICOVER:-90信元。
在正常的一天中,我不建议对一个老到可以喝酒的程序进行二进制编辑。然而,我确实相信在时间、精力和风险之间做出明智的权衡。在这种情况下,两天的调试和一次聪明的黑客攻击为一个团队节省了几个星期的工作。我们还能够通过测试获得(相对)情感安全的解决方案。经过数千次测试,我们已经确认,尽管我们采取了聪明的解决办法,但输出仍然保持不变。
每隔一段时间,您确实可以通过一次指令编辑来修复整个问题。
在本例中,为了安全起见,我对更一般的exec执行了grep。还存在其他几种EXEC风格。请参阅执行维基页面。-↩。
符号表已完全删除。即使我们从这个程序中获得堆栈跟踪,也是没有意义的。曾几何时,这样做是为了使分发文件更容易,因为符号表按比例很大。-↩。
出于完整性考虑:原始安装位置位于网络分布式CephInstall上。我的主目录是一个通过nfs挂载的gfs文件系统。/↩。
STAT既是命令又是系统调用。您可以使用MAN 1 STATE与MAN 2 STATE检查差异。“↩。
关于这一点的更详细的解释。
.