[原创]进程 Dump & PE unpacking & IAT 修复 - Windows 篇
2022-9-23 01:23:13 Author: bbs.pediy.com(查看原文) 阅读量:8 收藏

原始磁盘文件已删除,但进程尚在,需要想办法从进程中恢复原始程序以便后续分析

关键词:样本应急脱壳PE 修复IAT 重建

TL;DR 大致思路和步骤如下:

  1. 使用工具、软件等手段,创建目标进程的 minidump 文件
  2. 通过 WinDbg 从 dmp 文件中提取出程序在内存中展开后的状态,以及其他上下文信息(可选 ApiScout 进行辅助)
  3. 使用 PE-bear 或者 pe_unmapper 对上一步提取出来的文件进行修正(可选 LIEF 进行辅助)

ApiScout:对目标所在的 OS 运行时环境(系统 API 加载地址和其符号名映射关系等)进行快照,以便后期对目标环境所加载的库函数 API 地址和符号名等进行检索和查询(如目标环境开启了 ASLR)。还支持对 PE/dump 等文件基于前述收集到的系统 API 信息,进行系统 API 调用的定位和解析,便于快速确认该 PE 类文件实际所使用的系统函数情况及其在代码段中的调用/引用位置,为接下来的 IAT 重建提供参考信息。


LIEF:一套抽象了 PE/ELF/MachO/OAT/DEX/VDEX/ART 等不同平台可执行文件格式的解析库、编辑库,此处用其进行 IAT 重建,对 PE 文件直接进行二进制层面的 patch。

处理此类场景的 Workflow 如下(SVG 原图见附件):

1. 创建 minidmp

  • Open TaskManager with admin privilege, select the target process, right click context-menu and select Create dump file. 任务管理器右键创建 dump 文件
  • Use ProcessHacker. ProcessHacker 右键创建 dump 文件
  • Use x64dbg plugin MiniDumpPlugin
  • Use pe-sieve with /minidmp option(需注意,pe-sieve 只会在检测到程序存在 path/hook 等 inconsistent 场景时才会 dump suspicious staff,在此前提下 /minidmp 才会起效。若程序本身较为简单,没检测到 suspicious,即使加了 /minidmp 也不会有任何输出)
  • ...等

此步也可以直接使用 ProcessHacker 等工具对进程的 Memory region 进行转储,这样转储出来的内容即是 PE 文件在内存中展开后的样子,但不包含进程运行环境等上下文状态信息

2. 从 dmp 中提取内存程序

WinDbg 从内存中提取展开后的程序时,可能用到的命令/操作(供参考)

1

2

3

4

5

6

7

8

> lm

> !address

> .writemem /path/to/dumpfile.dmp StartAddress Range

> ln address

3. 修复 section 头

背景介绍: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

0:000> lmDvm112_http_stager

Browse full module list

start    end        module name

00400000 00409000   112_http_stager   (no symbols)          

    Loaded symbol image file: 112_http_stager.exe

    Image path: X:\unpack\112_http_stager.exe

    Image name: 112_http_stager.exe

    Browse all global symbols  functions  data

    Timestamp:        Tue Jun  9 08:17:15 2020 (5EDED50B)

    CheckSum:         00010C4A

    ImageSize:        00009000

    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4

    Information from resource tables:

并且已经将该 0x9000 大小的内存区域转储至磁盘 dump.dmp 文件中:

.writemem /path/to/dump.dmp 00400000 L9000

接下来,需要对 dump.dmp 进行修复。该 dump.dmp 在 PE-bear 中的解析结果如下图所示:

上图中 IAT 视图的字段显示红色解析异常,表明 IAT 表结构无法正常解析,需要进行修复。

3.1 pe_unmapper 工具法

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

2022-09-13  10:40 PM            14,336 112_http_stager.exe      <== 展开前/修复后的文件 1

2022-09-13  05:38 PM        52,110,299 112_http_stager.exe.dmp

2022-09-13  06:02 PM            36,864 dump.mem                 <== 展开后/修复前从 dmp 中提取的文件

可以看到,文件大小发生了变化,变小了

与此同时,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

2022-09-13  10:40 PM            14,336 112_http_stager.exe          <== 修复后的文件 1

