InnoSetup的机器码与注册码生成与验证机制

逆向工程 算法 其它InnoSetup机器码
浏览数 - 100发布于 - 2026-05-15 - 09:56

重新编辑于 - 2026-05-15 - 10:00

1. 执行摘要

本次分析对象 安装包.exe 不是传统意义上的带壳主程序,而是一个 Inno Setup 安装器。它的主要功能是释放包内安装文件,并在安装界面中实现一个本地注册码校验流程。

核心发现:

  • 安装包类型:Inno Setup。

  • 安装数据版本:Inno Setup Setup Data (5.4.2)

  • 没有发现独立业务主程序。

  • 机器码来源:Windows 磁盘卷序列号。

  • 关键 API:kernel32.GetVolumeInformationA

  • 核心脚本函数:

    • GENMACHINEID

    • GENREGCODE

    • CheckSerial

  • 机器码前缀:V94- 

  • 注册码前缀:X5B- 

  • 已复现工具:inno_keygen.py

2. 分析环境

2.1 系统环境

Windows10

2.2 工具清单

本次实际使用或验证过的工具:

text
innoextract 1.9
innounp
Binary Refinery
IDA
IDA MCP
Python 3.14
PowerShell
rg / Select-String

关键工具路径:

text
tools\innoextract19\pkg\innoextract.exe
C:\Users\x\AppData\Local\Programs\Python\Python314\Scripts\ifps.exe
C:\Users\x\AppData\Local\Programs\Python\Python314\Scripts\ifpsstr.exe
C:\Users\Kino\AppData\Local\Programs\Python\Python314\Scripts\emit.exe

3. 样本基本信息

3.1 文件信息

text
文件名: 安装包.exe
类型: Windows PE / Inno Setup installer

3.2 初始判断

字符串中出现:

text
Inno Setup Setup Data (5.4.2)
LzmaDecode failed

说明:

  • 样本是 Inno Setup 安装器。

  • 主体安装数据采用 LZMA 压缩。

  • “壳”的表现主要来自安装数据压缩,而不是 VMProtect、Themida、UPX 等传统壳。

3.3 PE 与 Overlay 观察

观察到:

text
overlay starts at 0x68e00
overlay size 30044975

这符合 Inno Setup 安装器结构:

text
PE stub
  + resources
  + compressed setup data overlay

4. Inno Setup 提取过程

4.1 工具尝试记录

旧版工具结果:

text
innoextract 1.4 -> stream error: basic_ios::clear
innounp 2.67.9 -> The setup files are corrupted

新版工具结果:

text
innoextract 1.9 -> 成功

有效命令:

Textile
.\tools\innoextract19\pkg\innoextract.exe --list .\安装包.exe
.\tools\innoextract19\pkg\innoextract.exe -d .\out19 --extract .\安装包.exe

4.2 提取结果

提取结果正常

4.3 文件功能

核心授权逻辑不在释放文件里,而在 Inno Pascal Script 字节码里。

5. Inno Header 与 IFPS 字节码提取

5.1 RCDATA 结构观察

从资源中解析到关键字段:

text
ID = 72446c507453cde6d77b0b2a
Version = 0x1
TotalSize = 0x1d1012f
OffsetEXE = 0x1c75de5
UncompressedSizeEXE = 0x110c00
CRCEXE = 0xfc461b2b
Offset0 = 0x1c0911f
Offset1 = 0x68e00
TableCRC = 0x529d5a77
CalcTableCRC = 0x529d5a77

5.2 Carved 文件

手动切分得到:

text
carved/setup-1.bin
carved/setup-0.bin
carved/setup.e32.compressed
carved/setup.exe

其中:

text
carved/setup-0.bin

包含:

text
Inno Setup Setup Data (5.4.2)

5.3 Header 解压

解压主 header block 后得到:

text
headers19_primary.bin
size = 613266

重要发现:

text
b'IFPS' marker
GENMACHINEID
GENREGCODE
CHECKSERIAL
V94-

5.4 IFPS 字节码文件

