我将如何重建打包可执行文件的 IAT?

信息安全 视窗 恶意软件 逆向工程
2021-08-22 15:54:06

当可执行文件使用 UPX 等工具打包时,实际代码和数据部分会被加密或混淆,然后使用注入的解密器存根加载到内存中。这使得静态分析成为不可能。

为了避免这种情况,我通常会运行可执行文件,附加调试器,进行内存转储,然后使用该转储生成解压缩的可执行文件。不幸的是,这会破坏导入地址表 (IAT)。

我知道某些可用于修补 IAT 的工具,但我不知道它们在内部是如何工作的。我将如何手动重新创建 IAT?

2个回答

好的,我说过我会寻求答案,正如承诺的那样,它就在这里。

首先,我想建立一个实际的目标来玩。没有什么能比得上一个有效的、可触摸的示例,而且这在很大程度上是一个手动过程。所以,事不宜迟,我在 MSVC 中编译了这个/MT

#define WINVER 0x501
#define _WIN32_WINNT   0x0501
#define _WIN32_WINDOWS 0x0501
#define _WIN32_IE      0x0501
#define UNICODE

#include <wchar.h>
#include <windows.h>

int APIENTRY wWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    MessageBox(NULL, L"Hello, world.", L"A messagebox", MB_OK | MB_ICONEXCLAMATION);
    return 0;
}

我们完全希望这个野兽能够进口user32.dll!MessageBoxW,而且确实如此。

要在可执行文件本身中找到它,让我们使用 WinDbg。我使用 WinDbg 有两个原因:首先,我买不起 IDA,其次,它与您用于内核驱动程序的调试器相同(即使使用 IDA - 它使用它的引擎),所以它很好,如果有点不直观。

我将我的可执行文件编译UnpackSimple.exe为 x86 架构,因为 UPX 不支持 Win64。现在,当您在 WinDbg 下运行它时,您首先会注意到这一行:

ModLoad: 01360000 01370000   UnpackSimple.exe

在那里,我们看到了UnpackSimple.exe映射到的地址范围这很重要,因为其标头中的所有地址都相对于01360000.

所以现在,要查找头信息,我们可以简单地请求它:

!dh UnpackSimple.exe
OPTIONAL HEADER VALUES

    10B magic #
   11.00 linker version
    5E00 size of code
    6C00 size of initialized data
       0 size of uninitialized data
    1193 address of entry point
    1000 base of code
         ----- new -----
01360000 image base            <--- this is the image base address.
    1000 section alignment
     200 file alignment
       2 subsystem (Windows GUI)
    6.00 operating system version
    0.00 image version

    ... blah blah blah ....

   0 [       0] address [size] of Export Directory
8D54 [      3C] address [size] of Import Directory
D000 [     1E0] address [size] of Resource Directory
   0 [       0] address [size] of Exception Directory
   0 [       0] address [size] of Security Directory
E000 [     6D0] address [size] of Base Relocation Directory
7150 [      38] address [size] of Debug Directory
   0 [       0] address [size] of Description Directory
   0 [       0] address [size] of Special Directory
   0 [       0] address [size] of Thread Storage Directory
89D8 [      40] address [size] of Load Configuration Directory
   0 [       0] address [size] of Bound Import Directory
7000 [     108] address [size] of Import Address Table Directory
   0 [       0] address [size] of Delay Import Directory
   0 [       0] address [size] of COR20 Header Directory
   0 [       0] address [size] of Reserved Directory

哦,看,一个 IAT!它存在01360000+7000并且我们可以说服 WinDbg 以相当直接的方式向我们提供这些信息。dps我们将要使用命令是 的缩写dump pointers, dereferencing with symbols,它需要一个参数地址和每个入口地址的空格。

dps 01360000+7000 L108/4
01367000  75b3d7ea kernel32!TerminateProcessStub
01367004  75b23f3c kernel32!CreateFileWImplementation
01367008  75b2520b kernel32!GetCommandLineWStub
... <snip> ...
013670f4  75b47ab2 kernel32!WriteConsoleW
013670f8  75b21410 kernel32!CloseHandleImplementation
013670fc  00000000
01367100  76bafd3f USER32!MessageBoxW
01367104  00000000