2022-09-13  05:38 PM        52,110,299 112_http_stager.exe.dmp

2022-09-13  10:54 PM            36,864 112_http_stager_mode_r.exe   <== 修复后的文件 2

2022-09-13  06:02 PM            36,864 dump.mem                     <== 展开后/修复前从 dmp 中提取的文件

文件大小并没有发生变化,均为 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,也能解决问题。此时,程序可正常运行启动。

3.2 手工 realign 法

技巧点:将各 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 进行重建。

4. 修复 IAT

这步是可选的,并非所有从 dmp 中提取出来的文件进行 section 修复后都需要进行 IAT 修复。是否需要进行修复,可以简单通过 PE 类查看工具检查导入表解析结果。若存在解析异常的情况,大概率需要进行 IAT 修复。

本小节中,方法 1 将介绍的是脱机/离线的修复场景,即,目标机已经不具备测试和分析条件,我们仅能凭借一开始在目标机上收集的 minidump 文件进行后续的修复和重建。而方法 2 则算是一种在线修复的场景,即目标机尚可访问,且未重启过,无论目标进程是否已退出时的情况。当然了,针对第二种场景,你也可以直接上 Scylla 类 ImpRec 工具进行在线 IAT 修复。

小结就是:

  • 介绍方法 1 时,所使用的示例场景 + 限制条件为:仅有/通过 mindump 文件来完成 IAT 修复
  • 介绍方法 2 时,所使用的示例场景 + 限制条件为:mindiump + 目标机尚可访问且未曾重启

注,并非强调"由于这样的环境限制导致仅能使用方法 n",而是我认为"以这样的环境视角来介绍 2 种方法的差异点,对比效果和使用感受会更好,更直观"。

以 ASPack 加壳后的 NotePad.exe eabfb9aaa4d1adec7c124bd0bda7a81c53249f2bac5743bedf67adf705d0d1f4 为例,介绍 IAT 修复方案。

该示例文件有以下特点:

  1. 部分 section 会在运行过程中展开,从而导致如果使用 unmap 方式进行 PE 修复会丢失部分 .text 数据
  2. OriginalFirstThunk 字段为 0,从而导致仅通过 realign 方式修复后的 PE 文件 IAT 仍然损坏(区别于前面提到的 xxx.exe 示例)

本小节会介绍 2 种方式:

  1. 方法 1:纯手工进行导入函数的引用查找,对应导入函数的符号解析及最后 PE 头文件中 IAT 的 patch
  2. 方法 2:借助 ApiScout 获取目标 dump 文件对导入函数的调用信息,利用该信息直接对 PE 中的 IAT 进行 patch

对 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 浏览此时的汇编指令,会呈现如下的代码风格:

这些函数调用其实均来自系统库的函数调用:

4.1 手动修复

方法 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

import idc

import ida_xref

import idautils

import ida_bytes

segm_start = 0x406000

segm_end = 0x407000

dInfos = dict()

for ea in range(segm_start, segm_end, 4):

    val = ida_bytes.get_dword(ea)

    for ref in idautils.XrefsTo(ea, ida_xref.XREF_DATA):

        if ref.type in [idc.dr_R]:

            entry = dInfos.setdefault(ea, {'val': val, 'refs': set()})

            entry['refs'].add(ref.frm)

lInfos = []

for k, v in dInfos.items():

    lInfos.append((k, v['val']))

print(dInfos)

根据结果,显示共有 138 处系统调用。示例代码输出中的 dInfos 字典格式结构如下:

1

2

3

4

5

6

7

8

9

10

11

{

    IAT1_VA: {

        'val': export_func_addr,

        'refs': set(call_insn_addrs),

        'name': export_func_name

    },

    IAT2_VA: {

    ...snip...

    },

    ...snip...

}

实际存储内容如下:

1

2

3

4

5

{  

    4219616: {'refs': {4203561, 4203598}, 'val': 1993408960},

    4219620: {'refs': {4203732, 4203654}, 'val': 1993402960}

    ...snip...

}

目前缺失从 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

from dumpulator import Dumpulator

dp = Dumpulator('NotePad.exe.dmp', quiet=True)

lInfos = [(4219616, 1993408960), (4219620, 1993402960), ...snip...]

