#前言

以下内容仅供动态分析学习交流
小学习了一下软件动态分析技术,迫不及待拿了一个简单的取模软件进行分析
Pasted_image_20260312214300.webp
这是一个比较经典的生成C语言数组的取模软件,在嵌入式开发时很常用,但是这个软件太老了,网上也有很多可以使用的注册码能搜到,这个软件大概是在0几年时发布的,当时大多数注册码保护的软件基本都是这种离线校验注册码的逻辑。接下来就准备使用OllyDbg对它进行逆向分析,争取做出来一个去掉注册码的“爆破版”即去掉注册码验证环节和写出一个注册机,生成可以使用的注册码

#断点分析

首先,先要找到注册码的验证汇编位置,窗口上有5个文本窗口+一个注册按钮,首先进入OllyDbg的Windows窗口,刷新并找到注册按钮,设置一个注册按钮松开消息断点(202 WM_LBUTTONUP)Pasted_image_20260312223041.webp
Pasted_image_20260312223058.webp
随便填入注册码并点击注册
Pasted_image_20260312223229.webp
可以看到此时断点断在了Windows处理按钮松开的事件位置,接下来我们要返回到程序领空,因此我们在内存中主程序的.text区块中添加断点
Pasted_image_20260312223558.webp

#程序跟踪

来到了此处,经过分析,可以看到0030CCED CMP EAX,111其中的 0x111是 Windows 消息 WM_COMMAND 的十六进制代码,即检查返回的消息是否是“命令”(按钮点击)因此继续设置断点并跟随下一句JE Img2Lcd.0040CE1C 进行跳转
Pasted_image_20260312224743.webp观察此处代码,根据ai辅助判断,可以分析出

  1. AND EDX, 0FFFF (地址 0040CE22):
    • 这是在提取 控件ID (Control ID)
    • 当按钮被点击时,系统会发送一个 ID 过来。这段代码把这个 ID 拿出来放在 EDX 里。
  2. JMP DWORD PTR DS:[EDX*4+40D0C8] (地址 0040CE3F):
    • 关键点在这里! 这是一个 Switch 跳转表
    • 程序根据按钮的 ID (EDX),计算出一个地址,然后直接跳过去。
    • 不同的按钮 ID 会跳去不同的代码段。其中某一个跳转目标,就是“注册”按钮的处理函数。

因此继续追踪Pasted_image_20260313104854.webp
按F8继续追踪,发现程序在0040CEF3 - 0040CF1E之间循环读取了我们输入的5组注册码
Pasted_image_20260315183300.webp
Pasted_image_20260315183545.webp
Pasted_image_20260315183608.webp观察EBX寄存器就会发现填入的注册数字被分别读取出来后在0040CC10处将十六进制的字符串(Hex String)转换为对应的 32 位整数(Integer)

#程序爆破

程序爆破的原理是将程序的验证逻辑直接跳过,让其一直处于验证通过状态
Pasted_image_20260317175952.webp
Pasted_image_20260315183912.webp继续追踪程序
可以看到0040CF44处程序调用了一个叫sss运行库内的函数
Pasted_image_20260315184418.webp接着往下面读,很快就能看到紧跟着一个CMP比较指令和一个JNZ如果不相等则跳转指令,很明显
Pasted_image_20260315184910.webp这几个指令就是判断输入的注册码是否正确的逻辑,我们使用F8跟踪到JNZ指令处,可以看到跳转会被触发
Pasted_image_20260315184811.webp为了证实猜想,双击Z (ZF - Zero Flag)零标志位将其从0置为1,以手动阻止JNZ指令触发跳转,接下来按下F9让程序正常运行,可以看到,注册成功的提示出现
Pasted_image_20260313105542.webp
Pasted_image_20260317194722.webp此时我们就找到了一个程序的爆破点,制作爆破注册机制的程序思路就很简单了,只需将JNZ这行命令使用nop(什么也不做)命令替换即可
Pasted_image_20260315185941.webp
Pasted_image_20260315190001.webp接下来选中修改的代码,右键菜单执行Copy to executable-Selection后在打开的文件编辑窗口右键选择Save File导出程序,这样我们就成功获得了一个不管输入什么注册码均可注册成功的爆破版软件。

#注册机编写

在进入sss运行库之前,我们可以看到有四句命令对接下来要给到sss库的数据添加了一些小料
Pasted_image_20260315193513.webp为了编写出注册机,我们要跟进sss运行库观察代码
Pasted_image_20260315194053.webp进入sss运行库

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
10007090 > 83EC 18          SUB ESP,18
10007093 B9 06000000 MOV ECX,6
10007098 55 PUSH EBP
10007099 8B6C24 20 MOV EBP,DWORD PTR SS:[ESP+20]
1000709D 56 PUSH ESI
1000709E 57 PUSH EDI
1000709F 8BF5 MOV ESI,EBP
100070A1 8D7C24 0C LEA EDI,DWORD PTR SS:[ESP+C]
100070A5 F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS>
100070A7 66:837C24 1E 00 CMP WORD PTR SS:[ESP+1E],0
100070AD 74 18 JE SHORT sss.100070C7
100070AF 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+C]
100070B3 33D2 XOR EDX,EDX
100070B5 8BC1 MOV EAX,ECX
100070B7 0FAF4424 1E IMUL EAX,DWORD PTR SS:[ESP+1E]
100070BC 8AD4 MOV DL,AH
100070BE 8AF0 MOV DH,AL
100070C0 03D1 ADD EDX,ECX
100070C2 66:895424 0E MOV WORD PTR SS:[ESP+E],DX
100070C7 8B4424 0E MOV EAX,DWORD PTR SS:[ESP+E]
100070CB 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+C]
100070CF 25 FFFF0000 AND EAX,0FFFF
100070D4 81E1 FFFF0000 AND ECX,0FFFF
100070DA 03C1 ADD EAX,ECX
100070DC 8D5424 0C LEA EDX,DWORD PTR SS:[ESP+C]
100070E0 894424 20 MOV DWORD PTR SS:[ESP+20],EAX
100070E4 33C0 XOR EAX,EAX
100070E6 8A6424 0E MOV AH,BYTE PTR SS:[ESP+E]
100070EA 52 PUSH EDX
100070EB 8A4424 11 MOV AL,BYTE PTR SS:[ESP+11]
100070EF 50 PUSH EAX
100070F0 E8 5BFFFFFF CALL sss.10007050
100070F5 B9 88990000 MOV ECX,9988
100070FA 83C4 08 ADD ESP,8
100070FD 66:894424 10 MOV WORD PTR SS:[ESP+10],AX
10007102 33F6 XOR ESI,ESI
10007104 894C24 20 MOV DWORD PTR SS:[ESP+20],ECX
10007108 8D5424 0C LEA EDX,DWORD PTR SS:[ESP+C]
1000710C BF 03000000 MOV EDI,3
10007111 66:8B02 MOV AX,WORD PTR DS:[EDX]
10007114 83C2 02 ADD EDX,2
10007117 03F0 ADD ESI,EAX
10007119 25 FFFF0000 AND EAX,0FFFF
1000711E C1E1 10 SHL ECX,10
10007121 03C8 ADD ECX,EAX
10007123 4F DEC EDI
10007124 894C24 20 MOV DWORD PTR SS:[ESP+20],ECX
10007128 ^75 E7 JNZ SHORT sss.10007111
1000712A 8B5424 10 MOV EDX,DWORD PTR SS:[ESP+10]
1000712E 8D4C24 0C LEA ECX,DWORD PTR SS:[ESP+C]
10007132 33D6 XOR EDX,ESI
10007134 51 PUSH ECX
10007135 52 PUSH EDX
10007136 E8 15FFFFFF CALL sss.10007050
1000713B 66:894424 1A MOV WORD PTR SS:[ESP+1A],AX
10007140 8B4424 1A MOV EAX,DWORD PTR SS:[ESP+1A]
10007144 8D4C24 14 LEA ECX,DWORD PTR SS:[ESP+14]
10007148 8DB406 6EE1FFFF LEA ESI,DWORD PTR DS:[ESI+EAX-1E92]
1000714F 51 PUSH ECX
10007150 33C6 XOR EAX,ESI
10007152 50 PUSH EAX
10007153 E8 F8FEFFFF CALL sss.10007050
10007158 8B4C24 3C MOV ECX,DWORD PTR SS:[ESP+3C]
1000715C 83C4 10 ADD ESP,10
1000715F 85C9 TEST ECX,ECX
10007161 66:894424 14 MOV WORD PTR SS:[ESP+14],AX ; 留意这个判断
10007166 74 1C JE SHORT sss.10007184 ; 跳到判断注册码是否正确逻辑
10007168 66:817D 0A 7205 CMP WORD PTR SS:[EBP+A],572 ; 留意这个判断2
1000716E 75 4F JNZ SHORT sss.100071BF ; 跳到直接结束逻辑
10007170 B9 06000000 MOV ECX,6
10007175 8D7424 0C LEA ESI,DWORD PTR SS:[ESP+C]
10007179 8BFD MOV EDI,EBP
1000717B F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS>
1000717D 5F POP EDI
1000717E 5E POP ESI
1000717F 5D POP EBP
10007180 83C4 18 ADD ESP,18
10007183 C3 RETN ; 结束
10007184 66:3B45 08 CMP AX,WORD PTR SS:[EBP+8] ; 下面是检查注册码是否正确函数
10007188 75 2E JNZ SHORT sss.100071B8 ; 如果不正确跳到返回不正确的语句
1000718A 66:8B5424 12 MOV DX,WORD PTR SS:[ESP+12]
1000718F 66:3B55 06 CMP DX,WORD PTR SS:[EBP+6]
10007193 75 23 JNZ SHORT sss.100071B8
10007195 66:8B4424 10 MOV AX,WORD PTR SS:[ESP+10]
1000719A 66:3B45 04 CMP AX,WORD PTR SS:[EBP+4]
1000719E 75 18 JNZ SHORT sss.100071B8
100071A0 66:8B4C24 0E MOV CX,WORD PTR SS:[ESP+E]
100071A5 66:3B4D 02 CMP CX,WORD PTR SS:[EBP+2]
100071A9 75 0D JNZ SHORT sss.100071B8
100071AB 5F POP EDI ; 返回注册码正确
100071AC 66:C745 0A 7205 MOV WORD PTR SS:[EBP+A],572
100071B2 5E POP ESI
100071B3 5D POP EBP
100071B4 83C4 18 ADD ESP,18
100071B7 C3 RETN ; 结束
100071B8 83CE 01 OR ESI,1 ; 返回注册码错误
100071BB 66:8975 0A MOV WORD PTR SS:[EBP+A],SI
100071BF 5F POP EDI
100071C0 5E POP ESI
100071C1 5D POP EBP
100071C2 83C4 18 ADD ESP,18
100071C5 C3 RETN ; 结束

