ptraceを使ってみよう!

 「たのしいバイナリの歩き方」では4章で簡易的なデバッガの作成を解説しています。ただWindows環境での話でした。ptraceを使うことで、同じようなことをLinux環境(Ubuntu 12.04.2)でも実現できます。基本的な仕組みはWindowsのときと同じです。

 試しにやってみましょう。

// dbg.c
// $ gcc -Wall dbg.c -o dbg

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/user.h>
#include <errno.h>

void err(char *str)
{
    fprintf(stderr, "ERROR: %s\n", str);
}

void target(char *argv[], char *argp[])
{
    if(ptrace(PTRACE_TRACEME, 0, NULL, NULL) != -1)
        execve(argv[0], argv, argp);
    else
        err("PTRACE_TRACEME");
    exit(0);
}

void controler(int pid)
{
    int status;
    struct user_regs_struct regs;

    while(1){
        waitpid(pid, &status, 0);
        if(WIFEXITED(status))
            break;
        ptrace(PTRACE_GETREGS, pid, 0, &regs);
        printf("%08x: \n", (unsigned int)regs.eip);
        ptrace(PTRACE_SINGLESTEP, pid, 0, NULL);
    }
}

int exec_prog(char *argv[], char *argp[])
{
    int pid;

    switch(pid = fork())
    {
    case 0:
        target(argv, argp);
        break;
    case -1:
        err("FORK");
        break;
    default:
        controler(pid);
        break;
    }
    return 0;
}

int main(int argc, char *argv[], char *argp[])
{
    if(argc < 2){
        fprintf(stderr, "%s <args>\n", argv[0]);
        return 1;
    }

    argv++;
    exec_prog(argv, argp);
    return 0;
}
// target.c
// $ gcc target.c -o target

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    if(argc != 2){
        printf("%s <PASSWORD>\n", argv[0]);
        return 1;
    }
    if(strcmp("BINARY", argv[1]) == 0)
        printf("OK!\n");
    else
        printf("ERR\n");
    return 0;
}
$ gcc target.c -o target
$ gcc -Wall dbg.c -o dbg
$ ./dbg target > eiplog
$ cat eiplog | more
b77551d0:
b77551d2:
b7758c80:
b7758c81:
b7758c83:
b7758c84:
(省略)

 4章まで読まれた方なら動作概要は容易に理解できるかと思います。基本的に、ptrace関数に渡す第1引数によって動作が異なります。

PTRACE_ATTACH 任意のプロセスにアタッチ
PTRACE_DETACH デバッギからデタッチ
PTRACE_KILL デバッギを強制終了
PTRACE_TRACEME 自身がデバッギになる
PTRACE_PEEKDATA (デバッギの)メモリ空間を読む
PTRACE_POKEDATA (デバッギの)メモリ空間へ書く
PTRACE_GETREGS (デバッギの)レジスタを取得する
PTRACE_SYSCALL 実行し、システムコールの前後で停止する
PTRACE_SINGLESTEP 実行し、1命令処理後に停止する

 ここに挙げた以外にもめちゃくちゃたくさんあるのですが、説明しきれないので詳細はman ptraceを見てください。これをベースに作り込むと、いろいろと便利なものが作れます。2年ほど前に私が書いたものもぜひ参考にしてみてください。