ptrace(2) は Linux を含む Unix 系OS にあるシステムコールで、実行中のプロセスに対して、メモリ上のデータやレジスタの値を抜き出したり、書き換えたりすることができる。
これを使ってごにょごにょすると、実行中の関数とその引数を取り出して、プロセスを止めずにスタックトレースを取得したり、デバッガを作ったり、標準出力を横取りして audit log を取ったり、オンラインでパッチをあてて脆弱性対応したりできる。夢が広がる。
例えば、普通のやつらの下を行け: ptrace で実行中のプロセスにちょっかいを出す では、32bit executable なバイナリに対して、実行中に出力文字列を置き換える例を紹介している。
strace コマンドは ptrace(2) を利用して、システムコールを追って出力している。これについては udzura さんの straceがどうやってシステムコールの情報を取得しているか の記事に情報があった。
参考図書
ptrace 入門のために、書籍またはウェブサイトがないのか探していたのだが、古い時代のものしかみつからない。サンプルも 32bit が基本で、入門したい身なのに色々置き換えながら読まないといけなくてツライ。
と思っていたところ、Learning Linux Binary Analysis という本が良さそうと教えてもらった。2016/2/29 出版で新しい。ptrace(2) についても ELF についても載っていて、欲しかったやつだった。
バイナリやアセンブリ周りは、和書は古いものしかないが、洋書だったら最近出版されているものもあるようで、次は最初から洋書を探そうと思うなどした。
※ この本は基本的にはリバースエンジニアリングの本で、Chapter 4 からは virus がどのようにバイナリが実行可能なまま自分自身を盛り込むのか、virus に injection されたことをどのように発見するのか、という内容になる。興味深いが自分はまだ読んでない。
Packt Publishing (2016-02-29)
売り上げランキング: 69,854
https://www.packtpub.com/networking-and-servers/learning-linux-binary-analysis
Linux システムコールの ABI
Linux のシステムコールを呼び出すには ABI (Application Binary Interface) が決まっていて、CPUの決まったレジスタに値を書き込んで INT 0x80
(Interrupt) 命令を投げると、カーネルに割り込みをしてシステムコールを実行してもらうことができる。
以下のようにアーキテクチャごとに定まっているレジスタに、システムコール番号と引数の値を書き込み、INT 0x80
命令を投げると呼び出すことができる。i386 だと eax レジスタに、x86_64 だと rax レジスタにシステムコール番号を書き込む。
ref. man : syscall(2)
例えば x86_64 アーキテクチャで sys_write して sys_exit するコードをアセンブリで書くと次のようになる。システムコールの番号は linux のヘッダから取ってくる。
ref. Linux で64bitアセンブリプログラミング (01) - hello world
References:
- Linuxでアセンブリプログラミング - 4. Linux カーネルとシステムコール
- Linuxでアセンブリプログラミング - 付録 B. システムコールの仕組み
- 0から作るLinuxプログラム システムコールその1 システムコールの呼び出し
ptrace でシステムコールを追う
ptrace(2)で対象プロセスのシステムコールを追うC言語プログラムはざっくり言うと以下の手順になる
#include <sys/ptrace.h>
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
- システムコール直前、または直後に停止した状態で
int e = ptrace(PTRACE_GETREGS, pid, 0, ®s);
ptrace(PTRACE_DETACH, pid, NULL, NULL);
regs の定義は x86_64 であれば struct user_regs_struct
であり、以下となる。
i386 の場合は、struct i386_user_regs_struct であり、以下となる。
PTRACE_GETREGS でレジスタの値を取り出して、i386 であれば eax レジスタ、x86_64 であれば rax レジスタからシステムコール番号を取り出せるので、どのシステムコールを呼んでいるのかがわかる。
strace はシステムコール番号を取得する以上のことをやっていて、システムコールごとに引数の型がなにかを定義して、適切に値を取り出して表示している。特に、レジスタにポインタのアドレスが書き込まれている場合、アドレスが示すメモリ領域から値を取り出す必要もある。このような処理をシステムコールごとに地道にコードを書いて対応しているとのこと。頭が下がる。
ref. straceがどうやってシステムコールの情報を取得しているか
ELF
ELF は Linux のような Unix 系OSで標準的なバイナリフォーマットで、実行ファイル、共有ライブラリ(.so)、オブジェクトファイル(.o)、コアダンプなどに使われている。
ELFバイナリは、実行するためにメモリに読み込まれた場合でもフォーマットはほとんど変わらないので、ELFバイナリフォーマットについて知識があれば値を取り出せる。このELFフォーマットについては、最初に紹介した「Learning Linux Binary Analysis」で詳細に解説があった。
通常はこのメモリ領域は、データを書き換えようとすると SEGV が起きるわけだけど、ptrace(2) を使うとなんと書き換えることができる。
PTRACE_POKEDATA
を使って hello, world
を hippo, world
に置き換えるサンプルが 普通のやつらの下を行け: ptrace で実行中のプロセスにちょっかいを出すにあったので、やってみると面白い。記事が 32bit 時代のものなので、64 bit に置き換えて動かすのも良い練習になる。
おわりに
ptrace(2)に入門した。これを使いつつさらにごにょごにょすれば、生きているプロセスにアタッチして、Cレベルのスタックトレースを出しつつ、Rubyレベルのスタックトレースを出すなんてこともできるだろう。まぁ、sigdumpでいいんだけど。
FYI: sigdump は対象 ruby プロセスに sigdump gem を入れて require しておかないといけない。ptrace(2) ベースでアプローチすれば、何も入れておく必要はなくなる。ただし、そのツールはCRubyのバイナリレベルの変更に追随する必要がある(´・ω・`)