您可以在此处查看 IAT 的一些重要功能。首先是左侧的地址——IAT 入口地址。左边的地址是 DLL 的内存地址。最后,请注意有一些零条目。每个 DLL 导入都以零条目结束,告诉加载程序没有更多条目要加载。

现在,我们来看看 main 函数的反汇编。为此,请键入这两个命令

bp UnpackSimple!wWinMain
g

然后,您将在进入wWinMain上述内容时中断。现在,如果你仔细观察:

UnpackSimple!wWinMain:
    01361000 6a30            push    30h
    01361002 68a0893601      push    offset UnpackSimple!`string' (013689a0)
    01361007 68bc893601      push    offset UnpackSimple!`string' (013689bc)
    0136100c 6a00            push    0
    0136100e ff1500713601    call    dword ptr [UnpackSimple!_imp__MessageBoxW (01367100)]
    01361014 33c0            xor     eax,eax
    01361016 c21000          ret     10h

不出所料,我们的函数 ,_imp__MessageBoxW引用了一个不错的 IAT 条目 :)

所以现在的问题是我们到底要如何倒退?好吧,首先要构建一些不起作用的东西,我们需要获取一个打包的可执行文件。

为此,我跑去upx.exe -o UnpackSimplePacked.exe UnpackSimple.exe生成一个很好的打包二进制文件。在 WinDbg 中加载这个二进制文件,你会注意到的第一件事是:

!dh UnpackSimplePacked.exe

绝对什么都不做。但是,我们可以注意到:

ModLoad: 01220000 01233000   image01220000

已经看到了——所以我们需要的图像就在那里,在内存中。它只是不作为磁盘映像存在。在这个阶段,我猜测upx实际上是直接加载 DLL,而不是使用 NT Loader。我们可以通过两种方式证明这一点:首先,如果您输入sxe ldWinDbg,它将在所有模块加载时中断。没有发生中断user32.dll,也没有加载。

其次,正如你想看到的:

    0 [       0] address [size] of Import Address Table Directory

它完全是空的。

但是,如果我们检查加载的模块列表:

lm
start    end        module name
00210000 00223000   image00210000   (deferred)             
74c20000 74c2c000   CRYPTBASE   (deferred)             
74c30000 74c90000   SspiCli    (deferred)             
74d80000 74e20000   ADVAPI32   (deferred)             
74eb0000 74f4d000   USP10      (deferred)             
74f50000 74f5a000   LPK        (deferred)             
75030000 750dc000   msvcrt     (deferred)             
755a0000 755b9000   sechost    (deferred)             
75720000 757b0000   GDI32      (deferred)             
757e0000 758d0000   RPCRT4     (deferred)             
75b10000 75c20000   kernel32   (deferred)             
76ab0000 76af7000   KERNELBASE   (deferred)             
76b40000 76c40000   USER32     (deferred)             
77550000 776d0000   ntdll      (pdb symbols) 

如您所见,USER32已分配空间准备就绪。

现在这里有一些诡计。我用我的 UnpackSimple.exe 版本打开了另一个 WinDbg 窗口,并找到了user32!MessageBoxW. 得到这个后,我发现它76bafd3f来自76b40000给我一个偏移量的基本偏移量6FD3F在我的打包版本中,我设置了一个断点bp 76b40000+6FD3F,果然,有一个堆栈跟踪!

k
003cfd0c 013a1014 USER32!MessageBoxW
WARNING: Stack unwind information not available. Following frames may be wrong.
003cfd6c 75b233aa image013a0000+0x1014
003cfd78 77589ef2 kernel32!BaseThreadInitThunk+0xe
003cfdb8 77589ec5 ntdll!__RtlUserThreadStart+0x70
003cfdd0 00000000 ntdll!_RtlUserThreadStart+0x1b

中位置的反汇编image0...如下所示:

013a100e ff1500713a01    call    dword ptr [image013a0000+0x7100 (013a7100)]
013a1014 33c0            xor     eax,eax
013a1016 c21000          ret     10h

现在我们到了某个地方!在那个地址,你会发现一些有趣的数据:

db 013a7100
013a7100  3f fd ba 76 00 00 00

