当源代码丢失时分析遗留代码:该做还是不做?

二进制代码(即直接由机器执行的代码)的分析是一项艰巨的任务。 在大多数情况下,如果有必要分析二进制代码,请先通过反汇编将其还原,然后反编译为某种高级表示形式,然后由他们分析发生了什么。

在这里必须说,根据文本表示,恢复的代码与最初由程序员编写并编译成可执行文件的代码几乎没有共同之处。 不可能完全恢复从诸如C / C ++,Fortran之类的已编译编程语言中接收到的二进制文件,因为这是算法上非形式化的任务。 在将程序员编写的源代码转换成机器执行的程序的过程中,编译器执行不可逆的转换。

在上个世纪90年代,人们普遍认为编译器像绞肉机一样研磨原始程序,还原程序的任务类似于从香肠还原公羊的任务。


但是,还不错。 在获取香肠的过程中,绵羊失去了功能,而二进制程序将其保存了。 如果最终的香肠可以奔跑和跳跃,那么任务将是相似的。

因此,一旦二进制程序保留了其功能,我们可以说可以将可执行代码恢复为高级表示形式,从而使原始表示不存在的二进制程序的功能与我们收到的文本表示形式的程序等效。

根据定义,如果在相同的输入数据上完成或无法完成执行,并且执行完成时产生相同的结果,则两个程序在功能上是等效的。

拆卸任务通常以半自动模式解决,也就是说,专家使用交互式工具(例如IdaPro交互式拆卸器, 雷达或其他工具)进行手动恢复。 此外,同样在半自动模式下,执行反编译。 适用于解决此反编译任务的HexRaysSmartDecompiler或其他反编译器用作帮助专家的反编译工具。

从字节码恢复程序的原始文本表示可以非常准确。 对于诸如Java或.NET系列语言之类的解释性语言(以字节码进行翻译),反编译任务的解决方式有所不同。 我们不在本文中考虑此问题。

因此,您可以通过反编译来分析二进制程序。 通常,执行此类分析以了解程序的行为以替换或修改它。

从使用遗留程序的实践


40年前用C和Fortran低级语言家族编写的某些软件控制着采油设备。 该设备的故障对于生产至关重要,因此非常不希望更改软件。 但是,在过去的几年中,源代码丢失了。

负责了解其工作原理的信息安全部门的新员工发现,传感器监控程序会以一定的规律性将某些内容写入磁盘,并且尚不知道该如何写入以及如何使用此信息。 他还想到可以在一个大屏幕上显示监视设备运行的信息。 为此,有必要了解程序的工作方式,以何种格式写入磁盘,如何解释此信息。

为了解决该问题,应用了反编译技术,然后分析了还原的代码。 我们首先一次分解一个软件组件,然后本地化负责信息输入/输出的代码,并在给定依赖性的情况下逐渐从该代码中恢复。 然后,恢复了程序的逻辑,这使得有可能回答有关分析软件的安全服务的所有问题。

如果您需要分析二进制程序以恢复其操作逻辑,部分或完全恢复将输入数据转换为输出数据的逻辑等,则使用反编译器可以方便地进行此操作。

除了这些任务之外,实际上还存在分析二进制程序以满足信息安全要求的问题。 而且,客户并不总是了解此分析非常耗时。 似乎要进行反编译并使用静态分析器运行结果代码。 但是作为定性分析的结果,它几乎永远不会成功。

首先,发现的漏洞不仅必须能够发现,而且必须能够解释。 如果以高级语言在程序中发现该漏洞,则分析人员或代码分析工具将在其中显示哪些代码片段包含某些缺陷,而这些缺陷的存在导致了该漏洞。 如果没有源代码怎么办? 如何显示导致漏洞的代码?

反编译器将恢复带有恢复工件的“乱七八糟”的代码,并且将显示的漏洞映射到此类代码是没有用的,但是尚不清楚。 此外,恢复的代码结构不良,因此很不适合代码分析工具。 用二进制程序来解释该漏洞也很困难,因为对其进行解释的人员应该精通程序的二进制表示。

其次,根据信息安全要求进行二进制分析时,必须了解如何处理结果,因为很难修复二进制代码中的漏洞,但是没有源代码。

尽管根据信息安全性要求对二进制程序进行静态分析具有所有特征和困难,但在许多情况下仍需要进行这种分析。 如果由于某种原因没有源代码,并且二进制程序执行了对信息安全要求至关重要的功能,则必须对其进行检查。 如果发现漏洞,则应尽可能将此类应用程序发送以进行修订,或者应为其附加一个“外壳”,这将允许控制敏感信息的移动。

当漏洞隐藏在二进制文件中时


如果程序运行的代码具有很高的关键性,即使该程序的源代码是高级语言,审核二进制文件也很有用。 这将有助于消除编译器通过执行优化转换可能引入的功能。 因此,在2017年9月,对Clang编译器执行的优化转换进行了广泛讨论。 其结果是调用了一个永远不应调用的函数

#include <stdlib.h> typedef int (*Function)(); static Function Do; static int EraseAll() { return system("rm -rf /"); } void NeverCalled() { Do = EraseAll; } int main() { return Do(); } 

作为优化转换的结果,编译器将获得此类汇编代码。 该示例在Linux X86下使用-O2标志进行编译。

  .text .globl NeverCalled .align 16, 0x90 .type NeverCalled,@function NeverCalled: # @NeverCalled retl .Lfunc_end0: .size NeverCalled, .Lfunc_end0-NeverCalled .globl main .align 16, 0x90 .type main,@function main: # @main subl $12, %esp movl $.L.str, (%esp) calll system addl $12, %esp retl .Lfunc_end1: .size main, .Lfunc_end1-main .type .L.str,@object # @.str .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "rm -rf /" .size .L.str, 9 

源代码中存在未定义的行为 。 由于编译器执行了优化转换,因此调用了NeverCalled()函数。 在优化过程中,他最有可能执行尿素分析,结果,Do()函数接收到NeverCalled()函数的地址。 并且由于main()方法调用了未定义的Do()函数,该函数是未定义的行为(未定义的行为),因此结果如下:调用EraseAll()函数,该函数执行rm -rf /命令。

下面的示例:由于编译器的优化转换,我们在取消对NULL的指针之前取消了对它的检查。

 #include <cstdlib> void Checker(int *P) { int deadVar = *P; if (P == 0) return; *P = 8; } 

由于第3行取消了指针的引用,因此编译器假定指针为非零。 接下来,由于优化“删除不可达代码”而删除了第4行,因为该比较被认为是多余的,此后,编译器由于优化了消除死代码”而删除了第3行。 仅剩下第5行,下面显示了在Linux x86下使用-O2标志编译gcc 7.3所产生的汇编代码。

  .text .p2align 4,,15 .globl _Z7CheckerPi .type _Z7CheckerPi, @function _Z7CheckerPi: movl 4(%esp), %eax movl $8, (%eax) ret 

上面的编译器优化工作示例是代码中UB未定义行为的结果。 但是,这是大多数程序员都认为安全的非常普通的代码。 今天,程序员花时间消除程序中未定义的行为,而10年前,他们并未关注它。 结果,旧版代码可能包含UB漏洞。

大多数现代静态源代码分析器都不会检测与UB相关的错误。 因此,如果代码执行对信息安全要求至关重要的功能,则必须检查其源代码和将要执行的代码。

Source: https://habr.com/ru/post/zh-CN415475/


All Articles