吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 120|回复: 5
上一主题 下一主题
收起左侧

[原创] Sublime Text 4 4206 ai逆的

[复制链接]
跳转到指定楼层
楼主
akirayo 发表于 2026-8-1 16:29 回帖奖励

全网没找到4206的补丁 只能让opus安全卫士动手了.



[Python] 纯文本查看 复制代码
#!/usr/bin/env python3
"""
Sublime Text 4 (Build 4206) license patcher
- 生成随机 RSA1024 e=17 密钥对
- patch exe: 公钥替换, 验签NOP, 弹窗NOP, 状态强制, 标题清理
- 生成 license 文件
用法: python patch_sublime.py [sublime_text.exe 路径]
"""
import os, sys, shutil, subprocess, re

# ============ 配置 (Build 4206, x64, file size 7918528) ============
BUILD = 4206

# --- 公钥 ---
PUBKEY_OFFSET = 0x6BD312        # 320 hex ASCII chars (160 bytes DER)
PUBKEY_LEN = 320

# --- 验签核心函数 ---
# 0x4E8A37: 原 push r15;push r14;push r13;push r12;push rsi (41 57 41 56 41 55 41 54 56)
# patch: mov rax,[rsp+0x30]; mov byte [rax],1; ret
# 效果: 第6参数(结果指针) *result = 1 (registered), 立即返回
VERIFY_FUNC_OFFSET = 0x4E8A37
ORIG_VERIFY_HEAD = bytes.fromhex("415741564155415456")
VERIFY_FUNC_PATCH = bytes.fromhex("488b442430c60001c3")

# --- is_registered 函数 (0xFCF7A) ---
# 原: mov rax,[rcx+8]; cmp byte[rax+8],1; jnz+4; mov al,[rax+10]; ret; xor eax,eax; ret
# patch: mov al,1; ret + nop padding (总是返回 1 = registered)
IS_REGISTERED_OFFSET = 0xFCF7A
ORIG_IS_REGISTERED = bytes.fromhex("488b41088078080175048a4010c331c0c3")  # 17 bytes
IS_REGISTERED_PATCH = bytes.fromhex("b001c3" + "90" * 14)  # mov al,1; ret + 14 nops

# --- is_unregistered 函数 (0xFCF60) ---
# 原: mov al,[rax+8]; xor al,1; ret (返回 !state)
# patch: xor al,al; ret (总是返回 0 = NOT unregistered)
IS_UNREGISTERED_OFFSET = 0xFCF60
ORIG_IS_UNREGISTERED = bytes.fromhex("8a40083401c3")
IS_UNREGISTERED_PATCH = bytes.fromhex("30c0c3909090")  # xor al,al; ret; nop*3

# --- 启动路径: 第2个 JNZ (Enter License vs registered path) ---
# 0x31A93: 75 0A (jnz +0xa) -> 90 90 (NOP, 强制 registered path)
DIALOG_JNZ_OFFSET = 0x31A93
ORIG_DIALOG_JNZ = bytes.fromhex("750a")
DIALOG_JNZ_PATCH = bytes.fromhex("9090")

# --- 窗口创建函数内部状态检查 ---
# 0xF73FF: 0F 84 EC 01 00 00 (jz +0x1EC, 跳过窗口创建) -> NOP*6
WNDCREATE_JZ_OFFSET = 0xF73FF
ORIG_WNDCREATE_JZ = bytes.fromhex("0f84ec010000")
WNDCREATE_JZ_PATCH = bytes.fromhex("909090909090")

# --- 标题 "(UNREGISTERED)" 清除 ---
# XOR-encoded "UNREGISTERED" decoded char-by-char in title function
# 跳过解码循环: 0x1E120E: 74 26 (jz +0x26) -> EB 26 (jmp +0x26, 无条件跳过)
TITLE_LOOP_OFFSET = 0x1E120E
ORIG_TITLE_LOOP = bytes.fromhex("7426")
TITLE_LOOP_PATCH = bytes.fromhex("eb26")  # jmp (always skip decode loop)

