「WEBRickをGDBでいじる」を実際に試して、バイナリーハックを体験してみよう

DECONで高林さんが紹介された「Binary Hacks in Action」のスライドを見ていて、「WEBRickGDBでいじる」という所がとても興味深く感じました。本当にそんなことができるのか?と一瞬思いました。そこで、ちょっと試してみました。最初はうまくいかなかったのですが、最終的にWEBRickGDBから本当に操作できることを確認しました。うまくいかなかった原因は、パッケージからRubyをインストールいたためのようです。(GDBが理解できるデバッグ情報が、パッケージからインストールしRubyには入っていなかったのが原因だと考えられます。) ソースからビルドしてインストールしたRubyでは、期待通りの結果が得られました。

準備

パッケージからインストールしたRubyでは、WEBRickGDBからうまくいじれなかったので、ソースからRubyをインストールしておきます。configureする時にprefixオプションでホームディレクトリの適当などこかを指定しておけば、root権限が無くても手軽にインストールして試せます。

手順

スライドに載っているWEBRickベースのWebサーバのコードを書きます。

[1] % vim webrick.rb
require 'webrick'
include WEBrick
s = HTTPServer.new(:Port => 2000)
s.mount("/", HTTPServlet::FileHandler, "/tmp", true)
trap("INT") { s.shutdown }
s.start

そして、WEBRickを起動します。

[1] % ruby webrick.rb
[2006-09-17 22:06:57] INFO  WEBrick 1.3.1
[2006-09-17 22:06:57] INFO  ruby 1.8.5 (2006-08-25) [i686-darwin8.5.2]
[2006-09-17 22:06:58] INFO  WEBrick::HTTPServer#start: pid=13645 port=2000

もう1つのターミナル[2]で、index.htmlを用意し、telnetで起動したWEBRickのプロセスに接続します。接続が完了したら、HTTPのGETメソッドで「/」(すなわち、index.html)を取得します。以下のように、index.htmlのコンテンツをうまく取得できることを確認できました。

[2] % echo "hello, world" > /tmp/index.html
[2] % telnet localhost 2000
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET /
hello, world
Connection closed by foreign host.

WEBRickのサーバのプロセスIDを調べて、そのプロセスIDでGDBからそのプロセスにアタッチします。僕の環境にはプロセスIDを簡単に調べられるpgrepはインストールされていないので、適当にコマンドを組み合わせてプロセスIDを取得することにしました。どうやら、うまくWEBRickのサーバにアタッチできたようです。

[3] % pid=`ps au | grep webrick | awk '{ print $2 }'`
[3] % gdb -p $pid
GNU gdb 6.1-20040303 (Apple version gdb-437) (Fri Jan 13 18:45:48 GMT 2006)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-apple-darwin".
Attaching to process 13671.
Reading symbols for shared libraries . done
Reading symbols for shared libraries ............... done
0x9001ab3c in select ()
(gdb) 

そして、rb_io_write関数にブレークポイントをセットします。「c」で再び実行状態にします。

(gdb) b rb_io_write
Breakpoint 1 at 0x257bf: file io.c, line 591.
(gdb) c
Continuing.

この状態で、他のターミナルからtelnetWEBRickのサーバを先ほどと同じように叩きます。そうすると、先ほどとは挙動が違って、GETリクエストを送信しても、WEBRickのサーバから応答がありません。

[2] % telnet localhost 2000
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET /

そこで、GDBを動かしているターミナルを見ると、先ほどセットしたブレークポイントで止まったのがわかります。あとは、スライドの説明にあるように、GDBでrb_io_write関数の第2引数のstrの内容を表示したり、strの内容を改ざんしたりして遊びます。

Breakpoint 1, rb_io_write (io=5368480, str=5362860) at io.c:591
591         return rb_funcall(io, id_write, 1, str);
(gdb) p ((struct RString*)str)->ptr
$1 = 0x112b1d0 "hello, world\n"
(gdb) set ((struct RString*)str)->ptr = "crazy"
(gdb) set ((struct RString*)str)->len = 5
(gdb) c
Continuing.

おわりに

今までGDBを全然使った経験がありませんでしたが、このバイナリーハックのおかげで、GDBの凄さがわかるようになりました。そして、バイナリーハックにはGDBが非常に強力なツールとなることを学びました。

あと、余談ですが、GDBについてちょっと調べてみて、GDBのbtコマンドでスタックトレースを表示したり、任意の呼び出し位置に移動したりする方法を知りました。こんな感じです。GDBを自由自在に使えるようになったら、かなり強い気がします。

(gdb) bt
#0  rb_io_write (io=5368480, str=4) at io.c:591
#1  0x00025802 in rb_io_addstr (io=5368480, str=4) at io.c:613
#2  0x0000c905 in rb_call0 (klass=2020060, recv=5368480, id=333, oid=333, argc=1, argv=0xbfff98d0, body=0x1ecbfc, flags=0) at eval.c:5810
#3  0x0000d451 in rb_call (klass=2020060, recv=5368480, mid=333, argc=1, argv=0xbfff98d0, scope=0) at eval.c:6048
#4  0x0000b341 in rb_eval (self=5368000, n=0x1) at eval.c:3443
#5  0x0000d109 in rb_call0 (klass=5725800, recv=5368000, id=19065, oid=19065, argc=2, argv=0xbfff9d50, body=0x5762c8, flags=2) at eval.c:5954
#6  0x0000d451 in rb_call (klass=5725800, recv=5368000, mid=19065, argc=2, argv=0xbfff9d50, scope=1) at eval.c:6048
#7  0x0000a321 in rb_eval (self=5368000, n=0x1) at eval.c:3458
#8  0x000093af in rb_eval (self=5368000, n=0x1) at eval.c:3097
#9  0x0000d109 in rb_call0 (klass=5725800, recv=5368000, id=19121, oid=19121, argc=4, argv=0xbfffa470, body=0x576458, flags=2) at eval.c:5954
#10 0x0000d451 in rb_call (klass=5725800, recv=5368000, mid=19121, argc=4, argv=0xbfffa470, scope=1) at eval.c:6048
[...]

(gdb) frame 1
#1  0x00025802 in rb_io_addstr (io=5368480, str=4) at io.c:613
613         rb_io_write(io, str);
(gdb) list
608
609     VALUE
610     rb_io_addstr(io, str)
611         VALUE io, str;
612     {
613         rb_io_write(io, str);
614         return io;
615     }
616
617     /*

Rubyソースコード完全解説

Rubyソースコード完全解説