commit 9dc113e7cb554ee02e0317d9f9748f621ba0ee88 Author: patryk025 Date: Thu Mar 5 21:23:58 2026 +0000 Added some scripts diff --git a/dump_analyzer.py b/dump_analyzer.py new file mode 100644 index 0000000..310b1a5 --- /dev/null +++ b/dump_analyzer.py @@ -0,0 +1,674 @@ +#!/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() diff --git a/dump_nemo.py b/dump_nemo.py new file mode 100644 index 0000000..e2199aa --- /dev/null +++ b/dump_nemo.py @@ -0,0 +1,309 @@ +import ctypes +import ctypes.wintypes as wt +import struct +import sys +from dataclasses import dataclass + +kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) +advapi32 = ctypes.WinDLL("advapi32", use_last_error=True) + +# ----------------------------- +# Windows constants +# ----------------------------- +TH32CS_SNAPPROCESS = 0x00000002 + +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_VM_READ = 0x0010 +PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + +MEM_COMMIT = 0x1000 + +MEM_IMAGE = 0x1000000 +MEM_MAPPED = 0x40000 +MEM_PRIVATE = 0x20000 + +PAGE_NOACCESS = 0x01 +PAGE_READONLY = 0x02 +PAGE_READWRITE = 0x04 +PAGE_WRITECOPY = 0x08 +PAGE_EXECUTE = 0x10 +PAGE_EXECUTE_READ = 0x20 +PAGE_EXECUTE_READWRITE = 0x40 +PAGE_EXECUTE_WRITECOPY = 0x80 +PAGE_GUARD = 0x100 +PAGE_NOCACHE = 0x200 +PAGE_WRITECOMBINE = 0x400 + +READABLE_MASK = ( + PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | + PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY +) + +# SeDebugPrivilege (opcjonalnie) +SE_PRIVILEGE_ENABLED = 0x00000002 +TOKEN_ADJUST_PRIVILEGES = 0x20 +TOKEN_QUERY = 0x8 + +# ----------------------------- +# Structures +# ----------------------------- +class PROCESSENTRY32(ctypes.Structure): + _fields_ = [ + ("dwSize", wt.DWORD), + ("cntUsage", wt.DWORD), + ("th32ProcessID", wt.DWORD), + ("th32DefaultHeapID", ctypes.c_void_p), + ("th32ModuleID", wt.DWORD), + ("cntThreads", wt.DWORD), + ("th32ParentProcessID", wt.DWORD), + ("pcPriClassBase", wt.LONG), + ("dwFlags", wt.DWORD), + ("szExeFile", wt.WCHAR * 260), + ] + +# MEMORY_BASIC_INFORMATION for VirtualQueryEx: +# This layout is correct for 32-bit target from a 64-bit Python too, because VirtualQueryEx +# returns pointers sized to the caller, but BaseAddress/RegionSize are pointer-sized. +# We'll store them in ctypes.c_size_t and then cast. +class MEMORY_BASIC_INFORMATION(ctypes.Structure): + _fields_ = [ + ("BaseAddress", ctypes.c_void_p), + ("AllocationBase", ctypes.c_void_p), + ("AllocationProtect", wt.DWORD), + ("RegionSize", ctypes.c_size_t), + ("State", wt.DWORD), + ("Protect", wt.DWORD), + ("Type", wt.DWORD), + ] + +class SYSTEM_INFO(ctypes.Structure): + _fields_ = [ + ("wProcessorArchitecture", wt.WORD), + ("wReserved", wt.WORD), + ("dwPageSize", wt.DWORD), + ("lpMinimumApplicationAddress", ctypes.c_void_p), + ("lpMaximumApplicationAddress", ctypes.c_void_p), + ("dwActiveProcessorMask", ctypes.c_void_p), + ("dwNumberOfProcessors", wt.DWORD), + ("dwProcessorType", wt.DWORD), + ("dwAllocationGranularity", wt.DWORD), + ("wProcessorLevel", wt.WORD), + ("wProcessorRevision", wt.WORD), + ] + +class LUID(ctypes.Structure): + _fields_ = [("LowPart", wt.DWORD), ("HighPart", wt.LONG)] + +class LUID_AND_ATTRIBUTES(ctypes.Structure): + _fields_ = [("Luid", LUID), ("Attributes", wt.DWORD)] + +class TOKEN_PRIVILEGES(ctypes.Structure): + _fields_ = [("PrivilegeCount", wt.DWORD), + ("Privileges", LUID_AND_ATTRIBUTES * 1)] + +# ----------------------------- +# API prototypes +# ----------------------------- +kernel32.CreateToolhelp32Snapshot.argtypes = [wt.DWORD, wt.DWORD] +kernel32.CreateToolhelp32Snapshot.restype = wt.HANDLE + +kernel32.Process32FirstW.argtypes = [wt.HANDLE, ctypes.POINTER(PROCESSENTRY32)] +kernel32.Process32FirstW.restype = wt.BOOL + +kernel32.Process32NextW.argtypes = [wt.HANDLE, ctypes.POINTER(PROCESSENTRY32)] +kernel32.Process32NextW.restype = wt.BOOL + +kernel32.OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD] +kernel32.OpenProcess.restype = wt.HANDLE + +kernel32.CloseHandle.argtypes = [wt.HANDLE] +kernel32.CloseHandle.restype = wt.BOOL + +kernel32.VirtualQueryEx.argtypes = [wt.HANDLE, ctypes.c_void_p, ctypes.POINTER(MEMORY_BASIC_INFORMATION), ctypes.c_size_t] +kernel32.VirtualQueryEx.restype = ctypes.c_size_t + +kernel32.ReadProcessMemory.argtypes = [wt.HANDLE, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] +kernel32.ReadProcessMemory.restype = wt.BOOL + +kernel32.GetSystemInfo.argtypes = [ctypes.POINTER(SYSTEM_INFO)] +kernel32.GetSystemInfo.restype = None + +advapi32.OpenProcessToken.argtypes = [wt.HANDLE, wt.DWORD, ctypes.POINTER(wt.HANDLE)] +advapi32.OpenProcessToken.restype = wt.BOOL + +advapi32.LookupPrivilegeValueW.argtypes = [wt.LPCWSTR, wt.LPCWSTR, ctypes.POINTER(LUID)] +advapi32.LookupPrivilegeValueW.restype = wt.BOOL + +advapi32.AdjustTokenPrivileges.argtypes = [wt.HANDLE, wt.BOOL, ctypes.POINTER(TOKEN_PRIVILEGES), + wt.DWORD, ctypes.c_void_p, ctypes.c_void_p] +advapi32.AdjustTokenPrivileges.restype = wt.BOOL + +kernel32.GetCurrentProcess.restype = wt.HANDLE + +# ----------------------------- +# Helpers +# ----------------------------- +def winerr(msg: str): + err = ctypes.get_last_error() + raise OSError(err, f"{msg} (WinError={err})") + +def enable_debug_privilege(): + hToken = wt.HANDLE() + if not advapi32.OpenProcessToken(kernel32.GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ctypes.byref(hToken)): + return False + try: + luid = LUID() + if not advapi32.LookupPrivilegeValueW(None, "SeDebugPrivilege", ctypes.byref(luid)): + return False + tp = TOKEN_PRIVILEGES(1, (LUID_AND_ATTRIBUTES(luid, SE_PRIVILEGE_ENABLED),)) + if not advapi32.AdjustTokenPrivileges(hToken, False, ctypes.byref(tp), 0, None, None): + return False + # AdjustTokenPrivileges can succeed but still not enable; GetLastError==ERROR_NOT_ALL_ASSIGNED + if ctypes.get_last_error() == 1300: # ERROR_NOT_ALL_ASSIGNED + return False + return True + finally: + kernel32.CloseHandle(hToken) + +def find_pid_by_name(exe_name: str) -> int: + snap = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) + if snap == wt.HANDLE(-1).value: + winerr("CreateToolhelp32Snapshot failed") + + try: + pe = PROCESSENTRY32() + pe.dwSize = ctypes.sizeof(PROCESSENTRY32) + if not kernel32.Process32FirstW(snap, ctypes.byref(pe)): + winerr("Process32FirstW failed") + + exe_name_l = exe_name.lower() + while True: + if pe.szExeFile.lower() == exe_name_l: + return int(pe.th32ProcessID) + if not kernel32.Process32NextW(snap, ctypes.byref(pe)): + break + return 0 + finally: + kernel32.CloseHandle(snap) + +def is_readable_page(prot: int) -> bool: + if prot & PAGE_GUARD: + return False + if prot & PAGE_NOACCESS: + return False + if prot & READABLE_MASK: + return True + return False + +@dataclass +class RegionIdx: + base: int + size: int + protect: int + type: int + fileOffset: int + +def dump_process_memory(pid: int, mem_path: str, idx_path: str, chunk_size: int = 1 << 20): + access = PROCESS_VM_READ | PROCESS_QUERY_INFORMATION + hProc = kernel32.OpenProcess(access, False, pid) + if not hProc: + # fallback for some processes + hProc = kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_LIMITED_INFORMATION, False, pid) + if not hProc: + winerr("OpenProcess failed") + + index = [] + try: + si = SYSTEM_INFO() + kernel32.GetSystemInfo(ctypes.byref(si)) + p = ctypes.c_void_p(si.lpMinimumApplicationAddress).value + max_addr = ctypes.c_void_p(si.lpMaximumApplicationAddress).value + + mbi = MEMORY_BASIC_INFORMATION() + + with open(mem_path, "wb") as fmem: + while p < max_addr: + got = kernel32.VirtualQueryEx(hProc, ctypes.c_void_p(p), ctypes.byref(mbi), ctypes.sizeof(mbi)) + if not got: + # jump by a page to avoid infinite loop on weird edges + p += 0x1000 + continue + + base = ctypes.c_void_p(mbi.BaseAddress).value + rsize = int(mbi.RegionSize) + prot = int(mbi.Protect) + state = int(mbi.State) + rtype = int(mbi.Type) + + if state == MEM_COMMIT and is_readable_page(prot) and rsize > 0: + file_off = fmem.tell() + written = 0 + + cur = base + left = rsize + + buf = (ctypes.c_ubyte * min(chunk_size, rsize))() + while left > 0: + to_read = min(left, chunk_size) + bytes_read = ctypes.c_size_t(0) + ok = kernel32.ReadProcessMemory( + hProc, + ctypes.c_void_p(cur), + ctypes.byref(buf), + to_read, + ctypes.byref(bytes_read), + ) + n = int(bytes_read.value) + if n > 0: + fmem.write(bytes(buf[:n])) + written += n + cur += n + left -= n + # jeśli RPM fail albo short read — kończymy region (jak w Twoim C++) + if (not ok) or (n < to_read): + break + + if written > 0: + # Twój format trzyma base jako u32 + index.append(RegionIdx( + base=base & 0xFFFFFFFF, + size=written & 0xFFFFFFFF, + protect=prot & 0xFFFFFFFF, + type=rtype & 0xFFFFFFFF, + fileOffset=file_off & 0xFFFFFFFFFFFFFFFF, + )) + + # następny region + p = base + rsize + + # zapis indexu + with open(idx_path, "wb") as fidx: + hdr = struct.pack(" {mem_path} (+ {idx_path})") + + finally: + kernel32.CloseHandle(hProc) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python dump_mem.py nemo.exe [dump.mem dump.idx]") + sys.exit(1) + + exe = sys.argv[1] + mem_path = sys.argv[2] if len(sys.argv) >= 3 else "dump.mem" + idx_path = sys.argv[3] if len(sys.argv) >= 4 else "dump.idx" + + dbg = enable_debug_privilege() + print(f"[*] SeDebugPrivilege: {'enabled' if dbg else 'not enabled (ok, maybe not needed)'}") + + pid = find_pid_by_name(exe) + if not pid: + print(f"[-] Process not found: {exe}") + sys.exit(2) + + print(f"[*] PID for {exe}: {pid}") + dump_process_memory(pid, mem_path, idx_path) \ No newline at end of file diff --git a/mem_idx_helper.py b/mem_idx_helper.py new file mode 100644 index 0000000..ece3259 --- /dev/null +++ b/mem_idx_helper.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +import struct +import sys +import os + +MAGIC = 0x584D454D # 'MEMX' +HEADER_FMT = " 10: + print(f" ... i {len(only_in_1) - 10} więcej") + print() + + if only_in_2: + print("[+] Regiony dodane (tylko w zrzucie 2):") + for base in sorted(only_in_2)[:10]: + r = regions2_map[base] + print(f" 0x{r.base:08X} - 0x{r.base + r.size:08X} (size=0x{r.size:X})") + if len(only_in_2) > 10: + print(f" ... i {len(only_in_2) - 10} więcej") + print() + + # Continue only if there are common regions + if not common: + print("[!] Brak wspólnych regionów do porównania") + return + + # Check if memory dump files exist + if not os.path.isfile(memdump1): + print(f"[!] Brak pliku zrzutu pamięci: {memdump1}") + return + if not os.path.isfile(memdump2): + print(f"[!] Brak pliku zrzutu pamięci: {memdump2}") + return + + print("[*] Porównywanie zawartości wspólnych regionów...") + + changed_regions = [] + + with open(memdump1, "rb") as f1, open(memdump2, "rb") as f2: + for base in sorted(common): + r1 = regions1_map[base] + r2 = regions2_map[base] + + # Check region size + if r1.size != r2.size: + changed_regions.append({ + 'base': base, + 'type': 'size_changed', + 'old_size': r1.size, + 'new_size': r2.size + }) + continue + + # Read region data from both dumps + f1.seek(r1.file_offset) + data1 = f1.read(r1.size) + + f2.seek(r2.file_offset) + data2 = f2.read(r2.size) + + if len(data1) != r1.size or len(data2) != r2.size: + print(f"[!] Błąd odczytu dla regionu 0x{base:08X}") + continue + + # Compare byte by byte + if data1 != data2: + # Find differing byte ranges + diffs = [] + diff_start = None + + for i in range(len(data1)): + if data1[i] != data2[i]: + if diff_start is None: + diff_start = i + else: + if diff_start is not None: + diffs.append((diff_start, i - 1)) + diff_start = None + + # Handle case where difference goes to the end + if diff_start is not None: + diffs.append((diff_start, len(data1) - 1)) + + changed_regions.append({ + 'base': base, + 'type': 'content_changed', + 'size': r1.size, + 'diffs': diffs, + 'total_changed': sum(end - start + 1 for start, end in diffs) + }) + + if not changed_regions: + print("[+] Brak różnic w zawartości wspólnych regionów!") + return + + print(f"[+] Znaleziono {len(changed_regions)} zmienionych regionów:") + print() + + for change in changed_regions[:20]: # Show max 20 + base = change['base'] + if change['type'] == 'size_changed': + print(f" Region 0x{base:08X}:") + print(f" Zmiana rozmiaru: 0x{change['old_size']:X} -> 0x{change['new_size']:X}") + else: + print(f" Region 0x{base:08X} (size=0x{change['size']:X}):") + print(f" Zmienione bajty: {change['total_changed']} / {change['size']} ({100.0 * change['total_changed'] / change['size']:.2f}%)") + print(f" Zakresów różnic: {len(change['diffs'])}") + + # Show first 5 diff ranges + for start, end in change['diffs'][:5]: + va_start = base + start + va_end = base + end + print(f" VA 0x{va_start:08X} - 0x{va_end:08X} (offset +0x{start:X}, {end - start + 1} bajtów)") + + if len(change['diffs']) > 5: + print(f" ... i {len(change['diffs']) - 5} więcej zakresów") + print() + + if len(changed_regions) > 20: + print(f"... i {len(changed_regions) - 20} więcej zmienionych regionów") + print() + + total_bytes_changed = sum(c.get('total_changed', 0) for c in changed_regions if c['type'] == 'content_changed') + print(f"[=] Podsumowanie: łącznie ~{total_bytes_changed} bajtów zmienionych") + +def main(): + if len(sys.argv) < 2: + print("Użycie: python mem_idx_helper.py Sekai_xxx.idx") + sys.exit(1) + + idx_path = sys.argv[1] + if not os.path.isfile(idx_path): + print(f"Brak pliku: {idx_path}") + sys.exit(1) + + print(f"[+] Wczytywanie indeksu z: {idx_path}") + regions = load_index(idx_path) + print(f"[+] Załadowano {len(regions)} regionów") + + mem_guess = idx_path.rsplit(".", 1)[0] + ".mem" + print(f"[i] Zakładany plik .mem: {mem_guess}") + print() + print("Tryb interaktywny. Komendy:") + print(" va - adres wirtualny → offset w .mem") + print(" offset - offset w .mem → adres wirtualny") + print(" mr - base modułu + RVA → offset w .mem") + print(" follow [offset] - rozwiązuje wskaźnik do obiektu pod danym adresem") + print(" info - podstawowe info") + print(" region - pokaż tylko info o regionie") + print(" diff - pokaż różnice pomiędzy dwoma zrzutami pamięci") + print(" q / quit / exit - wyjście") + print() + print("Przykłady:") + print(" va 0x14F80BD0") + print(" offset 0x1A3F4B0 # offset w .mem → VA") + print(" mr 0x0FFA0000 0xD354 # vtable Sekai: base + RVA") + print() + + while True: + try: + line = input("> ").strip() + except (EOFError, KeyboardInterrupt): + print() + break + + if not line: + continue + + parts = line.split() + cmd = parts[0].lower() + + if cmd in ("q", "quit", "exit"): + break + + if cmd == "info": + total_size = sum(r.size for r in regions) + print(f"Regionów: {len(regions)}, łączny rozmiar: 0x{total_size:X} ({total_size} bajtów)") + continue + + if cmd == "va": + if len(parts) != 2: + print("Użycie: va ") + continue + try: + va = parse_int(parts[1]) + except ValueError as e: + print(f"Błąd parsowania adresu: {e}") + continue + + r = find_region_for_va(va, regions) + if not r: + print(f"VA 0x{va:08X} nie należy do żadnego regionu") + continue + + offset = r.file_offset + (va - r.base) + print(f"VA 0x{va:08X} -> region base=0x{r.base:08X}, size=0x{r.size:X}") + print(f" file_offset=0x{r.file_offset:X} -> offset w .mem = 0x{offset:X} ({offset} dec)") + continue + + if cmd == "offset" or cmd == "off": + if len(parts) != 2: + print("Użycie: offset ") + continue + try: + offset = parse_int(parts[1]) + except ValueError as e: + print(f"Błąd parsowania offsetu: {e}") + continue + + r = find_region_for_offset(offset, regions) + if not r: + print(f"Offset 0x{offset:X} nie należy do żadnego regionu") + continue + + va = r.base + (offset - r.file_offset) + rva = va - r.base + print(f"Offset w .mem 0x{offset:X} ({offset} dec)") + print(f" -> region base=0x{r.base:08X}, size=0x{r.size:X}") + print(f" -> VA = 0x{va:08X}") + print(f" -> RVA (offset w regionie) = 0x{rva:X}") + continue + + if cmd == "mr": + if len(parts) != 3: + print("Użycie: mr ") + continue + try: + base = parse_int(parts[1]) + rva = parse_int(parts[2]) + except ValueError as e: + print(f"Błąd parsowania: {e}") + continue + + va = base + rva + r = find_region_for_va(va, regions) + if not r: + print(f"VA 0x{va:08X} (base+RVA) nie należy do żadnego regionu") + continue + + offset = r.file_offset + (va - r.base) + print(f"BASE 0x{base:08X} + RVA 0x{rva:X} = VA 0x{va:08X}") + print(f"VA 0x{va:08X} -> region base=0x{r.base:08X}, size=0x{r.size:X}") + print(f" file_offset=0x{r.file_offset:X} -> offset w .mem = 0x{offset:X} ({offset} dec)") + continue + + if cmd == "follow": + if len(parts) != 2 and len(parts) != 3: + print("Użycie: follow [offset]") + continue + try: + va = parse_int(parts[1]) + offset = 0 if len(parts) == 2 else parse_int(parts[2]) + except ValueError as e: + print(f"Błąd parsowania adresu: {e}") + continue + + if offset != 0: + va += offset + + r = find_region_for_va(va, regions) + if not r: + print(f"VA 0x{va:08X} nie należy do żadnego regionu") + continue + + offset = r.file_offset + (va - r.base) + + print(f"Rozwiązywanie wskaźnika pod VA 0x{va:08X} (offset w .mem: 0x{offset:X})") + print(f" -> region base=0x{r.base:08X}, size=0x{r.size:X}") + print(f" -> file_offset=0x{r.file_offset:X}") + print(f" -> offset w .mem = 0x{offset:X}") + print(f" -> VA = 0x{va:08X}") + print(f" -> RVA (offset w regionie) = 0x{va - r.base:X}") + + try: + with open(mem_guess, "rb") as f: + f.seek(offset) + data = f.read(4) # Assuming 32-bit pointer + if len(data) != 4: + print(f"Nie można odczytać wskaźnika z offsetu 0x{offset:X}") + continue + pointed_va = struct.unpack(" Wartość wskaźnika: VA 0x{pointed_va:08X}") + continue + except OSError as e: + print(f"Nie można otworzyć pliku zrzutu pamięci: {e}") + continue + + if cmd == "region": + if len(parts) != 2: + print("Użycie: region ") + continue + try: + va = parse_int(parts[1]) + except ValueError as e: + print(f"Błąd parsowania adresu: {e}") + continue + r = find_region_for_va(va, regions) + if not r: + print(f"VA 0x{va:08X} nie należy do żadnego regionu") + continue + print(r) + continue + + if cmd == "diff": + if len(parts) != 2: + print("Użycie: diff ") + continue + + try: + other_regions = load_index(parts[1]) + except RuntimeError as e: + print(f"Błąd wczytywania indexu: {e}") + continue + + other_mem_guessed = parts[1].rsplit(".", 1)[0] + ".mem" + + print(f"Analiza różnic między zrzutami") + print(f" {idx_path} -> {parts[1]}") + print(f" {mem_guess} -> {other_mem_guessed}") + print() + + diff_dumps(regions, other_regions, mem_guess, other_mem_guessed) + continue + + print("Nieznana komenda. Dostępne: va, offset, mr, info, region, diff, quit") + + +if __name__ == "__main__": + main() diff --git a/original_code_parsing_reconstr.py b/original_code_parsing_reconstr.py new file mode 100644 index 0000000..e6fc237 --- /dev/null +++ b/original_code_parsing_reconstr.py @@ -0,0 +1,102 @@ +code = "{@STRING(\"TEST\", \"\");@IF(\"2+3*4'\"2+3*4\"&&1'1\",\"{@RETURN(\"TRUE\");}\",\"{@RETURN(\"FALSE\");}\");@RETURN(TEST);}" + +def prepare_input(code: str) -> str: + # original used right and left instructions for striping code from braces (yeah, they are unnecessary) + code = code.strip() + if code[0] == '{': + code = code[1::] + if code[-1] == '}': + code = code[:-1] + code = code.strip() # yep, another strip to remove spaces after removing braces + return code + +def initializeBehaviours(code: str) -> str: + code_lines = [] + + depth = 0 + tmp_buffer = "" + for i, c in enumerate(code): + if c == '(': + depth += 1 + tmp_buffer += c + continue + elif c == ')': + depth -= 1 + tmp_buffer += c + continue + elif c == ';': + if depth == 0: + code_lines.append(tmp_buffer) + tmp_buffer = "" + continue + else: + tmp_buffer += c + continue + elif c == ' ': + continue + else: + tmp_buffer += c + + return code_lines + +def parse_line(line: str): + if line.endswith(';'): + line = line[:-1] # make sure to remove the trailing semicolon if it still exists + + current_char = line[0] + + left_paren_index = line.find('(') + + if current_char == '@': # special instruction + # get the instruction name + if left_paren_index == -1: + left_paren_index = 1 + + instruction_name = line[1:left_paren_index] + + # assign runner (not implemented here, just a placeholder) + # goto LABEL_40 + + elif current_char == "*": + close_bracket_index = line.find(']') + 1 + caret_index = line.find('^', close_bracket_index) + + if close_bracket_index >= left_paren_index: + caret_index = line.find('^') + else: + left_paren_index = line.find('(', caret_index) + + target_name = line[1:caret_index] + + if target_name[0] == "[": + target_name = target_name[1:-1] # remove the brackets + + # object = new CMC_Expression + else: + # object = CMC_ObjectsContainer::getObject + pass + + method_name = line[caret_index + 1:left_paren_index] + # goto LABEL_40 + + elif line.startswith("THIS"): + method_name = line[5:left_paren_index] + # goto LABEL_40 + + pass + +if __name__ == '__main__': + prepared_code = prepare_input(code) + + print("Prepared code:") + print(prepared_code) + + print("\nCode lines:") + code_lines = initializeBehaviours(prepared_code) + for line in code_lines: + print(line) + + for line in code_lines: + parse_line(line) + + \ No newline at end of file