# 跳过 " (" 前缀: 0x1E116D flag test jz
# 原: 40 f6 c7 3e; 74 11 -> 74 11 改 EB 11 (jmp, 总是跳过)
TITLE_PAREN_OPEN_OFFSET = 0x1E1171  # the JZ byte
ORIG_TITLE_PAREN_OPEN = bytes.fromhex("7411")
TITLE_PAREN_OPEN_PATCH = bytes.fromhex("eb11")

# 跳过 ")" 后缀: 0x1E1236 flag test jz
TITLE_PAREN_CLOSE_OFFSET = 0x1E123A  # the JZ byte
ORIG_TITLE_PAREN_CLOSE = bytes.fromhex("7411")
TITLE_PAREN_CLOSE_PATCH = bytes.fromhex("eb11")

# --- About 对话框: 强制 "Registered to" 路径 ---
# 0x30008: 75 4A (JNZ, state!=1 跳到 Unregistered) -> 90 90 (NOP, 强制走 Registered 路径)
ABOUT_JNZ_OFFSET = 0x30008
ORIG_ABOUT_JNZ = bytes.fromhex("754a")
ABOUT_JNZ_PATCH = bytes.fromhex("9090")

# --- About 对话框: "Registered to " -> 自定义文本 ---
# 该文本是一个 [begin, end) 字符串区间, 由两条 LEA 分别加载:
#   0x30022: lea rax,[rip+disp]  disp32 @ 0x30025, next_rip(file)=0x30029  -> begin
#   0x30034: lea rax,[rip+disp]  disp32 @ 0x30037, next_rip(file)=0x3003B  -> end
# 原始: begin=0x6C2668 ("Registered to "), end=0x6C2676 (=begin+14)
#
# 注意: 只改 begin 会让 begin==end 变成空串, 整行会被布局直接跳过(不渲染),
#       所以 begin 和 end 必须同时重定向。license 文件里的 name 不参与这一行 ——
#       验签函数被 stub 后 license 对象的 name 字段始终为空, 从来渲染不出来。
ABOUT_SPAN_BEGIN_DISP_OFFSET = 0x30025  # begin 的 disp32
ABOUT_SPAN_END_DISP_OFFSET = 0x30037    # end 的 disp32
ORIG_ABOUT_SPAN_BEGIN_DISP = bytes.fromhex("3f2a6900")
ORIG_ABOUT_SPAN_END_DISP = bytes.fromhex("3b2a6900")
# disp = 目标文件偏移 - 指令下一条的文件偏移 + RVA_DELTA
ABOUT_SPAN_BEGIN_NEXT_RIP = 0x30029
ABOUT_SPAN_END_NEXT_RIP = 0x3003B
RVA_DELTA = 0x400                       # 该 section 内 RVA 与文件偏移之差

# About 自定义文本存放的 .data cave (紧跟搬迁后的 "%d User License")
ABOUT_TEXT_CAVE_OFFSET = 0x6E91F5
ABOUT_TEXT_CAVE_LIMIT = 64              # 该 cave 有 128 字节全 0, 留一半足够

# --- About 窗口高度: 文本行数 ---
# 0x2FF04: BD 04 00 00 00 (mov ebp, 4) -> 立即数 @ 0x2FF05
# 该处早于 0x30008 的状态判断, 且我们没有改写 [rbx+8] 的原始状态字节,
# 所以实际走的是 ebp=4 这条路径。多渲染一行 (自定义文本) 后窗口会矮一行,
# 最后一行 "Dev Channel, Build ####" 被裁掉 -> 行数改成 5。
ABOUT_LINES_IMM_OFFSET = 0x2FF05
ORIG_ABOUT_LINES = 4
ABOUT_LINES = 5

# --- About 对话框: "Unlimited User License" -> 自定义文本 ---
# 直接覆写 .rdata 的字符串 (file 0x6AF930), 27+1 字节会覆盖相邻的 "%d User License"
# 所以把 "%d User License" 搬到 .data cave, 重定向其 LEA
ABOUT_STR_OFFSET = 0x6AF930             # "Unlimited User License" 原位置
ABOUT_STR_ORIG_LEN = 22                 # 原字符串长度 (不含 null)
# "%d User License" 搬迁: 原在 0x6AF947, LEA at 0x4DED65 (4C 8D 05 XX XX XX XX)
DUSER_STR_CAVE_OFFSET = 0x6E91E5       # .data cave 放 "%d User License"
DUSER_LEA_DISP_OFFSET = 0x4DED68       # LEA disp32 位置
ORIG_DUSER_LEA_DISP = bytes.fromhex("db0f1d00")
DUSER_LEA_DISP_PATCH = bytes.fromhex("79a82000")  # -> cave RVA 0x6EA1E5

# --- License 信息 ---
# ABOUT_TEXT: About 窗口里显示的第一行 (原 "Registered to " 的位置)
#   直接写进 exe 的 .data cave, 与 license 文件无关, 长度不受 14 字节限制,
#   任意 UTF-8 都可以 (含 U+30FB "・" 这类字符, 实测正常渲染)
ABOUT_TEXT = "Ciallo~(∠&#12539;ω< )⌒☆"
LICENSE_NAME = "Ciallo~(∠&#12539;ω< )⌒☆"
LICENSE_TYPE = "Unlimited User License"
LICENSE_PRODUCT = "EA7E-1000"

NOP5 = bytes.fromhex("9090909090")
NOP6 = bytes.fromhex("909090909090")


def find_exe():
    """找 sublime_text.exe"""
    if len(sys.argv) >= 2:
        return os.path.abspath(sys.argv[1])
    candidates = [
        r"D:\Program Files\Sublime Text 4\sublime_text.exe",
        r"D:\tools\windows\Sublime Text\sublime_text.exe",
        r"C:\Program Files\Sublime Text\sublime_text.exe",
        r"C:\Program Files (x86)\Sublime Text\sublime_text.exe",
    ]
    for c in candidates:
        if os.path.exists(c):
            return c
    for base in [r"D:\Program Files", r"C:\Program Files", r"C:\Program Files (x86)"]:
        if not os.path.exists(base):
            continue
        for root, dirs, files in os.walk(base):
            if "sublime_text.exe" in files:
                return os.path.join(root, "sublime_text.exe")
            if root[len(base):].count(os.sep) >= 3:
                dirs[:] = []
    return None


def check_build(exe_path):
    """验证 build (优先用 .bak 原版)"""
    check = exe_path + ".bak" if os.path.exists(exe_path + ".bak") else exe_path
    try:
        r = subprocess.run([check, "--version"], capture_output=True, text=True, timeout=10)
        out = (r.stdout + r.stderr).strip()
        if str(BUILD) not in out:
            print(f"[!] 警告: 目标 build 不是 {BUILD}: {out}")
            return False
        print(f"[+] 确认 build {BUILD}: {out}")
        return True
    except Exception as e:
        print(f"[!] 无法验证 build: {e}")
        return False


def ensure_crypto():
    """确保 pycryptodome 已安装"""
    try:
        from Crypto.PublicKey import RSA
        return True
    except ImportError:
        print("[*] 安装 pycryptodome...")
        subprocess.run([sys.executable, "-m", "pip", "install", "pycryptodome"], check=True)
        return True


def gen_keypair():
    """生成 RSA1024 e=17 密钥对, 公钥 DER 必须 160 bytes"""
    from Crypto.PublicKey import RSA
    print("[*] 生成 RSA1024 e=17 密钥对...")
    for i in range(200):
        key = RSA.generate(1024, e=17)
        pub_der = key.publickey().export_key(format="DER", pkcs=8)
        if len(pub_der) == 160:
            print(f"[+] 密钥生成成功 (第 {i+1} 次)")
            return key, pub_der
    raise RuntimeError("200 次未找到 160 bytes 公钥")


