IDA Pro的一些使用技巧

Published: 三 20 十月 2021

In Reverse.

头头

还记得第一次面试老大问我用过ida的哪些快捷键(后来我想到他不是要我背C键是啥意思),我答F5,因为我不记得其他的了,而且一般我是用鼠标点,工作后发现在频繁使用时必定会需要快捷键,这东西也没必要背,用到自然记住,不用自然遗忘,但是还是想贴张图: image.png

对IDA的使用,只有《IDA Pro权威指南》,毕竟它只是一个工具,能有一本书专门讲述它已经能证明它的成功了,除此的话,它的官方文档还是有必要再看一遍的,特别是它的反编译常见错误基本总结了会遇到的问题的解决办法。

脚本编写

ida支持通过Native(C/C++等)/IDC/Python扩展,我只学过IDC和Python,嗯前者也早忘了,所以最常使用的是Python。对于简单功能或测试可以直接在ida的输出窗口编写python代码,在复杂一点可以用今年的插件比赛出现的IPyIDA,它把IPython和IDA缝合了,安装好后按<Shift-.>即可:

再复杂一点就要用IDE开发了,我把Python3安装在了IDA目录下,使用Pycharm开发,做如下配置:

此处添加了一个库路径,它就是看雪下下来后解压出的那样,里面是ida.py等文件,方便使用。由于此时开发无法直接使用pycharm调试,推荐安装pydevd-pycharm库,之后在代码中添加如下代码:

import pydevd_pycharm

pydevd_pycharm.settrace('localhost', port=5555, stdoutToServer=True, stderrToServer=True)

ida运行该代码将会连接位于"localhost:5555"的调试器,再使用pycharm监听即可调试:

关于IDAPython的API可见官方文档,不过写的挺简单的,而且不同版本一直在变化,例如最常用的,根据一些字符串/调试信息对函数重命名,可很容易使用python实现,经分析该函数第2个参数代表命令名称,第5个参数代表命令的函数:

image.png

因此可编写脚本,ida本身未提供参数相关的接口,对此可使用idahunt(或flare),它通过一些传参特征追踪参数,另外也可以直接用hexrays的decompile接口获取参数,此处使用idahunt:

from idaapi import *
from idahunt.ida_helper import *
import re

def main():
    func_addr = 0x144F490
    all_used_addr = get_xrefs(func_addr)
    for used_addr in all_used_addr:
        try:
            arg_val = get_call_arguments(used_addr)[1]
            new_func_name = get_strlit_contents(arg_val)
            new_func_name = new_func_name.decode().replace(' ', '_')
            if not re.match(r'^\w+$', new_func_name):
                print('invalid func name', new_func_name)
                continue
            arg_val = get_call_arguments(used_addr)[4]
            func = get_func(arg_val)
            ori_func_name = get_ea_name(func.start_ea)
            if ori_func_name.startswith('sub_'):
                new_func_name = ori_func_name.replace('sub', new_func_name)
                print('ori name: ', ori_func_name, 'new name:', new_func_name)
                set_name(func.start_ea, new_func_name)
        except Exception as e:
            print(f'addr:{used_addr} err:{e}')


if '__main__' == __name__:
    main()

运行后结果如下:

image.png

尽管自己不写但很多插件会使用C/C++开发,对此可能需要我们自己编译为动态库,IDA对此有两套SDK,一套是针对本身的IDA SDK,另一套是针对反编译的Hexrays SDK,一般后者会被放置在plugins/hexrays_sdk下,前者通常需要自己解压,我也喜欢把它放在plugins目录:

betamao@DESKTOP:~/IDA7.5SP3/plugins$ ls -lh | grep -E 'sdk'
drwxrwxrwx 1 betamao betamao 4.0K Jul 15 16:32 hexrays_sdk
drwxrwxrwx 1 betamao betamao 4.0K Jul 15 11:27 idasdk75

在编译时,指定按需指定这两个目录即可,如HexRaysCodeXplorer只提供旧版本的bin文件,需要自己编译,在Windows下修改配置文件:

  <PropertyGroup Label="UserMacros">
    <IDADIR>D:\User\bm\IDA7.5SP3</IDADIR>
    <IDASDK>$(IDADIR)\plugins\idasdk75</IDASDK>
  </PropertyGroup>

后选择构建的目标架构即可,一般我们需要编译32位和64位的版本,64位的库文件名需要以64结尾。

