本文为笔者从零基础学习系统安全相关内容的笔记,如果您对系统安全、逆向分析等内容感兴趣或者想要了解一些内容,欢迎关注。本系列文章将会随着笔者在未来三年的读研过程中持续更新,由于笔者现阶段还处于初学阶段,不可避免参照复现各类书籍内容,如书籍作者认为侵权请告知,笔者将立刻删除。强调本系列所有内容仅作为学习研究使用,作者对此文章中的代码造成的任何后果不负法律责任。
前文链接
[系统安全] PE文件格式详解1
[系统安全] PE文件格式详解2
[系统安全] Windbg Preview调试记录
[系统安全]《黑客免杀攻防》MFC逆向基础实战
本篇文章参照《黑客免杀攻防》用C++写一个简单的加壳程序,在此基础上结合自己思路进行修改和补充,并完全删掉了MFC部分,将程序改成一个控制台应用程序。如书籍作者认为侵权请告知,笔者将立刻删除。
加壳的过程是在加壳之前,先读取宿主程序的基本信息,并根据这些信息来对代码段、数据段及资源段进行有选择的加密压缩操作。
然后添加一个足够大的空区段,并将负责解密解压的代码(称为Stub部分或Shell部分)与相关配置信息写入这个新添加的空区段中。这段代码的功能是自动解密宿主程序,并在完成解密操作后转到宿主程序的原入口点处执行。
整个加壳程序执行过程图如下
主要注意的是我们需要把Stub这段代码合并到宿主程序,所以它需要有重定位表,由此推出这段程序需要被制作为dll程序。
此程序只对宿主程序的代码段进行加密,并且只支持exe格式的文件,加密算法采用异或加密方式。
首先编写最简单的Stub内容,将其制作为一个dll文件。根据上面图示,这个Stub的主要功能及执行流程为:读取被加密区域,进行解密,弹框知否执行,执行源程序。
但是最开始需要初始化一些基本的windows API,是因为把Stub这个dll拼接的时候会丢失导入表,所以最简单的方法是从内存中获取这些函数句柄。
Stub程序的的dllmain.cpp文件内编写制作的dll文件的主程序部分。
#include "Stub.h"
#pragma comment(linker, "/entry:\"StubEntryPoint\"") // 指定dll程序入口函数为StubEntryPoint()
#pragma comment(linker, "/merge:.data=.text") // 将.data合并到.text
#pragma comment(linker, "/merge:.rdata=.text") // 将.rdata合并到.text
#pragma comment(linker, "/section:.text,RWE") // 将.text段的属性设置为可读、可写、可执行
void start()
{
// 1. 初始化所有API
if (!InitializationAPI()) return;
// 2. 解密宿主程序
Decrypt();
// 3. 询问是否执行解密后的程序
if (g_stcParam.bShowMessage)
{
int nRet = g_funMessageBox(NULL, L"解密完成,是否运行原程序?", L"解密完成", MB_OKCANCEL);
if (IDCANCEL == nRet) return;
}
// 4. 跳转到OEP
__asm jmp g_stcParam.dwOEP;
}
void __declspec(naked) StubEntryPoint()
{
__asm sub esp, 0x50; // 抬高栈顶,提高兼容性
start(); // 执行壳的主体部分
__asm add esp, 0x50; // 平衡堆栈
// 主动调用ExitProcess函数退出进程可以解决一些兼容性问题
if (g_funExitProcess)
{
g_funExitProcess(0);
}
__asm retn;
}
Stub的其他内容不在此体现,放到资源里了。
编写好Stub部分后需要以资源的方式添加到PackLab项目里。
加壳部分分两个内容,一部分是处理PE文件即获取PE信息和修改PE文件等功能,另一部分就是调用这些函数进行流程上的PE文件改造。
首先是ProcessPE.h,主要封装了对于PE文件的处理函数。ProcessPE.cpp内容不在此体现。
#pragma once
#include
#include
#include
// 关键PE信息
typedef struct _PE_INFO
{
DWORD dwOEP; // 入口点
DWORD dwImageBase; // 映像基址
PIMAGE_DATA_DIRECTORY pDataDir; // 数据目录指针
IMAGE_DATA_DIRECTORY stcExport; // 导出目录
PIMAGE_SECTION_HEADER pSectionHeader; // 节头表指针
}PE_INFO, * PPE_INFO;
class CProcessingPE
{
public:
CProcessingPE(void);
~CProcessingPE(void);
public:
DWORD RVAToOffset(ULONG uRvaAddr); // RVA转文件偏移
DWORD OffsetToRVA(ULONG uOffsetAddr); // 文件偏移转RVA
BOOL GetPeInfo(LPVOID lpImageData, DWORD dwImageSize, PPE_INFO pPeInfo); // 获取PE文件的信息
void FixReloc(DWORD dwLoadImageAddr); // 修复重定位信息
PVOID GetExpVarAddr(LPCTSTR strVarName); // 获取导出全局变量的文件偏移
void SetOEP(DWORD dwOEP); // 设置新OEP
PVOID AddSection(LPCTSTR strName, DWORD dwSize, DWORD dwChara, PIMAGE_SECTION_HEADER pNewSection, PDWORD lpSize); // 添加区段
private:
DWORD m_dwFileDataAddr; // 目标文件所在缓存区的地址
DWORD m_dwFileDataSize; // 目标文件大小
PIMAGE_DOS_HEADER m_pDos_Header; // DOS头指针
PIMAGE_NT_HEADERS m_pNt_Header; // NT头指针
PE_INFO m_stcPeInfo; // PE关键信息
};
对与PackLab.h文件
#pragma once
#include "resource.h"
#include "ProcessPE.h"
#include
#include
#pragma comment(lib,"shlwapi.lib")
// 用以保存传递给Stub部分的参数
typedef struct _GLOBAL_PARAM
{
BOOL bShowMessage; // 是否显示解密信息
DWORD dwOEP; // 程序入口点
PBYTE lpStartVA; // 起始虚拟地址(被异或加密区)
PBYTE lpEndVA; // 结束虚拟地址(被异或加密区)
}GLOBAL_PARAM, * PGLOBAL_PARAM;
void Pretreatment(PBYTE lpCodeStart, PBYTE lpCodeEnd, PE_INFO stcPeInfo);
DWORD Implantation(LPVOID& lpFileData, DWORD dwSize, CProcessingPE* pobjPE, PE_INFO stcPeInfo, GLOBAL_PARAM stcParam);
main.cpp中编辑主程序
#include "PackLab.h"
#include
using namespace std;
bool PackLab(LPWSTR strPath, bool bShowMsg)
{
//LPCSTR是Win32和VC++所使用的一种字符串数据类型
CProcessingPE objProcPE; // PE处理对象
PE_INFO stcPeInfo; // PE信息
HANDLE hFile_In;
HANDLE hFile_Out;
DWORD dwFileSize;
LPVOID lpFileImage;
WCHAR szOutPath[MAX_PATH] = { 0 };
// 1. 生成输出文件路径:a.exe->a_Pack.exe
LPWSTR strSuffix = PathFindExtension(strPath); // 获取文件的后缀名
wcsncpy_s(szOutPath, MAX_PATH, strPath, wcslen(strPath)); // 备份目标文件路径到szOutPath
PathRemoveExtension(szOutPath); // 将szOutPath中保存路径的后缀名去掉
wcscat_s(szOutPath, MAX_PATH, L"_Pack"); // 在路径最后附加“_Pack”
wcscat_s(szOutPath, MAX_PATH, strSuffix); // 在路径最后附加刚刚保存的后缀名
// 2. 获取文件信息,并映射进内存中
if (INVALID_HANDLE_VALUE == (hFile_In = CreateFile(strPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL)))
{
cout << "1";
return false;
}
if (INVALID_FILE_SIZE == (dwFileSize = GetFileSize(hFile_In, NULL)))
{
CloseHandle(hFile_In);
cout << "2";
return false;
}
if (!(lpFileImage = VirtualAlloc(NULL, dwFileSize * 2, MEM_COMMIT, PAGE_READWRITE)))
{
cout << "3";
CloseHandle(hFile_In);
return false;
}
DWORD dwRet;
//hFile_In是文件句柄,lpFileImage是用于保存读入数据的一个缓冲区
if (!ReadFile(hFile_In, lpFileImage, dwFileSize, &dwRet, NULL))
{
CloseHandle(hFile_In);
VirtualFree(lpFileImage, 0, MEM_RELEASE);
cout << "4";
return false;
}
// 3. 获取PE文件信息
objProcPE.GetPeInfo(lpFileImage, dwFileSize, &stcPeInfo);
// 4. 获取目标文件代码段的起始结束信息
// 读取第一个区段的相关信息,并将其加密(默认第一个区段为代码段,因为大多数编译器生成的PE文件第一个区段就是代码段)
PBYTE lpStart = (PBYTE)(stcPeInfo.pSectionHeader->PointerToRawData + (DWORD)lpFileImage);
PBYTE lpEnd = (PBYTE)((DWORD)lpStart + stcPeInfo.pSectionHeader->SizeOfRawData);
PBYTE lpStartVA = (PBYTE)(stcPeInfo.pSectionHeader->VirtualAddress + stcPeInfo.dwImageBase);
PBYTE lpEndVA = (PBYTE)((DWORD)lpStartVA + stcPeInfo.pSectionHeader->SizeOfRawData);
// 5. 对文件进行预处理
Pretreatment(lpStart, lpEnd, stcPeInfo);
// 6. 植入Stub
DWORD dwStubSize = 0;
GLOBAL_PARAM stcParam = { 0 };
stcParam.bShowMessage = bShowMsg;
stcParam.dwOEP = stcPeInfo.dwOEP + stcPeInfo.dwImageBase;
stcParam.lpStartVA = lpStartVA;
stcParam.lpEndVA = lpEndVA;
dwStubSize = Implantation(lpFileImage, dwFileSize, &objProcPE, stcPeInfo, stcParam);
// 7. 将处理完成后的结果写入到新文件中
if (INVALID_HANDLE_VALUE != (hFile_Out = CreateFile(szOutPath, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_ALWAYS, 0, NULL)))
{
DWORD dwRet = 0;
WriteFile(hFile_Out, lpFileImage, dwStubSize + dwFileSize, &dwRet, NULL);
}
// 8. 释放相关资源并返回
CloseHandle(hFile_In);
CloseHandle(hFile_Out);
VirtualFree(lpFileImage, 0, MEM_RELEASE);
return true;
}
void Pretreatment(PBYTE lpCodeStart, PBYTE lpCodeEnd, PE_INFO stcPeInfo)
{
// 1. 加密指定区域
while (lpCodeStart < lpCodeEnd)
{
*lpCodeStart ^= 0xA1;
*lpCodeStart += 0x88;
lpCodeStart++;
}
// 2. 给第一个区段附加上可写属性
PDWORD pChara = &(stcPeInfo.pSectionHeader->Characteristics);
*pChara = *pChara | IMAGE_SCN_MEM_WRITE;
}
DWORD Implantation(LPVOID& lpFileData, DWORD dwSize, CProcessingPE* pobjPE, PE_INFO stcPeInfo, GLOBAL_PARAM stcParam)
{
// 1. 在资源中读取文件内容
HRSRC hREC = NULL; // 资源对象
HGLOBAL hREC_Handle = NULL; // 资源句柄
DWORD dwStubSize = NULL; // 文件大小
LPVOID lpResData = NULL; // 资源数据指针
HMODULE hModule = GetModuleHandle(L"Stub.dll");
if (!(hREC = FindResource(hModule, MAKEINTRESOURCE(IDR_STUB1), L"STUB"))) return false;
if (!(hREC_Handle = LoadResource(hModule, hREC))) return false;
if (!(lpResData = LockResource(hREC_Handle))) return false;
if (!(dwStubSize = SizeofResource(hModule, hREC))) return false;
// 2. 提取Stub部分的关键信息
CProcessingPE objProcPE;
PE_INFO stcStubPeInfo;
PBYTE lpData = new BYTE[dwStubSize];
// 2.1 将Stub复制到临时缓冲区,防止重复操作
CopyMemory(lpData, lpResData, dwStubSize);
// 2.2 获取Stub的PE信息
objProcPE.GetPeInfo(lpData, dwStubSize, &stcStubPeInfo);
// 2.3 算出代码段的相关信息(默认第一个区段为代码段)
PBYTE lpText = (PBYTE)(stcStubPeInfo.pSectionHeader->PointerToRawData + (DWORD)lpData);
DWORD dwTextSize = stcStubPeInfo.pSectionHeader->SizeOfRawData;
// 3. 添加区段
DWORD dwNewSectionSize = 0;
IMAGE_SECTION_HEADER stcNewSection = { 0 };
PVOID lpNewSectionData = pobjPE->AddSection(L".lab", dwTextSize, IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_EXECUTE, &stcNewSection, &dwNewSectionSize);
// 4. 对Stub部分进行的重定位操作
// 新的加载地址 = (新区段的起始RVA - Stub的".Text"区段的起始RVA) + 映像基址
DWORD dwLoadImageAddr = (stcNewSection.VirtualAddress - stcStubPeInfo.pSectionHeader->VirtualAddress) + stcPeInfo.dwImageBase;
objProcPE.FixReloc(dwLoadImageAddr);
// 5. 写入配置参数
// 5.1 获取Stub的导出变量地址
PVOID lpPatam = objProcPE.GetExpVarAddr(L"g_stcParam");
// 5.2 保存配置信息到Stub中
CopyMemory(lpPatam, &stcParam, sizeof(GLOBAL_PARAM));
// 6. 将Stub复制到新区段中
CopyMemory(lpNewSectionData, lpText, dwTextSize);
// 7. 计算并设置新OEP
DWORD dwNewOEP = 0;
// 7.1 计算新OEP
DWORD dwStubOEP = stcStubPeInfo.dwOEP;
DWORD dwStubTextRVA = stcStubPeInfo.pSectionHeader->VirtualAddress;
DWORD dwNewSectionRVA = stcNewSection.VirtualAddress;
dwNewOEP = (dwStubOEP - dwStubTextRVA) + dwNewSectionRVA;
// 7.2 设置新OEP
pobjPE->SetOEP(dwNewOEP);
// 8. 释放资源,函数返回
delete[] lpData;
FreeResource(hREC_Handle);
return dwNewSectionSize;
}
int main() {
wchar_t filePath[32] = { 0 };
wcin >> filePath;
//C:/Users/jgc/Desktop/VB.exe
if (PackLab(filePath, true)) {
cout << "success" << endl;
}
else {
cout << "fail" << endl;
}
return 0;
}
为了简单起见,此加壳程序是将Stub这个dll以一个代码段的形式附加在宿主程序的最后一个节(区段)的最后面,如果宿主程序的最后一个节后面还有附加信息则会被默认覆盖从而可能产生一些错误,但是这里不考虑那些问题。
经过测试,此加壳程序对VS2010_MFC、VS2010_Console、VC++6和OD等可执行程序起作用,但是对VS2019编译生成的Console程序失败,猜测是编译器升级后导致不兼容错误。
项目源码https://download.csdn.net/download/weixin_42172261/72291058
项目源码
stdafx.h是预编译头文件,其中可以包含标准系统包含文件和经常使用且不常更改的特定于项目的包含文件
头文件预编译,就是把一个工程中使用的一些标准头文件预先编译,
以后该工程编译时,不再编译这部分头文件,而是仅仅使用预编译的结果。
这样可以加快编译速度节省时间。
pch.h为previous compiled header的缩写
目前都是用pch.h代替stdafx.h,两者一样都是预编译头文件,只不过pch从名字上更清楚一些。
comment-type 是一个预定义的标识符,它指定了注释记录的类型。
可选 commentstring 是一个字符串,它提供了某些注释类型的附加信息。
#pragma comment(lib,“shlwapi.lib”)表示链接shlwapi.lib这个库。
和在工程设置里写上链入shlwapi.lib的效果一样,不过这种方法写的
程序别人在使用你的代码的时候就不用再设置工程settings了
shlwapi.lib文件,包含了大量的Windows字符串处理方法
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
__declspec(dllexport)是导出dll中的变量或方法
__declspec(dllexport)是引入dll中的变量或方法