intel pin

什么是 pin

pin 是 intel 开发的一款二进制程序的插桩分析工具,支持 x86/x64 & windows/linux/mac,提供了丰富的 API 供使用者用 C/C++ 编写 pintool 分析程序

什么是插桩(instrument)

通俗的来说,插桩就是在已有的源代码/二进制程序中插入某些代码以便于自己分析,比如在调试时使用 printf 打印变量值就属于在源代码级别的插桩。而intel pin就是在二进制程序级别(没有源代码)插桩的一款工具

pin 和 pintool

pin 的安装,pintool 的编译

pin 的安装很简单,这里以 64 位的 Linux 为例来说明,从 官网 上下载 pin 组件后,解压即可,在解压后的文件夹内有编译好的二进制程序 pin

pin-3.6-gcc-linux ls
doc  extlicense  extras  ia32  intel64  LICENSE  pin  README  redist.txt  source
pin-3.6-gcc-linux file pin 
pin: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, BuildID[sha1]=7beaa83f9142955a6e933bf29d4a8aa1269298bc, stripped
pin-3.6-gcc-linux ./pin 
E: Missing application name
Pin: pin-3.6-97554-31f0a167d
Copyright (c) 2003-2017, Intel Corporation. All rights reserved.

Usage: pin [OPTION] [-t <tool> [<toolargs>]] -- <command line>
Use -help for a description of options

source/tools/ManualExamples 中有一些现成的 pintool 可以使用,基本涵盖了各个模块的用法,这里以 inscount0 这个 pintool 为例介绍 pin 的使用方法

pin-3.6-gcc-linux cd source/tools/ManualExamples
ManualExamples file inscount0.cpp
inscount0.cpp: C source, ASCII text
ManualExamples make obj-intel64/inscount0.so TARGET=intel64
g++ -Wall -Werror -Wno-unknown-pragmas -D__PIN__=1 -DPIN_CRT=1 -fno-stack-protector -fno-exceptions -funwind-tables -fasynchronous-unwind-tables -fno-rtti -DTARGET_IA32E -DHOST_IA32E -fPIC -DTARGET_LINUX -fabi-version=2  -I../../../source/include/pin -I../../../source/include/pin/gen -isystem /home/m4x/pin-3.6-gcc-linux/extras/stlport/include -isystem /home/m4x/pin-3.6-gcc-linux/extras/libstdc++/include -isystem /home/m4x/pin-3.6-gcc-linux/extras/crt/include -isystem /home/m4x/pin-3.6-gcc-linux/extras/crt/include/arch-x86_64 -isystem /home/m4x/pin-3.6-gcc-linux/extras/crt/include/kernel/uapi -isystem /home/m4x/pin-3.6-gcc-linux/extras/crt/include/kernel/uapi/asm-x86 -I../../../extras/components/include -I../../../extras/xed-intel64/include/xed -I../../../source/tools/InstLib -O3 -fomit-frame-pointer -fno-strict-aliasing   -c -o obj-intel64/inscount0.o inscount0.cpp
g++ -shared -Wl,--hash-style=sysv ../../../intel64/runtime/pincrt/crtbeginS.o -Wl,-Bsymbolic -Wl,--version-script=../../../source/include/pin/pintool.ver -fabi-version=2    -o obj-intel64/inscount0.so obj-intel64/inscount0.o  -L../../../intel64/runtime/pincrt -L../../../intel64/lib -L../../../intel64/lib-ext -L../../../extras/xed-intel64/lib -lpin -lxed ../../../intel64/runtime/pincrt/crtendS.o -lpin3dwarf  -ldl-dynamic -nostdlib -lstlport-dynamic -lm-dynamic -lc-dynamic -lunwind-dynamic
ManualExamples file obj-intel64/inscount0.so 
obj-intel64/inscount0.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=3baa29dd54235acaab02edc94bf9ac377dd7b0e5, not stripped

此时在 obj-intel64 下编译生成了 inscount0.so,这个 so 即为一种 pintool,功能为记录程序执行的指令的条数;

判断 pintool 的功能可以阅读 pintool 源代码或者使用下条指令

$ pin -t your_pintool -h -- your_application

类似的,要编译 32 位的 pintool 可以使用

make obj-ia32/inscount0.so

编译 ManualExamples 中的所有 pintool 可以使用

make all TAEGET=intel64
make all TAEGET=ia32

