下载地址:http://www.crackmes.de/users/haggar/keyme1/
使用工具:
反编译及调试工具:OllyDbg,IDA,ResHacker
程序开发包:MASM32
1. 程序结构分析
首先用OllyDbg载入程序。原程序在入口点之前有相当长一段花指令,如下所示:
=================================================================
00401000 > $ /EB 01 jmp short 00401003
00401002 |E9 db E9
00401003 > \EB 01 jmp short 00401006
00401005 E9 db E9
00401006 > EB 01 jmp short 00401009
00401008 E9 db E9
00401009 > EB 01 jmp short 0040100C
0040100B E9 db E9
0040100C > EB 01 jmp short 0040100F
0040100E EB db EB
0040100F > EB 01 jmp short 00401012
00401011 EB db EB
00401012 > 75 03 jnz short 00401017
00401014 . 74 01 je short 00401017
00401016 74 db 74 ; CHAR 't'
00401017 . 77 03 ja short 0040101C
00401019 . 72 01 jb short 0040101C
0040101B . 75 db 75 ; CHAR 'u'
0040101C > 75 03 jnz short 00401021
0040101E . 74 01 je short 00401021
00401020 74 db 74 ; CHAR 't'
00401021 . 77 03 ja short 00401026
00401023 . 72 01 jb short 00401026
00401025 . 75 db 75 ; CHAR 'u'
00401026 > 75 03 jnz short 0040102B
00401028 . 74 01 je short 0040102B
0040102A 74 db 74 ; CHAR 't'
//以下代码略
=================================================================
这里跳转指令后跟字节数据显然是打算欺骗反汇编程序,但开头几个跳转都是绝对跳
转,用绝对跳转似乎做不了什么手脚。相比jnz/je组合可能在某些场合有效。另外有
疑问的地方是ja/jb组合,这个组合从语义上不等同于绝对跳转,当CF=0且ZF=1时它
是不跳的。不过这看来不是什么大问题,一来调试器中加载完毕后CF总是等于1,二
来不管执行的流程怎么跳,正常情况下总要跳到入口点执行,如果最后导致异常而退
出,那就是程序自己的错了。
本着这个思想找到程序的第一句有用的代码是在地址401462处,而之前从401000
到这一句之前都是垃圾代码,可以用nop之类的“白色指令”刷掉。同样,从地址
401543到401B01(好长@_@)的部分也将其刷掉。把修改过的文件保存,再用IDA载入
进行分析。
程序的入口点处代码如下:
=================================================================
public start
start proc near
push NULL ; lpModuleName
call GetModuleHandleA
mov hInstance, eax
mov dword ptr szDllName, 72657375h
mov dword ptr szDllName+4, 642E3233h
mov dword ptr szDllName+8, 6C6Ch
push offset szDllName ; lpLibFileName
call LoadLibraryA ; Load user32.dll
mov dword ptr szProcName, 636F6C42h
mov dword ptr szProcName+4, 706E496Bh
mov dword ptr szProcName+8, 7475h
push offset szProcName ; lpProcName
push eax ; hModule
call GetProcAddress
push TRUE
call eax ; BlockInput
push NULL ; lpWindowName
push offset szODChrStr ; "OLLYDBG"
call FindWindowA
cmp eax, 0
jz short loc_4014EE
push MB_ICONWARNING ; uType
push offset szCapODFound ; "Got you!"
push offset szTextODFound ; "Buahaha ha ha ... Dude , what are you g"...
push NULL ; hWnd
call MessageBoxA
push 0 ; uExitCode
call ExitProcess
loc_4014EE: ; CODE XREF: start+4D2j
mov dword ptr szDllName, 72657375h
mov dword ptr unk_403200, 642E3233h
mov dword_403204, 6C6Ch
push offset szDllName ; lpLibFileName
call LoadLibraryA ; Load user32.dll
mov szProcName, 636F6C42h
mov dword_403264, 706E496Bh
mov dword_403268, 7475h
push offset szProcName ; lpProcName
push eax ; hModule
call GetProcAddress
push FALSE
call eax ; BlockInput
call GetVersion
cmp al, 5
jnz short loc_401B8B
mov dword ptr szDllName, 6E72656Bh
mov dword ptr szDllName+4, 32336C65h
mov dword ptr szDllName+8, 6C6C642Eh
mov dword ptr szDllName+0Ch, 0
push offset szDllName ; lpLibFileName
call LoadLibraryA
mov dword ptr szProcName, 65447349h
mov dword ptr szProcName+4, 67677562h
mov dword ptr szProcName+8, 72507265h
mov dword ptr szProcName+0Ch, 6E657365h
mov dword ptr szProcName+10h, 74h
push offset szProcName ; lpProcName
push eax ; hModule
call GetProcAddress
call eax ; IsDebuggerPresent
cmp eax, 0
jz short loc_401B8B
push 0 ; dwReserved
push EWX_LOGOFF ; uFlags
call ExitWindowsEx
loc_401B8B: ; CODE XREF: start+B0Aj start+B80j
push 0 ; dwInitParam
push offset DialogFunc ; lpDialogFunc
push NULL ; hWndParent
push DLG_MAIN ; lpTemplateName
push hInstance ; hInstance
call DialogBoxParamA
push eax ; uExitCode
call ExitProcess
start endp
=================================================================
程序在建立主窗口之前有一些抗调试的动作,其大意为:
(1) 先用BlockInput(TRUE)锁住键盘和鼠标。如果调试的过程中不小心跟踪执行
到这个语句,就会死机。(注:Ctrl+Alt+Del调出任务管理器还是可以响应的)
(2) 用FindWindow("OLLYDBG",NULL)检测OllyDbg,若检测到就会退出程序。
(看雪版的OD把标题改成了OllyICE,这一检测随之无效了)检测完后用
BlockInput(FALSE)解锁。
(3) 用IsDebuggerPresent()检测Ring3调试器,若检测到则用
ExitWindowsEx(EWX_LOGOFF, 0)注销当前用户。由于这是WinNT以上才有的功能,在
执行这段代码之前调用GetVersion(),如果返回值的al<5,就跳过这段执行。
有意思的是程序中虽然用到user32.dll中的API,但user32.dll却不是初始化
时自动装入,而是程序中调用LoadLibrary装入的;装入时使用的参数"user32.dll"
也不是预定义的字符串,而是在代码中赋值给字符串变量的,看上去跟赋值给普通的
双字变量一样。由于在这里不需要动态装入DLL带来的好处,同时为了明白起见,在
还原的代码中改成常规装入方法,以便将BlockInput和IsDebuggerPresent的调用明
确地写出来。
对话框的回调过程位于原程序地址401BAA处(去掉一大堆垃圾指令后,就会发现
其实它离入口点也不远),这个过程中只有对WM_COMMAND及WM_CLOSE的处理,其中
WM_COMMAND包含3个按钮的分支,按照按钮的标签分别取名为ID_CHECK、ID_ABOUT和
ID_EXIT。WM_CLOSE和WM_COMMAND的后两个分支都很简单,这里略去具体分析。而
ID_CHECK的分支则是我们主要关注的内容。
2.验证注册码算法分析
“Check”按钮被按下时将执行如下代码:
=================================================================
loc_401BC7: ; CODE XREF: DialogFunc+14j
cmp eax, ID_CHECK
jnz short loc_401BE9
push 30 ; nMaxCount
push offset szSerialString ; lpString
push EDT_SERIAL ; nIDDlgItem
push [ebp+hDlg] ; hDlg
call GetDlgItemTextA
call sub_401C16
jmp short loc_401C10
=================================================================
其中跟进过程sub_401C16看一下:
=================================================================
sub_401C16 proc near ; CODE XREF: DialogFunc+38p
var_8 = dword ptr -8
cmp eax, 0
jnz short loc_401C26
retn
loc_401C26: ; CODE XREF: sub_401C16+3j
cmp eax, 29 ; 序列号必须是29字符
jz short loc_401C3E
retn
loc_401C3E: ; CODE XREF: sub_401C16+13j
cmp szSerialString+5, '-'
jz short loc_401C53
retn
loc_401C53: ; CODE XREF: sub_401C16+2Fj
cmp szSerialString+11, '-'
jz short loc_401C6E
retn
loc_401C6E: ; CODE XREF: sub_401C16+44j
cmp szSerialString+17, '-'
jz short loc_401C86
retn
loc_401C86: ; CODE XREF: sub_401C16+5Fj
cmp szSerialString+23, '-'
jz short loc_401C9F
retn
loc_401C9F: ; CODE XREF: sub_401C16+77j
cmp szSerialString+29, 0
jz short loc_401CB2
retn
loc_401CB2: ; CODE XREF: sub_401C16+90j
push eax
rdtsc
push eax
rdtsc
sub eax, [esp]
cmp eax, 100h
jb short loc_401CDF
loc_401CDD: ; CODE XREF: sub_401C16:loc_401CDDj
jmp short loc_401CDD
loc_401CDF: ; CODE XREF: sub_401C16+C5j
pop eax
pop eax
call sub_401D35
cmp eax, 0
jz short loc_401CEF
retn
loc_401CEF: ; CODE XREF: sub_401C16+D6j
call sub_401D72
cmp eax, 0
jz short loc_401CFA
retn
loc_401CFA: ; CODE XREF: sub_401C16+E1j
call sub_401DAF
cmp eax, 0
jz short loc_401D05
retn
loc_401D05: ; CODE XREF: sub_401C16+ECj
call sub_401DEC
cmp eax, 0
jz short loc_401D10
retn
loc_401D10: ; CODE XREF: sub_401C16+F7j
call sub_401E29
cmp eax, 0
jz short loc_401D1B
retn
loc_401D1B: ; CODE XREF: sub_401C16+102j
call sub_401E6B
retn
sub_401C16 endp
=================================================================
这个过程中原本还有垃圾字节,现在所看到的是去掉垃圾字节后的版本。从开头几句
可以看出,注册码的格式应该是:
XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
下面还有一点零星的调试检测:求两次rdtsc指令的差值,这段代码正常执行的情形
一般不会超过100h个时钟周期,而在调试器中单步跟踪则十分缓慢,会远远超过这个
数值,因此如果检测到调试器或Loader,就进入一个jmp $的死循环。接下来就是验
证注册码了,注册码分5个小节,验证的时候是逐小节进行验证的,其算法都差不
多,以验证第一小节的过程为例:
=================================================================
sub_401D35 proc near ; CODE XREF: sub_401C16+CEp
xor eax, eax
xor ebx, ebx
movzx eax, szSerialString
movzx ebx, szSerialString+1
add eax, ebx
mul ebx
movzx ebx, szSerialString+2
add eax, ebx
mul ebx
movzx ebx, szSerialString+3
add eax, ebx
mul ebx
movzx ebx, szSerialString+4
add eax, ebx
mul ebx
xor eax, 31CF15A9h
retn
sub_401D35 endp
=================================================================
这个过程返回的时候检查eax的值,等于0时方为合法。也就是说:若把这个小节
记作abcde其中abcde为字符变量。则
((((a+b)*b+c)*c+d)*d+e)*e == 31CF15A9h
才是合法的。这里估计一下,ASCII码都在0到127之间,5个字符乘起来的结果最
大可能是2的35次方,eax已容纳不下。因此各字符的范围必须有所限制。这样,
字符e应该是31CF15A9h的一个因子,而31CF15A9h除以e后减去e得到的值必须有
另外一个能作为字符ASCII码的因子,依次类推。
检验5个小节可以用一个统一的过程实现。这样就可以构建出整个程序的代
码了。
上传文件内容:1.asm 还原的代码
1.rc 资源脚本
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)