IDM PATCH

IDM 6.43 Build 10 Disk Patch Script

仅供学习交流使用,请支持正版。

import argparse
import hashlib
import os
import shutil
import subprocess
import sys
import time

if os.name == "nt":
    for _s in (sys.stdout, sys.stderr):
        try:
            _s.reconfigure(encoding="utf-8", errors="replace")
        except (AttributeError, ValueError):
            pass

HERE = os.path.dirname(os.path.abspath(__file__))
BASE = 0x400C00                 # 文件偏移 = VA - BASE

# 系统工具目录:Windows 用 %SystemRoot%,WSL 下退回 /mnt/c。
SYS32 = (os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32")
         if os.name == "nt" else "/mnt/c/Windows/System32")


def off(va):
    return va - BASE


# —— 补丁点数据省略 ——
# (label, VA, ORIG, STUB):地址、原始字节、补丁字节,请自行逆向填写。
PATCHES = []

REG_KEY = r"Software\DownloadManager"
COMMON_DIRS = [
    r"C:\Program Files (x86)\Internet Download Manager",
    r"C:\Program Files\Internet Download Manager",
]

def _tool(*names):
    for n in names:
        p = shutil.which(n)
        if p:
            return p
    for n in names:
        if os.path.isabs(n) and os.path.exists(n):
            return n
    return None


REG = _tool("reg", "reg.exe", os.path.join(SYS32, "reg.exe"))
PWSH = _tool("powershell", "powershell.exe",
             os.path.join(SYS32, r"WindowsPowerShell\v1.0\powershell.exe"))
TASKKILL = _tool("taskkill", "taskkill.exe", os.path.join(SYS32, "taskkill.exe"))


def _run(cmd):
    try:
        return subprocess.run(cmd, capture_output=True, text=True, errors="replace")
    except OSError:
        return None


def _reg_get(hive, key, value):
    if os.name == "nt":
        try:
            import winreg
            h = {"HKCU": winreg.HKEY_CURRENT_USER,
                 "HKLM": winreg.HKEY_LOCAL_MACHINE}[hive]
            with winreg.OpenKey(h, key) as k:
                return winreg.QueryValueEx(k, value)[0]
        except OSError:
            return None
    if not REG:
        return None
    r = _run([REG, "query", "%s\\%s" % (hive, key), "/v", value])
    if not r or r.returncode != 0:
        return None
    for line in r.stdout.splitlines():
        parts = line.split(None, 2)
        if len(parts) == 3 and parts[0] == value:
            return parts[2].strip()
    return None


def _proc_path():
    if not PWSH:
        return None
    r = _run([PWSH, "-NoProfile", "-Command",
              "(Get-Process IDMan -ErrorAction SilentlyContinue | "
              "Select-Object -First 1).Path"])
    if r and r.returncode == 0:
        return r.stdout.strip() or None
    return None


def _to_native(p):
    if not p or os.name == "nt" or p.startswith("/"):
        return p
    w = shutil.which("wslpath")
    if not w:
        return p
    r = _run([w, "-u", p])
    return (r.stdout.strip() if r and r.returncode == 0 else p)


def _exe_in(p):
    """目录 -> 里面的 idman.exe(大小写不敏感);文件 -> 原样返回。"""
    if p and os.path.isdir(p):
        for n in os.listdir(p):
            if n.lower() == "idman.exe":
                return os.path.join(p, n)
        return os.path.join(p, "IDMan.exe")
    return p


def find_idm(path=None):
    if path:
        return _exe_in(_to_native(path.strip('"')))
    cands = []
    exe = _reg_get("HKCU", REG_KEY, "ExePath")
    if exe:
        cands.append(exe)
    loc = _reg_get(
        "HKLM",
        r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
        r"\Internet Download Manager",
        "InstallLocation")
    if loc:
        cands.append(os.path.join(loc, "IDMan.exe"))
    exe = _proc_path()
    if exe:
        cands.append(exe)
    cands += [os.path.join(d, "IDMan.exe") for d in COMMON_DIRS]
    cands.append(os.path.join(HERE, "IDMan.exe"))
    for c in cands:
        n = _to_native(c)
        if n and os.path.exists(n):
            return n
    return None


def stop_idm():
    if TASKKILL:
        _run([TASKKILL, "/F", "/IM", "IDMan.exe"])
        time.sleep(1)  # 等文件句柄释放,写入失败时下面还有重试兜底


def _is_admin():
    if os.name != "nt":
        return True
    try:
        import ctypes
        return bool(ctypes.windll.shell32.IsUserAnAdmin())
    except Exception:
        return True


def _try_elevate():
    """写 exe 被日时,用 UAC 以管理员在新窗口重跑本脚本(保留原参数)。"""
    if os.name != "nt" or _is_admin():
        return False
    import ctypes
    script = os.path.abspath(sys.argv[0])
    inner = subprocess.list2cmdline([sys.executable, script] + sys.argv[1:])
    try:
        # cmd 规则:首字符为引号时会剥掉首尾引号,故外层只包一对。
        rc = ctypes.windll.shell32.ShellExecuteW(
            None, "runas", os.path.join(SYS32, "cmd.exe"),
            '/k "{}"'.format(inner), None, 1)
    except Exception:
        return False
    if rc > 32:
        print("[+] 已请求管理员权限,正在新窗口重跑(UAC 弹窗请点『是』)...")
        return True
    print("[!] 提权被拒绝(UAC)。请右键『以管理员身份运行』后重试。")
    return False


def restore_file(exe):
    """从 exe.bak 还原原版文件。"""
    bak = exe + ".bak"
    if not os.path.exists(bak):
        print("[!] 没有备份 %s,无法还原" % bak)
        return "no-bak"
    for attempt in range(10):
        try:
            shutil.copy2(bak, exe)
            break
        except PermissionError:
            if attempt == 9:
                print("[!] 无法写入 %s(被占用或无权限)。" % exe)
                print("    请彻底退出 IDM(含托盘),或右键以管理员身份重跑。")
                return "locked"
            time.sleep(0.5)
    with open(exe, "rb") as f:
        md5 = hashlib.md5(f.read()).hexdigest()
    print("[+] 已从 %s 还原原版" % bak)
    print("[+] md5 %s" % md5)
    return "restored"


def patch_file(exe, write=True):
    bak = exe + ".bak"
    data = bytearray(open(exe, "rb").read())
    todo = []
    for label, va, orig, new in PATCHES:
        o = off(va)
        cur = bytes(data[o:o + len(new)])
        if cur == new:
            print("[=] %-38s already patched" % label)
            continue
        if bytes(data[o:o + len(orig)]) != orig:
            print("[!] unsupported at %#x (%s): %s" % (o, label, bytes(data[o:o + 16]).hex()))
            print("    re-analyze and update PATCHES in this script.")
            return "unsupported"
        todo.append((o, new, label))
    if not todo:
        return "patched"
    if not write:
        for o, new, label in todo:
            print("[~] %-38s would patch file %#x" % (label, o))
        return "dry"
    if not os.path.exists(bak):
        shutil.copy2(exe, bak)
        print("[+] backup ->", bak)
    for o, new, label in todo:
        data[o:o + len(new)] = new
        print("[+] %-38s file %#x" % (label, o))
    for attempt in range(10):
        try:
            with open(exe, "wb") as f:
                f.write(data)
            break
        except PermissionError:
            if attempt == 9:
                print("[!] 无法写入 %s(被占用或无权限)。" % exe)
                print("    请彻底退出 IDM(含托盘),或右键以管理员身份重跑。")
                return "locked"
            time.sleep(0.5)
    print("[+] patch OK. md5 %s" % hashlib.md5(bytes(data)).hexdigest())
    return "ok"


def main():
    ap = argparse.ArgumentParser(
        description="IDM 6.43b10 disk patch")
    ap.add_argument("--dir", "--install-dir", dest="dir", metavar="PATH",
                    help="IDM 安装目录或 IDMan.exe 路径(默认自动检测)")
    ap.add_argument("--restore", action="store_true",
                    help="还原原版 exe(从 .bak 回滚)")
    ap.add_argument("--dry-run", action="store_true", help="check only")
    a = ap.parse_args()

    exe = find_idm(a.dir)
    if not exe or not os.path.exists(exe):
        print("[!] 未找到 IDMan.exe;请用 --dir 指定安装目录或 IDMan.exe 路径")
        return 2
    print("[*] target:", exe)

    if a.restore:
        if a.dry_run:
            print("[~] would restore from %s" % (exe + ".bak"))
            return 0
        stop_idm()
        st = restore_file(exe)
        if st == "locked":
            return 0 if _try_elevate() else 1
        return 0 if st == "restored" else 1

    if not a.dry_run:
        stop_idm()

    state = patch_file(exe, write=not a.dry_run)
    if state == "locked":
        return 0 if _try_elevate() else 1
    if state == "unsupported":
        return 1

    if a.dry_run:
        print("[*] dry-run: no write")
    return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        print("\n[!] 已中断")
        sys.exit(130)