IDA Pro的一些使用技巧

Published: 2021年10月20日

In Reverse.

头头

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

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

img

脚本编写

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结尾。

⚠️:1.有时需要涉及到C/CPP写的插件,主要是别人写的但是有bug或者需要自己添加功能,此时可用VS编译调试版,再用VS附加到ida上,在需要的位置打断点,运行插件即可调试。

2.尽管7.0已经发布很多年了还是有些有用的插件用不兼容的api,此时要么开启兼容模式要么自己手动移植,移植时可直接运行插件,不兼容的位置通常会报错并给予提示,再参考7.0 API porting guidePorting from IDAPython 6.x-7.3, to 7.4即可,若是python插件还可能存在2升3的不兼容不多说。

类型与签名

类型信息

在逆向时经常遇到使用开源库,此时就可以获取到一部分变量的类型信息,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里生成头文件,其实和上面演示的方法一样

枚举类型

对枚举类型的识别也是很重要的操作,如下右侧为原始的反编译信息,根据定义signal的第一个参数信号值,它是一个无符号整数,无符号信息时这里只会显示数字需要查源码进行对照,而将其创建为枚举类型则可以由ida转换为有意义的数,如左侧在创建enum后在数字上按m选择对应的枚举即可:

img

再来看看如何新建枚举,最方便的方法当然还是在本地类型窗口里像C代码一样定义新枚举(本地类型需手动点击同步到结构体/枚举类型),也可以直接在枚举窗口(shift+f10)定义,在该窗口还能直接同步类库里已存在的枚举:

img

⚠️:从7.7开始,ida可使用libclang去解析任意复杂的头文件了(cpp的头文件需为hpp),不过它还是无法正确处理虚继承-_-

签名信息

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

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,若使用需要清空多少栈空间),感觉没啥用...

使用技巧

全局搜索

ida自带了搜索字节序列/字符串/立即数等功能,但是对于伪代码的的搜索能力较弱(Alt+T可搜索当前函数伪代码的内容),如想静态获取一个结构体某个域在哪里被赋值的,一个CPP虚函数在哪里被调用的,此时内建的搜索就无能为力了,若此时拥有类型信息较完善的代码(自己分析或存在调试符号),则可利用上文提到的导出功能将所有伪代码到处,从而实现全局搜索,如:

image-20220823112701023

当然,有时我们也会需要在单个反编译的函数中搜索,再使用这种方式就太重了,这时可直接在反编译窗口使用Alt+T去搜索当前函数伪代码...

为了方便需要将单个C分割成独立文件,这里就不贴代码了...

链表操作

主要是CONTAINING_RECORD识别,在很多代码中会使用双向链表,如linux内核的list_head

struct list_head {
    struct list_head *next, *prev;
};

对这种结构如果能识别出宏则看着会舒服很多,ida自然支持,但它只有三种情况能推测出来,见文档,主要是第一种情况,注意一定要手动(Y->Enter)确定两个类型:

image-20220915140327669

调试脚本

ida的调试是真的难用,但它的伪代码是真的好用,所以还是有用它的需求,辣么,首先,它的调试功能还是很丰富滴:

image-20220920110653545

例如它可以通过python脚本编写条件断点,使用硬件断点,也能设置触发后的行为,如通过指令/代码块/函数级别追踪...

但是总的来说还是很烂,必要时还是用gdb/windbg/r2调试舒服点,这时可以利用ida的分析数据,使用如dwarfexport/pwndbg-idascript/FakePDB/r2ida等插件...

字符串

支持中文,可在options->general的strings里设置编码

结构体引用

有时想知道一些结构体在哪些地方被引用了,或在哪里被赋值的,又不想调试时,可以直接导出C代码,再全局搜索...

CPP类

从7.2开始ida可以更好(处理层级结构,虚表等)的解析c++啦,使用时__cppobj属性修饰struct,要点如下:

// 1. 用__cppobj修饰对象 2. 虚函数表的名字必须是__vftable
struct __cppobj base1 {base1_vtbl *__vftable /*VFT*/;int data;}; 

// 3. 虚函数表类型的名称必须是 CLASSNAME_vtbl
struct /*VFT*/ base1_vtbl {int (*b1)(base1 *__hidden this);};

// 4. 派生类按酱紫的写法,可用virtual/public修饰
struct __cppobj derived : base1, base2 {}

// 5. 若有多个虚函数表,则用 CLASSNAME_XXXX_vtbl XXXX表示在派生类中的16进制偏移,如下,这样会被自动应用的
struct /*VFT*/ derived_vtbl {int (*b1)(base1 *__hidden this);};
struct /*VFT*/ derived_0008_vtabl {int (*c1)(base2 *__hidden this); }

也可以先用class关键字定义类,它会自动生成类实例的结构体和虚函数表的结构体,如:

class base1{
  int a;
  virtual int fa(); 
}
class derived: base1{}

之后再小心编辑,例如添加构造和析构函数等。除了定义内存布局,其实c++类的逆向还有一个关键点就是虚表的识别,详见我之前写的相关文章,ida在识别出typeinfo指针后就会自动解析虚表,所以知道从哪入手了趴...

注:据我所知当前它还无法正确处理虚继承,如果可以请告诉我!!!

结构体对齐

ida默认的对齐大小可能与实际编译出来的不一致,ida支持两个gcc的两个属性来设置:

1.__attribute__((packed)):作用于结构体,用于关闭默认的对齐,这是往小的变 2.__attribute__((aligned(4))): 可作用于struct或field,用于指定结构体整体或里面某个域的对齐方式,对齐必须为2的整次幂,这是往大的变

struct __attribute__((packed)) H                    // 64位下
{
  void* a;                                                              // 0x00
   __attribute__((aligned(16))) void *c;    // 0x10 若没有aligned(16)属性,指针应该按8字节对齐,即应为0x08
  int b;                                                                    // 0x18
  void * d;                                                             // 0x1c 若没有packed属性,指针应该按8字节对齐,即应为0x20
};

常见错误处理

1.XXXX: call analysis failed: 反编译时该行的函数调用分析失败,先点进被调函数反编译它,再回到原来想要反编译的函数即可。

2.XXXX: too big function: 函数大小超过限制,可修改hexray配置里的MAX_FUNCSIZE来允许反编译更大的函数。

3.todo...

命令行批处理

有时需要分析大量文件,且过程基本相同,此时再单个打开分析效率很低,可使用它的文本模式,即idat/idat64来进行批处理,如idat -B xxx即可直接生成分析数据库,同时也可以通过-S指定自动运行的分析脚本,更多参数用-h查看,实际上嘛它并没有"批",所以还需要配合shell等脚本来实现,当然有现成的工具ncc group的idahunt已经实现了,例如某服务用大量CGI实现,就可这样批量导出伪代码来批量审计:

python idahunt.py --inputdir Z:\nwork\cgi-bin \ # 扫描的这个目录下的所有文件
--analyse \ # 开始分析
--scripts Z:\Src\PycharmProjects\ida\export_c.py \  # 导出伪代码的脚本
--filter "filters\sf.py" \  # 指定过滤器,因为我的脚本会把c文件生成在同目录,所以在这里过滤掉这类文件
--cleanup # 清除.asm文件,批处理模式一定会生成这个文件很烦,删掉!

还有些有用的技巧,可见IDA Pro Tips to Add to Your Bag of Tricks

插件

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作为输入的覆盖率插件