Added some scripts
This commit is contained in:
674
dump_analyzer.py
Normal file
674
dump_analyzer.py
Normal 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
309
dump_nemo.py
Normal 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
408
mem_idx_helper.py
Normal 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()
|
||||
102
original_code_parsing_reconstr.py
Normal file
102
original_code_parsing_reconstr.py
Normal 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user