Giới thiệu Intel Pin Binary Instrumentation Tool

Intel Pin

Instrumentation system được hiểu là hệ thống đo đạc, theo dõi và điều khiển một tiến trình. Pin là một binary instrumentation system (do Intel xây dựng ) cho phép người sử dụng có thể inject các đoạn mã kiểm tra (tạm gọi là introspection code) vào một vị trí bất kỳ trong một tiến trình đang chạy. Các đoạn mã kiểm tra này có tác dụng quan sát hành vi của một tiến trình. Các tham số có thể quan sát bao gồm nội dung của tất cả các thanh ghi và giá trị của một ô nhớ bất kì tại một thời điểm. Introspection code được xây dựng dựa trên Intel Pin framework, được gọi là Pin tool. Pin tool được xây dựng để phục vụ nhiều mục đích khác nhau như: application profiling, phát hiện memory leak, ... Intel pin hỗ trợ cả 2 nền tảng 32bit và 64bit, có thể chạy trên Microsoft Windows, Linux, OSX và Android.

Tổng quan

Instrumentation là gì?

Instrumentation là phương pháp thêm một số đoạn mã trung gian ( tạm gọi là Instrumentation Code - IC) vào chương trình để lấy các thông tin của chương trình trong quá trình thực thi. Để thực hiện Instrumentation, có 2 phương pháp chính đang được sử dụng hiện nay:

  • Source Code Instrumentation (SCI): Tức là thêm IC vào chương trình trước khi biên dịch. Quá trình này chỉ thực hiện được nếu có source code của chương trình. Đây là phương pháp instrumentation mà American Fuzzy Lop (AFL Fuzzer) của lcamtuf đang sử dụng.
  • Binary Instrumentation (BI): thêm IC vào chính file thực thi của chương trình. BI cũng có 2 phương pháp có thể thực hiện:
    • Static Instrumentation: Sử dụng một chương trình disassembler hay emulator để ghi các đoạn mã IC vào trong binary nhưng không làm thay đổi hoạt động của chương trình ban đầu.
    • Dynamic Instrumentation: Chèn IC vào chương trình trong quá trình thực thi, không làm ảnh hưởng tới file binary ban đầu. Đây là cách tiếp cận của Pin framework.

Ví dụ về instrumentation

Một ví dụ về SCI

Trước khi thêm IC

void foo(int x){
    switch (x){
        case 0:
            printf("devel\n");
            break;
        case 1:
            printf("branch\n");
            break;
        default:
            printf("Invalid\n");
            break;
    }
}

Sau khi thêm IC

bool sci[5];

void foo(int x){
    sci[0] = true;
    switch (x){
        case 0:
            sci[1] = true;
            printf("devel\n");
            break;
        case 1:
            sci[2] = true;
            printf("branch\n");
            break;
        default:
            sci[3] = true;
            printf("Invalid\n");
            break;
    }
    sci[4] = true;
}

Ví dụ về BI

count++
xor ebx, ebx
count++
mov eax, 1337h
count++
xor eax, ebx

Ưu điểm và nhược điểm của sử dụng Pin tool ( hoặc các Dynamic Instrumentation Tool khác):

Ưu điểm:

  • Có thể thực hiện instrumentation với bất cứ một chương trình nào.
  • Không cần biết mã nguồn của chương trình.
  • Rất linh động, do có thể tùy trình được ( bằng code) trong quá trình instrumentation

Nhược điểm:

  • Hiệu năng thấp.

Xây dựng Pin tool

Tải Intel Pin framework

Để thực hiện xây dựng một pintool cho riêng mình, cần tải công cụ Intel Pin Framework từ trang chủ của Intel:

https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool

Tại thời điểm viết bài này, Intel đang duy trì 2 phiên bản: Bản 3.0 và bản 2.14.

Xây dựng Tool

Chúng ta nên bắt đầu bằng một ví dụ kinh điển nhất của Pin framework: Simple Instruction Count ( tham khảo source/tools/SimpleExample/icount.cpp và source/tools/ManualExample/inscount0.cpp )

Instruction Counting Tool

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

UINT64 ins_count = 0; // biến lưu lại số lượng instruction đã đi qua 

