IDA Plugin 编写基础
IDA是迄今为止最为强大的反汇编器,它有着众多的功能。但是如果它不具备通过附加的模块来对标准的函数进行扩展的功能(粗俗点说就是plugin)的话,也就有负此盛名了。现在我们就来更为详细地研究一下如何编写这些plugin。
喏,首要的就是要有这个叫做IDA的反汇编器,同时还要有与之匹配的IDA SDK。SDK里主要的部分就是include目录及其下的所有文件,这些文件里包含了编写plugin所用到的所有的宏定义和函数原型,同时用到它们的还有import lib —— ida.lib。我这里使用的编译器是Intel C++,与VC++结合起来用。我选择它的原因就是,在SDK中没有对它进行内嵌的支持(比如在SDK中就有Borland C++和VC++编译器的安装文件),而这能使我们更好地理解。ida.lib文件是ida.wll(位于IDA目录下)的import library。其中所有的函数都是用ordinal导出的,而ordinal与函数名的比较是在ida.lib里进行的。这就是需要使用与IDA版本相匹配的SDK的原因了,因为导出函数的ordinal会因版本的不同而发生变化。喏,好了,万事具备了,我们开始吧。
plugin到底是个什么东东呢?其实它也没什么稀奇的,就像一个通常的DLL,只是导出了下面这个名为PLUGIN的结构体:
struct plugin_t
{int version; // IDA的版本,用于plugin的编写,通常这个域的值为IDP_INTERFACE_VERSION - 表示SDK版本的constant。
如果这个域的值与所用IDA的版本不一致,就不会加载此plugin。
int flags // 这个好理解,就是些标志。嫌麻烦添0就行了。但这里要讲一下所有可能的取值。
PLUGIN_MOD - 置位表示plugin会修改database(坦白讲,没有什么plugin会修改它)。如果此位置位,
而进程模块却禁止修改database,则plugin就不会被使用。这个标志我们并不需要,另外的几个也是一样。
PLUGIN_DRAW - 置位表示IDA在调用plugin后需要重画(即刷新)所有窗口。
PLUGIN_SEG - plugin只在鼠标所放之处的地址属于某个segment时才能使用。这里也不需要。
PLUGIN_UNL - 若置位,则plugin在使用后会从内存中卸载。这对于调试器来说当然是好了,但对我们来说
就不太合适了。
这些标志可以用逻辑或(我们这里用or,C语言里用“|”)结合起来。但我说过,更为简单的是把所有这些
域都填成红色的NULL。
void (idaapi* init)(void); //初始化函数加载plugin的时候调用,如果函数返回了什么不好的东西,则plugin就不会被加载。
void (idaapi* term)(void); //卸载plugin时调用。一般不使用。
void (idaapi* run)(int arg); //也许这是最重要的域了 - working function,每次调用plugin都要运行,包含着plugin的主要
代码。
char* comment; //注释,作提示用的。
char* help; //help自然就是help了.
char* wanted_name; //首选的plugin名。
char* wanted_hotkey; //首选的热键。一般来说不依赖于plugin作者选用的键,实际的热键值记录在plugin.cfg,这个
文件后面会讲到。
};
一般来说,include里的loader.hpp文件里这个结构体是作为类来声明的,我想对那些不了解类的读者说,其实在C++里类和结构体在本质上都是一样的。喏,好像到了该给出源代码的时候了。我这里给出一个最简单的plugin,这个plugin会将database中的无用指令:
бесполезные группы инструкций:
pusha
popa
push eax
push edx
rdtsc
pop edx
pop eax
push edx
push eax
rdtsc
pop eax
pop edx
替换为“更为明了”的nop指令。下面是代码:
#pragma comment(linker,"/NODEFAULTLIB")
#pragma comment(linker,"/Entry:Dllmain") //Runtime库占了40kb字节,却没什么有用的东西,特别是对plugin。
然而,如果切断它,则需要重新标记入口点。
#define __NT__
#define __IDP__ //这个一定要有,否则plugin就罢工。
typedef unsigned char BYTE; //这里没有包含标准的include文件需要额外定义BYTE类型
#include <ida.hpp>
#include <idp.hpp>
#include <bytes.hpp>
#include <loader.hpp>
#include <kernwin.hpp> //"绅士的选择" IDA中的头文件
//重新标记入口点
bool Dllmain(void* hInstDLL, int reason, int reserved)
{return true;}
char comment[] = "Experimental plugin";
char help[] = "Experimental plugin";
char wanted_name[] = "ExpPlugin";
char wanted_hotkey[] = "F11";
BYTE rdtsc1[]={0x52, 0x0F, 0x31, 0x5A, 0x58};
BYTE rdtsc2[]={0x50, 0x0F, 0x31, 0x58, 0x5A}; //就是些opcodes
//plugin的initialization procedure
根据不同的返回值,IDA可以加载或者不加载plugin。可能返回的值有:
PLUGIN_SKIP - 不加载返回值。
PLUGIN_OK - 喏,一个OK自然要加载plugin啦。
PLUGIN_KEEP - 不但要加载,还要常驻内存。
比如说,要想让plugin只处理PE格式的文件,需要这样写initialization procedure:
if (inf.filetype != f_PE) return PLUGIN_SKIP;
return PLUGIN_OK;
这里就不研究那个inf结构体了,详情请见SDK。我们这里总是要加载plugin的,所以代码如下:
int init(void)
{return PLUGIN_OK;}
//在unload procedure里也是什么也不用干,空着它。
void term(void)
{return;}
//最后就是主要的run函数了
在研究主函数之前有兴趣可以研究一下database的读写函数,还有获取当前地址的函数,也即IDA中鼠标所放之处的地址:
get_byte
get_word
get_long - 从database中读取各种长度字节。接受一个参数——listing中的地址。
patch_byte
patch_word
patch_long - 向database中写入各种长度字节。两个参数,第一个——地址,第二个——值。
可以猜到,名字里有byte的函数是用于(即接受或返回)字节的,word——字,而long——双字。
get_screen_ea - 获取当前地址的函数,当前地址就是它的返回值。
void run(int arg)
{BYTE cb1;
int i, tmp0;
ea_t CA = get_screen_ea();
cb1=get_byte(CA);
//pusha
//popa
if (cb1==0x60 && get_byte(CA+1)==0x61) // pusha的opcode - 60h,而popa - 61h
{patch_word(CA, 0x9090);
return;}
//rdtsc
if (cb1==0x50) //序列的第一个字节 - push eax的opcode
{tmp0=1;
for (i=0; i<5; i++) //数组中的其它字节,使用循环检验
if (get_byte(CA+i+1)!=rdtsc1[i]) {tmp0=0; break;}
if (tmp0==1)
{patch_long(CA, 0x90909090);
patch_word(CA+4, 0x9090);
return;}}
if (cb1==0x52)
{tmp0=1;
for (i=0; i<4; i++)
if (get_byte(CA+i+1)!=rdtsc2[i]) {tmp0=0; break;}
if (tmp0==1)
{patch_long(CA, 0x90909090);
patch_word(CA+4, 0x9090);
return;}}
};
//现在放入前面讲过的导出结构体。
//一定要写上extern "C",使得linker能正确找到导出结构体的名字
extern "C" plugin_t PLUGIN = {
IDP_INTERFACE_VERSION,
0,
init,
term,
run,
comment,
help,
wanted_name,
wanted_hotkey
};
现在我们来看如何将它编译。以下是bat文件:
icl -Gz /I <SDK的include目录的路径> plugin.cpp ida.lib /link /subsystem:windows /DLL /def:plugin.def /out:exp.plw
del "<IDA目录路径>\plugins\exp.plw"
copy exp.plw "<IDA目录路径>\plugins\"
PAUSE
我已经说过,此处是以Intel C++为例的,如果用VC++的话,源代码不用改,而编译用的命令行则需要修改(不过就是把icl换成cl)。现在来详细讲一下。
-Gz告诉intel complier,默认使用stdcall规则向函数传递参数。
/I - IDA SDK的include目录的路径
plugin.cpp - plungin源代码。
ida.lib - SDK的import library,应该和源代码放入一个目录。
/link参数的后面是liner options,就不用多说了。除了def文件。因为这里已经有源代码和bat文件了,就需要生成def文件。
LIBRARY exp
EXPORTS PLUGIN
def文件指明需要导出PLUGIN结构体。一般来说,到这里就都完成了,plugin已经可以使用了,剩下的只是把它加到IDA里了。可以把它拷贝到IDA的plugins目录里。但这还不够,还需要配置一下。所有plugins的配置都保存在plugins目录下plugins.cfg文件里。通常文件的起始处是注释,后面就是plugin的配置了。为了将我们的exp.plw(Windows下的plugin的扩展名都是plw)加入进去,需要在文件末尾加入四个参数,顺序 如下:
plugin名 文件名 热键 参数。
这里的参数就是传给run函数的,我个人觉得没什么用,所以就设为0。热键嘛,可以设为F11,好像IDA用不到这个键。这样,我们所添加的这一行就是下面这个样子的:
expplugin exp F11 0
不主张在前面写一个句点加一个逗号,因为那样就成了注释。喏,一般来说可以认为,mission accomplished。剩下的就是启动IDA,打开某个文件,那个文件里要有我们前面给出的指令(比如,可以自己立马用ASM写一个),然后进行检查。如果没有问题,就按下F11键或从菜单里选择我们的plugin,这时那些指令就应该都被替换成nop了。当然,鼠标应该放在这些scrambled的指令(如果可以这样说的话)的前面,当然这些都是些垃圾指令了。到这里就真的都完成了。
[C] dragon
www.wasm.ru/article.php?article=idaplugin
鄙人拙译
http://greatdong.blog.edu.cn