返回介绍

9.1 交叉引用

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

首先,需要指出的是,IDA 中的交叉引用通常简称为 xref 。如果引用的是一个 IDA 菜单项或对话框中的内容,我们称这种引用为 xref 。对于其他引用,我们仍然使用 交叉引用 这一术语。

在 IDA 中有两类基本的交叉引用:代码交叉引用和数据交叉引用。这两种引用又分别包含几种不同的交叉引用。每种交叉引用都与一种方向表示法有关。所有的交叉引用都是在一个地址引用另一个地址。这些地址可能是代码地址或数据地址。如果你熟悉图论,可以把这里的地址看做是一个有向图(directed graph )中的节点,而把交叉引用看做是图中的边。图 9-1 可帮助你迅速了解有关图形的术语。在这个简单的图形中,两条有向边(➋)连接了 3 个节点(➊)。

图 9-1 基本的图形组件

需要注意的是,节点也叫做 顶点 (vertice )。有向边则使用箭头表示这条边可以指向的方向。在图 9-1 中,从上面的节点可以到达下面的两个节点之一,但从下面的两个节点却无法到达上面的节点。

代码交叉引用是一个非常重要的概念,因为它可帮助 IDA 生成 控制流图形函数调用图形 ,我们将在本章后面讨论这些图形。

在深入讨论交叉引用之前,先来了解 IDA 如何在反汇编代码清单中显示交叉引用信息。某反汇编函数( sub_401000 )的标题行如图 9-2 所示。该函数的常规注释(图右侧)中包含一个交叉引用。

图 9-2 基本的交叉引用

文本 CODE XREF 表示这是一个代码交叉引用,而非数据交叉引用( DATA XREF )。后面的地址(这里为 _main+2A )是交叉引用的源头地址。注意,这个地址比 .text:0040154A 之类的地址更具描述性。虽然这里的两个地址都可以表示同一个程序位置,但交叉引用中使用的地址提供了额外的信息,指出交叉引用是在一个名为 _main 的函数中提出的,具体而言是 _main 函数中的第 0x2A(42 )字节。地址后面总是有一个上行或下行箭头,表示引用位置的相对方向。在图 9-2 中,下行箭头表示 _main+2A 的地址比 sub_401000 要高,因此,你需要向下滚动才能到达该地址。同样,上行箭头表示引用地址是一个较低的内存地址,你需要向上滚动才能到达。最后,每个交叉引用注释都包含一个单字符后缀,用以说明交叉引用的类型。我们将在后面讨论 IDA 中的各类交叉引用时介绍这些后缀。

9.1.1 代码交叉引用

代码交叉引用用于表示一条指令将控制权转交给另一条指令。在 IDA 中,指令转交控制权的方式叫做 (flow )。IDA 中有 3 种基本流: 普通流跳转流调用流 。根据目标地址是近地址还是远地址,跳转流和调用流还可以进一步细分。只有在使用分段地址的二进制文件中,你才会遇到远地址。在接下来的讨论中,我们使用以下程序的反汇编版本:

int read_it;            //integer variable read in main  
int write_it;           //integer variable written 3 times in main  
int ref_it;             //integer variable whose address is taken in main  

void callflow() {}      //function called twice from main  
int main() {  
   int *p = &ref_it;    //results in an "offset" style data reference  
   *p = read_it;        //results in a "read" style data reference  
   write_it = *p;       //results in a "write" style data reference  
   callflow();          //results in a "call" style code reference  
   if (read_it == 3) {  //results in "jump" style code reference  
      write_it = 2;     //results in a "write" style data reference  
   }  
   else {               //results in an "jump" style code reference  
      write_it = 1;     //results in a "write" style data reference  
   }  
   callflow();          //results in an "call" style code reference  
}  

根据注释文本的描述,这个程序包含了 IDA 中体现所有交叉引用特性的操作。

普通流 (ordinary flow )是一种最简单的流,它表示由一条指令到另一条指令的顺序流。这是所有非分支指令(如 ADD )的默认执行流。除了指令在反汇编代码清单中的显示顺序外,正常流没有其他特殊的显示标志。如果指令 A 有一个指向指令 B 的普通流,那么,在反汇编代码清单中,指令 B 会紧跟在指令 A 后面显示。在代码清单 9-1 中,除➊、➋两处的指令外,其他每一条指令都有一个普通流指向紧跟在它们后面的指令。

