写在最前
如果你是信息安全爱好者,如果你想考一些证书来提升自己的能力,那么欢迎大家来我的 Discord 频道 Northern Bay。邀请链接在这里:
https://discord.gg/9XvvuFq9Wb
我拥有 OSCP,OSEP,OSWE,OSED,OSCE3,CRTO,CRTP,CRTE,PNPT,eCPPTv2,eCPTXv2,KLCP,eJPT 证书。
所以,我会提供任一证书备考过程中尽可能多的帮助,并分享学习和实践过程中的资源和心得,大家一起进步,一起 NB~
背景
红队/渗透测试离不开命令行,在命令行执行命令的时候,执行的命令以及命令的参数,会被系统日志记录。那么我们的真实目的,就会被一览无余。
为了进一步隐藏我们的真实目的,混淆视听,我们可以使用 Command Line Spoofing 这个技巧,修改进程的 PEB,来达到隐藏真实命令的效果。
这篇文章,我们就来看一下 Command Line Spoofing 是如何实现的。
Command Line Spoofing
概述
Command Line Spoofing 是通过修改进程 PEB(Process Environment Block) 中的 RTL_USER_PROCESS_PARAMETERS 结构来达成的。原理就是这样,我们实践一下,看一下具体如何实现。
RTL_USER_PROCESS_PARAMETERS 结构
在命令行用 notepad 随便打开一个文件。
WinDBG hook 进程。
查看一下 PEB。
dt !_PEB
在 offset 0x20 的地方可以看到 RTL_USER_PROCESS_PARAMETERS 结构。
看一下官方的结构原型。
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine; // 重点在这里
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
第四个参数,CommandLine 是我们关注的重点。
我们看一下当前 _RTL_USER_PROCESS_PARAMETERS 的数据。
dt _RTL_USER_PROCESS_PARAMETERS
CommandLine 参数在 0x70 的位置上。
查看一下 _RTL_USER_PROCESS_PARAMETERS 结构在内存中的地址。
dt _peb @$peb
查看一下内存中的内容。可以直接点击 ProcessParameters。
dx -r1 ((wintypes!_RTL_USER_PROCESS_PARAMETERS *)0x16c2c0f2b60)
查看 CommandLine 成员的内容。直接点击即可。
dx -r1 (*((wintypes!_UNICODE_STRING *)0x16c2c0f2bd0))
我们可以看到,整个命令被保存在 CommandLine 成员的 Buffer 属性中。
我们跟 Process Hacker 对比一下。一致。
看到了 _RTL_USER_PROCESS_PARAMETERS 结构的内部,下面我们就需要在运行时更改这个结构中 CommandLine 成员的值。
_RTL_USER_PROCESS_PARAMETERS Runtime Patch
整体步骤如下:
- 创建一个挂起(suspend)进程,使用假的命令及参数作为 CreateProcess 的 commandline 参数;
- 找到当前进程的 _RTL_USER_PROCESS_PARAMETERS 结构;
- 修改 _RTL_USER_PROCESS_PARAMETERS 结构的 CommandLine 成员为真实的命令及参数;
- 恢复(resume)进程运行,以修改之后的命令及参数执行;
一步一步实现。
-
创建一个挂起(suspend)进程,使用假的命令及参数作为 CreateProcess 的 commandline 参数;
STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; WCHAR goodArgs[] = L"notepad all-good.txt"; if (CreateProcess( L"C:\\Windows\\System32\\notepad.exe", goodArgs, // 使用假的参数创建这个进程 NULL, NULL, FALSE, CREATE_SUSPENDED, // 创建挂起状态的进程 NULL, L"C:\\", &si, &pi)) { _tprintf(L"Process created: %d", pi.dwProcessId); }
我们在 88 行打个断点,debug 一下。
WinDBG hook 重复以上步骤查看 _RTL_USER_PROCESS_PARAMETERS 中的 CommandLine 成员。
看到了当前的虚假参数。接下来我们要找到 _RTL_USER_PROCESS_PARAMETERS,以便修改 CommandLine 成员。
-
找到当前进程的 _RTL_USER_PROCESS_PARAMETERS 结构;
我们继续分解成几个小步骤来完成。
-
1)获取 NtQueryInformationProcess 函数在 ntdll 中的地址以调用;
-
2)使用 NtQueryInformationProcess 函数获取 PROCESS_BASIC_INFORMATION,PEB 的地址包含在这个结构中;
-
3)使用 ReadProcessMemory 函数,从 PROCESS_BASIC_INFORMATION 中获取 PebBaseAddress 地址;
-
4)使用 ReadProcessMemory 函数,从 PebBaseAddress 中获取 ProcessParameters 地址,读取 RTL_USER_PROCESS_PARAMETERS 结构;
下面我们一步一步来实现。
1)获取 NtQueryInformationProcess 函数在 ntdll 中的地址以调用;
要查看进程的信息,会使用到 NtQueryInformationProcess。
函数原型。
__kernel_entry NTSTATUS NtQueryInformationProcess( [in] HANDLE ProcessHandle, [in] PROCESSINFOCLASS ProcessInformationClass, [out] PVOID ProcessInformation, [in] ULONG ProcessInformationLength, [out, optional] PULONG ReturnLength );
NtQueryInformationProcess 这个函数不能直接使用,必须在运行时通过 GetProcAddress 函数获取其在 ntdll 中的地址。
因此我们先定义一个函数原型。然后在运行时解析他的地址。
typedef NTSTATUS(*QueryInformationProcess)(IN HANDLE, IN PROCESSINFOCLASS, OUT PVOID, IN ULONG, OUT PULONG); // 运行时解析地址 HMODULE ntdll = GetModuleHandle(L"ntdll.dll"); QueryInformationProcess NtQueryInformationProcess = (QueryInformationProcess)GetProcAddress(ntdll, "NtQueryInformationProcess");
至此,我们就可以调用 NtQueryInformationProcess 函数来获取
2)使用 NtQueryInformationProcess 函数获取 PROCESS_BASIC_INFORMATION,PEB 的地址包含在这个结构中;
PROCESS_BASIC_INFORMATION pbi; DWORD length; NtQueryInformationProcess( pi.hProcess, ProcessBasicInformation, // 从 ProcessBasicInformation 中读取 PROCESS_BASIC_INFORMATION &pbi, // 读取结果存放在 pbi 变量中 sizeof(pbi), &length);
3)使用 ReadProcessMemory 函数,从 PROCESS_BASIC_INFORMATION 中获取 PebBaseAddress 地址;
PEB peb; SIZE_T bytesRead; ReadProcessMemory( pi.hProcess, pbi.PebBaseAddress, // 从 pbi 中获取 PEB 的基地址 &peb, // PEB 的基地址存放在 peb 变量中 sizeof(PEB), &bytesRead);
4)使用 ReadProcessMemory 函数,从 PebBaseAddress 中获取 ProcessParameters 地址,读取 RTL_USER_PROCESS_PARAMETERS 结构;
RTL_USER_PROCESS_PARAMETERS rtlParams; ReadProcessMemory( pi.hProcess, peb.ProcessParameters, // 从 PEB 的 ProcessParameters 中读取 RTL_USER_PROCESS_PARAMETERS 结构 &rtlParams, // 读取结果存放在 rtlParams 变量中 sizeof(RTL_USER_PROCESS_PARAMETERS), &bytesRead);
至此,RTL_USER_PROCESS_PARAMETERS 结构我们已经找到。接下来就是用真实的命令及参数,覆盖掉原来的虚假参数。
-
-
修改 _RTL_USER_PROCESS_PARAMETERS 结构的 CommandLine 成员为真实的命令及参数;
使用 WriteProcessMemory 函数写入新的命令及参数到 RTL_USER_PROCESS_PARAMETERS 的 CommanLine 成员中。
WCHAR evilArgs[] = L"notepad C:\\Users\\opr\\Desktop\\target-file.txt"; SIZE_T bytesWritten; WriteProcessMemory( pi.hProcess, rtlParams.CommandLine.Buffer, // 写入到 RTL_USER_PROCESS_PARAMETERS -> CommandLine -> Buffer 中,修改原有的值 evilArgs, // 使用 evilArgs 覆盖 goodArgs sizeof(evilArgs), &bytesWritten);
-
恢复(resume)进程运行,以修改之后的命令及参数执行最终命令;
ResumeThread(pi.hThread);
我们运行起来看一看是否能打开用户桌面的指定文件,并且配置 Sysmon 来查看日志记录的命令行。Sysmon 在这里下载。
这里抓取 notepad 进程的最简单 Sysmon config.xml 配置文件如下,保存到任意位置即可(这里我保存在了 \Windows\config.xml)。
<Sysmon schemaversion="4.60">
<EventFiltering>
<!-- Event ID 1 == Process Creation - Includes -->
<RuleGroup groupRelation="or">
<ProcessCreate onmatch="include">
<OriginalFileName condition="is">notepad.exe</OriginalFileName>
<CommandLine name="technique_id=T9999,technique_name=whatever" condition="contains all">notepad.exe</CommandLine>
</ProcessCreate>
</RuleGroup>
</EventFiltering>
</Sysmon>
启动 Sysmon。
sysmon64.exe -i c:\windows\config.xml
确认 Sysmon 服务已经运行。
打开 Event Viewer,找到 Applications and Services Log > Microsoft > Windows > Sysmon Operational。
创建一个 Filter,过滤 Event ID 为 1 的事件,也就是 Process Creation 事件。
然后运行 CommandLineSpoofing.exe。之后查看 Event Viewer。
可以看到 Sysmon 抓到的 CommandLine 日志是 notepad all-good.txt,但是我们成功偷换了参数,打开了指定的 target-file.txt 文件。
Command line spoofing 成功。
发现的问题
Process Hacker 会在我们打开一个进程的属性菜单的时候重新读取该进程的 PEB 数据,因此 Command Line Spoofing 对于 Process Hacker 看似无效。
我们在 Process Hacker 看一下这个进程,发现 Command line 被截掉了一部分。
还记得我们之前在 WinDBG 中看到的,每个 CommandLine 成员有一个 Length 属性,这个就是当前整个命令的长度。
这个值是 0x2a,转换成十进制是 42 个 byte。
而 notepad all-good.txt 长度是 20 个字节(外加 1 个字节的 null-byte),共 21 字节。
这是因为 CommandLine 中用于存放命令的 Buffer 是 wchar_t 类型,一个字母占 2 个字节。
所以一共是 42 个字节。
这就是问题所在,这个 Buffer 只能放下 21 个字母,我们替换进去的命令,只有黑体部分 notepad C:\Users\opr\target-file.txt 被显示出来。
所以我们在使用 Command Line Spoofing 的时候,我们可以计算一下虚假参数的长度,配合真实参数的长度做截取,达到目的。
总结
这篇文章解释了 Command Line Spoofing 的原理,就是通过修改进程 PEB(Process Environment Block) 中的 RTL_USER_PROCESS_PARAMETERS 结构,来达到隐藏真实命令参数的效果。
不过 Command Line Spoofing 并不是全能。我们必须考虑多层的防护机制。比如,就算命令行参数隐藏了,但是如果命令会创建进程,那么创建进程的事件,依然会被 Sysmon 等日志系统记录。
因此, Command Line Spoofing 最好使用在不会创建额外进程的情况下。对于 Malware 或者 C2 开发来说,是一个很好的隐藏行为轨迹的思路。
参考链接
- https://stackoverflow.com/questions/12760878/contains-function-for-lpctstr
- https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentprocessid
- https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulefilenameexa
- https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulebasenamea?redirectedfrom=MSDN
- https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-rtl_user_process_parameters
- https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/exploring-process-environment-block
- https://www.codeproject.com/Articles/19685/Get-Process-Info-with-NtQueryInformationProcess
- https://stackoverflow.com/questions/766126/representation-of-wchar-t-and-char-in-windbg
- https://www.blumira.com/enable-sysmon/
- https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess