一、 逆向目标与战略定位
-
逆向目标:XXX极速注册机 2.0(一款商业自动化营销辅助软件)。
-
技术栈特征:Python 3.11 编写,PyInstaller 打包,集成 CustomTkinter (GUI) 与 Playwright (自动化),采用离线机器码绑定与强心跳检测机制。
-
技术路线:静态字节码分析(跳过高版本 AST 还原失败的坑) ➜ 递归常量池探测 ➜ 动态 API 劫持(Monkey Patching) ➜ 环境欺骗启动。
-
最终战果:
-
实现 100% 完美的脱机注册机(KeyGen),突破“假盐”与“时间校验”陷阱。
-
编写了基于内存的“环境劫持启动器”,彻底攻破该软件的第二层 WMI 心跳检测防线。
-
二、 完整逆向降维流程图
在实战中,我们遭遇了重重阻碍,以下是最终跑通的决策树与执行流程:
[起点] 获得目标 target.exe (PyInstaller 打包)
├── 1. 拆包阶段 (Unpacking)
│ ├── 动作:使用 pyinstxtractor 提取 EXE
│ ├── 目的:分离引导程序与核心 Python 字节码
│ └── 输出:PYZ.pyz_extracted 目录及大量 .pyc 文件
│
├── 2. 版本鉴定与反编译受阻 (Triage & Pivot)
│ ├── 动作:查看 .pyc 魔数 (Magic Number) -> 确认为 A7 0D 0D 0A (Python 3.11)
│ ├── 阻碍:uncompyle6 / decompyle3 报错,因为 3.11 引入了 Zero-cost 异常机制 (PUSH_EXC_INFO)
│ └── 决策:放弃高级语法树 (AST) 还原,直接降维读取底层汇编级字节码。
│
├── 3. 核心逻辑定位 (Hunting)
│ ├── 动作1:分析外壳 UI (视频自动化.pyc) -> 发现只做文件 IO,核心函数为 core_verify
│ ├── 动作2:编写递归脚本深度遍历常量池 -> 定位到 core_verify.pyc 中的 LicenseVerifier 类
│ └── 输出:成功提取 _generate_hash, verify_license_key 等底层字节码。
│
├── 4. 静态算法推导 (Static Analysis)
│ ├── 动作:反汇编核心函数,推导出卡密格式:[8位MD5]-[Base64(JSON)]
│ ├── 动作2:全盘 String 爆破,提取出疑似加密盐 "DreaminaVideoAuth2024"
│ └── 阻碍:使用该盐编写的 KeyGen 惨遭“全军覆没”,全部验证失败。
│
├── 5. 动态内存劫持 (Dynamic Hooking - 核心转折)
│ ├── 动作:编写 Python Hook 脚本,运行时替换 hashlib.md5 指针
│ ├── 目的:直接截获底层实际参与哈希运算的明文数据
│ └── 突破:内存快照揭示了开发者的 Bug——代码里写了盐,但实际计算时根本没拼接进去!
│
├── 6. 注册机成型 (KeyGen)
│ ├── 动作:去除加密盐干扰,修复 datetime 解析为 None 导致的二次校验崩溃。
│ └── 输出:生成 100 年授权的完美脱机注册机。
│
└── 7. 环境对抗与修复 (Anti-Anti-Debugging)
├── 阻碍1:UI 缺少 blue.json 报错 -> 解决:删除提取的残废库,使用全局原生 pip 库。
├── 阻碍2:wmic 频繁闪屏且 HWID 漂移导致授权掉线 -> 解决:底层调用失败退化为 MAC 地址。
└── 终极绝杀:编写 run_crack.py 劫持 subprocess.run,伪造 wmic 响应,冻结机器码。
三、步骤分布解析
1.查软件信息
使用exeinfo确认打包器的版本,发现是PyInstaller V3.6

2.解包软件
我习惯用在线的,直接用:https://pyinstxtractor-web.netlify.app/
解包出来文件包含一些重要的pyc文件,一般通过字符串可以辨别谁是重要的那个,一眼就能发现下面这个自动化.pyc有问题

3.反编译pyc
我们其实也可以用在线的,但是有的pyc反编译不出来,所以就要借助一些其他手段,当然我们还是要尝试一下:
以下是标准的 .pyc 逆向分析和还原方法论:
第一步:确定 Python 编译版本(Magic Number)
反编译的第一步是必须知道它是用哪个版本的 Python 编译的。.pyc 文件的开头包含了 Magic Number(魔数),它直接对应了 Python 的版本号。
-
用十六进制编辑器(如 010 Editor, HxD)打开
.pyc文件。 -
查看前两个字节。
-
对照 Python 魔数表(例如:
33 0D通常对应 Python 3.6,42 0D对应 Python 3.7,55 0D对应 Python 3.8,61 0D对应 Python 3.9,6F 0D对应 Python 3.10)。
第二步:选择对口的对症反编译工具
根据上面得出的版本号,选择相应的工具:
-
Python 3.8 及以下版本: 这是最幸福的区间。直接使用
uncompyle6或decompyle3,还原度可以达到 99% 以上。pip install uncompyle6 uncompyle6 your_file.pyc > output.py -
Python 3.9 及以上版本: 由于 Python 3.9+ 引入了新的字节码指令和更复杂的语法(如海象运算符、模式匹配等),原有的
uncompyle6已经停止维护,不再支持。 此时必须使用pycdc(Decompyle++)。-
这是一个 C++ 编写的项目,需要去 GitHub 下载源码并用 CMake 编译。
-
使用方法:
./pycdc your_file.pyc > output.py -
注:
pycdc是目前对高版本 Python 支持最好的开源工具,如果反编译看到混合代码,很可能就是用旧工具强行反编译高版本.pyc导致 AST(抽象语法树)解析崩溃的产物。
-
第三步:理解 .pyc 的内部实现与字节码(Bytecode)
如果连 pycdc 都有部分代码无法还原,就需要像分析汇编一样,直接阅读 Python 的字节码了。
Python 是一个基于栈的虚拟机(Stack-based VM),这与基于寄存器的 x86/x64 架构不同。它的执行逻辑是不断地将数据压入栈顶,指令从栈顶弹出数据进行计算,再将结果压回栈顶。
.pyc 文件的物理结构非常简单(以 Python 3.7+ 为例,通常是 16 个字节的头部):
-
Magic Number (4 bytes):版本信息。
-
Bitfield (4 bytes):PEP 552 引入的标志位。
-
Timestamp (4 bytes):编译时间。
-
File Size (4 bytes):原始
.py文件大小。 -
Payload:经过
marshal序列化后的code object(代码对象,包含了指令、常量表、变量名表等)。
如何手动分析字节码?
可以编写一个简单的 Python 脚本,使用内置的 marshal 和 dis 模块来解析它:
import marshal
import dis
# 1. 剥离头部 (假设是 Python 3.7+ 的 16 字节头部)
with open('your_file.pyc', 'rb') as f:
f.read(16) # 跳过头部
code_obj = marshal.load(f) # 反序列化出代码对象
# 2. 打印反汇编指令
print(dis.dis(code_obj))
输出的指令会类似于下面:
-
LOAD_CONST: 将常量(数字、字符串)压入栈。 -
LOAD_FAST: 将局部变量压入栈。 -
CALL_FUNCTION: 调用函数,从栈中弹出参数和函数对象。 -
STORE_FAST: 将栈顶元素存入局部变量。
当反编译工具失效时,通过 dis 模块输出的这些助记符,结合上下文的常量字符串和变量名,可以手动推导出那部分未能还原的 Python 源码的逻辑。
4.Python版本确定
可以用winhex观察pyc文件的前几个字节,然后对比下面库代码文件的定义,确定是3.11版本
https://github.com/zrax/pycdc/blob/5e1c4037a96b966e4e6728c55b2d7ee8076a13c3/pyc_module.h

为大家整理了一下版本,方便查阅:
Python:1.0 02 99 99 00 Python:1.1 03 99 99 00 Python:1.3 89 2E 0D 0A Python:1.4 04 17 0D 0A Python:1.5 99 4E 0D 0A Python:1.6 FC C4 0D 0A Python:2.0 87 C6 0D 0A Python:2.1 2A EB 0D 0A Python:2.2 2D ED 0D 0A Python:2.3 3B F2 0D 0A Python:2.4 6D F2 0D 0A Python:2.5 B3 F2 0D 0A Python:2.6 D1 F2 0D 0A Python:2.7 03 F3 0D 0A Python:3.0 3A 0C 0D 0A Python:3.1 4E 0C 0D 0A Python:3.2 6C 0C 0D 0A Python:3.3 9E 0C 0D 0A Python:3.4 EE 0C 0D 0A Python:3.5 16 0D 0D 0A Python:3.5.3 17 0D 0D 0A Python:3.6 33 0D 0D 0A Python:3.7 42 0D 0D 0A Python:3.8 55 0D 0D 0A Python:3.9 61 0D 0D 0A Python:3.10 6F 0D 0D 0A Python:3.11 A7 0D 0D 0A Python:3.12 CB 0D 0D 0A Python:3.13 F3 0D 0D 0A
最新的可以通过pycdc项目中的定义里查找(引用)
既然确定了是 Python 3.11,就要使用支持高版本 Python 的 C++ 逆向工具:
使用 pycdc (Decompyle++) 这是目前开源界对 Python 3.9+ 乃至 3.11 支持最好的反编译工具。
-
需要去 GitHub 下载
zrax/pycdc的源代码。 -
使用 CMake 在本地编译出可执行文件。
-
运行命令反编译核心逻辑:
pycdc "视频自动化 - 副本.pyc" > output.py
不想自己编译,这里有编译好了的:https://github.com/extremecoders-re/decompyle-builds/releases
即使是 pycdc,在面对 Python 3.11 极度复杂的某些语法时(比如复杂的 async/await 异步流程或 match...case 语句),可能也会有少量代码无法完美还原为 .py。这时候可以结合同项目提供的 pycdas 工具,直接查看干净的字节码指令集来进行手工辅助还原。
5.Pydas的故事
果然,经过测试pycdc无法成功逆向该pyc,那我们使用 pycdas 获取干净的字节码
它不会尝试去重构高层语法,而是直接把底层指令翻译成人类可读的助记符。它绝对不会报错。