pin 的使用

pin 的基本命令格式如下

pin -t your_pintool -- your_binary <arg> 

以刚刚编译的 inscount0 这个 pintool 为例

ManualExamples ../../../pin -t ./obj-intel64/inscount0.so -- /bin/ls -a
.              inscount2.cpp      obj-intel64
..             inscount.out   pinatrace.cpp
buffer_linux.cpp       inscount_tls.cpp   pin.log
buffer_windows.cpp     invocation.cpp     proccount.cpp
countreps.cpp          isampling.cpp      replacesigprobed.cpp
detach.cpp         itrace.cpp     safecopy.cpp
divide_by_zero_unix.c  little_malloc.c    stack-debugger.cpp
divide_by_zero_win.c   logtrace.cpp   stack-debugger-tutorial.sln
emudiv.cpp         makefile       stack-debugger-tutorial.vcxproj
fibonacci.cpp          makefile.rules     stack-debugger-tutorial.vcxproj.filters
follow_child_app1.cpp  malloc.cpp     statica.cpp
follow_child_app2.cpp  malloc_mt.cpp      staticcount.cpp
follow_child_tool.cpp  malloctrace.cpp    strace.cpp
fork_app.cpp           myInscount0.cpp    test
fork_jit_tool.cpp      myInscount1.cpp    test.c
imageload.cpp          myMalloctrace.cpp  test-packed
inscount0.cpp          nonstatica.cpp     tracer.cpp
inscount1.cpp          obj-ia32       w_malloctrace.cpp
ManualExamples cat inscount.out 
Count 813449

inscount 默认结果保存在 inscount.out 这个文件中,在上例中,即此时 ls -a 这条命令共执行了 813449 条指令

pintool 的分析

同样以 inscount0 为例分析,查看 inscount0.cpp 的内容

#include <iostream>
#include <fstream>
#include "pin.H"

ofstream OutFile;

// The running count of instructions is kept here
// make it static to help the compiler optimize docount
static UINT64 icount = 0;

// This function is called before every instruction is executed
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);
}

KNOB<string> KnobOutputFile(KNOB_MODE_WRITEONCE, "pintool",
    "o", "inscount.out", "specify output file name");

// This function is called when the application exits
VOID Fini(INT32 code, VOID *v)
{
    // Write to a file since cout and cerr maybe closed by the application
    OutFile.setf(ios::showbase);
    OutFile << "Count " << icount << endl;
    OutFile.close();
}

/* ===================================================================== */
/* Print Help Message                                                    */
/* ===================================================================== */

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

/* ===================================================================== */
/* Main                                                                  */
/* ===================================================================== */
/*   argc, argv are the entire command line: pin -t <toolname> -- ...    */
/* ===================================================================== */

int main(int argc, char * argv[])
{
    // Initialize pin
    if (PIN_Init(argc, argv)) return Usage();

    OutFile.open(KnobOutputFile.Value().c_str());

    // Register Instruction to be called to instrument instructions
    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;
}

从 main 开始,首先调用了 PIN_init(53 行)进行初始化,然后使用 INS_AddInstrumenFunction 注册了一个插桩函数(58行),根据 intel pin 的 用户手册

PIN_CALLBACK LEVEL_PINCLIENT::INS_AddInstrumentFunction (   INS_INSTRUMENT_CALLBACK     fun,
VOID *  val 
)       
Add a function used to instrument at instruction granularity

Parameters:
fun Instrumentation function for instructions
val passed as the second argument to the instrumentation function
Returns:
PIN_CALLBACK A handle to a callback that can be used to further modify this callback's properties
Note:
The pin client lock is obtained during the call of this API.
Availability:
Mode: JIT
O/S: Linux, Windows & OS X*
CPU: All

在这里该函数的作用是在指令粒度插入 Instruction 函数,即在每条指令执行前,都会进入 Instruction 这个函数中,其第2个参数为一个额外传递给 Instruction 的参数,即对应 VOID *v 这个参数,这里没有使用。而 Instruction 接受的第一个参数为 INS 结构,用来表示一条指令

而我们再看 Instruction 这个函数

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);
}