def patch_exe(exe_path, pub_der):
    """patch exe: 公钥 + 验签 + 状态函数 + 启动路径 + 窗口创建 + 标题

    流程: sublime_text.exe.bak = 原版, sublime_text.exe = 修改后
    """
    bak = exe_path + ".bak"

    # 备份原版 (仅首次)
    if not os.path.exists(bak):
        shutil.copy(exe_path, bak)
        print(f"[+] 备份原版: {bak}")
    else:
        print(f"[*] 原版备份已存在: {bak}")

    # 始终从原版读取
    data = bytearray(open(bak, "rb").read())

    # 验证文件大小
    if len(data) != 7918528:
        print(f"[!] 警告: 文件大小 {len(data)} != 7918528, 可能版本不对")

    # 验证公钥偏移
    orig_pub = data[PUBKEY_OFFSET:PUBKEY_OFFSET+PUBKEY_LEN].decode("ascii", errors="replace")
    if not re.fullmatch(r'[0-9A-Fa-f]{320}', orig_pub):
        raise RuntimeError(f"偏移 0x{PUBKEY_OFFSET:x} 处不是 320 hex 公钥")

    # Patch 公钥
    new_pub_hex = pub_der.hex().upper()
    data[PUBKEY_OFFSET:PUBKEY_OFFSET+PUBKEY_LEN] = new_pub_hex.encode("ascii")
    print(f"[+] 公钥替换: 0x{PUBKEY_OFFSET:x}")

    # Patch 验签函数 -> set *result=1; ret
    data[VERIFY_FUNC_OFFSET:VERIFY_FUNC_OFFSET+len(VERIFY_FUNC_PATCH)] = VERIFY_FUNC_PATCH
    print(f"[+] 验签函数: 0x{VERIFY_FUNC_OFFSET:x} -> *result=1; ret")

    # Patch is_registered -> always return 1
    data[IS_REGISTERED_OFFSET:IS_REGISTERED_OFFSET+len(IS_REGISTERED_PATCH)] = IS_REGISTERED_PATCH
    print(f"[+] is_registered: 0x{IS_REGISTERED_OFFSET:x} -> ret 1")

    # Patch is_unregistered -> always return 0
    data[IS_UNREGISTERED_OFFSET:IS_UNREGISTERED_OFFSET+len(IS_UNREGISTERED_PATCH)] = IS_UNREGISTERED_PATCH
    print(f"[+] is_unregistered: 0x{IS_UNREGISTERED_OFFSET:x} -> ret 0")

    # Patch 启动 JNZ -> NOP (force registered path)
    data[DIALOG_JNZ_OFFSET:DIALOG_JNZ_OFFSET+2] = DIALOG_JNZ_PATCH
    print(f"[+] 启动 JNZ->NOP: 0x{DIALOG_JNZ_OFFSET:x}")

    # Patch 窗口创建 JZ -> NOP (force window creation)
    data[WNDCREATE_JZ_OFFSET:WNDCREATE_JZ_OFFSET+6] = WNDCREATE_JZ_PATCH
    print(f"[+] 窗口创建 JZ->NOP: 0x{WNDCREATE_JZ_OFFSET:x}")

    # Patch 标题 UNREGISTERED 解码循环 -> jmp (skip)
    data[TITLE_LOOP_OFFSET:TITLE_LOOP_OFFSET+2] = TITLE_LOOP_PATCH
    print(f"[+] 标题循环 jz->jmp: 0x{TITLE_LOOP_OFFSET:x}")

    # Patch 标题 "(" 前缀 -> jmp (skip)
    data[TITLE_PAREN_OPEN_OFFSET:TITLE_PAREN_OPEN_OFFSET+2] = TITLE_PAREN_OPEN_PATCH
    print(f"[+] 标题 '(' skip: 0x{TITLE_PAREN_OPEN_OFFSET:x}")

    # Patch 标题 ")" 后缀 -> jmp (skip)
    data[TITLE_PAREN_CLOSE_OFFSET:TITLE_PAREN_CLOSE_OFFSET+2] = TITLE_PAREN_CLOSE_PATCH
    print(f"[+] 标题 ')' skip: 0x{TITLE_PAREN_CLOSE_OFFSET:x}")

    # Patch About 对话框 JNZ -> NOP (强制 Registered 路径)
    data[ABOUT_JNZ_OFFSET:ABOUT_JNZ_OFFSET+2] = ABOUT_JNZ_PATCH
    print(f"[+] About JNZ->NOP: 0x{ABOUT_JNZ_OFFSET:x} (强制 Registered 路径)")

    # Patch About 第一行文本: 写入 .data cave, begin/end 两条 LEA 同时重定向
    text_bytes = ABOUT_TEXT.encode("utf-8")
    if len(text_bytes) + 1 > ABOUT_TEXT_CAVE_LIMIT:
        raise RuntimeError(f"ABOUT_TEXT 过长: {len(text_bytes)} > {ABOUT_TEXT_CAVE_LIMIT - 1}")
    data[ABOUT_TEXT_CAVE_OFFSET:ABOUT_TEXT_CAVE_OFFSET+ABOUT_TEXT_CAVE_LIMIT] = b"\x00" * ABOUT_TEXT_CAVE_LIMIT
    data[ABOUT_TEXT_CAVE_OFFSET:ABOUT_TEXT_CAVE_OFFSET+len(text_bytes)] = text_bytes

    disp_begin = ABOUT_TEXT_CAVE_OFFSET - ABOUT_SPAN_BEGIN_NEXT_RIP + RVA_DELTA
    disp_end = ABOUT_TEXT_CAVE_OFFSET + len(text_bytes) - ABOUT_SPAN_END_NEXT_RIP + RVA_DELTA
    data[ABOUT_SPAN_BEGIN_DISP_OFFSET:ABOUT_SPAN_BEGIN_DISP_OFFSET+4] = disp_begin.to_bytes(4, "little")
    data[ABOUT_SPAN_END_DISP_OFFSET:ABOUT_SPAN_END_DISP_OFFSET+4] = disp_end.to_bytes(4, "little")
    print(f"[+] About 第一行: cave 0x{ABOUT_TEXT_CAVE_OFFSET:x} <- {ABOUT_TEXT!r} ({len(text_bytes)} bytes)")
    print(f"    span begin disp=0x{disp_begin:08x} @0x{ABOUT_SPAN_BEGIN_DISP_OFFSET:x}")
    print(f"    span end   disp=0x{disp_end:08x} @0x{ABOUT_SPAN_END_DISP_OFFSET:x}")

    # Patch About 窗口行数 (4 -> 5), 否则最后一行 "Dev Channel, Build ####" 被裁掉
    if data[ABOUT_LINES_IMM_OFFSET] != ORIG_ABOUT_LINES:
        print(f"[!] 警告: 0x{ABOUT_LINES_IMM_OFFSET:x} 原值不是 {ORIG_ABOUT_LINES}")
    data[ABOUT_LINES_IMM_OFFSET] = ABOUT_LINES
    print(f"[+] About 行数: 0x{ABOUT_LINES_IMM_OFFSET:x} {ORIG_ABOUT_LINES} -> {ABOUT_LINES} (修正窗口高度)")

    # Patch About 第二行: 覆写 "Unlimited User License" + 搬迁相邻字符串
    # 1) 把 "%d User License" 搬到 .data cave, 重定向其 LEA
    duser_str = b"%d User License\x00"
    data[DUSER_STR_CAVE_OFFSET:DUSER_STR_CAVE_OFFSET+len(duser_str)] = duser_str
    data[DUSER_LEA_DISP_OFFSET:DUSER_LEA_DISP_OFFSET+4] = DUSER_LEA_DISP_PATCH
    # 2) 覆写原位置 (0x6af930) 为我们的文本
    about_bytes = LICENSE_TYPE.encode("utf-8") + b"\x00"
    data[ABOUT_STR_OFFSET:ABOUT_STR_OFFSET+len(about_bytes)] = about_bytes
    print(f"[+] About 文本: 0x{ABOUT_STR_OFFSET:x} <- {LICENSE_TYPE!r}")

    # 写入: 修改后 -> sublime_text.exe (原版保留在 .bak)
    open(exe_path, "wb").write(data)
    print(f"[+] 写入修改后: {exe_path}")


