返回介绍

15.5 IDC 脚本示例

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

现在,分析一些完成特定任务的脚本示例会很有用。在本章的剩余部分,我们将介绍一些相当常见的情形,说明如何使用脚本来处理与数据库有关的问题。

15.5.1 枚举函数

许多脚本针对各个函数进行操作。例如,生成以某个特定函数为根的调用树,生成一个函数的控制流程图,或者分析数据库中每个函数的栈帧。代码清单 15-1 中的脚本遍历数据库中的每一个函数,并打印出每个函数的基本信息,包括函数的起始和结束地址、函数参数的大小、函数的局部变量的大小。所有输出全部在输口窗口中显示。

代码清单 15-1 函数枚举脚本

#include   
static main() {  
   auto addr, end, args, locals, frame, firstArg, name, ret;  
   addr = 0;  
   for (addr = NextFunction(addr); addr != BADADDR; addr = NextFunction(addr)) {  
      name = Name(addr);  
      end = GetFunctionAttr(addr, FUNCATTR_END);  
      locals = GetFunctionAttr(addr, FUNCATTR_FRSIZE);  
      frame = GetFrame(addr);    // retrieve a handle to the function’s stack frame  
      ret = GetMemberOffset(frame, " r"); // " r" is the name of the return address  
      if (ret == -1) continue;  
      firstArg = ret + 4;  
      args = GetStrucSize(frame) - firstArg;  
      Message("Function: %s, starts at %x, ends at %x\n", name, addr, end);  
      Message("   Local variable area is %d bytes\n", locals);  
      Message("   Arguments occupy %d bytes (%d args)\n", args, args / 4);  
   }  
}

这个脚本使用 IDC 的一些结构操纵函数,以获得每个函数的栈帧的句柄( GetFrame ),确定栈帧的大小( GetStrucSize ),并确定栈中保存的返回地址的偏移量( GetMemberOffset )。函数的第一个参数占用保存的返回地址后面的 4 个字节。函数的参数部分的大小为第一个参数与栈帧结束部分之间的空间。由于 IDA 无法为导入的函数生成栈帧,这个脚本检查函数的栈帧中是否包含一个已保存的返回地址,以此作为一种简单的方法,确定对某个导入函数的调用。

15.5.2 枚举指令

你可能想要枚举给定函数中的每一条指令。代码清单 15-2 中的脚本可用于计算光标当前所在位置的函数所包含的指令的数量。

代码清单 15-2 指令枚举脚本

    #include   
    static main() {  
       auto func, end, count, inst;  
➊     func = GetFunctionAttr(ScreenEA(), FUNCATTR_START);  
       if (func != -1) {  
➋        end = GetFunctionAttr(func, FUNCATTR_END);  
          count = 0;  
          inst = func;  
          while (inst  end) {
             count++;  
➌            inst = FindCode(inst, SEARCH_DOWN | SEARCH_NEXT);  
         }  
         Warning("%s contains %d instructions\n", Name(func), count);  
       }  
       else {  
         Warning("No function found at location %x", ScreenEA());  
       }  
     }

这个函数从➊处开始,它使用 GetFunctionAttr 确定包含光标地址( ScreenEA() )的函数的起始地址。如果确定了一个函数的起始地址,下一步➋是再次使用 GetFunctionAttr 确定该函数的结束地址。确定该函数的边界后,接下来执行一个循环,使用 FindCode 函数(➌)的搜索功能,逐个识别函数中的每一条指令。在这个例子中, Warning 函数用于显示结果,因为这个函数仅仅生成一行输出,而在警告对话框中显示输出,要比在消息窗口中显示输出更加明显。请注意,这个例子假定给定函数中的所有指令都是相邻的。另一种方法可以替代 FindCode 来遍历函数中每条指令的所有代码交叉引用。只要编写适当的脚本,你就可以采用这种方法来处理非相邻的函数(也称为“分块”函数)。

15.5.3 枚举交叉引用

