编写VMP爆破插件 (上)

使用工具:VS 2022 Ollydbg SDK Ollydbg sdk插件文档 Ollydbg Unicorn Capstone之前发过一个JCC爆破的插件,这里说一下开发的过程,最后会把源码开源出来由于帖子长度原因,本教程分为上下两期,上期为JCC爆破的实现(由于长度原因,只写到寄存器部分没写完),中期为对 JCC爆破插件 的完整代码(整个项目解决方案),下期为对IAT还原的实现(理论通杀全网iat抽取,哪怕虚拟)

第一步,需要去下载OD的SDK
这里用了寞叶修复过后的SDK,未修复前的SDK有bug
这个sdk在最后会有上传

好,第一步就来了
我们需要把所有导出函数都写好
ODBG_Plugindata 插件名
ODBG_Plugininit 插件初始化,可以加上od版本判断
ODBG_Pluginmenu显示菜单项 (也就是我们右键和在上面插件选项的那些东西)
PM_MAIN 主窗口
PM_DISASM 反汇编窗口
PM_CPUDUMP 内存数据窗口

|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 | if (origin == PM_MAIN) { ``strcpy_s(data, 4096, ``"0&关于"``); } if (origin == PM_DISASM) { ``strcpy_s(data, 4096, ``"JCC{0&第一个,1&第二个,2&关于,3&使用说明}"``); } |

ODBG_Pluginaction 点击执行函数,也就是我们在菜单选择某个选项,在这里写对应的执行功能
ODBG_Pluginreset 可选回调参数
ODBG_Plugindestroy退出od时调用此函数
以上就是我们od sdk里面的几个函数用途了

现在我们把这些东西写好
下面是我写的,可以参考修改
extern "C" __declspec(dllexport) int ODBG_Plugindata(char* shortname)
{
// h_exp_handler = AddVectoredExceptionHandler(0, ExceptionHandler);
// if (h_exp_handler == NULL)
// {
// _Addtolist(0, 1, "注册异常处理函数失败");
// }
const char* pluginName = "JCC 爆破插件"; 插件Name
strcpy_s(shortname, strlen(pluginName) + 1, pluginName);
return PLUGIN_VERSION;
}

extern "C" __declspec(dllexport) int ODBG_Plugininit(int ollydbgversion, HWND hw, ulong * features)
{
char msg[200] = {};
sprintf_s(msg, " 编译时间:%s %s", DATE, TIME);
_Addtolist(0, 0, "JCC爆破插件 V:1.0");
_Addtolist(0, -1, msg);
if (ollydbgversion < PLUGIN_VERSION) 版本判断
{
MessageBoxA(hw, "本插件不支持当前版本OD!", "鹤の妙妙屋 V:1.0", MB_TOPMOST | MB_ICONERROR | MB_OK); 信息框
return -1;
}
//g_hOllyDbg = hw;
return 0;
}

extern "C" __declspec(dllexport) cdecl int ODBG_Pluginmenu(int origin, char data[4096], VOID * item)
{
if (origin == PM_MAIN)
{
strcpy_s(data, 4096, "0&关于"); 主菜单的关于
}
if (origin == PM_DISASM)
{
strcpy_s(data, 4096, "JCC{0&不映射代码段,1&映射代码段,2&关于,3&使用说明}"); 反汇编窗口的选项
}
return 1;
}

