返回介绍

22.1 使用 IDA 发现新的漏洞

发布于 2024-10-11 21:05:49 字数 11016 浏览 0 评论 0 收藏 0

漏洞研究人员采用许多不同的方法发现程序中的新漏洞。如果源代码可用,我们可以利用任何数量的自动化源代码审核工具确定一个程序中可能的问题区域。许多时候,这些自动化工具只能发现最明显的漏洞,而要发现隐藏更深的漏洞则需要进行大量的手动审核。

有大量对二进制文件进行自动审核的工具,它们提供许多与自动源代码审核工具相同的报告功能。二进制文件自动分析的一个明显优势在于使用它不需要访问应用程序源代码。因此,它们可以对闭源、仅二进制的程序进行自动分析。Veracode1 开始提供一项基于订阅的服务,用户可以提交二进制文件,由 Veracode 的专用二进制文件分析工具进行分析。虽然这些工具不能保证能够发现一个二进制文件中的部分或全部漏洞,但是这些技术使得普通用户也能够利用二进制文件分析工具,从而获得一定程度的自信心,自信他们使用的软件没有漏洞或后门。

1. 参见 http://www.veracode.com/

无论是在源代码还是在二进制层次上进行审核,基本的静态分析技巧包括:审核问题函数(如 strcpysprint )的使用,审核动态内存分配例程(如 mallocVirtualAlloc )返回的缓冲区的用法,审核如何处理通过 recvreadfgets 和许多其他类似函数接收的用户提交的输入。在数据库中找到这些函数调用的位置并非难事。例如,为追踪对 strcpy 的所有调用,我们可以采取以下步骤。

  1. 找到 strcpy 函数。

  2. 将光标放在 strcpy 标签上,然后选择 View▶Open Subviews▶Cross References,显示 strcpy 函数的所有交叉引用。

  3. 访问每一个交叉引用并分析提交给 strcpy 的参数,确定是否可以实现缓冲区溢出。

步骤 3 需要你进行大量代码分析和数据流分析,以了解该函数调用的所有可能输入。希望这个任务不太复杂。而步骤 1 看起来相当简单,实际上需要你费点神。要想找准 strcpy 的位置,只需要使用 Jump▶Jump to Address 命令(G),并输入 strcpy 作为跳转目标地址即可。在 Windows PE 二进制文件或静态链接的 ELF 二进制文件中,你通常只要这样做就可以了。但是,对于其他的二进制文件,你可能需要采取其他的步骤。在动态链接的 ELF 二进制文件中,使用 Jump 命令并不能直接将你带到你想要的函数,但会将你带到 extern 节(参与了动态链接过程)中的一个条目。 extern 节中的 strcpy 条目的 IDA 表示形式如下所示:

➊  extern:804DECC          extrn strcpy:near     ; CODE XREF: _strcpy ↑ j  
    extern:804DECC                                ; DATA XREF: .got:off_804D5E4 ↑ o

使问题更加复杂的是,这个位置看起来似乎根本就不叫 strcpy (它的确是叫 strcpy ,但这个名称被缩排),对这个位置的唯一一个代码交叉引用(➊)是一个以 _strcpy 函数为源头的跳转交叉引用,同时,这个位置还有一个以.got 节为源头的数据交叉引用。实际上,引用的函数叫做 .strcpy ,在上面的代码段中你根本看不到这个名称。在这个例子中,IDA 用下划线替换了点字符,因为在默认情况下,IDA 将点视为无效的标识符字符。双击代码交叉引用,我们将看到程序中 strcpy 的过程链接表( .plt )条目,如下所示:

.plt:08049E90 _strcpy    proc near               ; CODE XREF: decode+5F↓ p  
.plt:08049E90                                    ; extract_int_argument+24↓p ...  
.plt:08049E90            jmp     ds:off_804D5E4  
.plt:08049E90 _strcpy    endp

如果我们访问数据交叉引用,最后我们将看到 strcpy 对应的 .got 条目,如下所示:

