【文章标题】: 1-Crack_Me-凉飕飕.zip 破解说明
【文章作者】: SilentGamb@pediy.com
【作者主页】: http://bbs.pediy.com/member.php?u=492631
【软件名称】: 1-Crack_Me-凉飕飕.zip
【软件大小】: 223 KB (228,352 字节)
【下载地址】: http://ctf.pediy.com/?game-fight-2.htm
【加壳方式】: 无
【保护方式】: 反调试
【编写语言】: vs2008
【使用工具】: 52pjOD1.1 + IDA6.1
【操作平台】: win7X64
【软件介绍】: 看雪CTF 2016 - 第一题
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
破解过程,我写了调试记录, 调试过程是一个凌乱试错的过程,调试记录的连贯性不强。
本破文摘自调试记录.
给作者的建议
* 即使程序被调试,也不要让程序报错. 容易让攻击者看出来. 让程序逻辑错乱好些. e.g. 程序被调试,就不走正常的注册码比对流程.
* 发布的程序不要编译成DEBUG版程序, 程序的体积会小很多. Debug版程序没有优化,比Release版容易看一些.
* 分段逐个判断注册码,只会增加攻击者的工作量,因为有一条正确的路引导攻击者找到注册码,根本不用写公式,在每个判断注册码的点上,只有唯一正确的选择.
动态基地址
cm用vs2008编译的, 打开了动态基地址选项.
后4位是相同的,可以和IDA对在一起.
发现反调试
00EE2BC9 . E8 43950000 call Crack_Me.00EEC111
过了这步挂了.
00EEC151 |. FF55 F8 |call dword ptr ss:[ebp-0x8] ; Crack_Me.00EE2B41
00EEC164 |. E8 5C6BFFFF call Crack_Me.00EE2CC5
00EE4A37 . E8 D5760000 call Crack_Me.00EEC111
00EE4A5C . E8 2A000000 call Crack_Me.00EE4A8B
00214A90 |. E8 FC910000 call Crack_Me.0021DC91
00964A5C . E8 2A000000 call Crack_Me.00964A8B // 这里就直接退出了
退出的处理
00134A8B /$ 8BFF mov edi,edi
00134A8D |. 55 push ebp
00134A8E |. 8BEC mov ebp,esp
00134A90 |. E8 FC910000 call Crack_Me.0013DC91
00134A95 |. 84C0 test al,al
00134A97 |. 74 20 je short Crack_Me.00134AB9
00134A99 |. 64:A1 3000000>mov eax,dword ptr fs:[0x30]
00134A9F |. 8B40 68 mov eax,dword ptr ds:[eax+0x68]
00134AA2 |. C1E8 08 shr eax,0x8
00134AA5 |. A8 01 test al,0x1
00134AA7 |. 75 10 jnz short Crack_Me.00134AB9
00134AA9 |. FF75 08 push dword ptr ss:[ebp+0x8] ; /ExitCode = 7EFDE000 (2130567168.)
00134AAC |. FF15 A4801400 call dword ptr ds:[<&KERNEL32.GetCurrent>; |[GetCurrentProcess
00134AB2 |. 50 push eax ; |hProcess = 766D3358
00134AB3 |. FF15 A8801400 call dword ptr ds:[<&KERNEL32.TerminateP>; \TerminateProcess
00134AB9 |> FF75 08 push dword ptr ss:[ebp+0x8]
00134ABC |. E8 0B000000 call Crack_Me.00134ACC
00134AC1 |. 59 pop ecx ; kernel32.766D336A
00134AC2 |. FF75 08 push dword ptr ss:[ebp+0x8] ; /ExitCode = 0x7EFDE000
00134AC5 \. FF15 E8801400 call dword ptr ds:[<&KERNEL32.ExitProces>; \ExitProcess
00134ACB CC int3
异常处理
.text:0040322A sub_40322A proc near ; CODE XREF: sub_402B41p
.text:0040322A push offset TopLevelExceptionFilter ; lpTopLevelExceptionFilter
.text:0040322F call ds:SetUnhandledExceptionFilter
.text:00403235 retn
.text:00403235 sub_40322A endp
.text:00403235
.text:00403236
.text:00403236 ; =============== S U B R O U T I N E =======================================
.text:00403236
.text:00403236 ; Attributes: bp-based frame
.text:00403236
.text:00403236 ; LONG __stdcall TopLevelExceptionFilter(struct _EXCEPTION_POINTERS *)
.text:00403236 TopLevelExceptionFilter proc near ; DATA XREF: sub_40322Ao
.text:00403236
.text:00403236 arg_0 = dword ptr 8
安装异常处理
011B322A /$ 68 36321B01 push Crack_Me.011B3236 ; /pTopLevelFilter = Crack_Me.011B3236
011B322F |. FF15 94801C01 call dword ptr ds:[<&KERNEL32.SetUnhandl>; \SetUnhandledExceptionFilter
退出处理注释掉
sub_404972 一共有4处调用
1
.text:00404B45 push 1
.text:00404B47 push 2
.text:00404B49 push 0
.text:00404B4B call sub_404972
.text:00404B50 add esp, 0Ch
2
.text:00404B54 push 1
.text:00404B56 push 0
.text:00404B58 push 0
.text:00404B5A call sub_404972
.text:00404B5F add esp, 0Ch
3
.text:00404B68 push 0
.text:00404B6A push 2
.text:00404B6C push [ebp+arg_0]
.text:00404B6F call sub_404972
.text:00404B74 add esp, 0Ch
4
.text:00404BB6 push 0
.text:00404BB8 push 0
.text:00404BBA push [ebp+arg_0]
.text:00404BBD call sub_404972
.text:00404BC2 add esp, 0Ch
挂了
01322BC9 . E8 43950000 call Crack_Me.0132C111
在这挂了
.text:0040C14B call ds:off_41818C
.text:0040C151 call [ebp+var_8]
0132C14B |. FF15 8C813301 |call dword ptr ds:[0x133818C] ; Crack_Me.01323720
0132C151 |. FF55 F8 |call dword ptr ss:[ebp-0x8] ; Crack_Me.01322B41
0132C14B |. FF15 8C813301 |call dword ptr ds:[0x133818C] ; Crack_Me.01323720
0132C151 |. FF55 F8 |call dword ptr ss:[ebp-0x8] ; Crack_Me.01321000
01321530 NOP掉
想先打掉反调试,让调试器带程序跑起来.
0122C151 |. FF55 F8 |call dword ptr ss:[ebp-0x8] ; Crack_Me.01221010
0122C151 |. FF55 F8 |call dword ptr ss:[ebp-0x8] ; Crack_Me.01221010
0122C164 |. E8 5C6BFFFF call Crack_Me.01222CC5
报错了
.text:00402C44 call loc_401560
建立窗口前有插入的汇编代码进行检测流程
003A15AB |. 83F8 02 cmp eax,0x2 ; |
003A15AE |. 0F8F DA000000 jg Crack_Me.003A168E ; |
必须打掉,窗口流程才能出来
进行时间差检测的函数
调用的参考还挺多, 必须让这个函数返回的时间差 < 2
在里面改不太好改,直接抹掉调用sub_4048DE后,使用结果的判断吧.
int __cdecl sub_4048DE()
{
int v0; // ecx@3
signed __int64 v1; // qax@5
LARGE_INTEGER PerformanceCount; // [sp+4h] [bp-8h]@2
if ( (HIDWORD(qword_41FC18) & (unsigned int)qword_41FC18) == -1
|| !QueryPerformanceCounter(&PerformanceCount)
|| (v0 = (unsigned __int64)(*(_QWORD *)&PerformanceCount - qword_41FC18) >> 32, v0 <= 0) && v0 < 0
|| (LODWORD(v1) = sub_404817(
PerformanceCount.s.LowPart - qword_41FC18,
(unsigned __int64)(*(_QWORD *)&PerformanceCount - qword_41FC18) >> 32),
v1 > 2147483647) )
LODWORD(v1) = -1;
return v1;
}
打掉时间差检测
NOP掉检测点 jg xx
比对点1
.text:004015A2 call sub_4048DE
.text:004015A7 sub eax, edi
.text:004015A9 push 0
.text:004015AB cmp eax, 2
.text:004015AE jg loc_40168E
比对点2
.text:00401DC9 call sub_4048DE
.text:00401DCE sub eax, [ebp-0D0h]
.text:00401DD4 cmp eax, 2
.text:00401DD7 jg short loc_401E3E
比对点3
.text:00401EC6 call sub_4048DE
.text:00401ECB sub eax, esi
.text:00401ECD cmp eax, 2
.text:00401ED0 jg loc_4020D4
已经过了所有的反调试
Crack_Me5.exe 可以用调试器带起来正常运行.
GetWindowTextW下断
点击按钮的处理
00E722C4 . A1 1803E900 mov eax,dword ptr ds:[0xE90318] ; Case 3EB of switch 00E72296
起线程
.text:004020E0 fnThreadProc proc near ; DATA XREF: sub_402120+24Co
.text:004020E0
.text:004020E0 arg_0 = dword ptr 8
取输入的文本
// 这里要F9, 才能到1C3E
01231C2C |. 8D85 34FFFFFF lea eax,dword ptr ss:[ebp-0xCC] // 接收注册码的缓冲区
01231C32 |. 6A 64 push 0x64 ; /Count = 64 (100.)
01231C34 |. 50 push eax ; |Buffer = 00000006
01231C35 |. FF76 0C push dword ptr ds:[esi+0xC] ; |hWnd = 00100A94 (class='Edit',parent=00180AA6)
01231C38 |. FF15 4C812401 call dword ptr ds:[<&USER32.GetWindowTex>; \GetWindowTextW
一个字符必须是’b‘
01231C65 |. BA 62000000 mov edx,0x62
01231C6A |. 66:0f1f4400 0>nop word ptr ds:[eax+eax]
01231C70 |> 66:3BD1 /cmp dx,cx
01231C73 |. 74 1E |je short Crack_Me.01231C93
过了一个正确的判断了
01231CDD |. /74 54 je short Crack_Me.01231D33
01231CDF |. |68 C8000000 push 0xC8
call之后发送了消息
01231D0F |. E8 3C0D0000 call Crack_Me.01232A50
01232A50是关键CALL.
01232A50 call后,如果发送了消息, 注册码又不对,就出现失败对话框
窗口处理过程
01232120 . 55 push
ebp
WM_COMMAND处理
01232290 > \8B4D 10 mov ecx,dword ptr ss:[ebp+0x10] ; Case 111 (WM_COMMAND) of switch 0123214D
关键CALL分析
signed int __fastcall sub_402A50(int a1, int a2, __int16 a3)
{
// a1 is ecx = 76E41EE3
// a2 is edx = 输入的注册码 “b1111111”
// a3 is ebp + 8 = 0x70
signed int result; // eax@2
__int16 v4; // ax@3
int v5; // ecx@3
if ( a2 )
{// 首先:注册码必须不是空字符串
v4 = *(_WORD *)a2;// 注册码是宽字符输入的
v5 = 0;
if ( *(_WORD *)a2 )
{
while ( a3 != v4 ) // 第个字母必须是’p’
{
v4 = *(_WORD *)(a2 + 2 * v5++ + 2);
if ( !v4 )
goto LABEL_6;
}
result = 1; // 走到这才是正确的注册码
}
else
{
LABEL_6:
result = 0;
}
}
else
{
result = 0;
}
return result;
}
已经有2个字母正确了
01231D1F |. E8 8CFFFFFF call Crack_Me.01231CB0
猜密码
从程序资源看,这个作者是15pb的学员.
有2个字符是bp,正好是15pb反着写
尝试输入bp51xxx, 得到口令必须是7位. 猜测是bp51 + 3个其他字符
拿bp5115p作为输入的注册码,继续调试. 从Crack_Me.01231CB0开始.
算了一次输入的注册码长度
01231DAA |> \33C9 xor ecx,ecx ; user32.76E41EE3
01231DBF |. E8 120F0000 call Crack_Me.01232CD6
开始比对字符串长度是否正确
0123C980 |. 83FE E0 cmp esi,-0x20
Crack_Me.0123C393 里面在堆上分配了0x10字节的缓冲区.
看到下面有SendMessage
01231DC4 |. 83C4 04 add esp,0x4
怀疑是算字符串长度的代码是宏
01231B42 |> /8D40 02 /lea eax,dword ptr ds:[eax+0x2]
将异或后的用户密文压入CALL
01231BDB |. E8 90FCFFFF call Crack_Me.01231870(ecx,edx)
ecx = 1
edx = 异或加密后的注册码输入
看到了,这就是算注册码的CALL, 因为算完后,会发送2个消息,wParam不同,
wParam = 1034,
wParam = 1035,
看看这是2个啥消息
在一个缓冲区内填入了宽字符L’1’~L’9’
012318A2 |> /66:8901 /mov word ptr ds:[ecx],ax
012318A5 |. |8D49 02 |lea ecx,dword ptr ds:[ecx+0x2]
012318A8 |. |40 |inc eax
012318A9 |. |83F8 39 |cmp eax,0x39
012318AC |.^\7E F4 \jle short Crack_Me.012318A2
在一个缓冲区内填入了宽字符L’a’~L’z’
012318B6 |> /66:8901 /mov word ptr ds:[ecx],ax
012318B9 |. |8D49 02 |lea ecx,dword ptr ds:[ecx+0x2]
012318BC |. |40 |inc eax
012318BD |. |83F8 7A |cmp eax,0x7A
012318C0 |.^\7E F4 \jle short Crack_Me.012318B6
将刚填入的小写a~z变成大些A~Z
012318E8 |. /72 0D |jb short Crack_Me.012318F7
012318EA |. |83F8 7A |cmp eax,0x7A
012318ED |. |77 08 |ja short Crack_Me.012318F7
012318EF |. |83C0 E0 |add eax,-0x20
异或加密后的用户输入
edi = 00570468
又开始算加密后的串长度
01231910 |> /8D40 02 /lea eax,dword ptr ds:[eax+0x2]
01231913 |. |41 |inc ecx
01231914 |. |66:8338 00 |cmp word ptr ds:[eax],0x0
01231918 |.^\75 F6 \jnz short Crack_Me.01231910
开始还原加密后的用户输入
01231920 |> /83F8 02 /cmp eax,0x2
01231923 |. /73 07 |jnb short Crack_Me.0123192C
01231925 |. |66:833447 0F |xor word ptr ds:[edi+eax*2],0xF
0123192A |. |EB 11 |jmp short Crack_Me.0123193D
第1个字符异或0xf
第2个字符异或0xf
第3个字符异或0x50
第4个字符异或0x42
第5个字符异或0x42
第6个字符异或0x42
将解密后的用户输入变成大写
01231960 |> /0FB7044F /movzx eax,word ptr ds:[edi+ecx*2]
01231964 |. |83F8 61 |cmp eax,0x61
01231967 |. |72 0C |jb short Crack_Me.01231975
01231969 |. |83F8 7A |cmp eax,0x7A
0123196C |. |77 07 |ja short Crack_Me.01231975
0123196E |. |83C0 E0 |add eax,-0x20
01231971 |. |66:89044F |mov word ptr ds:[edi+ecx*2],ax
01231975 |> |41 |inc ecx
01231976 |. |3BCA |cmp ecx,edx
01231978 |.^\72 E6 \jb short Crack_Me.01231960
在操作注册码
为啥我看到的合规字符集,是A~Z ?
01231996 |> /66:85C9 /test cx,cx
01231999 |. |74 2C |je short Crack_Me.012319C7
0123199B |. |0FB710 |movzx edx,word ptr ds:[eax]
0123199E |. |8D4D B0 |lea ecx,dword ptr ss:[ebp-0x50]
头2个字母必须是bp
现在用的注册码是 “bp12345”
可以过下面的判断,否则去失败处了.
001C19EA |. 83F9 02 cmp ecx,0x2
001C19ED |. 75 4A jnz short Crack_Me.001C1A39
001C1A39 |> \6A 00 push 0x0 ; 发送失败消息
向内存写15PB了
001C19F1 |. C745 F0 31003>mov dword ptr ss:[ebp-0x10],0x350031
001C19F8 |. C745 F4 50004>mov dword ptr ss:[ebp-0xC],0x420050
001C19FF |. 8D77 04 lea esi,dword ptr ds:[edi+0x4]
开始比较15PB
001C1A10 |> /66:8B444D F0 /mov ax,word ptr ss:[ebp+ecx*2-0x10]
001C1A15 |. |66:3B06 |cmp ax,word ptr ds:[esi]
001C1A18 |. |75 1F |jnz short Crack_Me.001C1A39
启用新的注册码
条件:
* bp在注册码中只能有2个字符
* 第3个字符开始的字符序列必须是15pb
* 总字符数为7
* 第一个字符和第2个字符加起来为0x63, 即第一个字符必须为’1’, 第2个字符必须为’2’
离胜利又进了一步
00F41832 |. 8B45 B4 mov eax,dword ptr ss:[ebp-0x4C]
00F41841 |. 3BC1 cmp eax,ecx ; 最后一个字符必须是'8' 即 0x38
00F41843 |. 75 16 jnz short Crack_Me.00F4185B ; 错误分支
胜利
00F41847 |. B8 01000000 mov eax,0x1 ; 这里是正确的分支,必须走到这里
得到正确的注册码
拿下面的字符串可以进入最后的比对
1215pb8
窗体还有个定时器
011D2285 . FF15 3C811E01 call dword ptr ds:[<&USER32.SetTimer>] ; \SetTimer
进入最后的注册码判断
01151740 <Crack_Me.fnLastRegJudge> /$ 55 push ebp
--------------------------------------------------------------------------------
【经验总结】
* 先打掉反调试,让调试器能带着cm跑起来。
* cm用vs2008编写,开了活动基地址,但是低16位地址和IDA能对应起来。e.g. 0100ABC0地址在IDA中为0040ABC0
* 不断的试错,只有一条通向正确注册码的大路,开心
* 我的正式破解处女篇成功了,感谢凉飕飕的cm.
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2016年11月02日 4:28:43
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!