原始磁盘文件已删除,但进程尚在,需要想办法从进程中恢复原始程序以便后续分析
关键词:样本应急
、脱壳
、PE 修复
、IAT 重建
TL;DR 大致思路和步骤如下:
ApiScout:对目标所在的 OS 运行时环境(系统 API 加载地址和其符号名映射关系等)进行快照,以便后期对目标环境所加载的库函数 API 地址和符号名等进行检索和查询(如目标环境开启了 ASLR)。还支持对 PE/dump 等文件基于前述收集到的系统 API 信息,进行系统 API 调用的定位和解析,便于快速确认该 PE 类文件实际所使用的系统函数情况及其在代码段中的调用/引用位置,为接下来的 IAT 重建提供参考信息。
LIEF:一套抽象了 PE/ELF/MachO/OAT/DEX/VDEX/ART 等不同平台可执行文件格式的解析库、编辑库,此处用其进行 IAT 重建,对 PE 文件直接进行二进制层面的 patch。
处理此类场景的 Workflow 如下(SVG 原图见附件):
Create dump file
. 任务管理器右键创建 dump 文件/minidmp
option(需注意,pe-sieve 只会在检测到程序存在 path/hook 等 inconsistent 场景时才会 dump suspicious staff,在此前提下 /minidmp
才会起效。若程序本身较为简单,没检测到 suspicious,即使加了 /minidmp
也不会有任何输出)此步也可以直接使用 ProcessHacker
等工具对进程的 Memory region 进行转储,这样转储出来的内容即是 PE 文件在内存中展开后的样子,但不包含进程运行环境等上下文状态信息
WinDbg 从内存中提取展开后的程序时,可能用到的命令/操作(供参考)
1 2 3 4 5 6 7 8 |
|
背景介绍:PE 文件有 2 种布局形态:磁盘上的文件形态 & 加载到内存中的映像形态。两者由于各自所对应的对齐单位不同,造成了从内存中 dmp 出来的映像文件结构没法直接被 PE 类分析工具直接加载解析。反之亦然,磁盘文件形态必须要经过 Loader 进行内存对齐、导入解析、重定位修正等一番操作后方可执行。
通常来说,我们将内存中的映像状态称为展开后的状态
,而磁盘上的文件形态则称为展开前的状态
一个较为典型的 PE32 文件,其磁盘状态下及映像状态下各 section 的映射和对齐方式如下图所示:
在该文件中,磁盘文件结构的对齐单位是 0x200(512),而内存映像结构的对齐单位是 0x1000(4096),这一不同也造成了通常情况下,PE 文件在加载到内存中后的空间大小要大于其在磁盘上保存时所占据的文件大小,对应的直观感受也就是"文件像是展开了一样"。
当然,并非所有的 PE 文件其两种结构的对齐单位、VA/RVA 等地址不同,如下:
两者在对齐单位、地址、大小等字段上均一致。这种文件形态通常是手动修复过的状态,一般是由 Virtual(mapped)->Raw(unmapped)转换后加以 realign
微调所呈现的效果。这种修复后的文件,只要 EP、IAT 等结构正常可解析,基本可以直接正常运行。
接下来,以一个 CobaltStrike stager 程序的运行示例为例,介绍 2 种从展开后的格式(Virtual/mapped)恢复至展开前格式(Raw/unmapped)的 section 修复方法。
假设:从 112_http_stager.exe.dmp 中发现 112_http_stager.exe 的加载信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
并且已经将该 0x9000 大小的内存区域转储至磁盘 dump.dmp 文件中:
.writemem /path/to/dump.dmp 00400000 L9000
接下来,需要对 dump.dmp 进行修复。该 dump.dmp 在 PE-bear 中的解析结果如下图所示:
上图中 IAT 视图的字段显示红色解析异常,表明 IAT 表结构无法正常解析,需要进行修复。
repo: https://github.com/hasherezade/libpeconv/tree/master/pe_unmapper
Small tool to convert beteween the PE alignments (raw and virtual). Allows for easy PE unmapping: useful in recovering executables dumped from the memory.
我们使用默认的选项参数即可(默认为 unmap
模式):
pe_unmapper.exe /in dump.mem 00400000 /out 112_http_stager.exe
1 2 3 |
|
可以看到,文件大小发生了变化,变小了。
与此同时,IAT 表解析正常,用 IDA 打开该文件进行静态分析,或者直接双击运行,都没有问题。
假设,我们以 /mode r
(REALIGN (Virtual to Raw, where: Raw == Virtual))方式进行修复:
pe_unmapper.exe /in dump.mem 00400000 /out 112_http_stager_mode_r.exe /mode r
1 2 3 4 |
|
文件大小并没有发生变化,均为 36864。
IAT 结构虽解析正常(在 section 视图中也可以看到,Raw 和 Virtual 的属性值完全相同),且 IDA 打开未报明显问题,但却无法双击运行或动态调试。
细究之下发现,由于我们是对运行时的程序进行了 dump 操作,dmp 文件中势必会包含一些已经初始化过/修改过的变量/数据内容,如 .data/.bss 区段的一些字段可能已经得到更改或赋值。我们在使用 pe_unmapper 默认模式,也即 /mode u
时,会照着 PE Header 中的字段值对 dmp 文件进行真正的裁剪/压缩,使得部分区段得到真正的重置或剔除(如 .bss 仅占位,实际在文件中并不占据任何空间)。
而 /mode r
修复后的程序,把这一部分污染后的 .bss 段保留了下来,且 section 中 Raw 字段的相关值也被修改为同 Virtual 一样,PE loader 会将文件结构中的这部分内容原封不动、直接加载在 0x5000(RVA) 的虚拟地址上(而不像通常情况下,.bss 段的内容是由 PE loader 在运行时动态申请并填充全 0),这就导致修复 2 在加载执行后,由于 .bss:argv 位置存在残影,造成程序直接访问在当前运行实例下,实际还未初始化过的地址 0x6C1648。
这样一组对比测试表明,尽管将 Raw 对齐 Virtual 的修复方法,不会对 IDA 静态分析的结果造成影响,但由于我们保留了一部分内存状态的残影数据,可能会在重新运行时发生潜在的 bug/异常。
由于 .bss 段的特点:存放在此段的通常是未初始化的变量,一般情况下大部分的开发语言和运行平台对于这部分变量的处置策略是初始化 0,故而 .bss 段的 RawSize 通常为 0,仅占位用。
可在修复 2 的基础上,将 .bss 的 RawSize 重置为 0,也能解决问题。此时,程序可正常运行启动。
技巧点:将各 section 的 RawAddress 修改为对应的 VirtualAddress 值,即 RawAddress <= VirtualAddress
可使用 PE-bear/CFF Explorer 类 PE 编辑器对 section 头中的字段值进行修改。
pe_unmapper 或 pe-sieve 工具集所额外提供的 realign 模式或本小节介绍的手动 realign 方法,修改后的文件大小基本等于从内存中展开后的映像大小。区别于 realign 模式,unmap 模式(pe_unmapper 的 /mode u
以及 pe-sieve 工具集的 /dmode 2
)则"相当于"按照 section 的 Raw-* 字段进行修正和裁剪,修复后的文件会小于内存中展开后的映像大小,可以理解成 PE Load 的逆过程。(暂时还没研究如何手动进行 Raw 格式的裁剪)
对于在加载阶段或程序本身初始阶段就会动态申请内存并填充可执行代码,或自修改,IAT 动态 patch 的场景,Raw 模式有可能会造成运行时所申请空间内的数据的丢失。但具体影响效果也要视情况而定,如果这种场景下 EP 指针不做修改,即仍旧是从头开始执行,有可能会执行顺利。而如果是想将 EP 直接调整到初始化加载、环境准备阶段结束后某一时刻的业务运行点,则需要确保此时 EP 所在的代码段及其它依赖数据段有被完整保留下来。
另外一个决定此方法修改完就能直接运行使用的点在于 OriginalFirstThunk 是否为 0.
若 realign 之后发现 OriginalFirstThunk 字段为 0,则有概率还需要对 IAT 表进行修复。反之,则大概率不需要再进行二次加工,realign 之后就直接能够静态分析和动态运行。
原因在于:
OriginalFirstThunk(INT) 和 FirstThunk(IAT) 在磁盘文件格式时,两者通常均指向 INT,即此时 FirstThunk 相当于 OriginalFirstThunk 的冗余备份。当 PE 被加载进内存后,FirstThunk 所指向的 IAT 会被填充上各个 DLL 库所导入的 API 的实际加载地址,如 0x7eeffxxxxx。部分修复工具或方式,在修复完成之后,OriginalFirstThunk 数据丢失,而 FirstThunk 却能保持原本在磁盘文件格式时的数组值。故而,尽管此时 OriginalFirstThunk 全 0 无法使用,但根据"当 OriginalFirstThunk 为 0 时会使用 FirstThunk 地址"的实现要求,仍可以根据 FirstThunk 来正确解析 INT。反之,若修复后的 OriginalFirstThunk 字段丢失全变 0,且 FirstThunk 指向内存残影留下的运行时的 IAT 填充数据状态,则此时该修复后的 PE 文件当在使用 PE 类查看工具或 IDA 打开时,会清楚地觉察到其 IAT 解析结果异常。针对此类情况,就需要对 IAT 进行重建修复。
以 @ZHH 同学的《内存中Dump样本并进行修复》为例,该文章中所使用的示例样本为 xxx.exe 0BBDE5E6B3AEFC33014EC3C1F1E61D8664A33A75
----以下援引自《内存中Dump样本并进行修复》----
使用Processhacker dump进程内存
此时dump下来的文件不能打开需要使用CFF Explorer进行修复。修复时仅需要将Section Headers中的Virtual Address值复制到Raw Address中即可。
复制后如下所示:
此时再打开就可以运行了(似乎)。
----以上援引自《内存中Dump样本并进行修复》----
上文还提到,由于是通过手动下断点后挂起的方式来创造 dump 时机,造成 dump 出来的内存中存在 int3 的断点,即使修复了直接运行也会报错,需要将该断点处的 int3 指令恢复到之前的数值。这一步只是因为演示步骤的原因,并不是每个 dump 的进程都需要进行此类修复。
如前面所说,这种手动 realign 的方法能在此例下直接使用的原因,是因为 dump 出来进行修复后的 PE 文件中 OriginalFirstThunk
非 0,通过调整 align 后,INT 表的指针正好能指向对齐后的 INT 位置上,尽管此时 FirstThunk 所指向的 IAT 里实际包含了 dump 时的内存残影(即,对目标进程 dump 时当时填充到 IAT 中各 API 函数的实际解析地址),但因为 PE 文件在被 load 之后该表会被重新覆盖,所以并不会对程序造成影响。
相反,如果我们做一个实验,即,将原始文件 xxx.exe 0BBDE5E6B3AEFC33014EC3C1F1E61D8664A33A75 section 中的 OriginalFirstThunk
全部抹 0 处理,如下:
可以看到,尽管我们把
OriginalFirstThunk
抹 0,但 PE-bear 依然能把导入函数解析出来,原因是 FirstThunk 的"冗余"作用,详见前文描述。
对修改后的程序再次使用同样的方法进行 dump+修复,效果如何呢?
如上图,我们通过 realign 方式修复后,PE-bear 仍旧无法正确解析导入函数的符号名。因为此时 OriginalFirstThunk
为 0,故 Loader 会转而使用 FirstThunk
的值进行解析,而此时 FirstThunk
所指向的 IAT 表存放的是程序先前运行时的 API 实际解析地址,而非 INT 内容,故而该程序的 IAT 内容无法得到正常解析。
这种情况下就必须得对 IAT 进行重建。
这步是可选的,并非所有从 dmp 中提取出来的文件进行 section 修复后都需要进行 IAT 修复。是否需要进行修复,可以简单通过 PE 类查看工具检查导入表解析结果。若存在解析异常的情况,大概率需要进行 IAT 修复。
本小节中,方法 1 将介绍的是脱机/离线的修复场景,即,目标机已经不具备测试和分析条件,我们仅能凭借一开始在目标机上收集的 minidump 文件进行后续的修复和重建。而方法 2 则算是一种在线修复的场景,即目标机尚可访问,且未重启过,无论目标进程是否已退出时的情况。当然了,针对第二种场景,你也可以直接上 Scylla 类 ImpRec 工具进行在线 IAT 修复。
小结就是:
注,并非强调"由于这样的环境限制导致仅能使用方法 n",而是我认为"以这样的环境视角来介绍 2 种方法的差异点,对比效果和使用感受会更好,更直观"。
以 ASPack 加壳后的 NotePad.exe eabfb9aaa4d1adec7c124bd0bda7a81c53249f2bac5743bedf67adf705d0d1f4 为例,介绍 IAT 修复方案。
该示例文件有以下特点:
unmap
方式进行 PE 修复会丢失部分 .text 数据OriginalFirstThunk
字段为 0,从而导致仅通过 realign
方式修复后的 PE 文件 IAT 仍然损坏(区别于前面提到的 xxx.exe 示例)本小节会介绍 2 种方式:
对 section 头的修复方式详见前文(这里采用 realign
模式):
使用
pe_unmapper.exe /in NotePad.virtual.pe 0x400000 /out NotePad.realign.exe /mode r
得到 section realign 后的 PE 文件
此时该 NotePad.realign.exe
文件的 section 信息如下:
由于 OriginalFirstThunk
为 0 且 FirstThunk
指向程序先前被 dump 时的内存残影(IAT),导致此时的 INT/IAT 解析异常。若用 IDA 浏览此时的汇编指令,会呈现如下的代码风格:
这些函数调用其实均来自系统库的函数调用:
方法 1: 纯手工建立导入函数加载地址及其符号名的映射关系,再由 lief 进行 IAT 修复
借助:IDAPython + dumpulator + lief
为此,我们需要先找到所有调用系统函数的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
根据结果,显示共有 138 处系统调用。示例代码输出中的 dInfos 字典格式结构如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
实际存储内容如下:
1 2 3 4 5 |
|
目前缺失从 API 加载地址到其符号名的映射关系。由于我们先前是保存的 minidump 文件,故而该进程运行时的上下文信息均可以从该 minidump 文件中获得,包括 API 地址与其符号名的映射关系。在这里,我们使用 Dumpulator 工具来完成函数地址到函数名的关联映射:
Dumpulator:是一个用以对 minidump 文件进行模拟执行的 Python 库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
由于 dumpulator 目前存在对 kernel32.dll
模块解析异常的 已知 issue,使用上述脚本除 kernel32.dll 中的 API 无法解析外,其他函数均可以建立起加载地址到其符号名的映射关系。
对于 kernel32.dll
模块的函数地址解析,可以手动完成(如,使用 WinDbg 加载 minidump 文件后,ln 0xaabbccdd
获取对应地址处的符号名,或使用扩展脚本自动化完成)。
这步操作最终得到的映射关系如下:
iat_address, func_address, func_name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
至此,已经知道了 PE 文件所使用的导入函数、导入函数被索引/调用的地方,那么接下里就可以基于上述信息对 IAT 表进行重建。
此处,使用 lief 库进行 PE 文件的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
输出物即为最终修复后的程序,可以使用 IDA、调试器进行动静态分析。需要注意,整套流程下来仍不能确保所有功能点都正常可用,如果在某些待测试点上出现运行或分析异常,可进一步围绕该点进行修复。
方法 2:ApiScout 获取导入函数加载地址及其符号名映射关系,,再由 lief 进行 IAT 修复
借助:ApiScout + lief
你可能会想,minidump 文件中所记录的各函数的运行时加载地址得和当时的环境相匹配才行吧,不一致的话怎么能继续往后修复?没错,这里我们必须得采集到目标环境运行时的环境"快照"信息(确切地说,是彼时彼刻在目标环境上运行的各进程中,系统函数库中各函数的加载地址信息)。
首先,我们使用 ApiScout 来生成一份目标环境函数加载地址的快照信息:
apiscout\db_builder> python DatabaseBuilder.py --auto
即可生成一份包含如下信息的 json 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
由于 Windows 系统 ASLR 的存在,系统重启过就会变更新的加载位置,因此这份文件可以说是专机专用,具有一定的"时效性"。
接下来将该份环境上下文信息应用到我们先前已经修复过 section 头的 PE 文件上,以得到该 PE 文件中的函数调用关系:
python scout.py \"X:\NotePad.demo.exe\" 10.0_filtered.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
其中,关于 ApiVector/ApiQR 的部分,可以移步 malpedia(示例) 进行了解。
上述输出中:
call [iat_address]
中 iat_address
的值iat_address
地址处存放的函数实际加载地址值,即 d/qword [iat_address]
x
xref 查看到的引用次数这里需要注意,offset 一列默认使用的是 RVA,如果运行时添加 -b 0x400000
选项,则可以输出 VA 效果,如下:
1 2 3 4 5 |
|
另外需要注意一点的是,------
以下解析出来的内容,有可能并没有真正被引用到:
如上图中的 idx:139,该 API 加载地址为 0x72005000,该地址被解析出存在一处 iat_address 0xa0cf(RVA) 的引用:
但其实该所谓的 0xa0cf iat_address 未被任何地方引用,造成这一问题的原因是由于前一步生成的函数地址快照中 0x72005000 的确是一函数入口, ApiScout 在扫描到 0x72005000 数据时认为当前偏移处就是一处 iat_address 引用,故而当做成了函数调用。
我们可以在命令行参数中增加一个 -o output.json
选项,来进一步过滤这些干扰信息,同时也将上述解析到的映射关系进行重组,以方便后续代码编写:
python scout.py \"X:\NotePad.demo.exe\" 10.0_filtered.json -o NotePad.demo.apis.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
|
该 json 输出中把我们在方法 1 中结合使用 IDAPython + dumpulator 所建立的映射关系直接呈现了出来。同时还把之前提到的实际未被引用的 API 也给标记了出来,如上图中 references
表示实际被应用的位置(即,call/jmp [iat_address]
指令的起始位置),可以看到,RasSignalMonitorThreadExit
等函数实际并未使用,references
列表为空。
最后,仍使用 lief 对 PE 文件进行修复,区别于方法 1 的地方在于,dInfos
变量的内容我们通过 json.load()
从外部导入前述生成的 json 文件即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
对比方法 1 开头部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
方法 2 小结:
修复策略:
DatabaseBuilder.py
工具一并采集好目标环境上各系统库的加载信息,以便分析人员后续进行数据修复时使用修复效果:
[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~
最后于 5天前 被renzhexigua编辑 ,原因: