This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / tools / memhack / memhack-repl.py
14 kB 404 lines
1"""memhack - memory editor with multi-type scan, freeze, and narrow""" 2 3import os 4import readline # noqa: F401 - enables line editing in input() 5import struct 6import sys 7import threading 8import time 9 10TYPES = { 11 "i8": (1, "<B"), 12 "i16": (2, "<H"), 13 "i32": (4, "<I"), 14 "i64": (8, "<Q"), 15 "f32": (4, "<f"), 16 "f64": (8, "<d"), 17} 18 19AUTO_SCAN_TYPES = ["i32", "i64", "f32", "f64"] 20 21 22def parse_addr(s): 23 """Parse address as hex (0x prefix optional).""" 24 return int(s.removeprefix("0x").removeprefix("0X"), 16) 25 26 27def parse_value(s, type_name="i32"): 28 """Parse value as int or float depending on type.""" 29 if type_name.startswith("f"): 30 return float(s) 31 return int(s, 0) 32 33 34def pack_value(value_str, type_name): 35 """Pack a value string into bytes for the given type.""" 36 _, fmt = TYPES[type_name] 37 if type_name.startswith("f"): 38 return struct.pack(fmt, float(value_str)) 39 return struct.pack(fmt, int(float(value_str))) 40 41 42def get_regions(pid): 43 """Parse /proc/<pid>/maps for writable memory regions.""" 44 regions = [] 45 with open(f"/proc/{pid}/maps") as f: 46 for line in f: 47 parts = line.split() 48 perms = parts[1] 49 if "r" not in perms or "w" not in perms: 50 continue 51 addr_range = parts[0].split("-") 52 start = int(addr_range[0], 16) 53 end = int(addr_range[1], 16) 54 regions.append((start, end)) 55 return regions 56 57 58def type_counts(matches): 59 """Count matches per type.""" 60 counts = {} 61 for _, tn in matches: 62 counts[tn] = counts.get(tn, 0) + 1 63 return counts 64 65 66class MemHack: 67 def __init__(self, pid): 68 self.pid = pid 69 self.mem_path = f"/proc/{pid}/mem" 70 self.mem_fd = None 71 self.matches = [] # list of (addr, type_name) 72 self.frozen = {} # addr -> (value, type_name, stop_event, thread) 73 self.write_error = None 74 75 def open_mem(self): 76 """Open /proc/<pid>/mem with read-write access (unbuffered).""" 77 if self.mem_fd is None: 78 self.mem_fd = os.open(self.mem_path, os.O_RDWR) 79 80 def close_mem(self): 81 if self.mem_fd is not None: 82 os.close(self.mem_fd) 83 self.mem_fd = None 84 85 def read_value(self, addr, type_name="i32"): 86 size, fmt = TYPES[type_name] 87 try: 88 self.open_mem() 89 data = os.pread(self.mem_fd, size, addr) 90 return struct.unpack(fmt, data)[0] 91 except OSError: 92 return None 93 94 def write_value(self, addr, value, type_name="i32"): 95 _, fmt = TYPES[type_name] 96 try: 97 self.open_mem() 98 os.pwrite(self.mem_fd, struct.pack(fmt, value), addr) 99 return True 100 except (OSError, struct.error) as e: 101 self.write_error = str(e) 102 return False 103 104 def scan(self, value_str, type_name=None): 105 """Scan all writable memory regions for value.""" 106 # determine which types to scan 107 if type_name: 108 scan_types = [type_name] 109 else: 110 num = float(value_str) 111 is_whole = num % 1 == 0 112 scan_types = [] 113 if is_whole: 114 scan_types.extend(["i32", "i64"]) 115 scan_types.extend(["f32", "f64"]) 116 117 # build packed targets per type 118 targets = {} 119 for tn in scan_types: 120 try: 121 targets[tn] = pack_value(value_str, tn) 122 except (struct.error, OverflowError, ValueError): 123 continue 124 125 regions = get_regions(self.pid) 126 matches = [] 127 bytes_scanned = 0 128 t0 = time.monotonic() 129 130 self.open_mem() 131 for start, end in regions: 132 try: 133 data = os.pread(self.mem_fd, end - start, start) 134 bytes_scanned += len(data) 135 for tn, target in targets.items(): 136 offset = 0 137 while True: 138 idx = data.find(target, offset) 139 if idx == -1: 140 break 141 matches.append((start + idx, tn)) 142 offset = idx + 1 143 except OSError: 144 continue 145 146 elapsed = time.monotonic() - t0 147 self.matches = matches 148 mb = bytes_scanned / (1024 * 1024) 149 counts = type_counts(matches) 150 breakdown = " ".join(f"{tn}:{n}" for tn, n in counts.items()) 151 print(f" {len(matches)} matches (scanned {mb:.0f} MB in {elapsed:.2f}s)") 152 if breakdown: 153 print(f" {breakdown}") 154 155 def narrow(self, value_str): 156 """Re-check existing matches for a new value.""" 157 if not self.matches: 158 print(" no matches to narrow (run scan first)") 159 return 160 remaining = [] 161 162 self.open_mem() 163 for addr, tn in self.matches: 164 try: 165 target = pack_value(value_str, tn) 166 except (struct.error, OverflowError, ValueError): 167 continue 168 size = TYPES[tn][0] 169 try: 170 data = os.pread(self.mem_fd, size, addr) 171 if data == target: 172 remaining.append((addr, tn)) 173 except OSError: 174 continue 175 176 self.matches = remaining 177 counts = type_counts(remaining) 178 breakdown = " ".join(f"{tn}:{n}" for tn, n in counts.items()) 179 print(f" {len(remaining)} matches remaining") 180 if breakdown: 181 print(f" {breakdown}") 182 if 0 < len(remaining) <= 20: 183 for addr, tn in remaining: 184 val = self.read_value(addr, tn) 185 print(f" {hex(addr)} = {val} ({tn})") 186 187 def set_matches(self, value_str): 188 """Write value to all current matches.""" 189 if not self.matches: 190 print(" no matches (run scan first)") 191 return 192 count = 0 193 for addr, tn in self.matches: 194 v = float(value_str) if tn.startswith("f") else int(float(value_str)) 195 if self.write_value(addr, v, tn): 196 count += 1 197 print(f" set {count}/{len(self.matches)} addresses to {value_str}") 198 199 def freeze(self, addr, value, type_name="i32"): 200 if addr in self.frozen: 201 self.unfreeze(addr, quiet=True) 202 203 # test write first to catch errors immediately 204 self.write_error = None 205 if not self.write_value(addr, value, type_name): 206 print(f" write failed for {hex(addr)}: {self.write_error}") 207 return 208 209 stop = threading.Event() 210 211 def loop(): 212 while not stop.is_set(): 213 self.write_value(addr, value, type_name) 214 stop.wait(0.01) 215 216 t = threading.Thread(target=loop, daemon=True) 217 t.start() 218 self.frozen[addr] = (value, type_name, stop, t) 219 print(f" frozen {hex(addr)} = {value} ({type_name})") 220 221 def freeze_matches(self, value_str): 222 """Freeze all current matches to a value.""" 223 if not self.matches: 224 print(" no matches (run scan first)") 225 return 226 for addr, tn in self.matches: 227 v = float(value_str) if tn.startswith("f") else int(float(value_str)) 228 self.freeze(addr, v, tn) 229 230 def unfreeze(self, addr, quiet=False): 231 if addr in self.frozen: 232 _, _, stop, t = self.frozen.pop(addr) 233 stop.set() 234 t.join(timeout=1) 235 if not quiet: 236 print(f" unfrozen {hex(addr)}") 237 elif not quiet: 238 print(f" {hex(addr)} is not frozen") 239 240 def unfreeze_all(self): 241 for addr in list(self.frozen): 242 self.unfreeze(addr) 243 244 def show_matches(self): 245 if not self.matches: 246 print(" no matches") 247 return 248 if len(self.matches) > 20: 249 counts = type_counts(self.matches) 250 breakdown = " ".join(f"{tn}:{n}" for tn, n in counts.items()) 251 print(f" {len(self.matches)} matches (narrow further to see addresses)") 252 print(f" {breakdown}") 253 return 254 for addr, tn in self.matches: 255 val = self.read_value(addr, tn) 256 frozen = " [frozen]" if addr in self.frozen else "" 257 print(f" {hex(addr)} = {val} ({tn}){frozen}") 258 259 def show_frozen(self): 260 if not self.frozen: 261 print(" no frozen addresses") 262 return 263 for addr, (value, type_name, _, _) in self.frozen.items(): 264 current = self.read_value(addr, type_name) 265 cur_str = str(current) if current is not None else "?" 266 print(f" {hex(addr)} frozen={value} current={cur_str} ({type_name})") 267 268 def print_help(self): 269 print("""commands: 270 scan VALUE [TYPE] scan memory (auto-detects i32/i64/f32/f64) 271 narrow VALUE narrow matches to new value 272 list show current matches 273 set VALUE write value to all matches (once) 274 freeze VALUE freeze all matches to value 275 freeze ADDR VALUE [TYPE] freeze specific address 276 unfreeze ADDR stop freezing address 277 unfreeze all stop all freezes 278 frozen show frozen addresses 279 read ADDR [TYPE] read value at address 280 write ADDR VALUE [TYPE] write value to address (once) 281 help show this help 282 quit exit 283 284types: i8 i16 i32 i64 f32 f64 (scan auto-detects if omitted) 285addresses are always hex (0x prefix optional) 286 287workflow: scan 100 → take damage → narrow 95 → repeat → freeze 999""") 288 289 def repl(self): 290 print(f"memhack attached to PID {self.pid}") 291 print('type "help" for commands, "scan VALUE" to start searching') 292 print() 293 294 while True: 295 try: 296 tags = [] 297 if self.matches: 298 tags.append(f"{len(self.matches)} matches") 299 if self.frozen: 300 tags.append(f"{len(self.frozen)} frozen") 301 tag = f" [{', '.join(tags)}]" if tags else "" 302 line = input(f"memhack{tag}> ").strip() 303 except (EOFError, KeyboardInterrupt): 304 print() 305 break 306 307 if not line: 308 continue 309 310 parts = line.split() 311 cmd = parts[0].lower() 312 313 try: 314 if cmd == "scan": 315 if len(parts) < 2: 316 print(" usage: scan VALUE [TYPE]") 317 continue 318 type_name = parts[2] if len(parts) > 2 else None 319 if type_name and type_name not in TYPES: 320 print(f" unknown type: {type_name}") 321 continue 322 self.scan(parts[1], type_name) 323 elif cmd == "narrow": 324 if len(parts) < 2: 325 print(" usage: narrow VALUE") 326 continue 327 self.narrow(parts[1]) 328 elif cmd == "list": 329 self.show_matches() 330 elif cmd == "set": 331 if len(parts) < 2: 332 print(" usage: set VALUE") 333 continue 334 self.set_matches(parts[1]) 335 elif cmd == "freeze": 336 if len(parts) == 2: 337 self.freeze_matches(parts[1]) 338 elif len(parts) >= 3: 339 type_name = parts[3] if len(parts) > 3 else "i32" 340 if type_name not in TYPES: 341 print(f" unknown type: {type_name}") 342 continue 343 self.freeze( 344 parse_addr(parts[1]), 345 parse_value(parts[2], type_name), 346 type_name, 347 ) 348 else: 349 print(" usage: freeze VALUE | freeze ADDR VALUE [TYPE]") 350 elif cmd == "unfreeze": 351 if len(parts) < 2: 352 print(" usage: unfreeze ADDR | unfreeze all") 353 elif parts[1] == "all": 354 self.unfreeze_all() 355 else: 356 self.unfreeze(parse_addr(parts[1])) 357 elif cmd == "frozen": 358 self.show_frozen() 359 elif cmd == "read": 360 if len(parts) < 2: 361 print(" usage: read ADDR [TYPE]") 362 continue 363 type_name = parts[2] if len(parts) > 2 else "i32" 364 val = self.read_value(parse_addr(parts[1]), type_name) 365 if val is not None: 366 print(f" {val}") 367 elif cmd == "write": 368 if len(parts) < 3: 369 print(" usage: write ADDR VALUE [TYPE]") 370 continue 371 type_name = parts[3] if len(parts) > 3 else "i32" 372 addr = parse_addr(parts[1]) 373 val = parse_value(parts[2], type_name) 374 if self.write_value(addr, val, type_name): 375 print(f" wrote {parts[2]} to {hex(addr)}") 376 elif cmd in ("quit", "exit", "q"): 377 break 378 elif cmd == "help": 379 self.print_help() 380 else: 381 print(f' unknown command: "{cmd}" (type "help")') 382 except (ValueError, IndexError) as e: 383 print(f" error: {e}") 384 385 self.unfreeze_all() 386 self.close_mem() 387 388 389def main(): 390 if len(sys.argv) < 2: 391 print("usage: memhack-repl PID", file=sys.stderr) 392 sys.exit(1) 393 394 pid = int(sys.argv[1]) 395 396 if not os.path.exists(f"/proc/{pid}"): 397 print(f"process {pid} not found", file=sys.stderr) 398 sys.exit(1) 399 400 MemHack(pid).repl() 401 402 403if __name__ == "__main__": 404 main()