.got:0804D5E4 off_804D5E4     dd offset strcpy        ; DATA XREF: _strcpy↑ r

.got 条目中,我们遇到另一个以 .plt 节中的 .strcpy 函数为目标的数据交叉引用。实际上,跟踪数据交叉引用是由 extern 节导航到 .plt 节最为可靠的方法。在动态链接的 ELF 二进制文件中,函数通过过程链接表间接调用。现在,我们已经到达 .plt 段,我们可以集中所有对 _strcpy (实际上是 .strcpy )的交叉引用,并开始审核每一个调用(在这个例子中至少有两个函数调用)。

如果我们有一组常用的函数,并且希望找到调用它们的位置并审核,那么这个过程可能会变得相当烦琐。这时,开发一段 IDC 脚本,使用注释自动定位和标记我们感兴趣的所有函数调用,会对我们有所帮助。利用注释标记,我们可以进行简单的搜索,由一个审核位置移动到另一个审核位置。这个脚本的基础是一个函数,它能够可靠地定位另一个函数,以便我们能够定位所有以它为目标的交叉引用。基于从前面的讨论获得的对 ELF 二进制文件的理解,代码清单 22-1 中的 IDC 函数以一个函数名称为参数,返回一个适合交叉引用迭代的地址。

代码清单 22-1 查找一个函数的可调用地址

 static getFuncAddr(fname) {  
    auto func = LocByName(fname);  
    if (func != BADADDR) {  
       auto seg = SegName(func);  
       //what segment did we find it in?  
       if (seg == "extern") { //Likely an ELF if we are in "extern"  
          //First (and only) data xref should be from got  
          func = DfirstB(func);  
          if (func != BADADDR) {  
             seg = SegName(func);  
             if (seg != ".got") return BADADDR;  
             //Now, first (and only) data xref should be from plt  
             func = DfirstB(func);  
             if (func != BADADDR) {  
                seg = SegName(func);  
                if (seg != ".plt") return BADADDR;  
             }  
          }  
       }  
       else if (seg != ".text") {  
          //otherwise, if the name was not in the .text section, then we  
          // don't have an algorithm for finding it automatically  
          func = BADADDR;  
       }  
    }  
    return func;
 }

利用得到的返回地址,现在我们可以追踪任何我们想要审核其用法的函数的引用。代码清单 22-2 中的 IDC 函数利用前一个例子中的 getFuncAddr 函数获得一个函数地址,并为对该函数的所有调用添加注释。

代码清单 22-2 标记对指定函数的调用

    static flagCalls(fname) {  
       auto func, xref;  
       //get the callable address of the named function  
➊     func = getFuncAddr(fname);  
       if (func != BADADDR) {  
          //Iterate through calls to the named function, and add a comment  
          //at each call  
➋        for (xref = RfirstB(func); xref != BADADDR; xref = RnextB(func, xref)) {  
             if (XrefType() == fl_CN || XrefType() == fl_CF) {  
                MakeComm(xref, "*** AUDIT HERE ***");  
             }  
          }  
          //Iterate through data references to the named function, and add a  
          //comment at reference  
➌        for (xref = DfirstB(func); xref != BADADDR; xref = DnextB(func, xref)) {  
             if (XrefType() == dr_O) {  
                MakeComm(xref, "*** AUDIT HERE ***");  
             }  
          }  
       }  
    }

找到我们想要的函数地址后(➊),再利用两个循环迭代以该函数为目标的交叉引用。在第一个循环(➋)中,在每一个调用该函数的位置插入一段注释。在第二个循环(➌)中,在每一个使用该函数地址的位置插入其他注释(使用一个偏移量交叉引用)。为了跟踪以下形式的调用,我们需要第二个循环:

➊  .text:000194EA                 mov     esi, ds:strcpy  
    .text:000194F0                 push    offset loc_40A006  
    .text:000194F5                 add     edi, 160h  
    .text:000194FB                 push    edi  
➋  .text:000194FC call    esi

