梦幻的彼岸 发表于 2022-2-27 10:01:06

LogNT32 - 在没有预定义的头列表的情况下跟踪所有 ntdll 函数调用

本帖最后由 梦幻的彼岸 于 2022-2-27 10:06 编辑

翻译原文地址: https://www.x86matthew.com/view_post?id=lognt32功能:在没有预定义的头列表的情况下跟踪所有 ntdll 函数调用我创建了一个小工具,可以让我们在ntdll.dll级别对32位exe文件进行快速分析。这对于各种目的都很有用——监视潜在的恶意exe,或者深入了解高级Windows APIs(如ShellExecute)是如何工作的。

由于这不是一个攻击性的测试工具,我也将在这个版本中包含二进制文件(本文底部的链接)。

该工具在用户模式下运行,并记录所有ntdll.dll函数调用。我不想依赖ntdll函数定义的硬编码列表,所以我想出了一些技巧,允许我们以通用的方式记录函数参数。这些方法将在本文中进一步描述。

请看下面的演示:默认情况下,这个程序拦截ntdll.dll内的每个导出——这意味着目标可执行文件将运行得非常慢。可以使用命令行过滤器(排除/仅包括特定导出)来设置过滤器,以提高性能和可读性。

LogNT32 consists of 2 parts - LogNT32.exe and LogNT32.dll.

LogNT32.exe

这只是启动目标可执行文件并将主模块(LogNT32.dll)加载到远程进程中的加载程序。它还处理LogNT32命令行参数(过滤选项),这些参数在日志模块加载后被传递给日志模块。没有使用特殊的注入技术——这只是使用了标准的CreateRemoteThread / LoadLibrary方法,因为这里不需要隐藏。
将LogNT32.dll模块加载到目标进程后,该程序会连续读取输出日志文件,并实时将其打印到控制台。

LogNT32.dll

如上所述,这个模块被注入到目标流程中。 在主程序执行开始前,采取以下初始化步骤:

1.创建ntdll中所有可执行部分的cloned。仅复制.text部分在这里不足够-ntdll的某些版本。dll还导出名为RT的单独可执行部分中的一些函数。这将被用作ntdll.dll的无污染(未拦截)副本。2.使用RtlAddVectoredExceptionHandler添加异常处理程序。3.读取ntdll中的导出地址表。ntdll.dll为除KiUserExceptionDispatcher之外的每个函数添加断点(0xCC)。4.向KiUserExceptionDispatcher函数添加一条JMP指令——这将直接重定向到KiUserExceptionDispatcher的“cloned”版本。我们不能在KiUserExceptionDispatcher上设置断点,否则将发生无限循环。5.处理用户指定的过滤器-设置标志以包括或排除特定的导出。6.主程序开始执行。

当目标进程调用ntdll.dll中的函数时,会发生以下一系列事件:

1.初始断点命中——自定义异常处理程序捕获EXCEPTION_BREAKPOINT异常。2. 存储此函数调用的堆栈指针(esp)和返回地址()。3. 使用 Dr0 调试寄存器在此函数调用的返回地址上设置硬件断点。4. 在导出列表中查找当前函数名(通过指令指针)。5. 添加一个日志条目以指示函数调用的开始。6. 继续执行。7. 函数返回时,应触发返回地址上的硬件断点并引发EXCEPTION_SINGLE_STEP异常。8. 存储返回值(eax)。9. 从当前堆栈指针中减去步骤#2 中的原始堆栈指针。将此值除以 4 (DWORD) 并减去 1(忽略返回地址)以计算提供给函数的参数数量。这是因为 WinAPI 函数使用 stdcall 调用约定——这也是为什么这个概念只适用于 32 位程序,x64 使用不同的调用约定。10. 遍历每个参数并检查它是否包含字符串值。此工具检查 OBJECT_ATTRIBUTES、ANSI_STRING 和 UNICODE_STRING 结构中的字符串。执行各种检查以计算这是否是有效的字符串参数。11、添加日志条目,表示函数调用结束,包括返回值和参数值。

这个程序为每个线程维护一个内部调用堆栈。链中每个调用的“depth”显示在日志条目中。跟踪调用堆栈非常重要,以确保硬件断点始终设置为链中的下一个预期返回地址。

需要进行各种其他检查,以确保一切按预期进行。例如,在递归函数调用中,在返回地址上设置硬件断点之前,我们添加了一个单步标志——这可以防止无限循环。

Windows程序中的执行流并不总是线性的。内核可以临时接管现有的用户模式线程来执行回调。这些回调函数通常用于GUI应用程序中的消息处理。当内核请求用户模式回调时,目标线程指令指针设置为KiUserCallbackDispatcher。此函数在用户模式下执行请求的回调(来自PEB中的KernelCallbackTable表),最后在完成时调用NtCallbackReturn以返回内核模式。然后恢复原始线程环境,并继续执行。

