前言:本来最初是想弄#2的,弄了两三天还是理不清头绪,
又下了#1回来,风格跟#2差不多,但运算简单些。 使用工具:
反编译及调试工具:OllyDbg,IDA,Resource Hacker
程序开发包:MASM32
KeyGenMe下载地址:http://www.crackmes.de/users/crackerlnn/keygenme_1/ 第一步:还原界面代码
首先用OllyDbg加载以查看是否有花指令。代码区域
还算清晰,并没有夹杂数据。然后用IDA加载分析。
入口处代码如下:
============================
public start
start proc near
push NULL ; lpModuleName
call GetModuleHandleA
mov hInstance, eax
push 0 ; dwInitParam
push offset DialogFunc ; lpDialogFunc
push NULL ; hWndParent
push DLGEX_MAIN ; lpTemplateName
push hInstance ; hInstance
call DialogBoxParamA
push eax ; uExitCode
call ExitProcess
start endp
============================
这是一个使用对话框界面的程序。对话框需要使用资源,
用Resource Hacker把它的资源提取出来辅助分析,根据
资源脚本中的ID号给代码中的常数增加一些符号定义,如
上面的DLGEX_MAIN等等。
对话框的行为是在回调过程中描述的,下面来看这个
回调过程的代码:
============================
; Attributes: bp-based frame
; BOOL __stdcall DialogFunc(HWND hDlg,UINT uMsg,WPARAM wParam,LPARAM lParam)
DialogFunc proc near ; DATA XREF: start+Eo
hDlg = dword ptr 8
uMsg = dword ptr 0Ch
wParam = dword ptr 10h
lParam = dword ptr 14h
push ebp
mov ebp, esp
cmp [ebp+uMsg], WM_INITDIALOG
jnz loc_401CF2
push [ebp+hDlg]
pop hDlgMain
push ICO_MAIN ; lpIconName
push hInstance ; hInstance
call LoadIconA
mov hIcoMain, eax
push hIcoMain ; lParam
push ICON_BIG ; wParam
push WM_SETICON ; Msg
push [ebp+hDlg] ; hWnd
call SendMessageA
push EDT_SERIAL ; nIDDlgItem
push [ebp+hDlg] ; hDlg
call GetDlgItem
mov hTextSerial, eax
push EDT_NAME ; nIDDlgItem
push [ebp+hDlg] ; hDlg
call GetDlgItem
mov hTextName, eax
xor eax, eax
push 0 ; nFileSystemNameSize
push NULL ; lpFileSystemNameBuffer
push NULL ; lpFileSystemFlags
push NULL ; lpMaximumComponentLength
push offset VolumeSerialNumber ; lpVolumeSerialNumber
push 0 ; nVolumeNameSize
push NULL ; lpVolumeNameBuffer
push offset RootPathName ; "c:\\\\"
call GetVolumeInformationA
mov eax, VolumeSerialNumber
xor eax, 12345678h
mov dword_403091, eax
jmp loc_401EDC
loc_401CF2: ; CODE XREF: DialogFunc+Aj
cmp [ebp+uMsg], WM_COMMAND
jnz loc_401ECC
mov eax, [ebp+wParam]
cmp eax, ID_CHECK ; 按下Check按钮
jnz loc_401EAC
push 10h
push offset MD5_of_Name
call RtlZeroMemory
push 28h ; nMaxCount
push offset szNameString ; lpString
push hTextName ; hWnd
call GetWindowTextA
push offset MD5_of_Name ; lpszOutput
push eax ; uLenInput
push offset szNameString ; lpszInput
call MD5_Transform
push ebx
push esi
push edi
push ebp
xor edx, edx
xor eax, eax
lea esi, MD5_of_Name
mov ecx, 4
loc_401D4E: ; CODE XREF: DialogFunc+FCj
or edx, eax
xor eax, eax
lodsb
shl edx, 8
loop loc_401D4E
or edx, eax
mov dword_40309D, edx ; 这里存放MD5_of_Name第一个双字的字节反序
mov ecx, 4
xor eax, eax
xor edx, edx
loc_401D69: ; CODE XREF: DialogFunc+115j
or edx, eax
lodsb
shl edx, 8
loop loc_401D69
or edx, eax
mov dword_403095, edx ; 这里存放MD5_of_Name第二个双字的字节反序
push 0Ch ; nCount
push offset aChinacracker ; "ChinaCracker"
call sub_4019D8
push offset dword_403095
push offset dword_40309D
call sub_40197C
nop
nop
nop
mov eax, 4
push 28h ; nMaxCount
push offset szSerialString ; lpString
push hTextSerial ; hWnd
call GetWindowTextA
cmp eax, 10h ; 序列号长度必须为16
jnz loc_401E93
mov ecx, 8
xor eax, eax
xor edx, edx
lea esi, szSerialString
lea edi, unk_4031AC
loc_401DCC: ; CODE XREF: DialogFunc+1C0j
movzx eax, byte ptr [esi] ; 取得序列号所代表的16进制数值
cmp eax, 'a'
jb short loc_401DD9
sub eax, 57h
jmp short loc_401DEB
loc_401DD9: ; CODE XREF: DialogFunc+178j
cmp eax, 'A'
jb short loc_401DE3
sub eax, 37h
jmp short loc_401DEB
loc_401DE3: ; CODE XREF: DialogFunc+182j
cmp eax, '0'
jb short loc_401DEB
sub eax, 30h
loc_401DEB: ; CODE XREF: DialogFunc+17Dj
; DialogFunc+187j DialogFunc+18Cj
shl eax, 4
inc esi
movzx edx, byte ptr [esi]
cmp edx, 'a'
jb short loc_401DFC
sub edx, 57h
jmp short loc_401E0E
loc_401DFC: ; CODE XREF: DialogFunc+19Bj
cmp edx, 'A'
jb short loc_401E06
sub edx, 37h
jmp short loc_401E0E
loc_401E06: ; CODE XREF: DialogFunc+1A5j
cmp edx, '0'
jb short loc_401E0E
sub edx, 30h
loc_401E0E: ; CODE XREF: DialogFunc+1A0j
; DialogFunc+1AAj DialogFunc+1AFj
add eax, edx
and eax, 0FFh
mov [edi], al
inc edi
inc esi
dec ecx
jnz short loc_401DCC
mov ecx, 8
lea esi, dword_40435E
lea edi, unk_4031C0
rep movsb
push offset aBcgFcgDfcg ; "[BCG][FCG][DFCG]"
push offset unk_4031AC ; lpszDest
call sub_401B84
pop ebp
pop edi
pop esi
pop ebx
xor eax, eax
mov ecx, 8
lea esi, unk_4031AC
lea edi, unk_4031C0
repe cmpsb ; 关键比较
or eax, ecx
jz short loc_401E74
push MB_ICONERROR ; uType
push offset Caption ; "KeyGen #1"
push offset aSerialError ; "Serial error"
push hDlgMain ; hWnd
call MessageBoxA
jmp short loc_401EDC
loc_401E74: ; CODE XREF: DialogFunc+1FFj
push MB_ICONINFORMATION ; uType
push offset Caption ; "KeyGen #1"
push offset aWellDoneNowCod ; "Well Done! Now Code a KeyGen"
push hDlgMain ; hWnd
call MessageBoxA
xor eax, eax
leave
retn 10h
loc_401E93: ; CODE XREF: DialogFunc+157j
push MB_ICONERROR ; uType
push offset Caption ; "KeyGen #1"
push offset aLengthOfSerial ; "length of Serial error"
push hDlgMain ; hWnd
call MessageBoxA
jmp short loc_401EDC
loc_401EAC: ; CODE XREF: DialogFunc+ADj
cmp eax, ID_ABOUT ; 按下About按钮
jnz short loc_401EDC
push MB_ICONINFORMATION ; uType
push offset aAbout ; "aBout"
push offset aKeygenme1ByLnn ; "KeyGenMe #1 By lnn1123\n\rCode With Win3"...
push hDlgMain ; hWnd
call MessageBoxA
jmp short loc_401EDC
loc_401ECC: ; CODE XREF: DialogFunc+9Fj
cmp [ebp+uMsg], WM_CLOSE
jnz short loc_401EDC
push 0 ; nResult
push [ebp+hDlg] ; hDlg
call EndDialog
loc_401EDC: ; CODE XREF: DialogFunc+93j
; DialogFunc+218j DialogFunc+237j
; DialogFunc+250j DialogFunc+257j
; DialogFunc+270j DialogFunc+276j
xor eax, eax
leave
retn 10h
DialogFunc endp
============================
对话框的初始化代码除了建立图标外,还取得两个输入框
的句柄。按下About按钮,显示一些关于程序的信息。按
下Check按钮,则调用GetWindowText到两个输入框中取文
本,用户名最多取40个字符,而序列号的长度必须为16,
否则显示“length of serial error”。取完文本后开始
检验注册码,检验失败显示“Serial error”,检验成功
则显示“Well Done! Now Code a KeyGen”。这些就是对
话框的基本行为。GetVolumeInformation函数调用返回的
结果在验证注册码过程中没有用到,可以当作冗余代码删
掉。剩下的部分就基本上可以照搬了。 第二步:注册码验证算法分析
由关键比较部分的代码,注册码检验成功的标志是:
qword_4031AC == qword_4031C0
(用qword_xxxxxx作为qword ptr [xxxxxx]的简写,下
同。)往上看不远处还可以总结出一个依赖关系:
qword_4031C0 = qword_40435E
其他的依赖关系,静态分析就不太好找了。用OllyDbg跟
踪程序,并监视所关心的内存单元。可以得出如下结论:
qword_4031AC在过程sub_401B84中被改写
调用前:qword_4031AC = 序列号字节反序的16进制数值
(如输入字符串"FEDCBA9876543210",则qword_4031AC = 1032547698BADCFE,
在内存中存放为FE DC BA 98 76 54 32 10)
qword_4031C0 = qword_40435E
qword_40435E在过程sub_4019D8(调用于401D80)中被改写
sub_4019D8运算过程完全使用程序自带数据,写入qword_40435E的值:0DDBAFB5BA2F07B6Ah
qword_40435E在过程sub_40197C(调用于401D8F)被改写
过程sub_40197C中:
qword_40435E = qword_403316 xor qword_temp
qword_temp以MD5_of_Name前两个双字的字节反序作为输入,经一些变换而得
变换中读取内存区域:4032D6――403316,在sub_4019D8被写入数值(不依赖于输入)
qword_403316在过程调用sub_4019D8中被写入数值(不依赖于输入):0E148481EF3EB9F33h
这里为了条理清晰起见,直接把后来总结的变量依赖关系
一次性列出。而实际过程则不是这么简单。事实上,当发
现qword_40435E在sub_4019D8中被改写时,我就迫不及待
地跟进去。这个过程中首先是复制一个长达4KB的数据块,
然后一大堆移位,异或,调用这个过程,调用那个过程,
大量的全局变量操作……弄了整整一天,也没弄清它在做
什么。偶然之间,发现怎么不见输入数据,诸如用户名之
类的东西参与运算?整个过程中就只有程序自带的数据在
做运算。猛然之间恍然大悟,既然如此,运算出来的结果
就不会依赖于输入,是一个常数,也就是说,我们根本不
用关心里面究竟做了什么运算,只要看它最后返回的数值
是什么就行了!为了确认起见,尝试输入不同的用户名及
序列号,步过过程sub_4019D8,再查看qword_40435E的内
容,果然每次都是一样!
过程sub_40197C中最后一次改写qword_40435E,这次
过程调用带了两个参数,分别是MD5_of_Name的前两个双
字的字节反序。既然涉及到用户名,这次就不能不管具体
运算了。跟进去一看:
============================
sub_40197C proc near ; CODE XREF: sub_4019D8+A2p
; sub_4019D8+DAp DialogFunc+135p
counter = dword ptr -4
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
push ebp
mov ebp, esp
add esp, 0FFFFFFFCh
push ebx
push edi
push esi
push edx
push ecx
mov eax, [ebp+arg_0]
mov ecx, [ebp+arg_4]
mov eax, [eax]
mov esi, [ecx]
mov edi, offset dword_4032D6
mov [ebp+counter], 10h
mov ebx, edi
loc_40199F: ; CODE XREF: sub_40197C+3Bj
xor eax, [ebx]
mov edx, eax
push eax
call sub_401910
mov ecx, [ebp+counter]
xor eax, esi
add ebx, 4
dec ecx
mov esi, edx
mov [ebp+counter], ecx
jnz short loc_40199F
mov ecx, [edi+40h] ; 403316
mov edx, [edi+44h] ; 40331A
xor ecx, eax
xor edx, esi
mov dword_404362, edx
mov dword_40435E, ecx
pop ecx
pop edx
pop esi
pop edi
pop ebx
leave
retn 8
sub_40197C endp
============================
这里在对作为参数输入的两个双字进行了一大堆运算以
后,才从qword_403316中取出数值与其异或后写入到
qword_40435E中,而qword_40435E原来的值竟然没有用到
就被覆盖了。qword_403316的数值也是在sub_4019D8中写
入的,同样道理,这个值也是个常数。其中涉及到的
sub_401910过程的代码如下:
============================
sub_401910 proc near ; CODE XREF: sub_40197C+28p
arg_0 = dword ptr 8
push ebp
mov ebp, esp
push ebx
push edi
push esi
push edx
push ecx
mov ecx, [ebp+arg_0]
mov al, cl
and eax, 0FFh
shr ecx, 8
mov edx, eax
mov al, cl
mov edi, offset dword_4032D6
and eax, 0FFh
shr ecx, 8
mov esi, eax
mov eax, ecx
shr eax, 8
and eax, 0FFh
and ecx, 0FFh
and esi, 0FFFFh
and edx, 0FFFFh ; 至此:edx,esi,ecx,eax
; 分别含有arg_0的第1,2,3,4
; 字节的零扩展
mov eax, [edi+eax*4+48h]
mov ebx, [edi+ecx*4+448h]
mov ecx, [edi+esi*4+848h]
add eax, ebx
xor eax, ecx
mov ecx, [edi+edx*4+0C48h]
add eax, ecx
pop ecx
pop edx
pop esi
pop edi
pop ebx
leave
retn 4
sub_401910 endp
============================
这里涉及一个1024个双字的表,具体就不列出来了。最后
书写代码的时候把这段代码提取出来嵌入源码中就行了。
至于qword_4031AC,它是在过程sub_401B84中被改写
的,查看一下这个过程的代码:
============================
; int __stdcall sub_401B84(int lpszDest,int lpParam1)
sub_401B84 proc near ; CODE XREF: DialogFunc+1DFp
lpszDest = dword ptr 8
lpParam1 = dword ptr 0Ch
push ebp
mov ebp, esp
pusha
mov esi, [ebp+lpParam1]
mov eax, [esi]
mov ebx, [esi+4]
mov ecx, [esi+8]
mov edx, [esi+0Ch]
mov dword_4053C0, eax
mov dword_4053C4, ebx
mov dword_4053C8, ecx
mov dword_4053CC, edx
push ebp
mov ebx, [ebp+lpszDest]
mov edx, 0C6EF3720h
mov esi, [ebx]
mov edi, [ebx+4]
mov ebp, 20h
loc_401BC0: ; CODE XREF: sub_401B84+83j
mov eax, esi
mov ebx, esi
mov ecx, esi
shl eax, 4
add eax, dword_4053C8
shr ebx, 5
add ebx, dword_4053CC
add ecx, edx
xor ecx, eax
xor ecx, ebx
sub edi, ecx
mov eax, edi
mov ebx, eax
mov ecx, eax
shl eax, 4
add eax, dword_4053C0
shr ebx, 5
add ebx, dword_4053C4
add ecx, edx
xor ecx, eax
xor ecx, ebx
sub esi, ecx
sub edx, 9E3779B9h
dec ebp
jnz short loc_401BC0
mov dword_4053C0, ebp
mov dword_4053C4, ebp
mov dword_4053C8, ebp
mov dword_4053CC, ebp
pop ebp
mov ebx, [ebp+lpszDest]
mov [ebx], esi
mov [ebx+4], edi
popa
leave
retn 8
sub_401B84 endp
============================
这个过程中又有一个冗长的计算过程。如果这里不去管
这个算法的细节,我们得到的注册成功方程式将会是:
sub_40197C(前2双字(MD5(用户名))) == sub_401B84(序列号)
(注:从过程sub_401000中的67452310,0EFCDAB89这些
常数容易识别出它是MD5算法,故重命名为
MD5_Transform。可用MD5计算器验证之)MD5算法是不可
逆的,无法从序列号出发获得对应的用户名,那就只能
从用户名来计算序列号。所以对于sub_401B84这个过程
必须求出它的逆变换来。注意到过程中有一条指令:
sub edx, 9E3779B9h,再将代码页面往上翻页,看到:
============================
JunkProc proc near
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
push ebp
mov ebp, esp
pusha
mov esi, [ebp+arg_4]
mov eax, [esi]
mov ebx, [esi+4]
mov ecx, [esi+8]
mov edx, [esi+0Ch]
mov dword_4053C0, eax
mov dword_4053C4, ebx
mov dword_4053C8, ecx
mov dword_4053CC, edx
push ebp
mov ebx, [ebp+arg_0]
xor edx, edx
mov esi, [ebx]
mov edi, [ebx+4]
mov ebp, 20h
loc_401B15: ; CODE XREF: JunkProc+80j
add edx, 9E3779B9h
mov eax, edi
mov ecx, eax
mov ebx, edi
shl eax, 4
shr ebx, 5
add eax, dword_4053C0
add ebx, dword_4053C4
add ecx, edx
xor ecx, eax
xor ecx, ebx
add esi, ecx
mov eax, esi
mov ebx, esi
mov ecx, esi
shl eax, 4
shr ebx, 5
add eax, dword_4053C8
add ebx, dword_4053CC
add ecx, edx
xor ecx, eax
xor ecx, ebx
add edi, ecx
dec ebp
jnz short loc_401B15
mov dword_4053C0, ebp
mov dword_4053C4, ebp
mov dword_4053C8, ebp
mov dword_4053CC, ebp
pop ebp
mov ebx, [ebp+arg_0]
mov [ebx], esi
mov [ebx+4], edi
popa
leave
retn 8
JunkProc endp
============================
这个过程本来没有被程序调用到,IDA也就没有分析,所
以起名叫JunkProc。这里却有一条add edx, 9E3779B9h
指令,会不会这个过程就是sub_401B84的逆变换?为了
验证这一猜测,转到OllyDbg中,在主程序中调用
sub_401B84之前,先记下qword_4031AC的值。单步调用
完以后,立即把call语句修改为call 00401ADC,也就是
上面这个JunkProc的地址,然后重置EIP到push第一个参
数的语句上,以同样的参数调用JunkProc。果然,
qword_4031AC的值被还原了!
这一来,计算注册码的方程式应该是:
JunkProc(sub_40197C(前2双字(MD5(用户名)))) == 序列号
根据模块化程序设计的思想把算法重写一遍,尽量去掉
不必要的全局变量(从程序反汇编的代码来看,其中使
用了不少全局变量,这也是造成程序难以跟踪的原因之
一):
验证注册码:(参数:用户名指针,序列号指针)
局部变量:MD5值缓冲区
注册码的16进制数值
将用户名计算出MD5值存入缓冲区……
将MD5值前2个双字作字节反序……
代入sub_40197C中变换……
获得注册码所示的16进制数值(逆序)
代入sub_401B84中变换……
比较二者的变换结果是否相等:
返回:非0(不相等)0(相等) 第三步:KeyGen
由上面给出的方程式,只须把验证注册码的过程稍
作修改,即可用来生成注册码。也就是说在sub_40197C
变换完后,不是转而处理输入码,而是直接把变换结果
再代入到那个JunkProc中处理,最后设法把内存数值转
换为字符串,就能得到正确的注册码了。
依照这种想法,可以用原来的界面稍作修改(如把
Check按钮的标题改为Generate等)作为KeyGen界面,
然后在源代码中加入条件汇编,依照一定条件确定编译
原程序代码还是注册机代码,具体实现过程从略。
上传文件内容:
KeyGenMe.asm 还原的源程序(包含注册机条件汇编)
reversed.rc 原程序资源脚本
KeyGen.rc 注册机资源脚本
Icon_1.ico 图标
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
上传的附件: