【文章标题】: SafeDisc 不完全剖析
【文章作者】: leexuany(小宝)
【作者邮箱】: leexuany@sina.com.cn
【软件名称】: 风来西林外传 For Windows
【下载地址】: 很久以前买的D版光盘
【保护方式】: SafeDisc 2.80.011
【使用工具】: OD,WinHex,VC6.0
【作者声明】: 为了促使该游戏的汉化重新启动,也可能什么作用都不起。
--------------------------------------------------------------------------------
【详细过程】
前言
前几天看到DS版的风来西林汉化已经可以下载了,不禁让我想起胎死腹中的风来西林外传 For Windows。汉化者在开始9个月后发布消息,说因为加了当时最新的SD2.8,无法破解,最终汉化被迫终止。时隔多年,再无音信……
一、准备工作
1、安装游戏,打好最新的补丁,运行一下,一切正常,退出。
2、OD载入,参考《Unpack SafeDisc v2.90.40 (更新代码修复)》一文运行至OEP,确定OD没问题。
3、来了解一下SafeDisc的运行流程。
SD加密的程序运行时,先将运行所需的动态链接库和调试器释放到临时文件夹,并创建调试进程。然后检测光盘,通过后解密程序代码,不过这些代码是加壳时预处理好的,存在很多问题代码。接下来,如果有加Logo就显示,之后与调试器通信,返回OEP。
运行时,由事先安置在代码中的间谍负责与壳代码和调试进程交互,直至程序结束。
二、推翻SafeDisc的三座大山
1、错误代码的修复
观察运行到OEP以后的程序代码,int3, ud2, 还有大量的翻译不出来的指令随处可见。如果直接按F9,系统还会向我们报告发生了错误,点击查看便看到错误发生的地址,Offset: 0012268d,在OD里查看一下,int3映入眼帘,正常代码会是这样的吗?这样的代码怎么运行呢?
正常情况下,SafeDisc会创建一个进程来调试程序本体。当程序执行到错误代码时会产生一个异常,执行中断。调试进程捕获这个异常,并根据异常发生的位置进行一系列的计算,求得正确代码,再根据计数器判断是否达到了频繁发生的水平,如果是则使用WriteProcessMemory还原代码,返回执行,如果不是,模拟代码,执行后返回。
正是这样,OD无法附加正在运行的游戏,或者,由OD启动的游戏无法通过错误代码。
在这种情况下,像《Unpack SafeDisc v2.90.40 (更新代码修复)》的作者那样直接把正常运行时游戏的代码段Dump出来是一个不错的选择,但我似乎没有那么幸运,有些代码的计数器没有达到还原的水平,依然有错误代码存在。
我该怎么办?难道要盯着错误代码,然后大喊“还我漂漂拳”?秋香是不会这样就出来的!
正常运行游戏,附加~e5d141.tmp进程(也就是SD创建的调试进程),之后在WriteProcessMemory下断了。好了,去玩游戏吧,万一什么时候触动了还原机制,我们就赚到了。
哈哈,断下了,写完数据,一路返回,沿途做好标记,看看到底是什么关系。
啊,WaitForDebugEvent,原来如此,那么在下一条指令下断,一有Debug事件就断下。
断下后又是漫长的分析,唉,不废话了,直接说算法。
先定义一个运算
unsigned int get_v2(unsigned int offset_RVA)
{
return (0xA0EDEC00 ^ 0x111EE638) * offset_RVA;
}
算法为:先对调用地址RVA和get_v2(调用地址RVA)共8字节计算MD5值,取第1个DWORD,以字节为单位高低逆序为key,在排序二叉树中查找,找到后使用第2个DWORD异或解密数据,解密后使用第4个DWORD校验。
最后编写程序对代码段穷举测试,150条数据,每条数据16字节,例如
05 00 02 B8 01 00 00 00 5D 5F 00 00 12 34 56 78
第1字节代表修复的长度,最长不超过7字节
第2、3字节作用不明,有相关判断,但比较分散,暂时不能确定
第4~10字节修复的数据
第11、12字节总是为零
第13~16字节用于校验。
鉴于有两字节数据含义不明,不要将解密出的数据直接写回文件中。
比较明智的做法是先Dump正常运行时的代码段,然后参考解密数据手工修复剩余错误代码。
如果有高人分析出第2、3字节的含义,烦请告诉我一声。
最后要确定搜索用的排序二叉树中的数据是哪里来的。
Ctrl+E双击~df394b.tmp模块,从代码开始出Ctrl+B搜索二进制串 00 EC ED A0,只有一处,代码如下:
100014BC |> \68 00ECEDA0 push A0EDEC00
100014C1 |. 58 pop eax
100014C2 |. 8945 FC mov dword ptr [ebp-4], eax
100014C5 |. 58 pop eax
100014C6 |. 8B45 FC mov eax, dword ptr [ebp-4]
100014C9 |. 24 00 and al, 0
100014CB |. 0305 F8C10510 add eax, dword ptr [1005C1F8] // 注意这里
100014D1 |. C9 leave
100014D2 \. C3 retn
其中[1005C1F8]里面也是一个解密用的参数,不过似乎没用到,这暂且不管,我们要关心的是这个地址,因为这个地址+4=1005C1FC就是我们要找的数据。
从1005C1FC开始3000字节就是我们要的解密信息,一共150条,每条20字节。
咦?不是16字节吗,怎么多了4字节?
别忘了我们在排序二叉树中搜索用的那个key不还4字节呢吗,不然比较什么啊。
可能有的朋友会问,你怎么知道就150条,怎么不是180条呢。
我写了个程序把SD用的二叉树遍历了一遍统计出来的,你也可以试试。不过要注意我们写二叉树叶子节点左右指针一般用NULL,而SD叶子节点的左右指针指向的是一个固定的节点,这个节点左右指针是NULL。
2、IAT争霸 混乱之治
同大多数加密壳一样,SD也加密了IAT,但这不算什么,最厉害的还是SD混乱调用的函数。虽然混乱,但不是胡乱,在计算机里一切都是有章法可循的。
随便找一个函数来看一下。
0051A282 . 6A 60 push 60
0051A284 . 68 30495300 push 00534930
0051A289 . E8 DE160000 call 0051B96C
0051A28E . BF 94000000 mov edi, 94
0051A293 . 8BC7 mov eax, edi
0051A295 . E8 36F6FFFF call 005198D0
0051A29A . 8965 E8 mov dword ptr [ebp-18], esp
0051A29D . 8BF4 mov esi, esp
0051A29F . 893E mov dword ptr [esi], edi
0051A2A1 . 56 push esi
0051A2A2 . FF15 90805200 call dword ptr [528090] // F7进入
0051A2A8 . 8B4E 10 mov ecx, dword ptr [esi+10]
到这里
01AA5043 68 F812EABF push BFEA12F8
01AA5048 9C pushfd
01AA5049 60 pushad
01AA504A 54 push esp
01AA504B 68 8350AA01 push 1AA5083 // 关键参数1
01AA5050 E8 40935A0E call ~df394b.1004E395
01AA5055 83C4 08 add esp, 8
01AA5058 6A 00 push 0
01AA505A 58 pop eax
01AA505B 61 popad
01AA505C 9D popfd
01AA505D C3 retn
把数据窗口调到1AA5083(关键参数1)看一下
01AA5083|07 00 00 00|0C 00 00 00|00 00 00 00|00 00 00 00
可以很明显地看到两个数值,第1个DWROD是00000007,我把它叫做目标模块编号,第2个DWORD是0000000C,我把它叫做默认函数编号。
那这两个参数是什么意思呢?这些参数的意思就是告诉壳代码我要调用0x00000007号模块的一个函数,这个函数的默认编号为0x0000000C. 注意,默认编号不一定就是最终调用的函数编号,符合某些条件时,还要重新计算。
下面就来看看函数调用的过程。
首先判断来自这个地址的CALL是不是已经调用过了。
1004D6A3 /$ 55 push ebp
1004D6A4 |. 8BEC mov ebp, esp
1004D6A6 |. 83EC 0C sub esp, 0C
1004D6A9 |. 8B45 10 mov eax, dword ptr [ebp+10] // 参数3,调用地址004F2D4F
1004D6AC |. 33D2 xor edx, edx
1004D6AE |. 6A 2F push 2F
1004D6B0 |. 59 pop ecx
1004D6B1 |. F7F1 div ecx // 除以$2F
1004D6B3 |. 8955 F8 mov dword ptr [ebp-8], edx // 保留余数,var_1 = offset % 0x2F
1004D6B6 |. 8B45 0C mov eax, dword ptr [ebp+C] // 参数2,默认函数编号
1004D6B9 |. 69C0 C3040000 imul eax, eax, 4C3
1004D6BF |. 8B4D 08 mov ecx, dword ptr [ebp+8] // 参数1,0x015F1218,一个IAT相关的指针(每个模块单独一个),指向的结构用来存储很多信息,比如N号函数默认的IAT表项地址,某地址是否调用过等等
1004D6C2 |. 03C8 add ecx, eax
1004D6C4 |. 8B45 F8 mov eax, dword ptr [ebp-8]
1004D6C7 |. 6BC0 18 imul eax, eax, 18
1004D6CA |. 8D4401 03 lea eax, dword ptr [ecx+eax+3] // 目的地址1 = (offset % 0x2F) * 0x18 + 参数1 + 默认函数编号 * 0x4C3
1004D6CE |. 8945 FC mov dword ptr [ebp-4], eax
1004D6D1 |. 8365 F4 00 and dword ptr [ebp-C], 0 // 清空计数器
1004D6D5 |. EB 07 jmp short 1004D6DE
1004D6D7 |> 8B45 F4 /mov eax, dword ptr [ebp-C]
1004D6DA |. 40 |inc eax
1004D6DB |. 8945 F4 |mov dword ptr [ebp-C], eax
1004D6DE |> 837D F4 03 cmp dword ptr [ebp-C], 3 // 计数器比较
1004D6E2 |. 73 1C |jnb short 1004D700 // for (i=0; i<3; i++)
1004D6E4 |. 8B45 F4 |mov eax, dword ptr [ebp-C]
1004D6E7 |. 8B4D FC |mov ecx, dword ptr [ebp-4] // 取出目的地址1
1004D6EA |. 8B04C1 |mov eax, dword ptr [ecx+eax*8]
1004D6ED |. 3B45 10 |cmp eax, dword ptr [ebp+10] // 同调用地址比较
1004D6F0 |. 75 0C |jnz short 1004D6FE
1004D6F2 |. 8B45 F4 |mov eax, dword ptr [ebp-C] // 如果相同,1004D6F0不跳,到这里
1004D6F5 |. 8B4D FC |mov ecx, dword ptr [ebp-4]
1004D6F8 |. 8B44C1 04 |mov eax, dword ptr [ecx+eax*8+4] // 取出目标函数的地址
1004D6FC |. EB 04 |jmp short 1004D702
1004D6FE |>^ EB D7 \jmp short 1004D6D7
1004D700 |> 33C0 xor eax, eax
1004D702 |> C9 leave
1004D703 \. C3 retn
函数最后用到一个简单的结构体
struct func_map {
unsigned int offset; // 调用此函数地址
unsigned int func_address; // 目标函数地址
};
首先判断是否是不是调用过了,如果是就直接根据保留下的信息进行调用。
先暂停一下,在继续前大家一起想想上面这个缓存下的信息对我们破解有什么用处。
恩,对啦,这些信息可以帮助我们快速的修复大量错误的函数调用。我们先打开游戏使劲玩上一会儿,把所有能做的操作都做一遍,然后写个程序把上面这些信息提取出来。就是这样,实际证明这个方法可以修复70%左右的函数调用,极大的减轻了我们手动修复的负担。而且只用这70%的函数,我们就可以让程序正确的运行起来,方便我们快速测试。
好了,继续,当壳代码发现这是一个新的函数调用时,来到如下代码:
1004E602 . FF75 F0 push dword ptr [ebp-10] // 从代码段开始计算的偏移量 0x004F2D4F - 0x00401000 = 0xF1D4F
1004E605 . E8 14070000 call 1004ED1E // 一个校验函数,用来初步判断这个地址是使用默认函数编号还是要重新计算。
1004E60A . 59 pop ecx
1004E60B . 0FB7C0 movzx eax, ax
1004E60E . 83F8 01 cmp eax, 1 // eax=1重新计算,其他时候使用默认函数编号
1004E611 . 0F85 8E000000 jnz 1004E6A5
1004E617 . 8B45 E4 mov eax, dword ptr [ebp-1C]
1004E61A . 69C0 C3040000 imul eax, eax, 4C3
1004E620 . 8B4D FC mov ecx, dword ptr [ebp-4]
1004E623 . 8B55 DC mov edx, dword ptr [ebp-24] 还是0x015F1218,那个IAT相关的指针
1004E626 . 8B49 02 mov ecx, dword ptr [ecx+2]
1004E629 . 3B8C02 AA0400>cmp ecx, dword ptr [edx+eax+4AA] // 判断1
1004E630 . 75 73 jnz short 1004E6A5
1004E632 . 8B45 FC mov eax, dword ptr [ebp-4]
1004E635 . 0FB600 movzx eax, byte ptr [eax]
1004E638 . 3D FF000000 cmp eax, 0FF // 判断2
1004E63D . 75 66 jnz short 1004E6A5
1004E63F . 8B45 FC mov eax, dword ptr [ebp-4]
1004E642 . 0FB640 01 movzx eax, byte ptr [eax+1]
1004E646 . 83F8 15 cmp eax, 15 // 判断3,这3处就是判断是不是call [00528XXX]的形式
1004E649 . 75 5A jnz short 1004E6A5 // 不是就跳走,使用默认函数编号,否则就重新计算编号
1004E64B . 8B45 E4 mov eax, dword ptr [ebp-1C]
看过这段代码之后,大家有在想什么呢?
我想到了几点,说出来,大家看看有没有落下什么。
<1>call 1004ED1E 一个校验函数,很重要,但是不难,简单模拟下,嵌入汇编或者LoadLibrary都可以。
<2>调用的时候要判断是不是call [00528XXX]的形式,这一点用处很多。
例如:mov edi, dword ptr [00528010]
call edi
我们不用劳神于类似这样的变形,对于那些经SD处理之后生成的代码也不担心,它们肯定都是调用默认编号指定的函数。
<3>注意判断1处,[IAT相关的指针 + 默认函数编号 * 0x4C3 + 0x4AA]就是默认编号对应的IAT表项地址00528090,换句话说就是我们可以推算出所有编号与IAT表项地址的对应关系,再确定编号与函数地址的关系就可以得到完美的IAT表了。
从$1004E64B~$1004E6A5之间就是重新计算编号的代码,只是简单的计算,这里就略过了,下面着重分析用模块编号和函数编号计算目标函数地址的代码。
在计算函数名和函数名长度时,SD使用内部函数编号,用目标函数编号求内部函数编号参考后面的公式。
1004E1D5 |. 8B45 08 mov eax, dword ptr [ebp+8] // 取出模块编号,暂时用□标记
1004E1D8 |. 69C0 8D000000 imul eax, eax, 8D
1004E1DE |. 8B0D B8CD0610 mov ecx, dword ptr [1006CDB8] // 这是一个固定指针,暂时用△标记
1004E1E4 |. 8B55 0C mov edx, dword ptr [ebp+C] // 取出内部函数编号
1004E1E7 |. 3B5401 58 cmp edx, dword ptr [ecx+eax+58] // [△+□*0x8D+0x58]=模块内函数个数,判断是否超出函数总数
1004E1EB |. 0F83 E1000000 jnb 1004E2D2
1004E1F1 |. 8B45 08 mov eax, dword ptr [ebp+8]
1004E1F4 |. 69C0 8D000000 imul eax, eax, 8D
1004E1FA |. 8B0D B8CD0610 mov ecx, dword ptr [1006CDB8]
1004E200 |. 8B8401 B50000>mov eax, dword ptr [ecx+eax+B5] // [△+□*0x8D+0xB5]=函数名字符串(加密的)地址列表首地址
1004E207 |. 8B4D 0C mov ecx, dword ptr [ebp+C]
1004E20A |. 8B0488 mov eax, dword ptr [eax+ecx*4] // 取出函数名字符串地址
1004E20D |. 8945 F4 mov dword ptr [ebp-C], eax
。。。。。。
1004E244 |> \8B45 08 mov eax, dword ptr [ebp+8]
1004E247 |. 69C0 8D000000 imul eax, eax, 8D
1004E24D |. 8B0D B8CD0610 mov ecx, dword ptr [1006CDB8]
1004E253 |. 8B8401 BF0000>mov eax, dword ptr [ecx+eax+BF] // [△+□*0x8D+0xBF]=函数名长度地址列表首地址
1004E25A |. 8B4D 0C mov ecx, dword ptr [ebp+C]
1004E25D |. 8B0488 mov eax, dword ptr [eax+ecx*4]
1004E260 |. 8B00 mov eax, dword ptr [eax] // 取出函数名长度
1004E262 |. 8945 E8 mov dword ptr [ebp-18], eax
1004E265 |. A1 B8CD0610 mov eax, dword ptr [1006CDB8]
1004E26A |. 83C0 26 add eax, 26 // △+0x26指向解密用的数据
1004E26D |. 50 push eax // 解密数据指针 unsigned char * arg1
1004E26E |. FF75 E8 push dword ptr [ebp-18] // 函数名长度 int func_name_length
1004E271 |. FF75 F4 push dword ptr [ebp-C] // 函数名(加密的)指针 char *func_name
1004E274 |. E8 437CFDFF call 10025EBC // 解密用的函数,有点难,我翻译不出C代码,直接LoadLibrary调用
1004E279 |. 83C4 0C add esp, 0C
至此我们已经看到了函数调用的曙光,$1004E274 call 10025EBC返回之后,函数名就摆在我们面前了。
这里总结一下函数调用中常用的几个地址。
设 △ = [1006CDB8];
□ = 模块编号;
N = 目标函数编号;(不一定是默认函数编号!)
○ = 内部函数编号;
那么
[△ + 0xF] = 模块总个数
[△ + 0x8D * □ + 0x58] = □模块内函数的总个数
[△ + 0x8D * □ + 0xB5] = □模块中函数名字符串指针列表首地址
[△ + 0x8D * □ + 0xBF] = □模块中函数名长度指针列表首地址
[△ + 0x8D * □ + 0x52] = □模块的句柄,就是GetProcAddress的第一个参数
[△ + 0x8D * □ + 0xC3] = □模块中那个与IAT相关的地址,暂用☆标记
[☆ + 0x4C3 * N + 0x4AA] = □模块中N号函数对应的IAT表项地址,N是未经过变换的。
[△ + 0x8D * □ + 0x4C] = □模块中用于把目标编号转换成内部编号的表的首地址,暂用★标记
○ = [★ + 4 * N] // 查表求得内部标号,计算函数名地址和函数名长度时使用内部编号○
最后编写2个程序,一个获得IAT表,一个穷举代码段中的call [00528XXX]并输出正确的地址。
注:《Unpack SafeDisc v2.90.40 (更新代码修复)》一文中通过修改代码强制写入原始IAT的方法与我写程序得到的IAT相同,那个方法不用自己写程序,推荐熟悉一下。
还有一部分函数调用伪装成了 jmp XXXXXXXX 的形式,这里提供一个快速修复的方法。先来看代码:
地址 大小 属主 区段
006E9000 00003000 AsfPc stxt774
006EC000 00004000 AsfPc stxt371
004012C9 . /74 25 je short 004012F0
004012CB . |6A 64 push 64
004012CD .-|E9 FA862E00 jmp 006E99CC // 跳到壳区段中去了,伪装的函数调用,跟进去
004012D2 . |8647 83 xchg byte ptr [edi-7D], al
004012D5 . |FF08 dec dword ptr [eax]
004012D7 .^|72 AE jb short 00401287
006E99CC 53 push ebx
006E99CD EB 06 jmp short 006E99D5
006E99D5 E8 CCFEFFFF call 006E98A6
006E99DA B2 43 mov dl, 43 // 记下这个地址1
006E98A6 870424 xchg dword ptr [esp], eax
006E98A9 9C pushfd
006E98AA 05 320F0000 add eax, 0F32 // eax=地址1+0x0F32
006E98AF 8B18 mov ebx, dword ptr [eax]
006E98B1 6BDB 13 imul ebx, ebx, 13
006E98B4 ^ EB C7 jmp short 006E987D
看下数据
006EA90C|C3 04 00 00|BF 04 62 01|.....
006E987D 0358 04 add ebx, dword ptr [eax+4] // ebx=0x4C3 * 0x13 + 0x016204BF=0x01625F38
006E9880 9D popfd
006E9881 58 pop eax
006E9882 871C24 xchg dword ptr [esp], ebx // 上面那个地址主要是用来计算调用的地址
006E9885 C3 retn
01625F38 68 D3124000 push 4012D3
01625F3D 68 3F12EABF push BFEA123F
01625F42 9C pushfd
01625F43 60 pushad
01625F44 54 push esp
01625F45 68 785F6201 push 1625F78 // 注意这个参数,修复关键全在这里
01625F4A E8 4684A20E call ~df394b.1004E395 // 原来还是这个函数
01625F4F 83C4 08 add esp, 8
01625F52 6A 00 push 0
01625F54 58 pop eax
01625F55 61 popad
01625F56 9D popfd
01625F57 C3 retn
再看下数据
01625F78|FF FF FF FF|FF FF FF FF|58 20 58 00|...
回忆一下正常调用时的参数是什么样的
01AA5083|07 00 00 00|0C 00 00 00|00 00 00 00|00 00 00 00
看出区别了吗?对了,$00582058就是正确的函数调用。
实际上进入函数1004E395后,程序会根据模块数、模块内函数数量以及[☆ + 0x4C3 * N + 0x4AA]这三个数据查询00582058这个地址,找到后再把模块编号、默认函数编号写入01625F78处,最后按正常调用运算。
再根据jmp xxxxxxxx不是call [xxxxxxxx]的形式,可推出00582058就是正确的函数调用。
好了,SD的混乱统治终结了,一个新的时代到来了。
3、伪装成CALL指令的跳转
Dump数据,修复了IAT,所有的函数调用,和大部分错误代码,应该可以运行了吧,F9不久,程序又停了下来,内存访问异常。
跟踪下代码,确定错误的范围,如下:
00402777 . 52 push edx
00402778 . 51 push ecx
00402779 . FF15 D4005000 call dword ptr [5000D4]
0040277F . 5F pop edi
00402780 > 8B8C24 080400>mov ecx, dword ptr [esp+408]
00402787 . 338C24 0C0400>xor ecx, dword ptr [esp+40C]
0040278E . 5E pop esi
0040278F . 81C4 08040000 add esp, 408
00402795 . E8 FF010000 call 00402999 // 出错的函数
0040279A 90 nop
0040279B 90 nop
0040279C 90 nop
00402999 /$ 51 push ecx
0040299A |. 50 push eax
0040299B |. E8 D3F1FFFF call 00401B73
004029A0 |$ 8B4424 0C mov eax, dword ptr [esp+C]
00401B73 $ B8 7B310000 mov eax, 317B
00401B78 . 59 pop ecx
00401B79 . 8D0408 lea eax, dword ptr [eax+ecx] // 还是计算一个关键地址0x405B1B
00401B7C . 8B00 mov eax, dword ptr [eax] // 取出0x10011387
00401B7E . FFE0 jmp eax // jump过去
10011387 . 58 pop eax
10011388 . 59 pop ecx
10011389 . 68 00004000 push 400000 // 算是一个小特征吧
1001138E . 9C pushfd
1001138F . 60 pushad
10011390 . 54 push esp
10011391 . E8 D2FFFFFF call 10011368
10011396 . 5C pop esp
10011397 . 61 popad
10011398 . 9D popfd
10011399 . C3 retn
跳来跳去,终于到了关键代码
100112BF |. 50 push eax
100112C0 |. FF75 08 push dword ptr [ebp+8]
100112C3 |. E8 E5FEFFFF call 100111AD // 初始化MD5参数
100112C8 |. 8B85 E8FDFFFF mov eax, dword ptr [ebp-218]
100112CE |. B9 38CE0610 mov ecx, 1006CE38
100112D3 |. 8BF8 mov edi, eax
100112D5 |. 2B43 04 sub eax, dword ptr [ebx+4]
100112D8 |. 50 push eax // 调用返回地址的RVA(0040279A-00400000=279A)
100112D9 |. E8 DCED0300 call 100500BA // 计算MD5
100112DE |. 50 push eax // eax 是MD5的前4字节,就是关键的key
100112DF |. E8 F1010000 call 100114D5
100112E4 |. 8BC8 mov ecx, eax
100112E6 E8 FC010000 call 100114E7 // 在一个128项的哈希表内查找key,算法有兴趣的自己看下,没有冲突时,顺序查找也是一样。
100112EB |. 8BF0 mov esi, eax
100112ED |. 85F6 test esi, esi
100112EF |. 74 3F je short 10011330
100112F1 |. 66:837B 08 01 cmp word ptr [ebx+8], 1
100112F6 |. 75 3D jnz short 10011335
100112F8 |. 8D85 30FDFFFF lea eax, dword ptr [ebp-2D0]
100112FE |. 8BCE mov ecx, esi
10011300 |. 50 push eax
10011301 |. E8 E4580100 call 10026BEA // 这里有模拟执行的代码,
10011306 |. 8BCB mov ecx, ebx
10011308 |. E8 8AFEFFFF call 10011197
1001130D |. 83F8 04 cmp eax, 4 // 比较1
10011310 |. 72 14 jb short 10011326
10011312 |. 8BCE mov ecx, esi
10011314 |. E8 B7570100 call 10026AD0
10011319 |. 83F8 04 cmp eax, 4 // 比较2(1和2有一个调用的次数,另一个不清楚)
1001131C |. 72 08 jb short 10011326
1001131E |. 57 push edi // 2次比较都不跳,会到这里
1001131F |. 8BCE mov ecx, esi
10011321 |. E8 FB570100 call 10026B21 // 内部有还原代码,如果前面跳走了,就不还原了
总结一下解密算法是先对调用返回地址的RVA(4 Bytes)求MD5,以结果的前4字节为Key,在存放还原信息的哈希表中查找。
找到之后模拟运行,再根据调用计数器判断是否还原代码,返回。
快速确定还原数据的方法,SafeDisc2.8中在还原信息前($1006CEB8)有很大一片0x0001,之后($1006CFB8)紧跟的就是还原信息。
还原信息的结构为
struct sd_info {
unsigned int size; // 使用前跟0x9877D4A7异或
unsigned int id; // 使用前跟0x6F68D2EB异或
unsigned int code2; // 使用前跟0xE2536C94异或
unsigned int code1; // 使用前跟0x07AEECEA异或
unsigned int zero[8];// 占位用,全是零
};
最后根据size的大小,将code1和code2拼接成原始数据,例如:
原始数据 size = 6; code1 = 000F8400; code2 = 00000175
拼接前 00 00 00 00 00 00 一块空内存
code1 0F 84 00
code2 75 01 00 00
拼接后 0F 84 75 01 00 00
最后同样写程序对代码段穷举一下,结果很快就出来了,128个刚刚好。
三、后记
写到最后反而没什么好写,那么删除stxt774和stxt371两个区段,重构一下文件吧。
14天说多不多,说少不少。我与safedisc的第一次邂逅就这样结束了。
风来西林外传的汉化版还要等多久呢?
--------------------------------------------------------------------------------
2009年05月16日 22:43:15
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课