ptrace(2)に入門。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) 命令を投げると、カーネルに割り込みをしてシステムコールを実行してもらうことができる。
rax レジスタにシステムコール番号を書き込み、以下のようにアーキテクチャごとに異なるレジスタに引数の値を書き込み、INT 0x80
命令を投げる
arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
──────────────────────────────────────────────────────────────
i386 ebx ecx edx esi edi ebp -
x86_64 rdi rsi rdx r10 r8 r9 -
ref. man : syscall(2)
例えば x86_64 アーキテクチャで sys_write して sys_exit するコードをアセンブリで書くと次のようになる。システムコールの番号は linux のヘッダから取ってくる。
;------------------------------------
; hellol.s
; nasm -f elf64 hellol.s
; ld -o hellol hellol.o
; ./hellol
;------------------------------------
bits 64
section .text
global _start
_start:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, msg ; address
mov rdx, len ; length (13)
int 0x80
mov rax, 60 ; sys_exit
xor rdi, rdi ; 0
int 0x80
section .data
msg db 'hello, world', 0x0A
len equ $ - msg
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 の定義は struct user_regs_struct regs
であり、user_regs_struct
(x86_64用構造体) は以下のようになっている。
struct user_regs_struct
{
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
unsigned long orig_rax;
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
unsigned long fs_base;
unsigned long gs_base;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
};
つまり、レジスタの値を取り出せるということだ。あとは rax レジスタのシステムコール番号から、どのシステムコールを呼んでいるのかがわかる。なお、i386_user_regs_struct (i386用構造体) は以下のようになっている。
struct i386_user_regs_struct {
uint32_t ebx;
uint32_t ecx;
uint32_t edx;
uint32_t esi;
uint32_t edi;
uint32_t ebp;
uint32_t eax;
uint32_t xds;
uint32_t xes;
uint32_t xfs;
uint32_t xgs;
uint32_t orig_eax;
uint32_t eip;
uint32_t xcs;
uint32_t eflags;
uint32_t esp;
uint32_t xss;
};
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 'sigdump/setup'
しておかないといけない。ptrace(2) ベースでアプローチすれば、何も入れておく必要はなくなる。ただし、そのツールはCRubyのバイナリレベルの変更に追随する必要がある(´・ω・`)