导语:这篇文章简要介绍了我编写的一个脚本,该脚本用调试输出的名称替换了IDA中的默认函数名,希望它能为你创建自己的函数名提供基本知识。
前言
这篇文章简要介绍了我编写的一个脚本,该脚本用调试输出的名称替换了IDA中的默认函数名,希望它能为你创建自己的函数名提供基本知识。
免责声明:这是我写的一个小脚本的解释,它帮助我可以在几秒内(而不是数周)映射大型二进制文件。我鼓励任何人修改脚本以供自己使用。我将这段代码用于我自己的私人研究——如果你发现它有用或者修复了一个bug,那就要买瓶啤酒好好谢谢我了。
存在的问题
我遇到的主要问题是我需要映射一个没有任何符号的大型二进制文件。对于二进制文件的第一个映射,我只有一个有限的时间框架,所以我必须找到一个更有效的方法来做到这一点。我非常喜欢为IDA编写脚本,尤其是映射部分,这也是我在此情况下所做的。为了自动化映射过程,我使用了一个简单的方式:查看是否有任何调试输出——幸运的是,二进制文件有很多调试输出。
实例分析
从装配方面来看,调试输出真是一个宝藏。它可以显示函数的用途,还可以显示真正的文件名,这有助于理解此函数所属的模块。值得注意的是,我最初研究的代码是在x64 OS上运行的8086程序集,而大多数函数都使用fastcall调用约定,因此我在我的文章中使用fastcall作为示例。
图1:调试输出带指示性错误字符串
图2:使用源文件名调试输出
查找日志函数名称
由于这段代码有太多的调试输出,我决定写一些东西来处理它们。有几种方法可以找出哪些函数处理调试输出,其中一种方法是根据其内部的libc函数调用或行为来查找这些函数,这是一种比较复杂和耗时的方法,但它看起来更优雅。第二种方式是快速且粗暴的,特别是当你没有很多时间又急需时,我建议你使用它。在这种情况下,只需查看可执行文件中的字符串并找到可疑的调试输出,在找到它们之后,查看一些函数是否将它们作为参数接收。如果使用调试输出作为参数重复调用函数,那么你可以在脚本中使用它。在创建脚本之前,我发现大约有10个不同的函数正在处理调试输出,并且我还发现了寄存器中的字符串参数存储在其中。
我的解决方案
我们的目标是根据调试输出更改IDA的默认函数名称。
例如:
图3:使用脚本更改函数名前后
接下来我将阐明脚本的不同部分。
把它们放在一起
正如我所说的,至少有两种方法可以找到调用的日志函数,一个懒人方案,一个非懒人方案。
懒人方案
遍历所有程序集并查找“call”指令,然后查找带有日志函数名称的参数。我决定将函数名称组织为全局字典的一部分:
FUNCTIONS_REGISTERS = {Function_Name:Register, Function_Name_1, Register_1... }
函数名称作为键,它们的值是调试输出的相关寄存器。例如:
FUNCTIONS_REGISTERS = {'g_WriteLogFile': 'rdx', 'g_LogError': 'rdx'}
我为该部分编写的脚本如下:
curr_addr = MinEA() end = MaxEA() while curr_addr < end: if curr_addr == idc.BADADDR: break elif idc.GetMnem(curr_addr) == 'call': if idc.GetOpnd(curr_addr, 0) in FUNCTIONS_REGISTERS.keys(): pass
非懒人方案
我想到的不那么懒惰的方法是将xref用于找到的相关函数。通过这种方式,我使用了相同的函数名字典。在这里,我所做的是找到每个函数的外部参照地址,即函数调用的地址。
for function_name in FUNCTIONS_REGISTERS.keys(): func_addr = idc.LocByName(function_name) a = idautils.XrefsTo(func_addr, 1) for xref in a: curr_addr = xref.frm # ea in func if curr_addr == idc.BADADDR: pass
获取函数参数
这些函数中包含在调用指令之前分配的寄存器中存储的调试输出。因为我有调用指令本身的地址,所以我需要向后查找,并从调用指令地址开始找到相关的寄存器值。
获取寄存器分配的地址名称的代码如下:
def get_string_for_function(call_func_addr, register): """ :param start_addr: The function call address :return: the string offset name from the relevant register """ cur_addr = call_func_addr start_addr = idc.GetFunctionAttr(cur_addr, idc.FUNCATTR_START) cur_addr = idc.PrevHead(cur_addr) # go through previous opcodes looking for assignment to the register while cur_addr >= start_addr: if idc.GetMnem(cur_addr)[:3] == "lea" and idc.GetOpnd(cur_addr, 0) == register: str_func = idc.GetOpnd(cur_addr, 1) return str_func cur_addr = idc.PrevHead(cur_addr) return str_func
我们有调试输出地址了,现在我们需要考虑如何得到它引用的实际字符串。下面的代码显示了它是如何完成的:(例如:更改“aErrorSavingFil”->“Error saving file %1”。我们可以通过简单地从其名称中提取地址然后获取存储在其中的字符串来实现。)
func_name = idc.GetString(idc.LocByName(addr)
从调试输出到函数名
在更改函数名称之前,我们应该稍微修改调试输出格式,因为要呈现的最终函数名称应该是干净且可读的,因此我在脚本中创建了一个函数。
免责声明:我在这里介绍的函数不是我使用的整个函数,它只对调试输出进行了一般性更改,如果你想为自己创建这样的脚本,你应该编写一个函数来更改调试中的相关部分输出格式。
在此函数中,还从地址名称中提取调试输出字符串。
def get_fixed_source_filename(addr): """ :param addr: The address of the source filename string :return: The fixed source filename's string """ func_name = idc.GetString(idc.LocByName(addr)).replace("/", "_").replace(" ", "_") func_name = "AutoFunc_" + func_name # if the debug print is a path, delete the extension if func_name.endwith(".c") or func_name.endwith(".h"): func_name = func_name[:-2] # you can add whatever you want here in order to have your preferred function name return func_name
更改函数名称
更改函数名是脚本的最后一部分,可以通过运行以下命令轻松完成:
idaapi.set_name(function_start, new_filename, idaapi.SN_FORCE)
值得注意的是,idaapi.SN_FORCE标志只能用于IDA 7及更高版本。
错误的处理
由于我有一个大型的二进制文件,所以我偶尔会发现一些调试函数的不同点,虽然在99.9%的情况下不会发生错误,但我也不能忽略其可能性。即使发生了一些错误,脚本也会继续在其他所有的函数上运行,不过我还是想跟踪错误并更改失败的函数名称。
发生这些错误时,消息将显示在输出窗口中:
图4:IDA输出窗口,出错
错误消息包含失败的地址,日志函数名称和函数的当前名称。
结论
总的来说,它不是什么高深的事,这通常是我脚本中的所有代码部分。希望它能帮助人们在他们的道路上增加代码覆盖率,或者只是打开他们到IDAPython的神奇世界。我希望你能喜欢这篇文章,也欢迎任何反馈。
点击这里可以获取完整的代码。