extern "C" __declspec(dllexport) cdecl void ODBG_Pluginaction(int origin, int action, VOID * item)
{
//在主窗口点击
if (origin == PM_MAIN)
{
if (action == 0)
{
char msg[256];
sprintf_s(msg, "开发:鹤の妙妙屋 时间:2023年");
MessageBoxA(g_hOllyDbg, msg, "关于", MB_TOPMOST | MB_ICONINFORMATION | MB_OK);
}
}
//在反汇编窗口点击
if (origin == PM_DISASM)
{
if (action == 0)
{
UnicornMoNi(); 刚刚的第一个选项 不映射代码段
}
else if (action == 1)
{
UnicornMoNi2(); 刚刚的第二个选项 映射代码段 这里可以用线程,我这里没用,可以自己后面加
}
else if (action == 2)
{
char msg[256];
sprintf_s(msg, "开发:吾爱汇编 时间:2023年");
MessageBoxA(g_hOllyDbg, msg, "关于", MB_TOPMOST | MB_ICONINFORMATION | MB_OK);
}
else if (action == 3)
{
char msg[256];
sprintf_s(msg, "1.不映射代码段 不映射指定内存的数据,更好定位函数调用流程\n2.映射代码段 映射代码段,方便进行函数分析时跳出vm段执行代码的问题,这里采用retn退出虚拟机,可能会有错误的问题");
MessageBoxA(g_hOllyDbg, msg, "说明", MB_TOPMOST | MB_ICONINFORMATION | MB_OK);
}
}

extern "C" __declspec(dllexport) cdecl void ODBG_Pluginreset()
{
//本插件不操作此函数
}

extern "C" __declspec(dllexport) cdecl void ODBG_Plugindestroy()
{
//本插件不操作此函数
}

上面差不多就是一些函数的代码了
然后下面到了Unicorn代码的实现了
如果你看过我以前发的帖子,可以知道我是采用手动dump内存的方式来映射内存模拟,寄存器也是手动输入的,这样效率十分低(主要是累人,之前分析个软件搞了几个小时)现在写成od插件的形式调用十分方便

在这里呢,我们以前的算法也是可用的
然后,第一步需要先导入capstone,unicorn lib,头文件 这些
导入完成后可以开始代码的编写
定义一个新函数

|-------|------------------------|
| 1 2 3 | int Unicorn(){ } |

然后准备开始写代码,实战
首先在dllmain.cpp加上几个变量声明
uc_err err; //UC执行返回值
uc_engine* uc; //UC引擎
cs_insn* insn //captone 引擎
然后第一步首先初始化unicorn,一定不要忘记(因为这个问题之前分析了一天)

err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc); //初始化引擎
if (err != UC_ERR_OK) { //如果不等于OK(成功)则进入判断
_Addtolist(0, 0, "uc模拟器初始化错误 UCOPEN"); //Addtolist 在od日志输出内容 这里没有成功初始化判断错误
return -1;
}

下一步该弹出一个输入框了,在这里输入代码段的地址,

//
函数显示一个用于用户输入 8-, 16- 或 32-位 的任意 3 种格式的整数:十六进制、无符号十进制、有符号十进制,或者仅输入十六进制格式(如果设置了 DIA_HEXONLY 位)。可选的复选框"Entire block"(全局块)和
"Aligned search" (定位搜索)受位 DIA_ASKGLOBAL 和 DIA_ALIGNED 控制,并控制全局标识 globalsearch 和 alignedsearch。返回 0 表示成功,如果用户取消或有错误发生,返回 -1 表示失败,函数 Getlongxy 附加包含对话窗口在屏幕的 xy 坐标位置。

int Getlong(char *title,ulong *data,int datasize,char letter,int mode);
int Getlongxy(char *title,ulong *data,int datasize,char letter,int mode,int x,int y);

参数:

title ?对话框标题;

data ?指向32 位缓冲区,包含初始的整数值。函数返回后,该值是最后修改值,如果用户取消,该值是保留在缓冲区的最后值;

datasize ?整数字节大小(1, 2 或 4),注意:由 data 指向的缓冲区必须是 32 位(4字节)长,其不依赖于 datasize;

letter ?编辑控件缺省已输入的首字符,如果没有输入就为 0。这对已经用本函数让用户输入了相应字符时非常有用;

mode - DIA_xxx 位的组合值指定 Getlong 附加特征:

DIA_HEXONLY 隐藏十进制输入窗口
DIA_ASKGLOBAL 显示复选框 "Entire block"(全部块)来控制全局搜索标识。实际标识值可以通过调用 Plugingetvalue(VAL_GLOBALSEARCH) 来返回
DIA_ALIGNED 显示复选框 "Aligned search"(对齐搜索)来控制对齐搜索标识。实际标识值可以通过调用 Plugingetvalue(VAL_ALIGNEDSEARCH) 来返回
x ?绝对屏幕 X 坐标,像素,为对话框左下角位置,如果需要, 对话框会自动适应位置并保持可见;

y - 绝对屏幕 Y 坐标,像素,为对话框右下角位置。
//

我们采用getlong api 函数
还要定义一个变量来存地址
下面是代码示例

t_memory* NeiCun; 定义od内存属性
DWORD data = 0; //编辑框数据存储变量
DWORD AddressVMP; //VMP地址
if (_Getlong("内存地址", &data, 4, '0', DIA_HEXONLY) == 0) //输入VMP区段头部地址
{
AddressVMP = data;
}

if (AddressVMP != 0 && AddressVMP !=NULL) { 编辑框返回的内容不为0 且 不为空 这里NULL一定要加,不然如果他点返回就会崩溃
NeiCun = _Findmemory(AddressVMP); 获取内存地址对应的内存块属性 比如我输入一个401004 他属于401000 就返回401000这个内存块的数据,比如大小之类的
}
else {
_Addtolist(0, 0, "输入地址错误");
return 0;
}

这里我们就得到了代码段的内存属性
然后开始哪一步了?
该内存映射了,不然uc怎么运行

