本文是对上一篇记录《Sublime Text 3143 Win32版本暴力破解过程》的补充之一。
整个破解工作到目前为止,似乎一切都很顺利,随便输入一个序列号就能完成注册了。然而今早起床刷牙时猛然想起一个细节:Sublime的作者在3126版本中就已经有了对注册函数进行验证的环节,故意传入了一个全0的参数,此时作为异常情况,注册函数应该返回代表注册失败的值,当初我们第一次破解3126版本的时候就因为没发现这点而折戟沉沙。
排查遇坑
一个本就在不断对抗破解,手段越来越恶劣、套路越来越猥琐的作者,会在新版本放弃这种方式吗?我不相信这种奇迹会发生。于是在IDA中找到注册函数:0x0044FA0E,Ctrl+X查找交叉引用,可以看到如下地方调用了该函数:
一共五处调用,先看第一处:
嗨呀,看到上面一串全0字符串顿时有种不妙的感觉,吓得我赶紧打开了OD。在目标位置下断后运行程序:注册函数两次被断下,第一次传入了我们之前输入的key,而第二次,就来到了这里:
ecx中传入的是一个字符串对象,其中第一个字段即为字符串指针:
来看看是何方神圣:
哇,社会是真的险恶。这种验证码就算是闭着眼睛也知道是有问题的。调用注册码校验函数完成后,果不其然,作者开始将返回值和1匹配(在Sublime 3143版本中,注册函数返回1表示注册成功),果然城里人套路深啊!
作者的套路1
接着分析,如果我们不问青红皂白地将注册函数修改为无论合适都返回注册成功,那就中了作者的套路了,下面这段代码就会执行:
.text:00426DAE 6A 0C push 0Ch ; size_t
.text:00426DB0 E8 6E DF 2F 00 call ??2@YAPAXI@Z ; 对象大小为12
.text:00426DB5 8B 4D 6C mov ecx, [ebp+6Ch] ; ecx指向全局注册结构
.text:00426DB8 C7 04 24 00 53+mov [esp+0FCh+var_FC], 75300h
.text:00426DBF 50 push eax
.text:00426DC0 89 48 08 mov [eax+8], ecx ; 新对象第三个成员为全局对象大小
.text:00426DC3 B9 04 31 8A 00 mov ecx, offset dword_8A3104
.text:00426DC8 89 45 60 mov [ebp+60h], eax ; 新对象保存起来了
.text:00426DCB C7 00 EC E7 7E+mov dword ptr [eax], offset ??_7callback_work_item@@6B@ ; const callback_work_item::`vftable'
.text:00426DD1 C7 40 04 63 6C+mov dword ptr [eax+4], offset sub_426C63
.text:00426DD8 E8 5E 68 17 00 call sub_59D63B ; 压栈两个参数,顶到底分别是:新创建对象的地址,0x75300,0xC
.text:00426DD8 ; ecx指向全局地址,疑似为消息响应
这里构造的就是一个Handler(我们在第一次尝试中分析过,其中第三个成员即是参数(全局注册结构),其中第二个成员即是处理函数),看看其内容:
.text:00426C63 sub_426C63 proc near
.text:00426C63
.text:00426C63 arg_0= dword ptr 4
.text:00426C63
.text:00426C63 8B 44 24 04 mov eax, [esp+arg_0]
.text:00426C67 C6 00 00 mov byte ptr [eax], 0
.text:00426C6A C3 retn
.text:00426C6A sub_426C63
很明显,被触发后,该函数会清空全局注册结构中的已注册标志。那么能不能按照Sublime 3126版本那样,在注册函数中判断edx的值是否为0或者通过堆栈压入的参数是否为0来区分是作者故意测试还是真的在校验注册码呢?
作者的套路2
第三次断点命中时情况如下:
这时,ECX中字符串地址为我们之前输入的验证码,EDX为0,传入的其他参数情况也是0:
但此时,注册函数仍然识别出了是正确的注册码,应返回1,并且结构偏移18处的整数应不为0,否则又会注册一个清空全局注册标志的handler上去。
将计就计
偏移18位置的整数是什么呢?我们并不清楚。但既然放在了全局结构中,想必是与注册信息有关的。于是对该字段下硬件访问断点,然后随意输入一串注册信息:
点击注册后,断在了上述位置。此时edi值为0,正将0写入esi+0x10位置,也就是0x00884600位置。在IDA中分析一下:
void __thiscall sub_405EC9(int g_pstRegInfo, char pUnknown, size_t nLength)
{
int _pstRegInfo; // esi
void *v4; // ebx
bool bTrue; // cf
_pstRegInfo = g_pstRegInfo;
if ( pUnknown && *(g_pstRegInfo + 0x14) >= 0x10u )
{
v4 = *g_pstRegInfo;
if ( nLength )
memmove_0(g_pstRegInfo, v4, nLength);
std::_Deallocate(v4, *(_pstRegInfo + 20) + 1, 1u);
}
*(_pstRegInfo + 0x14) = 0xF;
bTrue = *(_pstRegInfo + 0x14) < 0x10u;
*(_pstRegInfo + 0x10) = nLength;
if ( !bTrue )
_pstRegInfo = *_pstRegInfo;
*(_pstRegInfo + nLength) = 0;
}
通过这段我们知道了一个信息:
| 0x008845E8 | 一字节注册信息 | |
|---|---|---|
| 0x008845EC | ||
| 0x008845F0 | 从这开始是pRegInfo,存放用户名 | 0 |
| 0x008845F4 | 4 | |
| 0x008845F8 | 8 | |
| 0x008845FC | C | |
| 0x00884600 | 用户名长度 | 10 |
| 0x00884604 | 0xF | 14 这里是否大于16很重要 |
如果0x00884604位置大于等于16,则从0x008845F0位置取出地址,拷贝nLength位到0x008845F0,并且释放掉原来的内容,然后将0x00884604的值设为0xF,并将0x00884600的值设置为nLength,然后将从0x008845F0开始的,第nLength位置为0。
尽管目前仍然有点迷糊,但很明显,这里进行的是字符串操作。如果串长大于等于15,则取其前nLength位复制到0x008845F0,并添加结尾的NULL标识。同时将0x00884600设置为长度。我高度怀疑这里是在进行注册用户名的提取。如果用户名很长,那么0x008845F0存放的指向用户名的指针,否则就将用户名直接放在0x008845F0,其最大的容量为15(预留1位存放结尾的0标志),然后紧接着0x00884600放其长度。这样就解释的通了!
这个猜测可以立即加以验证,手动修改0x008845F0处的内容如下:
然后查看注册信息:
哈哈,我们的猜想得到了证实。下面就可以动手改注册函数的代码了。
修改注册函数,实现完美破解
接下来对注册函数进行修改,实现所谓完美破解,即注册时输入用户名即完成破解,且输入的用户名可以在关于窗口查看:
由于程序包含重定位信息,为了不影响ASLR机制的正常工作,有三点需要注意:
在代码中不出现绝对地址,要么需要自己根据某个全局地址(这里其实是有条件的,因为注册信息就是全局存放的)算出偏移,手动修正;否则就得加重定位表项
如果要调用外部API,则要么直接采用call到IAT的方法(E8+相对偏移),前提是sublime已经导入了该函数
也干脆直接想办法找kernel32基址,然后解析其导出表,只要知道LoadLibrary的地址就行,当然,顺带获取到GetProcAddress的地址就更方便了。但这种方法有被杀软误报的风险。
咱们尽量偷懒,注册函数有几点需要满足的:
正常注册情况下,应该返回1
如果是作者故意传入的那组错误的注册码,则应该返回2
正常情况下,获取用户输入,将用户输入的前15位作为注册用户名填入(不满15位的,取相应位数即可)
下面开始修改:
ecx中指向的是用户输入的key,edx如果不为0的话则指向全局注册信息结构。那么用户输入的key附近有没有存放其大小呢?如果有的话,咱们就不用自己写汇编去求了。本着“大海捞针,总不死心”的方针,在周围转转,还真有收获!
注意到偏移0x10位置就是字符串的长度。此外,在输入key的过程中,还发现如果输入的key长度小于等于0xF,则直接存在ECX指向的缓冲区中,否则将其指针存入ECX指向的结构中。
有了上面的信息,咱们就可以来写一个完美版的注册函数了。
0044FA0E 55 push ebp
0044FA0F 53 push ebx
0044FA10 56 push esi
0044FA11 57 push edi
0044FA12 B8 FB967600 mov eax,sublime_.007696FB ;这里有重定位信息,该语句保留不动
0044FA17 8BEC mov ebp,esp
0044FA19 83EC 38 sub esp,0x38
0044FA1C 33C0 xor eax,eax
0044FA1E 40 inc eax ;默认情况下设置返回值为1(注册成功)
0044FA1F 85D2 test edx,edx ;是否传入了全局注册信息结构
0044FA21 74 1F je short sublime_.0044FA42 ;没传入,作者在挖坑,单独处理
0044FA23 8BFA mov edi,edx ;传入了,将注册信息复制为用户名
0044FA25 8079 10 0F cmp byte ptr ds:[ecx+0x10],0xF ;比较用户输入是否大于15位
0044FA29 7F 08 jg short sublime_.0044FA33 ;大于15位,则[ECX]中为指针
0044FA2B 8BF1 mov esi,ecx ;小于15位,直接存在ecx指向的缓冲区中
0044FA2D 0FB649 10 movzx ecx,byte ptr ds:[ecx+0x10] ;待拷贝长度设置为输入字符串长度
0044FA31 EB 07 jmp short sublime_.0044FA3A
0044FA33 8B31 mov esi,dword ptr ds:[ecx] ;大于15位,取指针为字符串
0044FA35 B9 0F000000 mov ecx,0xF ;只取15位
0044FA3A FC cld ;通用过程,拷贝字符串到注册信息中
0044FA3B 894A 10 mov dword ptr ds:[edx+0x10],ecx
0044FA3E F3:A4 rep movs byte ptr es:[edi],byte ptr ds:[esi]
0044FA40 EB 2A jmp short sublime_.0044FA6C ;拷贝完后,跳到退出处
0044FA42 8079 10 4F cmp byte ptr ds:[ecx+0x10],0x4F ;从上面判断edx处跳来,判断是不是作者构造的非法key(该key长度为0x4F)
0044FA46 75 24 jnz short sublime_.0044FA6C ;不是则调到退出处
0044FA48 8B39 mov edi,dword ptr ds:[ecx] ;开始判断是否是作者故意传入的非法key
;非法key偏0x26的位置为一串0,每串0x20个,然后是一个0xA,这样一共重复9组
0044FA4A 8D7F 26 lea edi,dword ptr ds:[edi+0x26] ;从第一组0开始
0044FA4D 50 push eax ;暂存返回值
0044FA4E B0 30 mov al,0x30 ;找'0'
0044FA50 B9 29010000 mov ecx,0x129 ;最多找0x129次(到达末尾了)
0044FA55 8BF7 mov esi,edi ;暂存edi到esi
0044FA57 F3:AE repe scas byte ptr es:[edi] ;扫描目标串,直到ecx为0或找到非'0'字符
0044FA59 57 push edi ;暂存edi
0044FA5A 2BFE sub edi,esi ;求'0'串长,
0044FA5C 83FF 21 cmp edi,0x21 ;'0'串长若是0x20(向后挪了一格)
0044FA5F 5F pop edi ;恢复edi的值
0044FA60 75 03 jnz short sublime_.0044FA65 ;'0'串长若是0x20(向后挪了一格)
0044FA62 42 inc edx ;循环计数器自增,能跳到这个分支edx定为0
0044FA63 ^ EB F0 jmp short sublime_.0044FA55 ;跳回去继续执行(模拟了一个for循环)
0044FA65 83FA 09 cmp edx,0x9 ;循环结束后,判断是不是有9组0
0044FA68 58 pop eax ;恢复返回值
0044FA69 75 01 jnz short sublime_.0044FA6C ;如果不是,则是恰巧0x4F的串,返回1
0044FA6B 40 inc eax ;否则就是作者构造的用于钓鱼的串,返回2
0044FA6C 8BE5 mov esp,ebp
0044FA6E 5F pop edi
0044FA6F 5E pop esi
0044FA70 5B pop ebx
0044FA71 5D pop ebp
0044FA72 C3 retn
测试效果
保存后进行测试:
输入注册信息
查看注册信息
可以看到输入的注册信息出现在了关于中。

被折叠的 条评论
为什么被折叠?