在 Instruction 函数内部又使用 INS_InsertCall 注册了一个函数 docount,即在每条指令前实际插入了 docount 这个函数。需要注意的是 INS_InsertCall 试一个便餐函数,前三个参数分别为指令(ins),插入的实际(IPOINT_BEFORE,表示在指令运行之前插入 docount 函数),函数指针(docount,转化为了 AFUNPTR 类型),之后的参数为传递给 docount 函数的参数,以 IARG_END 结尾

而 docount 函数(12 行)的作用就很明显了,每次将一个全局变量加 1,因此此时 /bin/ls -a 运行的模式如下:

...
icount++;
sub $0xff, %edx
icount++;
cmp %esi, %edx
icount++;
jle <L1>
icount++;
mov $0x1, %edi
icount++;
add $0x10, %eax
...

所以 inscount0 的用途就很明显了,每条指令前都调用 docount 函数将全局变量 icount 自增,最后通过PIN_AddFiniFunction 函数注册的 Fini 函数(25行)将结果写到一个文件中。

当这些函数都定义完后,就可以使用 PIN_StartProgram 来启动程序了

这里分析了一个最简单的 pintool 例子,更多 pintool 例子的分析和其他函数的使用可以参考 BrieflyX 的 博客

pin in CTF

因为动态插桩有不重新编译即可收集二进制程序某些信息的特性,因此用 pin 求解一部分 CTF challenges 会异常的方便,下边给出一些例子和分析

NDH2K13-crackme-500

首先用常规方法对 crackme 文件进行分析

NDH2k13-crackme-500 [master] file crackme 
crackme: ELF 64-bit LSB executable, x86-64, invalid version (SYSV), for GNU/Linux 2.6.9, statically linked, corrupted section header size
NDH2k13-crackme-500 [master] nm ./crackme 

nm: out of memory allocating 109524665216 bytes after a total of 0 bytes
NDH2k13-crackme-500 [master] objdump -d ./crackme -M intel
objdump: ./crackme: 不可识别的文件格式
NDH2k13-crackme-500 [master] ./crackme 
Jonathan Salwan loves you <3
----------------------------

Password: test
Bad password

发现 section header 受到了损坏,用 IDA 打开时也有很多报错,这时我们尝试使用 intel pin 来求解这道题目,先使用最常见的统计指令条数的方法

NDH2k13-crackme-500 [master] ~/pin-3.6-gcc-linux/pin -t ./inscount0.so -- ./crackme <<< "a" >> /dev/null; cat inscount.out 
Count 160345
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "a" 
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 163218
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "aa"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 166014
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "aaa"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 168810
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "aaaa"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 171606
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "aaaaa"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 174402
NDH2k13-crackme-500 [master●] bpython
bpython version 0.17.1 on top of Python 2.7.13+ /usr/bin/python2
>>> 174402 - 171606 == 171606 - 168810 == 168810 - 166014 == 166014 - 163218
True
>>> 

此时我们发现了一个很有趣的特性,输入长度每次增加 1 时,指令条数也是以等差的规模递增的

myInscount0 是我在 inscount0 的基础上更改的 pintool,实现了从结果保存到文件到结果输出到标准输出的修改

我们写一个简单的脚本查看输入长度递增时,指令条数的变化规律

NDH2k13-crackme-500 [master] cat guessLen.py 
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from subprocess import Popen, PIPE
from sys import argv
import string

pinPath = "/home/m4x/pin-3.6-gcc-linux/pin"
pinInit = lambda tool, elf: Popen([pinPath, '-t', tool, '--', elf], stdin = PIPE, stdout = PIPE)
pinWrite = lambda cont: pin.stdin.write(cont)
pinRead = lambda : pin.communicate()[0]

if __name__ == "__main__":
    last = 0
    for i in xrange(1, 30):
        pin = pinInit("./myInscount0.so", "./crackme")
        pinWrite("a" * i + '\n')
        now = int(pinRead().split("Count: ")[1])

        print "inputLen({:2d}) -> ins({}) -> delta({})".format(i, now, now - last)
        last = now

在我电脑上运行结果如下:

NDH2k13-crackme-500 [master] python guessLen.py 
inputLen( 1) -> ins(160307) -> delta(160307)
inputLen( 2) -> ins(163103) -> delta(2796)
inputLen( 3) -> ins(165899) -> delta(2796)
inputLen( 4) -> ins(168695) -> delta(2796)
inputLen( 5) -> ins(171491) -> delta(2796)
inputLen( 6) -> ins(174287) -> delta(2796)
inputLen( 7) -> ins(177083) -> delta(2796)
inputLen( 8) -> ins(182804) -> delta(5721)
inputLen( 9) -> ins(182676) -> delta(-128)
inputLen(10) -> ins(185472) -> delta(2796)
inputLen(11) -> ins(188268) -> delta(2796)
inputLen(12) -> ins(191064) -> delta(2796)
inputLen(13) -> ins(193860) -> delta(2796)
inputLen(14) -> ins(196656) -> delta(2796)
inputLen(15) -> ins(199452) -> delta(2796)
inputLen(16) -> ins(202248) -> delta(2796)
inputLen(17) -> ins(205044) -> delta(2796)
inputLen(18) -> ins(207840) -> delta(2796)
inputLen(19) -> ins(210636) -> delta(2796)
inputLen(20) -> ins(213432) -> delta(2796)
inputLen(21) -> ins(216228) -> delta(2796)
inputLen(22) -> ins(219024) -> delta(2796)
inputLen(23) -> ins(221820) -> delta(2796)
inputLen(24) -> ins(224616) -> delta(2796)
inputLen(25) -> ins(227412) -> delta(2796)
inputLen(26) -> ins(230208) -> delta(2796)
inputLen(27) -> ins(244188) -> delta(13980)
inputLen(28) -> ins(244188) -> delta(0)
inputLen(29) -> ins(244188) -> delta(0)

可以发现,在输入长度 <8 时,指令条数是递增的,但长度为 8 与长度为 7 比较发生了突变,这个时候我们就可以大胆的推测当输入长度为 8 时,程序的运行流程有了较大的变化,正确的 flag 长度即为 8

我们以输入长度是 8 为前提,再查看不同输入下指令条数的变化规律

NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< ">???????"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 185714
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "????????"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 185714
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "@???????"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 185714
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "A???????"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 189752
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "B???????"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 185714
NDH2k13-crackme-500 [master●] ~/pin-3.6-gcc-linux/pin -t ./myInscount0.so -- ./crackme <<< "C???????"
Jonathan Salwan loves you <3
----------------------------

Password: Bad password
Count: 185714

可以发现,输入以 ASCII码 顺序递增时,第一位为 A 时指令条数发生了变化,此时我们在进一步推测正确的 flag 第一位即为 A

再写一个脚本逐位爆破

NDH2k13-crackme-500 [master] cat guessPWD.py 
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from subprocess import Popen, PIPE
from sys import argv
import string
import pdb

pinPath = "/home/m4x/pin-3.6-gcc-linux/pin"
pinInit = lambda tool, elf: Popen([pinPath, '-t', tool, '--', elf], stdin = PIPE, stdout = PIPE)
pinWrite = lambda cont: pin.stdin.write(cont)
pinRead = lambda : pin.communicate()[0]

if __name__ == "__main__":
    last = 0
    #  dic = map(chr, xrange(0x20, 0x80))
    dic = string.ascii_letters + string.digits + "+_ "
    pwd = '?' * 8
    dicIdx = 0
    pwdIdx = 0

    while True:
        pwd = pwd[: pwdIdx] + dic[dicIdx] + pwd[pwdIdx + 1: ]
        #  pdb.set_trace()
        pin = pinInit("./myInscount1.so", "./crackme")
        pinWrite(pwd + '\n')
        now = int(pinRead().split("Count: ")[1])

        print "input({}) -> now({}) -> delta({})".format(pwd, now, now - last)

        if now - last > 2000 and dicIdx:
            pwdIdx += 1
            dicIdx = -1
            last = 0
            if pwdIdx >= len(pwd):
                print "Found pwd: {}".format(pwd)
                break

        dicIdx += 1
        last = now

运行结果如下

