verify_gamedata.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. #!/usr/bin/env python3
  2. """
  3. verify_gamedata.py — offline validator for vprof_timings.txt.
  4. Mimics the parts of SourceMod's GameConfig signature resolution and Source
  5. Scramble's MemPatch application that matter, so the gamedata can be checked
  6. against a target binary without running a server:
  7. * KeyValues/SMC parse of the gamedata (comments, nested sections, duplicate
  8. keys preserved).
  9. * `@mangled` signatures resolved from the ELF `.symtab` (the same thing
  10. MemoryUtils::ResolveSymbol does when a game reports hidden symbols, as
  11. TF2 does). Byte-pattern signatures with `\\x2A` wildcards are still
  12. supported for completeness.
  13. * Source Scramble's `offset`/`verify`/`patch` semantics.
  14. Usage:
  15. python3 verify_gamedata.py vprof_timings.txt engine_srv.so engine_srv.x64.so
  16. The two binaries are matched to the `linux` and `linux64` blocks respectively.
  17. """
  18. import struct
  19. import sys
  20. # --------------------------------------------------------------------------
  21. # Minimal SMC/KeyValues parser
  22. # --------------------------------------------------------------------------
  23. def tokenize(text):
  24. i, n = 0, len(text)
  25. while i < n:
  26. c = text[i]
  27. if c in " \t\r\n":
  28. i += 1
  29. continue
  30. if c == "/" and i + 1 < n and text[i + 1] == "/":
  31. while i < n and text[i] != "\n":
  32. i += 1
  33. continue
  34. if c == "{":
  35. yield "{"
  36. i += 1
  37. continue
  38. if c == "}":
  39. yield "}"
  40. i += 1
  41. continue
  42. if c == '"':
  43. i += 1
  44. buf = []
  45. while i < n and text[i] != '"':
  46. if text[i] == "\\" and i + 1 < n:
  47. buf.append(text[i])
  48. buf.append(text[i + 1])
  49. i += 2
  50. else:
  51. buf.append(text[i])
  52. i += 1
  53. i += 1
  54. yield ("str", "".join(buf))
  55. continue
  56. j = i
  57. while j < n and text[j] not in " \t\r\n{}":
  58. j += 1
  59. yield ("str", text[i:j])
  60. i = j
  61. def parse_block(it):
  62. entries = []
  63. while True:
  64. try:
  65. t = next(it)
  66. except StopIteration:
  67. return entries
  68. if t == "}":
  69. return entries
  70. if t == "{":
  71. continue
  72. key = t[1]
  73. nxt = next(it)
  74. if nxt == "{":
  75. entries.append((key, parse_block(it)))
  76. else:
  77. entries.append((key, nxt[1]))
  78. def sub(entries, key):
  79. if entries is None:
  80. return None
  81. for k, v in entries:
  82. if k == key:
  83. return v
  84. return None
  85. # --------------------------------------------------------------------------
  86. # ELF symbol table (`@symbol` resolution)
  87. # --------------------------------------------------------------------------
  88. def elf_symbols(path):
  89. data = open(path, "rb").read()
  90. if data[:4] != b"\x7fELF":
  91. raise ValueError("%s is not an ELF file" % path)
  92. ei_class, ei_data = data[4], data[5]
  93. if ei_data != 1:
  94. raise ValueError("only little-endian ELF is supported")
  95. if ei_class == 1:
  96. e_shoff = struct.unpack_from("<I", data, 0x20)[0]
  97. e_shentsize = struct.unpack_from("<H", data, 0x2E)[0]
  98. e_shnum = struct.unpack_from("<H", data, 0x30)[0]
  99. e_shstrndx = struct.unpack_from("<H", data, 0x32)[0]
  100. sh_fmt = "<IIIIIIIIII"
  101. sym_fmt = "<IIIBBH"
  102. sym_entsize = 16
  103. elif ei_class == 2:
  104. e_shoff = struct.unpack_from("<Q", data, 0x28)[0]
  105. e_shentsize = struct.unpack_from("<H", data, 0x3A)[0]
  106. e_shnum = struct.unpack_from("<H", data, 0x3C)[0]
  107. e_shstrndx = struct.unpack_from("<H", data, 0x3E)[0]
  108. sh_fmt = "<IIQQQQIIQQ"
  109. sym_fmt = "<IBBHQQ"
  110. sym_entsize = 24
  111. else:
  112. raise ValueError("unknown ELF class %d" % ei_class)
  113. def section(i):
  114. return struct.unpack_from(sh_fmt, data, e_shoff + i * e_shentsize)
  115. shstr = section(e_shstrndx)
  116. shstr_off = shstr[4]
  117. def section_name(name_off):
  118. end = data.index(b"\0", shstr_off + name_off)
  119. return data[shstr_off + name_off : end].decode("ascii", "replace")
  120. symtab = strtab = None
  121. for i in range(e_shnum):
  122. s = section(i)
  123. name = section_name(s[0])
  124. if name == ".symtab":
  125. symtab = s
  126. elif name == ".strtab":
  127. strtab = s
  128. symbols = {}
  129. if symtab is None or strtab is None:
  130. return symbols
  131. sym_off, sym_size, sym_entsize_hdr = symtab[4], symtab[5], symtab[9]
  132. entsize = sym_entsize_hdr or sym_entsize
  133. str_off = strtab[4]
  134. count = sym_size // entsize
  135. for i in range(count):
  136. fields = struct.unpack_from(sym_fmt, data, sym_off + i * entsize)
  137. if ei_class == 1:
  138. st_name, st_value, _st_size, _st_info, _st_other, st_shndx = fields
  139. else:
  140. st_name, _st_info, _st_other, st_shndx, st_value, _st_size = fields
  141. if st_name == 0 or st_shndx == 0:
  142. continue
  143. end = data.index(b"\0", str_off + st_name)
  144. name = data[str_off + st_name : end].decode("ascii", "replace")
  145. symbols.setdefault(name, st_value)
  146. return symbols
  147. # --------------------------------------------------------------------------
  148. # Byte-pattern signatures (legacy fallback)
  149. # --------------------------------------------------------------------------
  150. def decode_pattern(s):
  151. out = bytearray()
  152. i = 0
  153. while i < len(s):
  154. if s[i] == "\\" and i + 1 < len(s) and s[i + 1] == "x":
  155. out.append(int(s[i + 2 : i + 4], 16))
  156. i += 4
  157. else:
  158. out.append(ord(s[i]))
  159. i += 1
  160. return bytes(out)
  161. def find_pattern(hay, pat):
  162. for off in range(0, len(hay) - len(pat) + 1):
  163. ok = True
  164. for i in range(len(pat)):
  165. if pat[i] != 0x2A and pat[i] != hay[off + i]:
  166. ok = False
  167. break
  168. if ok:
  169. return off
  170. return None
  171. def parse_offset(s):
  172. if s.lower().endswith("h"):
  173. return int(s[:-1], 16)
  174. return int(s, 0)
  175. # --------------------------------------------------------------------------
  176. # Main
  177. # --------------------------------------------------------------------------
  178. def main(argv):
  179. if len(argv) != 4:
  180. sys.stderr.write(__doc__)
  181. return 2
  182. gamedata, bin32, bin64 = argv[1], argv[2], argv[3]
  183. text = open(gamedata, encoding="utf-8", errors="replace").read()
  184. root = parse_block(iter(tokenize(text)))
  185. tf = sub(sub(root, "Games"), "tf")
  186. sigs = sub(tf, "Signatures")
  187. patches = sub(tf, "MemPatches")
  188. targets = {
  189. "linux": {
  190. "bin": bin32,
  191. "bytes": open(bin32, "rb").read(),
  192. "syms": elf_symbols(bin32),
  193. },
  194. "linux64": {
  195. "bin": bin64,
  196. "bytes": open(bin64, "rb").read(),
  197. "syms": elf_symbols(bin64),
  198. },
  199. }
  200. failures = 0
  201. for plat in ("linux", "linux64"):
  202. tgt = targets[plat]
  203. hay, syms = tgt["bytes"], tgt["syms"]
  204. print("=== %s (%s) ===" % (plat, tgt["bin"]))
  205. for name, body in patches:
  206. sig_name = sub(body, "signature")
  207. sigdef = sub(sigs, sig_name)
  208. library = sub(sigdef, "library")
  209. sig = sub(sigdef, plat)
  210. block = sub(body, plat)
  211. if sig.startswith("@"):
  212. symname = sig[1:]
  213. addr = syms.get(symname)
  214. how = "@%s" % symname
  215. else:
  216. addr = find_pattern(hay, decode_pattern(sig))
  217. how = "pattern"
  218. if addr is None:
  219. print(" [FAIL] %-62s %s not found" % (name, how))
  220. failures += 1
  221. continue
  222. if not isinstance(block, list):
  223. print(" [skip] %-62s no %s block" % (name, plat))
  224. continue
  225. off = parse_offset(sub(block, "offset"))
  226. verify = decode_pattern(sub(block, "verify"))
  227. replace = decode_pattern(sub(block, "patch"))
  228. at = addr + off
  229. current = hay[at : at + len(verify)]
  230. ok = all(v == 0x2A or v == c for v, c in zip(verify, current))
  231. if not ok:
  232. failures += 1
  233. print(
  234. " [%s] %-62s lib=%s addr=0x%x off=0x%x verify=%s patch=%s"
  235. % (
  236. "OK" if ok else "VERIFY-MISMATCH",
  237. name,
  238. library,
  239. addr,
  240. off,
  241. current.hex(),
  242. replace.hex(),
  243. )
  244. )
  245. print("\n%s" % ("ALL PATCHES VERIFIED" if failures == 0 else "FAILURES: %d" % failures))
  246. return 1 if failures else 0
  247. if __name__ == "__main__":
  248. raise SystemExit(main(sys.argv))