├── .gitignore ├── README.md ├── examples ├── callui_hook.py ├── find_hexrays_plugin.py ├── qstring_assign.py └── tinfo_t_assign.py ├── setup.py └── src └── ida_kern ├── __init__.py ├── core.py ├── definitions ├── __init__.py ├── _base_defmod.py ├── _sdkhdr │ ├── NOTE.md │ ├── idawin77_base.py │ ├── idawin77_hexrays.py │ ├── idawin77_libtypes.py │ └── idawin77_sdk.py └── sdkhdr.py ├── exceptions.py └── utils ├── __init__.py ├── ctypes_utils.py ├── ida_utils.py └── platform_helper.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | 3 | __pycache__ 4 | *.log 5 | *.egg-info -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IDAKern 2 | 3 | An IDAPython wrapper for IDA Pro's kernel dll. 4 | 5 | ## Why? 6 | 7 | Many useful and low level API aren't exposed by IDAPython's SWIG wrapper. 8 | 9 | Although those API are documented and exported in SDK, IDAPython still cannot easily use them due to lack of structure. 10 | 11 | ## Usage 12 | 13 | - Install (You can also do `import sys; sys.path.append(r'C:\ida_kern\src')` if you don't want to install) 14 | ``` 15 | pip install git+https://github.com/NyaMisty/ida_kern 16 | ``` 17 | 18 | - Profit! 19 | ``` 20 | import ida_kern 21 | k = ida_kern.IDAKern() 22 | print(k.idadir(None)) 23 | ``` 24 | 25 | ~Loading takes 3-5 seconds currently, be patient ;)~ 26 | 27 | Now only takes 0.5 seconds to load! 28 | 29 | Examples can be found in `examples/` folder: 30 | - hook_to_notification_point example usage (HT_UI hook as example) 31 | - qstring manipulation 32 | - tinfo_t manipulation 33 | - (more!) 34 | 35 | ## How does it work? 36 | 37 | With these tools we can now generate raw ctypes API binding for IDA SDK: 38 | - IDAClang: can export all types & symbols in headers to til 39 | - Tilib (in SDK): export types in til to C header 40 | - My Tool: sanitize tilib's output & generate compilable source code 41 | - ctypeslib: generate bindings from C/simple C++ source file (need to use my fork to fix some bugs) 42 | -------------------------------------------------------------------------------- /examples/callui_hook.py: -------------------------------------------------------------------------------- 1 | # adapted from https://gist.github.com/williballenthin/b5e7a80691ed5e44e7fea1964bae18dc 2 | 3 | import idc 4 | import idaapi 5 | import idautils 6 | 7 | from ctypes import * 8 | 9 | import ida_kern 10 | k = ida_kern.IDAKern() 11 | k.init() 12 | 13 | HookCb = k.hook_to_notification_point.argtypes[1] 14 | 15 | def do_callui(uicode, *args): 16 | ret = k.callui(uicode, *[c_void_p(c) for c in args]) 17 | return ret 18 | 19 | class CalluiHookPlugin(idaapi.plugin_t): 20 | flags = idaapi.PLUGIN_KEEP 21 | comment = "Callui Hook Example" 22 | 23 | help = "Callui Hook Example" 24 | wanted_name = "CalluiHookExample" 25 | 26 | def init(self): 27 | import datetime 28 | self.bypassing = False 29 | #self.logfile = open('ui_notify.log', 'w') 30 | 31 | # can't use a bound method as a callback (since `self` doesn't get provided) 32 | # so we'll create a closure that has access to `self`. 33 | # 34 | # via: http://stackoverflow.com/a/7261524/87207 35 | def cb(user_data, notification_code, va_list): 36 | if self.bypassing: 37 | return 0 38 | if notification_code == k.ui_ask_str: 39 | print('ui_ask_str:') 40 | print('>.. notification code: %s' % (notification_code)) 41 | print('>.. va_list: %s' % (va_list)) 42 | print('>.. args: %s' % (va_list[:3],)) 43 | prompt = cast(va_list[2], c_char_p) 44 | print('>.. prompt: %s' % prompt.value) 45 | if prompt.value == b'Please enter the type declaration': 46 | self.bypassing = True 47 | callui_ret = do_callui(notification_code, *va_list[:4]).cnd 48 | self.bypassing = False 49 | val = cast(va_list[0], POINTER(k.qstring))[0] 50 | if val.body.array: 51 | print("userInput: %s" % val) 52 | if callui_ret[0]: 53 | return 1 54 | return 0 # there's no way to tell IDA that user has cancelled in this event, that's IDA's fault 55 | elif notification_code == k.ui_ask_form: 56 | print('ui_ask_form:') 57 | print('>.. notification code: %s' % (notification_code)) 58 | print('>.. va_list: %s' % (va_list)) 59 | print('>.. args: %s' % (va_list[:2],)) 60 | form = cast(va_list[0], c_char_p) 61 | print('>.. form: %s' % form.value) 62 | if form.value == b'@0:0[]\nPlease enter a string\n\n \n\n': 63 | self.bypassing = True 64 | callui_ret = do_callui(notification_code, *va_list[:2]).cnd 65 | self.bypassing = False 66 | print(callui_ret[0]) 67 | if callui_ret[0] != 1: 68 | # but we can tell IDA user's cancellation here with value other than 0 and 1 69 | return 2 if not callui_ret[0] else callui_ret[0] 70 | # va_list: callui's argument list, supplied by hook_to_notification_point 71 | ask_form_va = cast(va_list[1], POINTER(c_void_p)) 72 | # ask_form_va: ask_form's argument list, supplied by vask_form 73 | qstr_out = cast(ask_form_va[0], POINTER(k.qstring))[0] 74 | val = qstr_out.body.array 75 | if val: 76 | print("userInput: %s" % val) 77 | return 1 78 | elif notification_code == k.ui_mbox: 79 | print('ui_mbox:') 80 | print('>.. notification code: %s' % (notification_code)) 81 | print('>.. va_list: %s' % (va_list)) 82 | print('>.. args: %s' % (va_list[:3],)) 83 | prompt = cast(va_list[1], c_char_p) 84 | print('>.. prompt: %s' % prompt.value) 85 | 86 | return 0 87 | 88 | 89 | # need to keep a ref around, or the function gets garbage collected 90 | self.cb = HookCb(cb) 91 | 92 | # need to keep a ref around, or the param gets garbage collected 93 | self.ctx = c_long(69) 94 | 95 | return idaapi.PLUGIN_OK 96 | 97 | def run(self, arg): 98 | print('hints: run') 99 | k.hook_to_notification_point(k.HT_UI, self.cb, byref(self.ctx)) 100 | 101 | def term(self): 102 | print('hints: term') 103 | k.unhook_from_notification_point(k.HT_UI, self.cb, byref(self.ctx)) 104 | 105 | try: 106 | plug.term() 107 | except: 108 | pass 109 | plug = CalluiHookPlugin() 110 | plug.init() 111 | plug.run(0) -------------------------------------------------------------------------------- /examples/find_hexrays_plugin.py: -------------------------------------------------------------------------------- 1 | import ida_kern 2 | k = ida_kern.IDAKern() 3 | k.init() 4 | 5 | # Find a plugin by its name, using Hex-Rays as example 6 | def find_hexrays(): 7 | plugs = k.get_plugins() 8 | while plugs: 9 | plug = plugs[0] 10 | if b'Hex-Rays Decompiler' == plug.name: 11 | return { 12 | "path": plug.path, 13 | "name": plug.name, 14 | "comment": plug.comment, 15 | "flags": plug.flags, 16 | } 17 | plugs = plugs[0].next 18 | 19 | hx = find_hexrays() -------------------------------------------------------------------------------- /examples/qstring_assign.py: -------------------------------------------------------------------------------- 1 | import ida_kern 2 | k = ida_kern.IDAKern() 3 | k.init() 4 | 5 | # IDA's qstring::assign is inline function, so we have to manually implement it in ctypes 6 | def qstr_assign(qstr, s): 7 | arr_var = qstr.body.get_cfield('array') 8 | buflen = len(s) + 1 9 | if buflen > qstr.body.alloc: 10 | arr_var.value = k.qvector_reserve(byref(qstr.body), arr_var, buflen, 1) 11 | qstr.body.n = buflen 12 | ctypes.memmove(arr_var, s + b'\x00', len(s) + 1) 13 | 14 | 15 | ret = qstring() 16 | qstr_assign(ret, b'test!!!') 17 | 18 | print("qstring struct: " % (ret.body.n, ret.body.alloc, ret.body.array)) -------------------------------------------------------------------------------- /examples/tinfo_t_assign.py: -------------------------------------------------------------------------------- 1 | import ida_kern 2 | k = ida_kern.IDAKern() 3 | k.init() 4 | 5 | # This function assign IDAPython's SWIG tinfo_t wrapper into CPP SDK's raw "tinfo_t*" 6 | # tinfo_t is only a simple wrapper around raw typid, like netnode, so we can copy them simply 7 | # But be careful to use the typid as it's having reference count in the IDA kernel internally. 8 | def tinfo_t_assign(c_tif, swig_tif): 9 | t = swig_tif.copy() 10 | c_newtif = cast(c_void_p(int(t.this)), POINTER(k.struct_tinfo_t)) 11 | temp = c_tif[0].typid 12 | c_tif[0].typid = c_newtif[0].typid 13 | c_newtif[0].typid = temp 14 | 15 | swig_t = idaapi.tinfo_t() 16 | assert idaapi.parse_decl(swig_t, None, "void **test;", 0) is not None 17 | c_t = k.struct_tinfo_t() 18 | 19 | print('before: [tinfo-c]: %s' % k.dstr_tinfo(byref(c_t))) 20 | print('before: [tinfo-swig]: %s' % swig_t.dstr()) 21 | 22 | tinfo_t_assign(pointer(c_t), swig_t) 23 | 24 | 25 | print('after: [tinfo-c]: %s' % k.dstr_tinfo(byref(c_t))) 26 | print('after: [tinfo-swig]: %s' % swig_t.dstr()) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='ida_kern', 5 | version='1.0.0', 6 | description='Raw IDA Kernel API for IDAPython', 7 | author='NyaMisty', 8 | author_email='misty@misty.moe', 9 | packages=find_packages('src'), 10 | package_dir={'': 'src'}, 11 | install_requires=[], 12 | ) -------------------------------------------------------------------------------- /src/ida_kern/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import IDAKern, IDAInfo 2 | from .exceptions import * -------------------------------------------------------------------------------- /src/ida_kern/core.py: -------------------------------------------------------------------------------- 1 | from .utils.ida_utils import find_hexrays 2 | from .utils.platform_helper import * 3 | from .definitions import get_registered_mods 4 | from .utils.ctypes_utils import hook_ctypes, unhook_ctypes 5 | 6 | 7 | class IDAKern(): 8 | def __init__(self, idainfo=None): 9 | self.loaded_mods = [] 10 | self.dlls = {} 11 | self.defs = {} 12 | if idainfo is None: 13 | self.idainfo = None 14 | else: 15 | self.idainfo = idainfo 16 | 17 | def add_dll(self, dllname, dll): 18 | self.dlls[dllname] = dll 19 | 20 | def _on_defmod_loaded(self): 21 | """ 22 | callback for receiving definition module loading event, to support loading modules that depends on other mod 23 | :return: True if updated something, so a new round of loading is needed 24 | """ 25 | try: 26 | if not 'hexrays' in self.dlls: 27 | self.dlls['hexrays'] = find_hexrays(self) 28 | return True 29 | except: 30 | pass 31 | return False 32 | 33 | def load_mods(self): 34 | """ 35 | Load all modules with dependency in mind 36 | :return: None 37 | """ 38 | hook_ctypes() 39 | try: 40 | while True: 41 | new_loaded = False 42 | for mod in get_registered_mods(): 43 | if mod in self.loaded_mods: 44 | continue 45 | if all(c in self.dlls for c in mod.dll_needed()): 46 | newdef = mod.load_definition(self.dlls, self.idainfo) 47 | self.defs.update(newdef) 48 | self.__dict__.update(newdef) 49 | self.loaded_mods.append(mod) 50 | new_loaded = True 51 | 52 | if not self._on_defmod_loaded() and not new_loaded: 53 | break 54 | finally: 55 | unhook_ctypes() 56 | 57 | def init(self): 58 | """ 59 | Initialize IDA information & DLL info, then load modules 60 | :return: 61 | """ 62 | if self.idainfo is None: 63 | self.idainfo = IDAInfo() 64 | self.add_dll('kernel', get_ida_kerndll()) 65 | self.load_mods() 66 | 67 | #def __dir__(self): 68 | # return list(self.__dict__) + list(self.defs) 69 | 70 | #def __getattr__(self, attr): 71 | # if attr in self.defs: 72 | # return self.defs[attr] 73 | # return super().__getattribute__(attr) 74 | 75 | __all__ = ['IDAKern', 'IDAInfo'] -------------------------------------------------------------------------------- /src/ida_kern/definitions/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["get_registered_mods"] 2 | 3 | import importlib 4 | import os 5 | from os.path import dirname, join 6 | 7 | from typing import * 8 | from ._base_defmod import BaseDefinitionMod 9 | 10 | registered_mods: List[BaseDefinitionMod] = [] 11 | 12 | path, dirs, files = next(os.walk(join(dirname(__file__)))) 13 | module_files = [f for f in files if f.endswith('.py') and not f.startswith('_')] 14 | for modname in [ f[:-3] for f in module_files ]: 15 | mod = importlib.import_module('.' + modname, __name__) 16 | #for c in dir(mod): 17 | # registered_mods.append(getattr(mod, c)) 18 | registered_mods.append(mod.DEFMOD) 19 | 20 | def get_registered_mods(): 21 | return registered_mods 22 | 23 | # def load_definition(dlls, platform, ea64): 24 | # defs = {} 25 | # for mod in registered_mods: 26 | # if all(c in dlls for c in mod.__DLL__): 27 | # defs.update(mod.load_definition(dlls, platform, ea64)) 28 | # return defs 29 | -------------------------------------------------------------------------------- /src/ida_kern/definitions/_base_defmod.py: -------------------------------------------------------------------------------- 1 | __all__ = ['BaseDefinitionMod'] 2 | from typing import * 3 | from abc import ABCMeta, abstractmethod 4 | 5 | from ida_kern.utils.platform_helper import IDAInfo 6 | 7 | 8 | class BaseDefinitionMod(metaclass=ABCMeta): 9 | @abstractmethod 10 | def dll_needed(self) -> List[str]: 11 | pass 12 | 13 | @abstractmethod 14 | def load_definition(self, dlls: Dict[str, Any], idainfo: IDAInfo) -> Dict[str, Any]: 15 | pass -------------------------------------------------------------------------------- /src/ida_kern/definitions/_sdkhdr/NOTE.md: -------------------------------------------------------------------------------- 1 | # IDA SDK Headers 2 | 3 | Python bindings here are all generated by ctypeslib 4 | 5 | The generation script can be found in: https://github.com/NyaMisty/ida_kern_til 6 | -------------------------------------------------------------------------------- /src/ida_kern/definitions/sdkhdr.py: -------------------------------------------------------------------------------- 1 | __all__ = ['DEFMOD'] 2 | 3 | import importlib 4 | 5 | from ._base_defmod import BaseDefinitionMod 6 | from ..exceptions import * 7 | 8 | import ctypes 9 | import _ctypes 10 | class CtypesDllStub(): 11 | def __init__(self, dll): 12 | self.dll = dll 13 | def __getattr__(self, func): 14 | parentFun = getattr(self.dll, func, None) 15 | if not parentFun: 16 | return ctypes.CFUNCTYPE(None)(0) 17 | return parentFun 18 | 19 | 20 | class IDASDKHeaderDef(BaseDefinitionMod): 21 | def dll_needed(self): 22 | return ["kernel"] 23 | 24 | def load_definition(self, dlls, idainfo): 25 | dll = dlls['kernel'] 26 | PLATFORM_MAP = { 27 | 'win': 'win', 28 | 'linux': 'linux', 29 | 'mac': 'mac', 30 | 'armmac': 'armmac', 31 | } 32 | platType = PLATFORM_MAP.get(idainfo.platform) 33 | if platType is None: 34 | raise UnknownArchitecture('Unsupported platform %s' % idainfo.platform) 35 | VERSION_MAP = [ 36 | (770, '77'), 37 | ] 38 | verName = None 39 | for v, vname in VERSION_MAP: 40 | if idainfo.idaver >= v: 41 | verName = vname 42 | if platType is None: 43 | raise UnknownArchitecture('Unsupported IDA version %s' % idainfo.idaver) 44 | 45 | hdr_prefix = f'ida{platType}{verName}_' 46 | 47 | get_sdkhdr = lambda x: importlib.import_module('._sdkhdr.' + hdr_prefix + x, package=__package__) 48 | 49 | retDef = {} 50 | 51 | for modType in ['sdk', 'libtypes', 'hexrays']: 52 | mod = get_sdkhdr(modType) 53 | try: 54 | mod._libraries['FIXME_STUB'] = CtypesDllStub(dll) 55 | except AttributeError: 56 | pass 57 | defs = mod.ctypeslib_define() 58 | 59 | newdefs = {} 60 | for k, v in defs.items(): 61 | incl = True 62 | if isinstance(v, _ctypes.CFuncPtr): 63 | if not v: 64 | incl = False 65 | if incl: 66 | newdefs[k] = v 67 | retDef.update(newdefs) 68 | 69 | return retDef 70 | 71 | DEFMOD = IDASDKHeaderDef() -------------------------------------------------------------------------------- /src/ida_kern/exceptions.py: -------------------------------------------------------------------------------- 1 | class IDAKernException(Exception): 2 | pass 3 | 4 | class IDAKernCtypesException(IDAKernException): 5 | pass 6 | 7 | class UnknownArchitecture(IDAKernCtypesException): 8 | pass -------------------------------------------------------------------------------- /src/ida_kern/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NyaMisty/ida_kern/04027a35bea7735faf4ba9fd90d5860ba14580f7/src/ida_kern/utils/__init__.py -------------------------------------------------------------------------------- /src/ida_kern/utils/ctypes_utils.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from ctypes import CFUNCTYPE as ctypes_CFUNCTYPE 3 | 4 | def CFUNCTYPE(*args, **kwargs): 5 | ret = ctypes_CFUNCTYPE(*args, **kwargs) 6 | def CFUNCTYPE_wrap(arg0, *args, **kwargs): 7 | if arg0 is None or arg0 == 0: 8 | return ret(0) 9 | else: 10 | return ret.__class__.from_param(ret, arg0, *args, **kwargs) 11 | ret.from_param = CFUNCTYPE_wrap 12 | #ret.from_param = lambda *args, **kwargs: ret(0) if not args[0:1] else ret.__class__.from_param(ret, *args, **kwargs) 13 | return ret 14 | 15 | def hook_ctypes(): 16 | ctypes.CFUNCTYPE = CFUNCTYPE 17 | 18 | def unhook_ctypes(): 19 | ctypes.CFUNCTYPE = ctypes_CFUNCTYPE -------------------------------------------------------------------------------- /src/ida_kern/utils/ida_utils.py: -------------------------------------------------------------------------------- 1 | from typing import * 2 | if False: 3 | # noqa 4 | from ida_kern import IDAKern 5 | 6 | def find_hexrays(k: 'IDAKern') -> Optional[str]: 7 | plugs = k.get_plugins() 8 | while plugs: 9 | plug = plugs[0] 10 | if b'Hex-Rays Decompiler' == plug.name: 11 | return plug.path 12 | plugs = plugs[0].next 13 | 14 | return None 15 | -------------------------------------------------------------------------------- /src/ida_kern/utils/platform_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ctypes 4 | from ..exceptions import * 5 | 6 | def is_ea64(): 7 | import idaapi 8 | ea64 = False 9 | if idaapi.BADADDR == 2**64 - 1: 10 | ea64 = True 11 | return ea64 12 | 13 | def get_platform_type(): 14 | if sys.platform == 'win32': 15 | return 'win' 16 | elif sys.platform == 'linux': 17 | return 'linux' 18 | elif sys.platform == 'darwin': 19 | if os.uname().machine == 'arm64': 20 | return 'armmac' 21 | else: 22 | return 'mac' 23 | else: 24 | raise UnknownArchitecture('unsupported os: %s' % sys.platform) 25 | 26 | def get_idaver(): 27 | import idaapi 28 | return idaapi.IDA_SDK_VERSION 29 | 30 | class IDAInfo(): 31 | def __init__(self, platform=None, ea64=None, idaver=None): 32 | self.platform = platform 33 | self.ea64 = ea64 34 | self.idaver = idaver 35 | if self.platform is None: 36 | self.platform = get_platform_type() 37 | if self.ea64 is None: 38 | self.ea64 = is_ea64() 39 | if self.idaver is None: 40 | self.idaver = get_idaver() 41 | 42 | def get_ida_kerndll(): 43 | ea64 = is_ea64() 44 | if sys.platform == 'win32': 45 | dll = ctypes.WinDLL 46 | else: 47 | dll = ctypes.CDLL 48 | if sys.platform == 'win32': 49 | dllname = ['ida64.dll', 'ida.dll'] 50 | elif sys.platform == 'linux': 51 | dllname = ['libida64.so', 'libida.so'] 52 | elif sys.platform == 'darwin': 53 | dllname = ['libida64.dylib', 'libida.dylib'] 54 | else: 55 | raise UnknownArchitecture('unsupported os: %s' % sys.platform) 56 | 57 | return dll(dllname[0] if ea64 else dllname[1]) 58 | 59 | __all__ = ['IDAInfo', 'get_ida_kerndll'] --------------------------------------------------------------------------------