我们先观察整个代码,可以不难发现从10007184100071A9连续4个判断逻辑,很明显,这四个判断逻辑正是在依次对照第5、4、3、2组注册码是否与前面计算出来的数据是否一致,所以不难猜想,在10007184前面的代码基本就是注册码生成的逻辑了,这个库根据注册码的第一位和前面传入的小料进行了各自复杂的运算,得出了注册码后四位来进行逐个对比,如果不对就提示错误。但是我在跟进汇编时发现一个异常现象,每次程序都从10007166 JE SHORT sss.10007184直接快进到了检查注册表的逻辑,但可以发现10007166地址的指令下面还有一段汇编代码,直到RETN返回,一直没有被执行。此时已经初露端倪了,这个库似乎想要回传什么数据。我们对这两个跳转打断点后更改标志位使两个跳转不动作,接下来到达10007175 LEA ESI,DWORD PTR SS:[ESP+C]这一行,在此处dump一下[ESP+C],看看发现了什么!
Pasted_image_20260316214532.webp
读一下此处红色的内存部分:1234 2EBE 63E7 BCE9 CE6D 11D2 F62F 50AE 15F8 2063如果你还记得的话,没错,其中11D2 F62F 50AE 15F8 2063就是程序在调用该库前传递的小料,那么前面的几位字符,不难看出,就是我们期待的以1234为第一组的正确注册码!没错,作者把注册机的逻辑也写在了这个库内,虽然添加了两个判断防止进入注册机逻辑。这样一来就好说了,也不需要修改dll库的汇编代码让它强制吐出注册码了。这样我们只需编写代码调用这个库里的注册机逻辑的程序即可
这里直接使用AI写了一个简单的Python脚本供参考

