diff --git a/.gitignore b/.gitignore index 8b94348..66a485a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ target/ # Uploaded binaries uploads/ engines/ida5/ida5_files/** +engines/ida66/ida66_files/** ### Intellij ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider diff --git a/engines/ida66/Dockerfile.ida66 b/engines/ida66/Dockerfile.ida66 new file mode 100644 index 0000000..54f650b --- /dev/null +++ b/engines/ida66/Dockerfile.ida66 @@ -0,0 +1,62 @@ +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +# Install 32-bit Wine, Xvfb, and utilities +RUN dpkg --add-architecture i386 && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + wine \ + wine32 \ + xvfb \ + xauth \ + fonts-dejavu-core \ + fonts-liberation && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Configure Wine environment variables +ENV WINEPREFIX=/wine/prefix +ENV WINEARCH=win32 +ENV WINEDLLOVERRIDES="mscoree,mshtml=" + +RUN mkdir -p /wine/prefix + +# Initialize the default Wine prefix +RUN xvfb-run -a wine wineboot -u && wineserver -w && ls -la /wine/prefix/*.reg + +# Install Python 2.7 in Wine for IDAPython +COPY ./python-2.7.18.msi /tmp/python-2.7.18.msi +RUN xvfb-run -a wine msiexec /i /tmp/python-2.7.18.msi /qn && \ + rm /tmp/python-2.7.18.msi + +# Set up application directories +RUN mkdir -p /app/ida /input /output +COPY ./ida66_files /app/ida + +# IDAPython local runtime: copy Python stdlib into IDA's python/ directory +RUN cp /wine/prefix/drive_c/Python27/python27.dll /app/ida/ && \ + cp -r /wine/prefix/drive_c/Python27/Lib /app/ida/python/lib/ && \ + cp -r /wine/prefix/drive_c/Python27/DLLs /app/ida/python/DLLs/ && \ + sed -i 's/USE_LOCAL_PYTHON = 0/USE_LOCAL_PYTHON = 1/' /app/ida/cfg/python.cfg + +# Import IDA 6.6 license configuration into Wine registry +COPY ida6_license.reg /tmp/ida6_license.reg +COPY ida-hkcu.sh /tmp/ida-hkcu.sh +RUN wine regedit /tmp/ida6_license.reg && \ + rm /tmp/ida6_license.reg && \ + bash /tmp/ida-hkcu.sh && \ + rm /tmp/ida-hkcu.sh && \ + wineserver -w && \ + grep "Hex-Rays" /wine/prefix/user.reg + +WORKDIR /app/ida + +# Embed the analysis scripts into the image +COPY extract.py /app/ida/extract.py + +# Configure the entrypoint script +COPY entrypoint.sh /app/ida/entrypoint.sh +RUN chmod +x /app/ida/entrypoint.sh + +ENTRYPOINT ["/app/ida/entrypoint.sh"] diff --git a/engines/ida66/entrypoint.sh b/engines/ida66/entrypoint.sh new file mode 100644 index 0000000..86e1c7c --- /dev/null +++ b/engines/ida66/entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +export WINEPREFIX="/wine/prefix" +export WINEARCH=win32 +export WINEDEBUG=-all +export DISPLAY=:99 + +echo "[INFO] Starting virtual X11 display on :99 (1280x720)..." +Xvfb :99 -screen 0 1280x720x24 +extension RANDR & +sleep 1 + +# Accept target filename as first argument +TARGET_FILENAME="${1:-target.exe}" + +if [ ! -f "/input/${TARGET_FILENAME}" ]; then + echo "[ERROR] Missing /input/${TARGET_FILENAME}" + exit 1 +fi + +echo "[INFO] Preparing internal analysis environment..." +cp "/input/${TARGET_FILENAME}" "/app/ida/target.exe" + +echo "[INFO] Launching IDA Pro 6.6 (auto-script)..." +wine idaq.exe -A -S"extract.py" target.exe + +echo "[INFO] Verifying generated artifacts..." +if [ -f "/app/ida/output.json" ]; then + cp /app/ida/output.json /output/output.json + echo "[SUCCESS] Static analysis completed. output.json is ready!" + RET_CODE=0 +else + echo "[ERROR] IDAPython script failed to generate output.json" + RET_CODE=1 +fi + +# Clean up temporary files inside the container before exiting +rm -f /app/ida/target.exe /app/ida/target.idb /app/ida/output.json + +exit $RET_CODE diff --git a/engines/ida66/extract.py b/engines/ida66/extract.py new file mode 100644 index 0000000..580b0b8 --- /dev/null +++ b/engines/ida66/extract.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# IDAPython extraction script for IDA Pro 6.6 +# Equivalent to extract.idc — same JSON output structure +import sys +import traceback +import os +import json + +import idc +import idaapi +import idautils + +OUTPUT = "output.json" + +# Cross-reference type constants (IDA SDK values) +FL_CF = 16 # Call Far +FL_CN = 17 # Call Near +FL_JF = 18 # Jump Far +FL_JN = 19 # Jump Near + + +def msg(text): + idc.Message(text) + sys.stderr.write(text) + sys.stderr.flush() + + +def _json_string(s): + """Escape a string for JSON — returns the quoted representation. + Handles raw bytes in disassembly that aren't valid UTF-8.""" + if isinstance(s, unicode): + return json.dumps(s) + try: + return json.dumps(s) + except UnicodeDecodeError: + return json.dumps(s.decode('latin-1')) + + +def _json_hex(addr): + """Format an address as 0x-prefixed hex string (uppercase, 8 digits).""" + return "0x%08X" % addr + + +def extract_func(f, ea): + """Extract a single function. Returns True on success, False on failure.""" + func_name = idc.GetFunctionName(ea) + func_end = idc.FindFuncEnd(ea) + func_flags = idc.GetFunctionFlags(ea) + + f.write(" {\n") + f.write(" \"function\": %s,\n" % _json_string(func_name)) + f.write(" \"address\": \"%s\",\n" % _json_hex(ea)) + f.write(" \"size\": %d,\n" % (func_end - ea)) + + # ---- flags ---- + flags_parts = [] + if func_flags & idc.FUNC_LIB: + flags_parts.append("LIBRARY") + if func_flags & idc.FUNC_THUNK: + flags_parts.append("THUNK") + if func_flags & idc.FUNC_FAR: + flags_parts.append("FAR") + if func_flags & idc.FUNC_FRAME: + flags_parts.append("FRAME") + f.write(" \"flags\": %s,\n" % _json_string("|".join(flags_parts))) + + # ---- xrefs (who calls/jumps to this function) ---- + xref_parts = [] + for xref in idautils.XrefsTo(ea): + if xref.type in (FL_CF, FL_CN, FL_JF, FL_JN): + caller_name = idc.GetFunctionName(xref.frm) + if caller_name != "": + if xref.type == FL_CN or xref.type == FL_CF: + type_str = "CALL" + elif xref.type == FL_JN or xref.type == FL_JF: + type_str = "JUMP" + else: + type_str = "UNKNOWN" + xref_parts.append("%s:%s" % (caller_name, type_str)) + f.write(" \"xrefs\": %s,\n" % _json_string(" ".join(xref_parts))) + f.flush() + + # ---- assembly ---- + asm_lines = [] + curr_inst = ea + last_inst = 0 + while curr_inst < func_end: + if curr_inst == last_inst: + break + last_inst = curr_inst + disasm = idc.GetDisasm(curr_inst) + if disasm: + asm_lines.append("%s %s" % (_json_hex(curr_inst), disasm)) + curr_inst = curr_inst + idc.ItemSize(curr_inst) + f.write(" \"assembly\": %s,\n" % _json_string("\n".join(asm_lines))) + f.flush() + + # ---- labels (named data/code references from this function) ---- + label_parts = [] + curr_inst = ea + last_inst = 0 + while curr_inst < func_end: + if curr_inst == last_inst: + break + last_inst = curr_inst + for xref in idautils.XrefsFrom(curr_inst): + if xref.type in (FL_CF, FL_CN, FL_JF, FL_JN): + label_name = idc.Name(xref.to) + if label_name != "": + if xref.type == FL_CN or xref.type == FL_CF: + type_str = "CALL" + elif xref.type == FL_JN or xref.type == FL_JF: + type_str = "JUMP" + else: + type_str = "UNKNOWN" + label_parts.append( + "%s:%s:%s" % (label_name, _json_hex(xref.to), type_str) + ) + curr_inst = curr_inst + idc.ItemSize(curr_inst) + f.write(" \"labels\": %s\n" % _json_string(" ".join(label_parts))) + f.write(" }") + f.flush() + return True + + +def extract(f): + entry_ea = idc.MinEA() + entry_name = idc.Name(entry_ea) + if entry_name == "": + entry_name = "entry_point" + f.write( + "{\n \"entry_point\": \"%s:%s\",\n" % (_json_hex(entry_ea), entry_name) + ) + + # ---- segments ---- + f.write(" \"segments\": [\n") + segs = [] + seg = idc.FirstSeg() + while seg != idc.BADADDR: + segs.append( + " {\"name\": %s, \"start\": \"%s\", \"end\": \"%s\"}" + % ( + _json_string(idc.SegName(seg)), + _json_hex(idc.SegStart(seg)), + _json_hex(idc.SegEnd(seg)), + ) + ) + seg = idc.NextSeg(seg) + f.write(",\n".join(segs)) + f.write("\n ],\n") + f.flush() + + # ---- functions ---- + f.write(" \"functions\": [\n") + f.flush() + + ea = idc.NextFunction(0) + func_count = 0 + error_count = 0 + + while ea != idc.BADADDR: + if func_count > 0: + f.write(",\n") + f.flush() + + try: + if extract_func(f, ea): + func_count += 1 + else: + error_count += 1 + msg("[extract.py] Aviso: falha ao extrair funcao em %s\n" + % _json_hex(ea)) + except Exception as e: + error_count += 1 + tb_str = traceback.format_exc().strip().replace("\n", "\\n") + msg("[extract.py] Erro na funcao %s: %s\n" + % (_json_hex(ea), str(e))) + # Write a placeholder with the error info so JSON remains valid + f.write( + " {\"function\": \"__ERROR__\"," + " \"address\": \"%s\"," + " \"error\": %s}" + % (_json_hex(ea), _json_string(tb_str)) + ) + f.flush() + + ea = idc.NextFunction(ea) + + msg("[extract.py] Extraidas %d funcoes, %d erros.\n" + % (func_count, error_count)) + + f.write("\n ]\n}\n") + f.flush() + + +def main(): + msg("[extract.py] Script iniciado. CWD=%s\n" % os.getcwd()) + + idaapi.autoWait() + msg("[extract.py] Analise concluida. Extraindo...\n") + + try: + # Line-buffered — flushes after every newline, prevents data loss + f = open(OUTPUT, "w", 1) + except Exception as e: + msg("[extract.py] ERRO ao criar %s: %s\n" % (OUTPUT, str(e))) + idc.Exit(1) + return + + try: + extract(f) + except Exception as e: + msg("[extract.py] ERRO na extracao: %s\n" % str(e)) + msg(traceback.format_exc() + "\n") + finally: + f.close() + + if os.path.exists(OUTPUT): + msg("[extract.py] %s gerado com sucesso (%d bytes)!\n" % + (OUTPUT, os.path.getsize(OUTPUT))) + else: + msg("[extract.py] FALHA: %s nao foi criado.\n" % OUTPUT) + + idc.Exit(0) + + +if __name__ == "__main__": + main() diff --git a/engines/ida66/ida-hkcu.sh b/engines/ida66/ida-hkcu.sh new file mode 100644 index 0000000..8922f77 --- /dev/null +++ b/engines/ida66/ida-hkcu.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Import HKCU keys for IDA 6.6 — license + suppress nag dialogs +# These key names contain special chars that are tricky to escape in Dockerfile RUN + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "License Zhou Tao, Jiangsu Australia Sinuo Network Technology Co., Ltd." \ + /t REG_DWORD /d 1 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "AutoCheckUpdates" /t REG_DWORD /d 0 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "AutoRequestUpdates" /t REG_DWORD /d 1 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "DisplayWelcome" /t REG_DWORD /d 1 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "InformedAboutUpdates2" /t REG_DWORD /d 1 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "InputDirectory" /t REG_SZ /d "C:/Program Files/IDA 6.6" /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "QuickFilterFlags" /t REG_DWORD /d 0 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "SplashWindow" /t REG_DWORD /d 0 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA" \ + /v "48-3057-7374-2C6.6" /t REG_DWORD /d 0 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA\\Hidden Messages" \ + /v "Congratulations the TIL validator is active " /t REG_DWORD /d 1 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA\\Hidden Messages" \ + /v "GOOD NEWS THE BTREE VALIDATOR v66 IS ACTIVE The btree validator plugin will now verify the input d" \ + /t REG_DWORD /d 1 /f + +wine reg add "HKEY_CURRENT_USER\\Software\\Hex-Rays\\IDA\\Hidden Messages" \ + /v "The technical support period of the Hex Rays decompiler has expired You have 3 month grace period t" \ + /t REG_DWORD /d 1 /f + +echo "HKCU keys imported" + +# Add Python27 to system PATH so python.plw finds python27.dll +echo "Updating system PATH..." +CUR=$(wine reg query "HKLM\\System\\CurrentControlSet\\Control\\Session Manager\\Environment" /v PATH 2>/dev/null | tr -d '\r' | awk '/PATH/{$1=$2=""; sub(/^[ \t]+/, ""); print}') +wine reg add "HKLM\\System\\CurrentControlSet\\Control\\Session Manager\\Environment" /v PATH /t REG_EXPAND_SZ /d "C:\\Python27;${CUR}" /f +echo "PATH: C:\\Python27;${CUR}" diff --git a/engines/ida66/ida6_license.reg b/engines/ida66/ida6_license.reg new file mode 100644 index 0000000..f95bb86 --- /dev/null +++ b/engines/ida66/ida6_license.reg @@ -0,0 +1,234 @@ +Windows Registry Editor Version 5.00 + +; ============================================================================= +; IDA Pro 6.6 — License, settings, file associations, and runtime configuration +; Extracted from system.reg + user.reg Wine registry dumps +; ============================================================================= + +; --- HKEY_CURRENT_USER: IDA application settings and license --- + +[HKEY_CURRENT_USER\Software\Hex-Rays\IDA] +"48-3057-7374-2C6.6"=dword:00000000 +"AutoCheckUpdates"=dword:00000000 +"AutoRequestUpdates"=dword:00000001 +"DisplayWelcome"=dword:00000001 +"InformedAboutUpdates2"=dword:00000001 +"InputDirectory"="C:/Program Files/IDA 6.6" +"License Zhou Tao, Jiangsu Australia Sinuo Network Technology Co., Ltd."=dword:00000001 +"QState"=hex:40,00,42,00,79,00,74,00,65,00,41,00,72,00,72,00,61,00,79,00,28,00,\ + 00,00,00,00,00,00,ff,00,00,00,00,00,00,00,01,00,fd,00,00,00,00,00,00,00,02,\ + 00,00,00,00,00,00,00,01,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + fc,00,02,00,00,00,00,00,00,00,01,00,fb,00,00,00,00,00,00,00,1c,00,00,00,41,\ + 00,00,00,64,00,00,00,64,00,00,00,72,00,00,00,65,00,00,00,73,00,00,00,73,00,\ + 00,00,44,00,00,00,65,00,00,00,74,00,00,00,61,00,00,00,69,00,00,00,6c,00,00,\ + 00,73,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,\ + 00,00,51,00,00,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,02,00,00,00,00,00,07,\ + 00,80,00,00,00,00,00,00,00,32,00,fc,00,01,00,00,00,00,00,00,00,01,00,fb,00,\ + 00,00,00,00,00,00,12,00,00,00,4e,00,00,00,61,00,00,00,76,00,00,00,69,00,00,\ + 00,67,00,00,00,61,00,00,00,74,00,00,00,6f,00,00,00,72,00,01,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,07,00,80,00,00,00,00,00,00,00,46,00,00,00,ff,00,ff,\ + 00,ff,00,00,00,00,00,07,00,80,00,00,00,00,00,03,00,7b,00,00,00,00,00,00,00,\ + 04,00,00,00,00,00,00,00,04,00,00,00,00,00,00,00,08,00,00,00,00,00,00,00,08,\ + 00,fc,00,00,00,00,00,00,00,01,00,00,00,00,00,00,00,02,00,00,00,00,00,00,00,\ + 19,00,00,00,00,00,00,00,16,00,00,00,46,00,00,00,69,00,00,00,6c,00,00,00,65,\ + 00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,\ + 00,00,72,00,01,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,16,00,00,00,4a,00,\ + 00,00,75,00,00,00,6d,00,00,00,70,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,\ + 00,6c,00,00,00,42,00,00,00,61,00,00,00,72,00,01,00,00,00,00,00,00,00,3a,00,\ + ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,00,1a,00,00,00,53,00,00,00,65,00,00,00,61,00,00,00,72,00,00,00,\ + 63,00,00,00,68,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,\ + 00,00,00,61,00,00,00,72,00,01,00,00,00,00,00,00,00,8c,00,ff,00,ff,00,ff,00,\ + ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,1e,\ + 00,00,00,41,00,00,00,6e,00,00,00,61,00,00,00,6c,00,00,00,79,00,00,00,73,00,\ + 00,00,69,00,00,00,73,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,\ + 00,42,00,00,00,61,00,00,00,72,00,01,00,00,00,00,00,01,00,34,00,ff,00,ff,00,\ + ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,16,00,00,00,45,00,00,00,64,00,00,00,69,00,00,00,74,00,00,00,54,00,00,00,\ + 6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,00,72,00,01,00,00,\ + 00,00,00,01,00,6e,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,18,00,00,00,44,00,00,00,65,00,00,00,62,\ + 00,00,00,75,00,00,00,67,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,\ + 00,00,42,00,00,00,61,00,00,00,72,00,01,00,00,00,00,00,02,00,27,00,ff,00,ff,\ + 00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,24,00,00,00,42,00,00,00,72,00,00,00,65,00,00,00,61,00,00,00,6b,00,00,\ + 00,70,00,00,00,6f,00,00,00,69,00,00,00,6e,00,00,00,74,00,00,00,73,00,00,00,\ + 54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,00,72,\ + 00,01,00,00,00,00,00,03,00,3f,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,1e,00,00,00,53,00,00,00,65,\ + 00,00,00,67,00,00,00,6d,00,00,00,65,00,00,00,6e,00,00,00,74,00,00,00,73,00,\ + 00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,\ + 00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,20,00,00,00,47,00,00,\ + 00,72,00,00,00,61,00,00,00,70,00,00,00,68,00,00,00,56,00,00,00,69,00,00,00,\ + 65,00,00,00,77,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,\ + 00,00,00,61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,\ + ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,22,\ + 00,00,00,53,00,00,00,74,00,00,00,72,00,00,00,75,00,00,00,63,00,00,00,74,00,\ + 00,00,75,00,00,00,72,00,00,00,65,00,00,00,73,00,00,00,54,00,00,00,6f,00,00,\ + 00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,00,\ + 00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,26,00,00,00,45,00,00,00,6e,00,00,00,75,00,00,00,\ + 6d,00,00,00,65,00,00,00,72,00,00,00,61,00,00,00,74,00,00,00,69,00,00,00,6f,\ + 00,00,00,6e,00,00,00,73,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,\ + 00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,\ + 00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,24,00,00,00,4f,00,00,00,70,00,00,00,65,00,00,00,72,00,00,00,61,00,00,\ + 00,6e,00,00,00,64,00,00,00,54,00,00,00,79,00,00,00,70,00,00,00,65,00,00,00,\ + 54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,00,72,\ + 00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,20,00,00,00,46,00,00,00,75,\ + 00,00,00,6e,00,00,00,63,00,00,00,74,00,00,00,69,00,00,00,6f,00,00,00,6e,00,\ + 00,00,73,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,\ + 00,61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,1c,00,00,\ + 00,54,00,00,00,72,00,00,00,61,00,00,00,63,00,00,00,69,00,00,00,6e,00,00,00,\ + 67,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,\ + 00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,18,00,00,00,4c,\ + 00,00,00,69,00,00,00,73,00,00,00,74,00,00,00,73,00,00,00,54,00,00,00,6f,00,\ + 00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,\ + 00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,1a,00,00,00,47,00,00,00,72,00,00,00,61,00,00,\ + 00,70,00,00,00,68,00,00,00,73,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,\ + 6c,00,00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,\ + 00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,24,00,00,00,44,00,00,00,62,00,00,00,67,00,00,00,43,00,00,00,6f,\ + 00,00,00,6d,00,00,00,6d,00,00,00,61,00,00,00,6e,00,00,00,64,00,00,00,73,00,\ + 00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,\ + 00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,2c,00,00,00,43,00,00,\ + 00,72,00,00,00,6f,00,00,00,73,00,00,00,73,00,00,00,52,00,00,00,65,00,00,00,\ + 66,00,00,00,65,00,00,00,72,00,00,00,65,00,00,00,6e,00,00,00,63,00,00,00,65,\ + 00,00,00,73,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,\ + 00,00,61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,20,00,\ + 00,00,55,00,00,00,74,00,00,00,69,00,00,00,6c,00,00,00,69,00,00,00,74,00,00,\ + 00,69,00,00,00,65,00,00,00,73,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,\ + 6c,00,00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,\ + 00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,20,00,00,00,53,00,00,00,69,00,00,00,67,00,00,00,6e,00,00,00,54,\ + 00,00,00,79,00,00,00,70,00,00,00,65,00,00,00,73,00,00,00,54,00,00,00,6f,00,\ + 00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,\ + 00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,1c,00,00,00,57,00,00,00,61,00,00,00,74,00,00,\ + 00,63,00,00,00,68,00,00,00,65,00,00,00,73,00,00,00,54,00,00,00,6f,00,00,00,\ + 6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,00,00,\ + 00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,00,00,00,00,16,00,00,00,48,00,00,00,69,00,00,00,64,00,00,00,65,\ + 00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,61,00,\ + 00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,22,00,00,00,53,00,\ + 00,00,74,00,00,00,72,00,00,00,75,00,00,00,63,00,00,00,74,00,00,00,45,00,00,\ + 00,6e,00,00,00,75,00,00,00,6d,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,\ + 6c,00,00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,\ + 00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,1e,00,00,00,43,00,00,00,6f,00,00,00,6d,00,00,00,6d,00,00,00,65,\ + 00,00,00,6e,00,00,00,74,00,00,00,73,00,00,00,54,00,00,00,6f,00,00,00,6f,00,\ + 00,00,6c,00,00,00,42,00,00,00,61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,\ + 00,ff,00,ff,00,ff,00,ff,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,\ + 00,00,00,00,00,00,18,00,00,00,56,00,00,00,69,00,00,00,65,00,00,00,77,00,00,\ + 00,73,00,00,00,54,00,00,00,6f,00,00,00,6f,00,00,00,6c,00,00,00,42,00,00,00,\ + 61,00,00,00,72,00,00,00,00,00,00,00,00,00,00,00,ff,00,ff,00,ff,00,ff,00,00,\ + 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,29,00 +"QuickFilterFlags"=dword:00000000 +"SplashWindow"=dword:00000000 +"SupportExpiredDisplayed_2.0.0.140605"=dword:6a285788 +"uiConfig"=hex:ff,ff,ff,ff,00,00,00,00,00,00,00,00,02,00,00,00,01,00,00,00,00,\ + 00,00,00,ff,ff,ff,ff,00,00,01,00,ff,ff,ff,ff,00,00,00,00,00,00,00,00,02,00,\ + 00,00,00,00,00,00,00,00,00,00,4d,00,01,05,00,00,00,00 + +[HKEY_CURRENT_USER\Software\Hex-Rays\IDA\Hidden Messages] +"Congratulations the TIL validator is active "=dword:00000001 +"GOOD NEWS THE BTREE VALIDATOR v66 IS ACTIVE The btree validator plugin will now verify the input d"=dword:00000001 +"The technical support period of the Hex Rays decompiler has expired You have 3 month grace period t"=dword:00000001 + +[HKEY_CURRENT_USER\Software\Hex-Rays\IDA\History] + +[HKEY_CURRENT_USER\Software\Hex-Rays\IDA\History_A] + +[HKEY_CURRENT_USER\Software\Hex-Rays\IDA\History_I] + +[HKEY_CURRENT_USER\Software\Hex-Rays\IDA\History_UIFLTR] + +[HKEY_CURRENT_USER\Software\Hex-Rays\IDA\Main Windows\Main window] +"Geometry"=hex:40,00,42,00,79,00,74,00,65,00,41,00,72,00,72,00,61,00,79,00,28,\ + 00,01,00,d9,00,d0,00,cb,00,00,00,01,00,00,00,00,00,00,00,00,00,01,00,e0,00,\ + 00,00,00,00,01,00,44,00,00,00,00,00,05,00,07,00,00,00,00,00,03,00,bd,00,00,\ + 00,00,00,01,00,e4,00,00,00,00,00,01,00,62,00,00,00,00,00,05,00,03,00,00,00,\ + 00,00,03,00,b9,00,00,00,00,00,00,00,00,00,00,00,00,00,29,00 +"Maximized"=dword:00000001 + +[HKEY_CURRENT_USER\Software\Hex-Rays\IDA\RecentScripts] + +; --- HKEY_LOCAL_MACHINE: File type associations --- + +[HKEY_LOCAL_MACHINE\Software\Classes\.i64] +@="IDApro.Database64" + +[HKEY_LOCAL_MACHINE\Software\Classes\.idb] +@="IDApro.Database32" + +[HKEY_LOCAL_MACHINE\Software\Classes\IDApro.Database32] +@="IDA Database" + +[HKEY_LOCAL_MACHINE\Software\Classes\IDApro.Database32\DefaultIcon] +@="C:\\Program Files\\IDA 6.6\\idaq.exe,0" + +[HKEY_LOCAL_MACHINE\Software\Classes\IDApro.Database32\shell\open\command] +@="\"C:\\Program Files\\IDA 6.6\\idaq.exe\" \"%1\"" + +[HKEY_LOCAL_MACHINE\Software\Classes\IDApro.Database64] +@="IDA Pro (64-bit) Database" + +[HKEY_LOCAL_MACHINE\Software\Classes\IDApro.Database64\DefaultIcon] +@="C:\\Program Files\\IDA 6.6\\idaq64.exe,0" + +[HKEY_LOCAL_MACHINE\Software\Classes\IDApro.Database64\shell\open\command] +@="\"C:\\Program Files\\IDA 6.6\\idaq64.exe\" \"%1\"" + +[HKEY_LOCAL_MACHINE\Software\Classes\WinGraph.File] +@="WinGraph file" + +[HKEY_LOCAL_MACHINE\Software\Classes\WinGraph.File\DefaultIcon] +@="C:\\Program Files\\IDA 6.6\\wingraph32.exe,0" + +[HKEY_LOCAL_MACHINE\Software\Classes\WinGraph.File\shell\open\command] +@="\"C:\\Program Files\\IDA 6.6\\wingraph32.exe\" \"%1\"" + +; --- HKEY_LOCAL_MACHINE: Uninstall registration --- + +[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\IDA Pro_6.6_is1] +"DisplayIcon"="C:\\Program Files\\IDA 6.6\\idaq.exe" +"DisplayName"="IDA Pro v6.6 and Hex-Rays Decompiler (ARM,x64,x86)" +"EstimatedSize"=dword:00030f1b +"Inno Setup: App Path"="C:\\Program Files\\IDA 6.6" +"Inno Setup: Deselected Tasks"="desktopicon" +"Inno Setup: Icon Group"="(Default)" +"Inno Setup: Language"="default" +"Inno Setup: Selected Tasks"="" +"Inno Setup: Setup Version"="5.5.3 (a)" +"Inno Setup: User"="rov" +"InstallDate"="20260609" +"InstallLocation"="C:\\Program Files\\IDA 6.6\\" +"NoModify"=dword:00000001 +"NoRepair"=dword:00000001 +"Publisher"="Hex-Rays SA" +"QuietUninstallString"="\"C:\\Program Files\\IDA 6.6\\unins000.exe\" /SILENT" +"UninstallString"="\"C:\\Program Files\\IDA 6.6\\unins000.exe\"" + +; --- HKEY_LOCAL_MACHINE: DLL search behavior for IDA executables --- + +[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\idaq.exe] +"CWDIllegalInDllSearch"=dword:ffffffff + +[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\idaq64.exe] +"CWDIllegalInDllSearch"=dword:ffffffff + +[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\idaw.exe] +"CWDIllegalInDllSearch"=dword:ffffffff + +[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\idaw64.exe] +"CWDIllegalInDllSearch"=dword:ffffffff + diff --git a/engines/ida66/python-2.7.18.msi b/engines/ida66/python-2.7.18.msi new file mode 100644 index 0000000..e3cb75c Binary files /dev/null and b/engines/ida66/python-2.7.18.msi differ diff --git a/src/main/java/ai/decompile/engine/model/EngineType.java b/src/main/java/ai/decompile/engine/model/EngineType.java index 956a3e3..162c82d 100644 --- a/src/main/java/ai/decompile/engine/model/EngineType.java +++ b/src/main/java/ai/decompile/engine/model/EngineType.java @@ -2,5 +2,6 @@ package ai.decompile.engine.model; public enum EngineType { IDA5, + IDA66, GHIDRA } diff --git a/src/main/java/ai/decompile/engine/service/AbstractIdaEngineService.java b/src/main/java/ai/decompile/engine/service/AbstractIdaEngineService.java new file mode 100644 index 0000000..026819d --- /dev/null +++ b/src/main/java/ai/decompile/engine/service/AbstractIdaEngineService.java @@ -0,0 +1,382 @@ +package ai.decompile.engine.service; + +import ai.decompile.docker.service.ContainerService; +import ai.decompile.engine.config.EngineProperties; +import ai.decompile.engine.model.AnalysisResult; +import ai.decompile.engine.model.FunctionResult; +import ai.decompile.engine.model.LabelEntry; +import ai.decompile.engine.model.SegmentEntry; +import ai.decompile.engine.model.XrefEntry; +import com.github.dockerjava.api.model.Bind; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public abstract class AbstractIdaEngineService implements EngineService { + private static final String KEY_ENTRY_POINT = "\"entry_point\": \""; + private static final String KEY_ADDRESS = "\",\n \"address\": \""; + private static final String KEY_SIZE = "\",\n \"size\": "; + private static final String KEY_FLAGS = ",\n \"flags\": \""; + private static final String KEY_XREFS = "\",\n \"xrefs\": \""; + private static final String KEY_ASSEMBLY = "\",\n \"assembly\": \""; + private static final String KEY_LABELS = "\",\n \"labels\": \""; + private static final String KEY_ENTRY_END = "\"\n }"; + private static final String KEY_SEGMENTS_START = "\"segments\": ["; + private static final String KEY_SEGMENT_NAME = "\"name\": \""; + private static final String KEY_SEGMENT_START = "\", \"start\": \""; + private static final String KEY_SEGMENT_END = "\", \"end\": \""; + + protected final ContainerService containers; + protected final EngineProperties engineProperties; + + protected AbstractIdaEngineService( + ContainerService containers, EngineProperties engineProperties) { + this.containers = containers; + this.engineProperties = engineProperties; + } + + protected abstract String getDockerfilePath(); + + protected abstract String getConfigKey(); + + protected abstract String getTempDirPrefix(); + + @Override + public AnalysisResult analyze(String filePath) { + var configKey = getConfigKey(); + var config = engineProperties.getEngineConfig(configKey); + var absolutePath = Path.of(filePath).toAbsolutePath().normalize(); + var filename = absolutePath.getFileName().toString(); + + containers.ensureImage(config.getImage(), new File(getDockerfilePath())); + + var outputDir = createTempDir(); + var containerId = createContainer(config.getImage(), filename, absolutePath, outputDir); + + try { + var exitCode = containers.waitForExit(containerId, config.getTimeoutSeconds()); + var logs = containers.captureLogs(containerId, 2); + + validateExitCode(configKey, exitCode, logs); + var outputFile = validateOutputFile(configKey, outputDir, logs); + return parseOutput(readStringTolerant(outputFile)); + } catch (Exception e) { + log.error("{} analysis failed for file {}: {}", configKey, filePath, e.getMessage()); + throw new RuntimeException(configKey + " analysis failed: " + e.getMessage(), e); + } finally { + containers.stopAndRemove(containerId); + cleanupTempDir(outputDir); + } + } + + private Path createTempDir() { + try { + return Files.createTempDirectory(getTempDirPrefix()); + } catch (IOException e) { + throw new RuntimeException("Failed to create temporary output directory", e); + } + } + + private String createContainer(String image, String filename, Path absolutePath, Path outputDir) { + var inputBind = Bind.parse(absolutePath.getParent() + ":/input:ro"); + var outputBind = Bind.parse(outputDir.toAbsolutePath() + ":/output:rw"); + + var containerId = + containers.createContainer(image, new String[] {filename}, inputBind, outputBind); + containers.startContainer(containerId); + return containerId; + } + + private static void validateExitCode(String configKey, int exitCode, String logs) { + if (exitCode != 0) { + log.error("{} container exited with code {}: {}", configKey, exitCode, logs); + throw new RuntimeException( + configKey + " analysis failed with exit code " + exitCode + ". Output: " + logs); + } + } + + private static Path validateOutputFile(String configKey, Path outputDir, String logs) { + var outputFile = outputDir.resolve("output.json"); + if (!Files.exists(outputFile)) { + throw new RuntimeException( + configKey + " analysis did not produce output.json. Container logs: " + logs); + } + + return outputFile; + } + + private static void cleanupTempDir(Path dir) { + try (var walk = Files.walk(dir)) { + walk.sorted(Comparator.reverseOrder()).forEach(AbstractIdaEngineService::deleteSilently); + } catch (IOException ignored) { + } + } + + private static void deleteSilently(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + } + + private static String readStringTolerant(Path path) throws IOException { + var decoder = + StandardCharsets.UTF_8 + .newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + return decoder.decode(ByteBuffer.wrap(Files.readAllBytes(path))).toString(); + } + + private static AnalysisResult parseOutput(String raw) { + var text = raw.replace("\r\n", "\n"); + + var entryPoint = parseEntryPoint(text); + var segments = parseSegments(text); + + var functions = new ArrayList(); + + var sections = text.split("\"function\"\\s*:\\s*\""); + for (int i = 1; i < sections.length; i++) { + var func = parseFunctionEntry(sections[i]); + if (Objects.nonNull(func)) { + functions.add(func); + } + } + + if (functions.isEmpty()) { + log.warn( + "IDA produced no parsable function entries. Raw output (first 500 chars): {}", + raw.length() > 500 ? raw.substring(0, 500) : raw); + } + + return new AnalysisResult(entryPoint, segments, functions); + } + + private static String parseEntryPoint(String text) { + var entryRemainder = after(text, KEY_ENTRY_POINT); + if (Objects.isNull(entryRemainder)) { + return "unknown"; + } + + var entryVal = extractField(entryRemainder, "\""); + if (Objects.isNull(entryVal)) { + return "unknown"; + } + + return entryVal; + } + + private static FunctionResult parseFunctionEntry(String section) { + var name = extractField(section, KEY_ADDRESS); + if (Objects.isNull(name)) { + return null; + } + + var remainder = after(section, KEY_ADDRESS); + if (Objects.isNull(remainder)) { + return null; + } + var address = extractField(remainder, KEY_SIZE); + if (Objects.isNull(address)) { + return null; + } + + remainder = after(remainder, KEY_SIZE); + if (Objects.isNull(remainder)) { + return null; + } + var sizeStr = extractField(remainder, KEY_FLAGS); + if (Objects.isNull(sizeStr)) { + return null; + } + var size = parseSize(sizeStr); + + remainder = after(remainder, KEY_FLAGS); + if (Objects.isNull(remainder)) { + return null; + } + var flags = extractField(remainder, KEY_XREFS); + if (Objects.isNull(flags)) { + return null; + } + + remainder = after(remainder, KEY_XREFS); + if (Objects.isNull(remainder)) { + return null; + } + var xrefsRaw = extractField(remainder, KEY_ASSEMBLY); + if (Objects.isNull(xrefsRaw)) { + return null; + } + + remainder = after(remainder, KEY_ASSEMBLY); + if (Objects.isNull(remainder)) { + return null; + } + var assembly = extractField(remainder, KEY_LABELS); + if (Objects.isNull(assembly)) { + return null; + } + + remainder = after(remainder, KEY_LABELS); + if (Objects.isNull(remainder)) { + return null; + } + var labelsRaw = extractField(remainder, KEY_ENTRY_END); + if (Objects.isNull(labelsRaw)) { + return null; + } + + var xrefs = parseXrefs(xrefsRaw.trim()); + var labels = parseLabels(labelsRaw.trim()); + return new FunctionResult( + name, + address, + size, + flags.endsWith("|") ? flags.substring(0, flags.length() - 1) : flags, + assembly, + null, + null, + xrefs, + labels); + } + + private static int parseSize(String sizeStr) { + try { + return Integer.parseInt(sizeStr.trim()); + } catch (NumberFormatException e) { + return 0; + } + } + + private static String extractField(String text, String endDelim) { + var end = text.indexOf(endDelim); + return end < 0 ? null : text.substring(0, end); + } + + private static String after(String text, String delim) { + var idx = text.indexOf(delim); + return idx < 0 ? null : text.substring(idx + delim.length()); + } + + private static List parseSegments(String text) { + var segments = new ArrayList(); + var remainder = after(text, KEY_SEGMENTS_START); + if (Objects.isNull(remainder)) { + return segments; + } + + var segmentsBlock = extractField(remainder, "\n ],\n"); + if (Objects.isNull(segmentsBlock)) { + return segments; + } + + var parts = segmentsBlock.split("},\\s*\\{"); + for (var part : parts) { + var segment = parseSegmentEntry(part); + if (Objects.nonNull(segment)) { + segments.add(segment); + } + } + + return segments; + } + + private static SegmentEntry parseSegmentEntry(String part) { + var clean = part.replace("{", "").replace("}", "").trim(); + if (clean.isBlank()) { + return null; + } + + var nameRemainder = after(clean, KEY_SEGMENT_NAME); + if (Objects.isNull(nameRemainder)) { + return null; + } + + var name = extractField(nameRemainder, KEY_SEGMENT_START); + if (Objects.isNull(name)) { + return null; + } + + var startRemainder = after(nameRemainder, KEY_SEGMENT_START); + if (Objects.isNull(startRemainder)) { + return null; + } + + var startAddress = extractField(startRemainder, KEY_SEGMENT_END); + if (Objects.isNull(startAddress)) { + return null; + } + + var endRemainder = after(startRemainder, KEY_SEGMENT_END); + if (Objects.isNull(endRemainder)) { + return null; + } + + var endAddress = extractField(endRemainder, "\""); + if (Objects.isNull(endAddress)) { + return null; + } + + return new SegmentEntry(name, startAddress, endAddress); + } + + private static List parseXrefs(String raw) { + if (Objects.isNull(raw) || raw.isBlank()) { + return List.of(); + } + + return Arrays.stream(raw.trim().split("\\s+")) + .map(AbstractIdaEngineService::parseXrefEntry) + .toList(); + } + + private static XrefEntry parseXrefEntry(String token) { + var colon = token.lastIndexOf(':'); + if (colon < 0) { + return new XrefEntry(token, "CALL"); + } + + return new XrefEntry(token.substring(0, colon), token.substring(colon + 1)); + } + + private static List parseLabels(String raw) { + if (Objects.isNull(raw) || raw.isBlank()) { + return List.of(); + } + + return Arrays.stream(raw.trim().split("\\s+")) + .distinct() + .map(AbstractIdaEngineService::parseLabelEntry) + .toList(); + } + + private static LabelEntry parseLabelEntry(String token) { + var lastColon = token.lastIndexOf(':'); + if (lastColon < 0) { + return new LabelEntry(token, "", "CALL"); + } + + var type = token.substring(lastColon + 1); + var nameAddr = token.substring(0, lastColon); + var addrColon = nameAddr.lastIndexOf(':'); + if (addrColon < 0) { + return new LabelEntry(nameAddr, "", type); + } + + return new LabelEntry( + nameAddr.substring(0, addrColon), nameAddr.substring(addrColon + 1), type); + } +} diff --git a/src/main/java/ai/decompile/engine/service/Ida5EngineService.java b/src/main/java/ai/decompile/engine/service/Ida5EngineService.java index c4c01b8..7177453 100644 --- a/src/main/java/ai/decompile/engine/service/Ida5EngineService.java +++ b/src/main/java/ai/decompile/engine/service/Ida5EngineService.java @@ -2,353 +2,34 @@ package ai.decompile.engine.service; import ai.decompile.docker.service.ContainerService; import ai.decompile.engine.config.EngineProperties; -import ai.decompile.engine.model.AnalysisResult; import ai.decompile.engine.model.EngineType; -import ai.decompile.engine.model.FunctionResult; -import ai.decompile.engine.model.LabelEntry; -import ai.decompile.engine.model.SegmentEntry; -import ai.decompile.engine.model.XrefEntry; -import com.github.dockerjava.api.model.Bind; -import java.io.File; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; -@Log4j2 @Service -@RequiredArgsConstructor @ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true") -public class Ida5EngineService implements EngineService { - private static final String DOCKERFILE_PATH = "engines/ida5/Dockerfile.ida5"; - private static final String KEY_ENTRY_POINT = "\"entry_point\": \""; - private static final String KEY_ADDRESS = "\",\n \"address\": \""; - private static final String KEY_SIZE = "\",\n \"size\": "; - private static final String KEY_FLAGS = ",\n \"flags\": \""; - private static final String KEY_XREFS = "\",\n \"xrefs\": \""; - private static final String KEY_ASSEMBLY = "\",\n \"assembly\": \""; - private static final String KEY_LABELS = "\",\n \"labels\": \""; - private static final String KEY_ENTRY_END = "\"\n }"; - private static final String KEY_SEGMENTS_START = "\"segments\": ["; - private static final String KEY_SEGMENT_NAME = "\"name\": \""; - private static final String KEY_SEGMENT_START = "\", \"start\": \""; - private static final String KEY_SEGMENT_END = "\", \"end\": \""; - - private final ContainerService containers; - private final EngineProperties engineProperties; +public class Ida5EngineService extends AbstractIdaEngineService { + public Ida5EngineService(ContainerService containers, EngineProperties engineProperties) { + super(containers, engineProperties); + } @Override - public AnalysisResult analyze(String filePath) { - var config = engineProperties.getEngineConfig("ida5"); - var absolutePath = Path.of(filePath).toAbsolutePath().normalize(); - var filename = absolutePath.getFileName().toString(); + protected String getDockerfilePath() { + return "engines/ida5/Dockerfile.ida5"; + } - containers.ensureImage(config.getImage(), new File(DOCKERFILE_PATH)); + @Override + protected String getConfigKey() { + return "ida5"; + } - Path outputDir = createTempDir(); - var containerId = createContainer(config.getImage(), filename, absolutePath, outputDir); - - try { - var exitCode = containers.waitForExit(containerId, config.getTimeoutSeconds()); - var logs = containers.captureLogs(containerId, 2); - - validateExitCode(exitCode, logs); - var outputFile = validateOutputFile(outputDir, logs); - return parseOutput(readStringTolerant(outputFile)); - } catch (Exception e) { - log.error("IDA5 analysis failed for file {}: {}", filePath, e.getMessage()); - throw new RuntimeException("IDA5 analysis failed: " + e.getMessage(), e); - } finally { - containers.stopAndRemove(containerId); - cleanupTempDir(outputDir); - } + @Override + protected String getTempDirPrefix() { + return "ida5-output-"; } @Override public EngineType supportedEngine() { return EngineType.IDA5; } - - private static Path createTempDir() { - try { - return Files.createTempDirectory("ida5-output-"); - } catch (IOException e) { - throw new RuntimeException("Failed to create temporary output directory", e); - } - } - - private String createContainer(String image, String filename, Path absolutePath, Path outputDir) { - var inputBind = Bind.parse(absolutePath.getParent() + ":/input:ro"); - var outputBind = Bind.parse(outputDir.toAbsolutePath() + ":/output:rw"); - - var containerId = - containers.createContainer(image, new String[] {filename}, inputBind, outputBind); - containers.startContainer(containerId); - return containerId; - } - - private static void validateExitCode(int exitCode, String logs) { - if (exitCode != 0) { - log.error("IDA5 container exited with code {}: {}", exitCode, logs); - throw new RuntimeException( - "IDA5 analysis failed with exit code " + exitCode + ". Output: " + logs); - } - } - - private static Path validateOutputFile(Path outputDir, String logs) { - var outputFile = outputDir.resolve("output.json"); - if (!Files.exists(outputFile)) { - throw new RuntimeException( - "IDA5 analysis did not produce output.json. Container logs: " + logs); - } - return outputFile; - } - - private static void cleanupTempDir(Path dir) { - try (var walk = Files.walk(dir)) { - walk.sorted(Comparator.reverseOrder()).forEach(Ida5EngineService::deleteSilently); - } catch (IOException ignored) { - } - } - - private static void deleteSilently(Path path) { - try { - Files.deleteIfExists(path); - } catch (IOException ignored) { - } - } - - private static String readStringTolerant(Path path) throws IOException { - var decoder = - StandardCharsets.UTF_8 - .newDecoder() - .onMalformedInput(CodingErrorAction.REPLACE) - .onUnmappableCharacter(CodingErrorAction.REPLACE); - return decoder.decode(ByteBuffer.wrap(Files.readAllBytes(path))).toString(); - } - - private static AnalysisResult parseOutput(String raw) { - var text = raw.replace("\r\n", "\n"); - - var entryRemainder = after(text, KEY_ENTRY_POINT); - var entryPoint = "unknown"; - if (Objects.nonNull(entryRemainder)) { - var entryVal = extractField(entryRemainder, "\""); - if (Objects.nonNull(entryVal)) { - entryPoint = entryVal; - } - } - - var segments = parseSegments(text); - - var functions = new ArrayList(); - - var sections = text.split("\"function\"\\s*:\\s*\""); - for (int i = 1; i < sections.length; i++) { - var section = sections[i]; - - var name = extractField(section, KEY_ADDRESS); - if (name == null) { - continue; - } - - var remainder = after(section, KEY_ADDRESS); - if (remainder == null) { - continue; - } - var address = extractField(remainder, KEY_SIZE); - if (address == null) { - continue; - } - - remainder = after(remainder, KEY_SIZE); - if (remainder == null) { - continue; - } - var sizeStr = extractField(remainder, KEY_FLAGS); - if (sizeStr == null) { - continue; - } - int size; - try { - size = Integer.parseInt(sizeStr.trim()); - } catch (NumberFormatException e) { - size = 0; - } - - remainder = after(remainder, KEY_FLAGS); - if (remainder == null) { - continue; - } - var flags = extractField(remainder, KEY_XREFS); - if (Objects.isNull(flags)) { - continue; - } - - remainder = after(remainder, KEY_XREFS); - if (remainder == null) { - continue; - } - var xrefsRaw = extractField(remainder, KEY_ASSEMBLY); - if (xrefsRaw == null) { - continue; - } - - remainder = after(remainder, KEY_ASSEMBLY); - if (remainder == null) { - continue; - } - var assembly = extractField(remainder, KEY_LABELS); - if (assembly == null) { - continue; - } - - remainder = after(remainder, KEY_LABELS); - if (remainder == null) { - continue; - } - var labelsRaw = extractField(remainder, KEY_ENTRY_END); - if (labelsRaw == null) { - continue; - } - - var xrefs = parseXrefs(xrefsRaw.trim()); - var labels = parseLabels(labelsRaw.trim()); - functions.add( - new FunctionResult( - name, - address, - size, - flags.endsWith("|") ? flags.substring(0, flags.length() - 1) : flags, - assembly, - null, - null, - xrefs, - labels)); - } - - if (functions.isEmpty()) { - log.warn( - "IDA5 produced no parsable function entries. Raw output (first 500 chars): {}", - raw.length() > 500 ? raw.substring(0, 500) : raw); - } - - return new AnalysisResult(entryPoint, segments, functions); - } - - private static String extractField(String text, String endDelim) { - int end = text.indexOf(endDelim); - return end < 0 ? null : text.substring(0, end); - } - - private static String after(String text, String delim) { - int idx = text.indexOf(delim); - return idx < 0 ? null : text.substring(idx + delim.length()); - } - - private static List parseSegments(String text) { - var segments = new ArrayList(); - var remainder = after(text, KEY_SEGMENTS_START); - if (Objects.isNull(remainder)) { - return segments; - } - - var segmentsBlock = extractField(remainder, "\n ],\n"); - if (Objects.isNull(segmentsBlock)) { - return segments; - } - - var parts = segmentsBlock.split("},\\s*\\{"); - for (var part : parts) { - var clean = part.replace("{", "").replace("}", "").trim(); - if (clean.isBlank()) { - continue; - } - - var nameRemainder = after(clean, KEY_SEGMENT_NAME); - if (Objects.isNull(nameRemainder)) { - continue; - } - - var name = extractField(nameRemainder, KEY_SEGMENT_START); - if (Objects.isNull(name)) { - continue; - } - - var startRemainder = after(nameRemainder, KEY_SEGMENT_START); - if (Objects.isNull(startRemainder)) { - continue; - } - - var startAddress = extractField(startRemainder, KEY_SEGMENT_END); - if (Objects.isNull(startAddress)) { - continue; - } - - var endRemainder = after(startRemainder, KEY_SEGMENT_END); - if (Objects.isNull(endRemainder)) { - continue; - } - - var endAddress = extractField(endRemainder, "\""); - if (Objects.isNull(endAddress)) { - continue; - } - - segments.add(new SegmentEntry(name, startAddress, endAddress)); - } - - return segments; - } - - private static List parseXrefs(String raw) { - if (Objects.isNull(raw) || raw.isBlank()) { - return List.of(); - } - - return Arrays.stream(raw.trim().split("\\s+")).map(Ida5EngineService::parseXrefEntry).toList(); - } - - private static XrefEntry parseXrefEntry(String token) { - var colon = token.lastIndexOf(':'); - if (colon < 0) { - return new XrefEntry(token, "CALL"); - } - - return new XrefEntry(token.substring(0, colon), token.substring(colon + 1)); - } - - private static List parseLabels(String raw) { - if (raw == null || raw.isBlank()) { - return List.of(); - } - return Arrays.stream(raw.trim().split("\\s+")) - .distinct() - .map(Ida5EngineService::parseLabelEntry) - .toList(); - } - - private static LabelEntry parseLabelEntry(String token) { - var lastColon = token.lastIndexOf(':'); - if (lastColon < 0) { - return new LabelEntry(token, "", "CALL"); - } - - var type = token.substring(lastColon + 1); - var nameAddr = token.substring(0, lastColon); - var addrColon = nameAddr.lastIndexOf(':'); - if (addrColon < 0) { - return new LabelEntry(nameAddr, "", type); - } - - return new LabelEntry( - nameAddr.substring(0, addrColon), nameAddr.substring(addrColon + 1), type); - } } diff --git a/src/main/java/ai/decompile/engine/service/Ida66EngineService.java b/src/main/java/ai/decompile/engine/service/Ida66EngineService.java new file mode 100644 index 0000000..7e13615 --- /dev/null +++ b/src/main/java/ai/decompile/engine/service/Ida66EngineService.java @@ -0,0 +1,35 @@ +package ai.decompile.engine.service; + +import ai.decompile.docker.service.ContainerService; +import ai.decompile.engine.config.EngineProperties; +import ai.decompile.engine.model.EngineType; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true") +public class Ida66EngineService extends AbstractIdaEngineService { + public Ida66EngineService(ContainerService containers, EngineProperties engineProperties) { + super(containers, engineProperties); + } + + @Override + protected String getDockerfilePath() { + return "engines/ida66/Dockerfile.ida66"; + } + + @Override + protected String getConfigKey() { + return "ida66"; + } + + @Override + protected String getTempDirPrefix() { + return "ida66-output-"; + } + + @Override + public EngineType supportedEngine() { + return EngineType.IDA66; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index eca8b23..562e6cf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -54,6 +54,9 @@ app: ida5: image: decompile-ai/ida5:latest timeout-seconds: 300 + ida66: + image: decompile-ai/ida66:latest + timeout-seconds: 300 ghidra: image: decompile-ai/ghidra:latest timeout-seconds: 600