Created
October 22, 2019 04:35
-
-
Save nosoop/e0f410f78e0c27a3ea3641b7f1010155 to your computer and use it in GitHub Desktop.
Dumps information from SMX files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# tfw no working sources to ensure values are correct | |
# https://web.archive.org/web/20100705221006/http://code.devicenull.org:80/index.php?title=Python:PluginReader | |
# https://wcfan.de/diverse/spfile.php | |
import struct, zlib, io, hashlib | |
import functools | |
@functools.lru_cache(maxsize = 32) | |
def __get_struct(fmt): | |
''' | |
creates or returns an existing struct | |
''' | |
return struct.Struct(fmt) | |
def unpack(fmt, stream, offset = None): | |
''' | |
unpacks values from a stream | |
source: https://stackoverflow.com/a/17537253 | |
''' | |
struct_obj = __get_struct(fmt) | |
if offset is not None: | |
stream.seek(offset) | |
return struct_obj.unpack(stream.read(struct_obj.size)) | |
def unpack_string(stream, offset = None, encoding = 'utf-8'): | |
''' unpacks a variable-length, zero-terminated string from a stream ''' | |
if offset is not None: | |
stream.seek(offset) | |
return bytes(iter(lambda: ord(stream.read(1)), 0)).decode(encoding) | |
if __name__ == '__main__': | |
import argparse | |
parser = argparse.ArgumentParser(description = "Dumps SourceMod plugin info", | |
usage = "%(prog)s [options]") | |
parser.add_argument('file', metavar='FILE') | |
args = parser.parse_args() | |
# https://github.com/alliedmodders/sourcepawn/blob/master/include/smx/smx-headers.h | |
with open(args.file, 'rb') as plugin_raw: | |
plugin = io.BufferedReader(plugin_raw) | |
magic, version, compression, disk_size, image_size, num_sections, stringtab, dataoffs = unpack('<IHBIIBII', plugin) | |
if not magic == 0x53504646: | |
raise AssertionError('File is not a valid SourceMod plugin file.') | |
print('magic', magic, 'version', hex(version), 'compression', compression, | |
'disk_size', disk_size, 'image_size', image_size, 'num_sections', num_sections, | |
'stringtab', stringtab, 'dataoffs', dataoffs) | |
if compression: | |
offs_header = plugin.tell() | |
nameoffs, dataoffs, size = unpack('<III', plugin) | |
print('nameoffs', nameoffs, 'dataoffs', dataoffs, 'size', size) | |
total_size = disk_size - dataoffs | |
print('total size', total_size) | |
plugin.seek(dataoffs) | |
data = zlib.decompress(plugin.read(total_size)) | |
plugin.seek(offs_header) | |
header_data = plugin.read(dataoffs - offs_header) | |
print('header_len', len(header_data)) | |
contents = io.BytesIO(header_data + data) | |
else: | |
contents = io.BytesIO(plugin.read(image_size)) | |
# read section list | |
section = {} | |
for i in range(num_sections): | |
# 12 = 3 32-bit values unpacked from header | |
contents.seek(i * 12) | |
section_nameoffs, section_dataoffs, section_size = unpack('<III', contents) | |
# end of offset section, start of name section | |
section_name = unpack_string(contents, offset = (num_sections * 12) + section_nameoffs) | |
print('section_name', section_name, 'nameoffs', section_nameoffs, 'dataoffs', section_dataoffs, 'size', section_size) | |
# TODO figure out why we need to backtrack by 24 | |
contents.seek(section_dataoffs - 24) | |
# add bytes, section size to section dict | |
section[section_name] = io.BytesIO(contents.read(section_size)), section_size | |
# parse .data | |
data_buffer, data_size = section['.data'] | |
datasize, memsize, data_file_offset = unpack('<III', data_buffer) | |
# parse .names | |
# other sections reference strings in this section by offset | |
names_buffer, names_size = section['.names'] | |
# parse .publics (public functions) | |
publics_buffer, publics_size = section['.publics'] | |
while publics_buffer.tell() < publics_size: | |
publics_addr, publics_name_off, *_ = unpack('<II', publics_buffer) | |
name = unpack_string(names_buffer, offset = publics_name_off) | |
print('publics:', name) | |
# parse .natives | |
# unpack cell from buffer and fetch name from .names | |
natives_buffer, natives_size = section['.natives'] | |
for i in range(natives_size // 4): | |
native_name_off, *_ = unpack('<I', natives_buffer) | |
name = unpack_string(names_buffer, offset = native_name_off) | |
print('native:', name) | |
# parse .pubvars | |
pubvars = {} | |
pubvars_buffer, pubvars_size = section['.pubvars'] | |
while pubvars_buffer.tell() < pubvars_size: | |
pubvar_addr, pubvar_name_off, *_ = unpack('<II', pubvars_buffer) | |
pubvar_name = unpack_string(names_buffer, offset = pubvar_name_off) | |
print('pubvars:', pubvar_name) | |
pubvars[pubvar_name] = pubvar_addr | |
# place plugin's data section (within but is not .data) into its own buffer | |
data_buffer.seek(data_file_offset) | |
plugin_data_buffer = io.BytesIO(data_buffer.read()) | |
# plugin info | |
if 'myinfo' in pubvars: | |
# pubvar is located within data buffer | |
# unpack 5 cells, corresponding to myinfo struct | |
# cells are locations within DAT | |
for addr in unpack('<IIIII', plugin_data_buffer, offset = pubvars['myinfo']): | |
print('myinfo:', unpack_string(plugin_data_buffer, offset = addr)) | |
# https://github.com/alliedmodders/sourcemod/blob/f156d48f45a7ffc4c2b7cef83a5399e9f74c76e5/core/logic/PluginSys.cpp#L297 | |
if '__version' in pubvars: | |
cell_version, cell_smvers, cell_date, cell_time = unpack('<IIII', plugin_data_buffer, offset = pubvars['__version']) | |
print('file version:', cell_version) | |
if cell_version >= 4: | |
print('compile date:', unpack_string(plugin_data_buffer, offset = cell_date)) | |
print('compile time:', unpack_string(plugin_data_buffer, offset = cell_time)) | |
if cell_version > 4: | |
print('compile version:', unpack_string(plugin_data_buffer, offset = cell_smvers)) | |
# sp_file_code_t | |
code_buffer, code_size = section['.code'] | |
*_, offs_code = unpack('<IBBHII', code_buffer) | |
# hash .code section starting from offset sp_file_code_t.code | |
m = hashlib.md5() | |
code_buffer.seek(offs_code) | |
m.update(code_buffer.read()) | |
code_hash = m.digest() | |
# hash .data section starting after the sp_file_data_t struct | |
m = hashlib.md5() | |
plugin_data_buffer.seek(0) | |
m.update(plugin_data_buffer.read()) | |
data_hash = m.digest() | |
# plugin hash is code_hash ^ data_hash | |
plugin_hash = bytes(c ^ d for c, d in zip(code_hash, data_hash)) | |
print('code hash:', code_hash.hex()) | |
print('data hash:', data_hash.hex()) | |
print('hash:', plugin_hash.hex()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment