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 されたことをどのように発見するのか、という内容になる。興味深いが自分はまだ読んでない。

Learning Linux Binary Analysis
Learning Linux Binary Analysis
posted with amazlet at 17.05.30
Ryan O'neil 
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 レジスタにシステムコール番号を書き込む。

arch/ABI      syscall# retval arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
──────────────────────────────────────────────────────────────
i386          eax      eax    ebx   ecx   edx   esi   edi   ebp   -
x86_64        rax      rax    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:

ptrace でシステムコールを追う

ptrace(2)で対象プロセスのシステムコールを追うC言語プログラムはざっくり言うと以下の手順になる

  • #include <sys/ptrace.h>
  • ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  • システムコール直前、または直後に停止した状態で
    • int e = ptrace(PTRACE_GETREGS, pid, 0, &regs);
  • ptrace(PTRACE_DETACH, pid, NULL, NULL);

regs の定義は x86_64 であれば struct user_regs_struct であり、以下となる。

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;
};

i386 の場合は、struct i386_user_regs_struct であり、以下となる。

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;
}; 

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のバイナリレベルの変更に追随する必要がある(´・ω・`)