我可以静态链接(而不是导入)Windows 系统 DLL 吗?

逆向工程 视窗 dll 编译器 符号 动态链接
2021-06-17 01:29:23

我在 VS2010 控制台项目中编译了以下 C 源代码。

#include <stdio.h>
int main(int argc, char* argv[]){
    printf("hello world\n");
    return 0;
}

然后我使用/MT发布模式选项来静态链接 C 运行时库。但是,据我所知,C-runtime 库仍然会调用较低级别的系统函数——例如,C-runtime 函数printf最终会调用WriteFileWindows API。

而 的实际函数体WriteFilekernel32.dll. 因此,即使我静态链接 C 运行时库,二进制文件也不包含整个例程,包括SYSENTER, 或INT 0x2E指令...核心部分仍然在 DLL 中。下图描述了我的理解:

在此处输入图片说明

我想要的是将所有内容静态链接到单个 EXE 文件中。包括kernel32.dll,user32.dll以消除加载程序解析 IAT 和解析函数名称的必要性。

下图描述了我想要的:

在此处输入图片说明

我知道这在 Linux 中很简单gcc我所要做的就是给出选择-static

VS2010 中是否有这样的选项?如果我误解了,请纠正我。

4个回答

首先让我告诉您,您想要的东西是不可能的,因为众所周知的 DLL 是如何工作的。您可以尝试使用诸如PEBundledllpackager 之类的工具进行类似的操作,但是通常(我肯定会说)会因众所周知的 DLL(例如系统 DLL 甚至 MSVC 运行时 DLL 的不同版本)而失败。请参阅有关知名 DLL 的相关性和含义的thisthis

kernel32.dll在 Win32 子系统中起着非常特殊的作用,它有助于向子系统 ( csrss.exe)注册 Win32 线程和进程


从 OP 对该问题的评论中回答部分:

事实上,我并不是在寻找性能优势。我想,通过这种方式,我可以像 Linux 剥离的二进制文件一样删除每个符号,并使反转更难。

那样做是没有意义的。您仍然只能导入单个函数并使用复杂的方式导入 DLL 和/或解析函数。即隐藏您正在从哪些 DLL 导入哪些函数。在黑客圈中比较流行的一件事是散列导出的函数名称,然后自己遍历加载图像的导出,散列找到的每个函数名称并与已知的散列值进行比较。

这是一篇关于用于您想要的方法的好论文,因为 shell 代码不知道在被劫持的进程中导入的函数地址。

正如 Igor 指出的那样,kernel32.dll将被加载到进程中,AFAIR 的顺序也随着 Vista 发生了变化(以前ntdll.dllPEB的 DLL 列表中的第一个,又名LoaderData)。所以确切的方法已经在上面的论文中列出了。

还有几点:

  1. 如果您不想使用LoadLibrary(或其ntdll.dll对应物)动态加载 DLL,您可以在 IAT 中保留对单个导入函数的引用 - 这就是一些可执行加壳程序的做法。
  2. 如果没有,请从解析开始LoadLibraryA,加载您想要的 DLL,然后使用解析的GetProcAddress(或您自己的方法已经使用kernel32.dll并在论文中概述)加载更多功能。
  3. 你可能会让你的生活变得更艰难,而对熟练/经验丰富的逆向工程师来说却没有明显地困难。他们中的大多数人都会看到类似的方案;) ...动态分析将很容易揭示您的技巧并使逆向工程师能够解决它们。

作为替代方案,您可以通过编写一个简化的反汇编程序来求助于系统调用号,该程序能够将索引挑选到 SSDT(系统服务描述符表)中,然后您自己完成其余的工作。这在很久以前就已记录在案,因为当人们想从内核模式驱动程序中挂钩它时,人们习惯于在 SSDT 中查找索引。粗略地说,如果您有指向ntdll.dll需要 SSDT 索引的函数的指针,您将检查您的假设,然后检索适当的值。在 Windows NT 4 到 2003(32 位)中,这看起来像

  B8 ?? ?? ?? ??

其中B8是 for mov eax, ????????,问号是 SSDT 的索引。所以在检查之后B8你会跳过它并获取下一个 DWORD。C 代码示例:

if ((lpAddr) && *((unsigned char *)lpAddr) == 0xB8)
{
    result = *((ULONG *)((unsigned char *)lpAddr+1));
}

不同的操作系统版本和位数会有所不同 - 您已被警告。

但我没有看到任何优势——无论是在性能方面还是在阻止逆向工程努力方面。

与 Linux 或 OS X 不同,Windows 内核不使用跨版本一致的系统调用编号。即使在 Servicepack 发布之后,这些数字也可能会发生变化。例如,NtReadFile系统调用0x0086在 Windows NT 4 上,但在 Windows 7 上是0x0111(请参阅此处获取完整列表)。这就是为什么所有适当的程序都使用kernel32.dll(或ntdll.dll)来执行实际调用的原因——这些 DLL 保证使用与内核匹配的系统调用号。

顺便说一下,如果不在kernel32.dllIAT 中列出,您将不会保存任何内容- 它总是由系统加载程序(从 Windows 2000 IIRC 开始)映射到 Win32 进程中。

可能出于 MS 的全部意图,不仅系统调用编号在版本之间发生变化,而且许多 DLL 序号值也会发生变化。如果您希望您的代码跨操作系统版本工作,则需要绑定到 Win32 并使用完整的函数名称。

其他人已经提到了这样做的缺点,但是如果您仍然对这条路径感兴趣,那么这里有一个将 ntdll API 名称转换为系统调用的库。