#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
dump_idb.py  (v2 - malware-analysis enriched)
IDAPython script for IDA Pro 9.3 (and compatible 9.x versions).

Extracts database metadata, imports, exports, strings, and functions with
Hex-Rays decompilation into a single consolidated JSON file.

v2 additions (for the "Malware analysis" agentic tool):
  - Fixed string extraction (forces the string list to be built, not just the
    already-displayed one) + per-string xrefs (which functions use it).
  - Per-function cross references:
        callees      : function addresses this function calls
        callers      : function addresses that call this function (xrefs-to)
        api_calls    : imported APIs referenced (name + module)
        string_refs  : strings referenced (address + value)
  - metadata.entry_point / image_base.
These precomputed relations let a downstream analyzer navigate the call graph
and pivot on APIs/strings in O(1) instead of grepping every function.

The JSON stays BACKWARD COMPATIBLE: all v1 keys are preserved; the new keys are
purely additive, so old consumers keep working.
"""

import json
import os
import idaapi
import ida_nalt
import ida_ida
import ida_funcs
import ida_hexrays
import ida_lines
import ida_loader
import ida_typeinf
import ida_auto
import ida_bytes
import idautils
import idc

# ---- Gioi han an toan (tranh JSON phinh to voi binary khong lo) ----
MAX_STRINGS = 200000
MAX_XREFS_PER_ITEM = 400          # callers/callees toi da liet ke moi ham
MAX_STRING_REFS_PER_FUNC = 80     # string moi ham
STRING_VALUE_TRUNC = 200          # cat gia tri string dai
MIN_STRING_LEN = 4


def bytes_to_str(val):
    """Chuyen bytes -> UTF-8 (hoac hex neu that bai)."""
    if isinstance(val, bytes):
        try:
            return val.decode('utf-8', errors='replace')
        except Exception:
            return val.hex()
    return str(val)


def hx(ea):
    """Dia chi -> chuoi hex chuan '0x...' (thong nhat toan file)."""
    try:
        return f"0x{ea:X}"
    except Exception:
        return str(ea)


def get_string_value(s):
    """Lay noi dung string cua 1 item idautils.Strings mot cach an toan."""
    try:
        val = str(s)
        if isinstance(val, bytes):
            return val.decode('utf-8', errors='replace')
        return val
    except Exception:
        try:
            val = s.string
            if isinstance(val, bytes):
                return val.decode('utf-8', errors='replace')
            return str(val)
        except Exception:
            return ""


def get_string_type_name(str_type):
    """Ma dinh dang string cua IDA -> ten doc duoc."""
    mapping = {
        0: "ASCII", 1: "UTF-16LE", 2: "UTF-32LE",
        3: "Pascal (1-byte length)", 4: "Pascal (2-byte length)",
        5: "Pascal (4-byte length)", 6: "UTF-16BE", 7: "UTF-32BE",
    }
    return mapping.get(str_type, f"Format {str_type}")


def get_output_path():
    """Duong dan JSON dich, dat canh file IDB."""
    idb_path = bytes_to_str(idc.get_idb_path())
    if not idb_path:
        idb_path = bytes_to_str(ida_nalt.get_input_file_path())
    if not idb_path:
        idb_path = os.path.join(os.getcwd(), "dump.idb")
    base_dir = os.path.dirname(idb_path)
    base_name = os.path.splitext(os.path.basename(idb_path))[0] or "ida_dump"
    return os.path.join(base_dir, f"{base_name}_dump_full.json")


def get_metadata():
    """Metadata cua binary + database."""
    print("[*] Extracting metadata...")
    metadata = {}
    try:
        metadata["input_file_path"] = bytes_to_str(ida_nalt.get_input_file_path())
        metadata["root_filename"] = bytes_to_str(ida_nalt.get_root_filename())
    except Exception as e:
        metadata["input_file_path"] = ""
        metadata["root_filename"] = ""
        print(f"[-] Error getting input file path: {e}")
    try:
        metadata["md5"] = bytes_to_str(ida_nalt.retrieve_input_file_md5())
    except Exception as e:
        metadata["md5"] = ""
        print(f"[-] Error getting MD5: {e}")
    try:
        metadata["sha256"] = bytes_to_str(ida_nalt.retrieve_input_file_sha256())
    except Exception as e:
        metadata["sha256"] = ""
        print(f"[-] Error getting SHA256: {e}")
    try:
        metadata["processor"] = bytes_to_str(ida_ida.inf_get_procname())
    except Exception as e:
        metadata["processor"] = ""
        print(f"[-] Error getting processor name: {e}")
    try:
        if ida_ida.inf_is_64bit():
            metadata["bitness"] = "64-bit"
        elif ida_ida.inf_is_32bit_exactly():
            metadata["bitness"] = "32-bit"
        else:
            metadata["bitness"] = "unknown"
    except Exception as e:
        metadata["bitness"] = "unknown"
        print(f"[-] Error getting bitness: {e}")
    try:
        metadata["file_type"] = bytes_to_str(ida_loader.get_file_type_name())
    except Exception as e:
        metadata["file_type"] = ""
        print(f"[-] Error getting file type: {e}")
    try:
        compiler_info = idc.get_inf_attr(idc.INF_COMPILER)
        if compiler_info:
            metadata["compiler"] = bytes_to_str(ida_typeinf.get_compiler_name(compiler_info.id))
        else:
            metadata["compiler"] = "unknown"
    except Exception as e:
        metadata["compiler"] = "unknown"
        print(f"[-] Error getting compiler: {e}")
    try:
        metadata["abi"] = bytes_to_str(ida_typeinf.get_abi_name())
    except Exception as e:
        metadata["abi"] = "unknown"
        print(f"[-] Error getting ABI: {e}")
    try:
        metadata["ida_sdk_version"] = idaapi.IDA_SDK_VERSION
    except Exception as e:
        metadata["ida_sdk_version"] = "unknown"
        print(f"[-] Error getting SDK version: {e}")
    # v2: entry point + image base (diem bat dau dieu tra malware)
    try:
        metadata["entry_point"] = hx(ida_ida.inf_get_start_ea())
    except Exception:
        try:
            metadata["entry_point"] = hx(idc.get_inf_attr(idc.INF_START_EA))
        except Exception:
            metadata["entry_point"] = ""
    try:
        metadata["image_base"] = hx(idaapi.get_imagebase())
    except Exception:
        metadata["image_base"] = ""
    return metadata


def get_import_map():
    """Map dia_chi_import -> {name, module}. Dung de nhan dien API call trong tung ham."""
    print("[*] Building import map...")
    imports_list = []
    import_ea_map = {}
    current = []

    def imp_cb(ea, name, ordinal):
        entry = {"address": hx(ea), "name": bytes_to_str(name) if name else None,
                 "ordinal": int(ordinal)}
        current.append(entry)
        return True

    try:
        qty = ida_nalt.get_import_module_qty()
        for i in range(qty):
            module_name = bytes_to_str(ida_nalt.get_import_module_name(i)) or f"module_{i}"
            current = []
            ida_nalt.enum_import_names(i, imp_cb)
            for e in current:
                if e["name"]:
                    try:
                        import_ea_map[int(e["address"], 16)] = (e["name"], module_name)
                    except Exception:
                        pass
            imports_list.append({"module": module_name, "functions": current})
    except Exception as e:
        print(f"[-] Error walking imports: {e}")
    return imports_list, import_ea_map


def get_exports():
    """Cac export entry point."""
    print("[*] Extracting exports...")
    exports_list = []
    try:
        for exp_i, exp_ord, exp_ea, exp_name in idautils.Entries():
            exports_list.append({"address": hx(exp_ea), "name": bytes_to_str(exp_name),
                                 "ordinal": int(exp_ord)})
    except Exception as e:
        print(f"[-] Error walking exports: {e}")
    return exports_list


def get_strings_with_xrefs():
    """Extract strings (ep IDA build lai danh sach, khong chi lay cai da hien).
    Kem xrefs: ham nao tham chieu string do. Tra (strings_list, string_ea_map)."""
    print("[*] Extracting strings (with xrefs)...")
    strings_list = []
    string_ea_map = {}
    try:
        sc = idautils.Strings()
        # QUAN TRONG: display_only_existing_strings=False -> ep quet toan bo, tranh danh sach RONG
        try:
            sc.setup(strtypes=[ida_nalt.STRTYPE_C, ida_nalt.STRTYPE_C_16],
                     minlen=MIN_STRING_LEN, only_7bit=False,
                     display_only_existing_strings=False)
        except Exception:
            try:
                sc.setup(minlen=MIN_STRING_LEN, only_7bit=False,
                         display_only_existing_strings=False)
            except Exception:
                pass
        count = 0
        for s in sc:
            if count >= MAX_STRINGS:
                print(f"[!] Reached string limit {MAX_STRINGS}. Skipping rest.")
                break
            val = get_string_value(s)
            # xrefs: ham nao dung string nay
            xr = []
            try:
                for r in idautils.DataRefsTo(s.ea):
                    fn = ida_funcs.get_func(r)
                    if fn:
                        h = hx(fn.start_ea)
                        if h not in xr:
                            xr.append(h)
                            if len(xr) >= MAX_XREFS_PER_ITEM:
                                break
            except Exception:
                pass
            entry = {"address": hx(s.ea), "length": int(s.length),
                     "type": get_string_type_name(s.type),
                     "value": val[:STRING_VALUE_TRUNC], "xrefs": xr}
            strings_list.append(entry)
            string_ea_map[s.ea] = val[:STRING_VALUE_TRUNC]
            count += 1
        print(f"[+] Extracted {len(strings_list)} strings.")
    except Exception as e:
        print(f"[-] Error extracting strings: {e}")
    return strings_list, string_ea_map


def compute_func_relations(func, func_starts, import_ea_map, string_ea_map):
    """Duyet instruction trong 1 ham -> (callees, api_calls, string_refs).
    callees: dia chi ham duoc goi; api_calls: API import; string_refs: string dung."""
    callees = []
    apis = {}
    srefs = []
    seen_callee = set()
    seen_sref = set()
    try:
        for head in idautils.Heads(func.start_ea, func.end_ea):
            # code refs (call/jmp toi ham khac)
            try:
                for ref in idautils.CodeRefsFrom(head, 0):
                    if ref in func_starts and ref != func.start_ea and ref not in seen_callee:
                        seen_callee.add(ref)
                        if len(callees) < MAX_XREFS_PER_ITEM:
                            callees.append(hx(ref))
                    if ref in import_ea_map:
                        nm, mod = import_ea_map[ref]
                        apis[nm] = mod
            except Exception:
                pass
            # data refs (call qua import thunk, tham chieu string)
            try:
                for ref in idautils.DataRefsFrom(head):
                    if ref in import_ea_map:
                        nm, mod = import_ea_map[ref]
                        apis[nm] = mod
                    elif ref in string_ea_map and ref not in seen_sref:
                        seen_sref.add(ref)
                        if len(srefs) < MAX_STRING_REFS_PER_FUNC:
                            srefs.append({"address": hx(ref), "value": string_ea_map[ref]})
            except Exception:
                pass
    except Exception as e:
        print(f"[-] relation scan failed @ {hx(func.start_ea)}: {e}")
    api_list = [{"name": n, "module": m} for n, m in sorted(apis.items())]
    return callees, api_list, srefs


def get_callers(ea):
    """Ham/dia chi nao goi (xrefs-to) toi dau ham nay."""
    callers = []
    seen = set()
    try:
        for r in idautils.CodeRefsTo(ea, 0):
            fn = ida_funcs.get_func(r)
            if fn and fn.start_ea != ea and fn.start_ea not in seen:
                seen.add(fn.start_ea)
                callers.append(hx(fn.start_ea))
                if len(callers) >= MAX_XREFS_PER_ITEM:
                    break
    except Exception:
        pass
    return callers


def get_functions(import_ea_map, string_ea_map):
    """Ham + decompile + quan he (callees/callers/api_calls/string_refs)."""
    funcs_list = []
    hexrays_available = False
    try:
        if ida_hexrays.init_hexrays_plugin():
            hexrays_available = True
            print("[+] Hex-Rays decompiler is available.")
        else:
            print("[-] Hex-Rays decompiler not available/licensed.")
    except Exception as e:
        print(f"[-] Hex-Rays check failed: {e}")

    print("[*] Extracting functions + decompile + relations...")
    try:
        functions_eas = list(idautils.Functions())
        func_starts = set(functions_eas)
        total = len(functions_eas)
        print(f"[+] Found {total} functions.")
        for idx, ea in enumerate(functions_eas, 1):
            if idx % 100 == 0 or idx == total:
                print(f"    Processed {idx}/{total} functions...")
            func = ida_funcs.get_func(ea)
            if not func:
                continue
            name = bytes_to_str(ida_funcs.get_func_name(ea))
            seg_name = ""
            try:
                seg_name = bytes_to_str(idc.get_segm_name(ea))
            except Exception:
                pass
            decompiled_code = None
            if hexrays_available:
                try:
                    cfunc = ida_hexrays.decompile(ea)
                    if cfunc:
                        decompiled_code = "\n".join(
                            ida_lines.tag_remove(sl.line) for sl in cfunc.get_pseudocode())
                except Exception:
                    decompiled_code = None
            callees, api_calls, string_refs = compute_func_relations(
                func, func_starts, import_ea_map, string_ea_map)
            callers = get_callers(func.start_ea)
            funcs_list.append({
                "address": hx(func.start_ea),
                "end_address": hx(func.end_ea),
                "name": name,
                "size": int(func.size()),
                "segment": seg_name,
                "decompiled_code": decompiled_code,
                "callees": callees,
                "callers": callers,
                "api_calls": api_calls,
                "string_refs": string_refs,
            })
    except Exception as e:
        print(f"[-] Error enumerating functions: {e}")
    return funcs_list


def main():
    print("[+] dump_idb.py v2 started...")
    print("[*] Waiting for auto-analysis to complete...")
    ida_auto.auto_wait()
    print("[+] Auto-analysis complete!")

    metadata = get_metadata()
    imports, import_ea_map = get_import_map()
    exports = get_exports()
    strings, string_ea_map = get_strings_with_xrefs()
    functions = get_functions(import_ea_map, string_ea_map)

    dump_data = {
        "schema_version": 2,
        "metadata": metadata,
        "imports": imports,
        "exports": exports,
        "strings": strings,
        "functions": functions,
    }

    output_path = get_output_path()
    print(f"[*] Saving dump data to: {output_path}")
    try:
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(dump_data, f, indent=2, ensure_ascii=False)
        print(f"[+] Exported: {output_path}")
        print(f"[+] Summary: {len(functions)} functions, {len(strings)} strings, "
              f"{sum(len(m.get('functions', [])) for m in imports)} imported APIs.")
    except Exception as e:
        print(f"[-] Failed to write JSON output: {e}")


if __name__ == "__main__":
    main()