headers19_primary.bin 中切出:

text
CompiledCode_full.bin

使用 Binary Refinery 反汇编:

powershell
cmd /c "emit.exe CompiledCode_full.bin | ifpsstr.exe > ifps_strings.txt"
cmd /c "emit.exe CompiledCode_full.bin | ifps.exe -b > ifps_disasm.txt"

6. 字符串证据链

ifps_strings.txt 中出现:

text
6J7-
V94-
3P6-
X5B-
C:\
D:\

ifps_disasm.txt 中出现:

text
external function __stdcall kernel32::GetVolumeInformationA(...)
function GENMACHINEID(Argument1: Integer): String
function GENREGCODE(Argument1: Integer): String
function CheckSerial(Serial: String): Boolean

这些字符串构成完整链:

text
磁盘卷序列号
  -> 机器码函数
  -> 注册码函数
  -> 输入校验函数

7. 机器码来源分析

7.1 关键 API

脚本调用:

text
kernel32::GetVolumeInformationA

相关片段:

text
Call kernel32::GetVolumeInformationA
SetPtr LocalVar12 := GlobalVar0

含义:

text
GlobalVar0 = VolumeSerialNumber

GlobalVar0 保存磁盘卷序列号。

7.2 盘符选择逻辑

脚本先判断:

text
DirExists('C:\')

如果存在:

text
LocalVar5 := 'C:\'

否则:

text
LocalVar5 := 'D:\'

所以机器码通常来自 C:\ 的卷序列号。

7.3 不是哪些机器特征

没有发现以下证据:

text
GetAdaptersInfo
GetAdaptersAddresses
MachineGuid
CPUID
WMI
BIOS Serial
Motherboard Serial

因此不是 MAC 地址、CPU ID、主板 ID 或 Windows MachineGuid。

8. 机器码生成机制

8.1 函数

text
GENMACHINEID(Argument1: Integer): String

其中:

text
Argument1 = GlobalVar0 = volume_serial

8.2 常量

机器码 pivot:

text
1087244140

8.3 逻辑

对于 V94- 分支:

text
machine_diff = 1087244140 - volume_serial

然后把 machine_diff 编码为字符串。

8.4 编码规则

数字差值先转为十进制字符串。

例:

text
machine_diff = 1170189346

编码过程:

text
1170189346
  -> 分块
  -> 插入字母
  -> 加横线
  ->机器码

字母计算:

python
letter = chr(65 + int(chunk) * 26 // 4653)

示例:

text
chunk = 701
65 + 701 * 26 // 4653 = 68
chr(68) = 'D'

所以:

text
701 -> D701

8.5 当前 Python 实现

inno_keygen.py 中:

python
def gen_machine_id(volume_serial: int) -> str:
    if volume_serial > MACHINE_PIVOT:
        return _encode_2_3_diff(volume_serial - MACHINE_PIVOT, "6J7-")
    return _encode_2_3_diff(MACHINE_PIVOT - volume_serial, "V94-")

9. 注册码生成机制

9.1 初始误判

反汇编文本中出现过:

text
1979254653

一开始据此推导:

text
reg_diff = 1979254653 - volume_serial

但该结果和真实通过样本冲突。

9.2 样本校正

text
reg_diff = 278178833

因此:

text
reg_pivot = reg_diff + volume_serial
reg_pivot = 278178833 + (-82945206)
reg_pivot = 195233627

所以实际用于当前 GUI/样本路径的注册码 pivot 等价于:

text
195233627

9.3 当前注册码逻辑

对于 X5B- 分支:

text
reg_diff = 195233627 - volume_serial

然后用同类编码方式生成注册码。

当前实现:

python
def gen_reg_code(volume_serial: int) -> str:
    if volume_serial > REG_PIVOT:
        return _encode_2_3_diff(volume_serial - REG_PIVOT, "3P6-")
    return _encode_2_3_diff(REG_PIVOT - volume_serial, "X5B-")

10. 校验流程分析

10.1 函数

text
CheckSerial(Serial: String): Boolean

10.2 反汇编逻辑

核心流程:

text
LocalVar2 := GlobalVar0
Call GENREGCODE
Compare Argument1 == LocalVar1

伪代码:

python
def CheckSerial(user_input):
    expected = GENREGCODE(GlobalVar0)
    if user_input == expected:
        return True
    return uppercase(user_input) == uppercase(expected)

10.3 大小写处理

CheckSerial 中调用:

text
Uppercase

说明字母大小写可能不敏感。

但注意:

text
横线位置、数字顺序、前缀仍然必须正确。

11. 当前工具说明

11.1 文件

text
inno_keygen.py

11.2 使用方式

不带参数打开 GUI:

powershell
python .\inno_keygen.py

带机器码参数输出结果:

Textile
python .\inno_keygen.py 注册码

兼容旧式参数:

Textile
python .\inno_keygen.py --machine 注册码
python .\inno_keygen.py --gui

11.3 GUI 行为

GUI 功能:

  • 输入机器码。

  • 点击生成。

  • 显示反推卷序列号。

  • 显示注册码。

  • 自动复制注册码到剪切板。

12. 风险评估

12.1 授权机制弱点

当前机制存在明显弱点:

  • 机器码来源单一。

  • 只绑定卷序列号。

  • 常量和算法在客户端。

  • 校验在客户端本地完成。

  • 没有服务器参与。

  • 没有非对称签名。

  • 算法是线性差值 + 可逆编码。

12.2 可被复现的原因

攻击者或研究人员只需要:

text
提取 IFPS 字节码
定位函数
读取常量
观察真实样本
用 Python 复现

即可生成对应注册码。

13.设计更安全的授权系统

13.1 不要把 secret 放在客户端

错误设计:

text
客户端内置注册码生成算法和私有常量。

改进:

text
服务器保存 secret。
客户端只做验证或请求授权。

13.2 使用非对称签名

建议 license 结构:

json
{
  "product": "example",
  "user": "user_id",
  "expires": "2026-12-31",
  "features": ["base", "pro"],
  "machine": "fingerprint"
}

服务器:

text
signature = Sign(private_key, payload)

客户端:

text
Verify(public_key, payload, signature)

13.3 服务端校验

对于重要产品,建议:

  • 在线激活。

  • 短期 token。

  • 设备绑定。

  • 吊销机制。

  • 异常登录检测。

13.4 客户端抗篡改

客户端仍可能被 patch,因此可以增加:

  • 完整性检查。

  • 关键逻辑分散。

  • 混淆。

  • 反调试。

  • 行为异常检测。

但这些只能提高成本,不能替代正确的授权架构。

14. 后续验证建议

为了进一步确认算法,建议继续收集更多真实样本:

text
机器码 -> 实际可通过注册码

建议样本至少覆盖:

  • 多个 V94- 机器码。

  • 是否存在 6J7- 机器码。

  • X5B-3P6- 是否都可能出现。

  • 卷序列号为正数和负数的情况。

  • 边界值附近:

    • volume_serial = 195233627

    • volume_serial = 1087244140

每个样本记录:

text
机器码
注册码
是否通过
系统盘符
GetVolumeInformationA 返回的卷序列号

18. 附录 A:关键命令

18.1 提取 Inno 安装包

powershell
.\tools\innoextract19\pkg\innoextract.exe --list .\Pets.exe
.\tools\innoextract19\pkg\innoextract.exe -d .\out19 --extract .\Pets.exe

18.2 反汇编 IFPS

powershell
cmd /c "emit.exe CompiledCode_full.bin | ifpsstr.exe > ifps_strings.txt"
cmd /c "emit.exe CompiledCode_full.bin | ifps.exe -b > ifps_disasm.txt"

18.3 搜索关键段

powershell
Select-String -Path .\ifps_disasm.txt -Pattern "GENMACHINEID|GENREGCODE|CheckSerial"
Select-String -Path .\ifps_disasm.txt -Pattern "GetVolumeInformationA"
Select-String -Path .\ifps_strings.txt -Pattern "V94|X5B|3P6|6J7"

19. 附录 B:核心 Python 逻辑摘要

当前核心参数:

python
MACHINE_PIVOT = 1087244140
REG_PIVOT = 195233627

机器码反推:

python
volume_serial = MACHINE_PIVOT - machine_diff

注册码差值:

python
reg_diff = REG_PIVOT - volume_serial

编码:

python
letter = chr(65 + int(chunk) * 26 // 4653)

20. 附录 C:参考资料

源码如下

javascript
import argparse
import ctypes
import re
import tkinter as tk
from tkinter import messagebox


MACHINE_PIVOT = 1087244140

# The running installer uses the same encoder as the machine-code field, but the
# registration pivot is 195233627.
REG_PIVOT = 195233627


def _encode_standard(value: int, pivot: int, high_prefix: str, low_prefix: str) -> str:
    if value > pivot:
        body = str(value - pivot)
        prefix = high_prefix
    else:
        body = str(pivot - value)
        prefix = low_prefix

    expanded = ""
    while len(body) > 3:
        chunk = body[:3]
        body = body[3:]
        expanded += chr(65 + (int(chunk) * 26 // 4653)) + chunk

    expanded += chr(65 + (int(body) * 26 // 4653)) + body
    return prefix + _group_left_3(expanded)


def _encode_2_3_diff(diff: int, prefix: str) -> str:
    body = str(diff)
    chunks = []

    if len(body) > 3:
        chunks.append(body[:2])
        body = body[2:]

    while len(body) > 3:
        chunks.append(body[:3])
        body = body[3:]

    if body:
        chunks.append(body)

    expanded = "".join(chr(65 + (int(chunk) * 26 // 4653)) + chunk for chunk in chunks)
    return prefix + _group_with_leading_singles(expanded)


def _group_left_3(text: str) -> str:
    parts = []
    while len(text) > 3:
        parts.append(text[:3])
        text = text[3:]
    if text:
        parts.append(text)
    return "-".join(parts)


def _group_with_leading_singles(text: str) -> str:
    parts = []
    first = min(2, len(text))
    while first:
        parts.append(text[:1])
        text = text[1:]
        first -= 1
    while text:
        parts.append(text[:3])
        text = text[3:]
    return "-".join(parts)


def _machine_prefix_and_diff(machine_id: str) -> tuple[str, int]:
    machine_id = machine_id.strip().upper()
    if machine_id.startswith("V94-"):
        prefix = "V94-"
        body = machine_id[4:]
    elif machine_id.startswith("6J7-"):
        prefix = "6J7-"
        body = machine_id[4:]
    else:
        raise ValueError("Machine code must start with V94- or 6J7-.")

    digits = "".join(re.findall(r"\d+", body))
    if not digits:
        raise ValueError("No digits found in machine code.")
    return prefix, int(digits)


def gen_machine_id(volume_serial: int) -> str:
    if volume_serial > MACHINE_PIVOT:
        return _encode_2_3_diff(volume_serial - MACHINE_PIVOT, "6J7-")
    return _encode_2_3_diff(MACHINE_PIVOT - volume_serial, "V94-")


def gen_reg_code(volume_serial: int) -> str:
    if volume_serial > REG_PIVOT:
        return _encode_2_3_diff(volume_serial - REG_PIVOT, "3P6-")
    return _encode_2_3_diff(REG_PIVOT - volume_serial, "X5B-")


def reg_code_from_machine_id(machine_id: str) -> str:
    prefix, machine_diff = _machine_prefix_and_diff(machine_id)
    if prefix == "V94-":
        serial = MACHINE_PIVOT - machine_diff
    else:
        serial = MACHINE_PIVOT + machine_diff
    return gen_reg_code(serial)


def serial_from_machine_id(machine_id: str) -> int:
    prefix, diff = _machine_prefix_and_diff(machine_id)
    if prefix == "V94-":
        return MACHINE_PIVOT - diff
    return MACHINE_PIVOT + diff


def get_volume_serial(root: str) -> int:
    serial = ctypes.c_uint32()
    ok = ctypes.windll.kernel32.GetVolumeInformationW(
        ctypes.c_wchar_p(root),
        None,
        0,
        ctypes.byref(serial),
        None,
        None,
        None,
        0,
    )
    if not ok:
        raise ctypes.WinError()
    return ctypes.c_int32(serial.value).value


def run_gui() -> None:
    root = tk.Tk()
    root.title("Inno Keygen")
    root.resizable(False, False)

    machine_var = tk.StringVar()
    serial_var = tk.StringVar()
    reg_var = tk.StringVar()
    status_var = tk.StringVar(value="Enter a machine code, then click Generate.")

    def generate() -> None:
        try:
            machine = machine_var.get()
            serial = serial_from_machine_id(machine)
            reg_code = reg_code_from_machine_id(machine)
        except Exception as exc:
            messagebox.showerror("Error", str(exc))
            return

        serial_var.set(str(serial))
        reg_var.set(reg_code)
        root.clipboard_clear()
        root.clipboard_append(reg_code)
        status_var.set("Registration code copied to clipboard.")

    def paste_and_generate() -> None:
        try:
            machine_var.set(root.clipboard_get().strip())
        except tk.TclError:
            pass
        generate()

    frame = tk.Frame(root, padx=14, pady=14)
    frame.grid(row=0, column=0)

    tk.Label(frame, text="Machine code").grid(row=0, column=0, sticky="w")
    tk.Entry(frame, textvariable=machine_var, width=44).grid(row=1, column=0, columnspan=3, pady=(4, 10))

    tk.Button(frame, text="Generate + Copy", command=generate, width=16).grid(row=2, column=0, sticky="w")
    tk.Button(frame, text="Paste + Generate", command=paste_and_generate, width=16).grid(row=2, column=1, padx=8)

    tk.Label(frame, text="Decoded serial").grid(row=3, column=0, sticky="w", pady=(12, 0))
    tk.Entry(frame, textvariable=serial_var, width=44, state="readonly").grid(row=4, column=0, columnspan=3, pady=(4, 10))

    tk.Label(frame, text="Registration code").grid(row=5, column=0, sticky="w")
    tk.Entry(frame, textvariable=reg_var, width=44, state="readonly").grid(row=6, column=0, columnspan=3, pady=(4, 10))

    tk.Label(frame, textvariable=status_var, fg="#555555").grid(row=7, column=0, columnspan=3, sticky="w")

    root.bind("<Return>", lambda _event: generate())
    root.mainloop()


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Reproduce the Inno Setup machine-code to registration-code logic."
    )
    parser.add_argument("machine", nargs="?", help="machine code such as V94-A-1-1D7-01E-893-A46")
    parser.add_argument("--root", default=None, help=r"drive root to query, for example C:\ or D:\ ")
    parser.add_argument("--machine", "-m", dest="machine_option", help="machine code such as V94-A-1-1D7-01E-893-A46")
    parser.add_argument("--serial", type=int, help="decimal volume serial number")
    parser.add_argument("--gui", action="store_true", help="open the GUI and copy generated codes")
    args = parser.parse_args()

    if args.gui or (not args.machine and not args.machine_option and not args.root and args.serial is None):
        run_gui()
        return

    machine = args.machine_option or args.machine
    if machine:
        serial = serial_from_machine_id(machine)
        print(f"volume_serial={serial}")
        print(f"machine_id={machine.strip().upper()}")
        print(f"reg_code={reg_code_from_machine_id(machine)}")
        return

    if args.root:
        serial = get_volume_serial(args.root)
    elif args.serial is not None:
        serial = args.serial

    print(f"volume_serial={serial}")
    print(f"machine_id={gen_machine_id(serial)}")
    print(f"reg_code={gen_reg_code(serial)}")


if __name__ == "__main__":
    main()

本文版权遵循 CC BY-NC 协议 本站版权政策

(。>︿<。) 已经一滴回复都不剩了哦~