lInfoWithSymbols1 = []

for entry in lInfos:

    iat_addr, func_addr = entry

    try:

        name = dp.exports[func_addr]

        lInfoWithSymbols1.append((iat_addr, func_addr, name))

    except:

        print(f'{iat_addr},{func_addr}, cannot parse address')

        continue

print(lInfoWithSymbols)

由于 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

[(4219616, 1993408960, 'advapi32.dll:RegSetValueExA'),

 ...snip...

 (4219628, 1993408992, 'advapi32.dll:RegOpenKeyA'),

 (4219640, 1993104976, 'gdi32.dll:GetStockObject'),

 (4219644, 1993096672, 'gdi32.dll:GetObjectA'),

 (4219820, 2004124400, 'ntdll.dll:RtlMoveMemory'),

 ...snip...

 (4219904, 1978456096, 'shell32.dll:DragQueryFileA'),

 (4219908, 1978456080, 'shell32.dll:DragFinish'),

 ...snip...

 (4220040, 1984116832, 'user32.dll:CharNextA'),

 (4220044, 1984147904, 'user32.dll:IsDialogMessageA'),

 ...snip...

 (4219816, 1994756464, 'kernel32.dll:GetProfileStringA'),

 ...snip...

 (4219872, 1994792672, 'kernel32.dll:GetCommandLineA')]

至此,已经知道了 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

import lief

input_path = "NotePad.demo.exe"

artifact_path = "NotePad.demo.fixed.exe"

binary = lief.parse(input_path)

dInfos = {

    4219616: {'refs': {4203561, 4203598}, 'val': 1993408960, 'name': 'advapi32.dll:RegSetValueExA'},

    4219620: {'refs': {4203732, 4203654}, 'val': 1993402960, 'name': 'advapi32.dll:RegQueryValueExA'},

    4219624: {'refs': {4204169}, 'val': 1993403168, 'name': 'advapi32.dll:RegCloseKey'},

    ...snip...

}

for info in dInfos.values():

    symbol_name = info['name']

    lib_name, entry_name = symbol_name.split(':')

    lib = binary.get_import(lib_name)

    if not lib:

        lib = binary.add_library(lib_name)

    entry = lib.get_entry(entry_name)

    if not entry:

        entry = lib.add_entry(entry_name)

imagebase = binary.optional_header.imagebase

for info in dInfos.values():

    symbol_name = info['name']

    lib_name, entry_name = symbol_name.split(':')

    iat_addr_va = imagebase + binary.predict_function_rva(lib_name, entry_name)

    for insn_va in info['refs']:

        binary.patch_address(insn_va + 2, iat_addr_va, 4, binary.VA_TYPES.VA)

binary.optional_header.addressof_entrypoint = 0x10CC

builder = lief.PE.Builder(binary)

builder.build_imports(True)

builder.patch_imports(True)

builder.build()

builder.write(artifact_path)

输出物即为最终修复后的程序,可以使用 IDA、调试器进行动静态分析。需要注意,整套流程下来仍不能确保所有功能点都正常可用,如果在某些待测试点上出现运行或分析异常,可进一步围绕该点进行修复。

4.2 ApiScout 修复

方法 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

