我最近做了一个关于shellcode入门和开发的专题课👩🏻💻,主要面向对网络安全技术感兴趣的小伙伴。这是视频版内容对应的文字版材料,内容里面的每一个环境我都亲自测试实操过的记录,有需要的小伙伴可以参考。
我的个人主页:https://imbyter.com
对于一些功能简单的shellcode编写,我们可以用前面介绍的shellcode框架直接编写生成,但对于功能比较复杂的需求,我们也可以选择先以常规C/C++开发方式,编写好dll和exe文件后,再将其转换为shellcode文件,或者对于已有的dll或exe文件,不需要重写代码,直接将其转换为shellcode文件。
这节主要介绍如何实现对指定的PE文件(dll和exe文件)转shellcode的方法。dll和exe两种类型的文件,两者在shellcode转换方面的主要区别是:
dll文件 | exe文件 | |
---|---|---|
重定位表的处理 | 一定存在重定位表,需要对其地址进行重定位处理。 | 不一定存在重定位表,存在处理重定位和不处理两种情况。 |
入口函数调用 | 可能有DllMain函数或自定义导出函数。 | main函数或自定义入口函数。 |
不管是dll还是exe,将其转为shellcode的关键点有以下几个:
- PE文件在shellcode中的存放;
- shellcode运行时PE文件的定位;
- PE文件在shellcode中的内存加载执行;
现在我们来逐一解决以上问题:
一、生成shellcode时PE的封装
在我们前面所说的shellcode编写方式中,shellcode总大小就是我们实际编写shellcode的所有代码,但对于PE转成的shellcode而言,最后生成的shellcode文件总大小为我们编写的shellcode与PE文件的总和。
我们可以根据自己的需求来设计PE文件在整个shellcode中的位置,可以放在前面、中间,或者后面。将PE文件放在整个shellcode的最后面实现起来相对容易,如图所示:
在代码上的实现如下:
0.createshellcode.cpp:
#include "a.start.h"
#include "z.end.h"
#pragma comment(linker,"/entry:EntryMain")
// 该函数用于生成shellcode文件。函数本身与生成的shellcode功能没有任何关系,
// 并没包含在生成的shellcode里面,所以该函数编写不受shellcode编写规范限制。
int EntryMain()
{
DWORD dwWriten = 0;
DWORD dwRead = 0;
HANDLE hShellcodeFile = NULL;
HANDLE hPEFile = NULL;
// 创建文件,用于保存最后的shellcode
hShellcodeFile = CreateFileA("shellcode.bin", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
if (hShellcodeFile == INVALID_HANDLE_VALUE)
{
return -1;
}
// 计算shellcode头大小
SIZE_T dwShellcodeHeaderSize = (SIZE_T)ShellCodeEnd - (SIZE_T)ShellCodeStart;
// 将shellcode写入到文件
WriteFile(hShellcodeFile, ShellCodeStart, (DWORD)dwShellcodeHeaderSize, &dwWriten, NULL);
// 打开目标PE文件(EXE或者DLL)
hPEFile = CreateFileA("TestDll.dll", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hPEFile == INVALID_HANDLE_VALUE)
{
CloseHandle(hShellcodeFile);
return -1;
}
// 获取目标PE大小
DWORD dwPESize = GetFileSize(hPEFile, NULL);
LPVOID lpPEData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwPESize);
// 读取目标文件内容
ReadFile(hPEFile, lpPEData, dwPESize, &dwRead, NULL);
// 将PE文件内容写入shellcode文件
WriteFile(hShellcodeFile, lpPEData, dwPESize, &dwWriten, NULL);
HeapFree(GetProcessHeap(), 0, lpPEData);
CloseHandle(hShellcodeFile);
CloseHandle(hPEFile);
return 0;
}
以上代码:
- 创建shellcode.bin文件;
- 将shellcode头写入到文件中;
- 读取目标PE文件,此处为动态链接库TestDll.dll文件(dll或exe文件,可根据实际需求修改);
- 将PE文件内容继续写入shellcode.bin中。
二、运行shellcode时PE的定位
将整个Shellcode拷贝到内存中运行时,我们首先需要知道PE文件在shellcode中的位置,才能将其内存加载运行。在内存中,PE文件是紧跟在shellcode头后面的,所以要想确定PE文件的位置,可以用shellcode所在内存的基址+shellcode头的长度得到。
- shellcode内存的基址的获取:我们可以在加载器中,将申请的shellcode执行地址作为参数,传递给shellcode的ShellCodeStart函数,这样就能轻松得到这个基址;
- shellcode头长度:即 ShellCodeEnd 与 ShellCodeStart 的差值;
a.start.cpp:
#include "a.start.h"
#include "api.h"
#include "loadpe.h"
#include "z.end.h"
// lpAddress:shellcode所在内存基址
void ShellCodeStart(LPVOID lpAddress)
{
CAPI api;
// 初始化所有用到的函数
api.InitFunctions();
// 定位到PE文件内容
// 完整的shellcode内容 = shellcode头 + PE文件内容
SIZE_T dwShellcodeHeaderLen = (SIZE_T)ShellCodeEnd - (SIZE_T)ShellCodeStart;
unsigned char* lpPEDataStart = (unsigned char*)lpAddress + dwShellcodeHeaderLen;
// 加载PE文件
CLoadPE PE(&api, lpPEDataStart);
PE.MemoryLoad();
}
三、运行shellcode时PE的执行
正常执行PE文件(dll或exe)时,PE文件是由系统加载运行的,在shellcode中,我们需要自己手动实现PE文件在内存中的“展开”和执行。 我们创建一个CLoadPE类,专门用于PE文件的内存加载。
- 构造函数CLoadPE(CAPI* api, PVOID lpFileData)接受两个参数,一个CAPI类型用于使用动态调用的函数,一个PVOID表示PE数据的地址:
- 函数IsValidPE()通过检查PE文件的"MZ"头和"PE"头,来确定PE文件是否有效;
- 函数AllocImageBaseAddress()用于给PE文件分配执行内存;
- 函数CopySections()用于将PE文件的个节“展开”到执行内存中;
- 函数RepairIAT()用于修复导入表;
- 函数RepairReloc()用于修复重定位表;
- 函数CallEntryPoint()用于执行入口函数功能;
- 为方便执行,整个流程封装在函数MemoryLoad()中执行;
类CLoadPE(loadpe.h和loadpe.cpp):
loadpe.h:
#pragma once
#include "api.h"
class CLoadPE
{
public:
CLoadPE(CAPI* api, PVOID lpFileData);
public:
BOOL MemoryLoad(); // 内存加载PE
private:
BOOL IsValidPE(); // 判断PE格式是否正确
BOOL IsDllFile(); // 判断PE是DLL还是EXE
BOOL AllocImageBaseAddress(); // 申请内存空间(基址)
VOID CopySections(); // 复制节到申请的内存中
BOOL RepairIAT(); // 修复导入表
BOOL RepairReloc(); // 修复重定位表
PVOID MemGetProcAddress(PCSTR FunctionName); // 获取内存中dll指定函数名的导出函数地址
VOID CallEntryPoint(CLoadPE* Object); // 执行入口函数
PVOID m_ImageBase; // 内存中PE文件基址
ULONG_PTR m_EntryPointer; // 内存中PE文件入口点地址
// 文件中的PE结构变量
PIMAGE_DOS_HEADER m_DosHeader;
PIMAGE_NT_HEADERS m_PeHeader;
PIMAGE_SECTION_HEADER m_SectionHeader;
// 加载到内存后的PE结构变量
PIMAGE_DOS_HEADER m_MemDosHeader;
PIMAGE_NT_HEADERS m_MemPeHeader;
private:
CAPI* m_api;
public:
PVOID m_FileData; // PE文件内容
};
loadpe.cpp:
#include "loadpe.h"
CLoadPE::CLoadPE(CAPI* api, PVOID lpFileData)
{
m_api = api;
m_FileData = lpFileData;
m_ImageBase = NULL;
m_EntryPointer = NULL;
m_DosHeader = NULL;
m_PeHeader = NULL;
m_SectionHeader = NULL;
m_MemDosHeader = NULL;
m_MemPeHeader = NULL;
}
BOOL CLoadPE::MemoryLoad()
{
if (m_FileData == NULL)
{
return FALSE;
}
m_DosHeader = (PIMAGE_DOS_HEADER)m_FileData;
m_PeHeader = (PIMAGE_NT_HEADERS)(m_DosHeader->e_lfanew + (ULONG_PTR)m_DosHeader);
// 检查PE格式是否有效
if (!IsValidPE())
{
return FALSE;
}
// 申请内存空间
if (!AllocImageBaseAddress())
{
return FALSE;
}
// 分配数据节块
CopySections();
// 修复导入表
if (!RepairIAT())
{
return FALSE;
}
// 修复重定位表
if (!RepairReloc())
{
return FALSE;
}
// 执行入口函数功能
CallEntryPoint(this);
return TRUE;
}
BOOL CLoadPE::IsValidPE()
{
if (m_DosHeader->e_magic != IMAGE_DOS_SIGNATURE || m_PeHeader->Signature != IMAGE_NT_SIGNATURE)
{
// 不是有效的PE文件
return FALSE;
}
#ifdef _WIN64
if (m_PeHeader->FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64)
{
return FALSE;
}
#else
if (m_PeHeader->FileHeader.Machine != IMAGE_FILE_MACHINE_I386)
{
return FALSE;
}
#endif
return TRUE;
}
BOOL CLoadPE::IsDllFile()
{
return m_PeHeader->FileHeader.Characteristics & IMAGE_FILE_DLL;
}
BOOL CLoadPE::AllocImageBaseAddress()
{
if (m_PeHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == 0)
{
// 尝试在基址处申请空间
m_ImageBase = m_api->VirtualAlloc((LPVOID)m_PeHeader->OptionalHeader.ImageBase, m_PeHeader->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (m_ImageBase == NULL)
{
return FALSE;
}
}
else
{
// 尝试申请新内存空间
m_ImageBase = m_api->VirtualAlloc(NULL, m_PeHeader->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (m_ImageBase == NULL)
{
return FALSE;
}
}
m_api->memcpy(m_ImageBase, m_DosHeader, m_PeHeader->OptionalHeader.SizeOfHeaders);
return TRUE;
}
VOID CLoadPE::CopySections()
{
m_SectionHeader = IMAGE_FIRST_SECTION(m_PeHeader);
for (size_t i = 0; i < m_PeHeader->FileHeader.NumberOfSections; i++)
{
if (m_SectionHeader->SizeOfRawData != 0 && m_SectionHeader->VirtualAddress != 0)
{
m_api->memcpy((PVOID)((ULONG_PTR)m_ImageBase + m_SectionHeader->VirtualAddress), (PVOID)((ULONG_PTR)m_DosHeader + m_SectionHeader->PointerToRawData), m_SectionHeader->SizeOfRawData);
}
m_SectionHeader++;
}
m_MemDosHeader = (PIMAGE_DOS_HEADER)m_ImageBase;
m_MemPeHeader = (PIMAGE_NT_HEADERS)(m_MemDosHeader->e_lfanew + (ULONG_PTR)m_MemDosHeader);
m_EntryPointer = (ULONG_PTR)m_ImageBase + m_MemPeHeader->OptionalHeader.AddressOfEntryPoint;
}
BOOL CLoadPE::RepairIAT()
{
#ifdef _WIN64
union
{
struct
{
ULONGLONG low : 63;
ULONGLONG high : 1;
}BitField;
ULONGLONG Value;
}Temp = { 0 };
#else
union
{
struct
{
ULONG low : 31;
ULONG high : 1;
}BitField;
ULONG Value;
}Temp = { 0 };
#endif
if (m_MemPeHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size > 0)
{
PIMAGE_IMPORT_DESCRIPTOR pIID = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)m_ImageBase + m_MemPeHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (pIID->Name != NULL)
{
PIMAGE_THUNK_DATA pITD = (PIMAGE_THUNK_DATA)((ULONG_PTR)m_ImageBase + pIID->OriginalFirstThunk);
HMODULE hModule = m_api->LoadLibraryA((char*)((ULONG_PTR)m_ImageBase + pIID->Name));
if (hModule == NULL)
{
// DLL加载失败
return FALSE;
}
PULONG_PTR pFuncAddr = (PULONG_PTR)((ULONG_PTR)m_ImageBase + pIID->FirstThunk);
while (*pFuncAddr != 0)
{
Temp.Value = pITD->u1.AddressOfData;
if (Temp.BitField.high == 1)
{
// 通过序号取函数地址
*pFuncAddr = (ULONG_PTR)m_api->GetProcAddress(hModule, (LPCSTR)Temp.BitField.low);
}
else
{
// 通过名称取函数地址
PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)m_ImageBase + pITD->u1.AddressOfData);
*pFuncAddr = (ULONG_PTR)m_api->GetProcAddress(hModule, pFuncName->Name);
}
pITD++;
pFuncAddr++;
}
pIID++;
}
}
return TRUE;
}
BOOL CLoadPE::RepairReloc()
{
if (m_MemPeHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size > 0)
{
typedef struct BASE_RELOCATION_ENTRY {
USHORT Offset : 12;
USHORT Type : 4;
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;
INT_PTR Offset = (INT_PTR)((ULONG_PTR)m_ImageBase - m_PeHeader->OptionalHeader.ImageBase);
IMAGE_BASE_RELOCATION* pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)m_ImageBase + m_MemPeHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
while (pIBR->VirtualAddress != 0)
{
int NumberOfBlocks = (pIBR->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);
BASE_RELOCATION_ENTRY* Block = (BASE_RELOCATION_ENTRY*)((ULONG_PTR)pIBR + sizeof(IMAGE_BASE_RELOCATION));
for (int i = 0; i < NumberOfBlocks; i++)
{
if (Block->Type != IMAGE_REL_BASED_ABSOLUTE)
{
PINT_PTR RepairAddr = (PINT_PTR)((ULONG_PTR)m_ImageBase + pIBR->VirtualAddress + Block->Offset);
if (*RepairAddr <= 0)
{
return FALSE;
}
*RepairAddr += Offset;
}
Block++;
}
pIBR = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)pIBR + pIBR->SizeOfBlock);
}
m_MemPeHeader->OptionalHeader.ImageBase = (ULONG_PTR)m_ImageBase;
}
return TRUE;
}
PVOID CLoadPE::MemGetProcAddress(PCSTR FunctionName)
{
// 从到处目录获取指定函数
if (m_MemPeHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size > 0)
{
PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)m_ImageBase + m_MemPeHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD FuncNameTable = (PDWORD)((ULONG_PTR)m_ImageBase + pIED->AddressOfNames);
PDWORD FuncAddrTable = (PDWORD)((ULONG_PTR)m_ImageBase + pIED->AddressOfFunctions);
for (DWORD i = 0; i < pIED->NumberOfNames; i++)
{
if (m_api->strcmp(FunctionName, (PCSTR)((ULONG_PTR)m_ImageBase + *FuncNameTable)) == 0)
{
return (PVOID)((ULONG_PTR)m_ImageBase + *FuncAddrTable);
}
FuncNameTable++;
FuncAddrTable++;
}
}
return NULL;
}
VOID CLoadPE::CallEntryPoint(CLoadPE* Object)
{
if (IsDllFile())
{
// DllMain入口函数
typedef BOOL(WINAPI* DllEntryProc)(HINSTANCE hInstDLL, DWORD fdwReason, LPVOID lpReserved);
((DllEntryProc)Object->m_EntryPointer)((HINSTANCE)Object->m_ImageBase, DLL_PROCESS_ATTACH, NULL);
// 导出函数(根据实际函数定义修改)
typedef void(*ExportFun)();
char szExpFun[] = {'t','e','s','t','f','u','n',0};
ExportFun lpproc = (ExportFun)MemGetProcAddress(szExpFun);
if (lpproc != NULL)
{
lpproc();
}
}
else
{
// main函数
((void(*)())(Object->m_EntryPointer))();
}
}
四、项目属性设置
-
禁用“优化”选项:
-
Release的“运行库”设为“多线程(/MT)”,禁用“安全检查”:
自己动手,更进一步:
- 在生成的shellcode中,将PE文件进行压缩和加密;
- 在运行shellcode时,先解密解压然后再内存加载。
如果有任何问题,可以在我们的知识社群中提问和沟通交流:
一个人走得再快,不如一群人走得更远!🤜🤛