IDA Pro headless 配置激活

donnad 2026-07-03 20:36 1

找了下9.3.260213版本的keygen,发现没有patch libidalib.so的,导致idalib-mcp运行会报错License not yet accepted, cannot run in batch mode,就让ai改了下

(仅测试linux环境)


正常安装完ida pro,执行node keygen.js,然后安装headless mcp


export IDA_INSTALL_DIR=/opt/ida
export VIRTUAL_ENV=/opt/venv
set -euo pipefail \
&& python3 -m venv "${VIRTUAL_ENV}" \
&& source "${VIRTUAL_ENV}/bin/activate" \
&& pip install --no-cache-dir --upgrade pip \
&& python "${IDA_INSTALL_DIR}/idalib/python/py-activate-idalib.py" -d "${IDA_INSTALL_DIR}" \
&& pip install --no-cache-dir "${IDA_INSTALL_DIR}/idalib/python/idapro-"*.whl \
&& pip install https://github.com/mrexodia/ida-pro-mcp/archive/refs/heads/main.zip \
&& python -c "import ida_pro_mcp, idapro" \
&& idalib-mcp --help >/dev/null
mkdir -p /root/.idapro \
&& install -m 600 "${IDA_INSTALL_DIR}/idapro.hexlic" /root/.idapro/ida.hexlic

idalib-mcp --host 0.0.0.0 --port 8745



keygen.js

const fs = require('fs');
const crypto = require('crypto');

const license = {
header: { version: 1 },
payload: {
name: 'auth',
email: '[email protected]',
licenses: [
{
description: 'license',
edition_id: 'ida-pro',
id: '14-0000-FFFF-88',
license_type: 'named',
product: 'IDA',
seats: 1,
start_date: '2024-08-10 00:00:00',
end_date: '2033-12-31 23:59:59',
issued_on: '2025-07-20 00:00:00',
owner: 'auth',
product_id: 'IDAPRO',
product_version: '9.3',
add_ons: [],
features: [],
}
],
},
};

function addons(license) {
var addons = [ //update as needed, doesnt include cloud addons
'LUMINA', 'TEAMS',
'HEXX86', 'HEXX64', 'HEXARM', 'HEXARM64',
'HEXMIPS', 'HEXMIPS64', 'HEXPPC', 'HEXPPC64',
'HEXRV', 'HEXRV64', 'HEXARC', 'HEXARC64',
'HEXV850'

];

addons.forEach((addon, i) => {
license.payload.licenses[0].add_ons.push({
id: `48-1337-B00B-${String(i + 1).padStart(2, '0')}`,
code: addon,
owner: license.payload.licenses[0].id,
start_date: '2025-07-20 00:00:00',
end_date: '2033-12-31 23:59:59',
});
});
}
addons(license);

// --- Helper funcs ---
//recursive sorting into json string
function sort(obj) {
if (Array.isArray(obj)) {
return '[' + obj.map(sort).join(',') + ']';
} else if (obj && typeof obj === 'object' && obj !== null) {
var keys = Object.keys(obj).sort();
return '{' + keys.map(k => '"' + k + '":' + sort(obj[k])).join(',') + '}';
} else {
return JSON.stringify(obj);
}
}

const cModulus = Buffer.from("edfd42cbf978546e8911225884436c57140525650bcf6ebfe80edbc5fb1de68f4c66c29cb22eb668788afcb0abbb718044584b810f8970cddf227385f75d5dddd91d4f18937a08aa83b28c49d12dc92e7505bb38809e91bd0fbd2f2e6ab1d2e33c0c55d5bddd478ee8bf845fcef3c82b9d2929ecb71f4d1b3db96e3a8e7aaf93", "hex");
const privateKey = Buffer.from("77c86abbb7f3bb134436797b68ff47beb1a5457816608dbfb72641814dd464dd640d711d5732d3017a1c4e63d835822f00a4eab619a2c4791cf33f9f57f9c2ae4d9eed9981e79ac9b8f8a411f68f25b9f0c05d04d11e22a3a0d8d4672b56a61f1532282ff4e4e74759e832b70e98b9d102d07e9fb9ba8d15810b144970029874", "hex");