{

 "aslr_offsets": true,

 "crawled_paths": [

  "C:\\Windows\\system32",

  "C:\\Windows\\SysWOW64",

  "C:\\Windows\\WinSxS",

  "C:\\Program Files\\Common Files"

 ],

 "dlls": {

  "32_0.0.0.0_ActionCenter.dll_0x10000000": {

   "aslr_offset": -1264844800,

   "base_address": 268435456,

   "bitness": 32,

   "exports": [

    {

     "address": 73712,

     "name": "DllCanUnloadNow",

     "ordinal": 1

    },

    {

     "address": 98864,

     "name": "DllGetClassObject",

     "ordinal": 2

    }

   ],

   "filepath": "C:\\Windows\\SysWOW64\\ActionCenter.dll",

   "version": "0.0.0.0"

  },

  "32_0.0.0.0_AudioSes.dll_0x10000000": {

   "aslr_offset": -1283457024,

   "base_address": 268435456,

   "bitness": 32,

   "exports": [

    {

     "address": 200912,

     "name": "DllGetClassObject",

     "ordinal": 9

    },

    ...snip...

  }

}

由于 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

idx: offset    ; VA                ; IT?;

  1: 0x000062e0;         0x76d101c0; err;    3; advapi32.dll_0x4c300000 (32bit)         ; RegSetValueExA

  2: 0x000062e4;         0x76d0ea50; err;    3; advapi32.dll_0x4c300000 (32bit)         ; RegQueryValueExA

  3: 0x000062e8;         0x76d0eb20; err;    2; advapi32.dll_0x4c300000 (32bit)         ; RegCloseKey

  4: 0x000062ec;         0x76d101e0; err;    2; advapi32.dll_0x4c300000 (32bit)         ; RegOpenKeyA

  5: 0x000062f0;         0x76d13190; err;    2; advapi32.dll_0x4c300000 (32bit)         ; RegCreateKeyA

  6: 0x000062f8;         0x76cc5e50; err;    2; gdi32.dll_0x4d500000 (32bit)            ; GetStockObject

  ...snip...

155: 0x0000e35d;         0x72005000; err;    1; rasman.dll_0x10000000 (32bit)           ; RasSignalMonitorThreadExit

---------------------------------------------------------------------------------------------------------------------------------

156: 0x0000e3d1;         0x72005000; err;    1; rasman.dll_0x10000000 (32bit)           ; RasSignalMonitorThreadExit

DLLs: 8, APIs: 145, references: 389

WinApi1024 Vector Results:

Windows 10  (AMD64): 63 / 145 (43.45%) APIs covered in WinApi1024 vector.

    Vector:     A67EAAIAQA5BA8CAQEA8CAAQAAQACA5EAHA+A4HAMA3QAABAwA3IgCggIEAAEDABgEGKIOKgA3CIgEiJgEBg

    Confidence: 89.97354108424373

其中,关于 ApiVector/ApiQR 的部分,可以移步 malpedia(示例) 进行了解。

上述输出中:

  • idx: 表示导入函数的 id(方便查看该 PE 共引用了多少个 API)
  • offset: iat_address 位置,即汇编指令 call [iat_address]iat_address 的值
  • VA:表示在该 dmp 文件中,iat_address 地址处存放的函数实际加载地址值,即 d/qword [iat_address]
  • IT?:PE 文件的 IAT 中是否原本就带有该 API
  • #ref:该函数在 PE 文件中被调用/call 的次数,也即在 IDA 中通过 x xref 查看到的引用次数
  • DLL:库函数所在的 DLL 模块在该 dmp 文件中的加载基址
  • API:函数名

这里需要注意,offset 一列默认使用的是 RVA,如果运行时添加 -b 0x400000 选项,则可以输出 VA 效果,如下:

1

2

3

4

5

idx: offset    ; VA                ; IT?;

  1: 0x004062e0;         0x76d101c0; err;    3; advapi32.dll_0x4c300000 (32bit)         ; RegSetValueExA

  2: 0x004062e4;         0x76d0ea50; err;    3; advapi32.dll_0x4c300000 (32bit)         ; RegQueryValueExA

  3: 0x004062e8;         0x76d0eb20; err;    2; advapi32.dll_0x4c300000 (32bit)         ; RegCloseKey

  4: 0x004062ec;         0x76d101e0; err;    2; advapi32.dll_0x4c300000 (32bit)         ; RegOpenKeyA

另外需要注意一点的是,------ 以下解析出来的内容,有可能并没有真正被引用到:

如上图中的 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

[

    {

        "offset": 25312,

        "apiAddress": 1993408960,

        "dll": "advapi32.dll(32 bit)",

        "api": "RegSetValueExA",

        "references": [

            9257,

            9294

        ]

    },

    {

        "offset": 25444,

        "apiAddress": 1995044640,

        "dll": "kernel32.dll(32 bit)",

        "api": "_lwrite",

        "references": [

            13005,

            13092

        ]

    },

    {

        "offset": 25448,

        "apiAddress": 1994786592,

        "dll": "kernel32.dll(32 bit)",

        "api": "LocalUnlock",

        "references": [

            13022,

            13174,

            13699,

            13794,

            13906,

            14657,

            15018,

            15062,

            15083,

            16643,

            17155

        ]

    },

    ...snip...

    {

        "offset": 57483,

        "apiAddress": 1991430304,

        "dll": "comdlg32.dll(32 bit)",

        "api": "GetOpenFileNameA",

        "references": []

    },

    {

        "offset": 57491,

        "apiAddress": 1993408960,

        "dll": "advapi32.dll(32 bit)",

        "api": "RegSetValueExA",

        "references": []

    },

    {

        "offset": 58205,

        "apiAddress": 1912623104,

        "dll": "rasman.dll(32 bit)",

        "api": "RasSignalMonitorThreadExit",

        "references": []

    },

    {

        "offset": 58321,

        "apiAddress": 1912623104,

        "dll": "rasman.dll(32 bit)",

        "api": "RasSignalMonitorThreadExit",

        "references": []

    }

]

该 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

import json

import lief

input_path = "NotePad.demo.exe"

artifact_path = "NotePad.demo.fixed.exe"

binary = lief.parse(input_path)

api_path = "api.json"

dInfos = None

with open(api_path, 'r') as f:

    dInfos = json.load(f)

for info in dInfos:

...snip...

对比方法 1 开头部分:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

import lief

input_path = "NotePad.demo.exe"

artifact_path = "NotePad.demo.fixed.exe"

binary = lief.parse(input_path)

dInfos = {

    4219616: {'refs': {4203561, 4203598}, 'val': 1993408960, 'name': 'advapi32.dll:RegSetValueExA'},

    4219620: {'refs': {4203732, 4203654}, 'val': 1993402960, 'name': 'advapi32.dll:RegQueryValueExA'},

    4219624: {'refs': {4204169}, 'val': 1993403168, 'name': 'advapi32.dll:RegCloseKey'},

    4219628: {'refs': {4204221}, 'val': 1993408992, 'name': 'advapi32.dll:RegOpenKeyA'},

    ...snip...

}

for info in dInfos:

...snip...

方法 2 小结:

  1. (ApiScout) 使用 python DatabaseBuilder.py --auto 在目标机器(未重启)上采集运行环境的上下文状态信息
  2. (ApiScout) 使用 python scout.py /path/to/binary /path/to/db -o iats.json 对修复后(section 修复后)的 PE 文件进行解析,以获取函数调用关系及映射信息
  3. (lief) 借助 lief 库对 PE 文件进行 IAT 重建

修复策略:

  1. 转储进程映像时,既可以考虑创建 minidump 文件,也可以考虑仅保存相关 memory region。后者仅包含目标 PE 相关内容,前者在此基础上还会包含额外的运行时环境上下文信息。推荐采用 minidump 转储,以备不时之需
  2. 如果目标环境允许,可以在创建转储文件的同时,使用 ApiScout 的 DatabaseBuilder.py 工具一并采集好目标环境上各系统库的加载信息,以便分析人员后续进行数据修复时使用
  3. 修复 section 时,使用 unmap/raw 模式还是 realign 模式,需要结合你 EP 设定的位置视情况而定,没有统一标准
    1. 使用 unmapped(Virtual to Raw) 模式进行 PE 修正,有可能会丢失部分 section/segment 数据
    2. 使用 Realign(Virtual to Raw + Raw == Virtual)模式进行 PE 修正,有可能会把运行时的内存残影保留下来,造成数据污染,影响修正后程序的运行
  4. 是否需要进一步修复 IAT 需根据前一步修复完 section 后的 IAT/INT 解析结果而定;如果 IAT/INT 解析异常可尝试进行 IAT 修复,可进一步提高反汇编、反编译的"渲染"效果

修复效果:

  1. PE 修复的投入产出比和修复程度,视具体情况和你的选择而定,没必要追求完美一定要达到能够运行/动态调试或最优 IDA 反编译效果的地步。有时能恢复到某种静态可分析的程度或许就能满足当下的需求,可以解决问题了
  2. 本文介绍的思路和方法,可保证静态分析不会有太大问题,至于修复后程序的实际运行效果以及部分功能点是否存在异常,可能仍需要进行进一步的测试和微调

[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 5天前 被renzhexigua编辑 ,原因:


文章来源: https://bbs.pediy.com/thread-274505.htm
如有侵权请联系:admin#unsafe.sh