Skip to content

Commit a91d18c

Browse files
committed
Add gamedata_checker.py
1 parent e1678bd commit a91d18c

2 files changed

Lines changed: 247 additions & 1 deletion

File tree

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,15 @@ Takes a SourceMod signature input and detects if it's unique or not.
1515

1616
Imports netprops and owner classes as structs and struct members into IDA's DB. Only works with the XML file provided by sm_dump_netprops_xml. It's still a WIP and doesn't catch all of them though; really shits the bed when it comes to datatables. You should also use the proper netprop dump for your OS, or else you will be very confused.
1717

18-
You also have the option of importing vtables from the found classes into IDA. I plan on separating this into another script, but until then, this will work.
18+
You also have the option of importing vtables from the found classes into IDA. I plan on separating this into another script, but until then, this will work.
19+
20+
21+
### gamedata_checker.py ###
22+
23+
Name says it all, but this verifies SourceMod gamedata files. This requires Valve's VDF library, install it with `pip install vdf`.
24+
25+
Has a few quirks with it at the moment:
26+
- It does not support multi-line comments within gamedata files nor will it support multiple instances of `#default` keys. Parsing core SourceMod gamedata files is essentially verboten.
27+
- Windows or stripped VTable offsets cannot be verified.
28+
- Function overloads tends to mess up VTable offset checking; e.g. `GiveNamedItem`.
29+
- Offset checking is variably difficult depending on naming conventions. If the gamedata key name is either not named exactly the same as the function name, it will not be found; e.g. `OnTakeDamage` -> `CBaseEntity::OnTakeDamage` and `CTFPlayer::OnTakeDamage` -> `CBaseEntity::OnTakeDamage` but `TakeDamage` != `CBaseEntity::OnTakeDamage`.