注意,由于该程序是32位程序,所以也需要使用对应的32位python环境运行,这里极力推荐使用uv python库管理器,输入命令uv venv --python 3.12-x86即可在当前文件夹下安装32位python虚拟环境,不会污染你电脑上的python环境,接着输入uv run register.py即可使用虚拟环境运行该程序,而无需麻烦的挂载虚拟环境脚本

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import ctypes
import struct

print("========== 启动终极 Python 注册机 ===========")

while True:

    user_input = input("\n请输入自定义种子 (4位十六进制,例如 1234, ABCD): ").strip()

    # 允许直接回车使用默认值

    if not user_input:

        print("[*] 检测到空输入,将使用默认种子: 1234")

        seed = 0x1234

        break

    try:

        # 将用户输入的字符串作为十六进制 (base 16) 转换为整数

        seed = int(user_input, 16)

        # 确保输入的值在两字节无符号整数 (0 - 0xFFFF) 范围内,否则 pack_into 会报错

        if 0 <= seed <= 0xFFFF:

            break

        else:

            print("[!] 错误: 种子数值超出范围,请输入 0000 到 FFFF 之间的值。")

    except ValueError:

        print("[!] 错误: 无效的十六进制输入(请勿包含 G-Z 等非十六进制字符)。")

# --------------------------------------------------------------

try:

    dll = ctypes.windll.LoadLibrary("./Patched.dll")

    func_address = dll._handle + 0x7090

    PROTOTYPE = ctypes.WINFUNCTYPE(None, ctypes.c_char_p, ctypes.c_int)

    generate_key = PROTOTYPE(func_address)

    buffer = ctypes.create_string_buffer(256)

    # ------------------ 1. 注入用户输入的种子 ------------------

    struct.pack_into("<H", buffer, 0x00, seed)

    # ------------------ 2. 注入上帝开关 ------------------

    # 覆盖原本的 11D2,触发 DLL 的内置注册机逻辑

    struct.pack_into("<H", buffer, 0x0A, 0x0572)

    # ------------------ 3. 注入核心加密盐值 ------------------

    # 对应汇编: MOV DWORD PTR [ESP+30], 50AEF62F (存放在偏移 0x0C)

    struct.pack_into("<I", buffer, 0x0C, 0x50AEF62F)

    # 对应汇编: MOV WORD PTR [ESP+34], 15F8 (存放在偏移 0x10)

    struct.pack_into("<H", buffer, 0x10, 0x15F8)

    # 对应汇编: MOV WORD PTR [ESP+36], 2063 (存放在偏移 0x12)

    struct.pack_into("<H", buffer, 0x12, 0x2063)

    print(f"\n[*] 已注入自定义种子: {seed:04X}")

    print(f"[*] 已注入全套加密盐值矩阵 (50AEF62F, 15F8, 2063)...")

    print("[*] 正在呼叫 DLL 核心引擎...\n")

    # 执行调用,传入参数 1 防止被前面的分支跳过

    generate_key(buffer, 1)

    # 从偏移 0x02 开始,连续读取计算出的 4 组真码

    parts = struct.unpack_from("<HHHH", buffer, 0x02)

    print("========== 🎉 完美破解成功! 🎉 ==========")

    print(f"你的专属合法注册码:")

    print(f"{seed:04X}-{parts[0]:04X}-{parts[1]:04X}-{parts[2]:04X}-{parts[3]:04X}")

    print("==========================================")

except Exception as e:

    print(f"\n[!] 发生错误: {e}")

Pasted_image_20260317194621.webp

#后记

至此,本程序的逆向过程暂时告一段落,由于时间问题,没能将注册码生成逻辑重写回高级语言有些遗憾,但是这次逆向过程还是学到了很多知识和技巧,在动态调试中寻找突破口打断点是一个很重要的环节,一旦找到这个突破口,成功定位到关键代码,一切问题就迎刃而解了。希望这篇文章能够对您有所帮助,感谢您的阅读。本篇文章仅作为学习交流使用!!!!
评论区请友善交流,无关话题将会被删除