def gen_license(priv_key):
    """生成 license 文件"""
    from Crypto.Signature import pkcs1_15
    from Crypto.Hash import SHA1
    data_to_sign = "\n".join([LICENSE_NAME, LICENSE_TYPE, LICENSE_PRODUCT]).encode("utf-8")
    sig = pkcs1_15.new(priv_key).sign(SHA1.new(data_to_sign))
    sig_hex = sig.hex()
    sig_lines = [sig_hex[i:i+32] for i in range(0, len(sig_hex), 32)]
    assert len(sig_lines) == 8, f"签名行数不对: {len(sig_lines)}"
    lines = ["----- BEGIN LICENSE -----", LICENSE_NAME, LICENSE_TYPE, LICENSE_PRODUCT] + sig_lines + ["------ END LICENSE ------"]
    return "\n".join(lines) + "\n"


def write_license(license_text, exe_path):
    """写 license 到 exe 同目录 Data/Local/ 和 APPDATA (UTF-8)"""
    # 写到 exe 旁边的 Data/Local/
    exe_dir = os.path.dirname(exe_path)
    local_dir = os.path.join(exe_dir, "Data", "Local")
    os.makedirs(local_dir, exist_ok=True)
    lic_path = os.path.join(local_dir, "License.sublime_license")
    with open(lic_path, "w", newline="\n", encoding="utf-8") as f:
        f.write(license_text)
    print(f"[+] license: {lic_path}")

    # 也写到 APPDATA
    appdata_dir = os.path.expandvars(r"%APPDATA%\Sublime Text\Local")
    os.makedirs(appdata_dir, exist_ok=True)
    appdata_lic = os.path.join(appdata_dir, "License.sublime_license")
    with open(appdata_lic, "w", newline="\n", encoding="utf-8") as f:
        f.write(license_text)
    print(f"[+] license: {appdata_lic}")