为了维持调用堆栈,我们需要跟踪每个内核回调的开始和结束时间。这是因为KiUserCallbackDispatcher函数没有返回地址,这意味着每个新的内核回调都需要一个临时调用堆栈。更复杂的是,多个回调可以嵌套在其内部。这意味着我们需要维持多层调用堆栈,并在每次回调结束时始终返回到链中的下一个条目。

注意:当我在本机32位系统上测试时(而不是在64位安装上测试WoW64),我注意到NtCallbackReturn很少用于返回内核。相反,user32.dll内部有一个非导出函数(姑且称之为User32CallbackReturn)直接执行int 0x2B指令返回内核。这意味着我们不能通过单独挂钩NtCallbackReturn来检测回调返回。幸运的是,在所有32位版本的user32.dll中,User32CallbackReturn函数似乎是相同的:mov eax, dword ptr
int 0x2B
retn 4在32位操作系统上,这个程序在user32.dll的代码段中搜索这个函数,并手动添加一个拦截来模拟NtCallbackReturn的行为。

我们不想从异常处理程序中调用任何拦截的函数,所以我们使用一个“safe”函数指针列表,这些指针直接指向“clean”ntdll.dll副本。使用c运行时函数通常是安全的,但是一些实现调用ntdll.dll函数(例如wcstombs-> RtlUnicodeToMultiByteN)应该避免。为了完整起见,我在异常处理程序中使用了所有外部函数调用的“safe”版本。

LogNT32包含一个预定义的要忽略的函数列表(不考虑命令行过滤器)。这个文件名为LogNT32_IgnoreList.txt,如果它不存在,它会被创建并预先填充。这包含RtlEnterCriticalSection和RtlLeaveCriticalSection等常用函数,可以根据需要进行修改。我还添加了一个名为string_only的命令行参数。如果指定了此参数,则只记录包含字符串参数值的函数调用。由于字符串值通常是最有用的监控字段,这可以从输出日志中去除很多“noise”。 由于该工具依赖于ntdll.dll的hooked exports,它不会检测恶意软件中可能使用的“direct”系统调用——这一功能超出了该工具的范围。
如之前所述,该工具对于记录高级WinAPI函数非常有用。例如,我们可以记录以下程序:int main()
{
    ShellExecute(NULL, "open", "notepad.exe", NULL, NULL, SW_SHOW);

    return 0;
}这会创建一个很大的输出文件,但是我在下面提取了一个简短的示例,显示了ShellExecute打开notepad.exe的App Paths注册表项:    RtlInitUnicodeStringEx
   RtlInitUnicodeStringEx(0x0019F420 <Software\Microsoft\Windows\CurrentVersion\App Paths\notepad.exe>, 0x0019F4A4)
   NtOpenKeyEx
   NtOpenKeyEx(0x0019F764, 0x00020019, 0x0019F36C <Software\Microsoft\Windows\CurrentVersion\App Paths\notepad.exe>, 0x00000000)
   RtlNtStatusToDosError
   RtlNtStatusToDosError(0xC0000034)
   RtlNtStatusToDosError
   RtlNtStatusToDosError(0xC0000034)
   RtlAcquireSRWLockExclusive
   RtlAcquireSRWLockExclusive(0x77A6A760)
   RtlReleaseSRWLockExclusive
   RtlReleaseSRWLockExclusive(0x77A6A760)
   NtOpenProcessToken
   NtOpenProcessToken(0xFFFFFFFF, 0x00000008, 0x0019F710)
   RtlNtStatusToDosError
   RtlNtStatusToDosError(0x00000000)
   NtQueryInformationToken
   NtQueryInformationToken(0x00000440, 0x00000012, 0x0019F6E4, 0x00000004, 0x0019F6E0)
   RtlNtStatusToDosError
   RtlNtStatusToDosError(0x00000000)
   NtClose
   NtClose(0x00000440)
   RtlQueryPackageClaims
   RtlQueryPackageClaims(0xFFFFFFFA, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x0019F7B8, 0x0019F7C0)
   RtlInitUnicodeStringEx
   RtlInitUnicodeStringEx(0x0019F83C <SetWorkingDirectoryFromTarget>, 0x73DCA844)
   NtQueryKey
   NtQueryKey(0x00000312, 0x00000003, 0x0019F598, 0x00000188, 0x0019F590)
   RtlInitUnicodeStringEx
   RtlInitUnicodeStringEx(0x0019F540 <\REGISTRY\MACHINE\SOFTWARE\Classes\exefile\shell\open\>, 0x0019F59C)
   RtlAppendUnicodeStringToString
   RtlAppendUnicodeStringToString(0x0019F540 <\REGISTRY\MACHINE\SOFTWARE\Classes\exefile\shell\open\>, 0x0019F57C) 下载下面的二进制文件:

Download zip

以下是完整代码:
代码过大上传附件保存
LogNT32.EXE代码LogNT32.DLL代码
页: [1]
查看完整版本: LogNT32 - 在没有预定义的头列表的情况下跟踪所有 ntdll 函数调用