由于可用于访问交叉引用数据的函数的数量众多,以及代码交叉引用的双向性,如何遍历交叉引用可能会令人困惑。为了获得你想要的数据,你必须确保自己访问的是适合当前情况的正确交叉引用类型。在我们的第一个交叉引用示例(如代码清单 15-3 所示)中,我们遍历函数中的每一条指令,确定这些指令是否调用了其他函数,从而获得该函数所做的全部函数调用。要完成这个任务,一个方法是解析 GetMnem 的结果,从中查找 call 指令。但是,这种方法并不是非常方便,因为用于调用函数的指令因 CPU 类型而异。此外,要确定到底是哪一个函数被调用,你还需要进行额外的解析。使用交叉引用则可以免去这些麻烦,因为它们独立于 CPU ,能够直接告诉我们交叉引用的目标。

代码清单 15-3 枚举函数调用

#include 
static main() {
  auto func, end, target, inst, name, flags, xref;
  flags = SEARCH_DOWN | SEARCH_NEXT;
  func = GetFunctionAttr(ScreenEA(), FUNCATTR_START);
  if (func != -1) {
    name = Name(func);
    end = GetFunctionAttr(func, FUNCATTR_END);
    for (inst = func; inst   end; inst = FindCode(inst, flags)) {
    for (target = Rfirst(inst); target != BADADDR; target = Rnext(inst, target)) {
         xref = XrefType();
         if (xref == fl_CN || xref == fl_CF) {
           Message("%s calls %s from 0x%x\n", name, Name(target), inst);
         }
       }
    }
  }
  else {
    Warning("No function found at location %x", ScreenEA());
  }
}

在这个例子中,必须遍历函数中的每条指令。然后,对于每一条指令,我们必须遍历从它们发出的每一个交叉引用。我们仅仅对调用其他函数的交叉引用感兴趣,因此,我们必须检查 XrefType 的返回值,从中查找 fl_CNfl_CF 类型的交叉引用。同样,这个特殊的解决方案只能处理包含相邻指令的函数。由于这段脚本已经遍历了每条指令的交叉引用,因此我们不需要进行太大的更改,就可以使用这段脚本进行基于流程的分析,而不是上面的基于地址的分析。

另外,交叉引用还可用于确定引用某个位置的每一个位置。例如,如果希望创建一个低成本的安全分析器,我们可能会有兴趣监视对 strcpysprint 等函数的所有调用。

危险函数

通常,C 函数 strcpysprintf 被视为是危险函数,因为使用它们可以无限制地向目标缓冲区复制数据。虽然程序员可以通过仔细检查来源和目标缓冲区的大小,来达到安全使用这些函数的目的,但是,由于没有意识到这些函数的危险性,程序员往往会忽略这类检查。例如, strcp 函数通过以下方式声明:

char *strcpy(char *dest, const char *source);

strcpy 函数已定义的行为是将源缓冲区中直到第一个零终止符(包括该终止符)的所有字符复制到给定的目标缓冲区( dest )中。问题在于,在运行时,没有办法确定数组的大小。在这种情况下, strcpy 也就没有办法确定目标缓冲区的容量是否足以容纳从源缓冲区中复制的所有数据。这类未经检查的复制操作是造成缓冲区溢出漏洞的主要原因。

在下面的例子中,如代码清单 15-4 所示,我们逆向遍历对某个符号(相对于前一个例子中的“发出引用”)的所有交叉引用。

代码清单 15-4 枚举一个函数的调用方

#include   
static list_callers(bad_func) {  
   auto func, addr, xref, source;  
➊  func = LocByName(bad_func);  
   if (func == BADADDR) {  
      Warning("Sorry, %s not found in database", bad_func);  
   }  
   else {  
➋     for (addr = RfirstB(func); addr != BADADDR; addr = RnextB(func, addr)) {  
➌       xref = XrefType();  
➍       if (xref == fl_CN || xref == fl_CF) {  
➎           source = GetFunctionName(addr);  
➏           Message("%s is called from 0x%x in %s\n", bad_func, addr, source);  
         }  
      }  
   }  
}  
static main() {  
   list_callers("_strcpy");  
   list_callers("_sprintf");  
}

在这个例子中, LocByName ➊函数用于查找一个给定的(按名称)非法函数的地址。如果发现这个函数的地址,则执行一个循环➋,处理对这个非法函数的所有交叉引用。对于每一个交叉引用,如果确定了交叉引用类型➌为调用类型➍,则确定实施调用的函数的名称➎,并向用户显示这个名称➏。

