Added some scripts

This commit is contained in:
2026-03-05 21:23:58 +00:00
commit 9dc113e7cb
4 changed files with 1493 additions and 0 deletions

674
dump_analyzer.py Normal file
View File

@@ -0,0 +1,674 @@
#!/usr/bin/env python3
import argparse
from io import BufferedReader
import os
import struct
MAGIC = 0x584D454D # 'MEMX'
HEADER_FMT = "<IIII"
REGION_FMT = "<IIIIQ"
HEADER_SIZE = struct.calcsize(HEADER_FMT)
REGION_SIZE = struct.calcsize(REGION_FMT)
CWINDOW_PTR = 0x00419ADC
CWINDOW_APPLICATION_IFC_OFFSET = 0x18
CAPPLICATION_MCAPP_OFFSET = 0x4C
CMC_OBJECTS_VECTOR_OFFSET = 0x4C
CMC_CURRENT_SCENE_OFFSET = 0xFC
CMC_OBJECT_NAME_OFFSET = 0x10
CMC_OBJECT_TYPE_OFFSET = 0x20
BEHAVIOUR_CODE_OFFSET = 0x4C
BEHAVIOUR_CONDITION_TEXT_OFFSET = 0x5C
BEHAVIOUR_CONDITION_PTR_OFFSET = 0x6C
EXPRESSION_OPERAND1_OFFSET = 0x58
EXPRESSION_OPERAND2_OFFSET = 0x68
EXPRESSION_HELPER_BEHAVIOUR_OFFSET = 0x78
CONDITION_OPERAND1_OFFSET = 0x5C
CONDITION_OPERAND2_OFFSET = 0x6C
CONDITION_OPERATOR_TEXT_OFFSET = 0x7C
CONDITION_OPERATOR_ID_OFFSET = 0x8C
CXVECTOR_ITEMS_OFFSET = 0x14
CXVECTOR_COUNT_OFFSET = 0x1C
ROOT_PATH_PTR = 0x10468FF8
DANE_PATH_PTR = 0x1024D964
TYPE_NAMES = {
1: "INTEGER",
2: "STRING",
3: "BOOL",
4: "DOUBLE",
10: "CONDITION",
11: "COMPLEX CONDITION",
12: "EXPRESSION",
13: "BEHAVIOUR",
100: "MOUSE",
101: "KEYBOARD",
102: "RANDOM",
103: "CANVAS OBSERVER",
1000: "APPLICATION",
1001: "EPISODE",
1002: "SCENE",
2000: "ANIMO",
2001: "BUTTON",
2002: "FONT",
2004: "SEQUENCE",
2005: "SOUND",
2006: "TEXT",
2007: "TIMER",
2008: "IMAGE",
2010: "NETPEER",
2013: "DIALOG",
2014: "FILTER",
3001: "ARRAY",
11000: "SYSTEM",
12000: "CNVLOADER",
}
CONTAINER_TYPES = {1000, 1001, 1002}
CONDITION_OP_NAMES = {
1: "EQUAL",
2: "NOTEQUAL",
3: "LESS",
4: "LESSEQUAL",
5: "GREATER",
6: "GREATEREQUAL",
7: "INSTANCEOF",
}
class CXString:
__slots__ = ("data", "length", "capacity")
def __init__(self, data, length, capacity):
self.data = data
self.length = length
self.capacity = capacity
def __repr__(self):
return (
f"CXString(data={self.data!r}, length={self.length}, "
f"capacity={self.capacity})"
)
class RegionIdx:
__slots__ = ("base", "size", "protect", "type", "file_offset")
def __init__(self, base, size, protect, rtype, file_offset):
self.base = base
self.size = size
self.protect = protect
self.type = rtype
self.file_offset = file_offset
def __repr__(self):
return (
f"Region(base=0x{self.base:08X}, size=0x{self.size:X}, "
f"protect=0x{self.protect:X}, type=0x{self.type:X}, "
f"file_offset=0x{self.file_offset:X})"
)
def load_index(path):
with open(path, "rb") as f:
header_data = f.read(HEADER_SIZE)
if len(header_data) != HEADER_SIZE:
raise RuntimeError("Index file too short (no header)")
magic, version, count, reserved = struct.unpack(HEADER_FMT, header_data)
if magic != MAGIC:
raise RuntimeError(f"Bad magic 0x{magic:08X}, expected 0x{MAGIC:08X}")
if version != 1:
raise RuntimeError(f"Unsupported version {version}")
regions = []
for i in range(count):
data = f.read(REGION_SIZE)
if len(data) != REGION_SIZE:
raise RuntimeError(f"Index truncated at region {i}/{count}")
base, size, protect, rtype, file_offset = struct.unpack(REGION_FMT, data)
regions.append(RegionIdx(base, size, protect, rtype, file_offset))
return regions
def find_region_for_va(va, regions):
for region in regions:
if region.base <= va < region.base + region.size:
return region
return None
def read_bytes(f: BufferedReader, regions, va, size):
region = find_region_for_va(va, regions)
if not region:
raise RuntimeError(f"VA 0x{va:08X} not found in any region")
offset = region.file_offset + (va - region.base)
f.seek(offset)
data = f.read(size)
if len(data) != size:
raise RuntimeError(f"Failed to read {size} bytes at VA 0x{va:08X}")
return data
def read_u32(f: BufferedReader, regions, va):
return struct.unpack("<I", read_bytes(f, regions, va, 4))[0]
def get_cwindow(f: BufferedReader, regions):
return read_u32(f, regions, CWINDOW_PTR)
def read_cxstring(f: BufferedReader, regions, ptr):
vtable, length, capacity, data_ptr = struct.unpack(
"<IIII", read_bytes(f, regions, ptr, 16)
)
if data_ptr == 0 or length == 0:
return CXString(b"", length, capacity)
text_data = read_bytes(f, regions, data_ptr, length)
return CXString(text_data, length, capacity)
def safe_read_cxstring_text(f: BufferedReader, regions, ptr):
try:
value = read_cxstring(f, regions, ptr)
except RuntimeError:
return None
return value.data.decode("latin-1", errors="replace")
def maybe_read_cxstring_text(f: BufferedReader, regions, ptr):
try:
return safe_read_cxstring_text(f, regions, ptr)
except Exception:
return None
def read_cxvector_layout(f: BufferedReader, regions, vector_ptr):
items_ptr = read_u32(f, regions, vector_ptr + CXVECTOR_ITEMS_OFFSET)
count = read_u32(f, regions, vector_ptr + CXVECTOR_COUNT_OFFSET)
return items_ptr, count
def iter_object_pointers(f: BufferedReader, regions, container_ptr):
vector_ptr = read_u32(f, regions, container_ptr + CMC_OBJECTS_VECTOR_OFFSET)
if vector_ptr == 0:
return vector_ptr, 0, []
items_ptr, count = read_cxvector_layout(f, regions, vector_ptr)
object_ptrs = []
for index in range(count):
object_ptrs.append(read_u32(f, regions, items_ptr + index * 4))
return vector_ptr, count, object_ptrs
def read_object_name(f: BufferedReader, regions, obj_ptr):
return safe_read_cxstring_text(f, regions, obj_ptr + CMC_OBJECT_NAME_OFFSET)
def read_object_type_id(f: BufferedReader, regions, obj_ptr):
return read_u32(f, regions, obj_ptr + CMC_OBJECT_TYPE_OFFSET)
def type_name(type_id):
return TYPE_NAMES.get(type_id, str(type_id))
def summarize_text(text, limit=48):
if text is None:
return None
compact = " ".join(text.split())
if len(compact) <= limit:
return compact
return compact[: limit - 3] + "..."
def parse_number(text):
text = text.strip()
if not text:
return None
try:
if "." in text:
return float(text)
return int(text, 10)
except ValueError:
return None
def tokenize_linear_expression(expr):
tokens = []
current = []
expect_value = True
for char in expr.strip():
if char.isspace():
continue
if char in "+-*%@":
if char == "-" and expect_value:
current.append(char)
continue
if not current:
return None
tokens.append("".join(current))
tokens.append(char)
current = []
expect_value = True
continue
current.append(char)
expect_value = False
if not current:
return None
tokens.append("".join(current))
if len(tokens) % 2 == 0:
return None
return tokens
def eval_linear_expression(expr):
tokens = tokenize_linear_expression(expr)
if not tokens:
return None
value = parse_number(tokens[0])
if value is None:
return None
index = 1
while index < len(tokens):
op = tokens[index]
rhs = parse_number(tokens[index + 1])
if rhs is None:
return None
if op == "+":
value += rhs
elif op == "-":
value -= rhs
elif op == "*":
value *= rhs
elif op == "@":
value /= rhs
elif op == "%":
value %= rhs
else:
return None
index += 2
if isinstance(value, float) and value.is_integer():
return int(value)
return value
def resolve_legacy_operand(token):
token = token.strip()
if token.startswith("[") and token.endswith("]"):
expr_value = eval_linear_expression(token[1:-1])
if expr_value is not None:
return expr_value
if len(token) >= 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 = "<unreadable>"
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()

309
dump_nemo.py Normal file
View File

@@ -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("<IIII", 0x584D454D, 1, len(index), 0) # 'MEMX'
fidx.write(hdr)
for ri in index:
fidx.write(struct.pack("<IIIIQ", ri.base, ri.size, ri.protect, ri.type, ri.fileOffset))
print(f"[+] DONE: wrote {len(index)} regions -> {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)

408
mem_idx_helper.py Normal file
View File

@@ -0,0 +1,408 @@
#!/usr/bin/env python3
import struct
import sys
import os
MAGIC = 0x584D454D # 'MEMX'
HEADER_FMT = "<IIII"
REGION_FMT = "<IIIIQ"
HEADER_SIZE = struct.calcsize(HEADER_FMT)
REGION_SIZE = struct.calcsize(REGION_FMT)
class RegionIdx:
__slots__ = ("base", "size", "protect", "type", "file_offset")
def __init__(self, base, size, protect, rtype, file_offset):
self.base = base
self.size = size
self.protect = protect
self.type = rtype
self.file_offset = file_offset
def __repr__(self):
return (f"Region(base=0x{self.base:08X}, size=0x{self.size:X}, "
f"protect=0x{self.protect:X}, type=0x{self.type:X}, "
f"file_offset=0x{self.file_offset:X})")
def load_index(path):
with open(path, "rb") as f:
header_data = f.read(HEADER_SIZE)
if len(header_data) != HEADER_SIZE:
raise RuntimeError("Index file too short (no header)")
magic, version, count, reserved = struct.unpack(HEADER_FMT, header_data)
if magic != MAGIC:
raise RuntimeError(f"Bad magic 0x{magic:08X}, expected 0x{MAGIC:08X}")
if version != 1:
raise RuntimeError(f"Unsupported version {version}")
regions = []
for i in range(count):
data = f.read(REGION_SIZE)
if len(data) != REGION_SIZE:
raise RuntimeError(f"Index truncated at region {i}/{count}")
base, size, protect, rtype, file_offset = struct.unpack(REGION_FMT, data)
regions.append(RegionIdx(base, size, protect, rtype, file_offset))
return regions
def find_region_for_va(va, regions):
# TODO: maybe use binsearch, but for about 600 regions it's working fine
for r in regions:
if r.base <= va < r.base + r.size:
return r
return None
def find_region_for_offset(offset, regions):
# Find region that contains the given file offset
for r in regions:
if r.file_offset <= offset < r.file_offset + r.size:
return r
return None
def parse_int(s):
s = s.strip()
return int(s, 0)
def diff_dumps(regions1, regions2, memdump1, memdump2):
# Create maps for quick lookup
regions1_map = {r.base: r for r in regions1}
regions2_map = {r.base: r for r in regions2}
# Find region differences
bases1 = set(regions1_map.keys())
bases2 = set(regions2_map.keys())
only_in_1 = bases1 - bases2
only_in_2 = bases2 - bases1
common = bases1 & bases2
print(f"[*] Analiza struktury regionów:")
print(f" Tylko w zrzucie 1: {len(only_in_1)} regionów")
print(f" Tylko w zrzucie 2: {len(only_in_2)} regionów")
print(f" Wspólnych regionów: {len(common)}")
print()
if only_in_1:
print("[+] Regiony usunięte (tylko w zrzucie 1):")
for base in sorted(only_in_1)[:10]: # Show max 10
r = regions1_map[base]
print(f" 0x{r.base:08X} - 0x{r.base + r.size:08X} (size=0x{r.size:X})")
if len(only_in_1) > 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 <addr> - adres wirtualny → offset w .mem")
print(" offset <offset> - offset w .mem → adres wirtualny")
print(" mr <base> <rva> - base modułu + RVA → offset w .mem")
print(" follow <addr> [offset] - rozwiązuje wskaźnik do obiektu pod danym adresem")
print(" info - podstawowe info")
print(" region <addr> - pokaż tylko info o regionie")
print(" diff <idx_path> - 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 <addr>")
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 <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 <base> <rva>")
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 <addr> [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("<I", data)[0]
print(f" -> 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 <addr>")
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 <idx_path>")
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()

View File

@@ -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)