先贴几个你可能用得上的链接
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分别如下:
