调试器在启动调试对象时如何绕过图像文件执行选项?

How do debuggers bypass Image File Execution Options when launching their debugee?

我正在 Windows 内部进行一些探索以获取一般启发,并且我正在尝试了解图像文件执行选项背后的机制。具体来说,我为 calc.exe 设置了一个调试器条目,其中 "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NoExit -Command "& { start-process -filepath $args[0] -argumentlist $args[1..($args.Length - 1)] -nonewwindow -wait}" 作为负载。这导致递归,启动了许多 powershell 实例,考虑到我正在拦截他们对 calc.exe.

的调用,这是有道理的

不过,这引出了一个问题:普通调试器如何在不引起这种递归行为的情况下启动被测程序?

无论如何,这是一个关于 Windows 内部结构的好问题,但我现在对它感兴趣的原因是它已成为我的一个实际问题。在我做有偿工作的地方有三台计算机,每台计算机都有不同的 Windows 版本甚至不同的调试器,使用这个 IFEO 技巧会导致调试器自行调试,显然陷入了困扰 OP 的循环.

调试人员通常如何避免这种循环?好吧,他们自己没有。 Windows 为他们避免它。

但是让我们先看看圆度。简单的演示几乎没有得到 PowerShell 组合的帮助,calc.exe 也不像以前那样了。让我们将 notepad.exeDebugger 值设置为 c:\windows\system32\cmd.exe /k。 Windows 会将此解释为尝试 运行 notepad.exe 通常应该 运行 c:\windows\system32\cmd.exe /k notepad.exe 代替。 CMD 会将此解释为 运行 notepad.exe 的意思并四处闲逛。但是 notepad.exe 的执行也会变成 c:\windows\system32\cmd.exe /k notepad.exe,依此类推。任务管理器很快就会显示数百个 cmd.exe 个实例。 (好消息是他们都在一个控制台上,可以一起杀死。)

OP 的问题是,为什么 CMD 及其 /k(或 /c)切换 运行 宁子在调试器值中循环,但 WinDbg,例如,没有。

从某种意义上说,答案是一个未记录的结构中的一位,PS_CREATE_INFO,它在用户模式和内核模式之间为 NtCreateUserProcess 函数交换。这种结构在某些圈子中已经广为人知,但他们似乎从未说过如何。我认为该结构可以追溯到 Windows Vista,但直到 Windows 8 才从微软的 public 符号文件中得知它,甚至从内核中也不知道,但从 Internet Explorer 组件 URLMON.DLL.

无论如何,在 PS_CREATE_INFO 结构的现代形式中,偏移量 0x08(32 位)或 0x10(64 位)处的 0x04 位控制内核是否检查 Debugger 值。符号文件告诉我们这个位被微软称为 IFEOSkipDebugger。如果此位已清除并且存在 Debugger 值,则 NtCreateUserProcess 失败。通过 PS_CREATE_INFO 结构的其他反馈告诉 KERNELBASE,它处理 CreateProcessInternalW,自己查看 Debugger 值并再次调用 NtCreateUserProcess,但(大概)一些其他可执行文件和命令行。

相反,当该位被设置时,内核不关心 Debugger 值并且 NtCreateUserProcess 可以成功。通常如何设置该位是由 KERNELBASE 设置的,因为调用者不仅要求创建一个进程,而且特别要求成为新进程的调试器,即已设置 DEBUG_PROCESSDEBUG_ONLY_THIS_PROCESS 在进程创建标志中。这就是我所说的调试器自己不做任何事情来避免循环的意思。 Windows 为他们做这件事只是因为他们想要调试可执行文件。

将调试器值视为可执行文件 X 的图像文件执行选项的一种方法是,该值的存在意味着 X 只能在调试器下执行,并且该值的内容可能会告诉您如何执行此操作。黑客早就注意到了,内核的程序员早就注意到了,内容不需要指定调试器,并且可以调整值,以便尝试 运行 X 而不是 运行 Y。很少有人注意到的是Y 将无法 运行 X,除非 Y 调试 X(或禁用 Debugger 值)。同样不太被注意的是,并非所有对 运行 X 的尝试都会改为 运行 Y:调试器对 运行 X 作为调试对象的尝试不会被转移。

Geoff 的出色回答的 TLDR - 使用 DEBUG_PROCESSDEBUG_ONLY_THIS_PROCESS 绕过 Debugger / Image File Execution Options (IFEO) 全局标志并避免递归。

在 C# 中,使用优秀的 Vanara.PInvoke.Kernel32 NuGet:

var startupInfo = new STARTUPINFO();
var creationFlags = Kernel32.CREATE_PROCESS.DEBUG_ONLY_THIS_PROCESS;

CreateProcess(path, null, null, null, false, creationFlags, null, null, startupInfo, out var pi);
DebugActiveProcessStop(pi.dwProcessId);

请注意,DebugActiveProcessStop 对我来说很关键(否则在打开 notepad.exe 时看不到 window)- 如果您的程序不是真正的调试器并且你只是想要旁路。