第五六课——解除程序重启验证,程序打补丁

1. 课前预习

  • 熟悉OD字符串插件的使用
    右键或插件->中文搜索引擎->智能搜索,Ctrl+F搜索字符串。

  • 熟悉OD如何下断点
    Ctrl+G,直接搜索API下断
    Ctrl+N,输入表搜索API->右键->在每个参考上设置断点
    利用插件,ApiBreak和API断点设置工具都可以

  • 熟悉procmon的使用
    可以监控文件,注册表,网络,进线程信息
    排除进程:Exclude
    查看指定进程:Include

  • 熟悉文件操作API的使用
    CreateFileA(W):创建文件
    ReadFile:读取文件
    WriteFile:写入文件
    CloseHandle:关闭句柄
    读取文件:CreateFile->ReadFile->CloseHandle
    写入文件:CreateFile->WriteFile->CloseHandle

  • 熟悉注册表操作API的使用
    创建注册表Key:RegCreateKey
    打开注册表Key:RegOpenKey
    查询注册表键值:RegQueryValueExA
    写入注册表键值:RegSetValueEx

2. 重启验证

什么是重启验证

  • 重启验证顾名思义就是在程序启动时验证注册信息。

执行流程

  • 基本的执行流程:注册信息输入—>程序重启—>执行验证机制—>正常执行

  • 扩展的执行流程:注册信息输入—>执行部分验证机制/执行假验证机制—>程序重启—>执行真验证机制—>正常执行

  • 对于有经验的作者来说,可以在注册信息输入和程序重启之间加入假的验证机制,假的验证机制一般比较简单,比如说只是当单纯的明码比较,当我们输入假的注册码,程序一般会提示注册成功,此时程序就会知道我们是逆向者,在程序重启时就会假装注册成功,在执行程序功能时就会报错或是无反应,这就是所谓的暗桩。

重启验证的类型

  • 重启验证根据写入信息位置的不同一般分两类,一类是将注册信息写入文件中,一类是将注册信息写入注册表中。

定位关键代码

  1. 字符串定位
    通过OD字符串插件扫描敏感字符串,一般出现的文件路径或是注册表路径都可能是验证信息的保存位置
  2. 监控工具定位
    通过procmon等监控工具监控注册信息的写入位置
  3. API定位
    通过定位CreateFile,RegCreateKey,GetPrivateProfileStringA等API来获取注册信息的写入位置

3. 重启验证示例

运行一下程序查看它的操作机制。

输入任意字符串,点击重启验证1,出现弹窗,在程序的本目录下生成一个.txt文件,程序退出。里面是我们输入的字符串。

1
888888888

再次,点击重启验证2,出现弹窗,在程序的本目录下生成一个.ini文件,程序退出。里面是我们输入的字符串。

1
2
[验证]
Key=888888888

再次,点击重启验证3,出现弹窗,在本机注册表里写入信息,程序退出。徽标键+R打开运行窗口,输入regedit打开注册表,在下图看到输入的字符串。

将所有生成的文件和注册表信息删除,载入OD,搜索敏感字符串。

首先看重启验证1,双击进入反汇编代码,找到函数开头下断。运行,输入假码,选择重启验证1,OD停在断点处。F8往下走走,遇到call先用enter探探路再考虑是否跟进去。这个call里面有很多API函数,程序经过这条指令后,eax变成我们输入的字符串,所以这个函数是取出输入框里的字符串。

1
2
3
4
0040275B  |.  E8 C8590100   call 52PoJie?00418128;eax=888888888
00402760 |. 8B45 EC mov eax,[local.5]
00402763 |. 8378 F4 00 cmp dword ptr ds:[eax-0xC],0x0;判断字符串是否为空
00402767 |. 74 74 je short 52PoJie?004027DD

继续F8,到CreateFileA函数,创建一个52Pojie.txt文件。

1
2
3
4
5
6
7
8
00402769  |.  6A 00               push 0x0                                     ; /hTemplateFile = NULL
0040276B |. 6A 00 push 0x0 ; |Attributes = 0
0040276D |. 6A 02 push 0x2 ; |Mode = CREATE_ALWAYS
0040276F |. 6A 00 push 0x0 ; |pSecurity = NULL
00402771 |. 6A 01 push 0x1 ; |ShareMode = FILE_SHARE_READ
00402773 |. 68 00000040 push 0x40000000 ; |Access = GENERIC_WRITE
00402778 |. FFB6 BC000000 push dword ptr ds:[esi+0xBC] ; |FileName = "C:\Documents and Settings\Administrator\桌面\吾爱破解培训第五课例子\52Pojie.txt"
0040277E |. FF15 10345400 call dword ptr ds:[<&KERNEL32.CreateFileA>] ; \CreateFileA

F8到WriteFileA函数,将我们输入的字符串写入52Pojie.txt文件中。

