143 lines
4.5 KiB
Python
143 lines
4.5 KiB
Python
|
#Generates a SourceMod-ready signature.
|
||
|
#@author nosoop
|
||
|
#@category _NEW_
|
||
|
#@keybinding
|
||
|
#@menupath
|
||
|
#@toolbar
|
||
|
|
||
|
from __future__ import print_function
|
||
|
|
||
|
import collections
|
||
|
import ghidra.program.model.lang.OperandType as OperandType
|
||
|
import ghidra.program.model.lang.Register as Register
|
||
|
import ghidra.program.model.address.AddressSet as AddressSet
|
||
|
|
||
|
MAKE_SIG_AT = collections.OrderedDict([
|
||
|
('fn', 'start of function'),
|
||
|
('cursor', 'instruction at cursor')
|
||
|
])
|
||
|
|
||
|
BytePattern = collections.namedtuple('BytePattern', ['is_wildcard', 'byte'])
|
||
|
|
||
|
def __bytepattern_ida_str(self):
|
||
|
# return an IDA-style binary search string
|
||
|
return '{:02X}'.format(self.byte) if not self.is_wildcard else '?'
|
||
|
|
||
|
def __bytepattern_sig_str(self):
|
||
|
# return a SourceMod-style byte signature
|
||
|
return r'\x{:02X}'.format(self.byte) if not self.is_wildcard else r'\x2A'
|
||
|
|
||
|
BytePattern.ida_str = __bytepattern_ida_str
|
||
|
BytePattern.sig_str = __bytepattern_sig_str
|
||
|
|
||
|
def dumpOperandInfo(ins, op):
|
||
|
t = hex(ins.getOperandType(op))
|
||
|
print(' ' + str(ins.getPrototype().getOperandValueMask(op)) + ' ' + str(t))
|
||
|
|
||
|
# TODO if register
|
||
|
for opobj in ins.getOpObjects(op):
|
||
|
print(' - ' + str(opobj))
|
||
|
|
||
|
def shouldMaskOperand(ins, opIndex):
|
||
|
"""
|
||
|
Returns True if the given instruction operand mask should be masked in the signature.
|
||
|
"""
|
||
|
optype = ins.getOperandType(opIndex)
|
||
|
# if any(reg.getName() == "EBP" for reg in filter(lambda op: isinstance(op, Register), ins.getOpObjects(opIndex))):
|
||
|
# return False
|
||
|
return optype & OperandType.DYNAMIC or optype & OperandType.ADDRESS
|
||
|
|
||
|
def getMaskedInstruction(ins):
|
||
|
"""
|
||
|
Returns a generator that outputs either a byte to match or None if the byte should be masked.
|
||
|
"""
|
||
|
# print(ins)
|
||
|
|
||
|
# resulting mask should match the instruction length
|
||
|
mask = [0] * ins.length
|
||
|
|
||
|
proto = ins.getPrototype()
|
||
|
# iterate over operands and mask bytes
|
||
|
for op in range(proto.getNumOperands()):
|
||
|
# dumpOperandInfo(ins, op)
|
||
|
|
||
|
# TODO deal with partial byte masks
|
||
|
if shouldMaskOperand(ins, op):
|
||
|
mask = [ m | v & 0xFF for m, v in zip(mask, proto.getOperandValueMask(op).getBytes()) ]
|
||
|
# print(' ' + str(mask))
|
||
|
|
||
|
for m, b in zip(mask, ins.getBytes()):
|
||
|
if m == 0xFF:
|
||
|
# we only check for fully masked bytes at the moment
|
||
|
yield BytePattern(is_wildcard = True, byte = None)
|
||
|
else:
|
||
|
yield BytePattern(byte = b & 0xFF, is_wildcard = False)
|
||
|
|
||
|
def process(start_at = MAKE_SIG_AT['fn']):
|
||
|
fm = currentProgram.getFunctionManager()
|
||
|
fn = fm.getFunctionContaining(currentAddress)
|
||
|
cm = currentProgram.getCodeManager()
|
||
|
|
||
|
if start_at == MAKE_SIG_AT['fn']:
|
||
|
ins = cm.getInstructionAt(fn.getEntryPoint())
|
||
|
elif start_at == MAKE_SIG_AT['cursor']:
|
||
|
ins = cm.getInstructionContaining(currentAddress)
|
||
|
|
||
|
if not ins:
|
||
|
raise Exception("Could not find entry point to function")
|
||
|
|
||
|
pattern = "" # contains pattern string (supports regular expressions)
|
||
|
byte_pattern = [] # contains BytePattern instances
|
||
|
|
||
|
# keep track of our matches
|
||
|
matches = []
|
||
|
match_limit = 128
|
||
|
|
||
|
while fm.getFunctionContaining(ins.getAddress()) == fn:
|
||
|
for entry in getMaskedInstruction(ins):
|
||
|
byte_pattern.append(entry)
|
||
|
if entry.is_wildcard:
|
||
|
pattern += '.'
|
||
|
else:
|
||
|
pattern += r'\x{:02x}'.format(entry.byte)
|
||
|
|
||
|
expected_next = ins.getAddress().add(ins.length)
|
||
|
ins = ins.getNext()
|
||
|
|
||
|
if ins.getAddress() != expected_next:
|
||
|
# we don't have a good way to deal with alignment bytes
|
||
|
# raise an exception for now
|
||
|
raise Exception("Instruction at %s is not adjacent"
|
||
|
" to previous (expected %s)" % (expected_next, ins.getAddress()))
|
||
|
|
||
|
if 0 < len(matches) < match_limit:
|
||
|
# we have all the remaining matches, start only searching those addresses
|
||
|
match_set = AddressSet()
|
||
|
for addr in matches:
|
||
|
match_set.add(addr, addr.add(len(byte_pattern)))
|
||
|
matches = findBytes(match_set, pattern, match_limit, 1)
|
||
|
else:
|
||
|
# the matches are sorted in ascending order, so the first match will be the start
|
||
|
matches = findBytes(matches[0] if len(matches) else None, pattern, match_limit)
|
||
|
|
||
|
if len(matches) < 2:
|
||
|
break
|
||
|
|
||
|
if not len(matches) == 1:
|
||
|
print(*(b.ida_str() for b in byte_pattern))
|
||
|
print('Signature matched', len(matches), 'locations:', *(matches))
|
||
|
raise Exception("Could not find unique signature")
|
||
|
else:
|
||
|
print("Signature for", fn.getName())
|
||
|
print(*(b.ida_str() for b in byte_pattern))
|
||
|
print("".join(b.sig_str() for b in byte_pattern))
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
fm = currentProgram.getFunctionManager()
|
||
|
fn = fm.getFunctionContaining(currentAddress)
|
||
|
if not fn:
|
||
|
raise Exception("Not in a function")
|
||
|
|
||
|
start_at = askChoice("makesig", "Make sig at:", MAKE_SIG_AT.values(), MAKE_SIG_AT['fn'])
|
||
|
process(start_at)
|