intel Pin:动态二进制插桩的安装和使用,以及如何开发一个自己的Pintool

先贴几个你可能用得上的链接

intel Pin的官方介绍Pin: Pin 3.21 User Guide (intel.com)

intel Pin的API文档Pin: API Reference (intel.com)

intel Pin的下载地址Pin - A Dynamic Binary Instrumentation Tool (intel.com)

Pin的介绍

Pin可以被看做一个即时JIT编译器(Just in Time)。它可以程序运行时拦截常规可执行文件的指令,并在指令执行前生成新的代码,然后去执行生成的新的代码,并在新的代码执行完成后,将控制权交给被拦截的指令。

就好像在指令前插了一根桩完成了其他的操作之后再执行程序正常的操作。

Pin支持多平台(Windows、Linux、OSX、Android)和多架构(x86,x86-64、Itanium、Xscale,好像支持的也不是很多。。)

Pin不开源

Pintool

Pin只是一个开发框架,在真正对程序进行动态插桩时,需要使用通过Pin编译而来的Pintool。

Pintool是一个动态链接库,在使用Pin时需要通过参数载入Pintool对选定的二进制文件进行插桩分析。

(Pin和Pintool的关系,呃,有点类似于LLVM 和LLVM Pass)

Kali安装Pin

我使用的环境是安装在vmware中的Kali2021.

首先去官网下载地址(见上面)下载对应平台的最新版本,例如Linux,然后点击第一行的kit栏中的内容开始下载

下载完成后将下载的文件放入Kali中并解压,解压后的文件中有一个名为Pin的二进制文件,此时就可以直接用了。

然而Pin只是一个动态插桩的开发框架,还需要Pintool才能完成对可执行文件的插桩,intel Pin提供了一些具有特定功能的Pintool,并且已经打包在压缩包当中,但是还需要编译。

首先进入ManualExamples文件夹

cd pin-3.25-98650-g8f6168173-gcc-linux/source/tools/ManualExamples/

这里存放了许多Pintool的源代码,它们使用C++开发。现在开始编译

编译ManualExamples中的所有Pintool:

# 编译可供32位可执行文件使用的pintool
make all TARGET=ia32
# 编译可供64位可执行文件使用的pintool
make all TARGET=intel64

编译完成后,生成的pintool的动态链接库(.so文件)就存放在/ManualExamples/obj-intel64(obj-ia32)

如果只想编译某个pintool(例如你开发了一个自己编写的pintool):

# 编译可供32位可执行文件使用的pintool
make obj-ia32/inscount0.so TARGET=ia32
# 编译可供64位可执行文件使用的pintool
make obj-intel64/inscount0.so TARGET=intel64

inscount0.so对应的源代码文件名应为inscount0.cpp

如果没有cmake的话,使用下面的指令安装就行

sudo apt-get install make

 

使用Pintool对二进制文件插桩

有了Pintool之后,就可以对二进制文件进行插桩分析了,这里以intel pin官方的pintool:inscount0.so为例

inscount0.so会在每一条指令前插桩,然后执行对某个全局变量加1的操作,最后会将全局变量的值写到inscount0.out里面(如果你不指定文件的话),因此它的功能是计算程序在运行时执行了多少条指令。由于是动态插桩,因此被多次执行的指令会被重复统计(所以它不能用于统计程序的指令条数)

可以使用以下指令来使用inscount(假设你现在的工作目录是ManualExamples):

../../../pin -t obj-intel64/inscount0.so -o inscount0.log -- /bin/ls

这条指令就会对/bin/ls这个可执行文件进行插桩分析,并将结果输出在inscount0.log中(而不是默认的inscount0.out,你可以通过-o参数设置路径)

查看inscount0.log,结果如下:

┌──(kali㉿kali)-[~/pin-3.25-98650-g8f6168173-gcc-linux/source/tools/ManualExamples]
└─$ cat inscount.log                                                  
Count 718889

一共执行了718889条指令

intel pin还提供了许多pintool,可以在这个连接中查到它们的作用Pin: Pin 3.21 User Guide (intel.com)

在你需要动态插桩时,通常可能会希望“桩”执行一些比较复杂的操作,因此就不一一说明pintool的作用了(但在介绍如何开发pintool时会以其中几个为例)。

 

开发自己的Pintool

插桩的颗粒度

Pin有四种插桩的模式,也叫颗粒度,它们的区别在于“何时进行插桩”:

  • INS instrumentation:指令级插桩,即在每条指令执行时插桩
  • TRACE instrumentation:基本块级插桩,即在每个基本块执行时插桩
  • RTN instrumentation:函数级插桩,即在每个函数执行时插桩
  • IMG instrumentation:镜像级插桩,对整个程序映像插桩

其中,TRACE和传统的基本块有所区别,在Pin中,trace从一个branch开始,以一个无条件跳转(jmp call ret)结束。因此会形成一个单一入口,单一出口的指令序列,因此如果按照传统基本块的定义概念去计算基本块数量,结果可能与预期的不一致。

Pintool的基本框架

仍以inscount0.cpp为例,该文件可以在ManulExamples中找到。

首先来看它的main函数

 

main函数中,首先调用了PIN_Init,这是Pin的初始化函数,暂时不需要了解。如果初始化失败,则会执行return Usage(),Usage的内容如下:

INT32 Usage()
{
    cerr << "This tool counts the number of dynamic instructions executed" << endl;
    cerr << endl << KNOB_BASE::StringKnobSummary() << endl;
    return -1;
}

一般来说,Usage用于提示一些帮助信息(不影响pintool的功能)。

然后是打开了一个文件流OutFile,其中KnobOutputFile与pin的一个类KNOB有关,它是管理调用pintool时传入的参数的,暂时不用了解(例如在inscount0中就参与-o参数指定结果输出文件的这块逻辑)

接下来,调用回调函数INS_AddInstrumentFunction(Instruction,0),函数INS_AddInstrumentFunction表示这会作用在每一条指令上,Instruction则是对每条指令执行的操作,第二个参数0,官方文档的解释如下:

passed as the second argument to the instrumentation function

也即作为插桩函数Instruction的第二个参数,然而官方提供的pintool中基本上都没有用这个参数,因此意义不明,开发时弄个0上去就行。

下面是函数Insrtuction的定义:

VOID docount() { icount++; }

// Pin calls this function every time a new instruction is encountered
VOID Instruction(INS ins, VOID* v)
{
    // Insert a call to docount before every instruction, no arguments are passed
    INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}

VOID是pin定义的一个全局宏,与C++的void并不完全一致(呃,不管了,记得全大写就行)

在函数Instruction,第一个参数INS ins就是当前指令,第二个参数v应该就是之前提到的那个0

然后在函数内部,使用了INS_InsertCall函数进行插桩,并向该函数传递了一个分析函数docont,而真正实现pintool逻辑(统计执行指令的次数)的函数就是docont,它每执行一次(意味着有一条指令被执行了),就会让全局变量icount加1.

INS_InsertCall的参数很讲究,它是一个变长参数,第一个参数为ins,传入插桩处的指令,第二个参数有四种选择:

  • IPOINT_BEFORE:在插桩对象执行之前插桩(即执行docount函数)
  • IPOINT_AFTER:在插桩对象执行之后插桩,对于inscount0来说,使用这个和IPOINT_BEFORE没区别,但对于其他逻辑,或者其他颗粒度的插桩(例如函数级插桩,只在函数执行完成后执行docount)区别很大
  • IPOINT_ANYWHERE:在插桩对象内部的任何地方插桩,例如可以理解为在一个函数中启用IPOINT_BEFORE,也就是局部的全局插桩
  • IPOINT_TAKEN_BRANCH:在插桩对象为判断语句后获取程序控制权的指令处插桩,例如if语句后的指令或else处的指令(二者选其一)

第三个参数为分析函数的函数指针,请注意要使用(AFUNPTR)来完成强制转换

之后则是分析函数的参数,这些参数可以在官方API文档中的IARG_TYPE中查看,inscount0的分析函数docount没有参数和返回值,因此这部分稍后换一个例子来说明。

最后,则是使用IARG_END来表示参数列表的结束。

因此INS_InsertCall的参数为:(待插桩对象,执行模式,分析函数的指针,分析函数的参数和返回值,IARG_END)

 

然后在main函数中,调用了PIN_AddFiniFunction,当pintool即将结束时调用Fini函数,完成一些收尾工作,例如inscount0就在Fini函数中完成了将结果写入文件inscount0.out中

最后调用PIN_StartProgram函数,用于启动程序。

因此一个pintool的大致结构为:初始化Pin->回调函数->结束前的操作->启动程序

 

分析函数的参数和返回值

接下来以safecopy这个pintool为例,来说明如何为分析函数传递参数和返回值

safecopy的main函数与inscount0差别不大,只是多了一行PIN_InitSymbols()它用于初始化程序的符号表,在更大颗粒度的插桩分析时这个函数不可缺少,这个后续再说。

safecopy中,向INS_AddInstrumentFunction传递的函数EmulatedLoad和分析函数DoLoad分别如下:

这个pintool的功能是删除程序中从内存取值并转移到寄存器的语句,并用PIN_SafeCopy这个函数来替代,这个函数更加安全,能够保证即使内存或寄存器是部分或完全不可访问的,这种数据转移也能安全地返回到调用方。

首先来看DoLoad,它需要两个参数,类型分别是REG和ADDRINT,代表着原指令的寄存器和内存地址。

而INS_InsertCall中,通过IARG_UINT32,REG(INS_OperandReg(ins,0))传递第一个参数,也就是寄存器,其中INS_OpeandReg(ins,0)能够获取指令的第0个操作数(也就是寄存器),而IARG_UINT32是这个寄存器的类型,也急速UINT32(无符号32位整型),这样的类型还有如下几个(均可在IARG_TYPE中查到):

  • IARG_ADDRINT
  • IARG_PTR
  • IARG_BOOL
  • IARG_UINT32
  • IARG_UINT64

分别表示地址、指针、布尔类型、无符号32位整型、无符号64位整型。并且在官方的API文档中,它们的描述最后都有一句(additional arg required),也就是说还需要在它们之后紧跟一个具体的参数的值(在上面的例子当中,就是REG(INS_OperandReg(ins,0)))

此外,还有一些常用的IARG_TYPE:

  • IARG_INST_PTR:指令ins的地址,是一个语法糖,等价于传递两个参数IARG_ADDRINT, INS_Address(ins)
  • IARG_CONTEXT:当前指令的上下文
  • CALL_ORDER:用于指定分析函数的调用顺序,如果你有多个分析函数的话
  • IARG_MEMORYREAD_EA(2):读内存指令中的第一个(第二个,例如cmp mem1,mem2)内存的地址
  • IARG_RETURN_REGS:保存分析函数返回值的寄存器,也需要额外参数

此外还有许多有关内存和寄存器还有函数的IARG_TYPE,可以自行查阅官方API文档,需要注意它们的使用有时会有一些条件,例如只能在IPOINT_BEFORE下使用等等。

现在INS_InsertCall剩下的参数就不难理解了,首先if语句的判断确定这必须是一条mov 寄存器,内存这样的地址。因此IARG_MEMORYREAD_EA获取内存的地址,然后使用IARG_RETURN_REGS指定返回值保存位置,也就是当前指令的第一个寄存器,通过INS_OperandReg(ins,0)来获取。

最后调用INS_Delete函数将原指令删除(因为在DoLoad中实现了更安全的数据转移,原指令实现的数据转移就不需要了)

 

其他颗粒度的pintool

之前都是指令级INS的pintool,现在来说说其他颗粒度的pintool。

这次以malloctrace(对应文件为malloctrace.cpp)为例,它的作用是打印出程序中调用malloc时传递的参数和返回值,以及调用free时传递的参数。

首先查看main函数

可以发现,这次是一个IMG级的插桩,因此在整个程序运行期间,IMG_AddInstrumentFunction只会被执行一次(相应的,其他颗粒度的插桩,AddInstrumentFunction的开头分别为INS_、TRACE_、RTN_)

此外,在Pin初始化时,多调用了PIN_InitSymbols,这是初始化符号表,对于RTN和IMG两个级别来说,这是必不可少的。

函数Image的内容如下

这里MALLOC和FREE是定义的两个宏,分别为字符串"malloc"和“free”

首先会通过RTN_FindByName获取函数malloc的RTN,它获取的实际上是映像中malloc这个函数的内容,而不是程序调用malloc的那一条语句。

之后使用RTN_Valid来判断获取的RTN是否合法

然后使用RTN_Open函数,其具体作用API文档没有说明,只是说明了必须在调用RTN_InsHead()、RTN_InsertCall()和RTN_InsHeadOnly()前调用

接下来使用RTN_InsertCall进行插桩,并且是在mallocRTN的前后插了两个桩,分别获取malloc的参数和返回值,因此这个pintool虽然是作用在映像上的,但实际上是在对RTN进行插桩,分析函数的参数不详细解释了,这里说说新出现的两个IARG_TYPE:

  • IARG_FUNCARG_ENTRYPOINT_VALUE:传递给RTN的参数,需要额外参数表示RTN的第几个参数,第一个参数从0开始(上面的例子表示函数malloc的第1个参数)
  • IARG_FUNCRET_EXITPOINT_VALUE:RTN的返回值

之后,RTN_Close,与RTN_Open成对使用

然后是对free的处理,与malloc相同,只是free没有返回值,因此少插一个桩。

分析函数Arg1Before和MallocAfter只是简单的打印出参数和返回值:

 

 

其他

在进行pintool开发时注意两点:

  1. main中回调函数和插桩函数的作用范围

   main中的回调函数,(INS_,TRACE_,RTN,_IMG_)AddInstrumentFunction(funptr,0)是表示它会将函数funptr作用在其规定的颗粒度上,但这个函数并没有完成插桩的操作。真正的插桩操作是通过(INS_,TRACE_,RTN,_IMG_)InsertCall来完成的。因此如果需要对大颗粒度下某个特定小颗粒的的目标插桩,有两种办法:

    1. 使用小颗粒度的回调函数,判断每个目标是否符合特定条件,符合的话就插桩,就像inscount0做的那样
    2. 使用大颗粒度的回调函数,筛选出符合特定条件的目标进行插桩,就像malloctrace做的那样

 

  2.不同颗粒度的回调函数会传递不同颗粒度的插桩对象,例如INS_AddInstrumentFunction(funptr,0)就会向函数funptr传递当前的指令ins,通过这个ins可以获取有关该指令的信息,以便于进行筛选插桩,对于TRACE、RTN、IMG也是同理,并且pin 提供了许多有关它们的API,可以在官方的API文档查询

 

posted @ 2022-12-08 14:15  Uiharu  阅读(2651)  评论(0编辑  收藏  举报