关于这里的内存映射,我借助了寞叶的内存映射·-·
t_table* memory_table = (t_table*)_Plugingetvalue(VAL_MEMORY);
t_sorted memory_data = memory_table->data;
for (DWORD i = 0; i < memory_data.n; i++)
{
t_memory* memory = (t_memory*)_Getsortedbyselection(&memory_data, i);
BYTE* buf = new BYTE[memory->size];
DWORD ret = _Readmemory(buf, memory->base, memory->size, MM_SILENT);
if (memory->base != NeiCun->base) { 读取的内存块 不等于 代码段内存块 才进行映射操作
if (ret != memory->size)
{
_Addtolist(0, 1, "读取内存 0x%08x-0x%08x 失败 实际读取%08x", memory->base, memory->base + memory->size, ret); //读取内存失败
}
if (!MapFromMemory(memory->base, memory->size, buf)) 映射内存函数
{
_Addtolist(0, 1, "映射内存 0x%08x-0x%08x 失败", memory->base, memory->base + memory->size);
}
delete[] buf;
_Progress((i + 1) * 1000 / memory_data.n, "正在映射内存 0x%08x-0x%08x 进度", memory->base, memory->base + memory->size);
}
_Progress(0, 0); //刷新od内存
}

bool MapFromMemory(const DWORD& base, const DWORD& size, void* buf)
{
err = uc_mem_map(uc, base, size, UC_PROT_ALL); 先创建一个新的内存,后面才能写代码进去
if (err != UC_ERR_OK)
{
return false;
}
err = uc_mem_write(uc, base, buf, size); 写入内存块数据进UC
if (err != UC_ERR_OK)
{
return false;
}
return true;
}

内存映射也完成了,现在可以初始化capstone反汇编引擎了,你后面初始化也行,我就先初始化了
if (cs_open(CS_ARCH_X86, CS_MODE_32, &handle)) {
_Addtolist(0, 0, "CS初始化错误 CSOPEN");
return -1;
}
cs_option(handle, CS_OPT_DETAIL, CS_OPT_ON); 打开DETAIL属性,打开效率会降低,不过我们分析vm需要这个属性

好,这里反汇编引擎也初始化完毕,下面可以开始初始化寄存器数据了
t_thread* thread = _Findthread(_Getcputhreadid()); 获取当前调试的属性
const t_reg& treg = thread->reg; 当前调试属性下的寄存器属性
DWORD eax, ecx, edx, ebx, esp, ebp, esi, edi, eip, efl = 0; 定义八个变量存储寄存器数据
eax = treg.r[REG_EAX];
ecx = treg.r[REG_ECX];
edx = treg.r[REG_EDX];
ebx = treg.r[REG_EBX];
esp = treg.r[REG_ESP];
ebp = treg.r[REG_EBP];
esi = treg.r[REG_ESI];
edi = treg.r[REG_EDI];
eip = treg.ip;
efl = treg.flags;

//写入UC中的寄存器
uc_reg_write(uc, UC_X86_REG_EAX, &eax);
uc_reg_write(uc, UC_X86_REG_ECX, &ecx);
uc_reg_write(uc, UC_X86_REG_EDX, &edx);
uc_reg_write(uc, UC_X86_REG_EBX, &ebx);
uc_reg_write(uc, UC_X86_REG_ESP, &esp);
uc_reg_write(uc, UC_X86_REG_EBP, &ebp);
uc_reg_write(uc, UC_X86_REG_ESI, &esi);
uc_reg_write(uc, UC_X86_REG_EDI, &edi);
uc_reg_write(uc, UC_X86_REG_EIP, &eip);
uc_reg_write(uc, UC_X86_REG_EFLAGS, &efl);

相关推荐
小猪佩奇TONY2 小时前
OpenGL-ES 学习(18) ---- 实例化渲染
学习·elasticsearch·信息可视化
一只乔哇噻3 小时前
java后端工程师+AI大模型开发进修ing(研一版‖day61)
java·开发语言·学习·算法·语言模型
车载测试工程师3 小时前
CAPL学习-SOME/IP交互层-回调函数
学习·tcp/ip·交互·以太网·capl·canoe
jtymyxmz3 小时前
《Maya 2024 超级学习手册》3.4.7 实例:制作哑铃模型
学习
柠檬水不加冰_3 小时前
Angular学习记录
javascript·学习·angular.js
学编程的闹钟3 小时前
100【form表单】
学习
嵌入式小能手3 小时前
飞凌嵌入式ElfBoard-文件I/O的深入学习之文件锁
java·服务器·学习
南清的coding日记3 小时前
从零开始学习微调简历分析大模型01 - LLaMA-Factory 扫盲
学习·语言模型
BlackWolfSky3 小时前
ES6 学习笔记3—7数值的扩展、8函数的扩展
前端·javascript·笔记·学习·es6