一直想分享些 idapython 的內容,網上現在的內容太少,都只是教了一些基礎的用法,官方的文檔和例子又比較散。希望我的分享能讓大家更加快樂的逆向!
這是這個系列的第一篇,分享一下如何用IDAPython 畫出兩個函數之間的交叉引用圖,可視化一直是我很喜歡的一個東西,逆向的目的就是理解代碼嘛,可視化是理解代碼的一個很重要的工具。
開發環境:
- Python3.8
- IDA Pro 7.7+
此外,本教程的實現還依賴了幾個 Python 包:
- sark
- networkx
- matplotlib
本教程安排如下:
① 定義遞歸函數 find_cross_refs
② IDAPython Action 概念介紹
③ 添加我們的 Action
④ 最終效果
一、定義遞歸函數 find_cross_refs
在這里,我們定義了一個名為 find_cross_refs 的遞歸函數,用于查找兩個函數之間的交叉引用關系。該函數包含以下參數:
- func: sark.Function 類型,表示要查找交叉引用關系的起始函數對象。
- target_func: sark.Function 類型,表示要查找交叉引用關系的目標函數對象。
- G: networkx.DiGraph 類型,表示用于存儲函數之間引用關系的有向圖對象。
- max_depth: int 類型,表示查找引用關系的最大深度。
- include_data_xref: bool 類型,表示在查找引用關系時是否要包含數據引用關系。
函數中主要分為以下幾個步驟:
① 首先判斷查找引用關系的深度是否達到最大深度,如果達到最大深度就直接返回 False。
② 然后根據 include_data_xref 的設置,獲取該函數中所有的引用 refes。如果 include_data_xref 為 True,則獲取所有數據引用關系,否則獲取所有函數調用引用關系。
③ 遍歷函數的所有引用 ref,如果該引用 ref 指向目標函數,則在有向圖 G 中通過 add_edge 函數添加一條從當前函數到目標函數的邊,并返回 True。
④ 如果引用指向另一個函數,則遞歸調用 find_cross_refs 函數查找兩個函數之間的交叉引用關系。
⑤ 如果所有引用遍歷完,仍然沒有找到交叉引用,則返回 False。
下面是 find_cross_refs 函數的詳細實現代碼:
MAX_SEARCH_DEPTH = 10# 定義遞歸函數,用于查找兩個函數之間的交叉引用關系def find_cross_refs(func: sark.Function, target_func, G, max_depth, include_data_xref): if max_depth == 0: return False max_depth -= 1 # 獲取函數的所有引用 if include_data_xref: refs = list(func.xrefs_from) else: refs = list(func.calls_from) # 遍歷函數的每一個引用 for ref in refs: # 獲取引用的地址 ref_addr = ref.to # type: ignore # 判斷引用是否指向目標函數 if ref_addr == target_func.start_ea: # 如果指向目標函數,則找到了交叉引用關系 # 在圖中添加一條邊表示函數之間的引用關系 # G.add_node(func, name=func.demangled) # G.add_node(target_func, name=target_func.demangled) G.add_edge(func.ea, target_func.ea) return True # 如果引用指向另一個函數,則遞歸調用find_cross_refs函數,查找兩個函數之間的交叉引用關系 seg = sark.Segment(ea=ref_addr) if seg.name != "__text": #是 external Function continue else: try: ref_func = sark.Function(ea=ref_addr) except Exception as e: print(e) continue if find_cross_refs(ref_func, target_func, G, max_depth, include_data_xref): # 如果找到了交叉引用關系,則在圖中添加一條邊表示函數之間的引用關系 # G.add_node(func, name=func.demangled) # G.add_node(ref_func, name=ref_func.demangled) G.add_edge(func.ea, ref_func.ea) return True # 如果遍歷完所有引用后仍然沒有找到交叉引用關系,則返回False return False
二、IDApython Action 概念介紹
第二步,我們要向 IDA 中添加 action,通過 action 來執行我們的 find_cross_refs函數,首先我們了解一下 IDA 中 action 的概念:
1、action 首先需要被注冊。一旦被注冊,action 可以用快捷鍵觸發(如果指定了的話),但是 action 還沒法在 UI 中看到。
2、action 被注冊之后,我們可以將 action attach 到以下三個地方:
① 主菜單
② Toolbar
③ 上下文菜單
3、同一個 action 可以在不同的地方被使用。
4、一個 action 有一個 handler,是一個結構體帶有以下兩個 callback。
① activate callback,當 action 觸發時調用。
② update callback,聲明 action 是否 enabled。
給一段代碼,看一下如何注冊一個 Action。
# 1) Create the handler class class MyHandler(idaapi.action_handler_t): def __init__(self): idaapi.action_handler_t.__init__(self) # Say hello when invoked. def activate(self, ctx): print "Hello!" return 1 # This action is always available. def update(self, ctx): return idaapi.AST_ENABLE_ALWAYS # 2) Describe the action action_desc = idaapi.action_desc_t( 'my:action', # The action name. This acts like an ID and must be unique 'Say hello!', # The action text. MyHandler(), # The action handler. 'Ctrl+H', # Optional: the action shortcut 'Says hello', # Optional: the action tooltip (available in menus/toolbar) 199) # Optional: the action icon (shows when in menus/toolbars) # 3) Register the action idaapi.register_action(action_desc)
然后是將 action 放到 UI 上的代碼。
# 將 action 放到菜單中idaapi.attach_action_to_menu( 'Edit/Other/Manual instruction...', # The relative path of where to add the action 'my:action', # The action ID (see above) idaapi.SETMENU_APP) # We want to append the action after the 'Manual instruction...'# 將 action 放到 toolbar 中idaapi.attach_action_to_toolbar( "AnalysisToolBar", # The toolbar name 'my:action') # The action ID # 將 action 永久的放到某個 widget 的上下文菜單中# Create a widget, or retrieve a pointer to it. form = idaapi.get_current_tform() idaapi.attach_action_to_popup(form, None, "my:action", None)
了解完這些我們就可以寫自己的 Action 了,為了方便我對官方的 Action api 做了一些封裝, 下面是我自己的 action.py 文件。
import idaapi from .hookers import global_hooker_managerfrom typing import Optional class ActionManager(object): def __init__(self): self.__actions = [] def register(self, action): self.__actions.append(action) idaapi.register_action( idaapi.action_desc_t(action.name, action.description, action, action.hotkey) ) if isinstance(action, HexRaysPopupAction): global_hooker_manager.register(HexRaysPopupRequestHandler(action)) def initialize(self): pass def finalize(self): for action in self.__actions: idaapi.unregister_action(action.name) action_manager = ActionManager() class Action(idaapi.action_handler_t): """ Convenience wrapper with name property allowing to be registered in IDA using ActionManager """ description: Optional[str] = None hotkey: Optional[str] = None def __init__(self): super(Action, self).__init__() @property def name(self): return "HexRaysPyTools:" + type(self).__name__ def activate(self, ctx: idaapi.action_ctx_base_t): raise NotImplementedError def update(self, ctx: idaapi.action_ctx_base_t): # return idaapi.AST_XXX # 通常會根據 ctx.widget_type == idaapi.BWN_XXX 來判斷當前是在哪個窗口 raise NotImplementedError class HexRaysPopupAction(Action): """ Wrapper around Action. Represents Action which can be added to menu after right-clicking in Decompile window. Has `check` method that should tell whether Action should be added to popup menu when different items are right-clicked. Children of this class can also be fired by hot-key without right-clicking if one provided in `hotkey` static member. """ def __init__(self): super(HexRaysPopupAction, self).__init__() def activate(self, ctx: idaapi.action_ctx_base_t): raise NotImplementedError def check(self, hx_view): # type: (idaapi.vdui_t) -> bool raise NotImplementedError def update(self, ctx: idaapi.action_ctx_base_t): if ctx.widget_type == idaapi.BWN_PSEUDOCODE: return idaapi.AST_ENABLE_FOR_WIDGET return idaapi.AST_DISABLE_FOR_WIDGET class HexRaysPopupRequestHandler(idaapi.Hexrays_Hooks): def __init__(self, action): super().__init__() self.__action = action def populating_popup(self, widget, popup_handle, vu): if self.__action.check(vu): idaapi.attach_action_to_popup(widget, popup_handle, self.__action.name, None) return 0
三、添加我們的 Action
然后我們開始定義顯示交叉引用圖的 Action,我把它叫做 XrefRoadMapAction。
具體來說,該類包含以下方法:
- check 方法:返回 True 表示該操作可用。
- activate 方法:執行實際的操作,該方法獲取當前函數地址,獲取用戶輸入的目標函數地址,并創建一個有向圖進行處理。
- update 方法:更新當前操作的 UI(例如菜單項)的圖形狀態。
可以看出,該類實現的核心是 activate 方法。其主要流程如下:
- 獲取當前光標所在的函數對象;
- 獲取用戶輸入的目標函數地址;
- 創建空的有向圖對象 G;
- 根據用戶設置調用 find_cross_refs 函數搜索從當前函數到目標函數的交叉引用路徑;
- 如果在搜索結果中找到了交叉引用路徑,則在 G 中添加表示函數之間引用關系的邊,然后使用 sark.ui.NXGraph 繪制有向圖;
- 如果沒有找到路徑,則顯示一個警告信息。
下面是 XrefRoadMapAction 類的詳細實現代碼:
class XrefRoadMapAction(HexRaysPopupAction): """ Convenience wrapper with name property allowing to be registered in IDA using ActionManager """ description: Optional[str] = "Find Xref Road map between A and B" hotkey: Optional[str] = "Alt-F3" def __init__(self): super(Action, self).__init__() def check(self, hx_view: idaapi.vdui_t) -> bool: return True def activate(self, ctx: idaapi.action_ctx_base_t): # 獲取光標所在的函數地址 cur_ea = idaapi.get_screen_ea() try: cur_func = sark.Function(ea=cur_ea) except Exception as e: traceback.print_exc() # 如果光標不在函數中,則提示錯誤 print("Error: cursor is not in a function!") return # 獲取用戶輸入的目標函數的地址 target_func_addr = idaapi.ask_addr(defval=0, format="Enter target function address:") if target_func_addr == idaapi.BADADDR or target_func_addr is None: # 如果用戶輸入的是無效的地址,則提示錯誤 print("Error: invalid target function address!") return # 獲取目標函數的函數對象 try: s = sark.Segment(ea=target_func_addr) if s.name == "UNDEF": target_func = sark.ExternFunction(ea=target_func_addr) else: target_func = sark.Function(ea=target_func_addr) # type: ignore except Exception as e: # 如果目標函數不存在,則提示錯誤 traceback.print_exc() print("Error: target function does not exist!") return import networkx as nx # 創建有向圖 G = nx.DiGraph() # 在這里實現查找兩個函數之間交叉引用關系的代碼... print(f"find a path between func {cur_func.demangled} and {target_func.demangled}, max search depth is {MAX_SEARCH_DEPTH}") btn_selected = idaapi.ask_yn(idaapi.ASKBTN_NO, "是否包括 data xrefs?") if btn_selected == idaapi.ASKBTN_CANCEL: return ret = find_cross_refs(cur_func, target_func, G, MAX_SEARCH_DEPTH, include_data_xref=True if btn_selected == idaapi.ASKBTN_YES else False) if ret == False: assert len(G.nodes()) == 0, "find_cross_refs 返回 False,但是 G 中有節點" if len(G.nodes()) > 0: # 繪制圖 G.nodes[cur_func.ea][sark.ui.NXGraph.BG_COLOR] = 0x80 title = f"Path from `{cur_func.demangled}` to `{target_func.demangled}`" # Create an NXGraph viewer viewer = sark.ui.NXGraph(G, handler=sark.ui.AddressNodeHandler(), title=title) # Show the graph viewer.Show() else: idaapi.warning("Cannot find a path!!!") def update(self, ctx: idaapi.action_ctx_base_t): # 獲取當前窗口的類型 widget_type = ctx.widget_type # 如果當前窗口是反匯編或者反編譯窗口,則菜單項可用 if widget_type == idaapi.BWN_DISASM or widget_type == idaapi.BWN_PSEUDOCODE: return idaapi.AST_ENABLE_FOR_WIDGET else: return idaapi.AST_DISABLE_FOR_WIDGET
代碼里面其實還用了很多 sark 庫的函數,大家可以自己學習其用法,這個庫是專門對 idapython 的 api 做封裝的,比較 pythonic,非常好用,我自己也給這個庫添加了很多新的功能。
四、最終效果
如下所示:



雷石安全實驗室
綠盟科技研究通訊
娜璋AI安全之家
看雪學苑
一顆小胡椒
HACK學習呀
系統安全運維
FreeBuf
0x00實驗室
安全行者老霍
看雪學苑
看雪學苑