function encrypt(message) {
let modulusBuf = 0n;
for (let i = cModulus.length - 1; i >= 0; i--)
modulusBuf = (modulusBuf << 8n) + BigInt(cModulus[i]);

let keyBuf = 0n;
for (let i = privateKey.length - 1; i >= 0; i--)
keyBuf = (keyBuf << 8n) + BigInt(privateKey[i]);

var reversed = Buffer.from(message).reverse();

let msgBuf = 0n;
for (let i = reversed.length - 1; i >= 0; i--)
msgBuf = (msgBuf << 8n) + BigInt(reversed[i]);

let base = msgBuf % modulusBuf, exponent = keyBuf, modulus = modulusBuf, encryptedBigInt = 1n;
while (exponent > 0n) {
if (exponent % 2n === 1n)
encryptedBigInt = (encryptedBigInt * base) % modulus;
exponent >>= 1n; base = (base * base) % modulus;
}

var bytes = [];
for (let buffer = encryptedBigInt; buffer > 0n; buffer >>= 8n) bytes.push(Number(buffer & 0xFFn));
return Buffer.from(bytes);
}

function patch(filePath, search, replace) {
try {
const buf = fs.readFileSync(filePath);
const idx = buf.indexOf(search);
if (idx !== -1) {
buf.set(replace, idx);
fs.writeFileSync(filePath, buf);
console.log(`Patched ${filePath}`);
return;
}
else {
console.error(`Pattern not found in ${filePath}`);
return;
}
} catch (err) {
console.error(`Error reading or writing file: ${filePath}`);
console.error('Elevated permissions given?')
return;
}
}

function sign(payload) {
var data = { payload };
var dataStr = sort(data);

var buffer = Buffer.alloc(128, 0);
buffer.fill(0x42, 0, 33); // first 33 bytes filled with 0x42

var hash = crypto.createHash('sha256').update(dataStr).digest();
hash.copy(buffer, 33); // copy hash after first 33 bytes

var encrypted = encrypt(buffer);
return encrypted.toString('hex').toUpperCase();
}
license.signature = sign(license.payload);

// --- Write License to file ---
fs.writeFileSync('idapro.hexlic', sort(license));
console.log('License written to idapro.hexlic');

// --- Patcher ---
const search = Buffer.from('EDFD425CF978', 'hex');
const replace = Buffer.from('EDFD42CBF978', 'hex');

["ida.dll", "ida32.dll", "libida.so", "libida32.so", "libida.dylib", "libida32.dylib"].forEach(file => {
if (fs.existsSync(file)) {
console.log(`Patching ${file}...`);
patch(file, search, replace);
}
});