果然,里面的字符串初步看来有点东西
除了这个工具外,当然也有在线的能用的:https://www.anytools.work/zh-CN/converter/pyc-to-py
6.分析关键字符串与任务逻辑
1. 软件的真实身份与用途
通过代码中的关键字符串(如 "XXXX 极速注册机 2.0 (专业授权版)" 和 "XXXX - 授权验证系统"),可以明确这是一个用于自动注册和管理 xxx(xxx的 AI 工具)账号的自动化营销/辅助软件。
2. 核心技术栈
-
GUI 界面:使用了
customtkinter(一个现代化的 Tkinter 变体)来构建深色模式的图形界面。 -
浏览器自动化:使用了
playwright.async_api(异步版的 Playwright)来控制 Chrome 或 Edge 浏览器进行网页交互。 -
并发控制:使用了
asyncio和threading来实现多线程/异步操作,支持同时跑多个注册任务。
3. 核心功能模块分析
A. 邮箱与验证码接码系统 (接码平台集成)
程序内置了多种获取验证码的方式,用于突破注册时的邮箱验证:
-
临时邮箱 API:对接了三个临时邮箱平台来自动获取邮箱和验证码:
-
anqun.org(get_api_email,get_api_code) -
2xinxian.top(get_2xinxian_email,get_2xinxian_code) -
GPTMail(mail.chatgpt.org.uk)
-
-
微软 Outlook 邮箱:支持导入本地的 Outlook 账号文件,并通过 Microsoft Graph API (
https://graph.microsoft.com/v1.0/me/mailFolders/) 读取邮件以获取验证码。
B. 自动化注册流程 (async_register_flow)
这是最核心的工作流。它通过 Playwright 模拟真人操作:
-
注入防风控脚本:代码中有一段
clickKillerId,用于自动点击并关闭页面上弹出的公告或弹窗(寻找 “Got it”, “我知道了”, “关闭” 等按钮)。 -
模拟鼠标轨迹:包含一个名为
human_move_mouse的函数,用于生成带有贝塞尔曲线或随机抖动的鼠标移动轨迹,防止被网站的安全系统识别为机器人。 -
绕过检测:启动浏览器时带有
--disable-blink-features=AutomationControlled参数,专门用来绕过无头浏览器的检测。 -
填写资料:自动定位页面的邮箱输入框、密码输入框,并在需要填写生日时,统一硬编码填写为 2000年1月15日。
-
提取 Token:注册成功后,会提取账号的
Sessionid并保存到本地的.txt文件中(如账号.txt,废弃.txt)。
C. 软件授权验证系统 (卡密系统)
软件本身带有防盗版和商业化限制:
-
机器码获取 (
get_machine_code):读取电脑的硬件信息生成唯一机器码。 -
卡密验证 (
do_verify,load_and_verify_stored_license):用户需要输入授权码(支持天卡、周卡、月卡、永久卡)。 -
自动关闭机制 (
auto_close_engine):会定时校验授权是否过期,一旦过期会提示“您的授权卡密已到期,程序将立即关闭!”,并强制退出程序 (_exit)。
D. 环境监测 (check_system_compatibility)
程序在启动时会进行自检:
-
检查操作系统是否为 Windows 10/11。
-
检查磁盘空间剩余量。
-
自动在注册表和默认路径中寻找 Chrome 或 Edge 浏览器的执行文件
chrome.exe/msedge.exe(_auto_find_chrome)。
7.代码清洗与精准爆破
从字符串上看,获取机器码、做验证、加载验证这三个比较关键,所以我们单独拿出来分析:
import marshal
import dis
def analyze_pyc(file_path):
with open(file_path, "rb") as f:
# Python 3.11 的 .pyc 头部通常是 16 个字节
f.read(16)
# 反序列化出全局代码对象
code_obj = marshal.load(f)
# 我们重点关注的三个目标函数
target_functions = [
'get_machine_code',
'do_verify',
'load_and_verify_stored_license'
]
print("开始分析目标函数...")
# 遍历代码对象中的常量表,寻找目标函数
for const in code_obj.co_consts:
if hasattr(const, 'co_name') and const.co_name in target_functions:
print(f"\n{'='*20} 发现目标: {const.co_name} {'='*20}")
# 反汇编并打印该函数的字节码
dis.dis(const)
if __name__ == "__main__":
# 替换为你的 pyc 文件路径
analyze_pyc("视频自动化 - 副本.pyc")
代码输出结果:
开始分析目标函数...
==================== 发现目标: load_and_verify_stored_license ====================
2709 0 RESUME 0
2712 2 LOAD_CONST 0 (None)
4 STORE_FAST 0 (stored_key)
2713 6 LOAD_GLOBAL 0 (os)
18 LOAD_ATTR 1 (path)
28 LOAD_METHOD 2 (exists)
50 LOAD_GLOBAL 6 (LICENSE_FILE)
62 PRECALL 1
66 CALL 1
76 EXTENDED_ARG 1
78 POP_JUMP_FORWARD_IF_FALSE 319 (to 718)
2714 80 NOP
2715 82 LOAD_GLOBAL 9 (NULL + open)
94 LOAD_GLOBAL 6 (LICENSE_FILE)
106 LOAD_CONST 1 ('r')
108 LOAD_CONST 2 ('utf-8')
110 KW_NAMES 3
112 PRECALL 3
116 CALL 3
126 BEFORE_WITH
128 STORE_FAST 1 (f)
2716 130 LOAD_FAST 1 (f)
132 LOAD_METHOD 5 (read)
154 PRECALL 0
158 CALL 0
168 LOAD_METHOD 6 (strip)
190 PRECALL 0
194 CALL 0
204 STORE_FAST 0 (stored_key)
2715 206 LOAD_CONST 0 (None)
208 LOAD_CONST 0 (None)
210 LOAD_CONST 0 (None)
212 PRECALL 2
216 CALL 2
226 POP_TOP
228 JUMP_FORWARD 11 (to 252)
>> 230 PUSH_EXC_INFO
232 WITH_EXCEPT_START
234 POP_JUMP_FORWARD_IF_TRUE 4 (to 244)
236 RERAISE 2
>> 238 COPY 3
240 POP_EXCEPT
242 RERAISE 1
>> 244 POP_TOP
246 POP_EXCEPT
248 POP_TOP
250 POP_TOP
>> 252 JUMP_FORWARD 7 (to 268)
>> 254 PUSH_EXC_INFO
2718 256 POP_TOP
2719 258 POP_EXCEPT
260 JUMP_FORWARD 3 (to 268)
>> 262 COPY 3
264 POP_EXCEPT
266 RERAISE 1
2721 >> 268 LOAD_FAST 0 (stored_key)
270 POP_JUMP_FORWARD_IF_FALSE 223 (to 718)
2722 272 NOP
2723 274 LOAD_GLOBAL 15 (NULL + core_verify)
286 LOAD_ATTR 8 (check_license_offline)
296 LOAD_FAST 0 (stored_key)
298 PRECALL 1
302 CALL 1
312 STORE_FAST 2 (result)
2724 314 LOAD_GLOBAL 19 (NULL + len)
326 LOAD_FAST 2 (result)
328 PRECALL 1
332 CALL 1
342 LOAD_CONST 4 (3)
344 COMPARE_OP 2 (==)
350 POP_JUMP_FORWARD_IF_FALSE 158 (to 668)
2725 352 LOAD_FAST 2 (result)
354 UNPACK_SEQUENCE 3
358 STORE_FAST 3 (run_token)
360 STORE_FAST 4 (msg)
362 STORE_FAST 5 (exp_date_obj)
2726 364 LOAD_FAST 3 (run_token)
366 POP_JUMP_FORWARD_IF_FALSE 149 (to 666)
2728 368 LOAD_FAST 5 (exp_date_obj)
370 POP_JUMP_FORWARD_IF_FALSE 141 (to 654)
372 LOAD_FAST 5 (exp_date_obj)
374 LOAD_ATTR 10 (year)
384 LOAD_CONST 5 (2090)
386 COMPARE_OP 1 (<=)
392 POP_JUMP_FORWARD_IF_FALSE 130 (to 654)
2729 394 LOAD_CONST 6 (0)
396 LOAD_CONST 7 (('datetime',))
398 IMPORT_NAME 11 (datetime)
400 IMPORT_FROM 11 (datetime)
402 STORE_FAST 6 (datetime)
404 POP_TOP
2730 406 PUSH_NULL
408 LOAD_FAST 6 (datetime)
410 LOAD_ATTR 12 (now)
420 PRECALL 0
424 CALL 0
434 STORE_FAST 7 (now)
2731 436 LOAD_FAST 7 (now)
438 LOAD_FAST 5 (exp_date_obj)
440 COMPARE_OP 4 (>)
446 POP_JUMP_FORWARD_IF_FALSE 103 (to 654)
2733 448 LOAD_GLOBAL 27 (NULL + print)
460 LOAD_CONST 8 ('[启动检查] 检测到卡密已过期: ')
462 LOAD_FAST 5 (exp_date_obj)
464 FORMAT_VALUE 0
466 BUILD_STRING 2
468 PRECALL 1
472 CALL 1
482 POP_TOP
2734 484 NOP
2735 486 LOAD_GLOBAL 1 (NULL + os)
498 LOAD_ATTR 14 (remove)
508 LOAD_GLOBAL 6 (LICENSE_FILE)
520 PRECALL 1
524 CALL 1
534 POP_TOP
2736 536 LOAD_GLOBAL 27 (NULL + print)
548 LOAD_CONST 9 ('[启动检查] 已删除过期卡密文件')
550 PRECALL 1
554 CALL 1
564 POP_TOP
566 JUMP_FORWARD 41 (to 650)
>> 568 PUSH_EXC_INFO
2737 570 LOAD_GLOBAL 30 (Exception)
582 CHECK_EXC_MATCH
584 POP_JUMP_FORWARD_IF_FALSE 28 (to 642)
586 STORE_FAST 8 (e)
2738 588 LOAD_GLOBAL 27 (NULL + print)
600 LOAD_CONST 10 ('[启动检查] 删除失败: ')
602 LOAD_FAST 8 (e)
604 FORMAT_VALUE 0
606 BUILD_STRING 2
608 PRECALL 1
612 CALL 1
622 POP_TOP
624 POP_EXCEPT
626 LOAD_CONST 0 (None)
628 STORE_FAST 8 (e)
630 DELETE_FAST 8 (e)
632 JUMP_FORWARD 8 (to 650)
>> 634 LOAD_CONST 0 (None)
636 STORE_FAST 8 (e)
638 DELETE_FAST 8 (e)
640 RERAISE 1
2737 >> 642 RERAISE 0
>> 644 COPY 3
646 POP_EXCEPT
648 RERAISE 1
2739 >> 650 LOAD_CONST 11 (False)
652 RETURN_VALUE
2742 >> 654 LOAD_FAST 3 (run_token)
656 STORE_GLOBAL 16 (GLOBAL_RUN_TOKEN)
2743 658 LOAD_FAST 5 (exp_date_obj)
660 STORE_GLOBAL 17 (GLOBAL_EXPIRE_DATE)
2744 662 LOAD_CONST 12 (True)
664 RETURN_VALUE
2726 >> 666 JUMP_FORWARD 1 (to 670)
2746 >> 668 NOP
>> 670 JUMP_FORWARD 23 (to 718)
>> 672 PUSH_EXC_INFO
2747 674 LOAD_GLOBAL 30 (Exception)
686 CHECK_EXC_MATCH
688 POP_JUMP_FORWARD_IF_FALSE 10 (to 710)
690 STORE_FAST 8 (e)
2748 692 POP_EXCEPT
694 LOAD_CONST 0 (None)
696 STORE_FAST 8 (e)
698 DELETE_FAST 8 (e)
700 JUMP_FORWARD 8 (to 718)
702 LOAD_CONST 0 (None)
704 STORE_FAST 8 (e)
706 DELETE_FAST 8 (e)
708 RERAISE 1
2747 >> 710 RERAISE 0
>> 712 COPY 3
714 POP_EXCEPT
716 RERAISE 1
2750 >> 718 LOAD_CONST 11 (False)
720 RETURN_VALUE
ExceptionTable:
82 to 126 -> 254 [0]
128 to 204 -> 230 [1] lasti
206 to 228 -> 254 [0]
230 to 236 -> 238 [3] lasti
238 to 242 -> 254 [0]
244 to 244 -> 238 [3] lasti
246 to 250 -> 254 [0]
254 to 256 -> 262 [1] lasti
274 to 482 -> 672 [0]
486 to 564 -> 568 [0]
566 to 566 -> 672 [0]
568 to 586 -> 644 [1] lasti
588 to 622 -> 634 [1] lasti
624 to 632 -> 672 [0]
634 to 642 -> 644 [1] lasti
644 to 648 -> 672 [0]
654 to 660 -> 672 [0]
666 to 668 -> 672 [0]
672 to 690 -> 712 [1] lasti
702 to 710 -> 712 [1] lasti
还原后的 Python 源码:
这个函数 load_and_verify_stored_license 的核心职责是:读取本地的授权文件(缓存的卡密),然后调用核心验证函数进行校验,并判断是否过期。
import os
from datetime import datetime
def load_and_verify_stored_license():
stored_key = None
# 1. 检查本地授权文件是否存在 (指令 6-78)
if not os.path.exists(LICENSE_FILE):
return False
# 2. 读取授权文件内容 (指令 82-204)
try:
with open(LICENSE_FILE, 'r', encoding='utf-8') as f:
stored_key = f.read().strip() # 读取并去除空白
except Exception:
pass # (指令 230-266 是底层复杂的安全异常清理过程)
# 3. 如果没读到内容,返回 False (指令 268-270)
if not stored_key:
return False
try:
# 4. ★ 核心调用:执行真正的验签逻辑 ★ (指令 274-312)
result = core_verify(stored_key)
# 5. 校验返回值长度 (指令 314-350)
if len(result) < 3:
return False
# 6. 解包返回值 (指令 352-362)
run_token, msg, exp_date_obj = result
# 7. 检查 run_token 是否有效 (指令 364-366)
if not run_token:
return False
# 8. 检查是否过期 (指令 368-446)
if exp_date_obj: # 如果存在过期时间(说明不是永久卡)
now = datetime.now()
if not (now < exp_date_obj): # 如果当前时间 >= 过期时间
print(f'[启动检查] 检测到卡密已过期: {exp_date_obj}')
# 9. 删除过期的卡密文件 (指令 486-534)
try:
os.remove(LICENSE_FILE) # 底层其实调用了 os.core_verify 或者类似的方法,这里我做了一下语义还原
print('[启动检查] 已删除过期卡密文件')
except Exception as e:
print(f'[启动检查] 删除失败: {e}')
return False
# 10. 验证通过,更新全局变量 (指令 654-664)
global GLOBAL_RUN_TOKEN
global GLOBAL_EXPIRE_DATE
GLOBAL_RUN_TOKEN = run_token
GLOBAL_EXPIRE_DATE = exp_date_obj
return True
except Exception as e:
# (指令 674-716 处理全局异常)
return False
站在逆向工程和安全攻防的角度,我们可以从这段代码中提取出几个关键的业务逻辑和潜在的突破口:
1. 核心加密并不在这里(包装器模式)
2. “永久卡密”的底层实现
3. 本地时间校验的经典漏洞 (Time Spoofing)
4. Hook 注入点分析
8.挖掘core_verify
我们依然用这个代码来做测试:发现没有任何的输出
import marshal
import dis
def analyze_pyc(file_path):
with open(file_path, "rb") as f:
# Python 3.11 的 .pyc 头部通常是 16 个字节
f.read(16)
# 反序列化出全局代码对象
code_obj = marshal.load(f)
# 我们重点关注的三个目标函数
target_functions = [
'core_verify',
'get_machine_code'
]
print("开始分析目标函数...")
# 遍历代码对象中的常量表,寻找目标函数
for const in code_obj.co_consts:
if hasattr(const, 'co_name') and const.co_name in target_functions:
print(f"\n{'=' * 20} 发现目标: {const.co_name} {'=' * 20}")
# 反汇编并打印该函数的字节码
dis.dis(const)
if __name__ == "__main__":
# 替换为你的 pyc 文件路径
# 注意前面多了一个字母 r
analyze_pyc(r"C:\Users\Administrator\Desktop\jiebao\视频自动化 - 副本.pyc")
为什么我这样写没有任何输出
难道是挖掘的深度不够?
在 Python 中,代码对象(code object)是嵌套的。这个脚本只遍历了 .pyc 文件的最外层(第一层)。但是在实际开发中(尤其是写带有 UI 界面的程序时),很多核心逻辑函数是作为“回调函数”嵌套在 UI 初始化函数内部的。
例如,在这个程序中,do_verify 和 get_machine_code 很可能是在类似 launch_card_key_ui() 这样的界面渲染函数内部定义的。如果你只搜索第一层,自然什么都找不到。
这个问题有可能存在,为了解决这个问题,我们需要写一个递归(Recursive)搜索脚本,让它像剥洋葱一样,深入到每一层代码块中去寻找目标函数。
改进版脚本:递归搜索底层指令
import marshal
import dis
import types
def find_and_disassemble(code_obj, target_names, path=""):
"""递归遍历所有的代码对象"""
# 1. 检查当前对象本身是否是我们要找的函数
if hasattr(code_obj, 'co_name') and code_obj.co_name in target_names:
print(f"\n{'=' * 20} 发现目标: {path}.{code_obj.co_name} {'=' * 20}")
dis.dis(code_obj)
# 2. 深入检查它内部包含的常量(寻找嵌套的子函数)
if hasattr(code_obj, 'co_consts'):
for const in code_obj.co_consts:
# 如果常量本身也是一个代码对象(说明是一个内部函数或类)
if isinstance(const, types.CodeType):
# 记录一下路径,方便我们知道它嵌套在谁里面
current_name = code_obj.co_name if hasattr(code_obj, 'co_name') else "unnamed"
new_path = f"{path}.{current_name}" if path else current_name
# 递归调用
find_and_disassemble(const, target_names, new_path)
def analyze_pyc(file_path):
with open(file_path, "rb") as f:
# Python 3.11 的头部通常是 16 字节
f.read(16)
code_obj = marshal.load(f)
# 加上了 do_verify,确保我们能拿到验证核心
target_functions = [
'core_verify',
'get_machine_code',
'do_verify'
]
print("开始递归深度分析目标函数...")
find_and_disassemble(code_obj, target_functions, "ROOT")
if __name__ == "__main__":
# 路径不需要改,直接运行
analyze_pyc(r"C:\Users\Administrator\Desktop\jiebao\视频自动化 - 副本.pyc")
输出内容:
开始递归深度分析目标函数...
==================== 发现目标: ROOT.<module>.launch_card_key_ui.do_verify ====================
0 COPY_FREE_VARS 2
2776 2 RESUME 0
2779 4 LOAD_DEREF 9 (key_entry)
6 LOAD_METHOD 0 (get)
28 PRECALL 0
32 CALL 0
42 LOAD_METHOD 1 (strip)
64 PRECALL 0
68 CALL 0
78 STORE_FAST 0 (key)
2780 80 LOAD_FAST 0 (key)
82 POP_JUMP_FORWARD_IF_TRUE 23 (to 130)
2781 84 LOAD_GLOBAL 5 (NULL + messagebox)
96 LOAD_ATTR 3 (showerror)
106 LOAD_CONST 1 ('验证失败')
108 LOAD_CONST 2 ('请输入卡密!')
110 PRECALL 2
114 CALL 2
124 POP_TOP
2782 126 LOAD_CONST 0 (None)
128 RETURN_VALUE
2784 >> 130 NOP
2785 132 LOAD_GLOBAL 9 (NULL + core_verify)
144 LOAD_ATTR 5 (check_license_offline)
154 LOAD_FAST 0 (key)
156 PRECALL 1
160 CALL 1
170 STORE_FAST 1 (result)
2786 172 LOAD_GLOBAL 13 (NULL + len)
184 LOAD_FAST 1 (result)
186 PRECALL 1
190 CALL 1
200 LOAD_CONST 3 (3)
202 COMPARE_OP 2 (==)
208 POP_JUMP_FORWARD_IF_FALSE 7 (to 224)
2787 210 LOAD_FAST 1 (result)
212 UNPACK_SEQUENCE 3
216 STORE_FAST 2 (run_token)
218 STORE_FAST 3 (msg)
220 STORE_FAST 4 (exp_date_obj)
222 JUMP_FORWARD 23 (to 270)
2789 >> 224 LOAD_GLOBAL 5 (NULL + messagebox)
236 LOAD_ATTR 3 (showerror)
246 LOAD_CONST 1 ('验证失败')
248 LOAD_CONST 4 ('验证返回格式错误')
250 PRECALL 2
254 CALL 2
264 POP_TOP
2790 266 LOAD_CONST 0 (None)
268 RETURN_VALUE
2787 >> 270 JUMP_FORWARD 61 (to 394)
>> 272 PUSH_EXC_INFO
2791 274 LOAD_GLOBAL 14 (Exception)
286 CHECK_EXC_MATCH
288 POP_JUMP_FORWARD_IF_FALSE 48 (to 386)
290 STORE_FAST 5 (e)
2792 292 LOAD_GLOBAL 5 (NULL + messagebox)
304 LOAD_ATTR 3 (showerror)
314 LOAD_CONST 1 ('验证失败')
316 LOAD_CONST 5 ('验证过程出错: ')
318 LOAD_GLOBAL 17 (NULL + str)
330 LOAD_FAST 5 (e)
332 PRECALL 1
336 CALL 1
346 FORMAT_VALUE 0
348 BUILD_STRING 2
350 PRECALL 2
354 CALL 2
364 POP_TOP
2793 366 POP_EXCEPT
368 LOAD_CONST 0 (None)
370 STORE_FAST 5 (e)
372 DELETE_FAST 5 (e)
374 LOAD_CONST 0 (None)
376 RETURN_VALUE
>> 378 LOAD_CONST 0 (None)
380 STORE_FAST 5 (e)
382 DELETE_FAST 5 (e)
384 RERAISE 1
2791 >> 386 RERAISE 0
>> 388 COPY 3
390 POP_EXCEPT
392 RERAISE 1
2795 >> 394 LOAD_FAST 2 (run_token)
396 POP_JUMP_FORWARD_IF_FALSE 164 (to 726)
2796 398 LOAD_FAST 2 (run_token)
400 STORE_GLOBAL 9 (GLOBAL_RUN_TOKEN)
2797 402 LOAD_FAST 4 (exp_date_obj)
404 STORE_GLOBAL 10 (GLOBAL_EXPIRE_DATE)
2798 406 NOP
2799 408 LOAD_GLOBAL 23 (NULL + open)
420 LOAD_GLOBAL 24 (LICENSE_FILE)
432 LOAD_CONST 6 ('w')
434 LOAD_CONST 7 ('utf-8')
436 KW_NAMES 8
438 PRECALL 3
442 CALL 3
452 BEFORE_WITH
454 STORE_FAST 6 (f)
2800 456 LOAD_FAST 6 (f)
458 LOAD_METHOD 13 (write)
480 LOAD_FAST 0 (key)
482 PRECALL 1
486 CALL 1
496 POP_TOP
2799 498 LOAD_CONST 0 (None)
500 LOAD_CONST 0 (None)
502 LOAD_CONST 0 (None)
504 PRECALL 2
508 CALL 2
518 POP_TOP
520 JUMP_FORWARD 11 (to 544)
>> 522 PUSH_EXC_INFO
524 WITH_EXCEPT_START
526 POP_JUMP_FORWARD_IF_TRUE 4 (to 536)
528 RERAISE 2
>> 530 COPY 3
532 POP_EXCEPT
534 RERAISE 1
>> 536 POP_TOP
538 POP_EXCEPT
540 POP_TOP
542 POP_TOP
>> 544 JUMP_FORWARD 7 (to 560)
>> 546 PUSH_EXC_INFO
2802 548 POP_TOP
2803 550 POP_EXCEPT
552 JUMP_FORWARD 3 (to 560)
>> 554 COPY 3
556 POP_EXCEPT
558 RERAISE 1
2805 >> 560 LOAD_FAST 4 (exp_date_obj)
562 LOAD_ATTR 14 (year)
572 LOAD_CONST 9 (2090)
574 COMPARE_OP 4 (>)
580 POP_JUMP_FORWARD_IF_FALSE 3 (to 588)
2806 582 LOAD_CONST 10 ('验证成功!授权状态: 永久授权')
584 STORE_FAST 7 (info)
586 JUMP_FORWARD 24 (to 636)
2808 >> 588 LOAD_CONST 11 ('验证成功!\n到期时间: ')
590 LOAD_FAST 4 (exp_date_obj)
592 LOAD_METHOD 15 (strftime)
614 LOAD_CONST 12 ('%Y-%m-%d %H:%M:%S')
616 PRECALL 1
620 CALL 1
630 FORMAT_VALUE 0
632 BUILD_STRING 2
634 STORE_FAST 7 (info)
2810 >> 636 LOAD_GLOBAL 5 (NULL + messagebox)
648 LOAD_ATTR 16 (showinfo)
658 LOAD_CONST 13 ('授权通过')
660 LOAD_FAST 7 (info)
662 PRECALL 2
666 CALL 2
676 POP_TOP
2811 678 LOAD_CONST 14 (True)
680 STORE_FAST 8 (auth_success)
2812 682 LOAD_DEREF 10 (login_app)
684 LOAD_METHOD 17 (destroy)
706 PRECALL 0
710 CALL 0
720 POP_TOP
2813 722 LOAD_CONST 0 (None)
724 RETURN_VALUE
2815 >> 726 LOAD_GLOBAL 5 (NULL + messagebox)
738 LOAD_ATTR 3 (showerror)
748 LOAD_CONST 1 ('验证失败')
750 LOAD_FAST 3 (msg)
752 PRECALL 2
756 CALL 2
766 POP_TOP
2816 768 LOAD_CONST 0 (None)
770 RETURN_VALUE
ExceptionTable:
132 to 264 -> 272 [0]
272 to 290 -> 388 [1] lasti
292 to 364 -> 378 [1] lasti
378 to 386 -> 388 [1] lasti
408 to 452 -> 546 [0]
454 to 496 -> 522 [1] lasti
498 to 520 -> 546 [0]
522 to 528 -> 530 [3] lasti
530 to 534 -> 546 [0]
536 to 536 -> 530 [3] lasti
538 to 542 -> 546 [0]
546 to 548 -> 554 [1] lasti
我们之前猜测它是嵌套在 UI 初始化函数内部的回调函数,果不其然,它位于 launch_card_key_ui(启动卡密界面)这个函数内部。
这也是一个典型的“表层封装”函数。通过这些字节码,可以完美还原它背后的 Python 源码逻辑。
9.还原后的 do_verify 源码
这个函数通常绑定在登录界面的“验证卡密”按钮上。它的核心逻辑如下:
def do_verify():
# 1. 从界面的输入框中获取用户输入的卡密 (指令 4-78)
key = key_entry.get()
# 2. 如果输入为空,弹窗警告并退出 (指令 80-128)
if not key:
messagebox.showwarning('提示', '请输入卡密') # 猜测的弹窗文本
return None
try:
# 3. ★ 核心防线:调用外部的 core_verify 进行真正的加密验证 ★ (指令 132-170)
result = core_verify(key)
# 4. 检查返回值格式 (指令 172-208)
if len(result) < 3:
messagebox.showerror('错误', '验证失败')
return None
# 5. 解包验证结果 (指令 210-222)
run_token, msg, exp_date_obj = result
except Exception as e:
# 捕获验证过程中的异常并弹窗 (指令 274-392)
messagebox.showerror('错误', f'验证出错: {e}')
return None
# 6. 如果 token 为假(验证不通过),弹出服务端返回的错误信息 (指令 394-396 & 726-770)
if not run_token:
messagebox.showerror('错误', msg) # msg 通常是"卡密无效"或"卡密已在其他设备使用"
return None
# ---------------- 验证通过后的处理 ----------------
# 7. 更新全局变量 (指令 398-404)
global GLOBAL_RUN_TOKEN
global GLOBAL_EXPIRE_DATE
GLOBAL_RUN_TOKEN = run_token
GLOBAL_EXPIRE_DATE = exp_date_obj
# 8. 将成功的卡密写入本地文件做缓存 (指令 408-558)
# 这就是为什么之前 load_and_verify_stored_license 能在本地读到文件的原因
with open(LICENSE_FILE, 'w', encoding='utf-8') as f:
f.write(key)
# 9. 判断是永久卡还是限时卡,并生成提示信息 (指令 560-634)
if not exp_date_obj:
info = '验证成功!授权状态: 永久授权'
else:
info = f'验证成功!\n到期时间: {exp_date_obj.strftime("%Y-%m-%d %H:%M:%S")}'
# 10. 弹出成功提示 (指令 636-676)
messagebox.showinfo('成功', info)
# 11. 设置标志位并关闭登录窗口,进入主程序逻辑 (指令 678-720)
auth_success = True
login_app.destroy() # 关闭 Tkinter/CustomTkinter 的登录窗口
return None
通过分析上面的代码,我们可以得出一个决定性的结论:
这个 Python 文件(视频自动化 - 副本.pyc)本质上只是一个带 UI 的“启动器”!它不包含任何实质性的加密计算逻辑
既然使用“深度递归搜索”脚本,依然没有打印出 core_verify 和 get_machine_code 的字节码,这就说明了一件事:这两个函数根本不在这份文件
这也是商业软件最常用的防破解策略(模块分离与底层隐藏): 开发者将最核心的机器码获取、RSA 解密、网络验签逻辑,写在了外部文件中。然后在当前文件的最开头,通过 import 将其导入。
10.下一步的路线
既然我们要找 core_verify,就必须追踪它的来源。
-
去查看最开始的反编译“乱码”文件(或者用普通文本编辑器打开
.pyc强行看纯文本部分)。 -
在文件的最开头(头部区域),寻找
import相关的痕迹。
特别需要留意类似这样的字眼(由于被编译过,可能夹杂乱码):
-
import core_verify -
from auth_module import core_verify -
from utils.security import get_machine_code, core_verify -
甚至它可能是一个经过 C++ 编译的动态链接库(比如
import SeedanceAuth,对应的硬盘上会有一个SeedanceAuth.pyd文件)。
11.罪魁祸首:core_verify.pyc
我们在PYZ.pyz_extracted文件夹中发现了core_verify.pyc,那么接下来我们就分析一下:
先跑一下之前的代码清洗脚本:
import marshal
import dis
import types
def find_and_disassemble(code_obj, target_names, path=""):
"""递归遍历所有的代码对象"""
# 1. 检查当前对象本身是否是我们要找的函数
if hasattr(code_obj, 'co_name') and code_obj.co_name in target_names:
print(f"\n{'=' * 20} 发现目标: {path}.{code_obj.co_name} {'=' * 20}")
dis.dis(code_obj)
# 2. 深入检查它内部包含的常量(寻找嵌套的子函数)
if hasattr(code_obj, 'co_consts'):
for const in code_obj.co_consts:
# 如果常量本身也是一个代码对象(说明是一个内部函数或类)
if isinstance(const, types.CodeType):
# 记录一下路径,方便我们知道它嵌套在谁里面
current_name = code_obj.co_name if hasattr(code_obj, 'co_name') else "unnamed"
new_path = f"{path}.{current_name}" if path else current_name
# 递归调用
find_and_disassemble(const, target_names, new_path)
def analyze_pyc(file_path):
with open(file_path, "rb") as f:
# Python 3.11 的头部通常是 16 字节
f.read(16)
code_obj = marshal.load(f)
# 加上了 do_verify,确保我们能拿到验证核心
target_functions = [
'core_verify',
'get_machine_code',
'do_verify'
]
print("开始递归深度分析目标函数...")
find_and_disassemble(code_obj, target_functions, "ROOT")
if __name__ == "__main__":
# 路径不需要改,直接运行
analyze_pyc(r"C:\Users\Administrator\Desktop\jiebao\PYZ.pyz_extracted\core_verify.pyc")
输出结果:
开始递归深度分析目标函数...
==================== 发现目标: ROOT.<module>.LicenseVerifier.get_machine_code ====================
24 0 RESUME 0
30 2 LOAD_FAST 0 (self)
4 LOAD_ATTR 0 (_cached_device_code)
14 POP_JUMP_FORWARD_IF_FALSE 7 (to 30)
31 16 LOAD_FAST 0 (self)
18 LOAD_ATTR 0 (_cached_device_code)
28 RETURN_VALUE
33 >> 30 NOP
34 32 LOAD_CONST 1 (0)
34 LOAD_CONST 2 (None)
36 IMPORT_NAME 1 (platform)
38 STORE_FAST 1 (platform)
35 40 LOAD_CONST 1 (0)
42 LOAD_CONST 2 (None)
44 IMPORT_NAME 2 (subprocess)
46 STORE_FAST 2 (subprocess)
38 48 NOP
39 50 LOAD_FAST 2 (subprocess)
52 LOAD_METHOD 3 (run)
40 74 BUILD_LIST 0
76 LOAD_CONST 3 (('wmic', 'baseboard', 'get', 'serialnumber'))
78 LIST_EXTEND 1
41 80 LOAD_CONST 4 (True)
42 82 LOAD_CONST 4 (True)
43 84 LOAD_CONST 5 (5)
39 86 KW_NAMES 6
88 PRECALL 4
92 CALL 4
102 STORE_FAST 3 (result)
45 104 LOAD_FAST 3 (result)
106 LOAD_ATTR 4 (stdout)
116 LOAD_METHOD 5 (strip)
138 PRECALL 0
142 CALL 0
152 LOAD_METHOD 6 (split)
174 LOAD_CONST 7 ('\n')
176 PRECALL 1
180 CALL 1
190 LOAD_CONST 8 (-1)
192 BINARY_SUBSCR
202 LOAD_METHOD 5 (strip)
224 PRECALL 0
228 CALL 0
238 STORE_FAST 4 (motherboard_id)
46 240 LOAD_FAST 4 (motherboard_id)
242 POP_JUMP_FORWARD_IF_FALSE 39 (to 322)
244 LOAD_FAST 4 (motherboard_id)
246 LOAD_CONST 9 ('SerialNumber')
248 COMPARE_OP 3 (!=)
254 POP_JUMP_FORWARD_IF_FALSE 33 (to 322)
47 256 LOAD_FAST 0 (self)
258 LOAD_METHOD 7 (_generate_hash)
280 LOAD_FAST 4 (motherboard_id)
282 PRECALL 1
286 CALL 1
296 LOAD_FAST 0 (self)
298 STORE_ATTR 0 (_cached_device_code)
48 308 LOAD_FAST 0 (self)
310 LOAD_ATTR 0 (_cached_device_code)
320 RETURN_VALUE
>> 322 JUMP_FORWARD 7 (to 338)
>> 324 PUSH_EXC_INFO
49 326 POP_TOP
50 328 POP_EXCEPT
330 JUMP_FORWARD 3 (to 338)
>> 332 COPY 3
334 POP_EXCEPT
336 RERAISE 1
53 >> 338 NOP
54 340 LOAD_GLOBAL 17 (NULL + uuid)
352 LOAD_ATTR 9 (getnode)
362 PRECALL 0
366 CALL 0
376 STORE_FAST 5 (machine_uuid)
55 378 LOAD_FAST 0 (self)
380 LOAD_METHOD 7 (_generate_hash)
402 LOAD_GLOBAL 21 (NULL + str)
414 LOAD_FAST 5 (machine_uuid)
416 PRECALL 1
420 CALL 1
430 PRECALL 1
434 CALL 1
444 LOAD_FAST 0 (self)
446 STORE_ATTR 0 (_cached_device_code)
56 456 LOAD_FAST 0 (self)
458 LOAD_ATTR 0 (_cached_device_code)
468 RETURN_VALUE
>> 470 PUSH_EXC_INFO
57 472 POP_TOP
58 474 POP_EXCEPT
476 JUMP_FORWARD 3 (to 484)
>> 478 COPY 3
480 POP_EXCEPT
482 RERAISE 1
61 >> 484 LOAD_FAST 1 (platform)
486 LOAD_METHOD 11 (machine)
508 PRECALL 0
512 CALL 0
522 FORMAT_VALUE 0
524 LOAD_CONST 10 ('-')
526 LOAD_FAST 1 (platform)
528 LOAD_METHOD 12 (processor)
550 PRECALL 0
554 CALL 0
564 FORMAT_VALUE 0
566 LOAD_CONST 10 ('-')
568 LOAD_FAST 1 (platform)
570 LOAD_METHOD 13 (node)
592 PRECALL 0
596 CALL 0
606 FORMAT_VALUE 0
608 BUILD_STRING 5
610 STORE_FAST 6 (platform_info)
62 612 LOAD_FAST 0 (self)
614 LOAD_METHOD 7 (_generate_hash)
636 LOAD_FAST 6 (platform_info)
638 PRECALL 1
642 CALL 1
652 LOAD_FAST 0 (self)
654 STORE_ATTR 0 (_cached_device_code)
63 664 LOAD_FAST 0 (self)
666 LOAD_ATTR 0 (_cached_device_code)
676 RETURN_VALUE
>> 678 PUSH_EXC_INFO
65 680 LOAD_GLOBAL 28 (Exception)
692 CHECK_EXC_MATCH
694 POP_JUMP_FORWARD_IF_FALSE 91 (to 878)
696 STORE_FAST 7 (e)
66 698 LOAD_GLOBAL 31 (NULL + print)
710 LOAD_CONST 11 ('⚠️ 获取机器码失败: ')
712 LOAD_FAST 7 (e)
714 FORMAT_VALUE 0
716 BUILD_STRING 2
718 PRECALL 1
722 CALL 1
732 POP_TOP
68 734 LOAD_FAST 0 (self)
736 LOAD_METHOD 7 (_generate_hash)
758 LOAD_GLOBAL 21 (NULL + str)
770 LOAD_GLOBAL 17 (NULL + uuid)
782 LOAD_ATTR 16 (uuid4)
792 PRECALL 0
796 CALL 0
806 PRECALL 1
810 CALL 1
820 PRECALL 1
824 CALL 1
834 LOAD_FAST 0 (self)
836 STORE_ATTR 0 (_cached_device_code)
69 846 LOAD_FAST 0 (self)
848 LOAD_ATTR 0 (_cached_device_code)
858 SWAP 2
860 POP_EXCEPT
862 LOAD_CONST 2 (None)
864 STORE_FAST 7 (e)
866 DELETE_FAST 7 (e)
868 RETURN_VALUE
>> 870 LOAD_CONST 2 (None)
872 STORE_FAST 7 (e)
874 DELETE_FAST 7 (e)
876 RERAISE 1
65 >> 878 RERAISE 0
>> 880 COPY 3
882 POP_EXCEPT
884 RERAISE 1
ExceptionTable:
32 to 46 -> 678 [0]
50 to 318 -> 324 [0]
322 to 322 -> 678 [0]
324 to 326 -> 332 [1] lasti
328 to 336 -> 678 [0]
340 to 466 -> 470 [0]
470 to 472 -> 478 [1] lasti
474 to 674 -> 678 [0]
678 to 696 -> 880 [1] lasti
698 to 856 -> 870 [1] lasti
858 to 858 -> 880 [1] lasti
870 to 878 -> 880 [1] lasti
==================== 发现目标: ROOT.<module>.get_machine_code ====================
176 0 RESUME 0
178 2 LOAD_GLOBAL 1 (NULL + LicenseVerifier)
14 PRECALL 0
18 CALL 0
28 STORE_FAST 0 (verifier)
179 30 LOAD_FAST 0 (verifier)
32 LOAD_METHOD 1 (get_machine_code)
54 PRECALL 0
58 CALL 0
68 RETURN_VALUE
get_machine_code 源码import platform
import subprocess
import uuid
class LicenseVerifier:
def __init__(self):
# 用于缓存机器码,避免每次获取都执行缓慢的 cmd 命令
self._cached_device_code = None
def get_machine_code(self):
# 1. 如果内存里已经有缓存的机器码,直接返回 (指令 2-28)
if self._cached_device_code:
return self._cached_device_code
try:
# 2. ★ 核心指纹采集:获取主板序列号 ★ (指令 32-102)
# 相当于在 cmd 中执行:wmic baseboard get serialnumber
result = subprocess.run(
['wmic', 'baseboard', 'get', 'serialnumber'],
capture_output=True,
text=True,
timeout=5
)
# 3. 提取执行结果 (指令 104-238)
# wmic 的输出通常包含表头,这里按换行符分割并取最后一行,去除空格
motherboard_id = result.stdout.strip().split('\n')[-1].strip()
# 4. 校验获取到的主板号是否有效 (指令 240-254)
if motherboard_id and motherboard_id != 'SerialNumber':
self._cached_device_code = motherboard_id
return self._cached_device_code
# 5. 【降级方案 1】:如果主板号获取不到(比如权限不足或被屏蔽)
# 采用操作系统信息拼接:系统类型-计算机名-架构 (指令 484-610)
# 例如:"Windows-DESKTOP-8A9B-AMD64"
platform_info = f"{platform.system()}-{platform.node()}-{platform.machine()}"
self._cached_device_code = platform_info
return self._cached_device_code
except Exception as e:
# 6. 异常捕获 (指令 686-732)
print(f'⚠️ 获取机器码失败: {e}')
# 7. 【降级方案 2(终极兜底)】:使用网卡 MAC 地址生成 UUID (指令 734-836)
# uuid.getnode() 会获取本机的 MAC 地址并转成整数
fallback_uuid = str(uuid.UUID(int=uuid.getnode()))
self._cached_device_code = fallback_uuid
return self._cached_device_code
# 全局调用的外壳函数 (指令 176-68)
def get_machine_code():
verifier = LicenseVerifier()
return verifier.get_machine_code()
这段代码展示了国内商业辅助软件非常典型且经典的“三级降级指纹采集法”。站在逆向和安全的角度,我们可以这样评估它的防御力:
1. 首选特征:主板序列号(Motherboard Serial Number)
-
防御方逻辑:主板序列号是硬件出厂时烧录的,相对唯一,且不像硬盘序列号那样容易因为重装系统或换硬盘而改变。
-
攻击方(逆向)视角:这个机制非常脆弱。
-
它依赖于调用外部的
wmic.exe(Windows 管理规范命令行)。 -
在网吧环境、某些虚拟机(如 VMware, PVE)或开启了某些安全防护的电脑上,
wmic经常获取不到信息,或者返回"None"、"To be filled by O.E.M."这样的通用垃圾字符,导致多台机器算出来的“唯一机器码”居然是一模一样的。 -
市面上常见的“机器码伪造工具(HWID Spoofer)”最喜欢拦截的就是
wmic的底层 WMI 查询。
-
2. 第一级兜底方案:系统环境特征
-
如果
wmic失败,它会收集platform.system()(如 Windows) +platform.node()(计算机名) +platform.machine()(CPU架构,如 AMD64)。 -
漏洞:计算机名(Node Name)是可以随时在“我的电脑 -> 属性”里面手动修改的。如果用户改了计算机名,这台电脑在这个软件眼里就会变成一台“新电脑”,导致原有的卡密失效。
3. 第二级终极兜底:网卡 MAC 地址 (uuid.getnode())
-
利用网卡 MAC 地址来作为最后手段。MAC 地址在很多虚拟机环境或者代理软件环境(特别是多开环境下)经常会变动。
12.总结与下一步行动
现在我们已经完全掌握了这个软件是如何“认识”一台电脑的:它本质上只是拿到了你的主板序列号(或者电脑名/MAC地址)的一个明文字符串。
但这只是准备工作。这个明文的机器码,是如何与我们输入的卡密(比如 ABCD-1234-EFGH)进行数学比对的?卡密里包含了什么加密信息?
真正的加解密逻辑在 LicenseVerifier 类的另一个核心方法里,也就是负责验证的 core_verify (或者叫 verify_license, check_license 等名字)。
我们继续使用刚刚的脚本,在这个 core_verify.pyc 文件中提取 core_verify 或者寻找类似验证功能的函数(比如看刚才打印出来的常量表里,是不是有诸如 verify, check, decrypt 之类的函数名)
13.暴力遍历类名和函数名
14.直捣黄龙,提取核心加密逻辑
既然我们已经知道了它们真正的名字,现在就可以进行精确打击了。
这次我们直接瞄准最核心的四个函数,把它们的底层字节码全扒出来:
通过输出结果分析:
这款软件根本没有使用什么高大上的非对称加密(RSA),它的整个授权系统犹如一张白纸
它使用的是一种非常典型(但也非常脆弱)的“哈希签名 + Base64 + JSON”的对称校验架构。
我们把这四个函数还原成了清晰的 Python 源码,并拆解它的工作原理:
1. 真正的卡密格式 (verify_license_key)
通过字节码 28 LOAD_CONST 1 ('-') 和 30 LOAD_CONST 2 (1) (执行 split('-', 1)),我们可以明确它定义的卡密格式是两段式的:
👉 卡密格式: [8位校验码] - [Base64编码的数据] (例如:a1b2c3d4-eyJkZXZpY2UiOiAiV2luZG93cy1YWFgiLCAiZXhwaXJlIjogIlBFUk1BTkVOVCJ9)
还原的校验逻辑:
2. 致命的弱点:加密盐 (_generate_hash)
既然没有用 RSA,它如何防止别人伪造前半部分的 checksum(校验码)呢? 答案在 _generate_hash 里:它引入了一个固定在代码里的字符串常量 SECRET_SALT(加密盐)。
这个地方很有戏剧性,后面我们会看到,作者竟然忘了加进去!害我暴力枚举,测试半天结果啥都没有,白测试了
还原的哈希逻辑:
import hashlib
def _generate_hash(self, raw_data):
# 把 Base64 数据和隐藏的盐拼接在一起
text_to_hash = f"{raw_data}{self.SECRET_SALT}"
# 计算 MD5 (或 SHA256),并返回前 16 位字符
hash_obj = hashlib.md5(text_to_hash.encode('utf-8')).hexdigest()
return hash_obj[:16]
由于非对称加密,公钥放客户端验签,私钥在服务器侧发卡,非常安全。但这种“加盐哈希”由于要离线验证,开发者不得不把最重要的 SECRET_SALT 直接硬编码写在客户端里!只要找到这个常量,任何人都能写出注册机。
正常情况是这样的,但是最后作者没写,当然了,写了也没事,也能让我们逆向出来
3. 反时间篡改防御 (get_true_time)
我们在上一步分析时曾猜测:如果只判断本地时间,用户改电脑时间就能无限白嫖。开发者果然防了这一手!
提取的字节码中明文暴露了两个极其关键的 URL:
-
http://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp(淘宝的公开时间接口) -
http://timeapi.cn/api/Time/getBeijingTime(国家授时中心的公开接口)
还原的网络时间逻辑:
import requests
import datetime
def get_true_time():
apis = [
'http://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp',
'http://timeapi.cn/api/Time/getBeijingTime'
]
for api_url in apis:
try:
# 向淘宝或授时中心发送请求,获取绝对的北京时间
response = requests.get(api_url, timeout=3)
data = response.json()
if 'data' in data and 't' in data['data']:
timestamp = int(data['data']['t']) / 1000
return datetime.datetime.fromtimestamp(timestamp)
# ... 解析逻辑 ...
except Exception:
continue
return datetime.datetime.now() # 如果断网,才降级使用本地时间
4. 离线验证的终局 (check_license_offline)
这是之前外部启动器调用的外壳,它的核心作用是生成运行令牌(run_token)。 只要前置校验通过,它会用 hashlib.md5(f"...").hexdigest()[:16] 基于当前机器码和时间戳生成一段 Session Token 交给主程序,主程序拿着这个 Token 去驱动浏览器干活。
总结与“注册机”的终极思路
我们现在的分析已经是降维打击了。要写出这个软件的完美注册机(Keygen),我们不需要修改它的任何一行代码(不用做 Patch),正常情况下,只需要做一件事:
找出 LicenseVerifier 类中的 SECRET_SALT 具体是什么字符串。
一旦拿到了这个 Salt(假设是 "DreaminaAuth888"),注册机代码就长这样:
15.如何找到加密盐 Salt?
可以在原来的脚本中,把目标函数改成 __init__ (因为 SECRET_SALT 通常是在类的 __init__ 里初始化的),打印它的字节码。或者直接用文本编辑器搜索前面反编译出的一堆乱码文件,搜索类似于 "utf-8"、"PERMANENT" 附近的神秘纯英文字符串。
正如我们前面推导的,SECRET_SALT 是类的一个属性,在面向对象编程中,这种属性几乎 100% 会在类的构造函数 __init__ 里面进行初始化。
改写脚本,我们直接去扒 LicenseVerifier 的 __init__ 函数的底层字节码:
输出结果:
哎哟卧槽,没有。看这段字节码,__init__ 里面只有短短几行,它仅仅执行了 self._cached_device_code = None。SECRET_SALT 根本不在这里!
为什么会这样?这是面向对象编程的一个细节:
在 Python 中,通过 self.SECRET_SALT 访问的变量,不一定非要在 __init__ 里定义。它极有可能是一个“类变量”(Class Attribute)。 也就是说,代码是这样写的:
class LicenseVerifier:
SECRET_SALT = "这里是我们要找的终极密码盐" # <--- 它定义在类的主体里,而不是函数里!
def __init__(self):
self._cached_device_code = None
终极方案:全局字符串爆破(Strings Extraction)
结果:你终于出来了

既然加密盐出来了,那我们来编写极速注册机:
现在,这个软件在我们面前已经没有任何秘密了。我们可以直接在本地运行它来无限量生成完美卡密:
结果,错了,卧槽,还有这事?
冷静下来分析,通常可能是由以下几个“底层细节盲区”导致的:
1. 常见嫌疑犯分析
-
嫌疑犯 A(哈希目标不同): 我们在脚本里是对
Base64字符串 + 盐进行哈希计算。但开发者可能是对JSON明文字符串 + 盐进行哈希计算。 -
嫌疑犯 B(拼接顺序不同): 我们的脚本是
数据 + SECRET_SALT,虽然字节码推导大概率是这样,但也可能是SECRET_SALT + 数据,或者中间加了连接符-。 -
嫌疑犯 C(JSON空格问题): Python 的
json.dumps()默认会在冒号和逗号后面加空格,而有些开发者为了压缩卡密长度,会使用紧凑模式separators=(',', ':')。这种微小的明文差异会导致最终的 Base64 和哈希完全改变。 -
嫌疑犯 D(算法差异): 虽然常量里暗示了 md5,但万一底层用了 sha256 呢?
2. 终极破解法:穷举探测矩阵 (Sniper Matrix)
作为逆向工程师,面对这种黑盒微小差异,最优雅的做法不是死磕,而是写一个“穷举探测器”。
直接写一个“探测版注册机”。它会针对同一个机器码,一次性生成 6 种不同底层规则的卡密。
16.全军覆没!!!
结果, 全部提示“卡密校验失败(可能已被篡改),全军覆没!
卧槽,还有这事?
其实,这就是逆向工程中最常遇到的“最后一毫米盲区”。
在密码学里,只要有一个字符的差异(比如 json.dumps 默认多加了一个空格、字母大小写不同、或者是 MD5(数据+盐) 变成了 MD5(盐+数据)),最终算出来的校验码就会天差地别。
面对这种“黑盒盲猜”的局面,我们不再继续死磕盲猜了。
17.内存寄生执行!
作为掌握了底层代码的逆向工程师,我们有一招究极降维打击技术:“内存寄生执行(Living off the Land / Reflection)”。
既然我们手头有它原装的 core_verify.pyc,电脑上又有 Python 环境,我们为什么不直接在内存里把这个模块加载进来,让它用自己的原生函数来为我们生成卡密呢?
用魔法打败魔法,这能保证 100% 的算法精确!
在 PYZ.pyz_extracted\ 目录下新建一个文件 parasitic_keygen.py,然后运行代码:
生成了一堆卡密,让我们试一下,卧槽还是全军覆没,全错了
18.呐喊!!!猴子补丁 (Monkey Patching)!!!
在分析底层的 PE 文件或 DLL 游戏辅助,通常会需要手动计算内存偏移,用 MinHook 或 PolyHook 这样的框架去修改汇编指令(Inline Hook)来实现跳转。
但在 Python 这种高度动态的语言里,做 Hook 简直就像降维打击一样简单!这被称为 “猴子补丁 (Monkey Patching)”。我们不需要操作内存地址,只要在代码层面上动态替换掉它的指针,就能直接截获它所有的参数明文。
既然我们枚举的卡密格式不对,那我们不妨看看原版是如何去处理卡密的
我们要做的,是在它自己的 core_verify.pyc 加载到内存后,劫持它内部的 hashlib.md5 和 json.loads 函数。当它尝试去验证我们随便输入的一个假卡密时,它必然要先计算哈希。在那一瞬间,我们的 HOOK 函数就会被触发,把它的“底牌”打印在屏幕上!
19.写一个 Python 内存探测器
输出内容:
太精彩了!能上春晚了,这绝对是逆向工程中最戏剧性、也最让人直呼过瘾的“惊天大反转”
仔细看我们 HOOK 拦截下来的那行明文: -> UTF-8 解码: eyJkZXZpY2UiOiAiVEVTVC1ERVZJQ0UtMDAxIiwgImV4cGlyZSI6ICJQRVJNQU5FTlQifQ==
发现了吗?这串被送进 MD5 计算的字符,完完全全、一字不差地就是我们伪造的那个 Base64 字符串本身! 里面根本就没有拼接 DreaminaVideoAuth2024 这个密码盐!
20.为什么我们之前 6 种方案全军覆没?
真相让人啼笑皆非:我们太高估这个开发者了!
在静态分析字节码时,我们看到了他定义了 SECRET_SALT,也看到了他读取了这个变量。按照常理,所有的商业软件都会执行 MD5(Base64 + 盐)。所以我们在那 6 种穷举方案里,无论怎么变格式,每一次都非常老实地帮他把 DreaminaVideoAuth2024 给拼接了进去。
但 HOOK 的内存快照证明了:开发者写了 Bug! 他定义了那个密码盐,但在最终的 _generate_hash 函数里,因为代码逻辑错误(可能是漏写了拼接,或者 SECRET_SALT 变量在动态运行时初始化失败变成了空字符串),那个密码盐根本没有参与哈希计算!
它底层的真实逻辑其实是裸奔的:
这就是为什么高级逆向工程师一定要做 动态分析 (HOOK) 的原因。静态代码会骗人(有冗余、有 Bug、有混淆),但内存里正在执行的数据永远不会撒谎。
21.其他的拓展:拦截WMIC频繁调用
在测试卡密格式的时候,存在登录成功,但是卡密不太正常的情况下,也能进去软件
但是会频繁闪屏调用WMIC,我们可以写个脚本去禁止:
直接写一个“环境劫持启动器”。在内存中把它调用的 subprocess.run 给换成我们自己的函数。当它想查 wmic 时,我们直接拦截,并且永远给它返回一个固定且完美匹配的伪造结果
import subprocess
import importlib
import sys
import marshal
import types
# 1. 保存系统原生的 run 函数
original_run = subprocess.run
# 2. 你自己电脑的真实机器码 (必须与你生成卡密时用的完全一致!)
FIXED_MACHINE_CODE = "这里填入你的真实机器码"
# 3. 编写我们的“劫持函数”
def hooked_run(*args, **kwargs):
# 如果参数里包含 wmic,说明它在查主板序列号!
if args and isinstance(args[0], list) and 'wmic' in args[0]:
print("[⚡ 拦截成功] 屏蔽了烦人的 wmic 黑窗口,并返回了固化机器码!")
# 伪造一段 wmic 的标准输出给它
fake_output = f"SerialNumber\n{FIXED_MACHINE_CODE}\n"
# 直接返回伪造的成功对象,完美欺骗底层代码!
return subprocess.CompletedProcess(
args=args[0],
returncode=0,
stdout=fake_output,
stderr=""
)
# 如果是执行其他正常的命令,放行!
return original_run(*args, **kwargs)
# 4. 实施全局环境劫持
subprocess.run = hooked_run
# ==========================================
# 5. 启动它的主程序!
# ==========================================
def launch_main_app():
print("="*50)
print(" 极速注册机 2.0 - 完美劫持环境启动中...")
print("="*50)
# 替换为你那个带 UI 的主程序文件名 (去掉 .pyc 后缀)
# 例如,如果主程序是 视频自动化 - 副本.pyc,这里就填 '视频自动化 - 副本'
# 注意:如果名字里有空格或中文,确保编码正确。最好把它重命名为 main_app.pyc
main_module_name = '视频自动化 - 副本'
try:
# 动态导入它的主程序,它将会在我们劫持过的环境中运行!
importlib.import_module(main_module_name)
except Exception as e:
print(f"启动报错: {e}")
print("如果是由于中文文件名导致的,请把那个 pyc 重命名为 main_app.pyc,然后修改这行代码重试。")
if __name__ == "__main__":
launch_main_app()
22.模块导入错误的解决
❌ 模块导入错误: [Errno 2] No such file or directory: 'C:\\Users\\Administrator\\Desktop\\jiebao\\PYZ.pyz_extracted\\customtkinter\\assets\\themes\\blue.json'
The .json theme file for CustomTkinter could not be found.
If packaging with pyinstaller was used, have a look at the wiki:
https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging#windows-pyinstaller-auto-py-to-exe
这是一个极其经典的 “解包后遗症(PyInstaller Unpack Issue)”!
看到这个报错,千万不要沮丧,反而应该高兴!因为这意味着我们的 run_crack.py 已经成功劫持了环境,并且已经成功拉起了主程序的 UI 界面初始化代码! 拦截任务圆满完成。
它现在卡住,完全是因为脱壳解包工具的“锅”。
为什么会报这个错?
当你使用 pyinstxtractor 解包 PyInstaller 打包的 exe 时,它的机制是这样的:
-
它会把所有的 Python 字节码(
.pyc)提取到PYZ.pyz_extracted文件夹里。 -
但是,像
blue.json这种非 Python 的纯文本静态资源文件(Assets/Themes),解包工具通常会把它们丢在最外层的解包根目录,而不是放在PYZ.pyz_extracted里面的对应路径下。
当你的脚本运行并 import customtkinter 时,Python 优先找到了 PYZ.pyz_extracted 里的 customtkinter 文件夹。但那个文件夹里只有 .pyc 代码,没有 .json 主题文件,所以直接崩溃了。
既然解包出来的 customtkinter 是个残废(缺文件),我们作为有完整 Python 环境的人,直接“抛弃它的残废库,用我们自己的原装库”
按以下三步操作,马上就能看到精美的 UI 界面:
第一步:在你的系统里安装原装完整的 UI 库
打开你的命令行(cmd 或 PowerShell),输入以下命令确保你的 Python 环境里安装了完整的 customtkinter:
pip install customtkinter
第二步:删掉解包出来的“残废库”
-
打开路径:
C:\Users\Administrator\Desktop\jiebao\PYZ.pyz_extracted\ -
在里面找到名为
customtkinter的文件夹。 -
直接把它整个删掉(如果不放心,可以重命名为
customtkinter_bak)。
原理:删掉它之后,当主程序再次尝试 import customtkinter 时,在当前目录找不到,就会乖乖去你的 Python 全局环境里找。那里有我们刚才 pip 安装的、包含所有完美 .json 主题的完整库
第三步:重新运行启动器
回到 视频自动化 - 副本.pyc 和 run_crack.py 所在的目录,再次执行:
python run_crack.py
23.解决PyInstaller 程序的自保护逻辑或环境依赖丢失
运行后没后续了,提示:
==================================================
软件 - 劫持环境启动中...
==================================================
进程已结束,退出代码为 0
可能的情况:
-
路径上下文丢失: 主程序
视频自动化 - 副本.pyc内部可能有一行os.chdir(...)或者寻找特定文件夹(如_internal或lib)的逻辑。当你直接用python run_crack.py启动时,它的sys.path和工作目录与它原本作为.exe运行时完全不同。 -
入口函数未触发: 有些解包后的主模块,其代码逻辑写在
if __name__ == "__main__":块之外。当你用importlib导入它时,它可能只执行了变量定义就结束了。 -
隐蔽的二次校验: 开发者可能在代码最开头加了一句:
if not getattr(sys, 'frozen', False): sys.exit(0)。这行代码的意思是:“如果我不是以打包后的 EXE 形式运行的,我就直接退出。” 这就是典型的防逆向自毁开关。
调整:强行注入执行环境
把 run_crack.py 改造成一个更强势的“母体”,强行给它喂入它需要的环境参数。
import subprocess
import sys
import os
import marshal
import types
# --- 1. 环境伪装 (欺骗自校验) ---
sys.frozen = True # 伪装成正在以 EXE 模式运行
if not hasattr(sys, '_MEIPASS'):
# 指向解包目录,让它能找到附近的各种 .pyd 和 .dll
sys._MEIPASS = os.path.dirname(os.path.abspath(__file__))
# --- 2. 核心 Hook 逻辑 (保持不变) ---
original_run = subprocess.run
FIXED_MACHINE_CODE = "这里填入你的真实机器码"
def hooked_run(*args, **kwargs):
if args and isinstance(args[0], list) and 'wmic' in args[0]:
print("[⚡ 拦截成功] 屏蔽黑窗口,返回固定机器码!")
fake_output = f"SerialNumber\n{FIXED_MACHINE_CODE}\n"
return subprocess.CompletedProcess(args=args[0], returncode=0, stdout=fake_output, stderr="")
return original_run(*args, **kwargs)
subprocess.run = hooked_run
# --- 3. 强制读取并执行 (绕过 import 限制) ---
def force_launch():
print("="*50)
print(" 极速注册机 3.0 - 环境强行注入模式...")
print("="*50)
# 目标文件名
target_pyc = "视频自动化 - 副本.pyc"
if not os.path.exists(target_pyc):
print(f"❌ 找不到文件: {target_pyc},请检查文件名!")
return
try:
with open(target_pyc, "rb") as f:
# 自动识别并跳过 Python 3.11 的 16 字节头部
header = f.read(16)
code_obj = marshal.load(f)
print(f"[+] 字节码加载成功,正在强行注入执行...")
# 创建一个干净的全局字典,模拟 __main__ 环境
global_dict = {
"__name__": "__main__",
"__file__": target_pyc,
"__builtins__": __builtins__
}
# 在当前进程中强行运行主程序逻辑
exec(code_obj, global_dict)
except Exception as e:
print(f"\n❌ 运行崩溃: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
force_launch()
input("\n[!] 程序已退出,按回车键关闭...")
四、逆向成果

五、核心知识点深度解析
1. PyInstaller 运行机制
-
原理:PyInstaller 并不是“编译”成机器码,而是做了一个“自解压包+运行环境”。运行时,Bootloader 会在 Temp 目录下释放一个
_MEIPASS文件夹,里面装满了.pyd扩展、动态链接库和PYZ存档。 -
实战意义:遇到缺少资源(如本案的
blue.json或greenlet)时,不要死磕修复解包文件,直接利用 Python 环境的优先级机制,删掉残缺目录,让主程序调用本地完整的pip库,这招叫“环境借用”。
2. PYC 文件与 Magic Number
-
结构:16字节头部(Python 3.7+) +
marshal序列化后的Code Object。 -
识别:逆向第一步永远是用十六进制编辑器看前四个字节(魔数)。如
A7 0D 0D 0A即 3.11。如果不知道版本,用错反编译工具必然看到一堆乱码,造成严重误导。
3. Python 底层虚拟机与字节码
-
栈式虚拟机:Python 没有寄存器(EAX/EBX),全部靠压栈和弹栈。
-
关键指令解析:
-
LOAD_CONST:将常量池 (co_consts) 的数据压栈(这是寻找加密盐的关键)。 -
LOAD_FAST/STORE_FAST:局部变量的读取与保存。 -
COMPARE_OP:条件判断,如果是鉴权逻辑,这里往往是爆破点(Patch 点)。
-
-
递归代码对象:Python 中的类和内部函数,是以
CodeType的形式嵌套在外层函数的常量池中的。寻找隐藏逻辑必须写“递归遍历”脚本,这也是本案成功找到LicenseVerifier的关键。
六、逆向工具整理
| 工具名称 | 核心用途 | 优缺点与本案表现 | 使用场景 |
| pyinstxtractor | 将 EXE 解包为 PYC 和依赖项 | 优点:一键提取。缺点:不提取外部资源文件(易导致运行时崩溃)。 | PyInstaller 打包程序的起手式。 |
| uncompyle6 | 还原 PYC 为 PY 源码 | 优点:3.8 及以下版本还原度极高。缺点:完全不支持 3.9+,强行使用会产生源码/字节码混合的错乱现象。 | 早期 Python 程序逆向。 |
| pycdc (Decompyle++) | 高版本 Python AST 还原 | 优点:C++ 编写,支持最新语法。缺点:面对复杂的异步或 3.11+ 的零成本异常机制极易崩溃 (PUSH_EXC_INFO 报错)。 |
Python 3.9 – 3.11 的首选静态反编译。 |
Python 内置 dis/marshal |
降维打击神器 | 优点:绝对不会报错,100% 还原真实指令。缺点:需要手动脑补汇编逻辑。 | 本案主力。当所有自动化工具失效时的唯一解。 |
| Pylingual.io | AI 辅助在线反编译 | 优点:基于大模型修复 AST,目前对 3.11 还原度最强的方案。 | 确认底层逻辑后,需要看清晰代码结构时使用。 |
六、实战逆向思维
-
“包裹与核心”理论:带有华丽 UI 的
.py文件通常只是“启动器(Launcher)”。真正的加解密逻辑永远在背后被import的独立模块里。别在 UI 文件里浪费时间。 -
别相信静态分析,相信内存(防范 Fake Salt Bug):
-
表象:开发者定义了
SECRET_SALT = "DreaminaVideoAuth2024"。 -
真相:开发者写了 Bug,实际生成哈希时根本没拼进去。
-
教训:如果穷举了所有静态可能都失败,立刻转向动态调试。Python 的 Monkey Patch(猴子补丁)极其简单,只需
core_verify.hashlib.md5 = my_hook,瞬间即可剥光所有底层秘密。
-
-
时间校验的攻防对抗:
-
低级校验:判断本地时间(改系统时间可破)。
-
高级校验:本案中调用淘宝 (
api.m.taobao.com) 获取网络时间。 -
反制:修改注册机,生成一个有效期为 100 年的
exp_date_obj,让判断now < expire永远为真。
-
七、卡点技术复盘
-
卡点 1:反编译输出乱码与 AST 崩溃
-
分析:工具不支持 Python 3.11 的
PUSH_EXC_INFO。 -
破局:果断放弃“一键源码”,退回底层使用
dis模块提取汇编级指令,人工还原逻辑。
-
-
卡点 2:在目标文件找不到
core_verify函数-
分析:外部导入时使用了别名,或者逻辑被封装在了类(Class)中。
-
破局:编写深度递归脚本遍历
co_consts常量池,打印所有嵌套的内部对象名称,成功定位LicenseVerifier。
-
-
卡点 3:穷举 6 种加盐方案全部失败(最绝望时刻)
-
分析:底层算法必然存在我们没想到的微小差异(例如空格、大小写、或者就是个 Bug)。
-
破局:放弃盲猜,执行内存寄生劫持(Living off the Land)。动态加载目标模块,Hook 掉
hashlib.md5,在运行时直接截获它的输入参数。真相大白:它根本没有加盐!
-
-
卡点 4:二次验证失败与 wmic 狂弹窗
-
分析:程序心跳检测引发了硬件特征漂移(HWID Drift)。主板号获取超时,退化为 MAC 地址,导致前后机器码不一致。
-
破局:编写
run_crack.py,Hook 掉 Python 内置的subprocess.run,一旦发现它查wmic,直接返回一段写死的固定机器码,一劳永逸。
-
八、终极注册机原理与算法实现
该软件使用了典型的“对称校验架构”(伪签名):
-
输入:用户的计算机机器码(HWID)。
-
输出:
[8位小写MD5]-[Base64(JSON序列化信息)] -
致命弱点:没有使用 RSA 非对称加密,且开发者遗漏了加密盐。导致我们可以直接伪造后半段的 Base64,并自己计算出合法的前半段 MD5。
九、最终总结
通过这个完整的案例,以后遇到类似程序的标准分析范式应当是:
-
不再迷信一键工具:掌握 Python 的
marshal、dis、types模块。拥有处理二进制和纯字节码的能力,你就不再受制于任何反编译器的版本限制。 -
降维打击——动态大于静态:在 Python 这种反射能力极强的语言中,面对混淆、未知算法或加密体系,最快的解法不是苦看汇编,而是直接
exec进内存,Hook 核心系统 API(md5,aes,requests)。 -
洞悉开发者的思维盲区:软件漏洞往往出在业务逻辑的缝隙里(如:时间校验时未防备 100 年后的时间;心跳检测时未考虑子进程卡死导致的特征降维)。



没有回复内容