ida_scripts

To use, load the load.py script in IDA Pro. Supports the latest IDAPython api.

View Source
''' To use, load the `load.py` script in IDA Pro. Supports the latest IDAPython api. '''

__docformat__ = "numpy"

__pdoc__ = {
	'utils' : False
}

from . import memory, struc, module, misc, func

__all__ = ['memory', 'struc', 'func', 'module', 'misc']
View Source
'''
Provides class(es) for interacting with the debugged process's memory.
'''

try:
	import idc
	import idaapi
	import ida_dbg
	IS_64BIT = idaapi.get_inf_structure().is_64bit()
except: pass
import struct
from typing import *

from .utils import *

FMT_TO_SIZE = {
	'b' : 1,
	'h' : 2,
	'i' : 4,
	'l' : 4,
	'q' : 8,
}

STR_MAXLEN = 10000

class Pointer:
	''' Represent an address.
	The `Pointer` class offers convenient access to read/write from a memory address.

	Init a `Pointer` object with `Pointer.__init__` or the shorthand `ptr`.

	A `Pointer` object provides convenient functions to read/write certain data types (integers, string, pointer) to its address in memory.
	These methods take an optional `value` parameter. They read and return the value if `value` is `None`, write and return success/failure as a `bool` when `value` is not None.
	
	`Pointer` can be added/subtracted from another `Pointer` or `int` to yield a new `Pointer`.
	'''

	default_string_encoding: str = 'utf-8'
	''' The default string encoding used by `Pointer.string`. Defaults to `'utf-8'`. '''

	# INIT	
	def __init__(self, addr: int):
		''' Init with an address. '''
		assert type(addr) is int, 'addr can only be int!'
		self.addr = addr

	# READ/WRITE
	def read(self, size: int, offset: int = 0) -> bytes :
		''' Read bytes from `self.addr + offset` of given size. '''
		return idc.get_bytes(self.addr + offset, size, True)

	def write(self, bytes: bytes, offset: int = 0) -> bool:
		''' Write bytes to `self.addr + offset`. '''
		return ida_dbg.write_dbg_memory(self.addr + offset, bytes) == len(bytes)

	# The following methods reads when `value` is None and writes otherwise.
	def read_write_with_struct(self, format: str, value: int = None) -> Union[int, bool]:
		''' Read/Wwrite an integer. '''
		if value is None:
			return struct.unpack(format, self.read(FMT_TO_SIZE[format.lower()]))[0]
		else:
			return self.write(struct.pack(format, value))
	
	def u8(self, value: int = None) -> Union[int, bool]:
		''' Read/Write uint8. '''
		return self.read_write_with_struct('B', value)
	
	def u16(self, value: int = None) -> Union[int, bool]:
		''' Read/Write uint16. '''
		return self.read_write_with_struct('H', value)

	def u32(self, value: int = None) -> Union[int, bool]:
		''' Read/Write uint32. '''
		return self.read_write_with_struct('I', value)

	def u64(self, value: int = None) -> Union[int, bool]:
		''' Read/Write uint64. '''
		return self.read_write_with_struct('Q', value)

	def s8(self, value: int = None) -> Union[int, bool]:
		''' Read/Write int8. '''
		return self.read_write_with_struct('b', value)

	def s16(self, value: int = None) -> Union[int, bool]:
		''' Read/Write int16. '''
		return self.read_write_with_struct('h', value)

	def s32(self, value: int = None) -> Union[int, bool]:
		''' Read/Write int32. '''
		return self.read_write_with_struct('i', value)

	def s64(self, value: int = None) -> Union[int, bool]:
		''' Read/Write int64. '''
		return self.read_write_with_struct('q', value)

	def string(self, value: str = None, encoding: str = None, read_length: int = None) -> Union[str, bool]:
		''' Read/Write string, optionally specifying a custom encoding and length to read.
		
		Parameters
		----------
		value : str, default = None
			Value to be written, the function reads if it's `None`.
		encoding : str, default = None
			Encoding to be used for reading/writing, specify `None` to use `Pointer.default_string_encoding`.
		read_length : int, defualt = None
			Length to read, leave None to read until `\\0`.

		Returns
		-------
		`str` or `bool`
			Either the read string, or a bool indicating whether write succeeded.
		'''
		if not encoding:
			encoding = Pointer.default_string_encoding
		# read
		if value is None:
			if read_length is not None:
				return self.read(read_length).decode(encoding, 'backslashreplace')
			# read null-terminated
			string = b''
			while True:
				string += self.read(1, len(string))
				if string[-1] == 0 or len(string) > STR_MAXLEN: break
			return string.decode(encoding, 'backslashreplace')
		# write
		else:
			self.write(value.encode(encoding))

	def ptr(self, value: 'Pointer' = None) -> Union['Pointer', bool]:
		''' Read/Write pointer. '''
		method = self.u64 if IS_64BIT else self.u32
		# read
		if value is None:
			return Pointer(method())
		# write
		else:
			method(value.addr)

	def hexdump_str(self, len: int = 100, offset: int = 0) -> str:
		''' Generate hexdump from `self.addr + offset` of specified length. '''
		return hexdump(self.read(len, offset), start_offset=self.addr)

	def hexdump(self, len: int = 100, offset: int = 0):
		''' Print hexdump from `self.addr + offset` of specified length. '''
		print(self.hexdump_str(len, offset))

	# OPERATORS
	def __add__(self, other: Union['Pointer', int]) -> 'Pointer':
		if type(other) is Pointer:
			other = other.addr
		return Pointer(self.addr + other)

	def __sub__(self, other: Union['Pointer', int]) -> 'Pointer':
		if type(other) is Pointer:
			other = other.addr
		return Pointer(self.addr - other)

	def __eq__(self, other: Union['Pointer', int]) -> 'Pointer':
		if type(other) is Pointer:
			other = other.addr
		return self.addr == other

	# OPERATOR ALIASES
	# for chaining
	def add(self, other: Union['Pointer', int]) -> 'Pointer':
		return self + other
	
	def sub(self, other: Union['Pointer', int]) -> 'Pointer':
		return self - other

	def __repr__(self):
		return f'*{hex(self.addr)}'

def ptr(addr: int) -> Pointer:
	''' Initialise a `Pointer`. '''
	return Pointer(addr)

Provides class(es) for interacting with the debugged process's memory.

View Source
''' Provides class(es) for interacting with structures (definition and instances in memory). '''

try:
	import ida_typeinf
	import ida_struct
	import ida_idaapi
	import ida_bytes
	import idc
except: pass

import re
from json import dumps
from typing import *

from .memory import Pointer
from .utils import *

@auto_repr(['id', 'name', 'dtype', 'offset', 'size'], { 'offset' : hex, 'id' : hex })
class MemberT:
	''' Represent a member of a struct type. '''

	def __init__(self, member: 'ida_struct.member_t', struct: 'StrucT'):
		''' Init with an `ida_struct.member_t` object. '''
		assert type(member) is ida_struct.member_t, 'Invalid member!'
		self.member: 'ida_struct.member_t' = member
		self.struct = struct
		''' The struct type containing this member. '''

	@property
	def id(self) -> int:
		''' Member id (mid). '''
		return self.member.id

	@property
	def name(self) -> str:
		''' Member name. '''
		return ida_struct.get_member_name(self.id)

	@property
	def dtype(self) -> 'ida_typeinf.tinfo_t':
		''' Member type, represented with `ida_typeinf.tinfo_t`. '''
		tif = ida_typeinf.tinfo_t()
		ida_struct.get_member_tinfo(tif, self.member)
		return tif

	@property
	def offset(self) -> int:
		''' Member offset in struct. '''
		return self.member.soff

	@property
	def size(self) -> int:
		''' Member size in bytes. '''
		return ida_struct.get_member_size(self.member)

	@property
	def is_gap(self) -> bool:
		return str(self.dtype)[:4] in ['char', '_BYT'] and self.name.startswith('gap')

	def instance_at(self, addr: Pointer) -> Union[Pointer, int]:
		''' Get the value from the given address.
		
		This method reads and parses the value at the given address using the appropriate type.

		Parameters
		----------
		addr : `Pointer`

		Returns
		-------
		`Pointer` or `int`
			The parsed value.
		'''
		dtype = self.dtype
		# return ptr/array as Pointer
		if dtype.is_ptr_or_array():
			return addr
		# treat everthing else as int
		else:
			assert not dtype.is_floating(), 'FP not supported yet!'
			signed = dtype.is_signed()
			length = dtype.get_size() * 8
			method = f'{"s" if signed else "u"}{length}'
			assert hasattr(addr, method), f'Cannot find method {method}, it is likely not supported.'
			return getattr(addr, method)()

	def __hash__(self) -> int:
		return self.id

	def __eq__(self, o: object) -> bool:
		return hash(self) == hash(o)

	def instance_at_struct(self, struct_addr: Pointer):
		''' Get the value from a struct at the given address.
		
		Identical to `instance_at` but applies the offset of this member.

		Parameters
		----------
		struct_addr : `Pointer`
			A pointer to an instance of the struct.

		Returns
		-------
		`Pointer` or `int`
			The parsed value.
		'''
		return self.instance_at(struct_addr + self.offset)

