最后更新于 23/1/30
前言
pin 作为一个动态插桩框架,由 intel 开发,在很多的领域中都有应用。在 ctf 中最为人熟知的应该就是 pintool+ inscount。不过这种功能显然是太过于简单了,仅仅是做了执行数量的计数,完全没能发挥出 pin 这样一个强大框架应有的水平。而写这篇文章正是为了修复自己用的一个 tracer
编译使用
pin 可以在 win linux mac 上使用,需要下载对应版本。对于能用 linux 的我觉得你编译不是什么大问题(
所以这里讲讲 Windows 上面怎么编译。首先你需要安装 visual studio 和 make 还有 cmake(当然不喜欢 vs 的可以使用别的编译器)
关于 make 怎么在 Windows 上安装,可以搜一下 cygwin,我使用这个是成功的。之后你需要下载 pin 的压缩包,并将其添加环境变量(当然如果你只编译其目录下的 tool 是可以不用的)。
之后找到你的 vs 的目录,使用 vcvars.bat 来初始化工具链,然后
make TARGET=ia32
make TARGET=intel64
就可以完成编译了。如果还是不会我写了个脚本,你如果安装了 Visual Studio 2022 就可以使用了
脚本链接
简单介绍
Pin 有四种粒度的插桩,它们的区别在于“何时进行插桩”:
- INS instrumentation:指令级插桩,即在每条指令执行时插桩
- TRACE instrumentation:基本块级插桩,即在每个基本块执行时插桩
- RTN instrumentation:函数级插桩,即在每个函数执行时插桩
- IMG instrumentation:镜像级插桩,对整个程序映像插桩
其实我自己研究了下这里很多都是需要混合使用的,部分函数虽然与插桩无关但是需要使用,如取得函数名称等,需要在 RTN 中找,所以大部分还是都需要学习的、
除了插桩指令之外,pin 还提供了针对 pin 框架本身的一些操作指令。这里以 tracer 的简单例子进行说明。
一个使用 pin 的例子
首先需要包含 pin 的头文件,pin.H
然后是 main 函数
int main(int argc, char* argv[])
{
// usage 是参数说明
if (PIN_Init(argc, argv)) return Usage();
// 打开需要输出的 log。那个前缀是由前面定义的 KNOB 决定的
OutFile.open(KnobOutputFile.Value().c_str());
// 以指令级粒度添加回调
INS_AddInstrumentFunction(Instruction, 0);
// Register Fini to be called when the application exits
PIN_AddFiniFunction(Fini, 0);
// Start the program, never returns
PIN_StartProgram();
return 0;
}
可以看到,其中是有一些以 pin 开头的函数的,这些就是能够直接控制框架本身的一些函数。
而需要的插桩回调函数也是在 main 里面注册了回调
关于 KNOB 的注册可以直接抄一个例子,添加自己需要的参数就可以了
插桩回调
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);
}
来看这个函数,InsertCall 的意思是接下来进行回调。IPOINT_BEFORE 表示在执行指令前就要调用被转换成 AFUNPTR 的函数(必须转换!),当然还有 IPOINT_AFTER,但是请注意调用 after 可能会有一些限制,例如没法进行某些内存操作判断。具体可以看文档。
回调参数
这里有个很坑的点,就是参数。pin 对参数的处理采取的是变长参数列表,在 AFUNPTR 后面的都是参数
最后一个参数必须是 IARG_END,而中间的参数则会被 pin 传给回调函数。请注意,参数必须是 pin 定义的参数之一!
那么有人要问了,我想传 string 什么的别的变量不行吗?这个东西挺坑,当初被坑了好久,pin 的参数处理是先读取类型,而有些类型会要求多个参数。。。例如,你可以这样
这样就会把这两个参数看作是一个参数传给你的回调函数,在你回调时用 const string * 来接受就可以了。。。好吧,挺奇葩
一些插桩的小问题
库函数
pin 的跟踪事实上是会记录下库函数的调用的。如果你想避免因为追踪库函数而导致产生你不想要的 trace,可以使用 pin 的追踪线程功能。可以在每个 IMG 加载的时候记录比对是否需要,然后在输出 log 之前进行区别
函数调用
这个问题比较奇怪,看起来 pin 的指令解析功能没有那么强大,例如对于库函数的一些调用,在 ida 中会被解析为
call FUNCNAME
而点进去会是
jmp cs:FUNCNAME
然而,在 pin 中不会有 funcname,若是通过 INS_DirectBranchOrCallTargetAddress(ins) 再加上 RTN 中的 getName 是取不到名字的,而且 jmp cs:FUNCNAME 这样的调用会被显示为 jmp [eip+xxxx],导致成为 IndirectControlFlow, 无法被 trace
而比较高效的解决方法是通过记录 IMG,具体可以看TinyTracer 这个效率较高的实现
(当然还有些暴力的方法,指我用的大聪明方法 😢)
系统函数 hook
除了上面提到的方案之外,还有一种方法是直接操作 RTN,首先在 IMG 层面添加回调,之后再在回调中找到需要的函数名进行插桩,具体实现可以看官方自带的 mallocTrace
函数参数
本来我是使用寄存器来取函数参数的,然而其实是不需要的。
IARG_FUNCARG_ENTRYPOINT_VALUE, 0
这样就能取到第一个参数,更改后面的数以此类推就能直接取到你要的参数了,不过前提是这个函数能被正确识别,否则是没用的,也就是能够在 RTN 层面进行插桩。
取寄存器
取寄存器在 pin 中有两种方法、
- 直接于 INS 回调中添加 REG 参数
- 在 InsertCall 之后通过 cpu 上下文取得
以下为第二种的实现,第一种只需按照文档传入 IARG_REG_VALUE 或 IARG_REG_REFERENCE 即可
VOID record_diff(const CONTEXT *cpu, ADDRINT pc, VOID *v) {
ADDRINT val;
// fetch the current register value
PIN_GetContextRegval(cpu, (REG) reg, reinterpret_cast<UINT8 *>(&val));
}
基本块
pin 的基本块概念指的是完全没有分支的块,所以即使是DirectControlFlow 也是会被认定不是同一个基本块,这点需要注意
检测与 syscall
pin 框架虽然很强大但是任然是可以被检测的。有个问题是指令计数,例如使用 GetTickCount 可以获取到指令计数器,而一种更加不容易被人发现的方法是使用 rdtsc 指令。这条指令同样可以查询 cpu 时钟,因此当指令数过大的时候就可以发现自己被 trace 了。
还有一点就是关于 syscall 的跟踪。由于使用 syscall 并没有经过系统库中被我们插桩的函数,可能导致我们遗漏一部分 trace。但是 pin 框架强大的地方,就是它直接提供了 INS_IsSyscall 等一些专门针对 syscall 的函数来帮助我们进行分析。具体实现同样可以查找自带的例子或是上面提到的 TinyTracer
修改指令
可以看一下这个 链接
pin 作为插桩工具是支持动态运行修改的,其实为 hook 提供了一种思路,不过感觉有点难用,以后再来用
总结
pin 还是比较强大的,虽然还存在一些不足,总的来说还是挺好用的。