在这个例子中,编译器将 strcpy 函数的地址缓存到 ESI 寄存器中(➊),以方便程序随后更快地调用 strcpy 函数(➋)。这里的 call 指令执行起来更加快捷,因为它不但更小(2 个字节),而且不需要执行额外的操作来解析调用目标,因为函数地址已经包含在 CPU 的 ESI 寄存器中。当一个函数多次调用另一个函数时,编译器可以选择生成这种类型的代码。

由于这个例子中的函数调用属于间接调用,因此我们例子中的 flagCalls 函数只能看到以 strcpy 为目标的数据交叉引用(➊),而无法看到对 strcpy 的调用(➋),因为 call 指令并不直接引用 strcpy 。但是,实际上,IDA 能够执行有限的数据流分析,并且可以生成下面的反汇编代码清单:

    .text:000194EA                 mov     esi, ds:strcpy  
    .text:000194F0                 push    offset loc_40A006  
    .text:000194F5                 add     edi, 160h  
    .text:000194FB                 push    edi  
➊  .text:000194FC                 call    esi ; strcpy

注意,这里的 call 指令(➊)包含一段注释,它指出 IDA 认为该指令所调用的函数。除了插入注释外,IDA 还添加了一个以调用点为源头、以被调用函数为目标的代码交叉引用。这对 flagCalls 函数有利,因为这样它将能够发现这个例子中的 call 指令,并通过一个代码交叉引用为其添加注释。

为了完善示例脚本,我们需要一个 main 函数,它将为所有我们想审核的函数调用 flag- Calls 。下面这个简单示例说明如何标记对本节前面提到的一些函数的调用:

static main() {  
   flagCalls("strcpy");  
   flagCalls("strcat");  
   flagCalls("sprintf");  
   flagCalls("gets");  
}

运行这段脚本后,可以通过搜索插入的注释文本 ***AUDIT*** ,由我们感兴趣的一个调用转移到另一个调用。当然,从分析的角度看,我们还有许多工作要做,因为一个程序调用 strcpy ,并不表示这个程序可以被利用。这时,我们需要进行数据流分析。为了理解 strcpy 函数的一个特殊调用是否可被利用,你必须确定 strcpy 接收到的参数,并评估是否能够以对你有利的方式操纵这些参数。

与寻找对问题函数的调用相比,数据流分析是一个更加复杂的任务。为了跟踪静态分析环境中的数据流,你需要全面理解这个环境所使用的指令集。因此,你的静态分析工具需要了解寄存器在什么地方分配到了值,这些值如何变化并扩散到其他寄存器。而且,你的工具需要确定在程序中被引用的来源和目标缓冲区的大小,这需要你了解栈帧和全局变量的布局,并推断动态分配的内存块的大小。当然,我们需要在不运行程序的前提下了解所有这些信息。

Halvar Flake 创建的 BugScam2 脚本是通过创意脚本编写方法完成的,它为我们提供了一个有趣的示例。BugScam 采用的技巧与前面的例子使用的技巧类似,即找到调用问题函数的位置,并采取额外的步骤对每一个函数调用进行基本的数据流分析。BugScam 分析的结果是一份 HTML 报告,指出二进制文件中可能存在的问题。分析 sprint 得到的样本报告表如表 22-1 所示。

2. 参见 http://www.sourceforge.net/projects/bugscam

表 22-1 样本报告表

地址严重程度描述
8048c035数据的最大扩展大于目标缓冲区,这可能是缓冲区溢出的原因。最大扩展为 1053。目标大小为 1036

在这个例子中,BugScam 能够确定输入和输出缓冲区的大小。如果与格式化字符串包含的格式指示符结合,它们可用于确定程序生成的输出的最大尺寸。

开发这种类型的脚本需要我们深入了解各种破解程序,以设计一种适用于各种二进制文件的算法。不过,即使缺乏这方面的知识,我们仍然能够开发出一些脚本(或插件)。与手动寻找答案相比,它们可以更快地为我们解答一些简单的问题。