@auto_repr(['id', 'name', 'size'], { 'id' : hex })
class StrucT:
	''' Represent a struct type. '''

	@staticmethod
	def find(name: str) -> 'StrucT':
		''' Find struct type by name. '''
		id = ida_struct.get_struc_id(name)
		return StrucT(ida_struct.get_struc(id)) if id != ida_idaapi.BADADDR else None

	@staticmethod
	def add_struc(name: str) -> 'StrucT':
		''' Create and return an empty struct. '''
		id = ida_struct.add_struc(ida_idaapi.BADADDR, name, False)
		return StrucT(ida_struct.get_struc(id)) if id != ida_idaapi.BADADDR else None

	def __init__(self, struc: 'ida_struct.struc_t'):
		''' Init with an `ida_struct.struc_t` object. '''
		assert type(struc) is ida_struct.struc_t, 'Invalid struc!'
		self.struc: ida_struct.struc_t = struc

	@property
	def id(self) -> int:
		''' Struct id. '''
		return self.struc.id

	@property
	def name(self) -> str:
		''' Struct name. '''
		return ida_struct.get_struc_name(self.id)

	@property
	def members(self) -> List[MemberT]:
		''' All member types of struct. '''
		return [MemberT(m, self) for m in self.struc.members]

	@property
	def size(self) -> int:
		''' Struct size in bytes. '''
		return ida_struct.get_struc_size(self.struc)

	def instance_at(self, addr: Pointer) -> 'StrucI':
		''' Get an instance of this struct at the given address. '''
		return StrucI(addr, self)
	
	def __getitem__(self, name: str) -> MemberT:
		''' Get a member type by name. '''
		member = ida_struct.get_member_by_name(self.struc, name)
		return MemberT(member, self) if member else None

	def member_at_offset(self, offset: int) -> MemberT:
		''' Get a member type by offset. '''
		member = ida_struct.get_member_by_id(ida_struct.get_member_id(self.struc, offset))
		return MemberT(member[0], self) if member else None

	def member_starting_at_offset(self, offset: int) -> bool:
		'''  Get a member type by offset, member must start at offset. '''
		member = self.member_at_offset(offset)
		return member if member and member.offset == offset else None

	# MUTATING METHODS
	# These methods do not check alignments.

	@staticmethod
	def create_struc(name: str) -> 'StrucT':
		''' Create and return a new struct with a given name. '''
		tid = ida_struct.add_struc(ida_idaapi.BADADDR, name, False)
		struct = ida_struct.get_struc(tid)
		return StrucT(struct)

	def add_member(self, declaration: str, offset: int) -> MemberT:
		''' Create and return a new member in this struct type.

		`declaration` should be like `TYPE_NAME NAME[SIZE]` (Array is optional).
		'''
		# parse decl
		tinfo, name = parse_declaration(declaration)
		size = tinfo.get_size()
		# create a bytes member of equivalent size
		result = ida_struct.add_struc_member(self.struc, name, offset, idc.FF_BYTE, None, size)
		assert result == 0, f'Failed to add member: {STRUC_ERROR_MEMBER_DESCRIPTIONS[result]}'
		member = self[name]
		# apply the correct tinfo
		ida_struct.set_member_tinfo(self.struc, member.member, 0, tinfo, 2) # SET_MEMTI_COMPATIBLE 2
		return member

	def add_gap(self, offset: int, size: int) -> MemberT:
		''' Create and return a new member representing a bytes gap. '''
		name = f'gap{to_hex(offset)}'
		return self.add_member(f'_BYTE {name}[{size}]', offset)

	def delete_member(self, member: MemberT) -> bool:
		''' Delete a given member. '''
		return ida_struct.del_struc_member(self.struc, member.offset)

	def add_member_auto(self, declaration: str, offset: int) -> MemberT:
		''' Create and return a new member in this struct type, automatically reworking gaps.

		`declaration` should be like `TYPE_NAME NAME[SIZE]` (Array is optional).

		A gap is recognised by:
		1. Is a single or array of 'char' or '_BYTE'.
		2. Name starting with 'gap' (all lowercase).

		If the requested offset falls within a gap of the above definition, the gap will automatically be adjusted to accomodate the new member.

		'''
		# check if offset's in a gap

		member = self.member_at_offset(offset)
		if member:
			# check if member's a gap
			# must be byte (one or array) with name starting with 'gap'
			assert member.is_gap, 'Failed to add member: offset is occupied by a non-gap member.'
			# recreate gap(s)
			gap_start = member.offset
			gap_end = member.offset + member.size
			# 1. delete existing gap
			assert self.delete_member(member), 'Failed to delete existing gap.'
			# 2. create lower gap if necessary
			if offset > gap_start:
				self.add_gap(gap_start, offset - gap_start)
			# 3. create member
			new_member = self.add_member(declaration, offset)
			# 4. create higher gap if necessary
			higher_gap_start = new_member.offset + new_member.size
			if higher_gap_start < gap_end:
				self.add_gap(higher_gap_start, gap_end - higher_gap_start)
			return new_member
		else:
			return self.add_member(declaration, offset)

	def __hash__(self) -> int:
		return self.id

	def __eq__(self, o: object) -> bool:
		return hash(self) == hash(o)


class StrucI:
	''' Represent a struct instance. '''

	def __init__(self, addr: Pointer, struc_t: StrucT):
		''' Init with a base address and a struct type. '''
		self.addr: Pointer = addr
		''' The address of this struct. '''
		self.struc_t: StrucT = struc_t
		''' The type definition of this struct. '''

	@property
	def members(self) -> Dict[str, Any]:
		''' Collect all members of this struct as a dict. '''
		members = self.struc_t.members
		return { m.name: m.instance_at_struct(self.addr) for m in members }

	def member(self, name: str, return_pointer: bool = True):
		''' Get a member's value or `Pointer` by its name. '''
		member = self.struc_t[name]
		if not member:
			return None
		return self.addr + member.offset if return_pointer else member.instance_at_struct(self.addr)

	def __getitem__(self, name: str):
		''' Get a member's value by its name. '''
		return self.member(name)

	def __setitem__(self, name: str, value) -> bool:
		''' Set a member's value by its name. '''
		member_ptr: Pointer = self.member(name)
		assert member_ptr, f'Cannot find member {name} for struct {self.struc_t}!'
		if type(value) is str:
			member_ptr.string(value)
		# treat everthing else as int
		else:
			dtype = self.struc_t[name].dtype
			assert not dtype.is_floating(), 'FP not supported yet!'
			signed = dtype.is_signed()
			length = dtype.get_size() * 8
			method = f'{"s" if signed else "u"}{length}'
			assert hasattr(member_ptr, method), f'Cannot find accessor for {method}, it is likely not supported.'
			return getattr(member_ptr, method)(value)

	def __repr__(self):
		return f'<StrucI addr={self.addr}, struc_t={self.struc_t}, data={dumps(self.members, default=str, indent=4)}>'

	def __hash__(self) -> int:
		return hash(self.struc_t) ^ self.addr

	def __eq__(self, o: object) -> bool:
		return type(o) is StrucI and self.struc_t == o.struc_t and self.addr == o.addr

def parse_declaration(declaration):
	m = re.search(r"^(\w+[ *]+)(\w+)(\[(\d+)\])?$", declaration)
	assert m, 'Member declaration should be like `TYPE_NAME NAME[SIZE]` (Array is optional)'

	type_name, field_name, _, arr_size = m.groups()
	assert not field_name[0].isdigit(), 'Bad field name'

	result = idc.parse_decl(type_name, 0)
	assert result, 'Failed to parse member type. It should be like `TYPE_NAME NAME[SIZE]` (Array is optional)'

	_, tp, fld = result
	tinfo = ida_typeinf.tinfo_t()
	tinfo.deserialize(ida_typeinf.cvar.idati, tp, fld, None)
	if arr_size:
		assert tinfo.create_array(tinfo, int(arr_size))
	return tinfo, field_name

Provides class(es) for interacting with structures (definition and instances in memory).

View Source
''' Provides class(es) for interacting with functions. '''

try:
	import idautils
	import ida_funcs
	import ida_name
	import idaapi
	import ida_hexrays
	import ida_lines
	import ida_pro
except: pass
import networkx as nx
from typing import *
from .utils import *
from .struc import *

@auto_repr(['op', 'address'], { 'id' : hex, 'address' : hex })
class CItem:
	''' Represent an item inside a graph of a decompiled function. '''

	def __init__(self, item: 'ida_hexrays.citem_t', graph: nx.DiGraph):
		''' Initialise with a `ida_hexrays.citem_t` object. '''
		self.item = item
		# python can handle retain cycles
		self.graph = graph

	@property
	def id(self) -> int:
		''' Item object id. '''
		return self.item.obj_id

	@property
	def address(self) -> int:
		''' Item address. '''
		return self.item.ea

	@property
	def op(self) -> str:
		''' Name of the operation represented by this item. '''
		return ida_hexrays.get_ctype_name(self.op_id)

	@property
	def op_id(self) -> int:
		''' The operation represented by this item. '''
		return self.item.op

	@property
	def type_name(self) -> str:
		''' The name of the type associated with this item. '''
		expr: ida_hexrays.cexpr_t = self.item.cexpr
		if self.item.is_expr() and not expr.type.empty():
			tstr = expr.type._print()
			return tstr

	@property
	def accessed_struct(self) -> StrucT:
		''' Find and return the `StrucT` accessed by this memptr/memref operation. '''
		assert self.op_id in [ida_hexrays.cot_memptr, ida_hexrays.cot_memref], 'Op is not memptr or memref.'
		if self.op_id == idaapi.cot_memptr:
			struct_tinfo = self.item.cexpr.x.type.get_pointed_object()
		elif self.op_id == idaapi.cot_memref:
			struct_tinfo = self.item.cexpr.x.type
		return StrucT.find(str(struct_tinfo))

	@property
	def accessed_struct_member(self) -> MemberT:
		''' Find and return the `MemberT` accessed by this memptr/memref operation. '''
		assert self.op_id in [ida_hexrays.cot_memptr, ida_hexrays.cot_memref], 'Op is not memptr or memref.'
		struct = self.accessed_struct
		member_offset = self.item.cexpr.m
		return struct.member_at_offset(member_offset)

	@property
	def called_function(self) -> 'Func':
		''' Find and return the `Func` called by this call operation. '''
		assert self.op_id in [ida_hexrays.cot_call], 'Op is not call!'
		return Func.func_at(self.item.x.obj_ea)

	@property
	def num_value(self) -> int:
		''' Return the value corresponding to this num operation. '''
		assert self.op_id in [ida_hexrays.cot_num], 'Op is not num!'
		return self.item.n._value

	# @property
	# def is_member_defined_in_struct(self) -> bool:
	# 	''' Checks whether there exists a member in the referenced struct type starting at the offset. '''
	# 	return self.accessed_struct.member_starting_at_offset(self.item.cexpr.m) is not None

	@property
	def label(self) -> str:
		''' Short label for item. '''
	
		def get_expr_name(expr):
			name = expr.print1(None)
			name = ida_lines.tag_remove(name)
			name = ida_pro.str2user(name)
			return name
		
		op = self.item.op
		insn = self.item.cinsn
		expr = self.item.cexpr
		parts = [ida_hexrays.get_ctype_name(op)]
		if op == ida_hexrays.cot_ptr:
			parts.append(".%d" % expr.ptrsize)
		elif op == ida_hexrays.cot_memptr:
			parts.append(".%d (m=%d)" % (expr.ptrsize, expr.m))
		elif op == ida_hexrays.cot_memref:
			parts.append(" (m=%d)" % (expr.m,))
		elif op in [
				ida_hexrays.cot_obj,
				ida_hexrays.cot_var]:
			name = get_expr_name(expr)
			parts.append(".%d %s" % (expr.refwidth, name))
		elif op in [
				ida_hexrays.cot_num,
				ida_hexrays.cot_helper,
				ida_hexrays.cot_str]:
			name = get_expr_name(expr)
			parts.append(" %s" % (name,))
		elif op == ida_hexrays.cit_goto:
			parts.append(" LABEL_%d" % insn.cgoto.label_num)
		elif op == ida_hexrays.cit_asm:
			parts.append("<asm statements; unsupported ATM>")
		if self.item.is_expr() and not expr.type.empty():
			tstr = expr.type._print()
			parts.append(tstr if tstr else "?")
		return "//".join(parts)

	@property
	def children(self) -> List['CItem']:
		''' The children of this item. '''
		return list(self.graph.successors(self))
	
	@property
	def n_children(self) -> int:
		''' The number of children of this item. '''
		return len(self.children)

	@property
	def parent(self) -> 'CItem':
		''' The parent of this item. '''
		try:
			return next(self.graph.predecessors(self))
		except:
			return None

	@property
	def parent_expression(self) -> 'CItem':
		''' Find and return the parent expression. '''
		return self.parent_where(lambda parent: parent.op == ida_hexrays.cit_expr)

	def child_where(self, condition: Callable[['CItem'], bool]) -> 'CItem':
		''' Preorder dfs search for the first child meeting the condition. '''
		dfs = nx.dfs_preorder_nodes(self.graph, self)
		node = next(dfs)
		while node:
			if condition(node):
				return node
			try:
				node = next(dfs)
			except:
				break

	def children_where(self, condition: Callable[['CItem'], bool]) -> List['CItem']:
		''' Preorder dfs search for all children meeting the condition. '''
		dfs = nx.dfs_preorder_nodes(self.graph, self)
		nodes = []
		node = next(dfs)
		while node:
			if condition(node):
				nodes.append(node)
			try:
				node = next(dfs)
			except:
				break
		return nodes

	def parent_where(self, condition: Callable[['CItem'], bool]) -> 'CItem':
		parent = self.parent
		while parent:
			if condition(parent):
				return parent
			parent = parent.parent

	def subtree_search_strict(self, query: Dict[str, Any]) -> List[List[List['CItem']]]:
		''' Perform a dfs tree search with the specified query.
		
		The query is a `dict` representing a subtree to search for, recursively defined as:
		```
		<query> = {
			"op" : <str>,
			"condition" : <lambda (CItem) -> bool>,
			"children" : [<query>, ...]
		}
		```

		`op` the name of the operation for the current item
		`condition` the matching condition for the current item
		`children` nested query(ies) specifying the children to match

		All fields are optional, but at least one of `op, condition` must be present.
		The query does not have to specify the complete subtree, but each of its specified level must be complete.
		ie. If node A has children B, C, query can either not specify A's children at all or it must specify A's children as both B and C.

		Returns results as a list of `List[List[CItem]]`, the outermost list representing the unique matches, each inner `List[List[CItem]]` representing a match.
		Example of a match:

		Query: `{ op : A, children: [{ op : B}, { op : C }]}`

		A Match: `[[A], [B, C]]`

		Returns: `[[[A], [B, C]], ...]`

		'''

		results = []

		def recursive_search(item: CItem, query: Dict[str, Any], result: List[List[CItem]] = None, index: int = 0) -> bool:
			assert 'condition' in query or 'op' in query, 'Query must have at least one of condition or op!'
			nonlocal results

			if index == 0:
				result = [[]]

			match = True
			# 1. check each of the other attributes
			attrs = { 'op' }
			for attr in attrs:
				if attr in query:
					match = match and query[attr] == getattr(item, attr)
			# 2. check condition, if exists
			if match and 'condition' in query:
				match = match and query['condition'](item)

			# always try to match deeper subtrees
			if index == 0:
				for child in item.children:
					recursive_search(child, query, None, 0)

			if match:
				# this node matches, search subtree
				subtree_match = False
				# add this item to the appropriate position
				if index == len(result):
					result.append([item])
				else:
					result[index].append(item)
				# this item matches, try to match children 'strictly'
				if item.n_children == 0 or not 'children' in query:
					# reached end of search
					# success if query has on more children
					subtree_match = not 'children' in query
				else:
					# needs to search deeper
					children = item.children
					if len(children) == len(query['children']):					
						for child_query in query['children']:
							for child_item in item.children:
								if recursive_search(child_item, child_query, result, index+1):
									children.remove(child_item)
									break

						if len(children) == 0:
							# queries 1-1 matched
							subtree_match = True
				if subtree_match and index == 0:
					results.append(result)
				return subtree_match
			
			# else this node doesn't match
			return False

		recursive_search(self, query)
		return results

	def __hash__(self):
		return self.item.obj_id

	def __eq__(self, other: 'CItem'):
		return hash(self) == hash(other)

try:
	class GraphBuilder(ida_hexrays.ctree_parentee_t):
		''' Utility class used to build the decompiled items graph. '''

		def __init__(self):
			ida_hexrays.ctree_parentee_t.__init__(self)
			self.graph = nx.DiGraph()
		
		def add_item(self, item: 'ida_hexrays.citem_t', type: str):
			parent = self.parents.back()
			item = CItem(item, self.graph)
			self.graph.add_node(item, type=type)
			if parent:
				self.graph.add_edge(CItem(parent, self.graph), item)
			return 0

		def visit_insn(self, i: 'ida_hexrays.cinsn_t'):
			return self.add_item(i, 'insn')

		def visit_expr(self, e: 'ida_hexrays.cexpr_t'):
			return self.add_item(e, 'expr')
except: pass

@auto_repr(['original'])
class CFunc:
	''' Represent a decompiled function. '''

	def __init__(self, original: 'Func', decompiled: 'ida_hexrays.cfunc_t'):
		''' Initialise with the original `Func` and the decompiled `ida_hexrays.cfunc_t` object. '''
		self.original = original
		''' The `Func` object from which this object is derived. '''
		self.decompiled = decompiled
		''' The decompiled `ida_hexrays.cfunc_t` object. '''
		# build graph
		gb = GraphBuilder()
		gb.apply_to(decompiled.body, None)
		self.body: nx.DiGraph = gb.graph
		''' The graph representing the decompiled function. '''
		self.body_root: CItem = next(nx.topological_sort(self.body))
		''' The root node of the graph for this function. '''
	
	@property
	def psuedocode(self) -> str:
		''' Psuedocode of the decompiled function. '''
		lines = self.decompiled.get_pseudocode()
		return '\n'.join([ida_lines.tag_remove(l.line) for l in lines])


@auto_repr(['name', 'start', 'end', 'size'], { 'start' : hex, 'end' : hex, 'size' : hex})
class Func:
	''' Represent a function. '''

	@staticmethod
	def find(name: str) -> 'Func':
		''' Find function by name. '''
		for addr in idautils.Functions():
			if ida_name.get_ea_name(addr) == name:
				func = idaapi.get_func(addr)
				return Func(func) if func else None

	@staticmethod
	def func_at(addr: Union[Pointer, int]) -> 'Func':
		if type(addr) is Pointer:
			addr = addr.addr
		return Func(idaapi.get_func(addr))

	def __init__(self, func: 'ida_funcs.func_t'):
		''' Initialise with a `ida_funcs.func_t` object.'''
		assert type(func) is ida_funcs.func_t, 'Invalid func!'
		self.func = func

	@property
	def name(self) -> str:
		''' Function name. '''
		return ida_name.get_ea_name(self.func.start_ea)

	@property
	def start(self) -> int:
		''' Function start address. '''
		return self.func.start_ea

	@property
	def end(self) -> int:
		''' Function end address. '''
		return self.func.end_ea

	@property
	def size(self) -> int:
		''' Function size. '''
		return self.func.size()

	@property
	def tif(self) -> 'ida_typeinf.tinfo_t':
		tif = ida_typeinf.tinfo_t()
		return tif if idaapi.get_tinfo(tif, self.func.start_ea) else None

	@property
	def arguments(self) -> Dict[str, 'ida_typeinf.tinfo_t']:
		''' Return a `dict` of argument name to type info. '''
		func_data = idaapi.func_type_data_t()
		self.tif.get_func_details(func_data)
		return { arg.name:arg.type for arg in func_data }

	def decompile(self) -> CFunc:
		''' Decompile this function. '''
		err = ida_hexrays.hexrays_failure_t()
		cfunc = ida_hexrays.decompile_func(self.func, err)
		assert cfunc, f'Decompilation failed: {err}'
		return CFunc(self, cfunc)

