调用Windows API实现命令行工具:Sharp4Cmd

基本介绍

Sharp4Cmd是一款基于.NET编写的Windows环境下的命令行交互工具,不像其他的.NET脚本或者工具依赖系统的cmd.exe执行命令。该工具通过直接调用Windows系统kernel32.dll中的CreateProcess函数实现任意命令执行并在黑屏上显示命令执行后的结果,可以在内网渗透阶段规避安全防护设备的拦截和告警,因此这款工具对于红队活动是非常有利的。

使用方法

编译Sharp4Cmd项目,生成可执行文件Sharp4Cmd.exe,在命令行中请按照以下规范运行命令。

Sharp4Cmd.exe [command]

其中,参数 [command] 是要执行的指令。比如执行 "whoami /priv",可以使用以下命令:Sharp4Cmd.exe whoami/priv,运行后如下图所示。

原理分析

Sharp4Cmd核心实现的代码主要分两大块,一块是自定义的类UnmanagedExecute,此类包含了大量的API方法,用于实现父子进程之间的相互操作。另一块是控制台默认的Main方法。程序启动时Main方法作为程序的入口,接收第一个参数作为等待执行的命令,具体代码如下所示。

internal class Program
{
    static void Main(string[] args)
    {
        string command = args[0];
        UnmanagedExecute.CreateProcess(0, command);
        Console.ReadKey();
    }
}

内部通过调用UnmanagedExecute.CreateProcess方法启动新的进程执行变量command引入的命令。但这注意CreateProcess方法的第一个参数是0,这里的值代表什么,我们在后面的内容展开来谈。

一、创建管道

工具最核心的一块就是自定义的UnmanagedExecute类,这是一个包含了一系列用于操作进程、线程和管道等底层系统功能的静态方法和结构体的类,如下图所示。

该类通过DllImport特性声明了几个常用的Windows API函数,比如CreateProcess、OpenProcess、InitializeProcThreadAttributeList等,下面我们结合代码展开说明。

1.OpenProcess函数

kernel32.dll 中的OpenProcess 函数,用于打开一个已存在的进程,常用于进程注入时读写进程内存或者获取句柄等场合。.NET声明调用OpenProcess的代码如下所示

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId);

参数processAccess指定了要对进程进行的访问权限,如读取内存、写入内存等。processId表示要打开的进程的进程ID。

2.InitializeProcThreadAttributeList函数

InitializeProcThreadAttributeList 函数用于初始化进程属性列表,这些属性列表可以在创建新进程时通过 CreateProcess 函数的 lpAttributeList 参数传递。.NET声明调用InitializeProcThreadAttributeList的代码如下所示

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);

该函数结合 UpdateProcThreadAttribute 和 CreateProcess 函数,可以实现复杂的进程创建和精细化的控制逻辑。

3.UpdateProcThreadAttribute函数

UpdateProcThreadAttribute 函数用于更新由 InitializeProcThreadAttributeList 函数初始化的属性列表,这个函数通常在创建新的进程时设置父子进程相关属性,如父进程相关的运行策略以满足对其行为的控制。

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute, IntPtr lpValue,IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize);

参数lpAttributeList指向属性列表的指针,属性列表由 InitializeProcThreadAttributeList 函数复制初始化,另一个参数Attribute表示指定要更新的属性,常见的属性有PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,表示指定进程应用策略。

4.SECURITY_ATTRIBUTES结构体

让笔者意外的是在UnmanagedExecute类并没有直接调用CreateProcess函数,而是定义了一个返回bool类型的同名方法,内部实现代码逻辑很长,我们一点一滴的拆解再看。首先在自定义的CreateProcess方法我们创建了一个安全属性结构SECURITY_ATTRIBUTES,用于定义管道句柄的继承性,实现代码如下所示。

public static bool CreateProcess(int parentProcessId, string command)
{
    ...........//省略
    var saHandles = new SECURITY_ATTRIBUTES();
    saHandles.nLength = Marshal.SizeOf(saHandles);
    saHandles.bInheritHandle = true;
    saHandles.lpSecurityDescriptor = IntPtr.Zero;
    ...........//省略
}

SECURITY_ATTRIBUTES结构有三个不同类型的成员,定义结构体如下所示

public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public bool bInheritHandle;
}

成员lpSecurityDescriptor指向安全描述符的指针,默认为IntPtr.Zero,表示使用默认安全描述符。bInheritHandle表示子进程是否继承句柄,如果为true子进程就可以继承句柄,所以在上述这段代码中允许创建的子进程继承父进程的句柄。

5.CreatePipe函数

接着,该工具代码实现上使用CreatePipe函数创建一个管道,我们知道在Windows系统编程中,pipe管道是一种重要的进程间IPC通信机制。具体代码如下所示

IntPtr hStdOutRead;
IntPtr hStdOutWrite;
CreatePipe(out hStdOutRead, out hStdOutWrite, ref saHandles, 0);
SetHandleInformation(hStdOutRead, HANDLE_FLAGS.INHERIT, 0);

通过管道系统从A进程的数据输出写入管道,然后B进程从管道中读取数据。此API函数的原型如下所示

BOOL CreatePipe(
  PHANDLE hReadPipe,
  PHANDLE hWritePipe,
  LPSECURITY_ATTRIBUTES lpPipeAttributes,
  DWORD nSize
);

hReadPipe指向接收读取端句柄的指针,hWritePipe指向接收写入端句柄的指针。CreatePipe的主要目的是将子进程的标准输出和错误输出重定向到管道,便于父进程可以读取这些输出。

二、配置进程策略

