Intel Pinでlivestringsを作ってみる

「Intel Pinでcmptraceを作ってみる」では、x86/x64用のDBIツールであるIntel Pinを使ってcmp命令のトレースを行うコードを書いた。 ここでは、同様にIntel Pinを使い、メモリ上に展開されているASCII文字列を列挙するlivestringsを作ってみる。

環境

Ubuntu 14.04.4 LTS 64bit版

$ uname -a
Linux vm-ubuntu64 3.19.0-25-generic #26~14.04.1-Ubuntu SMP Fri Jul 24 21:16:20 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.4 LTS
Release:        14.04
Codename:       trusty

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4

なお、Intel Pinの実行ファイルが32 bit用バイナリであるため、ここでは「64 bitのUbuntu Linuxで32 bitの実行ファイルを動かす方法のメモ」に書いた方法で32 bit用バイナリも実行できるようにしてある。

プログラムコードを書いてみる

「Intel Pinを使ってみる」でも取り上げたpinatraceのコードを参考に、読み書きが行われたメモリアドレスにある4文字以上の文字列を表示するコードを書くと次のようになる。

// livestrings.cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include "pin.H"

const int MIN_STR_LENGTH = 4;
int nullfd;

int IsValidPointer(char *addr)
{
    if (write(nullfd, addr, 1) < 0) {
        return 0;
    }
    return 1;
}

int GetPrintableLength(char *addr)
{
    int i = 0;
    while (IsValidPointer(&addr[i]) && isprint(addr[i])) {
        i++;
    }
    return i;
}

VOID RecordMemRead(VOID *ip, char *addr)
{
    int i;
    int len = GetPrintableLength(addr);
    if (len >= MIN_STR_LENGTH) {
        for (i=0; i<len; i++) {
            fprintf(stderr, "%c", addr[i]);
        }
        fprintf(stderr, "\n");
    }
}

VOID RecordMemWrite(VOID *ip, char *addr)
{
    int i;
    int len = GetPrintableLength(addr);
    if (len >= MIN_STR_LENGTH) {
        for (i=0; i<len; i++) {
            fprintf(stderr, "%c", addr[i]);
        }
        fprintf(stderr, "\n");
    }
}

VOID Instruction(INS ins, VOID *v)
{
    UINT32 memOperands = INS_MemoryOperandCount(ins);

    for (UINT32 memOp = 0; memOp < memOperands; memOp++) {
        if (INS_MemoryOperandIsRead(ins, memOp)) {
            INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordMemRead, IARG_INST_PTR, IARG_MEMORYOP_EA, memOp, IARG_END);
        }
        if (INS_MemoryOperandIsWritten(ins, memOp)) {
            INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)RecordMemWrite, IARG_INST_PTR, IARG_MEMORYOP_EA, memOp, IARG_END);
        }
    }
}

int main(int argc, char *argv[])
{
    PIN_Init(argc, argv);
    nullfd = open("/dev/null", O_WRONLY);
    INS_AddInstrumentFunction(Instruction, 0);
    PIN_StartProgram();
    return 0;
}

上のコードにおいて、RecordMemRead、RecordMemWrite関数は読み書きされたメモリアドレスの直後にあるASCII文字列を標準エラー出力に表示する関数である。 また、IsValidPointer関数は与えられたアドレスが読み取り可能なアドレスかどうかを返す関数である。

コードをコンパイルして実行してみる。

$ make livestrings.test TARGET=intel64

$ ../../../pin -t obj-intel64/livestrings.so -- /bin/ls 2>livestrings.log
buffer_linux.cpp          emudiv.cpp             fork_jit_tool.cpp  inscount_tls.cpp  livestrings.log  obj-intel64           stack-debugger-tutorial.sln              thread_unix.c
buffer_windows.cpp        fibonacci.cpp          imageload.cpp      invocation.cpp    makefile         pinatrace.cpp         stack-debugger-tutorial.vcxproj          thread_win.c
countreps.cpp             follow_child_app1.cpp  inscount0.cpp      isampling.cpp     makefile.rules   proccount.cpp         stack-debugger-tutorial.vcxproj.filters  w_malloctrace.cpp
detach.cpp                follow_child_app2.cpp  inscount1.cpp      itrace.cpp        malloc_mt.cpp    replacesigprobed.cpp  statica.cpp
divide_by_zero_linux.c    follow_child_tool.cpp  inscount2.cpp      little_malloc.c   malloctrace.cpp  safecopy.cpp          staticcount.cpp
divide_by_zero_windows.c  fork_app.cpp           inscount.out       livestrings.cpp   nonstatica.cpp   stack-debugger.cpp    strace.cpp

$ head livestrings.log
x86_64
x86_64
86_64
6_64
XDG_SESSION_ID=1
TERM=screen-256color-bce
SHELL=/bin/bash
SSH_CLIENT=192.168.56.1 50977 22
OLDPWD=/home/user/tmp
SSH_TTY=/dev/pts/0

上の結果から、実行時にメモリ上に展開されているASCII文字列が出力されていることがわかる。

使い道について

たとえば、入力したパスワード文字列を何らかの文字列と比較し、認証の成否を判定する次のようなコードを考える。

/* test.c */
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char buf[256];
    char secret[] = "~bcyucyuyoixo~";
    printf("Enter password: ");
    scanf("%s", buf);
    memfrob(secret, strlen(secret));
    if (strcmp(buf, secret) == 0) {
        puts("auth succeeded!");
    } else {
        puts("auth failed.");
    }
    return 0;
}

上のコードをコンパイルし、livestringsを適用してみると、実際に比較されている文字列が出力できていることがわかる。

$ gcc test.c -o test

$ ../../../pin -t obj-intel64/livestrings.so -- ./test 2>livestrings.log
Enter password: AAAA
auth failed.

$ cat livestrings.log
x86_64
x86_64
86_64
6_64
(snip)
AAAA
THIS_IS_SECRET
AAAA
THIS_IS_SECRET
(snip)

$ ./test
Enter password: THIS_IS_SECRET
auth succeeded!

関連リンク