代码清单 9-1 交叉引用源和目标

  .text:00401010 _main            proc near  
  .text:00401010  
  .text:00401010 p                = dword ptr -4  
  .text:00401010  
  .text:00401010                  push    ebp  
  .text:00401011                  mov     ebp, esp  
  .text:00401013                 push    ecx  
  .text:00401014                ➒ mov     [ebp+p], offset ref_it  
  .text:0040101B                  mov     eax, [ebp+p]  
  .text:0040101E                ➐ mov     ecx, read_it  
  .text:00401024                 mov     [eax], ecx  
  .text:00401026                 mov     edx, [ebp+p]  
  .text:00401029                mov     eax, [edx]  
  .text:0040102B               ➑ mov     write_it, eax  
  .text:00401030               ➌ call    callflow  
  .text:00401035               ➐ cmp     read_it, 3  
  .text:0040103C                 jnz     short loc_40104A  
  .text:0040103E               ➑ mov     write_it, 2  
  .text:00401048               ➊ jmp     short loc_401054  
➎.text:0040104A ; -------------------------------------------------------------
  .text:0040104A  
  .text:0040104A loc_40104A:                           ➏ ; CODE XREF: _main+2C↑j  
  .text:0040104A               ➑ mov     write_it, 1  
  .text:00401054
  .text:00401054 loc_401054:                           ➏ ; CODE XREF: _main+38↑j  
  .text:00401054               ➌ call    callflow  
  .text:00401059                  xor     eax, eax  
  .text:0040105B                  mov     esp, ebp  
  .text:0040105D                  pop     ebp  
  .text:0040105E                ➋ retn  
  .text:0040105E _main            endp  

指令用于调用函数,如➌处的 x86 call 指令,它分配到一个 调用流 (call flow ),表示控制权被转交给目标函数。多数情况下, call 指令也分配到一个普通流,因为大多数函数会返回到 call 之后的位置。如果 IDA 认为某个函数并不返回(在分析阶段确定),那么,在调用该函数时,它就不会为该函数分配普通流。调用流通过在目标函数(流的目的地址)处显示交叉引用来表示。 callflow 函数的反汇编代码清单如下所示:

.text:00401000 callflow        proc near               ; CODE XREF: _main+20↓p  
.text:00401000                                         ; _main:loc_401054 ↓ p  
.text:00401000                 push    ebp  
.text:00401001                 mov     ebp, esp  
.text:00401003                 pop     ebp  
.text:00401004                 retn  
.text:00401004 callflow        endp

在这个例子中, callflow 所在的位置显示了两个交叉引用,表示这个函数被调用了两次。除非调用地址有相应的名称,否则,交叉引用中的地址会以调用函数中的偏移量表示。这里的交叉引用分别用到了上述两种地址。由函数调用导致的交叉引用使用后缀 p (看做是 Procedure)。

每个无条件分支指令和条件分支指令将分配到一个 跳转流 (jump flow)。条件分支还分配到普通流,以在不进入分支时对流进行控制。无条件分支并没有相关的普通流,因为它总会进入分支。➎处的虚线表示相邻的两条指令之间并不存在普通流。跳转流与跳转目标位置显示的跳转式交叉引用有关,如➏处所示。与调用式交叉引用一样,跳转交叉引用显示引用位置(跳转的源头)的地址。跳转交叉引用使用后缀 j (看做是 Jump)。

9.1.2 数据交叉引用

数据交叉引用用于跟踪二进制文件访问数据的方式。数据交叉引用与 IDA 数据库中任何牵涉到虚拟地址的字节有关(换言之,数据交叉引用与栈变量毫无关系)。IDA 中最常用的 3 种数据交叉引用分别用于表示某个位置何时被读取、何时被写入以及何时被引用。下面是与前一个示例程序有关的全局变量,其中包含几个数据交叉引用。

.data:0040B720 read_it           dd ?               ; DATA XREF: _main+E ↑ r  
.data:0040B720                                      ; _main+25 ↑ r  
.data:0040B724 write_it          dd ?               ;  DATA XREF: _main+1B ↑w  
.data:0040B724                                   ➓ ; _main+2E↑ w ...  
.data:0040B728 ref_it            db    ? ;          ; DATA XREF: _main+4 ↑ o  
.data:0040B729                   db    ? ;  
.data:0040B72A                   db    ? ;  
.data:0040B72B                   db    ? ;

