编写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);

相关推荐
西岸行者2 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意2 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码2 天前
嵌入式学习路线
学习
毛小茛3 天前
计算机系统概论——校验码
学习
babe小鑫3 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms3 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下3 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。3 天前
2026.2.25监控学习
学习
im_AMBER3 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J3 天前
从“Hello World“ 开始 C++
c语言·c++·学习