INT32 Usage() {
    cerr << "This tool prints out the number of dynamic instructions executed to stderr.\n \n";
    cerr << KNOB_BASE::StringKnobSummary();
    cerr << endl;
    return -1;
}

VOID docount() { // analysis routine
    ins_count++; // biến được tăng lên trước khi 1 instruction được thực thi
}

VOID Instruction(INS ins, VOID *v) { // instrumentation routine
    /* hàm được gọi bởi Pin framework
       pin framework sẽ thực hiện các bước như sau:
         - chuyển stack của chương trình về pin stack 
         - lưu lại toàn bộ các thanh ghi
         - gọi hàm docount 
         - khôi phục toàn bộ các giá trị thanh ghi
         - chuyển stack về stack của chương trình 
    */
 
    /*  INS_InsertCall nhận các tham số sau:
            tham số 1 : Instruction được instrument
            tham số 2 : Vị trí của hàm docount được thực thi : 
                         IPOINT_BEFORE trước khi thực hiện lệnh
                         IPOINT_AFTER  sau khi thực hiện lệnh, thường không valid với các lệnh rẽ nhánh
                         IPOINT_TAKEN_BRANCH sau khi thực hiện lệnh rẽ nhánh
            tham số 3 : hàm phân tích, trong ví dụ này là docount
            từ tham số thứ 4 trở đi là các tham số của hàm docount. 
                        Kết thúc của chuỗi này là giá trị IARG_END. Nên tham khảo IARG_TYPE
    */
    INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_END);
}

VOID Fini(INT32 code, VOID *v) { // hàm này được gọi khi đã kết thúc chương trình 
    cout <<  "Count " << ins_count  << endl;
}

/*entry point của pin tool */
int main(int argc, char *argv[])
{
    // phân tích các tham số được truyền vào cho pin tool ( không phải cho chương trình)
    if( PIN_Init(argc,argv) ) {
        return Usage();
    }
    
    // đăng kí hàm callback để instrument 
    INS_AddInstrumentFunction(Instruction, 0);
    // đăng kí hàm callback khi chương trình chính kết thúc
    PIN_AddFiniFunction(Fini, 0);

    // Never returns
    PIN_StartProgram();  // bắt đầu thực thi chương trình và instrument nó
    return 0;
}

Trong trường hợp hàm docount muốn có nhiều tham số đầu vào hơn, thì viết như sau:

/*đọc thêm giá trị của thanh ghi PC mỗi khi thực thi instruction */
VOID docount(VOID * ip ) { // analysis routine
    cout << "pc:"<<ip<<endl;
    ins_count++; // biến được tăng lên trước khi 1 instruction được thực thi
}

VOID Instruction(INS ins, VOID *v) { // instrumentation routine
    INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, IARG_INST_PTR, IARG_END);
}

Trong ví dụ trên IARG_INST_PTR sẽ được pin tool hiểu là truyền tham số Instruction Pointer cho hàm docount. Hàm này cũng phải khai báo sẽ nhận thêm tham số VOID *ip để xử lý.

Một ví dụ khác:

/* Đọc thêm giá trị của thanh ghi PC mỗi khi thực thi instruction 
   Đọc thêm giá trị của thanh ghi EAX (hoặc RAX) khi thực thi
*/
VOID docount(VOID * ip, VOID * gax ) { // analysis routine
    cout << "pc:"<<ip<<endl;
    cout << "GAX:"<<gax<<endl;
    ins_count++; // biến được tăng lên trước khi 1 instruction được thực thi
}

VOID Instruction(INS ins, VOID *v) { // instrumentation routine
    INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)docount, 
                        IARG_INST_PTR,   // tương ứng với tham số VOID * ip
                        IARG_REG_VALUE, REG_GAX, // tương ứng với tham số VOID * gax
                        IARG_END);
}

Trong ví dụ trên, cặp giá trị IARG_REG_VALUE và REG_GAX sẽ được pin hiểu là truyền tham số có nội dung của thanh ghi EAX hoặc RAX cho hàm docount. Hàm này cũng phải khai báo sẽ nhận thêm tham số này.

Có thể sử dụng tường minh là REG_EAX hoặc REG_RAX. Môi trường 32bit, chỉ tồn tại EAX, trên môi trường 64bit mới có cả 2 thanh ghi này ( EAX là 1 phần của RAX). Do đó, để code build không phụ thuộc vào môi trường, nên dùng giá trị REG_GAX.