def main():
    print("=" * 60)
    print(f"Sublime Text 4 (Build {BUILD}) Patcher")
    print("=" * 60)

    exe_path = find_exe()
    if not exe_path:
        print("[!] 未找到 sublime_text.exe")
        print(f"    python {sys.argv[0]} <path-to-sublime_text.exe>")
        sys.exit(1)
    print(f"[+] 目标: {exe_path}")

    check_build(exe_path)
    ensure_crypto()

    priv_key, pub_der = gen_keypair()

    try:
        patch_exe(exe_path, pub_der)
    except PermissionError:
        print("[!] 文件被锁,请先关闭 Sublime Text")
        sys.exit(1)

    license_text = gen_license(priv_key)
    print(f"\n[*] License:\n{license_text}")
    write_license(license_text, exe_path)

    print("\n" + "=" * 60)
    print("[+] 完成!")
    print(f"    原版: {exe_path}.bak")
    print(f"    修改: {exe_path}")
    print("=" * 60)


if __name__ == "__main__":
    main()


发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
s3rious74 发表于 2026-8-3 14:59
可以讨论一下呀,关于opus,我在使用opus5的时候经常会它自己运行到一半就触碰到安全护栏,4.8倒是可以继续。不知道你使用的是5还是4.8呀。
3#
 楼主| akirayo 发表于 2026-8-4 02:13 |楼主
s3rious74 发表于 2026-8-3 14:59
可以讨论一下呀,关于opus,我在使用opus5的时候经常会它自己运行到一半就触碰到安全护栏,4.8倒是可以继续 ...

一般用5 渠道不可用的时候才会用4.8
4#
s3rious74 发表于 2026-8-4 08:22
akirayo 发表于 2026-8-4 02:13
一般用5 渠道不可用的时候才会用4.8

我靠,我感觉opus5的安全护栏很高,我但凡说学习逆向之类的,总是报 safeguards flagged this message.sometimes happens with safe..........我被杀疯了。。。。你这个解锁没有被safeguards啊
5#
music654 发表于 2026-8-4 08:57
AI逆的有过程吗?能不能给人工逆提供参考
6#
mhfymn 发表于 2026-8-4 09:12
感谢分享,非常受用。请问能否做成一个通用dll补丁?只能每个版本打补丁么?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-6 05:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表