""" IDA Pro script to find and decrypt XOR-obfuscated strings. Pattern: struct EncStr { char data[N]; // encrypted bytes at offset 0 char flag; // decrypted flag at offset N // 7 bytes padding QWORD key; // XOR key at offset N+8 }; Decryption: byte[i] = ~(byte[i] ^ (key >> (8 * (i & 7)))) Usage: Run in IDA Pro via File > Script File, or paste into the Python console. """ import idc import idaapi import idautils import struct def decrypt_string(ea, length, key_offset): flag = idc.get_wide_byte(ea + length) enc = [idc.get_wide_byte(ea + i) for i in range(length)] # If already plaintext (flag=0 but looks like ASCII), return as-is if flag != 1: try: raw = bytes(enc).rstrip(b'\x00').decode('utf-8', errors='replace') # Heuristic: if >80% printable, it's already plaintext printable = sum(0x20 <= b < 0x7F or b in (0x00, 0x0A, 0x0D) for b in enc) if printable / len(enc) > 0.7: raw = bytes(enc).rstrip(b'\x00').decode('utf-8', errors='replace') return raw + " [plaintext]" except: pass key = idc.get_qword(ea + key_offset) decrypted = bytearray() for i, b in enumerate(enc): kb = (key >> (8 * (i & 7))) & 0xFF decrypted.append((~(b ^ kb)) & 0xFF) try: decrypted = decrypted[:decrypted.index(0)] except ValueError: pass return decrypted.decode("utf-8", errors="replace") def extract_key_offset_from_function(func_ea): """ Find the key offset by looking for: mov rdx, [rax+Xh] or mov rdx, [rcx+Xh] inside the decryption loop (the qword key load). We want the one where the offset > length (so we skip the encrypted byte loads which use byte ptr). """ func = idaapi.get_func(func_ea) if not func: return None end_ea = func.end_ea ea = func_ea while ea < end_ea: mnem = idc.print_insn_mnem(ea) if mnem == "mov": op0 = idc.print_operand(ea, 0) op1 = idc.print_operand(ea, 1) # Looking for: mov rXX, qword ptr [rXX+offset] # i.e. a qword load (no "byte ptr") with a positive offset if (op0 in ("rdx", "rcx", "rsi") and "[" in op1 and "+" in op1 and "byte ptr" not in op1 and "rsp" not in op1 and "rbp" not in op1): val = idc.get_operand_value(ea, 1) if 0 < val < 0x10000: return val ea = idc.next_head(ea) return None def find_decrypt_functions(): """ Heuristic: scan all functions whose name starts with 'decrypt_string' or matches the known pattern. Adjust the name prefix to match yours. """ results = [] for func_ea in idautils.Functions(): name = idc.get_func_name(func_ea) if "decrypt_string" in name.lower(): results.append(func_ea) return results def extract_length_from_function(func_ea): func = idaapi.get_func(func_ea) if not func: return None end_ea = func.end_ea ea = func_ea length = None while ea < end_ea: mnem = idc.print_insn_mnem(ea) # Pattern 1: cmp [reg+imm], 1 (flag byte check at offset = length) if mnem == "cmp": op1 = idc.print_operand(ea, 1) op0 = idc.print_operand(ea, 0) if op1 == "1" and "[" in op0 and "+" in op0: val = idc.get_operand_value(ea, 0) if 0 < val < 0x10000: length = val break # Pattern 2: cmp [rbp+var], N where next insn is jle/jg # loop runs 0..N so string length = N+1, flag is at offset N+1 if mnem == "cmp": op1_val = idc.get_operand_value(ea, 1) next_ea = idc.next_head(ea) next_mnem = idc.print_insn_mnem(next_ea) if next_mnem in ("jle", "jng") and 0 < op1_val < 0x10000: length = op1_val + 1 # loop is 0..N inclusive → N+1 bytes break # Pattern 3: mov byte ptr [rax+N], 1 (writing the flag after loop) if mnem == "mov": op0 = idc.print_operand(ea, 0) op1 = idc.print_operand(ea, 1) if op1 == "1" and "byte ptr" in op0 and "+" in op0: val = idc.get_operand_value(ea, 0) if 0 < val < 0x10000: length = val # flag written at [base+length] break ea = idc.next_head(ea) return length def find_xref_args(func_ea): """ For each call site of func_ea, try to find the struct base address passed as the first argument (rdi / rcx on x86-64). Returns a list of (call_ea, struct_ea) tuples where struct_ea could be resolved statically (e.g. LEA rdi, [rip+offset]). """ xrefs = [] for xref in idautils.XrefsTo(func_ea, idaapi.XREF_FAR): call_ea = xref.frm # Walk back a few instructions to find the LEA/MOV loading the arg ea = call_ea for _ in range(8): ea = idc.prev_head(ea) mnem = idc.print_insn_mnem(ea) if mnem in ("lea", "mov"): reg = idc.print_operand(ea, 0) if reg in ("rdi", "rcx"): # first arg (SysV / Win64) struct_ea = idc.get_operand_value(ea, 1) if struct_ea and struct_ea != idc.BADADDR: xrefs.append((call_ea, struct_ea)) break return xrefs def run(): print("=" * 60) print("IDA String Decryptor") print("=" * 60) decrypt_funcs = find_decrypt_functions() if not decrypt_funcs: print("[!] No decrypt_string* functions found.") print(" Edit the name filter in find_decrypt_functions() to match yours.") return print(f"[+] Found {len(decrypt_funcs)} decrypt function(s).\n") for func_ea in decrypt_funcs: func_name = idc.get_func_name(func_ea) length = extract_length_from_function(func_ea) key_offset = extract_key_offset_from_function(func_ea) if length is None or key_offset is None: print(f"[-] {func_name}: could not extract length={length} key_offset={key_offset}") continue print(f"[*] {func_name} @ 0x{func_ea:X} length={length} key@+{key_offset}") xrefs = find_xref_args(func_ea) if not xrefs: print(f" [!] No static call sites found (possibly indirect call).") continue for call_ea, struct_ea in xrefs: try: plaintext = decrypt_string(struct_ea, length, key_offset) print(f" call @ 0x{call_ea:X} struct @ 0x{struct_ea:X} => \"{plaintext}\"") # Optional: write a comment at the call site idc.set_cmt(call_ea, f'decrypt => "{plaintext}"', 0) # Optional: rename the struct label if it has a generic name current_label = idc.get_name(struct_ea) if current_label and current_label.startswith("unk_"): new_name = "enc_" + "".join( c if c.isalnum() or c == "_" else "_" for c in plaintext[:24] ) idc.set_name(struct_ea, new_name, idc.SN_NOWARN) except Exception as e: print(f" call @ 0x{call_ea:X} struct @ 0x{struct_ea:X} => ERROR: {e}") print() print("Done.") run()