读取交叉引用 (read cross-reference)表示访问的是某个内存位置的内容。读取交叉引用可能仅仅源自于某个指令地址,但也可能引用任何程序位置。在代码清单 9-1 中,全局变量 read_it 在➐处被读取。根据上面代码中显示的相关交叉引用注释,我们可以知道 main 中有哪些位置引用了 read_it 。根据后缀 r ,可以确定这是一个读取交叉引用。对 read_it 的第一次读取是 ECX 寄存器中的 32 位读取,它使 IDA 将 read_it 格式化成一个双字。通常,IDA 会收集尽可能多的线索,根据程序访问变量的方式,以及函数如何将变量用作自己的参数,以确定变量的大小和/或类型。

在代码清单 9-1 中,全局变量 write_it 在➑处被引用。IDA 生成的相关 写入交叉引用 (write cross-reference)作为变量 write_it 的注释显示,其中指出了修改变量内容的程序位置。写入交叉引用使用后缀 w 。同样,在这里,IDA 根据 32 位的 EAX 寄存器被复制到 write_it 中这一事实,确定了这个变量的大小。值得注意的是, write_it 位置显示的交叉引用以省略号➓处结束,表明对 write_it 的交叉引用数量超出了当前的交叉引用显示限制。你可以通过 Options▶General 对话框中 Cross-references 选项卡中的 Number of displayed xrefs(显示的交叉引用数量)设置修改这个限制。和读取交叉引用一样,写入交叉引用可能仅仅源自于一条程序指令,但也可能引用任何程序位置。一般而言,以一个程序指令字节为目标的写入交叉引用表示这是一段自修改代码,这种代码通常被视为无效代码,在恶意软件使用的“去模糊例程”(de-obfuscation routine)中经常可以发现这类代码。

第三类数据交叉引用为 偏移量交叉引用 (offset cross-reference ),它表示引用的是某个位置的地址(而非内容)。在代码清单 9-1 中,全局变量 ref_it 的地址在➒处被引用,因此,在上面的代码中, ref_it 所在的位置显示了偏移量交叉引用(后缀为 o )的注释。通常,代码或数据中的指针操作会导致偏移量交叉引用。例如,数组访问操作一般通过在数组的起始地址上加上一个偏移量来实现。因此,许多全局数组的第一个地址通常可以由偏移量交叉引用来确定。为此,许多字符串数据(在 C/C++ 中,字符串作为字符数组)成为偏移量交叉引用的目标。

与仅源自于指令位置的读取和写入交叉引用不同,偏移量交叉引用可能源于指令位置或数据位置。例如,如果一个指针表(如虚表)从表中的每个位置向这些位置指向的地方生成一个偏移量交叉引用,则这种偏移量交叉引用就属于源于程序数据部分的交叉引用。分析第 8 章中类 SubClass 的虚表,就可以发现这一点,它的反汇编代码清单如下所示:

.rdata:00408148 off_408148    dd offset SubClass::vfunc1(void) ; DATA XREF: SubClass::SubClass(void)+12 ↑o  
.rdata:0040814C               dd offset BaseClass::vfunc2(void)  
.rdata:00408150               dd offset SubClass::vfunc3(void)  
.rdata:00408154               dd offset BaseClass::vfunc4(void)  
.rdata:00408158               dd offset SubClass::vfunc5(void)  

可以看到,类构造函数 SubClass:: SubClass(void) 使用了虚表的地址。函数 SubClass:: vfunc3(void) 的标题行如下所示,显示了连接该函数与虚表的偏移量交叉引用。

.text:00401080 public: virtual void __thiscall SubClass::vfunc3(void) proc near 

  
.text:00401080                                           ; DATA XREF: .rdata:00408150↓o

这个例子证实了 C++ 虚函数的一个特点,结合偏移量交叉引用来考查,这个特点显得尤为明显,即 C++ 虚函数绝不会被直接引用,也绝不应成为调用交叉引用的目标。所有 C++ 虚函数应由至少一个虚表条目引用,并且始终是至少一个偏移量交叉引用的目标。需要记住的是,你不一定需要重写一个虚函数。因此,如第 8 章所述,一个虚函数可以出现在多个虚表中。最后,回溯偏移量交叉引用是一种有用的技术,可迅速在程序的数据部分定位 C++ 虚表。

9.1.3 交叉引用列表

介绍了交叉引用的定义后,现在开始讨论如何访问 IDA 中的所有交叉引用数据。如前所述,在某个位置显示的交叉引用注释的数量由一个配置控制,其默认设置为 2。只要一个位置的交叉引用数量不超出这个限制,你就可以相当直接地访问这些交叉引用。将光标悬停在交叉引用文本上,IDA 将在一个类似于工具提示的窗口中显示交叉引用源头部分的反汇编代码清单。双击交叉引用地址,反汇编窗口将跳转到交叉引用的源位置。