需要注意的是,要正确确定一个导入函数的名称,你可能需要做出一些修改。具体来说,在 ELF 可执行文件中[这种文件结合一个过程链接表(PLT )和一个全局偏移量表(GOT)来处理共享库链接],IDA 分配给导入函数的名称可能并不十分明确。例如,一个 PLT 条目似乎名为 _memcpy ,但实际上它叫做 .memcpy ;IDA 用下划线替换了点,因为在 IDA 名称中,点属于无效字符。使问题更加复杂的是,IDA 可能只是创建了一个名为 memcpy 的符号,该符号位于一个 IDA 称为 extern 的节内。在尝试枚举对 memcpy 的交叉引用时,我们会对这个符号的 PLT 版本感兴趣,因为它是程序中其他函数调用的版本,因此也是所有交叉引用引用的版本。

15.5.4 枚举导出的函数

在第 13 章中,我们讨论了使用 idsutils 生成描述共享库内容的.ids 文件。我们提到,第一步是生成一个.idt 文件,它是包含库中每个导出函数的描述信息的文本文件。IDC 提供了一些函数,用于遍历共享库导出的函数。下面的脚本如代码清单 15-5 所示,可在 IDA 打开一个共享库后生成一个.idt 文件。

代码清单 15-5 生成.idt 文件的脚本

#include   
static main() {  
   auto entryPoints, i, ord, addr, name, purged, file, fd;  
   file = AskFile(1, "*.idt", "Select IDT save file");  
   fd = fopen(file, "w");  
   entryPoints = GetEntryPointQty();  
   fprintf(fd, "ALIGNMENT 4\n");  
   fprintf(fd, "0 Name=%s\n", GetInputFile());  
   for (i = 0; i  entryPoints; i++) {  
      ord = GetEntryOrdinal(i);  
      if (ord == 0) continue;  
      addr = GetEntryPoint(ord);  
      if (ord == addr) {  
         continue; //entry point has no ordinal  
      }  
      name = Name(addr);  
      fprintf(fd, "%d Name=%s", ord, name);  
      purged = GetFunctionAttr(addr, FUNCATTR_ARGSIZE);  
      if (purged > 0) {  
         fprintf(fd, " Pascal=%d", purged);  
      }  
      fprintf(fd, "\n");  
   }  
}

这个脚本的输出保存在用户指定的文件中。这段脚本引入的新函数包括 GetEntryPointQty ,它返回库导出的符号的数量; GenEntryOrdinal ,它返回一个序号(库的导出表的索引); GetEntryPoint ,它返回与一个导出函数关联的地址(该函数通过序号标识); GetInputFile ,它返回加载到 IDA 中的文件的名称。

15.5.5 查找和标记函数参数

调用一个函数之前,在 x86 二进制文件中,3.4 之后的 GCC 版本一直使用 mov 语句(而非 push 语句)将函数参数压入栈上。由于 IDA 的分析引擎依靠查找 push 语句来确定函数调用中压入函数参数的位置,这给 IDA 的分析造成了一些困难(IDA 的更新版本可以更好地处理这种情况)。下面显示的是向栈压入参数时的 IDA 反汇编代码清单:

.text:08048894                 push    0               ; protocol
.text:08048896                 push    1               ; type  
.text:08048898                 push    2               ; domain  
.text:0804889A                 call    _socket

请注意每个反汇编行右侧的注释。只有在 IDA 认识到参数正被压入,且 IDA 知道被调用函数的签名时,这些注释才会显示。如果使用 mov 语句将参数压入栈中,得到的反汇编代码清单提供的信息会更少,如下所示:

.text:080487AD                 mov     [esp+8], 0  
.text:080487B5                 mov     [esp+4], 1  
.text:080487BD                 mov     [esp], 2  
.text:080487C4                 call    _socket

可见,IDA 并没有认识到,在函数被调用之前,有 3 个 mov 语句被用于为函数调用设置参数。因此,IDA 无法在反汇编代码清单中以自动注释的形式为我们提供更多信息。

