Python PyInstaller软件高阶逆向实战笔记

一、 逆向目标与战略定位

  • 逆向目标:XXX极速注册机 2.0(一款商业自动化营销辅助软件)。

  • 技术栈特征:Python 3.11 编写,PyInstaller 打包,集成 CustomTkinter (GUI) 与 Playwright (自动化),采用离线机器码绑定与强心跳检测机制。

  • 技术路线:静态字节码分析(跳过高版本 AST 还原失败的坑) ➜ 递归常量池探测 ➜ 动态 API 劫持(Monkey Patching) ➜ 环境欺骗启动。

  • 最终战果

    1. 实现 100% 完美的脱机注册机(KeyGen),突破“假盐”与“时间校验”陷阱。

    2. 编写了基于内存的“环境劫持启动器”,彻底攻破该软件的第二层 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

watermarked-image-min

2.解包软件

我习惯用在线的,直接用:https://pyinstxtractor-web.netlify.app/

解包出来文件包含一些重要的pyc文件,一般通过字符串可以辨别谁是重要的那个,一眼就能发现下面这个自动化.pyc有问题

watermarked-image(1)-min

3.反编译pyc

我们其实也可以用在线的,但是有的pyc反编译不出来,所以就要借助一些其他手段,当然我们还是要尝试一下:

以下是标准的 .pyc 逆向分析和还原方法论:

第一步:确定 Python 编译版本(Magic Number)

反编译的第一步是必须知道它是用哪个版本的 Python 编译的。.pyc 文件的开头包含了 Magic Number(魔数),它直接对应了 Python 的版本号。

  1. 用十六进制编辑器(如 010 Editor, HxD)打开 .pyc 文件。

  2. 查看前两个字节。

  3. 对照 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 及以下版本: 这是最幸福的区间。直接使用 uncompyle6decompyle3,还原度可以达到 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 个字节的头部):

  1. Magic Number (4 bytes):版本信息。

  2. Bitfield (4 bytes):PEP 552 引入的标志位。

  3. Timestamp (4 bytes):编译时间。

  4. File Size (4 bytes):原始 .py 文件大小。

  5. Payload:经过 marshal 序列化后的 code object(代码对象,包含了指令、常量表、变量名表等)。

如何手动分析字节码?

可以编写一个简单的 Python 脚本,使用内置的 marshaldis 模块来解析它:

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

watermarked-image(2)-min

为大家整理了一下版本,方便查阅:

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 支持最好的反编译工具。

  1. 需要去 GitHub 下载 zrax/pycdc 的源代码。

  2. 使用 CMake 在本地编译出可执行文件。

  3. 运行命令反编译核心逻辑: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 获取干净的字节码

它不会尝试去重构高层语法,而是直接把底层指令翻译成人类可读的助记符。它绝对不会报错

watermarked-image(3)-min

果然,里面的字符串初步看来有点东西

除了这个工具外,当然也有在线的能用的: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 浏览器进行网页交互。

  • 并发控制:使用了 asynciothreading 来实现多线程/异步操作,支持同时跑多个注册任务。

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 模拟真人操作:

  1. 注入防风控脚本:代码中有一段 clickKillerId,用于自动点击并关闭页面上弹出的公告或弹窗(寻找 “Got it”, “我知道了”, “关闭” 等按钮)。

  2. 模拟鼠标轨迹:包含一个名为 human_move_mouse 的函数,用于生成带有贝塞尔曲线或随机抖动的鼠标移动轨迹,防止被网站的安全系统识别为机器人。

  3. 绕过检测:启动浏览器时带有 --disable-blink-features=AutomationControlled 参数,专门用来绕过无头浏览器的检测。

  4. 填写资料:自动定位页面的邮箱输入框、密码输入框,并在需要填写生日时,统一硬编码填写为 2000年1月15日

  5. 提取 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_verifyget_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_verifyget_machine_code 的字节码,这就说明了一件事:这两个函数根本不在这份文件

这也是商业软件最常用的防破解策略(模块分离与底层隐藏): 开发者将最核心的机器码获取、RSA 解密、网络验签逻辑,写在了外部文件中。然后在当前文件的最开头,通过 import 将其导入。

10.下一步的路线