Provides class(es) for interacting with functions.

View Source
''' Provides class(es) for interacing with modules. '''

try:
	import idc
except: pass
from typing import *

from .utils import *

@auto_repr(['name', 'addr'], { 'addr' : hex })
class Module:
	''' Represents a single module. '''

	@staticmethod
	def all_modules() -> List['Module']:
		''' Return a list of all the modules. '''
		m = idc.get_first_module()
		modules = []
		while m:
			modules.append(m)
			m = idc.get_next_module(m)
		return modules

	@staticmethod
	def find(name: str) -> 'Module':
		modules = Module.all_modules()
		for module in modules:
			if module.name.endswith(name):
				return module

	def __init__(self, addr: int):
		self.name = idc.get_module_name(addr)
		self.addr = addr

Provides class(es) for interacing with modules.

View Source
''' Provides additional helper functions. '''

try:
	import ida_frame
	import ida_struct
	import idaapi
	import idc
except: pass

from .memory import Pointer

def find_local_var(var_name: str) -> Pointer:
	''' Find a local variable by name when paused in a function frame. '''
	frame = ida_frame.get_frame(idc.here())
	loc_var = ida_struct.get_member_by_name(frame, var_name)
	if (loc_var is None):
		return None
	stack_ptr = idc.GetRegValue('rsp' if idaapi.get_inf_structure().is_64bit() else 'esp')
	ea = loc_var.soff + stack_ptr
	return Pointer(ea)

def find_symbol(name: str) -> Pointer:
	''' Find a global symbol by name. '''
	addr = idc.get_name_ea_simple(name)
	if addr == idc.BADADDR:
		return None
	return Pointer(addr)

Provides additional helper functions.