Skip to content

Instantly share code, notes, and snippets.

@nosoop
Created October 22, 2019 04:35
Show Gist options
  • Save nosoop/e0f410f78e0c27a3ea3641b7f1010155 to your computer and use it in GitHub Desktop.
Save nosoop/e0f410f78e0c27a3ea3641b7f1010155 to your computer and use it in GitHub Desktop.
Dumps information from SMX files
#!/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