intel Pin:動態二進位制插樁的安裝和使用,以及如何開發一個自己的Pintool

2022-12-08 15:02:00

先貼幾個你可能用得上的連結

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檔案查詢