在下面这种情形中,我们使用一个脚本恢复我们经常在反汇编代码清单中看到的信息。代码清单 15-6 中的脚本努力自动识别为函数调用设置参数的指令。

代码清单 15-6 参数自动识别

#include   
static main() {  
  auto addr, op, end, idx;  
  auto func_flags, type, val, search;  
  search = SEARCH_DOWN | SEARCH_NEXT;  
  addr = GetFunctionAttr(ScreenEA(), FUNCATTR_START);  
  func_flags = GetFunctionFlags(addr);  
  if (func_flags & FUNC_FRAME) {  //Is this an ebp-based frame?  
    end = GetFunctionAttr(addr, FUNCATTR_END);  
    for (; addr  end && addr != BADADDR; addr = FindCode(addr, search)) {  
      type = GetOpType(addr, 0);  
      if (type == 3) {  //Is this a register indirect operand?  
        if (GetOperandValue(addr, 0) == 4) {   //Is the register esp?  
          MakeComm(addr, "arg_0");  //[esp] equates to arg_0  
        }  
      }  
      else if (type == 4) {  //Is this a register + displacement operand?  
        idx = strstr(GetOpnd(addr, 0), "[esp"); //Is the register esp?  
        if (idx != -1) {  
          val = GetOperandValue(addr, 0);   //get the displacement  
          MakeComm(addr, form("arg_%d", val));  //add a comment  
        }  
      }  
    }  
  }  
}

这个脚本仅针对基于 EBP 的帧,并依赖于此:在函数被调用之前,当参数被压入栈中时,GCC 会生成与 esp 相应的内存引用。该脚本遍历函数中的所有指令。对于每一条使用 esp 作为基址寄存器向内存位置写入数据的指令,该脚本确定上述内存位置在栈中的深度,并添加一条注释,指出被压入的是哪一个参数。 GetFunctionFlags 函数提供了与函数关联的各种标志,如该函数是否使用一个基于 EBP 的栈帧。运行代码清单 15-6 中的脚本,将得到一个包含注释的反汇编代码清单,如下所示:

.text:080487AD                 mov     [esp+8], 0   ; arg_8  
.text:080487B5                 mov     [esp+4], 1   ; arg_4  
.text:080487BD                 mov     [esp], 2    ; arg_0  
.text:080487C4                 call    _socket

这里的注释并没有提供特别有用的信息。但是,现在,我们可以一眼看出,程序使用了 3 个 mov 语句在栈上压入参数,这使我们朝正确的方向又迈进了一步。进一步扩充上述脚本,并利用 IDC 的其他一些功能,我们可以得到另一个脚本,它提供的信息几乎和 IDA 在正确识别参数时提供的信息一样多。这个新脚本的最终输出如下所示:

.text:080487AD                 mov     [esp+8], 0   ;  int protocol
.text:080487B5                 mov     [esp+4], 1   ;  int type
.text:080487BD                 mov     [esp], 2    ;  int domain
.text:080487C4                 call    _socket

代码清单 15-6 中的脚本的扩充版本请参见与本书有关的网站1 ,该脚本能够将函数签名中的数据合并到注释中。

1. 参见 http://www.idabook.com/ch15_examples

15.5.6 模拟汇编语言行为

出于许多原因,你可能需要编写一段脚本,模拟你所分析的程序的行为。例如,你正在分析的程序可能和许多恶意程序一样,属于自修改程序,该程序也可能包含一些在运行时根据需要解码的编码数据。如果不运行该程序,并从正在运行的进程的内存中提取出被修改的数据,你如何了解这个程序的行为呢?IDC 脚本或许可以帮你解决这个问题。如果解码过程不是特别复杂,你可以迅速编写出一个 IDC 脚本,执行和程序运行时执行的操作。如果你不知道程序的作用,也没有可供该程序运行的平台,使用一个脚本以这种方式解码数据,你不需运行程序即可获得相关信息。如果你正使用 Windows 版本的 IDA 分析一个 MIPS 二进制文件,可能会出现上述后一种情况。没有任何 MIPS 硬件,你将无法运行这个 MIPS 二进制文件,观察它执行的任何数据解码任务。但是,你可以编写一个 IDC 脚本来模拟这个二进制文件的行为,并对 IDA 数据库进行必要的修改,所有这一切根本不需要在 MIPS 执行环境中进行。

