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 工具清单
本次实际使用或验证过的工具:
innoextract 1.9
innounp
Binary Refinery
IDA
IDA MCP
Python 3.14
PowerShell
rg / Select-String
关键工具路径:
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 文件信息
文件名: 安装包.exe
类型: Windows PE / Inno Setup installer
3.2 初始判断
字符串中出现:
Inno Setup Setup Data (5.4.2)
LzmaDecode failed
说明:
-
样本是 Inno Setup 安装器。
-
主体安装数据采用 LZMA 压缩。
-
“壳”的表现主要来自安装数据压缩,而不是 VMProtect、Themida、UPX 等传统壳。
3.3 PE 与 Overlay 观察
观察到:
overlay starts at 0x68e00
overlay size 30044975
这符合 Inno Setup 安装器结构:
PE stub
+ resources
+ compressed setup data overlay
4. Inno Setup 提取过程
4.1 工具尝试记录
旧版工具结果:
innoextract 1.4 -> stream error: basic_ios::clear
innounp 2.67.9 -> The setup files are corrupted
新版工具结果:
innoextract 1.9 -> 成功
有效命令:
.\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 结构观察
从资源中解析到关键字段:
ID = 72446c507453cde6d77b0b2a
Version = 0x1
TotalSize = 0x1d1012f
OffsetEXE = 0x1c75de5
UncompressedSizeEXE = 0x110c00
CRCEXE = 0xfc461b2b
Offset0 = 0x1c0911f
Offset1 = 0x68e00
TableCRC = 0x529d5a77
CalcTableCRC = 0x529d5a77
5.2 Carved 文件
手动切分得到:
carved/setup-1.bin
carved/setup-0.bin
carved/setup.e32.compressed
carved/setup.exe
其中:
carved/setup-0.bin
包含:
Inno Setup Setup Data (5.4.2)
5.3 Header 解压
解压主 header block 后得到:
headers19_primary.bin
size = 613266
重要发现:
b'IFPS' marker
GENMACHINEID
GENREGCODE
CHECKSERIAL
V94-
5.4 IFPS 字节码文件
从 headers19_primary.bin 中切出:
CompiledCode_full.bin
使用 Binary Refinery 反汇编:
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 中出现:
6J7-
V94-
3P6-
X5B-
C:\
D:\
ifps_disasm.txt 中出现:
external function __stdcall kernel32::GetVolumeInformationA(...)
function GENMACHINEID(Argument1: Integer): String
function GENREGCODE(Argument1: Integer): String
function CheckSerial(Serial: String): Boolean
这些字符串构成完整链:
磁盘卷序列号
-> 机器码函数
-> 注册码函数
-> 输入校验函数
7. 机器码来源分析
7.1 关键 API
脚本调用:
kernel32::GetVolumeInformationA
相关片段:
Call kernel32::GetVolumeInformationA
SetPtr LocalVar12 := GlobalVar0
含义:
GlobalVar0 = VolumeSerialNumber
即 GlobalVar0 保存磁盘卷序列号。
7.2 盘符选择逻辑
脚本先判断:
DirExists('C:\')
如果存在:
LocalVar5 := 'C:\'
否则:
LocalVar5 := 'D:\'
所以机器码通常来自 C:\ 的卷序列号。
7.3 不是哪些机器特征
没有发现以下证据:
GetAdaptersInfo
GetAdaptersAddresses
MachineGuid
CPUID
WMI
BIOS Serial
Motherboard Serial
因此不是 MAC 地址、CPU ID、主板 ID 或 Windows MachineGuid。
8. 机器码生成机制
8.1 函数
GENMACHINEID(Argument1: Integer): String
其中:
Argument1 = GlobalVar0 = volume_serial
8.2 常量
机器码 pivot:
1087244140
8.3 逻辑
对于 V94- 分支:
machine_diff = 1087244140 - volume_serial
然后把 machine_diff 编码为字符串。
8.4 编码规则
数字差值先转为十进制字符串。
例:
machine_diff = 1170189346
编码过程:
1170189346
-> 分块
-> 插入字母
-> 加横线
->机器码
字母计算:
letter = chr(65 + int(chunk) * 26 // 4653)
示例:
chunk = 701
65 + 701 * 26 // 4653 = 68
chr(68) = 'D'
所以:
701 -> D701
8.5 当前 Python 实现
在 inno_keygen.py 中:
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 初始误判
反汇编文本中出现过:
1979254653
一开始据此推导:
reg_diff = 1979254653 - volume_serial
但该结果和真实通过样本冲突。
9.2 样本校正
reg_diff = 278178833
因此:
reg_pivot = reg_diff + volume_serial
reg_pivot = 278178833 + (-82945206)
reg_pivot = 195233627
所以实际用于当前 GUI/样本路径的注册码 pivot 等价于:
195233627
9.3 当前注册码逻辑
对于 X5B- 分支:
reg_diff = 195233627 - volume_serial
然后用同类编码方式生成注册码。
当前实现:
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 函数
CheckSerial(Serial: String): Boolean
10.2 反汇编逻辑
核心流程:
LocalVar2 := GlobalVar0
Call GENREGCODE
Compare Argument1 == LocalVar1
伪代码:
def CheckSerial(user_input):
expected = GENREGCODE(GlobalVar0)
if user_input == expected:
return True
return uppercase(user_input) == uppercase(expected)
10.3 大小写处理
CheckSerial 中调用:
Uppercase
说明字母大小写可能不敏感。
但注意:
横线位置、数字顺序、前缀仍然必须正确。
11. 当前工具说明
11.1 文件
inno_keygen.py
11.2 使用方式
不带参数打开 GUI:
python .\inno_keygen.py
带机器码参数输出结果:
python .\inno_keygen.py 注册码
兼容旧式参数:
python .\inno_keygen.py --machine 注册码
python .\inno_keygen.py --gui
11.3 GUI 行为
GUI 功能:
-
输入机器码。
-
点击生成。
-
显示反推卷序列号。
-
显示注册码。
-
自动复制注册码到剪切板。
12. 风险评估
12.1 授权机制弱点
当前机制存在明显弱点:
-
机器码来源单一。
-
只绑定卷序列号。
-
常量和算法在客户端。
-
校验在客户端本地完成。
-
没有服务器参与。
-
没有非对称签名。
-
算法是线性差值 + 可逆编码。
12.2 可被复现的原因
攻击者或研究人员只需要:
提取 IFPS 字节码
定位函数
读取常量
观察真实样本
用 Python 复现
即可生成对应注册码。
13.设计更安全的授权系统
13.1 不要把 secret 放在客户端
错误设计:
客户端内置注册码生成算法和私有常量。
改进:
服务器保存 secret。
客户端只做验证或请求授权。
13.2 使用非对称签名
建议 license 结构:
{
"product": "example",
"user": "user_id",
"expires": "2026-12-31",
"features": ["base", "pro"],
"machine": "fingerprint"
}
服务器:
signature = Sign(private_key, payload)
客户端:
Verify(public_key, payload, signature)
13.3 服务端校验
对于重要产品,建议:
-
在线激活。
-
短期 token。
-
设备绑定。
-
吊销机制。
-
异常登录检测。
13.4 客户端抗篡改
客户端仍可能被 patch,因此可以增加:
-
完整性检查。
-
关键逻辑分散。
-
混淆。
-
反调试。
-
行为异常检测。
但这些只能提高成本,不能替代正确的授权架构。
14. 后续验证建议
为了进一步确认算法,建议继续收集更多真实样本:
机器码 -> 实际可通过注册码
建议样本至少覆盖:
-
多个
V94-机器码。 -
是否存在
6J7-机器码。 -
X5B-与3P6-是否都可能出现。 -
卷序列号为正数和负数的情况。
-
边界值附近:
-
volume_serial = 195233627 -
volume_serial = 1087244140
-
每个样本记录:
机器码
注册码
是否通过
系统盘符
GetVolumeInformationA 返回的卷序列号
18. 附录 A:关键命令
18.1 提取 Inno 安装包
.\tools\innoextract19\pkg\innoextract.exe --list .\Pets.exe
.\tools\innoextract19\pkg\innoextract.exe -d .\out19 --extract .\Pets.exe
18.2 反汇编 IFPS
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 搜索关键段
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 逻辑摘要
当前核心参数:
MACHINE_PIVOT = 1087244140
REG_PIVOT = 195233627
机器码反推:
volume_serial = MACHINE_PIVOT - machine_diff
注册码差值:
reg_diff = REG_PIVOT - volume_serial
编码:
letter = chr(65 + int(chunk) * 26 // 4653)
20. 附录 C:参考资料
-
Microsoft
GetVolumeInformationA:
https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationa -
Inno Setup Pascal Script:
https://documentation.help/inno-setup/topic_scriptintro.htm -
Inno Setup 脚本格式:
https://documentation.help/inno-setup/topic_scriptformatoverview.htm -
innoextract:
https://constexpr.org/innoextract/ -
innoextract GitHub:
https://github.com/dscharrer/innoextract -
Binary Refinery Inno 文档:
https://binref.github.io/lib/inno/index.html
源码如下
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()