1
2
3
4
5
6
7
004027AD  |> \6A 00               push 0x0                                     ; /pOverlapped = NULL
004027AF |. 8D45 E8 lea eax,[local.6] ; |
004027B2 |. 50 push eax ; |pBytesWritten = 00000009
004027B3 |. 57 push edi ; |nBytesToWrite = 0x9
004027B4 |. 51 push ecx ; |Buffer = 00161100
004027B5 |. 56 push esi ; |hFile = 000000D4 (window)
004027B6 |. FF15 2C345400 call dword ptr ds:[<&KERNEL32.WriteFile>] ; \WriteFile

继续F8,004027C9执行弹窗,退出,关闭句柄。再之后就退出程序了。

1
2
3
4
5
6
7
8
004027C0  |.  6A 00               push 0x0
004027C2 |. 6A 00 push 0x0
004027C4 |. 68 60465400 push 52PoJie?00544660 ; 你选择的验证类型是重启验证1
004027C9 |. E8 B17F0000 call 52PoJie?0040A77F
004027CE |. 6A 00 push 0x0 ; /ExitCode = 0x0
004027D0 |. FF15 40385400 call dword ptr ds:[<&USER32.PostQuitMessage>>; \PostQuitMessage
004027D6 |> 56 push esi ; /hObject = 000000D4 (window)
004027D7 |. FF15 24345400 call dword ptr ds:[<&KERNEL32.CloseHandle>] ; \CloseHandle

因为它是重启验证,所以在这个函数中找不到验证算法。而是在主程序出来前就已经运行验证算法了。再次打开程序时,程序会在相应的.txt,.ini或注册表里找注册码进行验证。

重载,找敏感字符串有关“52Pojie.txt”的双击进去反汇编代码,在函数开头下断。F9运行至断点处,F8路过这个函数

1
2
3
00402AE2  |.  50            push eax                                 ; /Buffer = 00174B68
00402AE3 |. 68 04010000 push 0x104 ; |BufSize = 104 (260.)
00402AE8 |. FF15 14345400 call dword ptr ds:[<&KERNEL32.GetCurrent>; \GetCurrentDirectoryA 获取当前目录

继续F8,发现这个跳转跳过了“验证通过”。

1
2
3
4
5
6
7
8
00402BC0  |.  E8 1BF4FFFF   call 52PoJie?00401FE0
00402BC5 |. 85C0 test eax,eax
00402BC7 |. /74 15 je short 52PoJie?00402BDE
00402BC9 |. |8B8D DCFEFFFF mov ecx,[local.73]
00402BCF |. |68 18465400 push 52PoJie?00544618 ; 验证通过
00402BD4 |. |E8 E7980100 call 52PoJie?0041C4C0
00402BD9 |. |BB 01000000 mov ebx,0x1
00402BDE |> \8B85 E0FEFFFF mov eax,[local.72]

如果是爆破的话,直接将跳转指令nop掉。保存,运行一下,成功。

如果想逆向分析,跟进去第1行的call指令,那个就是算法函数。

1
2
3
4
5
6
7
8
00401FF7  |.  56            push esi                                 ; /hTemplateFile = 00174B68
00401FF8 |. 56 push esi ; |Attributes = ARCHIVE|TEMPORARY|COMPRESSED|174248
00401FF9 |. 6A 03 push 0x3 ; |Mode = OPEN_EXISTING
00401FFB |. 56 push esi ; |pSecurity = 00174B68
00401FFC |. 6A 01 push 0x1 ; |ShareMode = FILE_SHARE_READ
00401FFE |. 6A 01 push 0x1 ; |Access = 1
00402000 |. FFB1 BC000000 push dword ptr ds:[ecx+0xBC] ; |FileName = "C:\Documents and Settings\Administrator\桌面\吾爱破解培训第五课例子\52Pojie.txt"
00402006 |. FF15 10345400 call dword ptr ds:[<&KERNEL32.CreateFile>; \CreateFileA 创建或打开文件,这里是打开文件

ReadFile函数,当程序运行到00402046时,Buffer指针指向文件内容首地址0012F348

1
2
3
4
5
6
7
00402039  |.  56            push esi                                 ; /pOverlapped = NULL
0040203A |. 50 push eax ; |pBytesRead = 0012F348
0040203B |. 68 04010000 push 0x104 ; |BytesToRead = 104 (260.)
00402040 |. 8D85 F4FEFFFF lea eax,[local.67] ; |
00402046 |. 50 push eax ; |Buffer = 0012F348
00402047 |. 57 push edi ; |hFile = 000000AC (window)
00402048 |. FF15 28345400 call dword ptr ds:[<&KERNEL32.ReadFile>] ; \ReadFile 读文件内容

在信息窗口选中右键->数据窗口跟随,发现全是空数据,执行完这个函数时,0012F348出现我们输入的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
00402048  |.  FF15 28345400 call dword ptr ds:[<&KERNEL32.ReadFile>] ; \ReadFile
0040204E |. 85C0 test eax,eax;返回值为1,表明文件不为空
00402050 |. 74 3D je short 52PoJie?0040208F
00402052 |. B9 24465400 mov ecx,52PoJie?00544624 ; JXU2MjExJXU2
00402057 |. 8D85 F4FEFFFF lea eax,[local.67]
0040205D |. 8D49 00 lea ecx,dword ptr ds:[ecx]
00402060 |> 8A10 /mov dl,byte ptr ds:[eax];这个循环执行的是strcmp函数,比较eax和ecx是否相等。执行到这一句时,eax是我们输入的字符串,ecx是上面的注释JXU2MjExJXU2
00402062 |. 3A11 |cmp dl,byte ptr ds:[ecx]
00402064 |. 75 1A |jnz short 52PoJie?00402080
00402066 |. 84D2 |test dl,dl
00402068 |. 74 12 |je short 52PoJie?0040207C
0040206A |. 8A50 01 |mov dl,byte ptr ds:[eax+0x1]
0040206D |. 3A51 01 |cmp dl,byte ptr ds:[ecx+0x1]
00402070 |. 75 0E |jnz short 52PoJie?00402080
00402072 |. 83C0 02 |add eax,0x2
00402075 |. 83C1 02 |add ecx,0x2
00402078 |. 84D2 |test dl,dl
0040207A |.^ 75 E4 \jnz short 52PoJie?00402060
0040207C |> 33C0 xor eax,eax

所以很容易就知道重启验证1的注册码是JXU2MjExJXU2。将生成的.txt文件内容改为这个,再打开原程序发现验证通过。

验证2、3一样操作,但需要注意,一定要先生成一个配置文件或写入注册表再进行重启验证调试。验证1生成的.txt文件不影响验证2和验证3的调试,可以不删除。

验证2:

1
2
3
4
00402C34  |.  E8 77F4FFFF   call 52PoJie?004020B0
00402C39 |. 85C0 test eax,eax
00402C3B |. 74 11 je short 52PoJie?00402C4E
00402C3D |. 68 18465400 push 52PoJie?00544618 ; 验证通过
1
0040210C  |.  B9 40465400   mov ecx,52PoJie?00544640                 ;  NjJGJXU3NTI

验证3:

1
2
3
4
00402CB2  |.  E8 B9F4FFFF   call 52PoJie?00402170
00402CB7 |. 85C0 test eax,eax
00402CB9 |. 74 37 je short 52PoJie?00402CF2
00402CBB |. 68 18465400 push 52PoJie?00544618 ; 验证通过
1
004021FE  |.  B9 54465400   mov ecx,52PoJie?00544654                 ;  4JXU2MjM3

细心一点就会在字符串列表发现这三个注册码

最后,重启验证的普通思路

  1. 如果是写进.txt文件,一般都是这个步骤:

    CreateFileA->WriteFile->ReadFile->比较算法

  2. 如果是写进.ini文件,一般都是这个步骤:

    WritePrivateProfileStringA(写入配置信息)->GetPrivateProfileStringA(读取配置信息)

  3. 如果是写进注册表,一般都是这个步骤:

    创建注册表Key:RegCreateKey->打开注册表Key:RegOpenKey->写入注册表键值:RegSetValueEx->查询注册表键值:RegQueryValue(Ex)

三个都通过后,会出现一个彩蛋,输入字符串后,弹出消息框。

输入不同字符串弹窗显示不同的内容。彩蛋很简单,其实就是把三个验证码拼接,Base64解码,UTF-8转换,得到“我是用户”。

1
2
3
4
5
6
7
JXU2MjExJXU2NjJGJXU3NTI4JXU2MjM3

Base64解码:
%u6211%u662F%u7528%u6237

Unicode转换:
我是用户

把三个验证码拼接,弹窗。

4. 重启验证作业

输入假码没什么反应。

拉去OD看看,搜索字符串,发现只有一个验证码ITN3UXJGJ显示。

双击进去发现有CreateFileReadFile,所以判定这个是写入.txt文件的注册码。

GetPrivateProfileStringA函数找到.ini文件的比较注册码算法。Ctrl+N,搜索GetPrivateProfileStringA右键->在每个参考上设置断点。或者在反汇编窗口右键->查找->所有模块间的调用,搜索函数,右键->在每个调用到GetPrivateProfileStringA上设置断点。一个一个断点点进去看,找到一个最像比较算法的(有循环、比较、跳转指令)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0040218F  |.  66:C785 F0FEF>mov word ptr ss:[ebp-0x110],0x1A28       ; |
00402198 |. FF15 1C345400 call dword ptr ds:[<&KERNEL32.GetPrivate>; \GetPrivateProfileStringA
0040219E |. 5E pop esi ; kernel32.7C817077
0040219F |. 85C0 test eax,eax
004021A1 |. 74 4D je short 吾爱破解.004021F0
004021A3 |. 8D8D F4FEFFFF lea ecx,[local.67]
004021A9 |. 8D51 01 lea edx,dword ptr ds:[ecx+0x1]
004021AC |. 8D6424 00 lea esp,dword ptr ss:[esp]
004021B0 |> 8A01 /mov al,byte ptr ds:[ecx]
004021B2 |. 41 |inc ecx
004021B3 |. 84C0 |test al,al
004021B5 |.^ 75 F9 \jnz short 吾爱破解.004021B0
004021B7 |. 2BCA sub ecx,edx ; ntdll.KiFastSystemCallRet
004021B9 |. 83F9 0E cmp ecx,0xE
004021BC |. 75 32 jnz short 吾爱破解.004021F0
004021BE |. 33C0 xor eax,eax
004021C0 |> 0FB69405 E4FE>/movzx edx,byte ptr ss:[ebp+eax-0x11C]
004021C8 |. 0FBE8C05 F4FE>|movsx ecx,byte ptr ss:[ebp+eax-0x10C]
004021D0 |. 83C2 30 |add edx,0x30
004021D3 |. 3BD1 |cmp edx,ecx
004021D5 |. 75 19 |jnz short 吾爱破解.004021F0
004021D7 |. 40 |inc eax
004021D8 |. 83F8 0E |cmp eax,0xE
004021DB |.^ 72 E3 \jb short 吾爱破解.004021C0

由于这个断点程序没有被经过,很难分析它的算法(我目前能力有限)。要想经过这段算法,就要看一下我们输入字符串后,程序怎么运行的。反汇编窗口右键->查找->所有模块间的调用,搜索GetWindowTextA,在每个调用到GetWindowTextA上设置断点。输入字符串后运行,程序停在某断点处。养成好习惯,把其它的GetWindowTextA断点取消。

1
2
3
4
5
6
7
0041837E  |.  FF15 2C365400 call dword ptr ds:[<&USER32.GetWindowTex>; \GetWindowTextA
00418384 |. 8B4D 08 mov ecx,[arg.1]
00418387 |. 6A FF push -0x1
00418389 |. E8 AEFEFEFF call 吾爱破解.0040823C
0041838E |. 5E pop esi ; 00940796
0041838F |. 5D pop ebp ; 00940796
00418390 |. C2 0400 retn 0x4

返回上一级函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
004029D3  |.  E8 7F590100   call 吾爱破解.00418357                       ;  GetWindowTextA函数所在
004029D8 |. 8B4D EC mov ecx,[local.5] ; 字符串存进ecx
004029DB |. 8B79 F4 mov edi,dword ptr ds:[ecx-0xC] ; 字符串长度存进edi
004029DE |. 85FF test edi,edi
004029E0 |. 74 1A je short 吾爱破解.004029FC ; 字符串长度为0跳转
004029E2 |. 33D2 xor edx,edx ; edx清零
004029E4 |. 85FF test edi,edi
004029E6 |. 7E 14 jle short 吾爱破解.004029FC ; 字符串长度小于等于0跳转
004029E8 |> 85D2 /test edx,edx
004029EA |. 78 36 |js short 吾爱破解.00402A22 ; 结果为负跳转
004029EC |. 3BD7 |cmp edx,edi
004029EE |. 7F 32 |jg short 吾爱破解.00402A22 ; 大于跳转
004029F0 |. 803C0A 2D |cmp byte ptr ds:[edx+ecx],0x2D ; 判断字符是否是“-”
004029F4 |. 75 01 |jnz short 吾爱破解.004029F7 ; 不是指向下一个字符
004029F6 |. 46 |inc esi ; 是就+1
004029F7 |> 42 |inc edx
004029F8 |. 3BD7 |cmp edx,edi
004029FA |.^ 7C EC \jl short 吾爱破解.004029E8 ; 遍历完字符串退出循环
004029FC |> C1E6 04 shl esi,0x4
004029FF |. 83EE 02 sub esi,0x2
00402A02 |. B8 CDCCCCCC mov eax,0xCCCCCCCD
00402A07 |. F7E6 mul esi
; 执行完这条指令后edx的值被覆盖,所以下面的0x18不能看作是字符串长度,字符串长度存储在edi里
00402A09 |. C1EA 03 shr edx,0x3 ; 根据edx右移3位后要等于3,所以3左移3位等于0x18
00402A0C |. BE FFFFFFFF mov esi,-0x1
00402A11 |. 83EA 03 sub edx,0x3 ; 根据下面跳转判断edx=3
00402A14 |. 0F85 C8020000 jnz 吾爱破解.00402CE2 ; 不为0跳转,这里跳转绕过注册成功
00402A1A |. 85FF test edi,edi
00402A1C |. 7F 0E jg short 吾爱破解.00402A2C
00402A1E |. 0BF6 or esi,esi
00402A20 |. EB 23 jmp short 吾爱破解.00402A45
00402A22 |> 68 57000780 push 0x80070057
00402A27 |. E8 C4E9FFFF call 吾爱破解.004013F0
00402A2C |> 6A 2D push 0x2D
00402A2E |. 51 push ecx
00402A2F |. E8 D4921100 call 吾爱破解.0051BD08

进一步分析这几条指令:

1
2
3
4
5
6
004029FC  |> \C1E6 04       shl esi,0x4                              ;  从下面逆推0x20右移4位为2,所以“-”的个数为2
004029FF |. 83EE 02 sub esi,0x2 ; 要使下面的edx=0x18,这里esi=0x1E+2=0x20才对
; esi=18 66666666除以CCCCCCCD=0X1E
00402A02 |. B8 CDCCCCCC mov eax,0xCCCCCCCD ; eax=CCCCCCCD
00402A07 |. F7E6 mul esi ; edx拼接eax=esi*eax要等于00000018 66666666
00402A09 |. C1EA 03 shr edx,0x3 ; 根据edx右移3位后要等于3,所以3左移3位等于0x18

懂了,所以字符串中需要两个“-”号。输入1234567-89012345-6789012试试(“-”号位置任意)。

发现注册成功。注册成功后会在目录下生成.txt,.ini文件,写入注册表。

1
2
3
4
5
6
7
8
9
52pojie.txt
1234567

52pojie.ini
[验证]
Key=89012345

注册表
6789012

从上面的分析中已经知道.txt文件的注册码是ITN3UXJGJ,改了再说,这里是9个字符。这时再下断GetPrivateProfileStringA函数,程序就会经过这个算法。此时就可以分析算法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
00402198  |.  FF15 1C345400 call dword ptr ds:[<&KERNEL32.GetPrivate>; \GetPrivateProfileStringA
0040219E |. 5E pop esi
0040219F |. 85C0 test eax,eax ; ini文件的字符串长度
004021A1 |. 74 4D je short 吾爱破解.004021F0 ; 字符串长度为0跳转
004021A3 |. 8D8D F4FEFFFF lea ecx,[local.67] ; ecx等于字符串
004021A9 |. 8D51 01 lea edx,dword ptr ds:[ecx+0x1]
004021AC |. 8D6424 00 lea esp,dword ptr ss:[esp]
004021B0 |> 8A01 /mov al,byte ptr ds:[ecx]
004021B2 |. 41 |inc ecx ; 指针+1指向下个字符
004021B3 |. 84C0 |test al,al
004021B5 |.^ 75 F9 \jnz short 吾爱破解.004021B0 ; 遍历字符串
004021B7 |. 2BCA sub ecx,edx ; ecx存进字符串长度
004021B9 |. 83F9 0E cmp ecx,0xE ; ini文件中字符串长度为14
004021BC |. 75 32 jnz short 吾爱破解.004021F0
004021BE |. 33C0 xor eax,eax ; eax清零
004021C0 |> 0FB69405 E4FE>/movzx edx,byte ptr ss:[ebp+eax-0x11C] ; 将真正的注册码存入edx
004021C8 |. 0FBE8C05 F4FE>|movsx ecx,byte ptr ss:[ebp+eax-0x10C] ; 将输入的字符串存入ecx
004021D0 |. 83C2 30 |add edx,0x30 ; 将真正的注册码+0x30
004021D3 |. 3BD1 |cmp edx,ecx ; 与输入的字符串对比
004021D5 |. 75 19 |jnz short 吾爱破解.004021F0
004021D7 |. 40 |inc eax
004021D8 |. 83F8 0E |cmp eax,0xE
004021DB |.^ 72 E3 \jb short 吾爱破解.004021C0

在数据窗口Ctrl+G输入ebp+eax-0x11c,第1行就是真正的注册码,第2行是输入的字符串。

1
2
0012F33C  3A 1E 02 25 28 1A 48 15 3A 1D 02 25 28 1A 00 00  :%(H:%(..
0012F34C 38 39 30 31 34 35 36 37 38 39 30 31 32 31 00 00 89014567890121..

将真正的注册码+0x30就是我们要输入的字符串jN2UXJxEjM2UXJ

1
6A 4E 32 55 58 4A 78 45 6A 4D 32 55 58 4A

将目录下的.ini文件信息修改为以上字符串。

注册表部分。在反汇编窗口右键->查找->所有模块间的调用,搜索RegQueryValueExA函数,右键->在每个调用到RegQueryValueExA上设置断点。找到有比较算法那个,分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00402285  |.  FF15 24305400 call dword ptr ds:[<&ADVAPI32.RegQueryVa>; \RegQueryValueExA
0040228B |. 85C0 test eax,eax
0040228D |. 0F85 E1000000 jnz 吾爱破解.00402374
00402293 |. 8D8D F4FEFFFF lea ecx,[local.67] ; 输入的字符串
00402299 |. 8D51 01 lea edx,dword ptr ds:[ecx+0x1]
0040229C |. 8D6424 00 lea esp,dword ptr ss:[esp]
004022A0 |> 8A01 /mov al,byte ptr ds:[ecx]
004022A2 |. 41 |inc ecx
004022A3 |. 84C0 |test al,al
004022A5 |.^ 75 F9 \jnz short 吾爱破解.004022A0 ; 遍历字符串
004022A7 |. 2BCA sub ecx,edx ; ecx=字符串长度=9
004022A9 |. 83C1 06 add ecx,0x6 ; 所以ecx=字符串长度+6=0xF
004022AC |. B8 CDCCCCCC mov eax,0xCCCCCCCD ; eax=CCCCCCCD
004022B1 |. F7E1 mul ecx ; 要使得edx拼接eax=ecx*eax=C 33333336
004022B3 |. C1EA 02 shr edx,0x2 ; edx=0x0C
004022B6 |. 83EA 03 sub edx,0x3 ; edx=0x3
004022B9 |. 0F85 B5000000 jnz 吾爱破解.00402374 ; 退出函数,不能跳
004022BF |. E8 D82D0000 call 吾爱破解.0040509C

所以注册表的注册码是9个字符,修改修改再继续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
004022BF  |.  E8 D82D0000       call 吾爱破解.0040509C                       ;  mov eax=lJT
004022C4 |. 8BC8 mov ecx,eax
004022C6 |. 85C9 test ecx,ecx
004022C8 |. 0F84 C3000000 je 吾爱破解.00402391
004022CE |. 8B01 mov eax,dword ptr ds:[ecx] ; eax=lJT.
004022D0 |. FF50 0C call dword ptr ds:[eax+0xC] ; eax指向地址58E5E0,也就是lJT.的下一地址
004022D3 |. 8A95 F8FEFFFF mov dl,byte ptr ss:[ebp-0x108] ; dl指向输入的字符第五个字符的地址
004022D9 |. 8D70 10 lea esi,dword ptr ds:[eax+0x10] ; esi=58e5f0
004022DC |. 0FBE85 F4FEFFFF movsx eax,byte ptr ss:[ebp-0x10C] ; eax=字符串的第1个字符
004022E3 |. 0FBECA movsx ecx,dl ; ecx=字符串的第5个字符
004022E6 |. 48 dec eax ; eax=eax-1
004022E7 |. 89B5 E4FEFFFF mov [local.71],esi ; 吾爱破解.0058E5F0
004022ED |. 3BC1 cmp eax,ecx ; str[0]-1==str[4]
004022EF |. 75 69 jnz short 吾爱破解.0040235A ; 不能跳

字符串中第1个字符要比第5个字符大1,修改修改继续。

1
2
3
4
004022F1  |.  0FBE85 FCFEFFFF   movsx eax,byte ptr ss:[ebp-0x104]        ;  eax=输入的第9个字符
004022F8 |. 83C1 02 add ecx,0x2
004022FB |. 3BC8 cmp ecx,eax ; str[8]==str[4]+2
004022FD |. 75 5B jnz short 吾爱破解.0040235A

字符串中第9个字符要比第5个字符大2,修改修改继续。

1
2
3
004022FF  |.  80F2 54           xor dl,0x54                              ;  dl是第5个字符
00402302 |. 80FA 66 cmp dl,0x66 ; 第5个字符与0x54异或要等于0x66
00402305 |. 75 53 jnz short 吾爱破解.0040235A

所以第5个字符是0x32(‘2’),也可以推算出,第1个字符是’3’,第9个字符是’4’。修改修改再继续。push了3个参数,第一个参数是长度,第二个参数是”MjM”,第三个参数是字符串的第2个字符的地址。猜测这个函数是比较2-4位是否为”MjM”。

1
2
3
4
5
6
7
8
00402307  |.  6A 03             push 0x3
00402309 |. 8D85 F5FEFFFF lea eax,dword ptr ss:[ebp-0x10B] ; eax=字符串的第2个字符地址
0040230F |. 68 3C465400 push 吾爱破解.0054463C ; MjM
00402314 |. 50 push eax
00402315 |. E8 76991100 call 吾爱破解.0051BC90
0040231A |. 83C4 0C add esp,0xC
0040231D |. 85C0 test eax,eax
0040231F |. 75 39 jnz short 吾爱破解.0040235A

修改一下试试。跳转没有实现,猜测正确。下面是一样的步骤。

1
2
3
4
5
6
7
8
00402321  |.  6A 03             push 0x3
00402323 |. 8D85 F9FEFFFF lea eax,dword ptr ss:[ebp-0x107] ; eax=第6个字符地址
00402329 |. 68 40465400 push 吾爱破解.00544640 ; UXJ
0040232E |. 50 push eax
0040232F |. E8 5C991100 call 吾爱破解.0051BC90
00402334 |. 83C4 0C add esp,0xC
00402337 |. 85C0 test eax,eax
00402339 |. 75 1F jnz short 吾爱破解.0040235A

综上,注册表的注册码为3MjM2UXJ4

完整注册码为ITN3UXJGJ-jN2UXJxEjM2UXJ-3MjM2UXJ4,得到彩蛋。

点击“我是彩蛋”有一连串的弹窗提示。这些弹窗语句早在我们搜索字符串的时候就已经看到了。

Key1=ITN3UXJGJ-jN2UXJxEjM2UXJ-3MjM2UXJ4

Key2_1=4JXU2MjM3-JXU2MjExJXU2Nj-JGJXU3NTl

Key2_2=3MjM2UXJ4-jN2UXJxEjM2UXJ-ITN3UXJGJ

Key2_3=JGJXU3NTl-JXU2MjExJXU2Nj-4JXU2MjM3

Key2的所有数字之和长度之间的差为4,且比长度大。所以彩蛋Key长度为12。

Key的倒数第二位为2,最后一位为=。Key=00000000002=

Key的第一位是什么呢?B,我们经常说的,NB!Key=N0000000002=

Key的倒数第三位是Key2的第三位?J、j、X?

Key的第二位为管理员名字的首字母,管理员是Hmily,所以是H,Key=NH000000002=

Key的第三位和第九位一样,都是第五位的小写字母。

好难猜啊…我看到答案是NHlkLXpdIU2=,但完全不知道怎么得来的。

5. 给程序打补丁

吾爱破解内存补丁生成器V1.00:做程序补丁

KeyMake V2.0 修改版:做内存注册机

拉进OD,查找敏感字符串。

1
2
3
4
5
6
7
8
9
0040342B  |. /75 07         jnz short 第六课例.00403434
0040342D |. |68 78E15700 push 第六课例.0057E178 ; 注册成功
00403432 |. |EB 10 jmp short 第六课例.00403444
00403434 |> \68 84E15700 push 第六课例.0057E184 ; 注册失败
00403439 |. EB 09 jmp short 第六课例.00403444
0040343B |> 6A 00 push 0x0
0040343D |. 6A 00 push 0x0
0040343F |. 68 6CE15700 push 第六课例.0057E16C ; 输入为空
00403444 |> E8 67080100 call 第六课例.00413CB0

爆破补丁版:将地址为40342Bjnz直接修改为jznop即可。

那如果使用吾爱破解内存补丁生成器V1.00爆破要怎么操作呢?找到需要修改的地址后,打开补丁生成器,将程序拖入补丁生成器中,输入内存地址和需要修改成什么指令,添加指令,导出补丁。

关闭OD后打开补丁程序,点击“开始补丁”就会自动运行补丁程序,此时输入什么都可以注册成功了。

注册机版:向上划到段首下断运行,F8单步到此处。寄存器窗口和堆栈窗口都可看到真正的注册码。

1
2
3
4
5
6
7
8
9
10
11
12
00403400  |> /8A10          /mov dl,byte ptr ds:[eax]                ;  eax指向真正的注册码
00403402 |. |3A11 |cmp dl,byte ptr ds:[ecx] ; ecx指向输入的注册码
00403404 |. |75 1A |jnz short 第六课例.00403420
00403406 |. |84D2 |test dl,dl
00403408 |. |74 12 |je short 第六课例.0040341C
0040340A |. |8A50 01 |mov dl,byte ptr ds:[eax+0x1]
0040340D |. |3A51 01 |cmp dl,byte ptr ds:[ecx+0x1]
00403410 |. |75 0E |jnz short 第六课例.00403420
00403412 |. |83C0 02 |add eax,0x2
00403415 |. |83C1 02 |add ecx,0x2
00403418 |. |84D2 |test dl,dl
0040341A |.^\75 E4 \jnz short 第六课例.00403400
1
2
3
4
5
6
7
0012F6EC   186B4420
0012F6F0 00000111
0012F6F4 00000000
0012F6F8 00194078 ASCII "202CB962AC59075B964B07152D234B70"
0012F6FC 00194078 ASCII "202CB962AC59075B964B07152D234B70"
0012F700 00192828 ASCII "852"
0012F704 001927B8 ASCII "123"

记下运行到403400,真正的注册码存在了寄存器窗口的eax的值(注册码字符串的首地址)里。

打开Keymake,其它->内存注册机,将需要制作注册机的程序加载进来,添加。编辑信息->添加->生成。

运行注册机,发现需要两个框都填了才能给注册码,否则提示“输入为空”。为了美观,也可将004033A7的跳转指令nop掉。

1
2
3
4
5
6
7
8
9
10
00403384  |.  50            push eax                                 ;  将用户名入栈
00403385 |. 8D8F 30010000 lea ecx,dword ptr ds:[edi+0x130]
0040338B |. E8 12630100 call 第六课例.004196A2
00403390 |. 8B45 EC mov eax,[local.5] ; eax=用户名
00403393 |. 83CF FF or edi,-0x1
00403396 |. 8378 F4 00 cmp dword ptr ds:[eax-0xC],0x0
0040339A |. 0F84 9B000000 je 第六课例.0040343B ; 用户名为空跳转
004033A0 |. 8B45 E8 mov eax,[local.6]
004033A3 |. 8378 F4 00 cmp dword ptr ds:[eax-0xC],0x0
004033A7 |. 0F84 8E000000 je 第六课例.0040343B ; 注册码为空跳转

nop掉后再生成注册机。虽然不会提示“输入为空”,但也会提示“注册失败”字样,暂时没有什么好的办法,就这样吧。(提示“输入为空”比“注册失败”要好,还是别改了吧)

也可以用Keymake制作内存补丁,Shark恒 制作内存补丁

6. 打补丁作业

程序加了VMProtect壳。VMProtect 是软件保护系统,将保护后的代码放到虚拟机中运行,这将使分析反编译后的代码和破解变得极为困难。这个我目前还不会手脱,那就在有壳的基础上修改吧。

F9运行程序跑起来,进入OD的E模块,双击进入程序代码块(.exe)。取消分析,智能搜索发现字符串都能看到了。搜索敏感字符串进入反汇编代码,还是熟悉的位置。

1
2
3
4
5
6
7
8
9
0040346B   /75 07           jnz short 吾爱破解.00403474
0040346D |68 80E15700 push 吾爱破解.0057E180 ; 注册成功
00403472 |EB 10 jmp short 吾爱破解.00403484
00403474 \68 8CE15700 push 吾爱破解.0057E18C ; 注册失败
00403479 EB 09 jmp short 吾爱破解.00403484
0040347B 6A 00 push 0x0
0040347D 6A 00 push 0x0
0040347F 68 6CE15700 push 吾爱破解.0057E16C ; 输入为空
00403484 E8 78080100 call 吾爱破解.00413D01

爆破补丁版:

但发现这样nop后保存文件会提示“无法定位数据”。

这时就需要吾爱破解内存补丁生成器V1.00了。jnz的机器码为0x75,je指令为0x74,nop的机器码为0x90。关闭OD再运行补丁。

注册机版:

由于程序有壳,无法重新载入,但还是可以在这个函数中一步步运行程序到算法处,按照5的方法生成注册机即可。

1
2
3
4
5
6
7
8
9
10
11
12
00403440  |>  8A10          /mov dl,byte ptr ds:[eax];真正注册码
00403442 |. |3A11 |cmp dl,byte ptr ds:[ecx];输入的字符串
00403444 |. |75 1A |jnz short 吾爱破解.00403460
00403446 |. |84D2 |test dl,dl
00403448 |. |74 12 |je short 吾爱破解.0040345C
0040344A |. |8A50 01 |mov dl,byte ptr ds:[eax+0x1]
0040344D |. |3A51 01 |cmp dl,byte ptr ds:[ecx+0x1]
00403450 |. |75 0E |jnz short 吾爱破解.00403460
00403452 |. |83C0 02 |add eax,0x2
00403455 |. |83C1 02 |add ecx,0x2
00403458 |. |84D2 |test dl,dl
0040345A |.^\75 E4 \jnz short 吾爱破解.00403440

分析算法:

在段首下断,输入用户名和注册码后运行,程序停在断点处。F8一步步跟,分析每一步程序做了什么。执行完4033BF地址的指令后,可以看到堆栈窗口中用户名和“123456”拼接在一起。

执行完4033FE地址的指令后,堆栈窗口出现真正的注册码。

那我们进去看看它是怎么生成的。看到这一大段就应该意识到这是MD5的初始化处理。

1
2
3
4
5
6
7
8
9
00401B55    C745 D8 0000000>mov dword ptr ss:[ebp-0x28],0x0
00401B5C C745 D4 0000000>mov dword ptr ss:[ebp-0x2C],0x0
00401B63 C745 DC 0123456>mov dword ptr ss:[ebp-0x24],0x67452301
00401B6A C745 E0 89ABCDE>mov dword ptr ss:[ebp-0x20],0xEFCDAB89
00401B71 C745 E4 FEDCBA9>mov dword ptr ss:[ebp-0x1C],0x98BADCFE
00401B78 C745 E8 7654321>mov dword ptr ss:[ebp-0x18],0x10325476
00401B7F C645 FC 02 mov byte ptr ss:[ebp-0x4],0x2
00401B83 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
00401B86 8B70 F4 mov esi,dword ptr ds:[eax-0xC]

传入的参数是用户名和“123456”拼接,所以密码是MD5(用户名+”123456”)。