IDA&Frida 您所在的位置:网站首页 frida插件lua IDA&Frida

IDA&Frida

2023-07-21 19:25| 来源: 网络整理| 查看: 265

IDA&Frida 前言

偶然间发现了一本秘籍《IDA脚本开发之旅》,这是白龙的系列文章,主要是安卓平台,笔者只是根据他的知识点学习,拓展,可以会稍微提及别的平台。本文并不会贴出他的思路分析,只对于源码进行学习,有兴趣可以加他星球一起学习!

另外本文也学习了无名侠的IDAFrida脚本,有兴趣可以加他星球一起学习!

脚本学习

IDA 静态分析 + Frida Hook,可以让分析变得更加丝滑。

这里我们会学习两个脚本 一个是白龙的 一个是 无名侠的。

ShowFridaCode

这是白龙写的一个Python脚本,用于生成Frida的Hook代码。主要包含了以下几个部分:

导入模块Frida Hook函数模板Frida Inline Hook函数模板打印参数的函数生成函数Hook代码的函数生成Inline Hook代码的函数根据地址生成Hook代码或Inline Hook代码的函数生成初始化代码的函数生成Dump内存代码的函数IDA View_Hooks类,用于处理在IDA视图中双击和单击事件插件类,实现插件的初始化、运行和退出 前置知识

frida ida就不说了,主要说一下 其他的知识

android_dlopen_ext 是 Android 系统中的一个函数,用于在运行时动态加载共享库。与标准的 dlopen() 函数相比,android_dlopen_ext 提供了更多的参数选项和扩展功能,例如支持命名空间、符号版本等特性。该函数通常用于 Android 应用程序或系统进程中,用于加载共享库并获取其中定义的符号。在 Android 应用程序中,常常会使用 System.loadLibrary() 函数调用 android_dlopen_ext(),从而加载与应用程序打包在一起的共享库文件。

在win上我们一般用 LoadLibrary 函数来加载 DLL(动态链接库)文件,并使用 GetProcAddress 函数来获取函数指针。在 iOS 平台上,可以使用 dlopen 函数来加载共享库,并使用 dlsym 函数来获取函数指针。

linker的调用流程:

在do_dlopen中通过find_library进行加载so在加载完so后通过call_constructors完成init_array的加载find_library最后调用load_libray完成so的转载最后通过load_library的elf_reader.load完成so的装载

了解了上面两个知识点,在Hook Native时,我们常常通过Spawn抢占两个较早的时机,1是 JNIOnLoad 前 2是 init_proc 以及 init_array 前 ,就可以通过hook上述函数。

接下来就看源码学习吧、我会在代码中给出详细的注释

# Template 类提供了一种简单而安全的字符串替换机制,可以避免常见的安全漏洞,例如 SQL 注入攻击。在使用模板字符串时,可以将需要被替换的变量名以 $ 作为前缀,并在后面用花括号 {} 将变量名括起来。然后使用 substitute() 方法将变量值传递给模板,该方法会将变量名替换为对应的值。 from string import Template # 文本行相关的函数和类。 import ida_lines # IDA Pro 插件开发相关的函数和类。 import idaapi import idc # 该类型是插件开发的基本接口 from ida_idaapi import plugin_t # hook函数的模板 """ 这段代码通过调用 Module.findBaseAddress("$soName") 找到指定so库的基地址,然后使用 Interceptor.attach() 函数 hook 在基地址加上指定偏移量的函数。 通过这些占位符,可以实现动态替换相应的值 $soName 表示要 hook 的so库的名字; $functionName 表示要 hook 的函数名; $offset 表示要 hook 的函数在so库中的偏移量; $args 表示在 onEnter() 函数中输出的参数值; $result 表示在 onLeave() 函数中输出的返回值。 """ hook_function_template = """ function hook_$functionName(){ var base_addr = Module.findBaseAddress("$soName"); Interceptor.attach(base_addr.add($offset), { onEnter(args) { console.log("call $functionName"); $args }, onLeave(retval) { $result console.log("leave $functionName"); } }); } """ # 这段模板和上面那段差不多 主要用于某行指令去call函数的时候 在onEnter回调中 可以查看函数参数、寄存器值等调用上下文的详细信息。 inline_hook_template = """ function hook_$offset(){ var base_addr = Module.findBaseAddress("$soName"); Interceptor.attach(base_addr.add($offset), { onEnter(args) { console.log("call $offset"); console.log(JSON.stringify(this.context)); }, }); } """ # 参数打印 logTemplate = 'console.log("arg$index:"+args[$index]);\n' # 在指定共享库加载时执行自定义逻辑的功能,可以用于动态监视和修改目标进程中的行为。 dlopenAfter_template = """ var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext"); if(android_dlopen_ext != null){ Interceptor.attach(android_dlopen_ext,{ onEnter: function(args){ var soName = args[0].readCString(); if(soName.indexOf("$soName") !== -1){ this.hook = true; } }, onLeave: function(retval){ if(this.hook) { this.hook = false; dlopentodo(); } } }); } function dlopentodo(){ //todo } """ # 在指定共享库初始化时执行自定义逻辑的功能 例如 init_proc 以及 init_array init_template = """ function hookInit(){ var linkername; var alreadyHook = false; var call_constructor_addr = null; var arch = Process.arch; if (arch.endsWith("arm")) { linkername = "linker"; } else { linkername = "linker64"; } var symbols = Module.enumerateSymbolsSync(linkername); for (var i = 0; i < symbols.length; i++) { var symbol = symbols[i]; if (symbol.name.indexOf("call_constructor") !== -1) { call_constructor_addr = symbol.address; } } if (call_constructor_addr.compare(NULL) > 0) { console.log("get construct address"); Interceptor.attach(call_constructor_addr, { onEnter: function (args) { if(alreadyHook === false){ const targetModule = Process.findModuleByName("$soName"); if (targetModule !== null) { alreadyHook = true; inittodo(); } } } }); } } function inittodo(){ //todo } """ # 以十六进制形式打印指定so文件中指定偏移量处内存数据 dump_template = """ function dump_$offset() { var base_addr = Module.findBaseAddress("$soName"); var dump_addr = base_addr.add($offset); console.log(hexdump(dump_addr, {length: $length})); } """ # 生成参数打印脚本 def generate_printArgs(argNum): if argNum == 0: return "// no args" else: temp = Template(logTemplate) logText = "" for i in range(argNum): logText += temp.substitute({'index': i}) logText += " " return logText # 生成hook函数模板 通过 soName, functionName, address, argNum, hasReturn def generate_for_func(soName, functionName, address, argNum, hasReturn): # 根据参数个数打印 argsPrint = generate_printArgs(argNum) # 根据是否有返回值判断是否打印retval retPrint = "// no return" if hasReturn: retPrint = "console.log(retval);" # 使用Python提供的Template字符串模板方法 temp = Template(hook_function_template) offset = getOffset(address) result = temp.substitute( {'soName': soName, "functionName": functionName, "offset": hex(offset), "args": argsPrint, "result": retPrint}) print(result) # 获取地址偏移 def getOffset(address): if idaapi.get_inf_structure().is_64bit(): return address else: return address + idc.get_sreg(address, "T") # 生成inline hook函数模板 通过 soName, address def generate_for_inline(soName, address): offset = getOffset(address) temp = Template(inline_hook_template) result = temp.substitute({'soName': soName, "offset": hex(offset)}) if idaapi.is_call_insn(address): callAddr = idaapi.get_name_ea(0, idc.print_operand(address, 0)) if callAddr != idaapi.BADADDR: callAddress = idc.get_operand_value(address, 0) argnum, _ = get_argnum_and_ret(callAddress) argsPrint = generate_printArgs(argnum) print(result.replace( "console.log(JSON.stringify(this.context));", argsPrint)) else: print(result) else: print(result) # 这个函数的作用是获取指定地址处的函数的参数个数和返回值类型。 def get_argnum_and_ret(address): # 获取反编译的代码 cfun = idaapi.decompile(address) # 获取参数长度 argnum = len(cfun.arguments) ret = True # 先调用cfun.print_dcl() 函数获取函数的声明语句 在调用 tag_remove 去掉字符串中的标记,它可以将类似 、 这样的标记从字符串中去掉 dcl = ida_lines.tag_remove(cfun.print_dcl()) # 判断函数声明语句是否有返回值 需要注意:当一个函数返回 "void *" 类型时,它实际上是返回一个指针,指向某个数据的内存地址。 if (dcl.startswith("void ") is True) & (dcl.startswith("void *") is False): ret = False return argnum, ret # 通过地址去生成 hook 脚本 def generate_for_func_by_address(addr): soName = idaapi.get_root_filename() functionName = idaapi.get_func_name(addr) argnum, ret = get_argnum_and_ret(addr) generate_for_func(soName, functionName, addr, argnum, ret) # 通过地址去生成 inline hook 脚本 def generate_for_inline_by_address(addr): soName = idaapi.get_root_filename() generate_for_inline(soName, addr) # 如果传入的地址等于函数头的地址 那么就hook函数头 否则生成函数中 def generate_snippet(addr): startAddress = idc.get_func_attr(addr, idc.FUNCATTR_START) if startAddress == addr: generate_for_func_by_address(addr) elif startAddress == idc.BADADDR: print("不在函数内") else: generate_for_inline_by_address(addr) # 生成初始化模板 脚本 def generateInitCode(): soName = idaapi.get_root_filename() print(Template(dlopenAfter_template).substitute({'soName': soName})) print(Template(init_template).substitute({'soName': soName})) # 生成dump 脚本 def generate_dump_script(start, length): soName = idaapi.get_root_filename() print(Template(dump_template).substitute( {'soName': soName, "offset": hex(start), "length": hex(length)})) # IDA界面hook class Hook(idaapi.View_Hooks): # 监听双击 如果是反汇编视图,则获取当前光标所在地址 然后生成模板 def view_dblclick(self, view, event): widgetType = idaapi.get_widget_type(view) if widgetType == idaapi.BWN_DISASM: global initialized if not initialized: initialized = True generateInitCode() address = idaapi.get_screen_ea() generate_snippet(address) # 监听单击事件 获取开始和结束地址 进行dump def view_click(self, view, event): widgetType = idaapi.get_widget_type(view) if widgetType == idaapi.BWN_DISASM: start = idc.read_selection_start() end = idc.read_selection_end() if (start != idaapi.BADADDR) and (end != idaapi.BADADDR): length = end - start generate_dump_script(start, length) class GenFrida_Plugin_t(plugin_t): # 关于插件的注释 # 当鼠标浮于菜单插件上方时,IDA左下角所示 comment = "A Toy Plugin for Generating Frida Code" # 帮助信息,我们选择不填 help = "unknown" # 插件的特性,是一直在内存里,还是运行一下就退出,等等 flags = idaapi.PLUGIN_KEEP # 插件的名字 wanted_name = "ShowFridaCode" # 快捷键,我们选择置空不弄 wanted_hotkey = "" # 插件刚被加载到IDA内存中 # 这里适合做插件的初始化工作 def init(self): print("ShowFridaCode init") return idaapi.PLUGIN_KEEP # 插件运行中 # 这里是主要逻辑 def run(self, arg): print("ShowFridaCode run") global myViewHook myViewHook = Hook() myViewHook.hook() # 插件卸载退出的时机 # 这里适合做资源释放 def term(self): pass initialized = False # register IDA plugin def PLUGIN_ENTRY(): return GenFrida_Plugin_t()

ShowFridaCode 这是一个十分简单但是很优秀的脚本 包含了ida界面hook frida语法 模板生成 安卓初始化流程 arm汇编的理解

IDAFrida

这是无名侠写的一个Python脚本。这段脚本主要是一个基于 IDA Pro 和 Frida 的集成工具,用于辅助逆向工程师进行动态调试。具体来说,这个工具通过 IDA Pro 分析程序的代码,提取出函数的信息(如函数名、地址、参数数量等),然后使用 Frida 注入 JS 脚本,在运行时拦截这些函数,并在函数进入和返回时打印出函数的参数和返回值。这个工具的具体功能包括:

通过 IDA Pro 分析程序代码,提取函数信息;使用 Frida 注入 JS 脚本,拦截函数并打印参数和返回值;提供一个 UI 界面,让用户可以修改 Frida 命令和 JS 脚本。

具体来说,这个脚本实现了以下几个类和函数:

ActionManager 类:一个动作管理器,用于管理 IDA Pro 中的动作。它提供了注册、初始化和销毁动作的功能;Action 类:一个动作基类,用于创建 IDA Pro 中的动作。它包含了动作的名称、描述、快捷键等信息,并提供了动作激活和更新的虚函数;Configuration 类:一个配置类,用于保存和读取用户配置信息。它包含了 Frida 命令和 JS 脚本的配置信息,并提供了保存和读取配置信息的方法;ConfigurationUI 类:一个 UI 类,用于提供一个 UI 界面,让用户可以修改配置信息;ScriptGenerator 类:一个脚本生成器类,用于生成注入到目标程序中的 JS 脚本。