类型与签名

类型信息

在逆向时经常遇到使用开源库,此时就可以获取到一部分变量的类型信息,IDA支持直接把这些信息导入,直接在File->Load File-> Parse C header file...(Ctrl+F9)里导入即可,并且ida也能处理宏定义与include指令,如果在gcc里使用-I指定include的路径,也可以在Options->Compiler里指定,如下可见它也能指定一些环境相关的宏:

例如在分析Android时经常需要导入jni.h,就可以用这种方式(默认会找不到stdio.h等文件,此时可以删掉该依赖,也可以在compiler options中添加库的搜索路径)。另外也可以使用tilib读.h去生成til文件,和上面的方法一样,只是需要将参数写入配置文件。不过我一般不会用这两种方法,因为项目打了总是需要修复各种错误。一种办法是先把这些库带符号编译,之后再通过IDA导出符号。例如,已知某文件使用了openssl,那么查看它的版本:

bm@top: / # busybox strings  /lib/libcrypto.so.1.1 | grep -i openssl
OpenSSL 1.1.1k  25 Mar 2021

可知版本是1.1.1k,那么编译带符号的同版本:

wget https://github.com/openssl/openssl/archive/refs/tags/OpenSSL_1_1_1k.zip
unzip OpenSSL_1_1_1k.zip
cd openssl-OpenSSL_1_1_1k/
./config -d
make

之后在IDA里打开,通过LocalTypes(Shift-F1)窗口导出它的所有类型信息:

再在目标项目里导入这个刚生成的类型信息文件:

现在有了结构体的定义,就可以对变量进行定义了。其实当编译好带符号的库时,还有如下方法,它们效果类似:

  1. 在Produce里生成IDC文件,再导入,不过它只有Structure窗口里的类型,可能不全
  2. 直接把库的.til文件导入,用ida打开idb或i64时,它会生成.til文件,这个文件中可能没有函数签名信息(我测试时没有,待进一步研究)
  3. 在Produce里生成头文件,其实和上面演示的方法一样

签名信息

上面的方法只能迁移结构体,而函数的参数和返回值信息(签名)不会被迁移,对此可以写个脚本实现这部分功能,它首先需要把库里的函数签名导出,再去目标里做匹配。

from ida_hexrays import decompile
from ida_typeinf import tinfo_t

func = decompile(0x11)
func.type
...

不不不!事实上til本身可以包含函数的签名信息,还是要想办法生成til文件,不过现在可以配合上面的带调试信息的二进制文件了,首先分析导出.c和.h文件:

导出后,它的.c文件前面会生成函数声明,把有名称的声明粘贴到.h文件里:

使用tilib去生成til文件,使用时需要把tilib命令行工具和ida.hlp文件放在同一目录,之后可以使用-h可查看帮助信息,最简单的用法是tilib -c -hlibjson-c.h libjson-c.til(文件名最多只能含一个.否则会出错),如果顺利会显示成功,一般都会出错,此时根据报错的点修改.h文件即可:

betamao@DESKTOP ~\win # .\tilib64.exe -c -Cc1 -hlibjson-c.h  t.til
Error libjson-c:87: Undefined type name '__va_list_tag'
Error libjson-c:90: Syntax error near: __gnuc_va_list
Error libjson-c:102: Syntax error near: va_list
Error libjson-c:541: Can't open include file 'defs.h'
# 此处可见87行报了__va_list_tag未定义,这中错误非常常见,是因为它先声明结构体,
# 并在后面定义,修改时把定义位置前移到使用前即可


betamao@DESKTOP ~\win # .\tilib64.exe -vvvv -c -hlibjson-c.h  t.til
Type Information Library Utility v1.227 Copyright (c) 2000-2020 Hex-Rays
16:51:36 Parsing the header file libjson-c...
Error libjson-c:728: Variable 'json_object_to_fd' has already been defined
  previous definition at libjson-c:727
Error libjson-c:735: Variable 'json_c_visit' has already been defined
  previous definition at libjson-c:734
16:51:36 Total 2 errors
# 此处是有两个同名函数,对此只能做取舍留一个了

betamao@DESKTOP ~\win # .\tilib64.exe -vvvv -c -hlibjson-c.h  t.til
Type Information Library Utility v1.227 Copyright (c) 2000-2020 Hex-Rays
16:52:30 Parsing the header file libjson-c...
16:52:30 Sorting the type information library...
16:52:30 Writing the type information library...
16:52:30 Done
# 生成成功

