forked from
lass.pds.lassul.us/superconfig
This repository has no description
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()