#!/usr/bin/env python3 import argparse from io import BufferedReader import os import struct MAGIC = 0x584D454D # 'MEMX' HEADER_FMT = "= 2 and token[0] == token[-1] == '"': return token[1:-1] upper = token.upper() if upper == "TRUE": return True if upper == "FALSE": return False numeric = parse_number(token) if numeric is not None: return numeric return token def split_legacy_comparison(expr): apos = expr.find("'") if apos != -1: op_text = "'" left_end = apos if apos > 0 and expr[apos - 1] in "!<>": op_text = expr[apos - 1] + "'" left_end -= 1 return expr[:left_end], op_text, expr[apos + 1 :] for op_text in ("<", ">", "?"): pos = expr.find(op_text) if pos != -1: return expr[:pos], op_text, expr[pos + 1 :] return None def compare_legacy_values(left_value, right_value, op_text): if op_text == "'": return left_value == right_value if op_text == "!'": return left_value != right_value if op_text in ("<", "<'", ">", ">'"): if isinstance(left_value, str) or isinstance(right_value, str): return False if op_text == "<": return left_value < right_value if op_text == "<'": return left_value <= right_value if op_text == ">": return left_value > right_value return left_value >= right_value if op_text == "?": return False return False def emulate_legacy_if_condition(expr): expr = expr.strip() has_and = "&&" in expr has_or = "||" in expr if has_and or has_or: delimiter = "&&" if has_and else "||" stop_on = has_or parts = [part.strip() for part in expr.split(delimiter)] evaluations = [] result = False for part in parts: part_result, part_trace = emulate_legacy_if_condition(part) evaluations.append(part_trace) result = part_result if part_result == stop_on: break return result, { "kind": "logic", "expr": expr, "delimiter": delimiter, "stop_on": stop_on, "result": result, "parts": evaluations, } pieces = split_legacy_comparison(expr) if pieces is None: value = resolve_legacy_operand(expr) result = bool(value) return result, { "kind": "value", "expr": expr, "value": value, "result": result, } left_text, op_text, right_text = pieces left_value = resolve_legacy_operand(left_text) right_value = resolve_legacy_operand(right_text) result = compare_legacy_values(left_value, right_value, op_text) return result, { "kind": "comparison", "expr": expr, "left_text": left_text.strip(), "right_text": right_text.strip(), "left_value": left_value, "right_value": right_value, "op": op_text, "result": result, } def print_legacy_trace(trace, depth=0): indent = " " * depth kind = trace["kind"] if kind == "logic": stop_label = "true" if trace["stop_on"] else "false" print( f"{indent}- logic {trace['delimiter']!r} stop_on={stop_label} " f"=> {trace['result']}" ) print(f"{indent} expr: {trace['expr']}") for part in trace["parts"]: print_legacy_trace(part, depth + 1) return if kind == "comparison": print( f"{indent}- cmp {trace['left_text']!r} {trace['op']} " f"{trace['right_text']!r} => {trace['result']}" ) print( f"{indent} values: {trace['left_value']!r} vs " f"{trace['right_value']!r}" ) return print(f"{indent}- value {trace['expr']!r} => {trace['value']!r} ({trace['result']})") def print_legacy_if_analysis(expr): result, trace = emulate_legacy_if_condition(expr) print("[+] Legacy @IF condition emulation") print(f"[+] Input: {expr}") print_legacy_trace(trace, depth=1) print(f"[+] Result: {result}") def describe_special_object(f: BufferedReader, regions, obj_ptr, type_id): if type_id == 13: code = summarize_text( maybe_read_cxstring_text(f, regions, obj_ptr + BEHAVIOUR_CODE_OFFSET) ) cond_text = summarize_text( maybe_read_cxstring_text( f, regions, obj_ptr + BEHAVIOUR_CONDITION_TEXT_OFFSET ) ) cond_ptr = read_u32(f, regions, obj_ptr + BEHAVIOUR_CONDITION_PTR_OFFSET) parts = [] if code: parts.append(f"code={code!r}") if cond_text: parts.append(f"cond={cond_text!r}") if cond_ptr: parts.append(f"cond_ptr=0x{cond_ptr:08X}") return ", ".join(parts) if type_id == 12: op1 = summarize_text( maybe_read_cxstring_text(f, regions, obj_ptr + EXPRESSION_OPERAND1_OFFSET) ) op2 = summarize_text( maybe_read_cxstring_text(f, regions, obj_ptr + EXPRESSION_OPERAND2_OFFSET) ) helper = read_u32(f, regions, obj_ptr + EXPRESSION_HELPER_BEHAVIOUR_OFFSET) parts = [] if op1: parts.append(f"op1={op1!r}") if op2: parts.append(f"op2={op2!r}") if helper: parts.append(f"helper=0x{helper:08X}") return ", ".join(parts) if type_id in (10, 11): op1 = summarize_text( maybe_read_cxstring_text(f, regions, obj_ptr + CONDITION_OPERAND1_OFFSET) ) op2 = summarize_text( maybe_read_cxstring_text(f, regions, obj_ptr + CONDITION_OPERAND2_OFFSET) ) op_text = summarize_text( maybe_read_cxstring_text( f, regions, obj_ptr + CONDITION_OPERATOR_TEXT_OFFSET ) ) op_id = read_u32(f, regions, obj_ptr + CONDITION_OPERATOR_ID_OFFSET) op_name = CONDITION_OP_NAMES.get(op_id, str(op_id)) parts = [] if op1: parts.append(f"op1={op1!r}") if op_text: parts.append(f"op={op_text!r}/{op_name}") else: parts.append(f"op={op_name}") if op2: parts.append(f"op2={op2!r}") return ", ".join(parts) return None def read_object_info(f: BufferedReader, regions, obj_ptr): type_id = read_object_type_id(f, regions, obj_ptr) name = read_object_name(f, regions, obj_ptr) if name is None: name = "" return { "ptr": obj_ptr, "type_id": type_id, "type_name": type_name(type_id), "name": name, "detail": describe_special_object(f, regions, obj_ptr, type_id), } def dump_container(f: BufferedReader, regions, container_ptr, label, visited=None, depth=0): if visited is None: visited = set() indent = " " * depth if container_ptr in visited: print(f"{indent}[=] {label}: 0x{container_ptr:08X} (already visited)") return visited.add(container_ptr) info = read_object_info(f, regions, container_ptr) vector_ptr, object_count, object_ptrs = iter_object_pointers(f, regions, container_ptr) print( f"{indent}[+] {label}: 0x{container_ptr:08X} " f"{info['name']} [{info['type_name']}] " f"vector=0x{vector_ptr:08X} count={object_count}" ) for index, obj_ptr in enumerate(object_ptrs): child = read_object_info(f, regions, obj_ptr) line = ( f"{indent} [{index:03d}] 0x{obj_ptr:08X} " f"{child['name']} [{child['type_name']}]" ) if child["detail"]: line += f" :: {child['detail']}" print(line) for obj_ptr in object_ptrs: child = read_object_info(f, regions, obj_ptr) if child["type_id"] in CONTAINER_TYPES and obj_ptr not in visited: dump_container( f, regions, obj_ptr, f"{child['type_name']} child", visited=visited, depth=depth + 1, ) def read_ascii_cxstring(f: BufferedReader, regions, ptr): value = read_cxstring(f, regions, ptr) return value.data.decode("ascii", errors="replace") def main(): parser = argparse.ArgumentParser() parser.add_argument("idx_path", nargs="?") parser.add_argument( "--emulate-if", dest="emulate_if", help='emulate legacy @IF("...") condition parsing', ) args = parser.parse_args() if args.idx_path: idx_path = args.idx_path if not os.path.isfile(idx_path): parser.error(f"Missing file: {idx_path}") print(f"[+] Loading index: {idx_path}") regions = load_index(idx_path) print(f"[+] Loaded regions: {len(regions)}") mem_guess = idx_path.rsplit(".", 1)[0] + ".mem" print(f"[i] Expected .mem file: {mem_guess}") try: with open(mem_guess, "rb") as f: print(f"[+] Opened dump: {mem_guess}") cwindow_ptr = get_cwindow(f, regions) print(f"[+] CWindow ptr: 0x{cwindow_ptr:08X}") app_pointer = read_u32( f, regions, cwindow_ptr + CWINDOW_APPLICATION_IFC_OFFSET ) mcapp_pointer = read_u32( f, regions, app_pointer + CAPPLICATION_MCAPP_OFFSET ) print(f"[+] CApplication ptr: 0x{app_pointer:08X}") print(f"[+] CMC_Application ptr: 0x{mcapp_pointer:08X}") current_scene_ptr = read_u32( f, regions, mcapp_pointer + CMC_CURRENT_SCENE_OFFSET ) print(f"[+] Current scene ptr: 0x{current_scene_ptr:08X}") if current_scene_ptr: current_scene_name = read_object_name(f, regions, current_scene_ptr) if current_scene_name is not None: print(f"[+] Current scene name: {current_scene_name}") visited = set() dump_container( f, regions, mcapp_pointer, "CMC_Application", visited=visited ) if current_scene_ptr: if current_scene_ptr in visited: current_info = read_object_info(f, regions, current_scene_ptr) print( f"[=] Current scene already covered: " f"0x{current_scene_ptr:08X} " f"{current_info['name']} [{current_info['type_name']}]" ) else: dump_container( f, regions, current_scene_ptr, "Current scene", visited=visited, ) print(f"[+] RootPath ptr: 0x{ROOT_PATH_PTR:08X}") print(f"[+] RootPath: {read_ascii_cxstring(f, regions, ROOT_PATH_PTR)}") print(f"[+] DanePath ptr: 0x{DANE_PATH_PTR:08X}") print(f"[+] DanePath: {read_ascii_cxstring(f, regions, DANE_PATH_PTR)}") except OSError as exc: print(f"Failed to open dump: {exc}") elif not args.emulate_if: parser.error("Provide dump.idx or use --emulate-if") if args.emulate_if: print_legacy_if_analysis(args.emulate_if) if __name__ == "__main__": main()