Repository navigation
Expand file tree
/
Copy pathmappings.generator.py
More file actions
136 lines (108 loc) · 4.71 KB
/
Copy pathmappings.generator.py
File metadata and controls
136 lines (108 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""Builds mappings.json from mappings.txt.
mappings.txt is the only file a human edits; mappings.json is what Coderpack
and every other consumer reads. A row joins the build by carrying a tag in its
notes column, `[key=hpDamage]` or `[key=hpDamage hooked]` for a site the
agent attaches to. Untagged rows stay documentation.
python mappings.generator.py write mappings.json
python mappings.generator.py --check fail if the file on disk is stale
Every error here is loud on purpose: a wrong address does not throw, it gives a
hook that silently never fires, which costs a debugging session to notice.
"""
import json
import pathlib
import re
import sys
HERE = pathlib.Path(__file__).resolve().parent
REGISTRY = HERE / "mappings.txt"
OUT = HERE / "mappings.json"
IMAGE_BASE = 0x00400000
HEX = re.compile(r"0x[0-9A-Fa-f]+$")
ROW = re.compile(r"^(\S+)(?:\s+(\S+))?")
TAG = re.compile(r"\[key=([^\]]*)\]")
TAG_BODY = re.compile(r"([A-Za-z][A-Za-z0-9_]*)( hooked)?$")
COMMENT = ("Generated by mappings.generator.py from mappings.txt. Do not edit: "
"add the row and its [key=...] tag there instead.")
HOOKED_COMMENT = ("Addresses the agent attaches to, as opposed to calls or "
"reads. Coderpack's tools/hooksafe.py checks each one for "
"branches landing inside the bytes Frida's trampoline "
"overwrites.")
class Failed(Exception):
pass
def fail(line_no, message):
raise Failed(f"mappings.txt:{line_no}: {message}")
def parse(text):
"""(rva, va, hooked) from the tagged rows, checking every address row."""
rva, va, hooked, seen = {}, {}, [], {}
body_started = False
for line_no, line in enumerate(text.split("\n"), 1):
# The header explains the tag syntax by example, so it is not scanned.
body_started = body_started or line.startswith("##")
if not body_started:
continue
tag = TAG.search(line)
if not line.startswith("0x"):
if tag:
fail(line_no, "[key=...] cannot appear on a non-address row")
continue
first, second = ROW.match(line).groups()
if not HEX.match(first):
fail(line_no, f"Malformed address: “{first}”")
address = int(first, 16)
# A two-address row is VA + RVA. A one-address row is a global (VA).
paired = second is not None and second.startswith("0x")
if paired:
if not HEX.match(second):
fail(line_no, f"Malformed RVA: “{second}”")
if int(second, 16) != address - IMAGE_BASE:
fail(line_no, f"{second} is not {first} - 0x{IMAGE_BASE:X}")
if "[hooked]" in line:
fail(line_no, "[hooked] alone marks nothing. Use "
"[key=<name> hooked]")
if not tag:
continue
body = TAG_BODY.match(tag.group(1))
if not body:
fail(line_no, f"Malformed tag [key={tag.group(1)}]. Expected "
"[key=<name>] or [key=<name> hooked]")
name = body.group(1)
if name in seen:
fail(line_no, f"Duplicate key “{name}”; already used on line "
f"{seen[name]}")
seen[name] = line_no
value = f"0x{address - IMAGE_BASE:X}" if paired else f"0x{address:X}"
(rva if paired else va)[name] = value
if body.group(2):
if not paired:
fail(line_no, f"{name} is a global. There is nothing to hook.")
hooked.append(name)
missing = [name for name in hooked if name not in rva]
if missing:
raise Failed(f"Hooked entries without keys: {', '.join(missing)}")
return rva, va, hooked
def build():
rva, va, hooked = parse(REGISTRY.read_text(encoding="utf-8"))
if not rva:
raise Failed("No [key=...] tags found. Is mappings.txt intact?")
config = {"_": COMMENT, "rva": rva, "va": va,
"hooked": hooked, "_hooked": HOOKED_COMMENT}
return config, json.dumps(config, indent=2) + "\n"
def main(argv):
try:
config, text = build()
except Failed as error:
print(error, file=sys.stderr)
return 1
if "--check" in argv:
current = OUT.read_text(encoding="utf-8") if OUT.is_file() else None
if current != text:
print("mappings.json is stale. Run: python "
"mappings.generator.py", file=sys.stderr)
return 1
print("mappings.json is up to date.")
return 0
OUT.write_text(text, encoding="utf-8")
print(f"mappings.json: {len(config['rva'])} RVAs, {len(config['va'])} "
f"globals, {len(config['hooked'])} hooked")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))