下面的 x86 代码摘自 DEFCON2 的一个“夺旗赛”3 二进制文件。

2. 参见 http://www.defcon.org/
3. 由 DEFCON 15 CTF 的组织者 Kenshoto 提供。“夺旗赛”是 DEFCON 每年举办的一项黑客竞赛。

.text:08049EDE                 mov     [ebp+var_4], 0  
.text:08049EE5  
.text:08049EE5 loc_8049EE5:  
.text:08049EE5                 cmp     [ebp+var_4], 3C1h  
.text:08049EEC                 ja      short locret_8049F0D  
.text:08049EEE                 mov     edx, [ebp+var_4]  
.text:08049EF1                 add     edx, 804B880h  
.text:08049EF7                 mov     eax, [ebp+var_4]  
.text:08049EFA                 add     eax, 804B880h  
.text:08049EFF                 mov     al, [eax]  
.text:08049F01                 xor     eax, 4Bh  
.text:08049F04                 mov     [edx], al  
.text:08049F06                 lea     eax, [ebp+var_4]  
.text:08049F09                 inc     dword ptr [eax]  
.text:08049F0B                 jmp     short loc_8049EE5

这段代码用于解码一个植入到程序二进制文件中的私钥。使用如代码清单 15-7 所示的 IDC 脚本,不必运行程序就可以提取出这个私钥。

代码清单 15-7 使用 IDC 模拟汇编语言

auto var_4, edx, eax, al;  
var_4 = 0;  
while (var_4 = 0x3C1) {  
   edx = var_4;  
   edx = edx + 0x804B880;  
   eax = var_4;  
   eax = eax + 0x804B880;  
   al = Byte(eax);  
   al = al ^ 0x4B;  
   PatchByte(edx, al);  
   var_4++;  
}

代码清单 15-7 只是对前面汇编语言代码(根据以下相当机械化的规则生成)的直接转换。

  1. 为汇编代码中的每一个栈变量和寄存器声明一个 IDC 变量。

  2. 为每一个汇编语言语句编写一个模拟其行为的 IDC 语句。

  3. 通过读取和写入在 IDC 脚本中声明的对应变量,模拟读取和写入栈变量。

  4. 根据被读取数据的数量(1 字节、2 字节或 4 字节),使用 ByteWordDword 函数从一个非栈位置读取数据。

  5. 根据被写入数据的数量,使用 PatchBytePatchWordPatchDword 函数向一个非栈位置写入数据。

  6. 通常,如果代码中包含一个终止条件不十分明确的循环,那么,模拟程序行为的最简单方法是首先使用一个无限循环(如 while(1) {} ),然后在遇到使循环终止的语句时插入一个 break 语句。

  7. 如果汇编代码调用函数,问题就变得更加复杂。为了正确模拟汇编代码的行为,你必须设法模拟被调用的函数的行为,包括提供一个被模拟的代码的上下文认可的返回值。仅仅这个事实可能就使得你无法使用 IDC 来模拟汇编语言程序的行为。

在编写和上面的脚本类似的脚本时,需要注意的是,有时候,你并不一定非要从整体上完全了解你所模拟的代码的行为。通常,一次理解一两条指令,并将这些指令正确转换成对应的 IDC 脚本就足够了。如果每一条指令都正确转换成 IDC 脚本,那么,整个脚本将能够正确模拟最初的汇编代码的全部功能。我们可以推迟分析汇编语言算法,直到 IDC 脚本编写完成,到那时,我们就可以利用 IDC 脚本深化对基本汇编代码的理解。了解上面示例中算法的工作机制后,我们可以将那个 IDC 脚本缩短成下面的脚本:

auto var_4, addr;  
for (var_4 = 0; var_4 = 0x3C1; var_4++) {  
   addr = 0x804B880 + var_4;  
   PatchByte(addr, Byte(addr) ^ 0x4B);  
}

另外,如果不希望以任何方式修改数据库,在处理 ASCII 数据时,我们可以用 Message 函数代替 PatchByte 函数,或者在处理二进制数据时,将数据写入到一个文件中。

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

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

发布评论

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