2005年11月13日

普通のやつらの下を行け: ptrace で実行中のプロセスにちょっかいを出す

Linux などの多くの Unix 的なOS には ptrace というシステムコールがあります。 ptrace を使うと実行中のプロセスに対して、レジスタの書き換えやメモリ上のデータの書き換えといったさまざまな操作を行うことができます。

普通のやつらの下を行けの第6回として、今回は ptrace を使って実行中のプロセスにちょっかいを出す方法を取り上げたいと思います。

 

ptrace とは

デバッガの理論と実装 に次のような記述があります。

Unix の ptrace() は本物のデバッガ API (アプリケーションプログラムインターフェイス) の一例であり、商品に相応しい品質を持ったデバッガをサポートするために設計された、最初の専用 API の1つである。

ptrace はデバッガ用に作られた API のようですが、使い方によっては他にもおもしろい用途があるかもしれません。

実行中のプロセスにちょっかいを出す

それでは、実行中のプロセスにちょっかいを出すプログラムを ptrace を使って書いてみたいと思います。実は、今回やることは 鵜飼さん作の livepatch でも同じことができるのですが、ptrace の要点を押さえるために必要最低限の小さいプログラムを書くことにしました。

まず、次のような罪のないプログラムを作ります。

#include <stdio.h>

int
main ()
{
    while (1) {
        printf("hello, world\n");
    }
    return 0;
}

このプログラムを実行すると、 hello, world という行が延々と表示されます。 今回の目的は、このプログラムのメッセージを途中から hippo, world に変えてしまおうというものです。

このプログラムを gcc -o hello-loop hello-loop.c でコンパイルして objdump コマンドで覗いてみると hello, world という文字列のアドレスがわかります。

% objdump -s hello-loop |grep -C1 hello
Contents of section .rodata:
 80484bc 03000000 01000200 68656c6c 6f2c2077  ........hello, w
 80484cc 6f726c64 0a00                        orld..

どうやら .rodata セクションの 0x80484c4 から hell から始まる文字列が出現しているようです。hell の 16進数での表現は 68656c6c です。

それでは、 ptrace を使ったプログラムを書いてみましょう。次のプログラムは ptrace の PTRACE_POKEDATA 命令を使って、任意のプロセスの任意のアドレスの値をワード単位で変更するものです。

#include <assert.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int
main (int argc, char **argv)
{
    assert(argc == 4);
    pid_t pid = atoi(argv[1]);
    void *addr = (void *)strtol(argv[2], NULL, 0);
    void *word = (void *)strtol(argv[3], NULL, 0);

    assert(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == 0);
    wait(NULL);
    assert(ptrace(PTRACE_POKEDATA, pid, addr, word) == 0);
    assert(ptrace(PTRACE_DETACH, pid, NULL, NULL) == 0);
    return 0;
}

このプログラムは gcc -o ptrace-pokedata ptrace-pokedata.c でコンパイルします。

いよいよ、実験に移ります。まず、hello-loop を実行します。

% ./hello-loop
hello, world
hello, world
hello, world
...

次に、別の端末から ps コマンドを実行してプロセス ID を調べます。

% ps a |grep hello-loop
17474 pts/8    S+     0:07 ./hello-loop

ptrace-pokedata はデータをワード単位、手元の環境では 32ビット単位で書き込みます。 ここでは hello を hippo に変えるために、 hipp の4バイトを指定します。hipp の 16進数での表現 (0x68697070) は od -x で調べられます。

% echo -n 'hipp' |od -x
0000000 6968 7070
0000004

これでようやく ptrace-pokedata を実行する準備が整いました。コマンドラインから次のように実行します。プロセスID、アドレス、データの順に引数を指定します。

% ./ptrace-pokedata 17474 0x80484c4 0x68697070

すると、別の端末で実行中だった hello-loop の表示が次のように変わりました。

ppiho, world
ppiho, world
ppiho, world
...

hippo のつもりが、 ppiho になってしまいました。これは x86 がリトルエンディアンのためです。0x70706869 のようにバイトオーダーをひっくり返せば無事、 hippo, world になります。

hippo, world
hippo, world
hippo, world
...

ところで、 hello, world という文字列は .rodata 、つまりリードオンリーのセクションに入っていたはずです。 .rodata 内のデータを書き換えようとすると通常は segmentation fault が起こるはずですが、 ptrace 経由では問題なく書き換えられます。ptrace の操作はカーネル内で行われるため、ユーザスペースではできないことも平気でできてしまいます。

まとめ

今回は ptrace を使って実行中のプロセスにちょっかいを出す方法を取り上げました。ptrace を使えばデータだけでなくコードの部分も書き換え可能なので、お互いのコードを書き換えながら協調的に動作するといった変態的なクライアント・サーバ型のプログラムを書くこともできると思います。ptrace のその他の使い方については livepatch が参考になります。

おまけ: livepatch での方法

livepatch を使って ptrace-pokedata と同じことを行うには次のように実行します。 livepatch はパッチ操作のためのミニ言語を内蔵しています。

% echo 'set 0x80484c4 int 0x70706968' | ./livepatch 17474 ./hello-loop

livepatch は 1日のハックで完成したそうですが、それまでに BFD や ptrace を使ったことはほとんどなかったとのことです。さすがアルファバイナリアンです。