This repository has no description
0

Configure Feed

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

tools.memhack: add memory editor with multi-type scan and freeze

+436
+25
tools/memhack/flake-module.nix
··· 1 + { ... }: 2 + { 3 + perSystem = 4 + { pkgs, ... }: 5 + let 6 + memhack-repl = pkgs.writeShellApplication { 7 + name = "memhack-repl"; 8 + runtimeInputs = [ 9 + pkgs.python3 10 + ]; 11 + text = ''exec python3 ${./memhack-repl.py} "$@"''; 12 + }; 13 + in 14 + { 15 + packages.memhack = pkgs.writeShellApplication { 16 + name = "memhack"; 17 + runtimeInputs = [ 18 + pkgs.procps 19 + pkgs.fzf 20 + memhack-repl 21 + ]; 22 + text = builtins.readFile ./memhack.sh; 23 + }; 24 + }; 25 + }
+404
tools/memhack/memhack-repl.py
··· 1 + """memhack - memory editor with multi-type scan, freeze, and narrow""" 2 + 3 + import os 4 + import readline # noqa: F401 - enables line editing in input() 5 + import struct 6 + import sys 7 + import threading 8 + import time 9 + 10 + TYPES = { 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 + 19 + AUTO_SCAN_TYPES = ["i32", "i64", "f32", "f64"] 20 + 21 + 22 + def parse_addr(s): 23 + """Parse address as hex (0x prefix optional).""" 24 + return int(s.removeprefix("0x").removeprefix("0X"), 16) 25 + 26 + 27 + def 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 + 34 + def 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 + 42 + def 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 + 58 + def 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 + 66 + class 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 + 284 + types: i8 i16 i32 i64 f32 f64 (scan auto-detects if omitted) 285 + addresses are always hex (0x prefix optional) 286 + 287 + workflow: 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 + 389 + def 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 + 403 + if __name__ == "__main__": 404 + main()
+7
tools/memhack/memhack.sh
··· 1 + #!/usr/bin/env bash 2 + pid=$(ps aux | fzf --header="Select a process to hack:" | awk '{print $2}') 3 + if [ -z "$pid" ]; then 4 + echo "No process selected." 5 + exit 1 6 + fi 7 + exec sudo "$(command -v memhack-repl)" "$pid"