生成成功后,可使用tilib -l json-c.til查看包含的信息:

betamao@DESKTOP ~\win # .\tilib64.exe -l  json-c.til

TYPE INFORMATION LIBRARY CONTENTS
Description: libjson-c64
Flags      : 0003 compressed macro_table_present
Base tils  :
Compiler   : Unknown
sizeof(near*) = 4 sizeof(far*) = 6 near code, near data
default_align = 0 sizeof(bool) = 1 sizeof(long)  = 4 sizeof(llong) = 8
sizeof(enum) = 4 sizeof(int) = 4 sizeof(short) = 2
sizeof(long double) = 0

SYMBOLS
00000004 00000008          $CFAC977D1FF097D47BD6BA025CE625B1 _ISalnum;
FFFFFFFF 00000000          json_object *__cdecl json_object_array_bsearch(const json_object *key, const json_object *jso, int (*sort_fn)(const void *, const void *));
FFFFFFFF 00000000          int __cdecl json_object_array_del_idx(json_object *jso, size_t idx, size_t count);
FFFFFFFF 00000000          void __cdecl json_object_array_delete(json_object *jso);
FFFFFFFF 00000000          void __cdecl json_object_array_entry_free(void *data);
FFFFFFFF 00000000          json_object *__cdecl json_object_array_get_idx(const json_object *jso, size_t idx);
...

TYPES
00000010 struct __attribute__((aligned(8))) $5303E8BE5D6BB6190793420F7F6CF7D6 {$677E9CE920B1CD24EA75AFEC3994F1B2 cint;json_object_int_type cint_type;};
FFFFFFFF typedef int json_c_shallow_copy_fn(json_object_0 *, json_object_0 *, const char *, size_t, json_object_0 **);
FFFFFFFF typedef int json_c_visit_userfunc(json_object_0 *, int, json_object_0 *, const char *, size_t *, void *);
00000048 struct json_object {json_type o_type;uint32_t _ref_count;json_object_private_delete_fn *_delete;json_object_to_json_string_fn *_to_json_string;printbuf *_pb;data o;json_object_delete_fn *_user_delete;void *_userdata;};
00000048 typedef json_object json_object_0;
FFFFFFFF typedef void json_object_delete_fn(json_object *, void *);
...

MACROS
__int16 short
...

之后,将该til文件放入$(IDADIR)/til/pc/下,在目标二进制里按<Shift+F11>打开类库窗口,加载刚生成的til,可见已成功应用: image.png 由于til里有类型信息,其实上一节的手动导入也不必要了。 另外在有时可能会遇到一些不常见的调用约定,如之前分析AWVS时遇到的Delphi先使用eax,edx,ecx再使用栈传参,对此可以通过@REG的方式指定类型:

int func@<eax>(int x@<eax>, int y@<edx>, int z@<ecx>, int a,...);

注:IDA SDK里还有两个工具,loadint能修改指令说明,idsutils可以根据dll/ar生成函数调用信息(是否使用stdcall,若使用需要清空多少栈空间),感觉没啥用...

函数指纹匹配

在逆向分析时,一般都遇不到有符号表的程序,但它们可能使用了开源项目,此时可使用开源项目生成特征来识别符号,此类插件评判主要看两点:

  1. 效率高:内存占用要低,时间要快,算法不好时程序稍微变大(10M以上)就无法工作了。
  2. 准确:尽可能多的匹配,尽可能少的误报,当然后者更重要,因为我们会假定有名称的函数名称代表了它的含义,就不会再分析它了。

Flirt

此时可直接先将程序编译为静态库文件,之后先使用pelf等生成pat文件,再使用sigmake生成sig文件,将其命名后放入ida的ids/pc目录后,使用File->load file->flirt选择即可,详细步骤如下:

# 根据目标类型选择工具生成pat文件,如.a文件用pelf
betamao@DESKTOP:~/flair75/bin/linux$ ./pelf ~/myapache/lib/libapr-1.a libapr.pat
~/myapache/lib/libapr-1.a: skipped 0, total 76

# 再将pat转换为.sig
betamao@DESKTOP:~/flair75/bin/linux$ ./sigmake libapr.pat libapr.sig

