前言:
感谢:易道云学院 tiger老师指导:
前提:需要导入小蜜蜂反汇编引擎,不会的可以回复问,这里不做介绍,MFC页面的搭建也不做详细介绍。因为任务量太大本贴只写代码保护工具的制作,后续解析在下个帖子介绍。
成品工具展示:
打开要保护的程序,会显示基本的PE信息。点击创建保护代码会弹出
这里设置具体要保护代码的起始VA 和结束VA 本程序没有考虑基址重定位的问题基础可以默认为0。名称主要做参考,可以把保护代码的函数参数等信息写入。所有要保护的代码设置完以后点击生成加壳程序会生成一个新的源程序。
大体思路
通过打开文件读取程序PE信息,获取到程序默认加载基址(我们这里做的是exe主程序,DLL等模块的加载可能会有基址重定位的问题这里没做考虑),程序有多少个节,内存和硬盘对齐方式,每一节在硬盘和内存中的起始地址。然后通过VA&FOA的转化得到要保护代码的FOA。(如何转化已经在上个帖子中做了介绍)然后我们分别读取每一段要保护的代码,然后把每一段要保护的代码起始地址、需要重定位的数量、长度做记录。把源程序需要保护代码的位置全部写入
这段汇编代码,其中push 0 是作为我们分发函数的参数来定位不同的游戏代码的地址,这个参数我们设计为依次递增。这些全部做完以后把我们要保存多少段代码,每段代码的信息,要保护的程序代码,程序原始大小依次写入源程序的末尾(也可以写入配置文件,或者加密写到网络上)。后续可以制作插件来实现解析功能,从而达到你的插件和源程序的高度绑定。
流程:
定义三块内存空间,第一块内存空间保存游戏源代码;第二段内存空间保存要保护的代码有多少段,每一段起始地址,代码中有多少需要重定位的数据,要保护代码的长度;第三块内存保存要保护的游戏代码,在最后写入源程序的大小。
定义变量: char* _data{};//记录游戏文件
char* _dataEntry{}; //保存保护代码
char* _dataEntryBegin{}; //保存多端代码_dataEntry指针要往后移动这个变量记录原始地址。
char* _dataHeader{};//保存有多少条保护数据
char* _dataHeaderBegin{};//和 _dataEntryBegin用处一样
unsigned codeCount{};//要保护的代码数量 char _AsmCode[12]{"x60x9cx6Ax00xB8xFCxEFx02x01xFFx20"}; //写入固定汇编代码 第4个元素 也就是下标3的元素是参数
定义函数:
bool LoadFile(wchar_t* _file); //加载文件 char* ReadData(unsigned _va, unsigned _base = 0); //读取保护代码 void Init(unsigned count); //初始化数据
void pushCode(unsigned va, unsigned len, unsigned base); //推送代码(核心代码)
bool Save(wchar_t* file); //写入文件
定义结构体:(记录保护代码的起始位置、重定位数量、大小)
typedef struct CODEConText
{
unsigned start; //代码开始位置
unsigned short r_count;//重定位信息表
unsigned short len; //代码长度
}*PCODEConText;
函数实现:
bool LoadFile(wchar_t* _file)
{
auto hFile = CreateFile(_file,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
return false;
}
DWORD dRead;
fileLen = GetFileSize(hFile, NULL);
if (fileLen == INVALID_FILE_SIZE)
{
return false;
}
if (fileLen>_datal)
{
if (_data)delete[]_data;
_data = new char[fileLen];
_datal = fileLen;
}
if (ReadFile(hFile,_data, fileLen,&dRead,NULL))
{
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)_data;
PIMAGE_NT_HEADERS32 ntHeader = PIMAGE_NT_HEADERS32(_data + dosHeader->e_lfanew);
SecCount = ntHeader->FileHeader.NumberOfSections;
exebase = ntHeader->OptionalHeader.Imagebase;
secArrys = (PIMAGE_SECTION_HEADER)(sizeof(IMAGE_NT_HEADERS32) + (unsigned)ntHeader);
CloseHandle(hFile);
if (fileLen>_dataEntryl)
{
if (_dataEntry)delete[] _dataEntry;
_dataEntry = new char[fileLen];
_dataEntryBegin = _dataEntry;
_dataEntryl = fileLen;
}
return true;
}
delete[]_data;
_data = nullptr;
return false;
}
解析:此函数主要做为打开文件按钮函数,在此函数中分配了两端内存_data(存放源程序代码) _dataEntry(存放保护代码和源程序大小)。这个函数已经得到了基本的PE信息,源程序代码,源程序大小。
char* ReadData(unsigned _va, unsigned _base)
{
int index = VaToFoa(_va, _base);
return _data+index;
}
解析:通过该函数返回要保护代码的硬盘地址FOA。
void Init(unsigned count)
{
codeCount = count; //指定保护代码数量
unsigned uSize = 4 + count * sizeof(CODEConText);
if (uSize>_dataHeaderl)
{
if (_dataHeader)delete[]_dataHeader;
_dataHeader = new char[uSize]; //定义数组,大小为4+保护代码数量*结构体大小
_dataHeaderBegin = _dataHeader;
_dataHeaderl = uSize;
}
unsigned* _count = (unsigned*)_dataHeader; //定义指针指向该数组
_count[0] = count; //把数组第一位存上数量
_dataHeader += sizeof(_count); //然后把指针往后移
} 解析:此函数申请了一块内存_dataHeader,并把有多少段要保护的代码写道了这块内存的起始位置.
void pushCode(unsigned va, unsigned len, unsigned base)
{
CStringA _tmp;
CStringA _output;
char* buffer = ReadData(va, base);
DISASM disasm;
memset(&disasm, 0, sizeof(DISASM));//初始化结构体
disasm.EIP = (UIntPtr)buffer; //指定初始写入地址
disasm.VirtualAddr = (UIntPtr)va;
disasm.Archi = IA32;//安装32位汇编解读
disasm.Options = MasmSyntax; //汇编显示方式,安装微软汇编的风格显示
int _len;
INT32 opcode;
PCODEConText _pcontext =(PCODEConText) _dataHeader; //让结构体指针指向数量后面 存放顺序,先是一共有多少段保护代码,后面是若干个结构体数据
_pcontext->start = va;
_pcontext->r_count = 0;
_pcontext->len = len;
while (!disasm.Error) //解读没有出现错误就一直循环
{
disasm.SecurityBlock = (UIntPtr)buffer + len - disasm.EIP;//计算还有多少代码没有解读
if (disasm.SecurityBlock <= 0) break; //没有代码可以解读
_len = Disasm(&disasm); //核心解读函数
switch (disasm.Error)
{
case OUT_OF_BLOCK: break;
case UNKNOWN_OPCODE:
disasm.EIP += 1;
disasm.VirtualAddr += 1;
break;
default:
opcode = disasm.Instruction.Opcode; //获取当前反汇编的汇编代码
if ((opcode==0xE8)||(opcode==0xE9)) //判断是否是远跳指令
{
_pcontext->r_count++; //找到远跳指令,让重定位个数加1
//计算绝对地址 E8xxxx call 00000 XXXX是相对地址= 目标地址-当前地址-5 那么 目标地址 = XXXX地址+当前地址+5;
unsigned short offset = (unsigned)disasm.EIP - (unsigned)buffer; //计算偏移 当前代码位置 - 开始代码位置
unsigned* e8 = (unsigned*)(disasm.EIP + 1); //获取call jmp 后面的相对地址值
unsigned callAdr = (va + offset) + e8[0] + 5; //当前地址+XXX地址+5 =目标地址
e8[0] = callAdr; //把计算好的地址重新写回去
unsigned short* _offset = (unsigned short*)_dataEntry; //定义指针指向加密数据
_offset[0] = offset; //在加密数据中写入
_dataEntry = _dataEntry + sizeof(offset); //把加密数据地址往后移动,移动到偏移后面下一条指令
}
disasm.EIP += _len;
disasm.VirtualAddr += _len;
}
}
memcpy(_dataEntry, buffer, len); //拷贝客户端代码到加密数据包
_dataEntry += len; //加密数据包指针往后移到第一个保护代码的末尾下一次写入的地址
memset(buffer, 0xcc, len);//把要保护游戏代码原来的地址用CC填充
memcpy(buffer, _AsmCode, 11); //把汇编代码拷贝到保护代码的位置
char* _punshIndex = _AsmCode + 3; //定义变量指向参数的位置
_punshIndex[0]++; //每次调用参数会递增
_dataHeader += sizeof(CODEConText); //把指针往后移,移动一个结构体的大小指向的位置刚好写下一个结构体。
} 解析:在该函数中把结构体写入到_dataHeader,通过循环依次解读要保护的代码,遇到E8 E9等需要从定位的数据就让结构体中的重定位表数量加一,并重新计算地址并写入_dataEntry(注意,如果是DLL等需要重定位的情况不要直接计算地址)循环解读完以后拷贝客户段源代码到_dataEntry。在源程序保护代码的位置写入我们定义好的汇编,写入后把参数加1 下次写入就是push 1。移动两个指针,_dataHeader指向写入结构体的末尾(下次写入结构体);_dataEntry指向游戏保护代码的末尾(写入下段保护代码).
bool Save(wchar_t* file)
{
unsigned* fCount = (unsigned*)_dataEntry; //定义指针指向游戏保护代码 现在这里已经是最后一段代码的末尾了
fCount[0] = fileLen; //写入原始文件长度
_dataEntry += sizeof(fCount);
auto hFile = CreateFile(file,
GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS, //总是创建新文件
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
return false;
}
DWORD dRead;
WriteFile(hFile, _data, fileLen, &dRead, NULL);//写入客户端数据
SetFilePointer(hFile, 0, 0, FILE_END);
WriteFile(hFile, _dataHeader, (unsigned)_dataHeader - (unsigned)_dataHeaderBegin, &dRead, NULL); //_dataHeader 一开始保存的是有N段保护代码,后面依次是N个结构体数据 存放的是保护代码的起始位置,需要重定位的数量,保护代码的长度
SetFilePointer(hFile, 0, 0, FILE_END);
WriteFile(hFile, _dataEntry, (unsigned)_dataEntry - (unsigned)_dataEntryBegin, &dRead, NULL); //_dataEntry 保存的是保护代码
CloseHandle(hFile);
_dataEntry = _dataEntryBegin; //恢复指针到原位
_dataHeader = _dataHeaderBegin;//恢复指针到原位
return true;
}
解析:Save函数 首先把源文件大小追加到_dataEntry末尾,打开文件依次先后写入源程序,后面写入有多少段要保护的代码,每段代码的基本信息。最后依次写入各段保护代码,及源文件大小。
生成加壳程序按钮函数:
void OnBnClickedButton2() //生成加壳程序
{
int count = myList.GetItemCount(); //获取列表中有多少条保护代码
_protecter.Init(count); //初始化
CStringA _read;
for (int i = 0; i < count; i++)
{
unsigned va = (unsigned)myList.GetItemData(i); //获取列表中VA的值
_read = myList.GetItemText(i, 4); //读取base值
unsigned _base = strtoul(_read.Right(8), NULL, 16); //获取16进制的base值
_read = myList.GetItemText(i, 3); //读取len 值
unsigned _len = strtoul(_read, NULL, 10);
_protecter.pushCode(va, _len, _base);
}
if (_protecter.Save(outFile.GetBuffer()))
{
AfxMessageBox(L"生成加壳程序成功!");
}
else
{
AfxMessageBox(L"生成加壳程序失败");
}
}
解析:通过获取列表数据对前面函数的调用。
总结:该工具可以实现对目标程序多段代码进行保护,通过设计插件注入到源程序依次解析我们生成的保护程序,来实现我们对源程序进行扩展的同时,与源程序实现高度绑定。