你可以通过两种方法查看某个位置的交叉引用完整列表。第一种方法是打开与某一特定位置有关的交叉引用子窗口。将光标放在一个或多个交叉引用的目标地址上,并选择 View▶Open Subviews ▶Cross-References(查看▶打开子窗口▶交叉引用),即可打开指定位置的交叉引用完整列表,如图 9-3 所示,其中显示了变量 write_it 的交叉引用完整列表。

图 9-3 交叉引用显示窗口

窗口中的每列分别表示交叉引用源头的方向(向上或向下)、交叉引用的类型(基于前面讨论的类型后缀)、交叉引用的源地址以及源地址处显示的对应反汇编文本,包括注释。和其他显示地址列表的窗口一样,双击窗口中的任何条目,反汇编窗口将跳转到对应的源地址。交叉引用窗口一旦打开,将会始终显示,你可以通过反汇编代码清单工作区上方的一个标题标签(与其他打开的子窗口的标题标签一起显示)访问这个窗口。

第二种访问交叉引用列表的方法是突出显示一个你感兴趣的名称,在菜单中选择 Jump▶Jump to xref(使用热键 CTRL+X )打开一个对话框,其中列出了引用选中符号的每个位置。最终的对话框如图 9-4 所示,该对话框在外观上与图 9-3 中的交叉引用子窗口几乎一模一样。选中 write_it 的第一个实例( .text:0040102B )并使用热键 CTRL+X ,即可打开图 9-4 中的对话框。

图 9-4 跳转到交叉引用对话框

图 9-3 中的子窗口与图 9-4 中的对话框之间的区别主要表现在行为方面。图 9-4 显示的对话框是一个模式1 对话框(modal dialog),它提供了用于交互和关闭对话框的按钮。这个对话框的主要用途是选择一个引用位置,并跳转到该位置。双击其中列出的一个位置,对话框会立即关闭,同时反汇编窗口将跳转到你选择的位置。对话框与交叉引用子窗口之间的第二个区别在于前者可以通过选择任何符号并使用热键或上下文菜单打开,而后者只能通过将光标放在一个交叉引用目标地址上,然后选择 View▶Open Subviews▶Cross-References 打开。换句话说,对话框可以在任何交叉引用的源位置打开,而子窗口只能在交叉引用的目标位置打开。

1. 在继续与基础应用程序进行正常交互之前,你必须关闭模式对话框。在继续与应用程序正常交互时,你可以始终打开非模式对话框。

交叉引用列表可用于迅速确定调用某个特殊函数的位置。许多人认为使用 C strcpy 2 函数很危险。如果使用交叉引用,定位每一个 strcpy 调用和查找任何一个 strcpy 调用一样简单,你只需使用热键 CTRL+X 打开交叉引用对话框,并浏览其中的每一个调用交叉引用即可。如果你不想花时间查找二进制文件所使用的 strcpy 函数,你甚至可以添加一段包含 strcpy 文本的注释,并使用该注释3 激活“交叉引用”对话框。

2. C strcpy 函数将一个源字符数组(包括相关的空终止符)复制到一个目标数组中,而不检查目标数组是否拥有足够的空间可以容纳源数组中的所有字符。
3. 如果一个符号名称出现在注释中,IDA 会将这个符号作为反汇编指令中的一个操作数处理。双击该符号,反汇编窗口将跳转到相应位置。同时,右击该符号,将显示上下文菜单。

9.1.4 函数调用

有一种交叉引用列表专门处理函数调用,选择 View▶Open Subviews▶Function Calls 即可打开该窗口。图 9-5 所示为结果对话框,窗口的上半部分列出了所有调用当前函数(由打开窗口时光标所在位置决定)的位置,窗口的下半部分列出了当前函数做出的全部调用。

enter image description here

图 9-5 函数调用窗口

同样,使用窗口中列出的交叉引用,可以迅速将反汇编代码清单定位到对应的交叉引用位置。如果仅仅考查函数调用交叉引用,我们将能够更多地考虑函数之间的抽象关系,而不只是一个地址与另一个地址之间的对应关系。在下一节中,我们将讨论如何通过 IDA 提供的各种帮助你解释二进制文件的图形,利用这种抽象关系。

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

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

发布评论

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