如果顺利的话这就结束了,但一般都会报错,那是因为有多个函数有同样的签名,此时它会生成一个exc文件,需要通过编辑该文件进行取舍,之后再重新执行sigmake,详见帮助文档。从它的官方文档可见,它使用通过如下算法生成的函数签名:

  1. 取函数前32字节,若里面包含可变数据(如重定位数据)将以.进行通配
  2. 之后是CRC32校验和
  3. 接下来会以^XXXX FuncName表示它在偏移XXXX处调用了函数FuncName,这个可以重复多次

此处推荐个库sig-database,它里面有一些ubuntu和windows下的C库,打CTF分析静态链接文件时可用(逃

idb2pat

有些项目比较复杂,不便于修改为库,此时依然可以先生成普通文件,使用ida生成idb信息,再由idb2pat.py生成pat,之后的方法同时,只是需要注意,idb2pat使用python2编写,若需要在ida7.5(默认使用python3.8)上使用,需要做如下修改:

diff --git a/python/flare/idb2pat.py b/python/flare/idb2pat.py
index c1633f0..300a4c8 100644
--- a/python/flare/idb2pat.py
+++ b/python/flare/idb2pat.py
@@ -35,7 +35,8 @@ def zrange(*args):
         raise RuntimeError("Invalid arguments provided to zrange: {:s}".format(str(args)))
     if end < start:
         raise RuntimeError("zrange only iterates from smaller to bigger numbers only: {:d}, {:d}".format(start, end))
-    return iter(itertools.count(start).next, end)
+    return itertools.takewhile(lambda x:x<end, itertools.count(start, 1))


 def get_ida_logging_handler():
@@ -255,7 +256,7 @@ def make_func_sig(config, func):
         else:
             sig += "%02X" % (get_byte(ea))

-    sig += ".." * (32 - (len(sig) / 2))
+    sig += ".." * (32 - (len(sig) // 2))

     if func.end_ea - func.start_ea > 32:
         crc_data = [0 for i in zrange(256)]
@@ -292,7 +293,7 @@ def make_func_sig(config, func):

         sig += public_format % (public - func.start_ea, name)

-    for ref_loc, ref in refs.iteritems():
+    for ref_loc, ref in refs.items():
         # TODO: what is the first arg?
         name = get_true_name(0, ref)
         if name is None or name == "":
@@ -460,14 +461,14 @@ def main():
     sigs = make_func_sigs(c)

     if c.pat_append:
-        with open(filename, "ab") as f:
+        with open(filename, "a", encoding='ascii') as f:
             for sig in sigs:
                 f.write(sig)
                 f.write("\r\n")
             f.write("---")
-            f.write("\r\n")
+            f.write("\n")
     else:
-        with open(filename, "wb") as f:
+        with open(filename, "w", encoding='ascii') as f:
             for sig in sigs:
                 f.write(sig)
-            f.write("\r\n")
+            f.write("\n")

另外它生成的部分数据可能有误,导致sigmake运行失败,此时可使用-vvvv参数增加输出详细度,这样能快速定位到错误行。

Diaphora

没有仔细分析过它的原理,看它的官网和代码量感觉应该很厉害,使用时,先对开源库生成一个sqlite数据库文件,它不是插件,直接把下载的源码包解压后,按打开运行脚本窗口,选择diaphora.py并执行: image.png 选择导出的地址(1)确认即可。之后在待分析的目标文件里按同样的方式打开,选择导出的sqlite文件地址(1),以及上一步开源库导出的sqlite文件的地址(2)后就会开始比较,比较后结果会按相似度排列,此时可以选择一个阈值右键直接进行函数名迁移。 ​

rizzo

该项目下有多个插件和脚本,主要关注的就是rizzo,它能生成函数的签名,并在另一个项目中导入此签名,其实它和idb2pat作用类似,都是通过IDB生成签名信息,之后在另一个项目中用签名识别函数并为其重命名,但是它们实现的方式不同,Rizzo没有使用Flirt机制,它完全用idapython实现,并使用了如下几种识别方式:

o "Formal" signatures, where functions must match exactly o "Fuzzy" signatures, where functions must only resemble each other in terms of data/call references. o String-based signatures, where functions are identified based on unique string references. o Immediate-based signatures, where functions are identified based on immediate value references.

安装时,可以直接下载整包,之后执行plugins里的install.py脚本进行安装:

python ./install.py --install -d /path/to/your/ida/install/directory

不过它有很多插件可能实际上并不想用,所以这里手动把它里面的rizzo.py文件和shims文件夹移到插件目录即可:

bm@Desktop: ~/IDA7.5SP3/plugins$ tree

├── rizzo.py
├── shims
│   └── ida_shims.py

使用时,先打开已分析过的文件,打开File->Produce File-> Rizzo signature file...生成签名文件,再打开待分析的文件,打开File->Load File-> Rizzo signature file...image.png 在运行结束后,会在Output窗口输出统计信息,并把识别成功的函数属性改为FUNC_LIB(函数窗口看,函数名称变为浅蓝色)。事实上它的匹配效果较差,适合用在升级文件的符号迁移,而在同文件间做迁移,直接写脚本效率更高,可写如下脚本:

from ida_funcs import get_func_name, get_func
from ida_nalt import get_input_file_path
from ida_kernwin import ask_file, ask_buttons
from ida_name import set_name
from idautils import Functions
from json import dump, load

input_file_path = get_input_file_path()


def export_map():
    out_path = ask_file(True, input_file_path + '.map', '要导出的文件路径')
    func_addr_name = {}
    for func_addr in Functions():
        func_name = get_func_name(func_addr)
        if func_name.startswith(('sub_',)):
            continue
        func_addr_name[func_addr] = func_name

    with open(out_path, 'w', encoding='utf8') as f:
        dump(func_addr_name, f, indent=4)


def import_map():
    in_path = ask_file(False, input_file_path + '.map', '要导入的文件路径')
    with open(in_path, 'r', encoding='utf8') as f:
        func_addr_name = load(f, object_hook=lambda x:{int(k):v for k,v in x.items()})
    for func_addr in Functions():
        func_name = get_func_name(func_addr)
        if not func_name.startswith(('sub_',)):  # 已有的名字就不处理了
            continue
        if func_addr not in func_addr_name:  # 原项目也没有名字
            continue
        func = get_func(func_addr)
        new_func_name = func_addr_name[func_addr]
        print('ori name: ', func_name, 'new name:', new_func_name)
        set_name(func.start_ea, new_func_name)


if __name__ == '__main__':
    answer = ask_buttons('export', 'import', 'cancel', 1, '你要干啥?')
    if answer == 1:
        export_map()
    elif answer == 0:
        import_map()
    else:
        ...

bindiff

作为炒鸡牛批的二进制对比工具,能进行函数识别很合情合理吧!它提供了很多种匹配算法,但是缺少文档看不懂是啥意思...它提供了一篇论文,描述了其中一种基于调用关系的匹配方式。 ​

finger

阿里云开发的,没看过代码,按描述是云端收集了大量样本,建立了一个很庞大的指纹库,使用时它会把本地数据(至少是签名)上传到云端进行匹配,按描述在识别公共库时应该很有用,我自己试了下某特殊应用效果不理想,安装时,先安装sdk:

pip install finger-sdk

之后再把插件下载到plugins目录重启,在函数窗口右键选择Finger可尝试识别函数: image.png

其他

fingermatch还没看过不知道效果如何... ​

插件

IDA哒插件很多很有名啊,上面已经提到一些,而每年的插件比赛也会有很多好用的插件,这里记录一些感觉有意思的插件,这里面大多没用过也没有深入分析它的功能,先记在这里有时间再看:

  1. idaref:用于查看指令解释
  2. bincat:静态二进制分析工具包
  3. HexRaysCodeXplorer:很好用的结构体/类重构工具,CPP那篇文章里有描述
  4. Kam1n0:二进制分析平台,不明觉厉
  5. deREferencing:实现了更友好的寄存器和堆栈视图,类似pwndbg会让调试更好看
  6. Karta:识别开源库,可以和上面提到的指纹匹配配合
  7. heap-viewer:ptmalloc可视化插件
  8. retdec:反编译插件
  9. Ponce:符号执行工具
  10. labeless:将ida分析信息同步到其他调试器
  11. autore:一个小脚本,可以对只调用一个非dummy函数的函数进行重命名,也能列出函数调用的所有非dummy函数,方便在不看代码时猜测功能
  12. capa:用于猜测程序功能的
  13. SmartJump:跳转增强,可以直接在跳转窗口里输入表达式
  14. lighthouse:使用drcov作为输入的覆盖率插件