Build tool

Intel có đưa kèm rất nhiều ví dụ để viết một tool dựa vào Pin.Các tool này nằm trong thư mục source/tools/ManualExample, source/tools/SimpleExample

  • Build với gcc
  • Build với windows: Trang chủ của Intel yêu cầu cài cygwin để build. Tuy nhiên, mình vốn không thích cài nhiều nên mình sẽ hướng dẫn cách build mà không cần cygwin.

Build pin tool với Microsoft Visual Studio

Intel build sẵn pin thành các file lib, ở chế độ Release. Các cấu hình dưới đây sẽ dành cho profile Release trong project của Visual Studio.

Tạo một project có dạng  Win32 DLL

pin0
Giải nén pin tải được vào thư mục pin-windows, cùng cấp với solution của project trên

Thiết lập các Preprocessor tại nhánh C/C++ > Preprocessor như sau:

TARGET_WINDOWS;TARGET_IA32;HOST_IA32;_SECURE_SCL=0;USING_XED;NDEBUG

pin1

Cấu hình Runtime Library là MT: C/C++ > Code Generation:

Multi-threaded (/MT)

pin2

Cấu hình các thư mục chứa file header của pin : C/C++ > General > Additional Include Directories

"$(SolutionDir)..\pin-windows\source\include\pin";"$(SolutionDir)..\pin-windows\source\include\pin\gen";"$(SolutionDir)..\pin-windows\extras\xed-ia32\include";"$(SolutionDir)..\pin-windows\extras\components\include"

pin3

Cấu hình thư mục chứa lib để link: Linker > General > Additional Library Directories

"$(SolutionDir)..\pin-windows\ia32\lib";"$(SolutionDir)..\pin-windows\ia32\lib-ext";"$(SolutionDir)..\pin-windows\extras\xed-ia32\lib"

pin4

Cấu hình thư viện dependency:

Linker > Input > Additional Dependencies

ntdll-32.lib libxed.lib pin.lib pinvm.lib libcmt.lib libcpmt.lib

Linker > Input > Ignore All Default Libraries

Yes (/NODEFAULTLIB)

pin5

Cấu hình Entry point: Linker > Advanced > Entry Point

Ptrace_DllMainCRTStartup@12

pin6

Sử dụng tool

Để sử dụng pin chạy lệnh sau:

pin -t path/to/pintool.so -- path/to/application [application args]

Nếu chạy ở chế độ Inject code, thì sử dụng lệnh sau:

pin -t path/to/pintool.so --pid 1234

Khi chơi CTF, hãy nghĩ đến pin tool

Một ví dụ khiến người ta nghĩ đến instruction counter và pin tool là khi chơi CTF. Một kĩ thuật khá hay trong khi giải các bài CTF dạng reverse engineering là Side channel attack. Thông thường, các bài reverse sẽ có những trick rất khó để vượt qua trong thời gian ngắn. Dạng phổ biến nhất, thường là cho người chơi input chuỗi kí tự, qua nhiều phép biến đổi lằng nhằng sẽ kiểm tra xem có đúng là input hợp lệ hay không. Nếu hợp lệ thì in ra cờ. Phép kiểm tra này thường duyệt qua từng kí tự. Nếu kí tự đó khớp thì mới kiểm tra kí tự kế tiếp. Chính vì vậy, người ta sẽ input từng kí tự (printable characters chẳng hạn) và đếm số lượng instruction đã thực thi đối với kí tự đó. Nếu gặp 1 kí tự có số instruction đột biến so với các kí tự còn lại, đó là kí tự của flag. Writeup của Vulnhub Team đã thể hiện rất rõ điều này (rất may là mình ko phải viết lại vào đây, vì mình lười). Cám ơn Vulnhub Team. : https://ctf-team.vulnhub.com/picoctf-2014-baleful/

Vấn đề hiệu năng

Có một paper rất hay nói về các vấn đề khi build một công cụ phân tích chương trình bằng phương pháp BI, đồng thời, paper có so sánh các framework với nhau:  Building Customized Program Analysis Tools with Dynamic Instrumentation

Trong bài kế tiếp mình sẽ làm rõ hơn về kĩ thuật xây dựng pin tool.