编写Ollydbg插件对某国产强壳 CB版本进行脱壳修复
本帖最后由 寞叶 于 2023-9-14 14:04 编辑本来是想出个很详细的脱壳教程的从upx开始教,录了几个小时语音没声音。心态有点崩,基础的内容就略过了,放音乐当背景了。
我录了个视频演示教程,建议和本文一起食用(视频分析的是最近的样本,本文使用的是去年底的发现的一个很适合拿来分析的crackme)
叠甲声明:
1、本文不提供任何破解成品与样本。(无法保证安全性)
2、插件是我花了很长时间,修了无数个bug才写出来的,开源在github。如果你觉得本插件不错,可以给我一个免费的评分或者star,这也能给我继续更新文章的动力。(详细用例在仓库,功能如下截图)
3、你不应该将此插件用于任何不适当的行为包括但不限于将此插件源码或编译后的成品进行售卖,进行任何违法行为等等。
4、此插件是实验性的,可能不完善并且存在bug(比如在分析时摇晃od的主窗口会使得od崩溃,以及不知原因崩在qemu中等等)
5、如果你觉得本文有任何不妥或者描述有误的地方还请联系我进行更改
Free Link
【应官方要求,现已删除开源代码,不提供任何文件下载】
视频教程链接:https://share.weiyun.com/h1gSqpuG 密码:52pj52
先简单介绍一下插件的框架,再进行分析
插件所用到的库
1、ollydbg sdk (我修复了一些sdk中的bug,找bug找了几天甚至对编译器失去信任,最后发现是sdk的问题,6)
2、unicorn2.0.1 (仿真,自动化分析)
3、capstone4.0.2(提供强大的反汇编功能)
Analyzer.h, Analyzer.cpp
此源代码主要提供反汇编的功能(我删除了一些不必要的代码)
Emulator.h, Emulator.cpp
此源代码对unicorn进行二次封装,主要提供模拟器与OD进行数据交互、模拟执行的功能
dllmain.cpp
插件的主要逻辑,具体函数实现都在此。提供方便的字符串、数据格式提取功能,多段内存dump提取功能,od 一键trace功能,一键模拟执行,api调用跟踪、通用iat修复、内存访问分析功能等等
接下来结合代码与截图进行脱壳分析
1、找到程序OEP
考虑到源程序是易语言程序,对GetVersion进行下断即可断在oep下方
2、iat加密分析
可以发现api调用被加密成了call; nop的形式
在执行了以下代码后跳转到GetVersion
004D854D 57 push edi
004D854E 9C pushfd
004D854F E9 30000000 jmp SPceshi_.004D8584
004D8584 BF 00000000 mov edi,0x0
004D8589 E9 27000000 jmp SPceshi_.004D85B5
004D85B5 8DBF 07C14B00 lea edi,dword ptr ds:
004D85BB^ EB DD jmp short SPceshi_.004D859A
004D859A 8DBF A75CED42 lea edi,dword ptr ds:
004D85A0 E9 05000000 jmp SPceshi_.004D85AA
004D85AA 8DBF EDAB13BD lea edi,dword ptr ds:
004D85B0^ EB DF jmp short SPceshi_.004D8591
004D8591 8B3F mov edi,dword ptr ds:
004D8593 8B3F mov edi,dword ptr ds:
004D8595^ EB C0 jmp short SPceshi_.004D8557
004D8557 47 inc edi
004D8558 81C7 970D90CF add edi,0xCF900D97
004D855E C1CF 1B ror edi,0x1B
004D8561 C1C7 0B rol edi,0xB
004D8564 81F7 259B3F25 xor edi,0x253F9B25
004D856A 8DBF D4F7BBBD lea edi,dword ptr ds:
004D8570 E9 03000000 jmp SPceshi_.004D8578
004D8578 877C24 04 xchg dword ptr ss:,edi
004D857C E9 26000000 jmp SPceshi_.004D85A7
004D85A7 9D popfd
004D85A8 C3 retn
我看了几个api调用都是一样的形式
首先算出一个地址,这个地址里存放了一个动态分配的内存地址,这个地址里存了加密后的api地址,解密后通过ret跳转。
有点像shellcode
我最先是用od trace进行测试修复,但是我发现速度慢的不正常,然后发现有部分api调用被虚拟化
解密部分被虚拟化,通过计算加密数据,新建一个jmp表ret过去的方法就不是很好用。
3、编写iat修复代码
加密前调用的三种类型
call dword ptr
jmp dword ptr
mov reg, dword ptr
加密后均为call nop
如何区分加密前的类型?
1、将模拟eip指向api调用前,一直模拟直至遇到api调用或者eip处于代码段。如果处于代码段,那就是mov类型,否则是 call或者jmp
2、判断【esp】处的数值,如果位于call的下方,则为call类型否则为jmp类型
3、将模拟器除esp外7个寄存器置0,通过模拟结束时寄存器的值判断mov reg,dword ptr 是哪个寄存器
代码太长,请参考dllmain.cpp中的函数(有注释)
DWORD __stdcall FixSpIAT(LPVOID lpThreadParameter)
OD提供了函数 Findsymbolicname可以查询地址与符号关联性,用它来识别具体是调用的哪个api
然后是iat表重建,不同名称的函数用4个字节存放,不同dll的函数之间用0间隔
if (vec_iat_data.empty())
{
MessageBoxA(0, "未查找到需要修复的地方", "提示", MB_TOPMOST | MB_ICONWARNING | MB_OK);
return 0;
}
sort(vec_iat_data.begin(), vec_iat_data.end()); //排序
DWORD tmp_addr = tmp_iat_begin; //当前api存放的地址
char tmp = {}; //汇编字符串缓冲区
char errtext;
t_asmmodel asmmodel;
vec_iat_data.push_back({}); //末尾标记,并且防越界
for (DWORD i = 0; i < vec_iat_data.size() - 1; i++)
{
_Progress(i * 1000 / (vec_iat_data.size() - 1), (char*)"重建IAT表中...进度");
switch (vec_iat_data.type)
{
case 1:
_Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
_Writememory((char*)"\xFF\x25", vec_iat_data.fix_addr, 2, MM_SILENT);
_Writememory(&tmp_addr, vec_iat_data.fix_addr + 2, 4, MM_SILENT);
break;
//在这种修复方式下,不需要call类型的修复
// case 2:
// _Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
// _Writememory((char*)"\x58\xFF\x25", vec_iat_data.fix_addr, 3, MM_SILENT);
// _Writememory(&tmp_addr, vec_iat_data.fix_addr + 3, 4, MM_SILENT);
// break;
case 3:
_Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
sprintf_s(tmp, "mov eax, dword ptr [%08X]", tmp_addr);
_Assemble(tmp, vec_iat_data.fix_addr, &asmmodel, 0, 0, errtext);
_Writememory(asmmodel.code, vec_iat_data.fix_addr, asmmodel.length, MM_SILENT);
_Writememory((void*)"\xC3", vec_iat_data.fix_addr + 6, 1, MM_SILENT);
break;
case 4:
_Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
sprintf_s(tmp, "mov ecx, dword ptr [%08X]", tmp_addr);
_Assemble(tmp, vec_iat_data.fix_addr, &asmmodel, 0, 0, errtext);
_Writememory(asmmodel.code, vec_iat_data.fix_addr, asmmodel.length, MM_SILENT);
_Writememory((void*)"\xC3", vec_iat_data.fix_addr + 6, 1, MM_SILENT);
break;
case 5:
_Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
sprintf_s(tmp, "mov edx, dword ptr [%08X]", tmp_addr);
_Assemble(tmp, vec_iat_data.fix_addr, &asmmodel, 0, 0, errtext);
_Writememory(asmmodel.code, vec_iat_data.fix_addr, asmmodel.length, MM_SILENT);
_Writememory((void*)"\xC3", vec_iat_data.fix_addr + 6, 1, MM_SILENT);
break;
case 6:
_Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
sprintf_s(tmp, "mov ebx, dword ptr [%08X]", tmp_addr);
_Assemble(tmp, vec_iat_data.fix_addr, &asmmodel, 0, 0, errtext);
_Writememory(asmmodel.code, vec_iat_data.fix_addr, asmmodel.length, MM_SILENT);
_Writememory((void*)"\xC3", vec_iat_data.fix_addr + 6, 1, MM_SILENT);
break;
case 7:
_Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
sprintf_s(tmp, "mov ebp, dword ptr [%08X]", tmp_addr);
_Assemble(tmp, vec_iat_data.fix_addr, &asmmodel, 0, 0, errtext);
_Writememory(asmmodel.code, vec_iat_data.fix_addr, asmmodel.length, MM_SILENT);
_Writememory((void*)"\xC3", vec_iat_data.fix_addr + 6, 1, MM_SILENT);
break;
case 8:
_Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
sprintf_s(tmp, "mov esi, dword ptr [%08X]", tmp_addr);
_Assemble(tmp, vec_iat_data.fix_addr, &asmmodel, 0, 0, errtext);
_Writememory(asmmodel.code, vec_iat_data.fix_addr, asmmodel.length, MM_SILENT);
_Writememory((void*)"\xC3", vec_iat_data.fix_addr + 6, 1, MM_SILENT);
break;
case 9:
_Writememory(&vec_iat_data.api_addr, tmp_addr, 4, MM_SILENT);
sprintf_s(tmp, "mov edi, dword ptr [%08X]", tmp_addr);
_Assemble(tmp, vec_iat_data.fix_addr, &asmmodel, 0, 0, errtext);
_Writememory(asmmodel.code, vec_iat_data.fix_addr, asmmodel.length, MM_SILENT);
_Writememory((void*)"\xC3", vec_iat_data.fix_addr + 6, 1, MM_SILENT);
break;
default:
MessageBoxA(0, "重建IAT表时异常:不正确的类型", "错误", MB_TOPMOST | MB_ICONERROR | MB_OK);
break;
}
if (vec_iat_data.dll_name != vec_iat_data.dll_name)
{
//不同dll的函数集合,用0隔开
DWORD data = 0;
tmp_addr += 4;
_Writememory(&data, tmp_addr, 4, MM_SILENT);
tmp_addr += 4;
}
else if (vec_iat_data.api_name != vec_iat_data.api_name)
{
//如果不是相同的函数名,代表碰到了新的函数,需要扩大4字节放它的地址
tmp_addr += 4;
}
}`
然后用scylla dump并且重建输入表即可
载入修复的程序,如果将你的程序拖入虚拟机中能f8步过GetVersion调用,就代表到此为止是成功的
antidump分析
此易语言程序的几个库函数地方被虚拟化并塞入antidump代码
函数头部直接jmp到虚拟机入口
antidump不过,程序会直接异常或者死循环
使用插件内存访问分析(具体代码请参考DWORD __stdcall MemAccessAnalysis(LPVOID lpThreadParameter))
该函数可记录访问的敏感内存区域与执行过的特殊的指令如(rdtsc,cpuid)
第一个antidump函数分析如图
可以看出程序校验了分配的内存存放的数据,资源段,代码段api调用是否被修复,枚举区段数量判断是否被脱壳,记录的进程pid
后续还有几个antidump,是越来越复杂,还有不同的antidump方法,不一一演示了。
大致的流程是这样的
--保存环境--
进行各种antidump校验
--恢复环境--
执行原来代码
我个人是想到了几个修复antidump的方法
1、直接对虚拟机handler进行hook(缺点:累,handler太多)
2、还原整个易语言库函数(缺点:特征匹配,要求熟悉语言特征)
3、还原函数头+补数据
下文使用第三种方法
图中的Write .svmp表示程序已经执行过了antidump
还原函数头加补数据如图
即可绕过此antidump,虽然后续执行的反dump更多而且不一样,均可像这样绕过。最后一处要补的数据挺多的
这样补完就行了,如果有sdk的话,上述方法就不好使了。视具体情况采用不同方法
脱壳分析就到这结束
反dump的方法(个人思路)
PE结构层次的反dump
1、修改SizeofImage
2、检测VirtualAddress,区段数量等等
3、检测代码段是否被修改,api调用是否被修复
4、Stolen code 偷走几个字节
基于内存的反dump
把部分数据存放到堆或者栈上,dump无法保存这些数据,会丢失
程序运行环境反dump
进程id,线程id,进程运行时间,程序的启动时间等等
系统环境反dump
cpuid
系统启动时间
用户名
cpu?显卡?硬盘大小?
思路打开,只要是另一个电脑没有的数据,你都可以获取一个保存甚至主动创造一个
赞一个{:1_921:}
第一个在论坛里面把CB版本的AntiDump讲得体无完肤的, 膜拜
call nop是很久很久以前的cb版本, 为了解决这个特征, 我改成reg call, A版本更是加入了ret指令
AntiDump讲得不错了, 但也有人会直接根据api来过掉检测
CB版本的AntiDump最大的缺点就是加密的代码是集中执行的, 而且是在最后才执行, 所以只要过掉了前面的检测, 就行了.
A版本开始每个版本的AntiDump都有一些不同, 跟CB相差还是很多的, 被加密的代码会随机分散在检测的代码中一起执行了, 需要多花一些时间抠出来, 但我相信难不倒你{:1_921:}
楼主用心了, {:1_921:}{:1_921:}{:1_921:}
我需要思考一下是否更新一下CB这个古老的版本{:1_937:} ->来自SProtect最最最最最最原始的打工人
大佬威武,学习学习 谢谢分享好内容,学习下 OD要是能升级下就好了 界面太乱 版本也老了 大佬大佬 有没有生成好的没有安装VS也不知道用的是哪个版的VS 视频教程声音丢了真是太可惜了,虽然不太用od,x96dbg用的多一点,但还是感谢大佬。 感谢分享 这么优秀的帖子坐等加精{:1_921:} 你好,我想学习没有声音的教程,请问能否提供一份?