gamedata_checker.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import idautils
2+
import idaapi
3+
import idc
4+
import vdf
5+
6+
def get_os():
7+
# Lazy af lol
8+
return "linux" if ida_nalt.get_root_filename().endswith(".so") else "windows"
9+
10+
def checksig(sig):
11+
if sig[0] == '@':
12+
# Just check for existence of this mangled name
13+
return idc.get_name_ea_simple(sig[1:]) != idc.BADADDR
14+
15+
sig = sig.replace(r"\x", " ").replace("2A", "?").replace("2a", "?").replace("\\", "").strip()
16+
count = 0
17+
addr = 0
18+
addr = idc.find_binary(addr, idc.SEARCH_DOWN|idc.SEARCH_NEXT, sig)
19+
while addr != idc.BADADDR:
20+
count = count + 1
21+
addr = idc.find_binary(addr, idc.SEARCH_DOWN|idc.SEARCH_NEXT, sig)
22+
23+
return count == 1
24+
25+
# Unfortunately I don't care too much about overtly complex gamedata files
26+
# If you have multiple #default's in you first subsection or you have #default
27+
# anywhere else other than that first subsection, you're SOL. Sorry Silvers :c
28+
def get_gamedir(kv):
29+
gamedir = ""
30+
# If we've got multiple games supported, so let's just ask
31+
if len(kv.items()) > 1:
32+
gamedir = ida_kernwin.ask_str("", 0, "There are multiple supported games with this file. Which game directory is this for?")
33+
# Not in the basic game shit, check for support in default
34+
if gamedir not in kv.keys():
35+
default = kv.get("#default")
36+
# There's a default entry, check for supported
37+
if default:
38+
supported = kv.get("#supported")
39+
if supported:
40+
if gamedir in supported.values():
41+
return gamedir
42+
return ""
43+
return "#default"
44+
return ""
45+
else:
46+
# 1 item, see if it's a default
47+
gamedir = kv.keys()[0]
48+
if gamedir == "#default":
49+
default = kv.items()[0]
50+
# If it has multiple supports, check and see if we're in there
51+
supported = kv.get("#supported")
52+
if supported:
53+
if len(supported.items()) > 1:
54+
gamedir = ida_kernwin.ask_str("", 0, "There are multiple supported games with this file. Which game directory is this for?")
55+
if gamedir in default["#supported"].values():
56+
return gamedir
57+
return ""
58+
return supported.values()[0]
59+
return "#default"
60+
61+
return gamedir
62+
63+
def get_thisoffs(name):
64+
mangled = "_ZTV{}{}".format(len(name), name)
65+
return idc.get_name_ea_simple(mangled)
66+
67+
def read_vtable(funcname, ea):
68+
if "(" in funcname:
69+
funcname = funcname[:funcname.find("(")]
70+
71+
funcs = {}
72+
offset = 0
73+
while ea != idc.BADADDR:
74+
offs = idc.get_wide_dword(ea)
75+
if not ida_bytes.is_code(ida_bytes.get_full_flags(offs)):
76+
break
77+
78+
name = idc.get_name(offs, ida_name.GN_VISIBLE)
79+
demangled = idc.demangle_name(name, idc.get_inf_attr(idc.INF_SHORT_DN))
80+
if demangled == None:
81+
demangled = name
82+
83+
if "(" in demangled:
84+
demangled = demangled[:demangled.find("(")]
85+
funcs[demangled.lower()] = offset
86+
87+
offset += 1
88+
ea = ida_bytes.next_not_tail(ea)
89+
90+
# We've got a list of function names, let's do this really shittily because idk any other way
91+
92+
# Try by exactness
93+
if "(" in funcname:
94+
# If you've got a template func good luck
95+
funcname = funcname[:funcname.find("(")]
96+
97+
# This is a good programmer who makes their gamedata the proper way :)
98+
offs = funcs.get(funcname.lower(), -1)
99+
if offs != -1:
100+
return offs
101+
102+
# Often done but sometimes there are subclass types thrown in, save those too
103+
if "::" in funcname:
104+
funcname = funcname[funcname.find("::")+2:]
105+
106+
# Try by exact function name
107+
funcnames = {}
108+
for key, value in funcs.iteritems():
109+
# Function overloads can fuck right off
110+
s = key[key.find("::")+2:].lower() if "::" in key else key.lower()
111+
funcnames[s.lower()] = value
112+
113+
offs = funcnames.get(funcname.lower(), -1)
114+
# Second best way, exact function name
115+
if offs != -1:
116+
return offs
117+
118+
return -1
119+
# Anything else near here is either some random mem offset or some other crap
120+
# possibilities = [key for key in funcnames.keys() if funcname in key]
121+
# return [found for found in funcnames[x] for x in possibilities]
122+
123+
# So we've a few options with finding appropriate vtable offsets
124+
# Option 1: Check and see if they use the optimal naming sequence "Type::Function" and revel in that
125+
# If we can't deduce that exactly, try option 2
126+
# Option 2: They must've used just the function name, run through every function that has a name like that
127+
# and perform option 1 on each
128+
# Windows can suck a wiener on this one
129+
def try_get_voffset(funcname):
130+
if "::" in funcname:
131+
# Option 1
132+
typename = funcname[:funcname.find("::")]
133+
thisoffs = get_thisoffs(typename)
134+
offs = -1
135+
if thisoffs != idc.BADADDR:
136+
offs = read_vtable(funcname, thisoffs + 8)
137+
if offs != -1:
138+
return offs
139+
140+
funcname = funcname[funcname.find("::")+2:]
141+
142+
if "(" in funcname:
143+
funcname = funcname[:funcname.find("(")]
144+
145+
# Let's chug along all of these functions, woohoo for option 2!
146+
for func in idautils.Functions():
147+
name = idc.get_name(func, ida_name.GN_VISIBLE)
148+
if funcname not in name: # funcname should only be a plain function decl, so it would be unfettered in a mangled name
149+
continue
150+
151+
demangled = idc.demangle_name(name, idc.get_inf_attr(idc.INF_SHORT_DN))
152+
if demangled == None:
153+
continue
154+
155+
demname = demangled
156+
if "::" in demname:
157+
demname = demname[demname.find("::")+2:]
158+
if "(" in demname:
159+
demname = demname[:demname.find("(")]
160+
161+
if funcname == demname: # Here's an exact match, let's get the type name then read the vtable
162+
if "::" not in demangled: # Okay, so someone somewhere is an idiot and managed to provide an offset name that is the
163+
continue # same name as some non-class function and this will manage to catch that
164+
165+
typename = demangled[:demangled.find("::")]
166+
thisoffs = get_thisoffs(typename)
167+
if thisoffs != idc.BADADDR:
168+
offs = read_vtable(funcname, thisoffs + 8)
169+
if offs != -1:
170+
return offs
171+
172+
return -1 # Your naming conventions suck and you should feel bad. Or this is Windows and you should still feel bad
173+
174+
def main():
175+
kv = None
176+
with open(ida_kernwin.ask_file(0, "*.txt", "Select a gamedata file")) as f:
177+
kv = vdf.load(f)
178+
179+
if kv == None:
180+
ida_kernwin.warning("Could not load file!")
181+
return
182+
183+
kv = kv.values()[0]
184+
os = get_os()
185+
gamedir = get_gamedir(kv)
186+
if not gamedir:
187+
ida_kernwin.warning("Could not find game directory in file")
188+
return
189+
190+
kv = kv[gamedir]
191+
found = {
192+
"Signatures": {},
193+
"Offsets": {}
194+
}
195+
196+
signatures = kv.get("Signatures")
197+
if signatures:
198+
for name, handle in signatures.items():
199+
s = handle.get(os)
200+
if s:
201+
found["Signatures"][name] = checksig(s)
202+
203+
offsets = kv.get("Offsets")
204+
if offsets and os != "windows":
205+
for name, handle in offsets.items():
206+
offset = handle.get(os, -1)
207+
if offset != -1:
208+
found["Offsets"][name] = [offset, try_get_voffset(name)]
209+
210+
if len(found["Signatures"].items()):
211+
print("Signatures:")
212+
for key, value in found["Signatures"].iteritems():
213+
print("\t{} - {}".format(key, "VALID" if value else "INVALID"))
214+
215+
if len(found["Offsets"].items()):
216+
print("Offsets:")
217+
for key, value in found["Offsets"].iteritems():
218+
s = "\t{} - ".format(key)
219+
if isinstance(value[1], list):
220+
s += "{} == {} - {}".format(value[0], value[1], "VALID" if value[0] in value[1] else "INVALID")
221+
else:
222+
if int(value[0]) == int(value[1]):
223+
s += "{} == {} - VALID".format(value[0], value[1])
224+
else:
225+
s += "{} == {} - {}".format(value[0], value[1], "NOT FOUND" if value[1] == -1 else "INVALID")
226+
227+
print(s)
228+
229+
if os == "windows" and kv.get("Offsets"):
230+
print("Offset checking is not supported on Windows binaries")
231+
232+
ida_kernwin.warning("Check console for output")
233+
234+
if __name__ == "__main__":
235+
main()

0 commit comments

Comments
 (0)