我们直接看源码学习吧 同样我也会给出详细的注释

import ida_name import idaapi ################### # from: https://github.com/igogo-x86/HexRaysPyTools # 这是一个名为 ActionManager 的类,它用于管理多个动作(Action)并提供初始化和清理方法 class ActionManager(object): def __init__(self): self.__actions = [] def register(self, action): self.__actions.append(action) idaapi.register_action( # 创建一个动作描述符(action descriptor),以便注册动作并将其添加到IDA的菜单或工具栏中。action.name是动作的名称,action.description是动作的描述,action是动作的处理程序,action.hotkey是动作的快捷键。 idaapi.action_desc_t(action.name, action.description, action, action.hotkey) ) def initialize(self): pass def finalize(self): for action in self.__actions: idaapi.unregister_action(action.name) # 可以通过 register函数注册动作 action_manager = ActionManager() #这是一个基类。它继承自 idaapi.action_handler_t 类,并定义了 activate 和 update 方法作为接口,但是这些方法都是抛出了 NotImplementedError 异常,需要在子类中进行实现。此外,该类还定义了 name 属性,用于返回一个标识名称,方便在 IDA 中进行注册和调用 class Action(idaapi.action_handler_t): """ Convenience wrapper with name property allowing to be registered in IDA using ActionManager """ description = None hotkey = None def __init__(self): super(Action, self).__init__() @property def name(self): return "FridaIDA:" + type(self).__name__ # 当从菜单、弹出菜单、工具栏或以编程方式触发操作时,会调用它 def activate(self, ctx): # type: (idaapi.action_activation_ctx_t) -> None raise NotImplementedError # 当UI的上下文发生更改时会调用此函数 def update(self, ctx): # type: (idaapi.action_activation_ctx_t) -> None raise NotImplementedError ############################################################################ import ida_funcs import idc import json import os from PyQt5 import QtCore from PyQt5.Qt import QApplication from PyQt5.QtWidgets import QDialog, QHBoxLayout, QVBoxLayout, QTextEdit # [offset] => offset of target function in hex value format. # [funcname] => function name # [filename] => input file name of IDA. e.g. xxx.so / xxx.exe default_template = """ (function () { // @ts-ignore function print_arg(addr) { try { var module = Process.findRangeByAddress(addr); if (module != null) return "\\n"+hexdump(addr) + "\\n"; return ptr(addr) + "\\n"; } catch (e) { return addr + "\\n"; } } // @ts-ignore function hook_native_addr(funcPtr, paramsNum) { var module = Process.findModuleByAddress(funcPtr); try { Interceptor.attach(funcPtr, { onEnter: function (args) { this.logs = ""; this.params = []; // @ts-ignore this.logs=this.logs.concat("So: " + module.name + " Method: [funcname] offset: " + ptr(funcPtr).sub(module.base) + "\\n"); for (let i = 0; i < paramsNum; i++) { this.params.push(args[i]); this.logs=this.logs.concat("this.args" + i + " onEnter: " + print_arg(args[i])); } }, onLeave: function (retval) { for (let i = 0; i < paramsNum; i++) { this.logs=this.logs.concat("this.args" + i + " onLeave: " + print_arg(this.params[i])); } this.logs=this.logs.concat("retval onLeave: " + print_arg(retval) + "\\n"); console.log(this.logs); } }); } catch (e) { console.log(e); } } // @ts-ignore hook_native_addr(Module.findBaseAddress("[filename]").add([offset]), [nargs]); })(); """ # 配置类: 设置和存储配置信息的方法 class Configuration: def __init__(self) -> None: self.frida_cmd = """frida -U --attach-name="com.example.app" -l gen.js --no-pause""" self.template = default_template if os.path.exists("IDAFrida.json"): self.load() def set_frida_cmd(self, s): self.frida_cmd = s self.store() def set_template(self, s): self.template = s self.store() def reset(self): self.__init__() def store(self): try: data = {"frida_cmd": self.frida_cmd, "template": self.template} open("IDAFrida.json", "w").write(json.dumps(data)) except Exception as e: print(e) def load(self): try: data = json.loads(open("IDAFrida.json", "r").read()) self.frida_cmd = data["frida_cmd"] self.template = data["template"] except Exception as e: print(e) global_config = Configuration() # 修改配置UI 界面类 class ConfigurationUI(QDialog): def __init__(self, conf: Configuration) -> None: super(ConfigurationUI, self).__init__() self.conf = conf self.edit_template = QTextEdit() self.edit_template.setPlainText(self.conf.template) layout = QHBoxLayout() layout.addWidget(self.edit_template) self.setLayout(layout) def closeEvent(self, a0) -> None: self.conf.set_template(self.edit_template.toPlainText()) self.conf.store() return super().closeEvent(a0) # 脚本生成类 class ScriptGenerator: def __init__(self, configuration: Configuration) -> None: self.conf = configuration self.imagebase = idaapi.get_imagebase() @staticmethod def get_idb_filename(): return os.path.basename(idaapi.get_input_file_path()) @staticmethod def get_idb_path(): return os.path.dirname(idaapi.get_input_file_path()) # 根据地址获取函数的名字 def get_function_name(self, ea): # https://hex-rays.com/products/ida/support/ida74_idapython_no_bc695_porting_guide.shtml """ Get the real function name """ # Try to demangle function_name = idc.demangle_name(idc.get_func_name(ea), idc.get_inf_attr(idc.INF_SHORT_DN)) # if function_name: # function_name = function_name.split("(")[0] # Function name is not mangled if not function_name: function_name = idc.get_func_name(ea) if not function_name: function_name = idc.get_name(ea, ida_name.GN_VISIBLE) # If we still have no function name, make one up. Format is - 'UNKN_FNC_4120000' if not function_name: function_name = "UNKN_FNC_%s" % hex(ea) return function_name #替换模板中某些字符串 def generate_stub(self, repdata: dict): s = self.conf.template for key, v in repdata.items(): s = s.replace("[%s]" % key, v) return s # 根据地址列表来生成模板 def generate_for_funcs(self, func_addr_list) -> str: stubs = [] for func_addr in func_addr_list: dec_func = idaapi.decompile(func_addr) repdata = { "filename": self.get_idb_filename(), "funcname": self.get_function_name(func_addr), "offset": hex(func_addr - self.imagebase), "nargs": hex(dec_func.type.get_nargs()) } stubs.append(self.generate_stub(repdata)) return "\n".join(stubs) # 把生成frida脚本 保存到文件 并且置剪辑版 def generate_for_funcs_to_file(self, func_addr_list, filename) -> bool: data = self.generate_for_funcs(func_addr_list) try: open(filename, "w").write(data) print("The generated Frida script has been exported to the file: ", filename) except Exception as e: print(e) return False try: QApplication.clipboard().setText(data) print("The generated Frida script has been copied to the clipboard!") except Exception as e: print(e) return False return True class Frida: def __init__(self, conf: Configuration) -> None: self.conf = conf # 菜单动作类 该方法首先判断当前窗口是否是函数列表、伪代码或反汇编窗口(通过检查 ctx.form_type 属性),如果是,则将当前菜单项关联到右键弹出菜单,并返回 AST_ENABLE_FOR_WIDGET;否则返回 AST_DISABLE_FOR_WIDGET。 如果菜单项与右键弹出菜单关联成功,用户就可以在相应的窗口右键单击并选择该菜单项来触发菜单操作。在 activate 方法中可以实现具体的菜单操作逻辑。 class IDAFridaMenuAction(Action): TopDescription = "IDAFrida" def __init__(self): super(IDAFridaMenuAction, self).__init__() def activate(self, ctx) -> None: raise NotImplemented def update(self, ctx) -> None: if ctx.form_type == idaapi.BWN_FUNCS or ctx.form_type==idaapi.BWN_PSEUDOCODE or ctx.form_type==idaapi.BWN_DISASM: idaapi.attach_action_to_popup(ctx.widget, None, self.name, self.TopDescription + "/") return idaapi.AST_ENABLE_FOR_WIDGET return idaapi.AST_DISABLE_FOR_WIDGET # 继承菜单动作类 class GenerateFridaHookScript(IDAFridaMenuAction): description = "Generate Frida Script" def __init__(self): super(GenerateFridaHookScript, self).__init__() #在activate方法中,使用ScriptGenerator类生成Frida脚本,并将其写入到文件中。如果当前活动窗口类型是函数窗口(BWN_FUNCS),则选择被选中的函数,否则只选择当前屏幕所在函数作为目标。最后调用gen.generate_for_funcs_to_file方法生成脚本并写入到指定的文件中 def activate(self, ctx): gen = ScriptGenerator(global_config) idb_path = os.path.dirname(idaapi.get_input_file_path()) out_file = os.path.join(idb_path, "IDAhook.js") if ctx.form_type==idaapi.BWN_FUNCS: selected = [idaapi.getn_func(idx).start_ea for idx in ctx.chooser_selection] #from "idaapi.getn_func(idx - 1)" to "idaapi.getn_func(idx)" else: selected=[idaapi.get_func(idaapi.get_screen_ea()).start_ea] gen.generate_for_funcs_to_file(selected, out_file) class ViewFridaTemplate(IDAFridaMenuAction): description = "View Frida Template" def __init__(self): super(ViewFridaTemplate, self).__init__() def activate(self, ctx): ui = ConfigurationUI(global_config) ui.show() ui.exec_() class RunGeneratedScript(IDAFridaMenuAction): description = "Run Generated Script" def __init__(self): super(RunGeneratedScript, self).__init__() def activate(self, ctx): print("template") # 设置执行命令 class SetFridaRunCommand(IDAFridaMenuAction): description = "Set Frida Command" def __init__(self): super(SetFridaRunCommand, self).__init__() def activate(self, ctx): print("template") action_manager.register(GenerateFridaHookScript()) # action_manager.register(RunGeneratedScript()) action_manager.register(ViewFridaTemplate()) # action_manager.register(SetFridaRunCommand())

总的来说,这个脚本实现了一个较为完整的 IDA Pro 和 Frida 集成工具,并提供了一些 UI 功能和便捷的函数接口,使得逆向工程师能够更加高效地进行动态调试和分析。

MyFridaPlugin

通过上面两个例子,我们已经学习了frida模板的生成 以及如何和ida的ui交互 这样我们就可以自己写个插件 整合上面的代码 并且可以添加自己想要的功能

请添加图片描述

源码: https://github.com/AnxiangLemon/MyIdaFrida



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有