既然我们要找 core_verify,就必须追踪它的来源。

  1. 去查看最开始的反编译“乱码”文件(或者用普通文本编辑器打开 .pyc 强行看纯文本部分)。

  2. 在文件的最开头(头部区域),寻找 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
貌似成功深入到了软件的“真正核心” ,我们从输出结果中可以清晰地看到,这个文件里定义了一个专门的安全类 LicenseVerifier,而在文件的最底部,还有一个全局的外壳函数 get_machine_code 用来调用这个类。
 
还原后的 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:

  1. http://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp (淘宝的公开时间接口)

  2. 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 = NoneSECRET_SALT 根本不在这里!

为什么会这样?这是面向对象编程的一个细节:

在 Python 中,通过 self.SECRET_SALT 访问的变量,不一定非要在 __init__ 里定义。它极有可能是一个“类变量”(Class Attribute)。 也就是说,代码是这样写的:

class LicenseVerifier:
    SECRET_SALT = "这里是我们要找的终极密码盐"  # <--- 它定义在类的主体里,而不是函数里!

    def __init__(self):
        self._cached_device_code = None

终极方案:全局字符串爆破(Strings Extraction)

结果:你终于出来了

watermarked-image(5)-min

既然加密盐出来了,那我们来编写极速注册机:

现在,这个软件在我们面前已经没有任何秘密了。我们可以直接在本地运行它来无限量生成完美卡密:

结果,错了,卧槽,还有这事?

冷静下来分析,通常可能是由以下几个“底层细节盲区”导致的:

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.md5json.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 时,它的机制是这样的:

  1. 它会把所有的 Python 字节码(.pyc)提取到 PYZ.pyz_extracted 文件夹里。

  2. 但是,像 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

第二步:删掉解包出来的“残废库”

  1. 打开路径:C:\Users\Administrator\Desktop\jiebao\PYZ.pyz_extracted\

  2. 在里面找到名为 customtkinter 的文件夹。

  3. 直接把它整个删掉(如果不放心,可以重命名为 customtkinter_bak)。

原理:删掉它之后,当主程序再次尝试 import customtkinter 时,在当前目录找不到,就会乖乖去你的 Python 全局环境里找。那里有我们刚才 pip 安装的、包含所有完美 .json 主题的完整库

第三步:重新运行启动器

回到 视频自动化 - 副本.pycrun_crack.py 所在的目录,再次执行:

python run_crack.py

23.解决PyInstaller 程序的自保护逻辑环境依赖丢失

运行后没后续了,提示:

==================================================
软件 - 劫持环境启动中...

==================================================

进程已结束,退出代码为 0

可能的情况:

  • 路径上下文丢失: 主程序 视频自动化 - 副本.pyc 内部可能有一行 os.chdir(...) 或者寻找特定文件夹(如 _internallib)的逻辑。当你直接用 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[!] 程序已退出,按回车键关闭...")

四、逆向成果

watermarked-image(6)-min

五、核心知识点深度解析

1. PyInstaller 运行机制

  • 原理:PyInstaller 并不是“编译”成机器码,而是做了一个“自解压包+运行环境”。运行时,Bootloader 会在 Temp 目录下释放一个 _MEIPASS 文件夹,里面装满了 .pyd 扩展、动态链接库和 PYZ 存档。

  • 实战意义:遇到缺少资源(如本案的 blue.jsongreenlet)时,不要死磕修复解包文件,直接利用 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。

九、最终总结

通过这个完整的案例,以后遇到类似程序的标准分析范式应当是:

  1. 不再迷信一键工具:掌握 Python 的 marshaldistypes 模块。拥有处理二进制和纯字节码的能力,你就不再受制于任何反编译器的版本限制。

  2. 降维打击——动态大于静态:在 Python 这种反射能力极强的语言中,面对混淆、未知算法或加密体系,最快的解法不是苦看汇编,而是直接 exec 进内存,Hook 核心系统 API(md5, aes, requests)。

  3. 洞悉开发者的思维盲区:软件漏洞往往出在业务逻辑的缝隙里(如:时间校验时未防备 100 年后的时间;心跳检测时未考虑子进程卡死导致的特征降维)。

请登录后发表评论

    没有回复内容