Windows操作系统中ProcessID为0的内核进程是System Idle Process,负责表示系统的空闲状态,并进行低优先级的后台任务调度。由于是系统内核级进程不便于当作父进程,因此Sharp4Cmd在Main方法中传递的第一个参数是0,表示寻找哪个进程作为子进程的父进程时,优先排除掉PID为0的内核进程。判断PID进程大于0时才会进入配置策略的阶段。具体代码如下所示

if (parentProcessId > 0)
{
    var lpSize = IntPtr.Zero;
    var success = InitializeProcThreadAttributeList(IntPtr.Zero, 2, 0, ref lpSize);
    if (success || lpSize == IntPtr.Zero)
    {
        return false;
    }

    siEx.lpAttributeList = Marshal.AllocHGlobal(lpSize);
    success = InitializeProcThreadAttributeList(siEx.lpAttributeList, 2, 0, ref lpSize);
    if (!success)
    {
        return false;
    }
}

接着Sharp4Cmd.exe在创建子进程时还设置一些安全策略,比如阻止加载非微软签名的DLL,并处理句柄的继承和复制。实现代码如下所示

IntPtr lpMitigationPolicy = Marshal.AllocHGlobal(IntPtr.Size);
Marshal.WriteInt64(lpMitigationPolicy, PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON);

success = UpdateProcThreadAttribute(
    siEx.lpAttributeList,
    0,
    (IntPtr)PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
    lpMitigationPolicy,
    (IntPtr)IntPtr.Size,
    IntPtr.Zero,
    IntPtr.Zero);

通过Marshal.WriteInt64(lpMitigationPolicy, PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON),将仅允许加载微软签名的DLL文件这项策略的值写入内存中。然后调用UpdateProcThreadAttribute函数将策略添加到属性列表。如果失败,返回false。

三、数据重定向传输

通过OpenProcess函数打开parentProcessId指向的父进程句柄并调用,UpdateProcThreadAttribute函数更新父进程的属性列表,具体代码如下所示。

IntPtr parentHandle = OpenProcess(ProcessAccessFlags.CreateProcess | ProcessAccessFlags.DuplicateHandle, false, parentProcessId);
lpValueProc = Marshal.AllocHGlobal(IntPtr.Size);
Marshal.WriteIntPtr(lpValueProc, parentHandle);

success = UpdateProcThreadAttribute(
    siEx.lpAttributeList,
    0,
    (IntPtr)PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
    lpValueProc,
    (IntPtr)IntPtr.Size,
    IntPtr.Zero,
    IntPtr.Zero);

接着打开父进程的句柄,再通过DuplicateHandle函数将输出数据的句柄复制到父进程,使得子进程的输出数据能够重定向到父进程中,代码实现如下所示。

IntPtr hCurrent = System.Diagnostics.Process.GetCurrentProcess().Handle;
IntPtr hNewParent = OpenProcess(ProcessAccessFlags.DuplicateHandle, true, parentProcessId);
success = DuplicateHandle(hCurrent, hStdOutWrite, hNewParent, ref hDupStdOutWrite, 0, true, DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS);
siEx.StartupInfo.hStdError = hDupStdOutWrite;
siEx.StartupInfo.hStdOutput = hDupStdOutWrite;

四、读取子进程数据

经过上面的配置后我们通过调用API函数CreateProcess 创建进程,此函数用于创建一个新的进程,允许程序通过指定父进程ID和命令来启动一个新的进程。具体代码如下所示

siEx.StartupInfo.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
siEx.StartupInfo.wShowWindow = SW_HIDE;

var ps = new SECURITY_ATTRIBUTES();
var ts = new SECURITY_ATTRIBUTES();
ps.nLength = Marshal.SizeOf(ps);
ts.nLength = Marshal.SizeOf(ts);

bool ret = CreateProcess(null, command, ref ps, ref ts, true, EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW, IntPtr.Zero, null, ref siEx, out pInfo);

新进程创建后,调用SafeFileHandle封装了 hStdOutRead 句柄,再结合StreamReader从管道读取新进程输出的Stream流数据,再通过do语句循环不断的读取输出数据,直到子进程完全退出管道中无数据为止。实现代码如下所示。

SafeFileHandle safeHandle = new SafeFileHandle(hStdOutRead, false);
var encoding = Encoding.GetEncoding(GetConsoleOutputCP());
var reader = new StreamReader(new FileStream(safeHandle, FileAccess.Read, 4096, false), encoding, true);
string result = "";
bool exit = false;

try
{
    do
    {
        if (WaitForSingleObject(pInfo.hProcess, 100) == 0)
        {
            exit = true;
        }

        char[] buf = null;
        int bytesRead;

        uint bytesToRead = 0;

        bool peekRet = PeekNamedPipe(hStdOutRead, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, ref bytesToRead, IntPtr.Zero);

        if (peekRet == true && bytesToRead == 0)
        {
            if (exit == true)
            {
                break;
            }
            else
            {
                continue;
            }
        }

        if (bytesToRead > 4096)
            bytesToRead = 4096;

        buf = new char[bytesToRead];
        bytesRead = reader.Read(buf, 0, buf.Length);
        if (bytesRead > 0)
        {
            result += new string(buf);
        }

    } while (true);
    reader.Close();
}

综上所述,Sharp4Cmd原理上通过使用 Windows API 函数如 CreateProcess、OpenProcess 和 DuplicateHandle实现父子进程间的数据交互,工具本身主要在处理子进程输出时,通过创建管道并使用 StreamReader 实时读取数据,使得用户可以即时获取子进程的执行结果。这样的设计思路好处在于不必再依赖传统的cmd.exe,从而为红队活动带来更好的隐蔽性。

附件:
0 条评论
某人
表情
可输入 255