Sublime Text是一款强大的文本编辑器,在不注册的情况下也可以使用,但标题栏的未注册字样与时不时弹出的nag窗口有时也让人感觉很不爽,于是尝试对其注册过程进行分析与破解。
截至写本篇文章时,Sublime Text的最新稳定版本为3143版本。由于之前我们破解过其旧版本,所以对其注册机制还算比较了解(已将之前破解Sublime Text 3126版本时写的笔记整理成了博文,请见《Sublime Text 3126 Win32版本暴力破解过程》)。如果有心的话,大家不妨对比两个版本的注册机制和流程,看看从3126更新到3143版本这一年多的时间里,作者为了增大破解的难度做出了哪些努力。毕竟在攻与防的较量中,大家都在进步,我们在提高自己二进制代码分析能力的同时,其实也有幸见证了软件作者的成长。
一、第一次尝试
在上个版本的Sublime中,我们很快就查到了注册相关的逻辑,然后打开了局面。针对新版本,我们当然希望能够故伎重演,就看作者给不给我们这个机会了。
首先随意输入一串注册码,点击确定后弹出错误提示框,此时中断程序,查看调用栈如下图。
根据MessageBoxW的调用者地址即可回溯到Wrapper函数头部。
从调用栈中可以看到再上帧的返回地址为0x00449817,为了保险,我们在Wrapper函数的头部下断后,重新输入一次错误的注册码看看:
试图继续回溯时发现不是第一现场(ecx为0,而主调方是用ecx寻址的)
很明显,这里把ecx清掉了。于是用IDA的交叉引用功能查到了调用源。emmmm,是JMP过去的,并且在之前清了ecx的值,作者也变猥琐了。
继续回溯:
分析发现ecx像是一个结构指针,其中第二个位置放的就是函数指针,第三个位置放的是传入的参数。于是利用函数指针地址下条件断点。显示从ecx开始,[ECX+4]==0x0044F4E0,在下面代码中注释部分标注了下断点的方法,由于ecx的值是通过esi取到的,于是[ESI+4]==0x0044F4E0,然后esi又是通过eax取到的,于是又更进下[[EAX+8]+4]==0x0044F4E0。果然这些条件断点都被命中了。
.text:00589A1D loc_589A1D:
.text:00589A1D 8B 70 08 mov esi, [eax+8] ;[[EAX+8]+4]==0x0044F4E0
.text:00589A20 8D 48 10 lea ecx, [eax+10h]
.text:00589A23 50 push eax
.text:00589A24 E8 B7 62 00 00 call sub_58FCE0
.text:00589A29 83 2D F8 30 8A+sub dword_8A30F8, 10h
.text:00589A30 8B 06 mov eax, [esi]
.text:00589A32 59 pop ecx
.text:00589A33 8B CE mov ecx, esi ;[ESI+4]==0x0044F4E0
.text:00589A35 FF 50 04 call dword ptr [eax+4] ;[ECX+4]==0x0044F4E0
.text:00589A38 8B 06 mov eax, [esi]
.text:00589A3A 8B CE mov ecx, esi
.text:00589A3C 6A 01 push 1
.text:00589A3E FF 10 call dword ptr [eax]
.text:00589A40 E8 3D 6B 00 00 call sub_59058
OD中在这句上下断:
00589A1D >|> 8B70 08 mov esi,dword ptr ds:[eax+0x8]
再来一次,断下后Ctrl+A分析一下程序,程序就显示出了跳转的来源,这里十分不巧,有两点,都下条件断判定一下:
再来一次,嗯,是从下面跳上来的:
此时eax中的值是关键,当前为0x03DCA1C0,应该是堆栈的地址。那是什么时候取出来的呢?
哇,全局变量来了,咱们终于落地了。只要有了全局变量,一切都好商量。
哈哈,全局就只有一处可以设值的,其他三处都是改写的。
大胆猜测,现在所在的函数就是从消息到消息响应函数的映射函数,而目标函数就是负责查出二者映射关系的函数。
结合IDA发现这些函数是通过消息循环被调用的:
WPARAM sub_58BA83()
{
signed int v0; // esi
_DWORD *v1; // eax
bool v2; // bl
int v3; // ecx
_DWORD *v4; // eax
bool v5; // bl
MSG Msg; // [esp+Ch] [ebp-64h]
char v8; // [esp+28h] [ebp-48h]
char v9; // [esp+30h] [ebp-40h]
char v10; // [esp+38h] [ebp-38h]
char v11; // [esp+40h] [ebp-30h]
int v12; // [esp+48h] [ebp-28h]
_DWORD *v13; // [esp+50h] [ebp-20h]
WPARAM v14; // [esp+58h] [ebp-18h]
int v15; // [esp+6Ch] [ebp-4h]
v14 = -1;
MsgHandler();
LABEL_2:
Msg.hwnd = 0;
memset(&Msg.message, 0, 0x18u);
v0 = 0;
while ( 1 )
{
if ( !PeekMessageW(&Msg, 0, 0, 0, 1u) )
{
sub_58EFBC(&v13);
v15 = 0;
while ( 1 )
{
v1 = (_DWORD *)sub_58EF7A(&v11);
v2 = v13 != (_DWORD *)*v1;
sub_452FC0(&v11);
if ( !v2 )
break;
sub_589E11(*v13);
sub_452FA3(&v9);
sub_452FC0(&v9);
}
v15 = -1;
sub_452FC0(&v13);
sub_590582();
if ( v0 == 2 )
MsgHandler();
if ( !PeekMessageW(&Msg, 0, 0, 0, 0) )
{
sub_58EFBC(&v12);
v15 = 1;
while ( 1 )
{
v4 = (_DWORD *)sub_58EF7A(&v10);
v5 = v12 != *v4;
sub_452FC0(&v10);
if ( !v5 )
break;
v3 = *(_DWORD *)(*(_DWORD *)v12 + 4);
if ( v3 )
(*(void (**)(void))(*(_DWORD *)v3 + 24))();
sub_452FA3(&v8);
sub_452FC0(&v8);
}
v15 = -1;
sub_452FC0(&v12);
sub_590582();
MsgWaitForMultipleObjectsEx(0, 0, 0xFFFFFFFF, 0x1CFFu, 6u);
}
goto LABEL_2;
}
if ( Msg.message == 18 )
break;
switch ( Msg.message )
{
case 0x7E9u:
if ( v0 )
{
if ( v0 == 1 )
v0 = 2;
}
else
{
MsgHandler();
v0 = 1;
}
break;
case 0x7EAu:
v14 = Msg.wParam;
break;
case 0x7EBu:
return v14;
default:
if ( (Msg.message == 256 || Msg.message == 257 || Msg.message == 260 || Msg.message == 261)
&& sub_589BFD(Msg.hwnd) )
{
if ( sub_58A100(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam) != 0 )
TranslateMessage(&Msg);
}
else
{
TranslateMessage(&Msg);
DispatchMessageW(&Msg);
}
sub_590582();
break;
}
}
PostQuitMessage(Msg.wParam);
return v14;
}
可以看到,Sublime的作者自定义了0x7E9消息,在其中完成了错误对话框的显示。并且经过分析,该消息至少还有如下两种触发方式:
1. 作者设置的定时器会调用MsgHandler函数
2. 该函数还负责处理其他窗口消息,如窗口获得焦点等等
因此,在该函数处下断或在SendMessage/PostMessage等函数上下断都会有较大的工作量,意味着我们要从众多干扰信息中找出真正关注的目标信息。并且即便找到了发送该消息的真身,也可能并不意味着我们找到了注册相关的逻辑。因为作者完全可以在二者之间再采用一些猥琐的方法增加熵值。基于上述分析,我决定放弃这种思路。
第二次尝试
Sublime的未注册版本中有一个nag窗口,是在用户保存文件到一定次数时触发的。依然采用下断MessageBoxW的方式,配合栈回溯和IDA静态分析,结果又来到了上面的0x7E9消息处理函数中。
这里就不再截图了,因为大部分图都和上面类似。
新思路
在思路二中,我们其实想利用未注册版Sublime的一些提示信息,如保存时的nag窗口、关于窗口中的未注册等等、标题栏的未注册字样。既然nag窗口走不通,我们就试试后面的吧。
搜索字符串:
放到IDA里看看流程图:
可以看到一个典型的分支结构。并且判断的条件是将一个结构的第一字节是否为0,如果为0则进入了未注册分支;且该结构为全局结构。因此可对其下硬件访问断点:
hr 0x008845E8
先暂时不忙启用该断点,而是在注册对话框中输入了key后启用该断点,然后确认,程序断在如下位置:
回溯即找到0x45075D的函数,依然是IDA结合OD的方法:
.text:0045075D B8 EC 97 76 00 mov eax, offset loc_7697EC
.text:00450762 E8 69 1B 31 00 call __EH_prolog
.text:00450767 81 EC 8C 00 00+sub esp, 8Ch
.text:0045076D 53 push ebx
.text:0045076E 56 push esi
.text:0045076F 57 push edi
.text:00450770 8B F9 mov edi, ecx
.text:00450772 8D 8D 68 FF FF+lea ecx, [ebp+lpszKeyUnicode]
.text:00450778 51 push ecx
.text:00450779 8B 87 E0 01 00+mov eax, [edi+1E0h]
.text:0045077F 8B 80 B4 00 00+mov eax, [eax+0B4h]
.text:00450785 8B 88 80 03 00+mov ecx, [eax+380h]
.text:0045078B E8 3F 9D 04 00 call GetKey
.text:00450790 33 DB xor ebx, ebx
.text:00450792 8D 4D D4 lea ecx, [ebp+lpszKey]
.text:00450795 8B D0 mov edx, eax
.text:00450797 ; try {
.text:00450797 89 5D FC mov [ebp+var_4], ebx
.text:0045079A E8 C6 28 07 00 call ConvertKeyToASCII
.text:0045079F 53 push ebx
.text:004507A0 6A 01 push 1
.text:004507A2 8D 8D 68 FF FF+lea ecx, [ebp+lpszKeyUnicode]
.text:004507A2 FF ; } // starts at 450797
.text:004507A8 ; try {
.text:004507A8 C6 45 FC 02 mov byte ptr [ebp+var_4], 2
.text:004507AC E8 0C 77 FC FF call FreeString
.text:004507B1 8B 8F D8 01 00+mov ecx, [edi+1D8h] ; 这里取到了注册结构
.text:004507B7 6A 0F push 0Fh
.text:004507B9 58 pop eax
.text:004507BA 89 45 9C mov [ebp+var_64], eax
.text:004507BD 89 45 B4 mov [ebp+var_4C], eax
.text:004507C0 8D 45 80 lea eax, [ebp+pOrgRegInfo]
.text:004507C3 50 push eax
.text:004507C4 66 89 5D 80 mov [ebp+pOrgRegInfo], bx ; 这里ebx是0,因此是在清空该结构的各字段
.text:004507C8 89 5D 84 mov [ebp+var_7C], ebx
.text:004507CB 89 5D 98 mov [ebp+var_68], ebx
.text:004507CE 88 5D 88 mov [ebp+var_78], bl
.text:004507D1 89 5D B0 mov [ebp+var_50], ebx
.text:004507D4 88 5D A0 mov [ebp+var_60], bl
.text:004507D7 E8 60 EF FF FF call CopyRegInfo ; 这里将清空后的注册结构拷贝给全局的注册结构
.text:004507DC 53 push ebx ; size_t
.text:004507DD 6A 01 push 1 ; char
.text:004507DF 8D 4D A0 lea ecx, [ebp+var_60] ; void *
.text:004507E2 E8 E2 56 FB FF call sub_405EC9
.text:004507E7 53 push ebx ; size_t
.text:004507E8 6A 01 push 1 ; char
.text:004507EA 8D 4D 88 lea ecx, [ebp+var_78] ; void *
.text:004507ED E8 D7 56 FB FF call sub_405EC9
.text:004507F2 39 5D E4 cmp [ebp+var_1C], ebx
.text:004507F5 0F 84 C4 01 00+jz loc_4509BF
.text:004507FB 8B 97 D8 01 00+mov edx, [edi+1D8h] ;这里取出了全局结构
.text:00450801 8D 4D D4 lea ecx, [ebp+lpszKey]
.text:00450804 8D 42 01 lea eax, [edx+1]
.text:00450807 50 push eax
.text:00450808 8D 45 EC lea eax, [ebp+lpParameter]
.text:0045080B 50 push eax
.text:0045080C 8D 42 04 lea eax, [edx+4]
.text:0045080F 83 C2 08 add edx, 8
.text:00450812 50 push eax
.text:00450813 E8 F6 F1 FF FF call CheckKey
.text:00450818 8B 8F D8 01 00+mov ecx, [edi+1D8h] ; 取出全局结构
.text:0045081E 83 C4 0C add esp, 0Ch
.text:00450821 8B F0 mov esi, eax
.text:00450823 83 FE 01 cmp esi, 1
.text:00450826 0F 94 C2 setz dl
.text:00450829 88 11 mov [ecx], dl
.text:0045082B 8B 8F D8 01 00+mov ecx, [edi+1D8h]
.text:00450831 38 19 cmp [ecx], bl
.text:00450833 74 13 jz short loc_450848
其中,CheckKey函数在注册码正确时应该返回1,紧接着就是对该返回值的判断:
可以看到,注册失败返回的是2。考虑到程序中可能还有其他地方调用注册验证函数,我们修改其头部代码,让其直接返回1即可:
下面是原验证函数的入口:
修改以后变成了这样:
此外,通过IDA的静态分析结合OD动态调试,在注册流程下面还开启了一个线程向服务器报告注册情况:
对应的线程是由下面这段代码开启的。
.text:00450938
.text:00450938 loc_450938: ; lpThreadId
.text:00450938 53 push ebx
.text:00450939 53 push ebx ; dwCreationFlags
.text:0045093A FF 75 EC push [ebp+lpParameter] ; lpParameter
.text:0045093D 68 73 F7 44 00 push offset sub_44F773 ; lpStartAddress
.text:00450942 53 push ebx ; dwStackSize
.text:00450943 53 push ebx ; lpThreadAttributes
.text:00450944 FF 15 1C 61 78+call ds:CreateThread
.text:0045094A 50 push eax ; hObject
.text:0045094B FF 15 3C 63 78+call ds:CloseHandle
.text:00450951 8B 87 D8 01 00+mov eax, [edi+1D8h]
.text:00450957 6A 0C push 0Ch
.text:00450959 80 78 01 00 cmp byte ptr [eax+1], 0
.text:0045095D 74 08 jz short loc_450967
在OD中查看如下:
本着不打扰作者也不被作者打扰的考量将其干掉:
首先干掉线程函数,直接使用头部ret大法即可。注意这里是__stdcall调用约定,线程函数传入一个lpParameter,因此应该为ret4.
下面再把负责CreateThread的那片代码也干掉:
虽然将传参、压栈过程全部用0x90填充了,但是由于CreateThread的某些参数(如函数入口)在可执行程序加载时涉及到重定位,因此还是直接用一个短跳转略过这一片比较好。至此,sublime破解完成。随意输入序列号即可完成注册。
在启动过程中,Sublime也会发送注册信息到验证服务器:
于是将其启动时连网进行版本检查的代码也干掉。还是在InternetOpenW处下断点,然后回溯到调用处:0x005A750D,进而回溯到函数头部,使用ret大法。
让程序启动即为破解版
对于懒人来说,连手动注册这一步都希望能省掉。根据前面的分析,我们知道程序是否注册是由一个全局结构来控制的。因此在启动过程中对该全局地址下硬件访问断点。看该地址中的值何时被读取:
在OD中设置硬件访问断点:hr 0x008845E8,然后重新运行程序。
很快断点命中:
可以看出,此时全局结构中注册标志为0,而这里取出了该标志与0判断。因此我们将其改为
or byte ptr [eax], 1
这样无论如何执行完该语句后,该结构中的值都为1了。再次运行程序,直接就成为了已注册版。至此破解完毕。
5870

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