NDH2k13-crackme-500 [master●] time python guessPWD.py 
input(a???????) -> now(182804) -> delta(182804)
input(b???????) -> now(182804) -> delta(0)
input(c???????) -> now(182804) -> delta(0)
input(d???????) -> now(182804) -> delta(0)
input(e???????) -> now(182804) -> delta(0)
input(f???????) -> now(182804) -> delta(0)
input(g???????) -> now(182804) -> delta(0)
input(h???????) -> now(182804) -> delta(0)
input(i???????) -> now(182804) -> delta(0)
input(j???????) -> now(182804) -> delta(0)
input(k???????) -> now(182804) -> delta(0)
input(l???????) -> now(182804) -> delta(0)
input(m???????) -> now(182804) -> delta(0)
input(n???????) -> now(182804) -> delta(0)
input(o???????) -> now(182804) -> delta(0)
input(p???????) -> now(182804) -> delta(0)
input(q???????) -> now(182804) -> delta(0)
......
input(AzI0wBsO) -> now(211070) -> delta(0)
input(AzI0wBsP) -> now(211069) -> delta(-1)
input(AzI0wBsQ) -> now(211069) -> delta(0)
input(AzI0wBsR) -> now(211069) -> delta(0)
input(AzI0wBsS) -> now(211069) -> delta(0)
input(AzI0wBsT) -> now(211069) -> delta(0)
input(AzI0wBsU) -> now(211069) -> delta(0)
input(AzI0wBsV) -> now(211069) -> delta(0)
input(AzI0wBsW) -> now(211069) -> delta(0)
input(AzI0wBsX) -> now(214976) -> delta(3907)
Found pwd: AzI0wBsX
python guessPWD.py  31.04s user 14.72s system 105% cpu 43.341 total

验证一下

NDH2k13-crackme-500 [master●] ./crackme 
Jonathan Salwan loves you <3
----------------------------

Password: AzI0wBsX
Good password

这样,我们用不到 5 分钟的时间就猜出了 flag

inscount1(BB级插桩) 与 inscount0(ins级插桩) 效果相同,但 inscount1 速度更快,实际解题时可以用 inscount1 代替 inscount0

hxpCTF-2017-main_strip

再以 hxpCTF2017 的一道题目演示改造 pintool 用于解题,我们着重演示改造 pintool 的步骤,因此恢复符号表和分析程序流程的部分可以参考这篇 writeup

我们先尝试用上例的方法解这道题目

hxpCTF-2017-main_strip [master●●] ~/pin-3.6-gcc-linux/pin -t ./myInscount1.so -- ./main_strip a
Nope.
Count: 517715
hxpCTF-2017-main_strip [master●●] ~/pin-3.6-gcc-linux/pin -t ./myInscount1.so -- ./main_strip a 
Nope.
Count: 545828
hxpCTF-2017-main_strip [master●●] ~/pin-3.6-gcc-linux/pin -t ./myInscount1.so -- ./main_strip a
Nope.
Count: 532656
hxpCTF-2017-main_strip [master●●] ~/pin-3.6-gcc-linux/pin -t ./myInscount1.so -- ./main_strip a
Nope.
Count: 524544
hxpCTF-2017-main_strip [master●●] ~/pin-3.6-gcc-linux/pin -t ./myInscount1.so -- ./main_strip a
Nope.
Count: 582401

很不幸,即使我们使用同一个输入,指令数也是有较大变化的,使用现有的 pintool 难以解出这道题目,我们进行更深一步的分析,验证 flag 的关键部分可以表示为如下代码

for (int i=0; i<length(provided_flag); i++)
{
    if (main_mapanic(provided_flag[i]) != constant_binary_blob[i])
    {
        bad_boy();
        exit();
    }
    goodboy();
}

可以看出判断相等是逐位进行的,因此可以考虑对 inscount0 的 docount 函数做如下更改

更改前:
VOID docount() { icount++; }
更改后:
VOID docount(void *ip) 
{
    // .text:000000000047B96E  cmp al, cl; cmp mapanic(provided_flag[i]), constant_binary_blob[i]
    if ((long long int)ip == 0x000000000047B96E)
     icount++; 
}

只有运行到 0x47B96E 一句才会计数,这样我们就可以根据 pintool 的结果来逐位爆破 flag 了

然而,因为该题目的指令较多,指令级别的插桩会耗费较长时间,需要1h左右才能得到 flag

题目

  • xman2018选拔赛-ollvm

总结

  • 使用 intel pin 可以解一部分 CTF challenges,尤其是虚拟机或者混淆严重的逆向题目,但 pin 的用途绝不局限于求解 CTF challenges
  • 使用 pin 可以解一部分 CTF 题目,但也仅仅是一部分题目,多数题目由于插桩代价大,难以提取侧信道信息,pintool 难以编写等原因使用 pin 求解得不偿失,因此使用 pin 求解 CTF challenges 可以总结为下条原则:
    • 能用血赚,凉了不亏

Reference