Chad Austin

学校不会教你的:在你的进程中禁用 Kernel 函数

Mar 31, 2009 - c++, crashes, imvu, x86, reddit

使用 SetUnhandledExceptionFilter 检测并报告未处理的异常似乎很合理,而且实际上,它确实有效……一段时间。最终,我们开始注意到一些本应作为最后机会异常报告的失败却没有被报告。经过大量调查,我们发现 Direct3DFlash 都安装了自己的未处理异常过滤器!更糟糕的是,它们还在争夺它,每秒安装多次它们自己的处理程序!实际上,这意味着我们的最后机会崩溃报告很少生成,让我们误以为我们的崩溃指标比实际情况要好。(非常糟糕的库!)

我们不得不解决这个问题是很荒谬的,但是,正如 Avery Lee 所说,“仅仅因为这不是你的错,并不意味着这不是你的问题。”

显而易见的解决方案是加入混战,每一帧都调用 SetUnhandledExceptionFilter,对吧?不如我们尝试一些更可靠的方法……我讨厌实现那些有明显缺陷的解决方案。因此,我们选择在安装我们自己的处理程序后立即禁用(通过代码修改)SetUnhandledExceptionFilter 函数。当 Direct3D 和 Flash 尝试调用它时,它们的请求将被忽略,从而保留我们安装的异常处理程序。

代码修改……这不是很可怕吗?只要有 一点 知识 和防御性编程,它并没有那么糟糕。实际上,我将向您展示前面的代码:

// 如果这让你觉得不好理解,跳过代码并回来!
void lockUnhandledExceptionFilter() {
  HMODULE kernel32 = LoadLibraryA("kernel32.dll");
  Assert(kernel32);
  if (FARPROC gpaSetUnhandledExceptionFilter = GetProcAddress(kernel32, "SetUnhandledExceptionFilter")) {
    unsigned char expected_code[] = {
      0x8B, 0xFF, // mov edi,edi
      0x55,    // push ebp
      0x8B, 0xEC, // mov ebp,esp
    };
    // only replace code we expect
    if (memcmp(expected_code, gpaSetUnhandledExceptionFilter, sizeof(expected_code)) == 0) {
      unsigned char new_code[] = {
        0x33, 0xC0,    // xor eax,eax
        0xC2, 0x04, 0x00, // ret 4
      };
      BOOST_STATIC_ASSERT(sizeof(expected_code) == sizeof(new_code));
      DWORD old_protect;
      if (VirtualProtect(gpaSetUnhandledExceptionFilter, sizeof(new_code), PAGE_EXECUTE_READWRITE, &old_protect)) {
        CopyMemory(gpaSetUnhandledExceptionFilter, new_code, sizeof(new_code));
        DWORD dummy;
        VirtualProtect(gpaSetUnhandledExceptionFilter, sizeof(new_code), old_protect, &dummy);
        FlushInstructionCache(GetCurrentProcess(), gpaSetUnhandledExceptionFilter, sizeof(new_code));
      }
    }
  }
  FreeLibrary(kernel32);
}

如果这对您来说显而易见,那就太好了:我们正在 招聘

否则,这是一个概述:

使用 GetProcAddress 获取 SetUnhandledExceptionFilter 的真实地址。(如果您只是键入 &SetUnhandledExceptionFilter,您将获得可重定位的导入 thunk,而不是实际的 SetUnhandledExceptionFilter 函数。)

大多数 Windows 函数以五个字节的序言开始:

mov edi, edi ; 用于热补丁支持的 2 个字节
push ebp   ; 堆栈帧
mov ebp, esp ; 堆栈帧 (con't)

我们要用 return 0; 替换这五个字节。请记住,__stdcall 函数在 eax 寄存器中返回值。我们要用以下代码替换上面的代码:

xor eax, eax ; eax = 0
ret 4    ; 弹出 4 个字节(参数)并返回

也是五个字节!多么方便!在替换序言之前,我们验证前五个字节是否符合我们的预期。(如果不是,我们不能对代码替换的效果感到满意。)VirtualProtectFlushInstructionCache 调用是代码修改的标准操作。

在实现此操作之后,值得在调试器中单步执行汇编代码,以验证 SetUnhandledExceptionFilter 是否不再有任何影响。(如果您真的喜欢编写单元测试,那么绝对可以对所需的行为进行单元测试。我将把它留给读者作为练习。)

最后,我们的最后机会异常报告确实有效了!

GDC Notes: Comparison of XBLA, PSN, iPhone - NextPrevious - Reporting Crashes in IMVU: Last-Chance Exceptions

/r/programming/r/programming 上查看讨论。

有任何意见?给我发送电子邮件或 [tweet](https://chadaustin.me/2009/03/disabling-functions/<https:/twitter.com/intent/tweet?text=@chadaustin regarding You Won't Learn This in School: Disabling Kernel Functions in Your Process: %0a&url=https://chadaustin.me/2009/03/disabling-functions/>)。

Mastodon icon follow on Mastodon Feedly icon follow with Feedly Atom feed icon RSS for the rest Imported Comments [?] Zaratustra on Mar 31, 2009 这是黑魔法 Ted Reed on Mar 31, 2009 对我来说,它是如何工作的很明显,尽管我不知道5字节的前导码。我只是认为你浏览了该函数的前五个字节。 当然,如果你让我写出来,我可能无法做到。有点像我可以阅读西班牙语报纸,但是如果你要求我翻译成西班牙语,我会茫然地看着你。 Kraln on Mar 31, 2009 太好了-直到您遇到更新的Windows内核,它会积极寻求阻止代码修改。在Win64上会发生什么? Ross on Mar 31, 2009 现在,此技巧是否能够禁用诸如 FindWindow 之类的东西或任何其他调用? sharvil on Apr 1, 2009 很高兴看到帖子一直到这一层。我想知道这对其他流程的影响是什么。DLL(尤其是系统 DLL)是共享模块,因此它们应该仅在物理内存中的一个实例的情况下映射到您的进程的地址空间中。当您修改代码时,_所有_加载该 DLL 的进程都应该看到新版本。 由于您正在调整kernel32.dll,因此所有其他进程都应该看到更改。您注意到这种行为了吗? pspda5id on Apr 1, 2009 我想更好的方法是对 direct3d 和 flash 执行 IAT 钩子。这样,只有来自这些模块的SetUnhandledExceptionFilter 调用才会受到影响。而且您不必进行令人讨厌的代码修补(应该使用类似 detours 的方法来完成),并且是 x64/x86 可移植的。 pspda5id on Apr 1, 2009

由于您正在调整kernel32.dll,因此所有其他进程都应该看到更改。您注意到这种行为了吗? 通过这种方式修补代码只会影响当前的进程。DLL 被映射为写时复制,因此它只会影响 1 个进程。如果不是这样,那么进程可能会失控并破坏kernel32.dll,并且用户区将被破坏。Win95 已经是很久以前的事了。 :) Aaron Davies on Apr 1, 2009 听起来像是病毒编写者的东西。存在哪些保护措施来防止它被用于关闭防病毒软件? Ramsey Stone on Apr 1, 2009 这显然完成了这项工作,但我认为一个更加“简洁”的解决方案是使用 Detours 或 madCHook 钩住 SetUnhandledExceptionFilter,它们的运行方式大致相同,并且具有相同的效果,但是对于尝试理解您的代码的人来说,看起来不那么令人恐惧。 admin on Apr 1, 2009 我很想使用 Detours,但是它很昂贵。我们在其他地方使用“APIHook”。 ilm on Apr 1, 2009 我喜欢 Kraln 试图教你一些东西,但他甚至不知道自己在说什么。 另一方面,这不一定是恶意软件编写者的东西。如果您在流程中运行第三方代码,它也很有用。想想插件。当然,如果他们是恶意的,他们可能会撤消你的工作,但是如果他们真的是恶意的,你就已经输了。 顺便说一句,您甚至可以将系统 DLL 视为自己流程中的第三方代码。当我们遇到 ShellExecute 中的错误时(ShellExecute 在 WOW64 上的行为),我使用了类似的技术来更改 shell32.dll 的行为。该代码修改了 shlwapi 中的一个辅助函数,ShellExecute/shell32 正在调用该函数。最后,IAT 钩子证明是足够的,但是在中间阶段,代码按照此处所示重写了辅助函数的序言。时髦的东西。 至于代码显而易见 - 我不这么认为。我当然不会立即知道字节代码,并且通常会忘记调用 FlushInstructionCache。这个错误会严重伤害你。 ilm on Apr 1, 2009 补充:我通过 reddit 找到了您的博客,并且已经阅读了您的整个 minidump/debug symbols/stack traces 系列。我喜欢它!我已经烹饪了自己的一个,并且肯定学到了一两件事(并且也为此页面添加了书签)。 Memet on Apr 1, 2009 我前段时间在 OSX 上做了类似的事情。 但是我所做的事情有点“和平主义”,并且我着手使我的代码修改尽可能小。 我通过等效于 LoadLibrary 的方法将 DLL 加载到目标进程中,获取函数的地址,然后使要挂钩的函数的第一个指令 jmp 到“代码岛”。 本质上,我的代码岛是一个 declspec(naked) 函数,其中包含对内部挂钩实现的功能调用,以及末尾的“ret”内联汇编(这意味着该挂钩函数将变成零堆栈准备函数:jmp、call、ret)。 这样,我只需要在二进制文件中修改一个指令,然后就可以得到一个完整的 C++ 解决方案。 无论如何,所有这些都已由 mach_star 完成,它会注入代码并为您安装该岛等等。 最后,我必须说标题有点误导:您没有禁用kernel函数。您禁用了kernel库函数。该库只是一个客户端。kernel是服务器。 Chad Austin on Apr 1, 2009 ilm:酷,很高兴它有帮助!随着 x86 如此普及,我认为这些类型的技术越来越有效。 memet:mach_star 听起来非常方便。谢谢你的提示! drnemo22 on Apr 1, 2009 你的方法很好。但是请记住,标题实际上具有误导性,正如其他人所提到的。不是因为您没有禁用kernel系统调用,而是因为 SetUnhandledExceptionFilter(..) 甚至不是系统调用...当然,当它调用 VirtualQuery() 时,它会间接调用kernel,但这不是您禁用的。 Chris on Apr 1, 2009 实际上,是的,这确实很明显。但是我将跳过在 IMVU 工作 ;) tb on Apr 10, 2009 操作系统如何让您摆脱这种困境?我认为这种行为本应受到与 DEP 实施的同一类安全措施的禁止(尽管在这种情况下,DEP 显然不适用)。我可以肯定地说,我读到过一些关于 Windows 较新版本如何不允许进程写入已加载模块的可执行空间的内容,但是我找不到这些参考资料... Chad Austin on Apr 10, 2009 在 Win9x 上,kernel DLL(以及可能所有 DLL?)以只读方式映射到该进程,因此该技术将不起作用。在 NT 上,DLL 被映射为写时复制,因此此修改会在您的进程中创建该页面的私有副本。因此,每当您的进程调用 SetUnhandledExceptionFilter 时,它将运行修改后的代码,而不是原始代码。 tb on Apr 10, 2009 但是该技术是否适用于最新版本的 Windows(Vista、Server 2008、Win7)?我确信他们已经更新了该功能来防止这种情况(除非您位于 .NET 运行时中或其他类似的东西)。我不确定代码签名对此是否非常有效,但是 DLL 是否有一种强制只读加载的方式?或者操作系统是否可以监视它知道对操作系统至关重要的 DLL 的这种行为?我以为这就是 Vista 如此缓慢的全部原因。 Windows Internals 的下一个版本要到五月才会发布,所以我没有可参考的文本,而且我在网上的搜索也一无所获。 ...我想我回家后必须自己尝试一下。 Chad Austin on Apr 10, 2009 是的,它也适用于 Vista。有很多程序都依赖于这样的技术,因此我无法想象它很快就会消失。 这并非安全违规或其他任何内容。它只会影响您进程中系统 API 调用的用户模式部分。 Duong on Apr 20, 2009 它适用于第三方 dll,但不适用于 MS CRT dll(我用 msvcr71d.dll 进行了测试)。当 msvcr71d.dll 中的函数生成异常时,Windows 错误报告仍会显示。此代码段将在 msvcr71d.dll 中生成异常:char * str1=""; char str2[20]; strncpy(str2, str1,strlen(str1)-10); Paul Betts on Sep 2, 2009 这个技巧有效,但实际上最好修补 IAT (http://sandsprite.com/CodeStuff/Understanding_imports.html),而不是实际编辑 SetUnhandledExceptionFilter 的代码。 Chad Austin on Sep 2, 2009 嗨,Paul, 我同意,我们为大量其他函数修补了 IAT(包括 HeapAlloc 和我们 Flash 集成的一些 mm API),但您必须记住在每次 LoadLibrary 动态组件后修补 IAT。 如果您直接修改该函数,则更改将持续应用于所有动态加载的库。 干杯,Chad Paul Betts on Sep 2, 2009 @Chad 不,您不必这样做 - 一旦加载了库,LL 将不会覆盖您的更改,它只会增加 DLL 上的 refcount。一旦您操纵了 IAT,它就会保持操纵状态。这是 AppCompat shims 工作的主要机制 Chad Austin on Sep 2, 2009 嗯,我指的是在加载库之前挂钩 IAT。我们使用一个名为“APIHook”的库在应用程序启动时修补 IAT,但我们动态加载 Flash 和 Direct3D。在加载 d3d.dll 或 flash10b.ocx 之后,我们还必须记住修改它们的 IAT。 或者我的术语是否弄错了,我们谈论的是不同的事情? Dean Hoggard on May 14, 2012 我今天真的感觉像赢得了金牌 Radu Ialovoi on Nov 3, 2014 对于找到 x64 架构的相同内容的人 xor eax, eax ; eax = 0 ret 4 ; 弹出 4 个字节(参数)并返回 在 x64 上转换为 xor rax,rax ;这次 rax = 0 是返回值 ret ;在 64 位上,调用者将展开堆栈。 这导致以下代码机器: unsigned char new_code[] = { 0x48, 0x33, 0xC0, 0xC3 }; 至于期望的代码部分,我没有检查是否相同。也许有人会这样做。 © 2025 Chad Austin。要发布翻译,请 联系我