// --- libidalib.so EULA gate (arch-aware auto-discovery) ---
// Bypass "License not yet accepted, cannot run in batch mode" so idalib runs
// headlessly. The gate reads the "EULA" registry value and conditionally
// branches to the accepted path; we locate that branch via the unique error
// string and flip it to unconditional. Supports x86_64 and aarch64 builds.
function patchEulaGate(filePath) {
const buf = fs.readFileSync(filePath);
if (buf[0]!==0x7f||buf[1]!==0x45||buf[2]!==0x4c||buf[3]!==0x46||buf[4]!==2) {
console.error(` EULA gate: ${filePath}: not ELF64, skipping`); return;
}
const e_machine = buf.readUInt16LE(0x12);
const e_phoff = Number(buf.readBigUInt64LE(0x20));
const e_phentsize = buf.readUInt16LE(0x36);
const e_phnum = buf.readUInt16LE(0x38);
const segs = [];
for (let i=0;i<e_phnum;i++){
const o=e_phoff+i*e_phentsize;
if (buf.readUInt32LE(o)!==1) continue;
segs.push({
p_offset:Number(buf.readBigUInt64LE(o+8)),
p_vaddr:Number(buf.readBigUInt64LE(o+16)),
p_filesz:Number(buf.readBigUInt64LE(o+32)),
p_flags:buf.readUInt32LE(o+4),
});
}
const off2va = (off)=>{ for (const s of segs) if(off>=s.p_offset && off<s.p_offset+s.p_filesz) return s.p_vaddr+(off-s.p_offset); return -1; };
const execSegs = segs.filter(s=>s.p_flags&1);
const sIdx = buf.indexOf(Buffer.from("License not yet accepted, cannot run in batch mode\x00"));
if (sIdx<0) { console.error(` EULA gate: ${filePath}: error string not found`); return; }
const errVa = off2va(sIdx);
const SCAN = 0x4000;
let site = null;
if (e_machine===0x3e) site = findEulaX64(buf, execSegs, off2va, errVa, SCAN);
else if (e_machine===0xb7) site = findEulaArm64(buf, execSegs, off2va, errVa, SCAN);
else { console.error(` EULA gate: ${filePath}: unsupported machine 0x${e_machine.toString(16)}`); return; }
if (!site) { console.error(` EULA gate: ${filePath}: patch site not located`); return; }
for (const [off,val] of site.patches) buf[off]=val;
fs.writeFileSync(filePath, buf);
console.log(`Patched ${filePath} (EULA gate @0x${site.addr.toString(16)}, ${site.desc})`);
}
function findEulaX64(buf, execSegs, off2va, errVa, SCAN) {
let xref=-1;
for (const s of execSegs) for (let o=s.p_offset; o<s.p_offset+s.p_filesz-7 && xref<0; o++) {
if (buf[o]===0x48 && (buf[o+1]===0x8d || buf[o+1]===0x8b) && (buf[o+2]&0xc7)===0x05)
if (off2va(o)+7+buf.readInt32LE(o+3)===errVa) xref=o;
}
if (xref<0) return null;
for (let o=Math.min(xref-3,xref); o>=Math.max(0,xref-SCAN); o--) {
if (buf[o]===0x85 && buf[o+1]===0xc0 && (buf[o+2]===0x74||buf[o+2]===0x75) && buf[o-5]===0xe8)
return {addr:off2va(o+2), desc:`jcc 0x${buf[o+2].toString(16)}->0xeb`, patches:[[o+2,0xeb]]};
}
for (let o=Math.min(xref-3,xref); o>=Math.max(0,xref-SCAN); o--) {
if (buf[o]===0x85 && buf[o+1]===0xc0 && buf[o+2]===0x0f && (buf[o+3]===0x84||buf[o+3]===0x85) && buf[o-5]===0xe8) {
const dd=buf.readInt32LE(o+4), dd2=(dd+1)>>>0;
return {addr:off2va(o+2), desc:`near jcc->jmp`, patches:[[o+2,0xe9],[o+3,dd2&0xff],[o+4,(dd2>>>8)&0xff],[o+5,(dd2>>>16)&0xff],[o+6,(dd2>>>24)&0xff],[o+7,0x90]]};
}
}
return null;
}
function findEulaArm64(buf, execSegs, off2va, errVa, SCAN) {
let xref=-1;
for (const s of execSegs) for (let o=s.p_offset; o<s.p_offset+s.p_filesz-8 && xref<0; o+=4) {
const ins=buf.readUInt32LE(o);
if (((ins&0x9F000000)>>>0)===0x90000000) {
let imm=((((ins>>>5)&0x7ffff)<<2)|((ins>>>29)&3)); if (imm&(1<<20)) imm-=(1<<21);
const rd=ins&0x1f, page=(off2va(o)&~0xfff)+(imm<<12);
const ins2=buf.readUInt32LE(o+4);
if (((ins2&0xFF800000)>>>0)===0x91000000) {
const imm12=(ins2>>>10)&0xfff, rn=(ins2>>>5)&0x1f, rd2=ins2&0x1f;
if (rd2===rd && rn===rd && page+imm12===errVa) xref=o;
}
}
}
if (xref<0) return null;
const start=Math.max(execSegs[0].p_offset, xref-SCAN);
for (let o=xref; o>=start; o-=4) {
const ins=buf.readUInt32LE(o);
if (((ins&0xFC000000)>>>0)===0x94000000) {
const ins2=buf.readUInt32LE(o+4), top=(ins2>>>24)&0xff;
if (top===0x34||top===0xb4||top===0x35||top===0xb5) {
let imm19=(ins2>>>5)&0x7ffff; if (imm19&(1<<18)) imm19-=(1<<19);
const cbVa=off2va(o+4), target=cbVa+imm19*4, off=((target-cbVa)/4)>>>0;
const bIns=(0x14000000|(off&0x3ffffff))>>>0;
return {addr:cbVa, desc:`cb${(top&1)?'nz':'z'}->b 0x${target.toString(16)}`, patches:[[o+4,bIns&0xff],[o+5,(bIns>>>8)&0xff],[o+6,(bIns>>>16)&0xff],[o+7,(bIns>>>24)&0xff]]};
}
}
}
return null;
}
["libidalib.so"].forEach(file => {
if (fs.existsSync(file)) {
console.log(`Patching ${file} (EULA gate)...`);
patchEulaGate(file);
}
});


