| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- #!/usr/bin/env python3
- """
- verify_gamedata.py — offline validator for vprof_timings.txt.
- Mimics the parts of SourceMod's GameConfig signature resolution and Source
- Scramble's MemPatch application that matter, so the gamedata can be checked
- against a target binary without running a server:
- * KeyValues/SMC parse of the gamedata (comments, nested sections, duplicate
- keys preserved).
- * `@mangled` signatures resolved from the ELF `.symtab` (the same thing
- MemoryUtils::ResolveSymbol does when a game reports hidden symbols, as
- TF2 does). Byte-pattern signatures with `\\x2A` wildcards are still
- supported for completeness.
- * Source Scramble's `offset`/`verify`/`patch` semantics.
- Usage:
- python3 verify_gamedata.py vprof_timings.txt engine_srv.so engine_srv.x64.so
- The two binaries are matched to the `linux` and `linux64` blocks respectively.
- """
- import struct
- import sys
- # --------------------------------------------------------------------------
- # Minimal SMC/KeyValues parser
- # --------------------------------------------------------------------------
- def tokenize(text):
- i, n = 0, len(text)
- while i < n:
- c = text[i]
- if c in " \t\r\n":
- i += 1
- continue
- if c == "/" and i + 1 < n and text[i + 1] == "/":
- while i < n and text[i] != "\n":
- i += 1
- continue
- if c == "{":
- yield "{"
- i += 1
- continue
- if c == "}":
- yield "}"
- i += 1
- continue
- if c == '"':
- i += 1
- buf = []
- while i < n and text[i] != '"':
- if text[i] == "\\" and i + 1 < n:
- buf.append(text[i])
- buf.append(text[i + 1])
- i += 2
- else:
- buf.append(text[i])
- i += 1
- i += 1
- yield ("str", "".join(buf))
- continue
- j = i
- while j < n and text[j] not in " \t\r\n{}":
- j += 1
- yield ("str", text[i:j])
- i = j
- def parse_block(it):
- entries = []
- while True:
- try:
- t = next(it)
- except StopIteration:
- return entries
- if t == "}":
- return entries
- if t == "{":
- continue
- key = t[1]
- nxt = next(it)
- if nxt == "{":
- entries.append((key, parse_block(it)))
- else:
- entries.append((key, nxt[1]))
- def sub(entries, key):
- if entries is None:
- return None
- for k, v in entries:
- if k == key:
- return v
- return None
- # --------------------------------------------------------------------------
- # ELF symbol table (`@symbol` resolution)
- # --------------------------------------------------------------------------
- def elf_symbols(path):
- data = open(path, "rb").read()
- if data[:4] != b"\x7fELF":
- raise ValueError("%s is not an ELF file" % path)
- ei_class, ei_data = data[4], data[5]
- if ei_data != 1:
- raise ValueError("only little-endian ELF is supported")
- if ei_class == 1:
- e_shoff = struct.unpack_from("<I", data, 0x20)[0]
- e_shentsize = struct.unpack_from("<H", data, 0x2E)[0]
- e_shnum = struct.unpack_from("<H", data, 0x30)[0]
- e_shstrndx = struct.unpack_from("<H", data, 0x32)[0]
- sh_fmt = "<IIIIIIIIII"
- sym_fmt = "<IIIBBH"
- sym_entsize = 16
- elif ei_class == 2:
- e_shoff = struct.unpack_from("<Q", data, 0x28)[0]
- e_shentsize = struct.unpack_from("<H", data, 0x3A)[0]
- e_shnum = struct.unpack_from("<H", data, 0x3C)[0]
- e_shstrndx = struct.unpack_from("<H", data, 0x3E)[0]
- sh_fmt = "<IIQQQQIIQQ"
- sym_fmt = "<IBBHQQ"
- sym_entsize = 24
- else:
- raise ValueError("unknown ELF class %d" % ei_class)
- def section(i):
- return struct.unpack_from(sh_fmt, data, e_shoff + i * e_shentsize)
- shstr = section(e_shstrndx)
- shstr_off = shstr[4]
- def section_name(name_off):
- end = data.index(b"\0", shstr_off + name_off)
- return data[shstr_off + name_off : end].decode("ascii", "replace")
- symtab = strtab = None
- for i in range(e_shnum):
- s = section(i)
- name = section_name(s[0])
- if name == ".symtab":
- symtab = s
- elif name == ".strtab":
- strtab = s
- symbols = {}
- if symtab is None or strtab is None:
- return symbols
- sym_off, sym_size, sym_entsize_hdr = symtab[4], symtab[5], symtab[9]
- entsize = sym_entsize_hdr or sym_entsize
- str_off = strtab[4]
- count = sym_size // entsize
- for i in range(count):
- fields = struct.unpack_from(sym_fmt, data, sym_off + i * entsize)
- if ei_class == 1:
- st_name, st_value, _st_size, _st_info, _st_other, st_shndx = fields
- else:
- st_name, _st_info, _st_other, st_shndx, st_value, _st_size = fields
- if st_name == 0 or st_shndx == 0:
- continue
- end = data.index(b"\0", str_off + st_name)
- name = data[str_off + st_name : end].decode("ascii", "replace")
- symbols.setdefault(name, st_value)
- return symbols
- # --------------------------------------------------------------------------
- # Byte-pattern signatures (legacy fallback)
- # --------------------------------------------------------------------------
- def decode_pattern(s):
- out = bytearray()
- i = 0
- while i < len(s):
- if s[i] == "\\" and i + 1 < len(s) and s[i + 1] == "x":
- out.append(int(s[i + 2 : i + 4], 16))
- i += 4
- else:
- out.append(ord(s[i]))
- i += 1
- return bytes(out)
- def find_pattern(hay, pat):
- for off in range(0, len(hay) - len(pat) + 1):
- ok = True
- for i in range(len(pat)):
- if pat[i] != 0x2A and pat[i] != hay[off + i]:
- ok = False
- break
- if ok:
- return off
- return None
- def parse_offset(s):
- if s.lower().endswith("h"):
- return int(s[:-1], 16)
- return int(s, 0)
- # --------------------------------------------------------------------------
- # Main
- # --------------------------------------------------------------------------
- def main(argv):
- if len(argv) != 4:
- sys.stderr.write(__doc__)
- return 2
- gamedata, bin32, bin64 = argv[1], argv[2], argv[3]
- text = open(gamedata, encoding="utf-8", errors="replace").read()
- root = parse_block(iter(tokenize(text)))
- tf = sub(sub(root, "Games"), "tf")
- sigs = sub(tf, "Signatures")
- patches = sub(tf, "MemPatches")
- targets = {
- "linux": {
- "bin": bin32,
- "bytes": open(bin32, "rb").read(),
- "syms": elf_symbols(bin32),
- },
- "linux64": {
- "bin": bin64,
- "bytes": open(bin64, "rb").read(),
- "syms": elf_symbols(bin64),
- },
- }
- failures = 0
- for plat in ("linux", "linux64"):
- tgt = targets[plat]
- hay, syms = tgt["bytes"], tgt["syms"]
- print("=== %s (%s) ===" % (plat, tgt["bin"]))
- for name, body in patches:
- sig_name = sub(body, "signature")
- sigdef = sub(sigs, sig_name)
- library = sub(sigdef, "library")
- sig = sub(sigdef, plat)
- block = sub(body, plat)
- if sig.startswith("@"):
- symname = sig[1:]
- addr = syms.get(symname)
- how = "@%s" % symname
- else:
- addr = find_pattern(hay, decode_pattern(sig))
- how = "pattern"
- if addr is None:
- print(" [FAIL] %-62s %s not found" % (name, how))
- failures += 1
- continue
- if not isinstance(block, list):
- print(" [skip] %-62s no %s block" % (name, plat))
- continue
- off = parse_offset(sub(block, "offset"))
- verify = decode_pattern(sub(block, "verify"))
- replace = decode_pattern(sub(block, "patch"))
- at = addr + off
- current = hay[at : at + len(verify)]
- ok = all(v == 0x2A or v == c for v, c in zip(verify, current))
- if not ok:
- failures += 1
- print(
- " [%s] %-62s lib=%s addr=0x%x off=0x%x verify=%s patch=%s"
- % (
- "OK" if ok else "VERIFY-MISMATCH",
- name,
- library,
- addr,
- off,
- current.hex(),
- replace.hex(),
- )
- )
- print("\n%s" % ("ALL PATCHES VERIFIED" if failures == 0 else "FAILURES: %d" % failures))
- return 1 if failures else 0
- if __name__ == "__main__":
- raise SystemExit(main(sys.argv))
|