通过无符号程序的日志函数来恢复函数名字
2022-07-24 #ida #python2023-03-12 更新:添加了使用 ida_hexrays.decompile 的方法
在逆向二进制程序中,经常碰到将程序 strip 了,但是日志函数中保留着函数名字的情况。所以写文记录在这种情况下,使用 IDAPython 恢复函数名字的过程。
日志函数长什么样
下面是我手写的一个日志函数。主要有几个部分:
- 日志级别 LogLevel,记录日志的最低级别do_log_level,用来在不同的情况下过滤日志。
- 日志文件 log_file,将日志输出到对应的文件中。
- 记录日志的函数 logger_log,还有几个辅助记录日志的宏。在宏中则是将函数的名称也记录了下来,方便后期调试。
代码也非常简单粗暴,唯一需要注意的是 logger_log 用了 C 中的变长函数参数,并且在相关的宏中用了 gcc 扩展 “GCC variadic macro” 来处理宏中的变长参数。
;
static FILE *log_file = NULL;
static int do_log_level = 0;
/// @brief init log file and log level
void 
/// @brief do log
void 
接下来是一点测试代码
int 
int 
下面是程序运行后的输出,可以看到 DEBUG 级别低于要记录的 INFO 级别,所以 DEBUG 级别的日志没有输出。
在 IDA 中,上面的代码长什么样
在反汇编结果中,可以看到虽然 DEBUG 级别的日志没有输出,但是相关的函数调用仍然在代码中保留着。并且每个日志函数的调用,在参数中都有函数名字。在这种情况下,就可以使用 IDAPython 进行自动化提取函数名字并且恢复回去了。
int 
__int64 __fastcall 
IDAPython 登场
下面是脚本。思路则是使用 idautils.CodeRefsTo 获取日志函数的调用,并且从调用处使用 idaapi.get_arg_addrs 获得带函数名字参数的地址,然后从这个地址读取出函数名字,调用 idc.set_name 重命名当前函数。
 = 0x4011a0  # log function address, Use 'y' to declare argument's type first
# https://reverseengineering.stackexchange.com/questions/25301/getting-function-arguments-in-ida
 = 2  # function name in log call position
# https://hex-rays.com/products/ida/support/ida74_idapython_no_bc695_porting_guide.shtml
     = 
     = 
     = 
     = 
    """Check `addr` is valid address"""
    return 
    """Read C string from `addr`"""
    
        return b
     = 
     = b
    
         = 
        
            break
        
            break
         += 
         += 1
    return 
    """Search function that `addr` belongs to"""
     = 0x0
    
        
            break
         = 
    return 
 = 
    # Get function argument
     = 
    
        
        continue
    # Get argument address
     = 
    
        
        continue
    # Get argument's immendiate value
     = 
     = 
     = .
    
        
        continue
    # Get function name
     = 
     = 
    
    
有些地方需要注意:判断函数地址是否在程序运行虚拟地址,读取 C 式字符串,获得当前指令所属的函数地址是自己写的函数,可能有更好的内置函数来辅助判断。还有 get_arg_addrs 函数,获得的是给参数赋值的指令地址,例如对下面的代码,使用 ["%x" % i for i in idaapi.get_arg_addrs(0x4012d6)],得到的返回值是 ['4012cc', '4012c7', '4012c2'],而不是字符串地址,所以得对指令使用 idaapi.decode_insn 进行解码,获得立即数的地址,即字符串的地址。还有就是日志函数有可能类型定义有问题,会导致 get_arg_addrs 返回 None,这种情况下,对着日志函数按 "y" 对参数类型和数量进行编辑,编辑后再次运行脚本即可获取参数。
.text:00000000004012C2                 mov     edx, offset aMain ; "main"
.text:00000000004012C7                 mov     esi, offset aDebugSDebugWor ; "[DEBUG]   [%s] Debug, world!"
.text:00000000004012CC                 mov     edi, 0          ; a1
.text:00000000004012D1                 mov     eax, 0
.text:00000000004012D6                 call    sub_4011A0
代码运行结果如下,可以看到,补全了没有符号的 a_important_function 函数名字,达到了基本效果。
[+] ref len 4
[+] 40127b a_important_function
[+] 4012a4 main
[+] 4012a4 main
[+] 4012a4 main
更精准的方法
上面的代码和恢复方式可能会出现找不准函数的参数的情况,或者会出现传参约定和上述代码不一样的情况,这就会导致恢复的函数名字错误。经过搜索我发现 IDA 允许通过 API 来调用反编译器来进行反编译,并且可以遍历反编译出来的 C 语言代码,这样就可以借助反编译器的结果来恢复函数名字了。
下面是相应的 IDAPython 代码,通过 idaapi.decompile 对 xref 的函数进行反编译,然后再通过编写的 LogVisitor 对反编译出来的 C 语言代码进行遍历,在 LogVisitor 类的 visit_expr 函数中定义遍历 C 语言表达式的相应逻辑。这里我们主要是寻找调用日志函数的表达式,并从中提取参数,获得参数的值,来命名当前函数。
    """Recover log by visiting ctree"""
        
         = 
         = 
         = 
         = 
        # named, skip
        
            return 0
        # only need call expression
        
            return 0
        # call expression is iog_func
        
            return 0
        # length of call argument < idx
        
            return 0
        # get argument
         = 
        
            return 0
         = 
        
            return 0
        
        # set name
        
        
        return 0
 = 0x00015CDC  # XXX: EDIT THIS VALUE
 = 4  # XXX: EDIT THIS VALUE
 = 
 = 
    
         = 
    
        
        continue
    
        continue
     =