最新回复 (5)
  • laris 07-04 22:02
    1

    EULA也要顺手搞一下

    另外可以升级到sp2了


    magnet:?xt=urn:btih:57bd6e22aa74309330f6613c0cf9302edd1c586e&xt=urn:btmh:12209a2919bd4b6517d5df765e66dc9fc2f7173231e17bbe78056f4d75e60dc37455&dn=ida93sp2&xl=6803161088

    Source: https://bbs.kanxue.com/…
  • 陈寒彤 07-04 22:05
    2

    EULA 是不是只能 GUI 过, 我最后的 SSH X11 forward 解决的(

  • laris 07-04 23:11
    3

    可以auto过

    我贴几个脚本你们试试

    tools/accept_eula.py


    # accept_eula.py — pre-accept the Hex-Rays GUI EULA (IDA headless -S script).
    #
    # The GUI shows the "Hex-Rays End User License Agreement" dialog on launch until
    # the "EULA 90" key is set in ~/.idapro/ida.reg (this is a SEPARATE surface from
    # the headless-batch EULA gate patched in libidalib — see docs 03 §3.2 / §3.5).
    # Accepting once writes the key; this script writes it non-interactively.
    #
    # Run (headless batch must work — apply the libidalib EULA patch first):
    # idat -A -B -S/path/to/tools/accept_eula.py <any-small-binary>
    # # writes to the ida.reg selected by $IDAUSR (default ~/.idapro)
    #
    # Value 1 matches a real GUI acceptance (verified against a prior accepted ida.reg).
    import ida_pro
    try:
    import ida_registry
    ida_registry.reg_write_int("EULA 90", 1)
    got = ida_registry.reg_read_int("EULA 90", -1)
    print("accept_eula: EULA 90 = %r" % got)
    except Exception as e:
    print("accept_eula: FAILED %r" % e)
    ida_pro.qexit(0)


    tools/find_eula_guard.py


    #!/usr/bin/env python3
    """find_eula_guard.py — locate (and optionally patch) IDA's headless-EULA guard.

    IDA blocks batch/headless mode until the EULA is accepted
    ("License not yet accepted, cannot run in batch mode"). The check reads the
    "EULA 90" registry key via reg_int_op and branches on the result. This tool
    finds that branch and reports the byte patch that makes IDA always take the
    "accepted" path — so the offset never has to be re-derived by hand on a new
    build.

    Supported:
    * Linux x86-64 ELF (libidalib.so) guard: test eax,eax ; jne ACC -> jmp ACC (0x75 -> 0xEB)
    * macOS arm64 Mach-O (libidalib.dylib) guard: cbnz w0, ACC -> b ACC (recomputed)

    Usage:
    find_eula_guard.py <libidalib.{so,dylib}> # report only
    find_eula_guard.py <libidalib.{so,dylib}> --apply # patch in place (writes .eulabak)

    Deps: pip install capstone lief
    macOS: after --apply you MUST re-sign: codesign --force --sign - <libidalib.dylib>

    Verified: 9.3.260213 / 9.3.260421 Linux -> 0x95cbf ; 9.3.260421 macOS -> 0x58b4.
    """
    import sys
    import os
    import re
    import argparse

    try:
    import lief
    import capstone as cap
    except ImportError:
    sys.exit("Missing deps: pip install capstone lief")

    EULA_KEY = b"EULA 90\x00"

    def load(path):
    data = open(path, "rb").read()
    b = lief.parse(path)
    if b is None:
    sys.exit("not a parseable binary: " + path)
    if isinstance(b, lief.MachO.FatBinary): # universal -> pick arm64 slice
    pick = next((m for m in b if "ARM64" in str(m.header.cpu_type)), None)
    b = pick or b.at(0)
    return data, b

    def file_offset(section):
    off = getattr(section, "offset", None)
    return section.file_offset if off is None else off

    def sec_va(b, fo):
    for s in b.sections:
    off = file_offset(s)
    if s.virtual_address and off <= fo < off + s.size:
    return s.virtual_address + (fo - off)
    return None

    def text_section(b, is_elf):
    name = ".text" if is_elf else "__text"
    for s in b.sections:
    if s.name == name:
    return s
    return max(b.sections, key=lambda s: s.size)

    def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("lib")
    ap.add_argument("--apply", action="store_true", help="patch in place (backup .eulabak)")
    a = ap.parse_args()

    data, b = load(a.lib)
    is_elf = isinstance(b, lief.ELF.Binary)
    is_macho = isinstance(b, lief.MachO.Binary)
    if not (is_elf or is_macho):
    sys.exit("unsupported binary format")

    fo = data.find(EULA_KEY)
    if fo < 0:
    sys.exit('"EULA 90" string not found — unexpected build?')
    eula_va = sec_va(b, fo)

    t = text_section(b, is_elf)
    tva, toff, code = t.virtual_address, file_offset(t), bytes(t.content)

    if is_elf:
    md = cap.Cs(cap.CS_ARCH_X86, cap.CS_MODE_64)
    arch = "x86-64 ELF"
    else:
    arch_const = getattr(cap, "CS_ARCH_ARM64", None) or getattr(cap, "CS_ARCH_AARCH64")
    md = cap.Cs(arch_const, cap.CS_MODE_LITTLE_ENDIAN)
    arch = "arm64 Mach-O"
    ins = list(md.disasm(code, tva))

    # 1) find the instruction that loads the "EULA 90" string address
    load_idx = None
    if is_elf:
    rip = re.compile(r"\[rip\s*([+-])\s*(0x[0-9a-fA-F]+)\]")
    for i, x in enumerate(ins):
    if x.mnemonic == "lea" and "rip" in x.op_str:
    m = rip.search(x.op_str)
    if m:
    disp = int(m.group(2), 16) * (1 if m.group(1) == "+" else -1)
    if x.address + x.size + disp == eula_va:
    load_idx = i
    break
    else:
    page = {}
    for i, x in enumerate(ins):
    if x.mnemonic == "adrp":
    p = [q.strip() for q in x.op_str.split(",")]
    try:
    page[p[0]] = int(p[1].replace("#", ""), 16)
    except Exception:
    pass
    elif x.mnemonic == "add":
    p = [q.strip() for q in x.op_str.split(",")]
    if len(p) == 3 and p[1] in page and p[2].startswith("#"):
    try:
    if page[p[1]] + int(p[2].replace("#", ""), 16) == eula_va:
    load_idx = i
    break
    except Exception:
    pass
    if load_idx is None:
    sys.exit("could not locate the EULA-key load site")

    # 2) scan forward for the guard conditional branch after the reg_int_op call
    guard = None
    for j in range(load_idx, min(load_idx + 16, len(ins))):
    x = ins[j]
    if is_elf and x.mnemonic in ("jne", "jnz"):
    guard = x
    break
    if is_macho and x.mnemonic in ("cbnz", "cbz"):
    guard = x
    break
    if guard is None:
    sys.exit("could not locate the guard branch after the EULA read")

    gfo = toff + (guard.address - tva)
    if is_elf:
    cur = data[gfo:gfo + 2]
    if cur[0] != 0x75:
    sys.exit("expected jne(0x75) at file 0x%x, got 0x%02x (rel32 form? patch by hand)"
    % (gfo, cur[0]))
    new = bytes([0xEB]) # jne -> jmp
    desc = "jne -> jmp"
    else:
    cur = data[gfo:gfo + 4]
    tgt = int(guard.op_str.split("#")[-1], 16)
    rel = (tgt - guard.address) // 4
    new = (0x14000000 | (rel & 0x03FFFFFF)).to_bytes(4, "little") # cbnz w0 -> b
    desc = "%s w0 -> b (target 0x%x)" % (guard.mnemonic, tgt)

    print("format :", arch)
    print('"EULA 90" : va 0x%x (file 0x%x)' % (eula_va, fo))
    print("guard : 0x%x %s %s" % (guard.address, guard.mnemonic, guard.op_str))
    print("file offset : 0x%x" % gfo)
    print("current bytes: %s" % cur.hex())
    print("patch to : %s (%s)" % (new.hex(), desc))
    if is_macho:
    print("reminder : codesign --force --sign - '%s' (after --apply)" % a.lib)

    if a.apply:
    if not os.path.exists(a.lib + ".eulabak"):
    open(a.lib + ".eulabak", "wb").write(data)
    d = bytearray(data)
    d[gfo:gfo + len(new)] = new
    open(a.lib, "wb").write(d)
    print("APPLIED — backup at %s.eulabak" % a.lib)

    if __name__ == "__main__":
    main()


    tools/patch_modulus.py


    #!/usr/bin/env python3
    """patch_modulus.py — apply IDA's license-signature modulus patch (ALL occurrences).

    Replaces the RSA modulus `EDFD425CF978` -> `EDFD42CBF978` (byte index 3, 5C->CB)
    in every occurrence. This is the same patch keygen.js applies, but keygen.js uses
    indexOf and patches only the FIRST of two occurrences — this tool patches both.

    Targets: libida.so / libida32.so / libida.dylib / libida32.dylib / ida.dll / ida32.dll.
    No third-party deps.

    Usage:
    patch_modulus.py <file> [<file> ...] # report occurrence counts
    patch_modulus.py <file> [<file> ...] --apply # patch in place (writes .bak once)

    macOS: after --apply, re-sign each dylib: codesign --force --sign - <dylib>
    """
    import sys
    import os
    import argparse

    SEARCH = bytes.fromhex("EDFD425CF978") # original modulus (first 6 bytes)
    REPLACE = bytes.fromhex("EDFD42CBF978") # patched

    def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("files", nargs="+")
    ap.add_argument("--apply", action="store_true", help="patch in place (backup .bak)")
    a = ap.parse_args()

    rc = 0
    for f in a.files:
    name = os.path.basename(f)
    d = open(f, "rb").read()
    orig, patched = d.count(SEARCH), d.count(REPLACE)
    if not a.apply:
    print("%s: orig=%d patched=%d" % (name, orig, patched))
    continue
    if orig == 0 and patched:
    print("%s: already fully patched (%d occ)" % (name, patched))
    continue
    if orig == 0:
    print("%s: modulus NOT found — wrong file/build?" % name)
    rc = 1
    continue
    if not os.path.exists(f + ".bak"):
    open(f + ".bak", "wb").write(d)
    nd = d.replace(SEARCH, REPLACE)
    open(f, "wb").write(nd)
    print("%s: patched %d occurrence(s); remaining orig=%d %s"
    % (name, nd.count(REPLACE), nd.count(SEARCH),
    "OK" if nd.count(SEARCH) == 0 else "!! still has originals"))
    sys.exit(rc)

    if __name__ == "__main__":
    main()

  • jacklove 07-04 23:13
    4

    佬9.4出来了。可以直接装9.4了

  • laris 07-05 10:09
    5

    9.4还是beta

    其实一般用区别不大,beta后边还有sp patches

    感觉AI后时代,软件更新快了很多

* 帖子来源Linux.do
返回