From 7eaa260114cb00006b1abdde97b9cc2fcfac2e33 Mon Sep 17 00:00:00 2001 From: Anubhav Dash <83800189+AnubhavDash@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:53:06 +0530 Subject: [PATCH 1/3] Update extract_dispatch.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(ghidra): resolve UnicodeEncodeError in Jython environment Address a critical crash (UnicodeEncodeError) occurring during the decompilation of drivers containing non-ASCII characters (e.g., RtsUer.sys). Because Ghidra's Python 2.7 (Jython) environment defaults to ASCII for I/O operations, explicit UTF-8 handling has been implemented. Key Changes: 1. Global Encoding Stability: - Imported 'io' module to utilize 'io.open', which backports Python 3 encoding support to the Jython 2.7 environment. - Cleaned up "mojibake" (corrupted characters like ΓÇö) in docstrings and comments to ensure the script source is valid UTF-8. 2. Robust File I/O: - Migrated all 'open()' calls to 'io.open(..., encoding="utf-8")' for consistent handling of decompiled C code across different locales. - Applied 'unicode()' casting to decompilation buffers before writing. This is the primary fix to prevent crashes when encountering characters like u'\uffe0'. 3. Unicode String Literals: - Prefixed divider strings and UI headers with 'u' (e.g., u"\n\n// =") to ensure string concatenation remains within the Unicode domain, preventing implicit ASCII downcasting crashes. 4. JSON Output Integrity: - Updated 'json.dumps' with 'ensure_ascii=False'. This ensures that special characters are preserved as actual characters in 'ghidra_result.json' rather than escaped sequences, improving readability and downstream parsing. 5. Refactored Error Handling: - Consolidated crash-logging logic to use the centralized 'write_result' function. This prevents a secondary crash from occurring inside the exception handler when the error message itself contains non-ASCII traceback data. --- .../scripts/extract_dispatch.py | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/processors/ghidra_decompile/scripts/extract_dispatch.py b/processors/ghidra_decompile/scripts/extract_dispatch.py index b1bb3c6..a0dc276 100644 --- a/processors/ghidra_decompile/scripts/extract_dispatch.py +++ b/processors/ghidra_decompile/scripts/extract_dispatch.py @@ -7,6 +7,7 @@ import json import os import re +import io # Added for stable UTF-8 file writing from ghidra.app.decompiler import DecompileOptions, DecompInterface @@ -95,7 +96,7 @@ def _resolve_handler_name(fm, name): if candidate.getName() == name: return candidate - # just a symbol/label ΓÇö try to create a function at that address + # just a symbol/label — try to create a function at that address syms = currentProgram.getSymbolTable().getSymbols(name) if syms.hasNext(): sym = syms.next() @@ -257,8 +258,9 @@ def main(): result["driver_entry_c"] = entry_c # write DriverEntry decompilation - with open(os.path.join(output_dir, "driver_entry.c"), "w") as f: - f.write(entry_c) + # FIX: Use io.open with utf-8 encoding and unicode() cast for Jython stability + with io.open(os.path.join(output_dir, "driver_entry.c"), "w", encoding="utf-8") as f: + f.write(unicode(entry_c)) # extract device name and symbolic link from strings for s in currentProgram.getListing().getDefinedData(True): @@ -298,7 +300,7 @@ def main(): max_subfuncs = 60 def _is_external(name): - """skip known external api calls ΓÇö follow everything else""" + """skip known external api calls — follow everything else""" for prefix in EXTERNAL_PREFIXES: if name.startswith(prefix): return True @@ -327,15 +329,16 @@ def get_subfuncs(func, depth=0): # append subfunctions to the dispatch text so they are included in all output files if subfuncs_c: - dispatch_c += "\n\n// " + "=" * 40 + " //\n" - dispatch_c += "// INTERNAL SUBFUNCTIONS CALLED BY DISPATCH //\n" - dispatch_c += "// " + "=" * 40 + " //\n\n" - dispatch_c += "\n\n".join(subfuncs_c) + dispatch_c += u"\n\n// " + u"=" * 40 + u" //\n" + dispatch_c += u"// INTERNAL SUBFUNCTIONS CALLED BY DISPATCH //\n" + dispatch_c += u"// " + u"=" * 40 + u" //\n\n" + dispatch_c += u"\n\n".join(subfuncs_c) # write full decompiled payload result["dispatch_c"] = dispatch_c - with open(os.path.join(output_dir, "dispatch_ioctl.c"), "w") as f: - f.write(dispatch_c) + # FIX: Use io.open with utf-8 encoding and unicode() cast for Jython stability + with io.open(os.path.join(output_dir, "dispatch_ioctl.c"), "w", encoding="utf-8") as f: + f.write(unicode(dispatch_c)) # create ioctls subdirectory ioctls_dir = os.path.join(output_dir, "ioctls") @@ -355,20 +358,23 @@ def get_subfuncs(func, depth=0): result["ioctl_handlers"].append(handler_info) # write per-ioctl file - with open(os.path.join(ioctls_dir, "0x%08X.c" % code), "w") as f: - f.write("// IOCTL Code: 0x%08X\n" % code) - f.write("// Method: %d\n" % (code & 0x3)) - f.write("// Device Type: 0x%04X\n" % ((code >> 16) & 0xFFFF)) - f.write("// Function: 0x%03X\n\n" % ((code >> 2) & 0xFFF)) - f.write(dispatch_c) + # FIX: Use io.open with utf-8 encoding and unicode() cast for Jython stability + with io.open(os.path.join(ioctls_dir, "0x%08X.c" % code), "w", encoding="utf-8") as f: + f.write(u"// IOCTL Code: 0x%08X\n" % code) + f.write(u"// Method: %d\n" % (code & 0x3)) + f.write(u"// Device Type: 0x%04X\n" % ((code >> 16) & 0xFFFF)) + f.write(u"// Function: 0x%03X\n\n" % ((code >> 2) & 0xFFF)) + f.write(unicode(dispatch_c)) result["success"] = True write_result(output_dir, result) def write_result(output_dir, result): - with open(os.path.join(output_dir, "ghidra_result.json"), "w") as f: - json.dump(result, f, indent=2, default=str) + # FIX: Use io.open with utf-8 and ensure_ascii=False for JSON dump + with io.open(os.path.join(output_dir, "ghidra_result.json"), "w", encoding="utf-8") as f: + data = json.dumps(result, indent=2, default=str, ensure_ascii=False) + f.write(unicode(data)) if __name__ == "__main__": @@ -386,5 +392,4 @@ def write_result(output_dir, result): "success": False, "error": "script crashed:\n" + traceback.format_exc(), } - with open(os.path.join(output_dir, "ghidra_result.json"), "w") as f: - json.dump(res, f, indent=2) + write_result(output_dir, res) From 917c87c8475ded7ab5b7b9b61a5fd8e2179a185b Mon Sep 17 00:00:00 2001 From: Anubhav Dash <83800189+AnubhavDash@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:24:19 +0530 Subject: [PATCH 2/3] Update extract_dispatch.py --- processors/ghidra_decompile/scripts/extract_dispatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processors/ghidra_decompile/scripts/extract_dispatch.py b/processors/ghidra_decompile/scripts/extract_dispatch.py index a0dc276..3e2b4bf 100644 --- a/processors/ghidra_decompile/scripts/extract_dispatch.py +++ b/processors/ghidra_decompile/scripts/extract_dispatch.py @@ -4,10 +4,10 @@ # runs inside ghidra's jython environment via analyzeHeadless -postScript # ruff: noqa: F821 +import io # Added for stable UTF-8 file writing import json import os import re -import io # Added for stable UTF-8 file writing from ghidra.app.decompiler import DecompileOptions, DecompInterface From 2fc759c1e6403c6a3a78e73ea8f8d00287171916 Mon Sep 17 00:00:00 2001 From: AnubhavDash Date: Mon, 27 Apr 2026 15:25:54 +0530 Subject: [PATCH 3/3] style: fix ruff sorting and formatting --- .../ghidra_decompile/scripts/extract_dispatch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/processors/ghidra_decompile/scripts/extract_dispatch.py b/processors/ghidra_decompile/scripts/extract_dispatch.py index 3e2b4bf..4f16184 100644 --- a/processors/ghidra_decompile/scripts/extract_dispatch.py +++ b/processors/ghidra_decompile/scripts/extract_dispatch.py @@ -329,10 +329,10 @@ def get_subfuncs(func, depth=0): # append subfunctions to the dispatch text so they are included in all output files if subfuncs_c: - dispatch_c += u"\n\n// " + u"=" * 40 + u" //\n" - dispatch_c += u"// INTERNAL SUBFUNCTIONS CALLED BY DISPATCH //\n" - dispatch_c += u"// " + u"=" * 40 + u" //\n\n" - dispatch_c += u"\n\n".join(subfuncs_c) + dispatch_c += "\n\n// " + "=" * 40 + " //\n" + dispatch_c += "// INTERNAL SUBFUNCTIONS CALLED BY DISPATCH //\n" + dispatch_c += "// " + "=" * 40 + " //\n\n" + dispatch_c += "\n\n".join(subfuncs_c) # write full decompiled payload result["dispatch_c"] = dispatch_c @@ -360,10 +360,10 @@ def get_subfuncs(func, depth=0): # write per-ioctl file # FIX: Use io.open with utf-8 encoding and unicode() cast for Jython stability with io.open(os.path.join(ioctls_dir, "0x%08X.c" % code), "w", encoding="utf-8") as f: - f.write(u"// IOCTL Code: 0x%08X\n" % code) - f.write(u"// Method: %d\n" % (code & 0x3)) - f.write(u"// Device Type: 0x%04X\n" % ((code >> 16) & 0xFFFF)) - f.write(u"// Function: 0x%03X\n\n" % ((code >> 2) & 0xFFF)) + f.write("// IOCTL Code: 0x%08X\n" % code) + f.write("// Method: %d\n" % (code & 0x3)) + f.write("// Device Type: 0x%04X\n" % ((code >> 16) & 0xFFFF)) + f.write("// Function: 0x%03X\n\n" % ((code >> 2) & 0xFFF)) f.write(unicode(dispatch_c)) result["success"] = True