有趣的是,这是我们的地址!76bafd3f在好旧的小端。让我们退后一步:

db 013a7098
013a7098  a0 22 57 77 60 22 57 77-c9 14 b2 75 ff 10 b2 75  ."Ww`"Ww...u...u
013a70a8  7b 44 b2 75 9c 17 b2 75-91 d1 b4 75 71 51 b2 75  {D.u...u...uqQ.u
013a70b8  45 49 b2 75 c4 d1 b4 75-13 49 b2 75 b3 d1 b4 75  EI.u...u.I.u...u
013a70c8  46 e0 57 77 f1 24 59 77-0d 17 b2 75 46 19 b2 75  F.Ww.$Yw...uF..u
013a70d8  2a 30 58 77 61 48 ba 75-83 46 b2 75 07 7c bc 75  *0XwaH.u.F.u.|.u
013a70e8  28 13 b2 75 bf 45 ba 75-ef c7 b3 75 b2 7a b4 75  (..u.E.u...u.z.u
013a70f8  10 14 b2 75 00 00 00 00-3f fd ba 76 00 00 00 00  ...u....?..v....

我看到了指针!

这是我们之前发现的 IAT,或者至少是一组IMAGE_IMPORT_DESCRIPTORthunk。

走到这一步,还剩下什么?好吧,我们需要搜索转储的内存以尝试找到与已知 Windows DLL 匹配的函数。从那里,我们可以开始为我们的新 PE 文件重建一个完整的 IAT。我怀疑ImpRec 经历的过程,如 Viv 的回答中所述,是尝试像我手动完成的那样找到这些条目。我选择使用断点,但我想有很多方法可以实现不同的自动化。我从来没有写过一个自动化工具来手动完成这个过程......!也就是说,在猜测call ptr指令时,在映射区域之外查看它们的条目,然后尝试找到那些针对已知 DLL 偏移量的条目就可以了。

两个注意事项:

  1. 我将此答案写为“这是我手动攻击它的方式”类型的答案,具有相关背景,而不是“使用此工具”。
  2. 注意 ASLR。这些模块地址将不断变化,因此请留意模块库的变化!

首先是对pe格式的一般概述,我建议阅读Microsoft给出的pecoff文件格式。导入表被大多数加壳程序部分或全部破坏。Imprec 通常是重建 IAT(导入地址表)的首选,但如果你真的想了解细节,那么你可以阅读这篇优秀的论文——安全评估团队的 Paul Craig在恶意软件中使用的 PE 打包程序。你可以还可以尝试开源工具scylla

在 Windows 可执行文件中,导入表是包含该映像的所有导入信息(Dll 名称和地址空间中引用的函数)的表(数据目录)。

IMAGE_IMPORT_DESCRIPTOR 具有以下格式:

struct IMAGE_IMPORT_DESCRIPTOR {
DWORD   OriginalFirstThunk; //same as FirstThunk
DWORD   TimeDateStamp;
DWORD   ForwarderChain;     //usually set to 0
DWORD   Name;               //name of the dll
DWORD   FirstThunk;         //before loading,contains the pointer to the api name thunk array 
};

每个导入的 DLL 都有一个描述符,最后一个由 NULL 组成。Windows 加载程序将通过查看 stucture 的名称字段来加载可执行文件中引用的所有 DLL(记住每个 DLL 一个 DLL IMAGE_IMPORT_DESCRIPTOR)。然后它尝试以下列方式构建 IAT。对于 指向的数组中的每个名称FirstThunk,它将该地址替换为 api 的实际地址。如果在 中找不到该 api 名称的地址FirstThunk,它将转到OriginalFirstThunk并尝试从那里获取信息(将其视为FirstThunk).覆盖FirstThunk的是你的IAT!(所有这些都发生在指令指针之前,即 eip 到达 exe 的入口点)。

在构建新导入表(或修复旧表)时,Imprec 将首先通过自动搜索代码或通过用户提供的值来查找 IAT。然后从新 IAT 中查找其 API 名称地址并填充. 这简化了我们的工作,因为加载器现在可以找到导入表中的所有信息。这在lena 的教程IMAGE_IMPORT_DESCRIPTORs中得到了很好的解释。21.