举最后一个例子,假设需要定位包含栈分配的缓冲区的所有函数,因为这些函数可能会受到基于栈的缓冲区溢出攻击。相比于手动浏览数据库,我们可以开发一个脚本,分析每个函数的栈帧,寻找占用大量空间的变量。代码清单 22-3 中的 Python 函数遍历一个给定函数的栈帧的已定义成员,从中搜索其大小大于指定最小大小的变量。

代码清单 22-3 扫描栈分配的缓冲区

      def findStackBuffers(func_addr, minsize):  
         prev_idx = -1  
         frame = GetFrame(func_addr)  
         if frame == -1: return   #bad function  
         idx = 0  
         prev = None  
         while idx &lt GetStrucSize(frame):  
➊          member = GetMemberName(frame, idx)  
            if member is not None:  
               if prev_idx != -1:  
                  #compute distance from previous field to current field  
➋                delta = idx - prev_idx  
➌                if delta >= minsize:  
                     Message("%s: possible buffer %s: %d bytes\n" %  \  
                            (GetFunctionName(func_addr), prev, delta))  
               prev_idx = idx  
               prev = member  
➎             idx = idx + GetMemberSize(frame, idx)  
            else:  
➍             idx = idx + 1

这个函数通过对栈帧中的所有有效偏移量重复调用 GetMemberName (➊),定位该栈帧中的所有变量。变量的大小通过两个连续变量起始偏移量之间的差值计算出来(➋)。如果这个大小超过一个阈值大小( minsize ,➌),则在报告中指出,这个变量是一个可能溢出的栈缓冲区。如果当前偏移量位置处没有定义结构体成员,则结构体索引以 1 字节递增(➍ ),否则,则按在当前偏移量位置发现的任何成员的大小递增(➎ )。在计算每个栈变量的大小时, GetMemberSize 函数似乎是一个更合适的选择,但前提是,IDA 或用户已经正确确定了变量的大小。以下面的栈帧为例:

.text:08048B38 sub_8048B38     proc near  
.text:08048B38  
.text:08048B38 var_818         = byte ptr -818h  
.text:08048B38 var_418         = byte ptr -418h  
.text:08048B38 var_C           = dword ptr -0Ch  
.text:08048B38 arg_0           = dword ptr  8

使用列表中显示的字节偏移量,我们可以计算出:在 var_818var_418 的起始偏移量之间有 1024 个字节( 818h-418h=400h ),在 var_418var_C 的起始偏移量之间有 1036 个字节( 418h-0Ch )。但是,这个栈帧可以被扩展,以显示以下布局:

-00000818 var_818         db ?  
-00000817                 db ? ; undefined  
-00000816                 db ? ; undefined  
...  
-0000041A                 db ? ; undefined  
-00000419                 db ? ; undefined  
-00000418 var_418         db 1036 dup(?)  
-0000000C var_C           dd ?

从中可以看到, var_418 已经折叠成一个数组,而 var_818 仅仅只有一个字节(有 1023 个未定义的字节填充 var_818var_418 之间的空间)。对于这个栈布局, GetMemberSize 将报告 var_818 的大小为 1 字节, var_418 的大小为 1036 个字节,这并不是我们希望见到的结果。无论 var_818 被定义为一个字节还是一个 1024 字节的数组,调用 findStackBuffers(0x08048B38, 16) 将得到以下输出:

sub_8048B38: possible buffer var_818: 1024 bytes  
sub_8048B38: possible buffer var_418: 1036 bytes

创建一个 main 函数,使它遍历数据库中的所有函数(参见第 15 章),并为每个函数调用 findStackBuffers ,我们将得到一个脚本,该脚本能够迅速指出程序的栈缓冲区的使用情况。当然,要确定这些缓冲区是否能够溢出,需要我们对每个函数进行额外的分析(通常是手动分析)。正是由于静态分析非常单调乏味,才使得模糊测试变得如此流行。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文