本帖最后由 Sound 于 2018-2-5 14:22 编辑
0x00 前段时间实在是太忙了,没时间看这个cm,最近闲下来了, 准备开搞。
sd大佬来玩 cm 了 爆破就成 算法太简单 https://www.52pojie.cn/thread-682451-1-1.html
0x01 初步了解这个cm
要求玩家输入一串字符串, 如果错误则显示fail, 如果正确...应该就会显示其他信息, 作者没有给出成功后的截图, PE中也没有发现这类字符串...
拖进调试器, 发现这个cm的算法是通过虚拟机加密了的, 难怪作者说只要爆破就可以了, 废话不多,直接进入下一个步奏吧.
0x02 一个带虚拟机加密的cm
首先查虚拟机壳的相关信息, 发现毛都没看到, 但使用PE工具查看节表信息发现有个sp0101节, 直接反汇编这个节, 发现这里
就是那一处虚拟机..
因为这个虚拟机应该是作者自写的, 所以我也决定手动来会会这个虚拟机..
加密段进入虚拟机的代码:
虚拟机入口代码:
虚拟机的过程大概是 -> 保存现场 -> 设置esi edi vmesp -> 设置重定位字段 -> 进入第一个handler
有兴趣的朋友可以自行跟踪了解, 这里就不详细述说了.
0x03 虚拟机自效验
我想手写hook记录hanlder的日志信息, 发现只要hook就会出问题, 仔细操作发现只要在虚拟机任意位置下一个断点(int3)然后直接运行就会发生同样的错误,
猜测虚拟机有自效验, 这就尴尬了, 想直接hook是不可能的了, 得先过这个自效验才行..
错误:
经过多次尝试发现, 仅仅只是修改虚拟机以及虚拟机的跳转表以及虚拟机内 jmp dword [eax * 4 + X] 的最后4字节会发生这个错误, 作者显然是不想别人动他的虚拟机了..
不过作者没有效验整个PE文件, 这就给我们留了很大的空间发挥了, 如果作者这样做了, 那这个搞起来就很伤身体了..
0x04 六耳猕猴
现在先搞定它的自效验.
步奏如下:
1. 使用loadpe工具复制sp0101节.
2. 使用loadpe工具添加复制的节到末尾.
3. 删除重定位节. (因为虚拟机里面有很多地方都有重定位, 把这些重定位一并修复显然蛋疼, 还是直接删掉重定位爽~)
4. 使用任意十六进制工具打开cm0.exe 查找加密段的最后的那个jmp, 请看图1, 将其跳转修改为跳转到我们复制的虚拟机入口处.
*** 到这里之后, 我们先保存这个exe, 取个名字为: 修改至第一阶段.exe, 之后每次修改都从这个exe复制一份出来改名为:cm0.exe ***
现在只是将虚拟机入口修改好了, 现在要修正跳转表, 否则跳转表依然指向的是之前的虚拟机位置, 跳转表有多大不清楚.. 只能去查特征,
发现几乎所有的handler最后一行代码都是 jmp dword ptr[eax * 4 + X], 且X是虚拟机内唯一的特征, 那么就好办了, 写一段代码现查然后在
重新指向我们拷贝的虚拟机节中的跳转表(不能修改原始的, 因为原始的依然会被检测.). 前面说了X是虚拟机内唯一, 但是我们拷贝了一份, 因此
在2处位置会撞脸, 我们首先手动将我们拷贝那一处手动替换成新的跳转表地址吧, 然后在写代码查询这个就一定是唯一的了.
手动替换跳转表地址, 新的跳转表VA为: 0x0047534a, 我使用的工具是HxD, 使用替换功能就可以了, 记得是从新虚拟机的位置开始哦, 如图:
查询修正跳转表代码如下
[Asm] 纯文本查看 复制代码 start.
//
// 打开cm0.exe
CFMBuffer fp("cm0.exe");
//
// 移动至虚拟机机器码开始位置
fp.FM_MOV_SET(0x6cc00);
//
// 获取虚拟机机器码 0x134a 为虚拟机机器码的总长度, 使用7534a(跳转表RVA) - 74000(新虚拟机节RVA) == 134a 但我不确定是不是, 不过使用134a貌似没有问题
string vm_bc;
char *vm_bytecode = new char[0x134a];
fp.FM_R_DATA(vm_bytecode, 0x134a);
vm_bc.append(vm_bytecode, 0x134a);
//
// 确定 跳转表的个数
DWORD Addr = 0;
DWORD table_cnt = 0;
for (;;)
{
char Number[5] = { 0x4a, 0x53, 0x47, 0x00, 0x00 };
Addr = vm_bc.find(Number, Addr);
if (Addr == -1)
{
break;
}
table_cnt++;
Addr += 4;
}
printf("一共查询到跳转表数据:%d个.\n", table_cnt);
//
// 获取跳转表中的VA
vector<DWORD> table_data;
for (DWORD i = 0; i < table_cnt; ++i)
{
DWORD data = 0;
data = fp.FM_R_UINT32();
table_data.push_back(data);
}
fp.FM_OFF();
fp.FM_ON_AB();
fp.FM_MOV_SET(0x6cc00 + 0x134a);
//
// 根据table_cnt 替换跳转表
for (DWORD i = 0; i < table_cnt; ++i)
{
DWORD data = table_data[i] - 0x28000 + 0x74000;
fp.FM_W_UINT32(data);
}
printf("一共重写跳转表数据:%d个.\n", table_cnt);
fp.FM_OFF();
查询修正跳转表代码如下 end.
运行结果:
现在运行处理后的cm0.exe, 发现会GG, 查看问题发现代码异常还是跟之前图3一样.. 这是为啥, 仔细检查确定该修改的都修改了,
为什么还是出问题呢? 百思不得其解, 吃饭去了, 这里就告一段落.. - _ -
0x05 花世界
上面遇到挫折后猜测了很多可能, 但是由于虚拟机有大量花指令插在里面影响阅读, 所以决定去花然后看看有没有什么异常的地方..
前面说了手动干, 因此去花也决定手动写代码解决, 其实这个虚拟机使用的花并不多, 也就5 6 7朵吧, 还都是一些古老的花指令, 去起来也好办.
由于代码太长我就不发了, 截图发个效果吧.
去花结果:
虚拟机去花后的效果:
现在虚拟机干净了, 就是很多nop好长, 不过至少可以正常阅读了, 花了一些时间快速扫了很久, 发现一处handler:
[Asm] 纯文本查看 复制代码 VA: 00474D40
00474D40 push eax
00474D41 push edx
00474D42 push ecx
... nop 无关紧要省略
00474D51 push eax
00474D52 fldz // ------> 注意这个
... nop 无关紧要省略
00474D62 mov ecx,0xFF08AD38
00474D67 not ecx
... nop 无关紧要省略
00474D79 wait
00474D7A fstenv [esp-0xC] // ------> 注意这个
00474D7E pop ecx
00474D7F lea esp,[esp+0xC]
00474D83 push ecx
00474D84 lodsb
00474D85 xor al,0xD8
00474D87 not al
00474D89 sub al,0x76
00474D8B add bl,al
00474D8D not bl
00474D8F movzx eax,al
00474D92 jmp dword ptr [eax*4+0x47534A]
这个handler是获取当前运行时的eip的, 我在原始的cm0.exe这个handler上下断点将栈顶数据改为一个很大的值, 然后去掉断点, 发现GG了, 又是那个错误..
经过很多次的实验, 我发现这个虚拟机居然还检测当前位置是否在合法的范围内, 应该就是用来防止拷贝虚拟机过自效验的, 现在知道了这个handler我们只需要
强行修改它, 在栈顶设置一个跟原始cm0.exe一模一样的值就可以了.
方法: 从新虚拟机的位置开始搜索特征码, 找到这个handler, 然后自行发挥修改即可, 我的修改如下:
代码:
[Asm] 纯文本查看 复制代码 UCHAR bc[] = {
0x50, 0x52, 0x51, 0x90, 0x90, 0x90, 0x68, 0x58, 0x8C, 0x42, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x59, 0x8D,
0x64, 0x24, 0x0C, 0x51, 0xAC, 0x34, 0xD8, 0xF6, 0xD0, 0x2C, 0x76, 0x02, 0xD8, 0xF6, 0xD3, 0x0F,
0xB6, 0xC0, 0xFF, 0x24, 0x85, 0x4A, 0x53, 0x47, 0x00
};
CFMBuffer fp("cm0.exe");
fp.FM_OFF();
fp.FM_ON_AB();
//
// 移动至虚拟机机器码开始位置 与 handler 的开始位置
fp.FM_MOV_SET(0x6cc00 + 0xD40);
fp.FM_W_DATA(bc, sizeof(bc));
fp.FM_OFF();
printf("修改handler_geteip代码已经完成.\n");
效果图:
然后在运行一次cm0.exe, 哈哈哈哈哈哈啊, 完美运行, OK鸟, 虚拟机自效验算是完整突破(因为我们修改了一个handler, 还是可以正常运行), 接下来就可以回到最前面, hook~~
0x06 糟糕的体力活 hook
这里hook我使用了最简单的方法, inline hook, 一步一步的hook这个虚拟机使用的handler, 然后打印出日志信息, 有了详细的日志信息, 基本上在notepad++上通过查询一些关键数据
就可以突破了。
下面是我hook的一些handler:
由于hook的细节有一些繁琐, 这里就不说了, 总而言之就是记录每个handler的作用, 以及当时的参数 数据等信息, 我录下来的日志是这样的:
现在有了这些日志, 但是完整的日志显然太大了, 这非常不利于我们分析, 于是我又想了一个办法, 过滤掉cm0.exe输入之前的所有记录, 于是我加了个
Sleep(6000), 运行后6秒重新创建一个日志文件, 将输入以及之后的handler保存到另外一个单独的文件中, 这样我们就可以直接分析我们关心的handler逻辑了。
虽然几句话就说完了hook, 但实际动手去搞真的很蛋疼, 需要一点点耐心.
0x07 大海捞针之抽象定位
很久以前, 我这样分析一个VM, 都会将表达式整出来, 这样做其实是相当消耗时间跟耐心的, 但这次我没有这样做, 而是从爆破出发, 所以我只关心了一个指令: VMSetPC.
从所有handler找到操作esi寄存器的handler, 这个十有八九就是VMSetPC指令, 然后hook, 打印出所有我们关心的信息, 然后看日志:
我们看到有一行日志是: VMSetPC key:[0x95], h:[0x74], pc:[0x0004684a], 当前pc:[0x0046cb45], 跳转pc:[0x0046fe55].
key是跳转过去后修正的key
h是跳转过去后下一个指令的handler
pc是字节码内的RVA
当前pc是指执行这个VMSetPC时esi的值
跳转的pc是指esi被修改后的值
有了这些数据, 一切都好办了, 现在唯一要解决的问题就是怎么爆破, 因为我们输入的数据是胡乱输入的, 因此那个跳转pc, 一定是跳到提示错误的地方去了,
所以我们要找到如果它不是要跳到那, 那究竟是要跳到哪...
这个需要分析这个jcc的vm指令, 向上找日志, 搜索关键字0x0004684a, 向上找看看这个0x0004684a是怎么被计算出来的.
向上找到第一个0x004684a, 如图:
其中日志: VMNulNor32 [0x03e08020], [0xfc1b1795], 结果:[0x0004684a].
是一个nor操作, 那我们向上跟踪这3个值, 可以发现是5个nor得出的, 很显然, 这是一个xor, 也就是说, 0x0004684a是一个数被解密出来的, 这个数是第一个nor里面的:
VMNulNor32 [0xfc1b5f95], [0xfc1f37df], 结果:[0x03e08020]. 加上后面的4个nor, 其实就是 xor 0xfc1b5f95, 0xfc1f37df, 知道了加密方法, 那我们看看哪个是被加密的数值,
继续向上查询这两个数, 直到找不到为止, 就一定是第一次出现的地方.
找到如下日志:
日志内容:
VMPushImm32 [0x0000005d].
VMPushImm32 [0x00000074].
VMPushImm32 [0x00000046].
VMPushImm32 [0x00000095].
VMPushImm32 [0xfc1f6aae].
VMPushImm32 [0xfc1f37df].
里面包含了数据 0xfc1f37df, 那么就好办了, 它上面的那个一定会是跳到正确路径的RVA了, 我们拿出它跟 0xfc1b5f95 xor 得到RVA 0x0004353B,
那么不用犹豫了, 上面的其他数据也是跟着配套的, 请回顾:
VMSetPC key:[0x95], h:[0x74], pc:[0x0004684a], 当前pc:[0x0046cb45], 跳转pc:[0x0046fe55].
那我们这个VMSetPC要强制修正为:
VMSetPC key:[0x46], h:[0x5d], pc:[0x0004353B] (为什么只要改这3个? 因为只有这3个是参数, 后面2个是影响后的数据, 不能修改.)
那么我们回到hook的地方, 自己写段代码强行将数据修改为我上面的指出的数据即可.
然后在尝试录制一次日志, 你会发现, 嘿嘿, 现在有2个VMSetPC了, 这表示我们之前的修改是完全正确的, 因此, 我们只要使用这个方法, 一直修改下去,
就可以爆破这个cm0.exe了, 为了节省篇幅, 我直接截图我这边的修改吧.
好了, 当这些补丁全部分析完打上去之后, 爆破就成功了, 效果:
0x08 总结
1 制造六耳猕猴 过掉自效验
2 多想想有可能导致自效验依然生效的原因
3 利用日志的关键字搜索 快速定位核心信息
4 要有耐心
题外话:
这个cm0.exe里面使用的虚拟机, 只是作者拿出来娱乐的虚拟机,
真正有强度的虚拟机跟这个虚拟机的架构完全是不一样的, 无法使用相同的方法过自效验, 也没有跳转表, 想找handler非常不容易, 总之架构已经完全不同,
以后你们遇见sp0101的虚拟机壳, 很可能就是作者的SProtect壳了. 这些是之后我向作者请教才了解的,
52cm.rar
(1.48 MB, 下载次数: 121)
|