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