本文将从0到1教大家实现一个简单的IDA TryCatch(x32) 插件。
掌握IDA插件开发,可以实现一些自动化逆向工作,以提高我们的逆向效率。
效果图:
IDA功能拓展有两种方式:
◆ 可扩展模块:运行速度快,调用方便,能实现更底层的功能。
C++开发的DLL
◆ 脚本:开发方便,不用重启IDA。
IDC或Python开发
IDC是IDA自带的脚本语言。本质由idaPython插件负责支持Python脚本。
对于可扩展模块开发:
IDA提供的SDK(库、头文件)支持我们二次开发四类可扩展模块,我们主要讲插件。
eg:可以在ida反编译的基础上二次开发反编译插件:
反编译工具(F5)文件,以hex开头
hexrays负责x86指令集反编译
hexppc负责PowerPC指令集反编译
hexmips负责mips指令集反编译
复制到Plugins文件夹下。分析32位程序的ida.exe和分析64位程序的 ida64.exe都在这里找各自的插件。
ida\plugins\plugins.cfg对插件配置:
IDA主程序、IDA_SDK&ida.lib、ida.dll、VS这几个个版本要一致,以保证其函数名称、数量及顺序一致。
IDA规定:
分析32位程序的32位插件名为XXX.DLL
而
分析64位程序的64位插件名为XXX64.DLL
分析32、64程序的插件均为x64.DLL
SDK一般为IDA自带,如果没有可以去看雪工具区下载。
XX表示32位或者64位,这要看您所运行的平台架构。
我们可以使用这些文件里带有前缀ida_export、ida_export_data 的函数或者全局变量,以及所有类和结构体、枚举。
其它的函数、变量是IDA自己内部使用的。
复制配置好的解决方案,编辑即可
如需编写x64ida的插件或调试版,更换项目配置即可。
$(ProjectDir)idasdk77\include
如果是x64插件,还需额外添加__EA64__
针对 ida32.exe
,则添加 idasdk\lib\x64_win_vc_32
。
针对 ida64.exe
,则添加 idasdk\lib\x64_win_vc_64
注:这里32、64指ida操作的程序为32位或64位。
ida32.exe依旧是x64程序,只是它处理的目标文件为32位程序。
如果目标文件是x64,则链接器->常规->输出文件改为 $(OutDir)My$(TargetName)64$(TargetExt)
x64插件dll名以64结尾(自己额外加个My前缀以便区分其它插件)
同DLL调试,VS ctrl alt p
附加到IDA.exe。
下面我会快速地介绍一些SDK中常用的数据结构概念,以供参考。
PS:更多细节以及函数细节在需要时请自行查阅IDA官方手册及阅读SDK源码。
pro.h 定义了一些数据类型和宏
x64项目要配置好宏__EA64__
则对应数据类型变为64位,不再赘述。
ida与插件交互的中介 对象
元数据信息用于检查插件环境,是一些文件、IDA环境的一些基本信息。
地址范围
Debugger->Run
IDA 插件可以与IDA的调试器交互。(虽然IDA调试模块也行)
ida自带的调试器插件:(IDA plugins目录中)
下面的事件通知由插件接收,并交付给您的回调函数处理。
有了前面的知识,编写一个TryCatch(x32)分析插件就很简单了。前提是你需要了解TryCatch的底层结构,x64TryCatch及其它静态分析自动化都可以用类似流程解决。
TryCatch的实现可以参考MSDN以及ehdata.h相关文件
这里给出结构图
值得指出的是,在TryCatch机制底层需要处理Rtti类型
在TryCatch源码中,简单提供了分析Rtti类型的几种解决方法:
◆ 正则处理ida自动分析的Rtti结果
◆ 利用Windows的<typeinfo>自带的机制分析
◆ 自行解析Rtti机制(暂未研究)
考虑我们需要在x64位插件中调用x32的<typeinfo>机制,这里采用进程通讯处理,(亦可采用天堂之门,核心业务不变。)参考代码如下:
文卒,创作不易,共同进步!
模块类型 |
作用 |
处理器 |
增加对不同处理器架构的支持,也被称做IDP ( IDAProcessor)模块,负责解析硬编码生成反汇编。ida\procs\目录下。可应对虚拟壳。 |
插件 |
扩展IDA功能 |
文件加载器 |
增加对不同的可执行文件格式的支持,本质是一个导出loader_t结构的dll |
调试器 |
在不同平台下,增强与其他调试器( 或远程调试)交互的调试能力。ida\dbg\dbg目录下,包括IDA原生调试器,也是该方式实现。 |
配置按行组织,每行指定一个插件的
插件名 、插件文件(dll)名 、快捷键、参数、插件标签
menu_name filename hotkey arg flags
插件标签解释:表示啥类型的插件
0
:通常设为
0
DEBUG: Debugger plugin
WIN: Enable plugin
for
MS Windows
MAC: Enable plugin
for
Mac
LINUX: Enable plugin
for
Linux
参数:允许给同一个插件编写多行配置项以传入不同的参数
eg:
menu_name1 xxx.dll alt
-
1
1
0
menu_name2 xxx.dll alt
-
2
65500
0
配置按行组织,每行指定一个插件的
插件名 、插件文件(dll)名 、快捷键、参数、插件标签
menu_name filename hotkey arg flags
插件标签解释:表示啥类型的插件
0
:通常设为
0
DEBUG: Debugger plugin
WIN: Enable plugin
for
MS Windows
MAC: Enable plugin
for
Mac
LINUX: Enable plugin
for
Linux
参数:允许给同一个插件编写多行配置项以传入不同的参数
eg:
menu_name1 xxx.dll alt
-
1
1
0
menu_name2 xxx.dll alt
-
2
65500
0
IDA主程序:
通过Detect it easy可以查看到其是MSVC编译器编译的,因此其生成的ida.dll有些语法细节 存在版本问题。
ida.dll:
IDA目录下的,实现了SDK的内部功能
IDA.lib:
即dll的基本使用,通过ida.lib定位到具体ida.dll里面的函数实现。
IDA_SDK:
一堆头文件,其使用的C++语法不能用老的MSVC编译器,会不支持。
32/64位插件 SDK头文件一致,但所使用的lib不同。即分析32/64 PE 的函数名一致,但实现不同。
IDA主程序:
通过Detect it easy可以查看到其是MSVC编译器编译的,因此其生成的ida.dll有些语法细节 存在版本问题。
ida.dll:
IDA目录下的,实现了SDK的内部功能
IDA.lib:
即dll的基本使用,通过ida.lib定位到具体ida.dll里面的函数实现。
IDA_SDK:
一堆头文件,其使用的C++语法不能用老的MSVC编译器,会不支持。
32/64位插件 SDK头文件一致,但所使用的lib不同。即分析32/64 PE 的函数名一致,但实现不同。
目录 |
内容 |
/ |
不同平台下的makefile 编译配置文件,以及您应该首先阅读的readme.txt,特别是当版本有变化的时候。 |
include/ |
以功能分类的头文件,重点 |
libbor.wXX/ |
用于Borland C编译时,要用到的IDA库文件。 |
libgccXX.Inx/ |
Linux下的GCC编译时,要用到的IDA库文件。 |
libgcc.wXX/ |
Windows下,GCC编译时,要用到的IDA库文件。 |
libvc.wXX/ |
Windows下,Visual C++编译时,要用到的IDA库文件。 |
plugins/ |
插件例子代码。 |
dbg/ |
ida调试器插件源码 |
ldr/ |
ida各平台可执行文件加载器的源码 |
module |
各处理器架构解析源码 |
文件 |
内容 |
area.hpp |
area_t和aareacbb_t类,他们表示代码中的"域(areas)" |
bytes. hpp |
反汇编文件中,处理字节的函数和一些定义。 |
dbg.hpp & idd.hpp |
调试器类和函数 |
diskio. hpp & fpro.h |
IDA自己的fopen(), open()等文件操作函数 |
entry. hpp |
获取和操作执行文件的入口点(entry point)信息的相关函数。 |
frame.hpp |
处理堆栈、函数帧、局部变量以及地址标志的函数 |
funcs.hpp |
funcs_t类和许多与函数相关的东西 |
ida.hpp |
idainfo结构,它包含被反汇编的文件的许多重要信息 |
kernwin.hpp |
用于跟IDA界面进行交互的函数和类。 |
lines.hpp |
相关的函数和定义,用来处理反汇编文本、代码着色,等等。 |
loader.hpp |
加载或操作IDB文件的一些函数。 |
name.hpp |
获取或者设置名称的函数和定义(例如局部变量,函数名,等等)。 |
pro.hpp |
所有其他的函数的定义。 |
search. hpp |
各种函数和定义,用来搜索反汇编文件中的文本,数据代码等等。 |
segment.hpp |
segment_t 类和所有处理二进制文件中的段(区块)的函数。 |
strlist.hpp |
string_ info_t 结构和用来获取IDA的字符串列表的一些函数。 |
ua.hpp |
insn_ t, op_ t和optype_ t类分别表示指令、操作数与操作数类型,以及与IDA分析器一同运作的函数。 |
xref.hpp |
处理交叉参考引用代码,和数据参考引用的函数。 |
#include <ida.hpp>
#include <idp.hpp>
#include <loader.hpp>
#include <kernwin.hpp>
plugmod_t* idaapi init(
void
)
{
return
PLUGIN_OK;
}
void
idaapi term(
void
)
{
return
;
}
bool
idaapi run(
size_t
)
{
warning(
"Hello, world!"
);
return
true
;
}
static
char
comment[] =
"It's a plugin to show Hello world!"
;
extern
"C"
plugin_t PLUGIN =
{
IDP_INTERFACE_VERSION,
0,
init,
term,
run,
comment,
""
,
"Hello, world"
,
"Alt-F1"
};
#include <ida.hpp>
#include <idp.hpp>
#include <loader.hpp>
#include <kernwin.hpp>
plugmod_t* idaapi init(
void
)
{
return
PLUGIN_OK;
}
void
idaapi term(
void
)
{
return
;
}
bool
idaapi run(
size_t
)
{
warning(
"Hello, world!"
);
return
true
;
}
static
char
comment[] =
"It's a plugin to show Hello world!"
;
extern
"C"
plugin_t PLUGIN =
{
IDP_INTERFACE_VERSION,
0,
init,
term,
run,
comment,
""
,
"Hello, world"
,
"Alt-F1"
};
IDA插件是通过PLUGIN类来实现的,唯一需要被导出的一个对象
#include <ida.hpp>
#include <idp.hpp>
#include <loader.hpp>
#include <kernwin.hpp>
int
IDAP_init(
void
)
{
IDA启动时仅尝试加载,要运行插件时重新加载,之后一直驻留内存。
即执行一遍IDAP_init、IDAP_term,当点击插件时再执行IDAP_init、IDAP_run,不再卸载。
适合那些执行一次性功能的插件
IDA启动时加载插件后,一直处于内存中。
即执行一遍IDAP_init,当点击插件时再执行IDAP_run
适合那些需要一直保持功能的插件
检查发现当前环境不支持该插件 运行及使用
IDA将不会加载该插件
return
PLUGIN_OK;
}
void
IDAP_term(
void
)
{
return
;
}
bool
IDAP_run(
int
arg)
{
msg(
"Hello world!"
);
return
PLUGIN_OK;
}
char
IDAP_name[] =
"My plugin"
;
char
IDAP_hotkey[] =
"Alt-X"
;
char
IDAP_comment[] =
"This is my test plug-in"
;
char
IDAP_help[] =
"My plugin"
;
plugin_t PLUGIN =
{
IDP_INTERFACE_VERSION,
0,
IDAP_init,
IDAP_term,
IDAP_run,
IDAP_comment,
IDAP_help,
IDAP_name,
IDAP_hotkey /插件想要注册的功能快捷键
};
run(self,arg)
{
ref=idc.get_screen_ea()
string =d.encode(
'utf-8'
)
idc.MakeComm(ref,d);
}
IDA插件是通过PLUGIN类来实现的,唯一需要被导出的一个对象
#include <ida.hpp>
#include <idp.hpp>
#include <loader.hpp>
#include <kernwin.hpp>
int
IDAP_init(
void
)
{
IDA启动时仅尝试加载,要运行插件时重新加载,之后一直驻留内存。
即执行一遍IDAP_init、IDAP_term,当点击插件时再执行IDAP_init、IDAP_run,不再卸载。
适合那些执行一次性功能的插件
IDA启动时加载插件后,一直处于内存中。
即执行一遍IDAP_init,当点击插件时再执行IDAP_run
适合那些需要一直保持功能的插件
检查发现当前环境不支持该插件 运行及使用
IDA将不会加载该插件
return
PLUGIN_OK;
}
void
IDAP_term(
void
)
{
return
;
}
bool
IDAP_run(
int
arg)
{
msg(
"Hello world!"
);
return
PLUGIN_OK;
}
char
IDAP_name[] =
"My plugin"
;
char
IDAP_hotkey[] =
"Alt-X"
;
char
IDAP_comment[] =
"This is my test plug-in"
;
char
IDAP_help[] =
"My plugin"
;
plugin_t PLUGIN =
{
IDP_INTERFACE_VERSION,
0,
IDAP_init,
IDAP_term,
IDAP_run,
IDAP_comment,
IDAP_help,
IDAP_name,
IDAP_hotkey /插件想要注册的功能快捷键
};
run(self,arg)
{
ref=idc.get_screen_ea()
string =d.encode(
'utf-8'
)
idc.MakeComm(ref,d);
}
eg:
#ifdef __EA64__
typedef
uint64 ea_t;
...
#else
typedef
uint32 ea_t;
...
eg:
#ifdef __EA64__
typedef
uint64 ea_t;
...
#else
typedef
uint32 ea_t;
...
类型 |
本质 |
描述 |
ea_t |
uint32 |
有效地址(Effective Address),表示IDA中很多不同类型的地址(如内存,文件, limits 等等) |
sel_t |
uint32 |
段选择子,如 代码,栈和数据段选择子 |
uval_t |
uint32 |
被用来表示无符号值 |
asize_t |
uint32 |
通常用来表示某些东西的尺寸,例如一块内存。 |
|
|
|
sval_t |
int32 |
用来表示有符号值 |
adiff_t |
int32 |
表示两个地址间的不同处 |
|
|
|
BADADDR |
-1 |
表示一个无效或不存在的地址,用来检测 |
MAXSTR |
1024 |
表示最大字符串长度 |
struct
plugin_t
{
int
version;
int
flags;
void
(idaapi* init)(
void
);
void
(idaapi* term)(
void
);
void
(idaapi* run)(
int
arg);
char
* comment;
char
* help;
char
* wanted_name;
char
* wanted_hotkey;
}
struct
plugin_t
{
int
version;
int
flags;
void
(idaapi* init)(
void
);
void
(idaapi* term)(
void
);
void
(idaapi* run)(
int
arg);
char
* comment;
char
* help;
char
* wanted_name;
char
* wanted_hotkey;
}
元数据信息idainfo存储在 IDA 数据库中(即 IDB 文件)
元数据信息idainfo在第一个文件被 IDA 加载反汇编后,以后无论再多的文件被加载,元数据信息都不会改变。
struct
idainfo
{
...
char
procName[8];
ushort filetype;
ea_t startSP;
ea_t startIP;
ea_t beginEA;
ea_t minEA;
ea_t maxEA;
...
}inf;
如果您要编写的插件,仅处理x86平台下的PE和ELF二进制格式文件,则代码如下:
int
IDAP_init(
void
)
{
if
(
strncmp
(inf.procName,
"metapc"
, 8) != 0 || inf.filetype != f_ELF && inf.filetype != f_PE))
{
error(
"Only PE and ELF binary type compiled for the x86 platform is supported, sorry."
);
return
PLUGIN_SKIP;
}
return
PLUGIN_KEEP;
}
元数据信息idainfo存储在 IDA 数据库中(即 IDB 文件)
元数据信息idainfo在第一个文件被 IDA 加载反汇编后,以后无论再多的文件被加载,元数据信息都不会改变。
struct
idainfo
{
...
char
procName[8];
ushort filetype;
ea_t startSP;
ea_t startIP;
ea_t beginEA;
ea_t minEA;
ea_t maxEA;
...
}inf;
如果您要编写的插件,仅处理x86平台下的PE和ELF二进制格式文件,则代码如下:
int
IDAP_init(
void
)
{
if
(
strncmp
(inf.procName,
"metapc"
, 8) != 0 || inf.filetype != f_ELF && inf.filetype != f_PE))
{
error(
"Only PE and ELF binary type compiled for the x86 platform is supported, sorry."
);
return
PLUGIN_SKIP;
}
return
PLUGIN_KEEP;
}
“域”由很多area_t实例构成。
域是一个连续的非空地址范围(由它的起始和结束地址指定,但不包括结束地址在内),还有地址范围的属性,也是
域的内容。
段和函数都是域的派生类,这就意味着,域可以包含另外的域。一组段就是一组域。
struct
area_t
{
...
ea_t startEA;
ea_t endEA;
bool
contains(ea_t ea)
const
{
return
startEA <= ea && endEA > ea; }
bool
empty(
void
)
const
{
return
startEA >= endEA; }
asize_t size(
void
)
const
{
return
endEA - startEA; }
...
};
域的一些派生类:
struct
func_t:
public
area_t
struct
segment_t:
public
area_t
struct
hidden_area_t :
public
area_t(bytes. hpp)
代码或数据被替换成的隐藏域,它以一个描述作为摘要,并能被展开查看隐藏的信息。
struct
regvar_t:
public
area_t(frame. hpp)
被用户自定义所替换的寄存器名称(寄存器变量)
struct
memory_info_t:
public
area_t (idd. hpp)
一块内存的相关信息(当使用调试器时)
struct
segreg_t:
public
area_t(srarea. hpp)
段寄存器(在x86平台是,CS,SS,等等)信息
“域”由很多area_t实例构成。
域是一个连续的非空地址范围(由它的起始和结束地址指定,但不包括结束地址在内),还有地址范围的属性,也是
域的内容。
段和函数都是域的派生类,这就意味着,域可以包含另外的域。一组段就是一组域。
struct
area_t
{
...
ea_t startEA;
ea_t endEA;
bool
contains(ea_t ea)
const
{
return
startEA <= ea && endEA > ea; }
bool
empty(
void
)
const
{
return
startEA >= endEA; }
asize_t size(
void
)
const
{
return
endEA - startEA; }
...
};
域的一些派生类:
struct
func_t:
public
area_t
struct
segment_t:
public
area_t
struct
hidden_area_t :
public
area_t(bytes. hpp)
代码或数据被替换成的隐藏域,它以一个描述作为摘要,并能被展开查看隐藏的信息。
struct
regvar_t:
public
area_t(frame. hpp)
被用户自定义所替换的寄存器名称(寄存器变量)
struct
memory_info_t:
public
area_t (idd. hpp)
一块内存的相关信息(当使用调试器时)
struct
segreg_t:
public
area_t(srarea. hpp)
段寄存器(在x86平台是,CS,SS,等等)信息
struct
areacb_t
{ ...提供了一些控制域的通用方法,但并不常用。通常采用域派生类自带的方法
iget_area_qty()
get_next_area()
}
有两个areacb_t类的全局实例,名为segs(segment.hpp中定义)和funcs(funcs.hpp中定义),
很明显,在当前的反汇编文件中,它们表示所有的段和函数。
您可以使用下面的代码获取段和函数的数量:
#include <segment.hpp>
#include <funcs.hpp>
msg(
"Segments: %d, Functions: %d\n"
,segs.get_area_qty(),funcs.get_area_qty());
struct
areacb_t
{ ...提供了一些控制域的通用方法,但并不常用。通常采用域派生类自带的方法
iget_area_qty()
get_next_area()
}
有两个areacb_t类的全局实例,名为segs(segment.hpp中定义)和funcs(funcs.hpp中定义),
很明显,在当前的反汇编文件中,它们表示所有的段和函数。
您可以使用下面的代码获取段和函数的数量:
#include <segment.hpp>
#include <funcs.hpp>
msg(
"Segments: %d, Functions: %d\n"
,segs.get_area_qty(),funcs.get_area_qty());
class
segment_t :
public
area_t
{
public
:
uchar perm;
#define SEGPERM_EXEC 1//可执行
#define SEGPERM_WRITE 2//可写
#define SEGPERM_READ 4//可读
uchar type;
#define SEG_NORM 0//未知类型,没有使用
#define SEG_XTRN 1//定义为"extern"的段,仅被允许包含非指令的内容
#define SEG_CODE 2//代码段
#define SEG_DATA 3//数据段
#define SEG_NULL 7//零长度的段
#define SEG_BSS 9//未初始化的段
...
}
SEG_XTRN是一个特殊的(即非实际存在内容)段类型,由IDA建立,但是其他的类型,表示实际存在部分。
对于一个被IDA载入的执行文件,例如.text区块的类型值为SEG_CODE,
则perm成员的值为SEGPERM_EXEC|SEGPERM_READ。
在一个二进制文件中,要遍历所有的段,并在IDA的日志窗口中,打印段名和地址,您可以使用下面的代码。
#include <segment.hpp>
for
(
int
s = 0; s < get_segm_qty(); s++)
{
segment_t *curSeg = getnseg(s);
msg(
"%s @ %a\n"
,get_segm_name (curSeg),curSeg->startEA) ;
}
class
segment_t :
public
area_t
{
public
:
uchar perm;
#define SEGPERM_EXEC 1//可执行
#define SEGPERM_WRITE 2//可写
#define SEGPERM_READ 4//可读
uchar type;
#define SEG_NORM 0//未知类型,没有使用
#define SEG_XTRN 1//定义为"extern"的段,仅被允许包含非指令的内容
#define SEG_CODE 2//代码段
#define SEG_DATA 3//数据段
#define SEG_NULL 7//零长度的段
#define SEG_BSS 9//未初始化的段
...
}
SEG_XTRN是一个特殊的(即非实际存在内容)段类型,由IDA建立,但是其他的类型,表示实际存在部分。
对于一个被IDA载入的执行文件,例如.text区块的类型值为SEG_CODE,
则perm成员的值为SEGPERM_EXEC|SEGPERM_READ。
在一个二进制文件中,要遍历所有的段,并在IDA的日志窗口中,打印段名和地址,您可以使用下面的代码。
#include <segment.hpp>
for
(
int
s = 0; s < get_segm_qty(); s++)
{
segment_t *curSeg = getnseg(s);
msg(
"%s @ %a\n"
,get_segm_name (curSeg),curSeg->startEA) ;
}
func_t
先理解下函数块(chunk),函数源( parents),以及函数尾(tail)的概念。
它们都是func_t类型,通过func_t.flags区分。只是由于编译器优化可能把一个函数代码体打散了的不同称呼。
class
func_t :
public
area_t
{
public
:
...
ushort flags;
#define FUNC_NORET 0x00000001L//函数并不返回
#define FUNC_LIB 0x00000004L //库函数
#define FUNC_HIDDEN 0x00000040L//一段隐藏函数块
#define FUNC_THUNK 0x00000080L//块(jump)函数
#define FUNC_TAIL 0x00008000L// 一段函数尾
union
{
struct
{
asize_ t argsize;
ushort pntqty;
int
tailqty;
area_t *tails;
}
struct
{
ea_t owner;
}
...
}
eg代码:
列出了所有函数名和它们在反汇编文件中的地址,并在IDA的日志窗口中显示结果。
#include <funcs.hpp>
for
(
int
f = 0; f < get_func_qty(); f++)
{
func_t *curFunc = getn_func(f) ;
char
funcName [MAXSTR] ;
get_func_name (curFunc->startEA,funcName,
sizeof
(funcName)-1) ;
msg (
"%s: \t %a\n"
,funcName, curFunc->startEA) ;
}
func_t
先理解下函数块(chunk),函数源( parents),以及函数尾(tail)的概念。
它们都是func_t类型,通过func_t.flags区分。只是由于编译器优化可能把一个函数代码体打散了的不同称呼。
class
func_t :
public
area_t
{
public
:
...
ushort flags;
#define FUNC_NORET 0x00000001L//函数并不返回
#define FUNC_LIB 0x00000004L //库函数
#define FUNC_HIDDEN 0x00000040L//一段隐藏函数块
#define FUNC_THUNK 0x00000080L//块(jump)函数
#define FUNC_TAIL 0x00008000L// 一段函数尾
union
{
struct
{
asize_ t argsize;
ushort pntqty;
int
tailqty;
area_t *tails;
}
struct
{
ea_t owner;
}
...
}
eg代码:
列出了所有函数名和它们在反汇编文件中的地址,并在IDA的日志窗口中显示结果。
#include <funcs.hpp>
for
(
int
f = 0; f < get_func_qty(); f++)
{
func_t *curFunc = getn_func(f) ;
char
funcName [MAXSTR] ;
get_func_name (curFunc->startEA,funcName,
sizeof
(funcName)-1) ;
msg (
"%s: \t %a\n"
,funcName, curFunc->startEA) ;
}
从insn_t结构研究,其表示一整条指令,如
"MOV EAX, 0x0A"
class
insn_t
{
public
:
ea_t cs;
ea_t ip;
ea_t ea;
ushort itype;
op_t ops[8];
ushort size;
#define Op1 Operands[0] // 第一个操作数
#define Op2 Operands[1] // 第二个操作数
#define Op3 Operands[2] // ...
#define Op4 Operands[3]
#define Op5 Operands[4]
#define Op6 Operands[5]
};
insn_t cmd;
代码eg:
获取入口点的指令序号,地址和大小, 并在IDA的日志窗口显示出来。
ua_ana0(inf.beginEA);
msg(
"instruction number: %d, at %a is %d bytes in size.\n"
, cmd.itype, cmd.ea, cmd.size);
从insn_t结构研究,其表示一整条指令,如
"MOV EAX, 0x0A"
class
insn_t
{
public
:
ea_t cs;
ea_t ip;
ea_t ea;
ushort itype;
op_t ops[8];
ushort size;
#define Op1 Operands[0] // 第一个操作数
#define Op2 Operands[1] // 第二个操作数
#define Op3 Operands[2] // ...
#define Op4 Operands[3]
#define Op5 Operands[4]
#define Op6 Operands[5]
};
insn_t cmd;
代码eg:
获取入口点的指令序号,地址和大小, 并在IDA的日志窗口显示出来。
ua_ana0(inf.beginEA);
msg(
"instruction number: %d, at %a is %d bytes in size.\n"
, cmd.itype, cmd.ea, cmd.size);
op_t 用于描述 对应指令中的一个操作数信息,可以是一个特定的optype_t值(比如,通用寄存器,立即数,等等)。
class
op_t
{
public
:
char
n;
optype_t type;
ushort reg;
uval_t value;
ea_t addr;
...
}
optype_t 本质是
typedef
uchar optype_t; 表示一条指令中的操作数类型,有一些全局常量(宏)表示如下。
const
optype_t
o_void = 0,
o_reg = 1,
o_mem = 2,
op_t 用于描述 对应指令中的一个操作数信息,可以是一个特定的optype_t值(比如,通用寄存器,立即数,等等)。
class
op_t
{
public
:
char
n;
optype_t type;
ushort reg;
uval_t value;
ea_t addr;
...
}
optype_t 本质是
typedef
uchar optype_t; 表示一条指令中的操作数类型,有一些全局常量(宏)表示如下。
const
optype_t
o_void = 0,
o_reg = 1,
o_mem = 2,
操作数 |
描述 |
反汇编举例(操作数以 粗体标出) |
o_void |
不含操作数 |
pusha |
o_reg |
通用寄存器 |
dec eax
|
o_mem |
直接内存引用 |
mov eax, ds:1001h
|
o_phrase |
间接内存引用[基址寄存器+偏移寄存器] |
push dword ptr [eax]
|
o_displ |
间接偏移内存引用[基址寄存器+偏移寄存 器+偏移量] |
push [esp+8]
|
o_imm |
立即数 |
add ebx, 10h
|
o_near |
立即近地址 |
call _initterm
|
因此,举个例子来说,对于[esp+8]这样的操作数op_t结构有:
op_t.type =o_displ
op_t.reg=4
op_t.addr =8
代码eg:
获取IDA光标所处指令的第一个操作数的op_t值:
#include <kernwin.hpp>
#include <ua.hpp>
ua_ana0(get_screen_ea());
msg(
"n = %d type = %d reg = %d value = %a addr = %a\n"
,
cmd.Operands[0].n,
cmd.Operands[0].type,
cmd.Operands[0].reg,
cmd.Operands[0].value,
cmd.Operands[0].addr);
因此,举个例子来说,对于[esp+8]这样的操作数op_t结构有:
op_t.type =o_displ
op_t.reg=4
op_t.addr =8
代码eg:
获取IDA光标所处指令的第一个操作数的op_t值:
#include <kernwin.hpp>
#include <ua.hpp>
ua_ana0(get_screen_ea());
msg(
"n = %d type = %d reg = %d value = %a addr = %a\n"
,
cmd.Operands[0].n,
cmd.Operands[0].type,
cmd.Operands[0].reg,
cmd.Operands[0].value,
cmd.Operands[0].addr);
即PUSH,MOV等
insn_t{
uint16 itype
}
Ps:
ua_mnem()可将itype转为对应助记符文本。instruc_t(allins.hpp)枚举保存了所有的助记符由NN打头
enum
{
NN_null = 0,
NN_aaa,
NN_aad,
NN_aam,
如果您知道您要寻找或测试的指令,您可以使用它,而不是使用文本表示的指令。
比如,测试二进制文件中,某条指令的助记符是否为PUSH,您可以这么做:
#include <ua.hpp>
#include <allins.hpp>
ua_ana0(inf.startIP);
if
(cmd.itype == NN_push)
msg(
"First instruction is a PUSH"
);
else
msg(
"First instruction isn't a PUSH"
);
return
;
即PUSH,MOV等
insn_t{
uint16 itype
}
Ps:
ua_mnem()可将itype转为对应助记符文本。instruc_t(allins.hpp)枚举保存了所有的助记符由NN打头
enum
{
NN_null = 0,
NN_aaa,
NN_aad,
NN_aam,
如果您知道您要寻找或测试的指令,您可以使用它,而不是使用文本表示的指令。
比如,测试二进制文件中,某条指令的助记符是否为PUSH,您可以这么做:
#include <ua.hpp>
#include <allins.hpp>
ua_ana0(inf.startIP);
if
(cmd.itype == NN_push)
msg(
"First instruction is a PUSH"
);
else
msg(
"First instruction isn't a PUSH"
);
return
;
IDA通过B-tree结构来存储这些交叉引用参考的信息,并通过 xrefblk_t 结构来访问。
当一条指令后续又有一条指令,IDA会潜在地看作第一条指令引用第二条指令,可以用 xrefblk_t 的成员方法来关掉。
首先需要使用first_from()或first_to()成员函数来填充xrefblk_t
(用哪个取决于引用到‘reference to’或引用于‘reference from’)
然后遍历引用的时候,就用next_from()或next_to()成员函数来填充。
struct
xrefblk_t
{
ea_t from;
ea_t to;
uchar iscode;
uchar type;
...
};
如果xrefblk_t 的iscode 成员变量为1的话,那么type成员变量将会是下面列出来的枚举值。
enum
cref_t
{
...
fl_CF = 16,
fl_CN,
fl_JF,
fl_JN,
fl_F,
...
};
.text:712D9BF6 jz
short
loc_712D9BFE
...
.text:712D9BFE loc_712D9BFE:
.text:712D9BFE lea ecx, [ebp+var_14]
如果xrefblk_t 的iscode 成员变量为0的话,说明它是一个数据参考引用。
那么type成员变量将会是下面列出来的枚举值。
enum
dref_t
{
...
dr_O,
或者
dr_W,
dr_R,
...
};
请记住,当您看见如下代码时,您实际上看到的是数据引用,因此712D9BD9在引用712C119C:
.idata:712C119C extrn wsprintfA:dword
...
.text:712D9BD9 call ds:wsprintfA
这种情况下,xrefblk_t的type成员将是典型的dr_R值,因为是简单地读取了ds:wsprintfA这一行的地址。
另外一种数据参考引用如下,在712EABE2处的PUSH指令引用了位于712C255C的一串字符。
.text:712C255C aVersion:
.text:712C255C unicode 0, <Version>,0
...
.text:712EABE2 push offset aVersion
这种情况,xrefblk_t的type成员变量将会是dr_O,因为它以偏移访问该数据。
代码eg:
显示当前光标所在的位置,相关的交叉参考引用信息:
#include <kernwin.hpp>
#include <xref.hpp>
xrefblk_t xb;
ea_t addr = get_screen_ea();
for
(
bool
res = xb.first_to(addr, XREF_FAR); res; res = xb.next_to()) {
msg(
"From: %a, To: %a\n"
, xb.from, xb.to);
msg(
"Type: %d, IsCode: %d\n"
, xb.type, xb.iscode);
}
IDA通过B-tree结构来存储这些交叉引用参考的信息,并通过 xrefblk_t 结构来访问。
当一条指令后续又有一条指令,IDA会潜在地看作第一条指令引用第二条指令,可以用 xrefblk_t 的成员方法来关掉。
首先需要使用first_from()或first_to()成员函数来填充xrefblk_t
(用哪个取决于引用到‘reference to’或引用于‘reference from’)
然后遍历引用的时候,就用next_from()或next_to()成员函数来填充。
struct
xrefblk_t
{
ea_t from;
ea_t to;
uchar iscode;
uchar type;
...
};
如果xrefblk_t 的iscode 成员变量为1的话,那么type成员变量将会是下面列出来的枚举值。
enum
cref_t
{
...
fl_CF = 16,
fl_CN,
fl_JF,
fl_JN,
fl_F,
...
};
.text:712D9BF6 jz
short
loc_712D9BFE
...
.text:712D9BFE loc_712D9BFE:
.text:712D9BFE lea ecx, [ebp+var_14]
如果xrefblk_t 的iscode 成员变量为0的话,说明它是一个数据参考引用。
那么type成员变量将会是下面列出来的枚举值。
enum
dref_t
{
...
dr_O,
或者
dr_W,
dr_R,
...
};
请记住,当您看见如下代码时,您实际上看到的是数据引用,因此712D9BD9在引用712C119C:
.idata:712C119C extrn wsprintfA:dword
...
.text:712D9BD9 call ds:wsprintfA
这种情况下,xrefblk_t的type成员将是典型的dr_R值,因为是简单地读取了ds:wsprintfA这一行的地址。
另外一种数据参考引用如下,在712EABE2处的PUSH指令引用了位于712C255C的一串字符。
.text:712C255C aVersion:
.text:712C255C unicode 0, <Version>,0
...
.text:712EABE2 push offset aVersion
这种情况,xrefblk_t的type成员变量将会是dr_O,因为它以偏移访问该数据。
代码eg:
显示当前光标所在的位置,相关的交叉参考引用信息:
#include <kernwin.hpp>
#include <xref.hpp>
xrefblk_t xb;
ea_t addr = get_screen_ea();
for
(
bool
res = xb.first_to(addr, XREF_FAR); res; res = xb.next_to()) {
msg(
"From: %a, To: %a\n"
, xb.from, xb.to);
msg(
"Type: %d, IsCode: %d\n"
, xb.type, xb.iscode);
}
对于反汇编文件的每一个字节,IDA录入了一个相应的四字节(32位)值Flag,并保存在id1文件中。
这些个四字节中,前3字节描述该字节信息。
.text:010060FA 0x55 push ebp 对应一个四字节信息:0x00010755
同理:
.text:010011DE 0x83 0xEC 0x14 sub esp, 14h
0x010011DE:0x83 Flag:41010783
0x010011DF:0xEC Flag:001003EC
0x010011E0:0x14 Flag:00100314
关于标志值的含义:
对于反汇编文件的每一个字节,IDA录入了一个相应的四字节(32位)值Flag,并保存在id1文件中。
这些个四字节中,前3字节描述该字节信息。
.text:010060FA 0x55 push ebp 对应一个四字节信息:0x00010755
同理:
.text:010011DE 0x83 0xEC 0x14 sub esp, 14h
0x010011DE:0x83 Flag:41010783
0x010011DF:0xEC Flag:001003EC
0x010011E0:0x14 Flag:00100314
关于标志值的含义:
标志名 |
标志值 |
含义 |
封装函数 |
FF_CODE |
0x00000600L |
该字节是代码吗? |
isCode() |
FF_DATA |
0x00000400L |
该字节是数据吗? |
isData() |
FF_TAIL |
0x00000200L |
该字节是一条指令的一部分(非指令头)吗? |
isTail() |
FF_UNK |
0x00000000L |
IDA 能分辨该字节吗? |
isUnknown() |
FF_COMM |
0x00000800L |
该字节被注释了吗? |
has_cmt() |
FF_REF |
0x00001000L |
该字节被别的地方引用吗? |
hasRef() |
FF_NAME |
0x00004000L |
该字节有名称吗? |
has_name() |
FF_FLOW |
0x00010000L |
上条指令是否流过这里? |
isFlow() |
... |
... |
... |
... |
通过getFlags(ea_t ea)获取标志,并通过一些函数检测字节的信息。
函数底层即与运算:
Ps:
0x00010755 & 0x00000600 (FF_CODE) = 0x00000600. 由此,我们知道这是一条指令。
0x00010755 & 0x00000800 (FF_COMM) = 0x00000000. 我们知道这没有被注释。
代码eg:
返回光标所在行地址的标志。
#include <bytes.hpp>
msg(
"Chick Add:%p\n"
, get_screen_ea());
msg(
"Flags: %08x\n\n"
, get_flags(get_screen_ea()));
通过getFlags(ea_t ea)获取标志,并通过一些函数检测字节的信息。
函数底层即与运算:
Ps:
0x00010755 & 0x00000600 (FF_CODE) = 0x00000600. 由此,我们知道这是一条指令。
0x00010755 & 0x00000800 (FF_COMM) = 0x00000000. 我们知道这没有被注释。
代码eg:
返回光标所在行地址的标志。
#include <bytes.hpp>
msg(
"Chick Add:%p\n"
, get_screen_ea());
msg(
"Flags: %08x\n\n"
, get_flags(get_screen_ea()));
插件文件名 |
描述 |
win32_user.plw |
Windows 本机调试器 |
win32_stub.plw |
Windows 远程调试器 |
linux_user.plw |
Linux 本机调试器 |
linux_stub.plw |
Linux 远程调试器 |
debugger_t 结构,导出了一个dbg 指针,表示当前激活的调试器插件。当调试器模块被加载时,该指针就是有效的了。
struct
debugger_t
{
...
char
*name;
#define DEBUGGER_ID_X86_IA32_WIN32_USER 0 // win32用户态进程
#define DEBUGGER_ID_X86_IA32_LINUX_USER 1 // linux用户态进程
register_info_t *registers;
int
registers_size;
...
}
作为插件模块,可能您会需要访问name指针成员变量,来测试您的插件与哪个调试器交互。
registers 和 registers_size 用于获取一些可用寄存器。
debugger_t 结构,导出了一个dbg 指针,表示当前激活的调试器插件。当调试器模块被加载时,该指针就是有效的了。
struct
debugger_t
{
...
char
*name;
#define DEBUGGER_ID_X86_IA32_WIN32_USER 0 // win32用户态进程
#define DEBUGGER_ID_X86_IA32_LINUX_USER 1 // linux用户态进程
register_info_t *registers;
int
registers_size;
...
}
作为插件模块,可能您会需要访问name指针成员变量,来测试您的插件与哪个调试器交互。
registers 和 registers_size 用于获取一些可用寄存器。
IDA SDK 里,寄存器以register_info_t 结构来描述,保存寄存器的值由regval_t结构表示。
struct
register_info_t
{
const
char
*name;
ulong flags;
#define REGISTER_READONLY 0x0001 // 用户不能修改该寄存器的当前值
#define REGISTER_IP 0x0002 // 指令指针
#define REGISTER_SP 0x0004 // 栈顶指针
#define REGISTER_FP 0x0008 // 栈帧指针
#define REGISTER_ADDRESS 0x0010 // 寄存器可以包含地址
...
}
这个结构的唯一实例,可以用*dbg(SDK导出的一个debugger_t实例)的数组成员*
register
来访问。
要获取寄存器的值,最起码调试器得运行起来。
通过regval_t.ival、regval_t.fval(浮点数)获取寄存器的值。
struct
regval_t
{
ulonglong ival;
ushort fval[6];
};
ival/fval将直接对应于一个寄存器里存储的东西,
因此,如果EBX寄存器为0xDEADBEEF,ival成员(一旦用get_reg_val()函数填充后),同样将是 0xDEADBEEF。
下面的例子将循环遍历所有有效寄存器,并显示每个寄存器的值。但如果您没祭起调试器,而运行这段代码,那么值将会是0xFFFFFFFF:
#include <dbg.hpp>
for
(
int
i = 0; i < dbg->registers_size; i++) {
regval_t val;
get_reg_val((dbg->registers+i)->name, &val);
msg(
"%s: %08a\n"
, (dbg->registers+i)->name, val.ival);
}
IDA SDK 里,寄存器以register_info_t 结构来描述,保存寄存器的值由regval_t结构表示。
struct
register_info_t
{
const
char
*name;
ulong flags;
#define REGISTER_READONLY 0x0001 // 用户不能修改该寄存器的当前值
#define REGISTER_IP 0x0002 // 指令指针
#define REGISTER_SP 0x0004 // 栈顶指针
#define REGISTER_FP 0x0008 // 栈帧指针
#define REGISTER_ADDRESS 0x0010 // 寄存器可以包含地址
...
}
这个结构的唯一实例,可以用*dbg(SDK导出的一个debugger_t实例)的数组成员*
register
来访问。
要获取寄存器的值,最起码调试器得运行起来。
通过regval_t.ival、regval_t.fval(浮点数)获取寄存器的值。
struct
regval_t
{
ulonglong ival;
ushort fval[6];
};
ival/fval将直接对应于一个寄存器里存储的东西,
因此,如果EBX寄存器为0xDEADBEEF,ival成员(一旦用get_reg_val()函数填充后),同样将是 0xDEADBEEF。
下面的例子将循环遍历所有有效寄存器,并显示每个寄存器的值。但如果您没祭起调试器,而运行这段代码,那么值将会是0xFFFFFFFF:
#include <dbg.hpp>
for
(
int
i = 0; i < dbg->registers_size; i++) {
regval_t val;
get_reg_val((dbg->registers+i)->name, &val);
msg(
"%s: %08a\n"
, (dbg->registers+i)->name, val.ival);
}
IDA用bpt_t来表示不同的硬件和软件断点。
struct
bpt_t
{
ea_t ea;
asize_t size;
bpttype_t type;
int
pass_count;
int
flags;
#define BPT_BRK 0x01 // 调试器停在这个断点吗?
#define BPT_TRACE 0x02 // 当到达这个断点时,调试器添加了跟踪信息吗?
char
condition[MAXSTR];
};
有许多的函数可以创建,操作,读取该结构,
但目前,我给出一个很简单的例子,它遍历所有的断点,
并且在IDA的Log窗口显示到底是一个软件或硬件断点。
这些函数将在以后详细解释。
#include <dbg.hpp>
for
(
int
i = 0; i < get_bpt_qty(); i++) {
bpt_t brkpnt;
getn_bpt(i, &brkpnt);
if
(brkpnt.type == BPT_SOFT)
msg(
"Software breakpoint found at %a\n"
, brkpnt.ea);
else
msg(
"Hardware breakpoint found at %a\n"
, brkpnt.ea);
}
IDA用bpt_t来表示不同的硬件和软件断点。
struct
bpt_t
{
ea_t ea;
asize_t size;
bpttype_t type;
int
pass_count;
int
flags;
#define BPT_BRK 0x01 // 调试器停在这个断点吗?
#define BPT_TRACE 0x02 // 当到达这个断点时,调试器添加了跟踪信息吗?
char
condition[MAXSTR];
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)