「NetAgent Security Contest 2010 解答 Level5 ~ Level8 (1/2)」
■Level8 x86_64 (FILE)
64ビットのLinuxで動作するサーバプログラムを解析する問題です。右図のIPアドレス:ポートの場所で問題として配布されているサーバプログラムが動いており、そのサーバと適切なデータ交換を行うことで解答パスワードが得られます(競技終了時にポートは閉じられていますので、今はアクセスできません)。
64ビット版ELFはIDAProのフリーウェア版では読み込めないため、objdumpを用いて逆アセンブルしますが、objdumpではなくとも、64ビットELFを逆アセンブルできれば何のツールでも構いません。また、本記事ではUbuntu Linuxの64ビット版を利用して解析しています。gdbやツール等もこの環境での動作確認のみを行っていますので、予めご了承ください。
では、解析していきます。
$ ./level8
./level8 [port]
$ ./level8 5555
$
引数としてポート番号を与えても実行されないため、straceでログを見ます。
$ strace ./level8 5555
(省略)
brk(0) = 0x118a000
brk(0x11ab000) = 0x11ab000
open("/etc/Password", O_RDONLY) = -1 ENOENT
(No such file or directory)
exit_group(1) = ?
/etc/Passwordというファイルがないためopenが失敗しています。該当ファイルを作成し、もう一度実行します。
# cat > /etc/Password
THIS IS LEVEL8 PASSWORD.
[Ctrl+C]
# ./level8 5555
今度はうまくいきました。netcatで開いたポート5555へ接続します。
$ nc localhost 5555
abcdefghijklmnopqrstuvwxyz0123456789
$
適当な入力を受け付けて切断されました。
では、逆アセンブルコードを読んでいきましょう。
$ objdump -d level8 > level8.asm
objdumpの結果をlevel8.asmへ出力し、level8.asmを読みます。
まず、サーバプログラムであるため、socket、accept、forkといった関数が呼ばれていると推測し、これらを検索します。するとforkを読んでいる箇所が401ac6の関数内(アドレス401ad1)で見つかります。よって、この関数を手掛かりに解析を進めます。
// level8.asm
401ad1: callq 400e08 <fork@plt>
401ad6: mov %eax,-0x4(%rbp)
401ad9: mov -0x4(%rbp),%eax
// eax == -1: jmp 401afb;
401adc: cmp $0xffffffffffffffff,%eax
401adf: je 401afb <accept@plt+0xca3>
// eax != 0: jmp 401b07;
401ae1: test %eax,%eax
401ae3: jne 401b07 <accept@plt+0xcaf>
// eax == 0: call 40181c;
401ae5: mov -0x14(%rbp),%eax
401ae8: mov %eax,%edi
401aea: callq 40181c <accept@plt+0x9c4>
401aef: mov -0x14(%rbp),%eax
401af2: mov %eax,%edi
401af4: callq 400c68 <close@plt>
forkの戻り値が-1の場合はエラーです。戻り値が0の場合は子プロセス、0以外の場合は親プロセスの処理で、親プロセスはそのままacceptの処理に戻るため、クライアントと会話する処理に繋がるであろう子プロセス40181cを追います。
// level8.asm
40181c: push %rbp
(省略)
4018dc: callq 400c98 <select@plt>
4018e1: cmp $0xffffffffffffffff,%eax
4018e4: je 40191e <accept@plt+0xac6>
4018e6: test %eax,%eax
4018e8: je 401921 <accept@plt+0xac9>
4018ea: mov -0xb4(%rbp),%eax
4018f0: mov %eax,0x20387a(%rip) # 605170 <stderr+0x30>
4018f6: mov $0x4017eb,%esi
4018fb: mov $0xe,%edi
401900: callq 400d68 <signal@plt>
401905: mov $0xa,%edi
40190a: callq 400d98 <alarm@plt>
40190f: mov -0xb4(%rbp),%eax
401915: mov %eax,%edi
401917: callq 4017c3 <accept@plt+0x96b>
40181c以降を読むとselect関数が呼ばれています。selectが正常に終わると、signal、alarmと続いて4017c3がcallされます。signal関数の引数はシグナル番号、シグナルハンドラのアドレスなので signal(0xe、0x4017eb) となります。0x4017ebはソケットのcloseとプロセスのexitをやっています。
続いて、alarm(0xa) で待ち時間を10秒に設定して4017c3へ進みます。
// level8.asm
4017c3: push %rbp
(省略)
4017d1: mov %eax,%edi
4017d3: callq 401395 <accept@plt+0x53d>
401395を呼び出しているだけですので401395へ進みます。401395以降が子プロセスのメインルーチンで、クライアントと話している箇所です。
さて、このまま読み進めてもよいですが、全体のおおまかな処理の流れを見るために、ptraceを利用したプログラム(execlog1.c)を用いて、重要なポイントに対してある程度の当たりをつけます。execlog1.cはforkにより作成された子プロセスの処理を1命令ずつ追いながら逆アセンブルしていくプログラムです。また、call時とretn時に引数の状態を表示します。逆アセンブルするためのディスアセンブルライブラリであるudis86を利用しています。
$ readelf -a level8
(省略)
[14] .text PROGBITS 0000000000400e70
0000000000002bb8 0000000000000000
level8のコードのみを出力したいので、level8のテキストセクションのアドレスとサイズを調べ、execlog1.exeのTEXT_ADDRESSとTEXT_SIZEに設定して実行します。
$ gcc -Wall execlog1.c -o execlog1 -ludis86
$ ./execlog1 ./level8 5555 > execlog1.txt
この状態で別の端末(or terminal)から5555ポートへアクセスして、ログデータをexeclog1.txtへ出力します。
$ nc localhost 5555
abcdefghijklmnopqrstuvwxyz0123456789
$
これでexeclog1.txtへ逆アセンブルログが出力されました。クライアントから入力したデータはabcdef...であるため、61 62 63...と続くデータが見つかるところを検索します。するとアドレス40102eのcall命令時の第2引数がヒットします。
// execlog1.txt
40102e: call 0xfffffffffffffcba
-- call --------------------------------------------
rdi = 4
rsi = 7ffff86692a0 ( 28 51 40 d3 24 7f 00 00 )
rdx = 24
rcx = 0
r8 = 7ffff86693b0 ( 00 00 00 00 00 00 00 00 )
----------------------------------------------------
-- retn (args) -------------------------------------
rdi = 4
rsi = 7ffff86692a0 ( 61 62 63 64 65 66 67 68 )
rdx = 24
rcx = 0
r8 = 7ffff86693b0 ( 00 00 00 00 00 00 00 00 )
rax = 24
----------------------------------------------------
引数や戻り値からみて、call先はおそらくrecv関数か、それに類するものだと考えられます。この61 62 63...をキーワードにさらに処理を追っていくと、以下のコードに辿り着きます。
// execlog1.txt
40146e: call 0xa30
-- call --------------------------------------------
rdi = 7ffff86692a2 ( 63 64 65 66 67 68 69 6a )
rsi = 4
rdx = 7ffff86692a6 ( 67 68 69 6a 6b 6c 6d 6e )
rcx = 7ffff86692a6 ( 67 68 69 6a 6b 6c 6d 6e )
r8 = 1e
----------------------------------------------------
401e9e: push rbp
関数401e9eは第1引数に63以降のデータ、そして第3引数と第4引数に67以降のデータがあります。第2引数は4で、第5引数は1eであるため、4 + 1e = 22で、クライアントから送ったデータはabcdefghijklmnopqrstuvwxyz0123456789の24バイトです。61から送っているため、先頭の2バイト分を加算するとちょうど2 + 4 + 1e = 24となります。つまり、クライアントから受け取ったデータの先頭から3バイト目からの4バイト分が第1引数、7バイト目からの1eバイト分が第3引数と第4引数になっています。
そして、この関数が終了した段階で、与えられた引数は以下に変化しています。
// execlog1.txt
401f3d: ret
-- retn (args) -------------------------------------
rdi = 7ffff86692a2 ( 63 64 65 66 af 4f 86 49 )
rsi = 4
rdx = 7ffff86692a6 ( af 4f 86 49 4d 10 30 09 )
rcx = 7ffff86692a6 ( af 4f 86 49 4d 10 30 09 )
r8 = 1e
rax = 0
----------------------------------------------------
401473: lea rax, [rbp-0x220]
以上から、関数401e9eは67 68 69 6a 6b 6c 6d 6eからaf 4f 86 49 4d 10 30 09へ暗号化/復号、あるいはデコードといった処理を行う関数だと考えられます。また、63 64 65 66はそのためのキーであると推測できます。
世の中に出回っているメジャーな暗号アルゴリズムは限られているため、ひとつひとつ試していってもよいですし、関数401e9eを読み進めて解読してもよいでしょう。重要なのは、このアルゴリズムがどのような特徴を持っているかを調べることです。
objdumpの出力結果であるlevel8.asmを読み、401e9e以降の処理を解析すると、このアルゴリズムはRC4であると分かります。そして、RC4は暗号化と復号を同じアルゴリズムで実現します。つまり、63 64 65 66をキーとしてaf 4f 86 49 4d 10 30 09を再び関数401e9eへ渡すと67 68 69 6a 6b 6c 6d 6eに戻ります。関数401e9eはRC4であり、暗号化と復号を同じアルゴリズムで実現するという事実を調べ上げることがlevel8を解く最初の課題となります。
execlog1.txtへ出力された実行結果を確認する限りにおいては、401e9eは2か所から呼ばれています。もう一か所はアドレス4014a5です。
// execlog1.txt
4014a5: call 0x9f9
-- call --------------------------------------------
rdi = 7ffff86694a0 ( aa aa aa aa aa aa aa aa )
rsi = 8
rdx = 7ffff86692b0 ( fd b1 01 8a 64 b6 52 3b )
rcx = 7ffff86692b0 ( fd b1 01 8a 64 b6 52 3b )
r8 = 14
----------------------------------------------------
401e9e: push rbp
(省略)
401f3d: ret
-- retn (args) -------------------------------------
rdi = 7ffff86694a0 ( aa aa aa aa aa aa aa aa )
rsi = 8
rdx = 7ffff86692b0 ( 1e d9 6e 6d 26 8e 83 7c )
rcx = 7ffff86692b0 ( 1e d9 6e 6d 26 8e 83 7c )
r8 = 14
rax = 0
----------------------------------------------------
4014aa: mov rax, [rbp-0x248]
今度はaa aa aa aa aa aa aa aaという8バイトのキーで7ffff86692b0以降の14バイトを復号しています。
クライアントからのデータは7ffff86692a0に置かれているので、1回目の呼び出しで7ffff86692b2からの4バイトをキーとして7ffff86692b3以降の1eバイトを復号、次の2回目の呼び出しでaa aa aa aa aa aa aa aaの8バイトをキーとして7ffff86692b0からの14バイトを復号、という流れです。
結果的に右図のようになります。赤い部分がキー、青い部分が復号の対象となるデータです。
復号処理が終わると、次のルーチンへ進みます。
// level8.asm
4014b1: add $0x4,%rax
4014b5: mov %rax,%rdi
4014b8: callq 4012eb <accept@plt+0x493>
4014bd: test %eax,%eax
4014bf: je 4014cb <accept@plt+0x673>
4014c1: mov $0xffffffff,%eax
4014c6: jmpq 4017a5 <accept@plt+0x94d>
2度目に復号されたデータからさらに4バイト進んだアドレスを第1引数として4012ebを呼び出します。戻り値が0ではないなら、エラーとして4017a5へジャンプします。
// level8.asm
4012eb: push %rbp
4012ec: mov %rsp,%rbp
4012ef: sub $0x40,%rsp
4012f3: mov %rdi,-0x38(%rbp)
4012f7: mov %fs:0x28,%rax
401300: mov %rax,-0x8(%rbp)
401304: xor %eax,%eax
401306: movb $0xaa,-0x20(%rbp)
40130a: movb $0xaa,-0x1f(%rbp)
40130e: movb $0xaa,-0x1e(%rbp)
401312: movb $0xaa,-0x1d(%rbp)
401316: movb $0xaa,-0x1c(%rbp)
40131a: movb $0xaa,-0x1b(%rbp)
40131e: movb $0xaa,-0x1a(%rbp)
401322: movb $0xaa,-0x19(%rbp)
401326: movb $0xaa,-0x18(%rbp)
40132a: movb $0xaa,-0x17(%rbp)
40132e: movb $0xaa,-0x16(%rbp)
401332: movb $0xaa,-0x15(%rbp)
401336: movb $0xaa,-0x14(%rbp)
40133a: movb $0xaa,-0x13(%rbp)
40133e: movb $0xaa,-0x12(%rbp)
401342: movb $0xaa,-0x11(%rbp)
401346: movl $0x0,-0x24(%rbp)
40134d: jmp 401374 <accept@plt+0x51c>
40134f: mov -0x24(%rbp),%eax
401352: cltq
401354: movzbl -0x20(%rbp,%rax,1),%edx
401359: mov -0x24(%rbp),%eax
40135c: cltq
40135e: add -0x38(%rbp),%rax
401362: movzbl (%rax),%eax
401365: cmp %al,%dl
401367: je 401370 <accept@plt+0x518>
401369: mov $0xffffffff,%eax
40136e: jmp 40137f <accept@plt+0x527>
401370: addl $0x1,-0x24(%rbp)
401374: cmpl $0xf,-0x24(%rbp)
401378: jle 40134f <accept@plt+0x4f7>
40137a: mov $0x0,%eax
40137f: mov -0x8(%rbp),%rdx
401383: xor %fs:0x28,%rdx
40138c: je 401393 <accept@plt+0x53b>
40138e: callq 400db8 <__stack_chk_fail@plt>
401393: leaveq
401394: retq
見ての通り、4012ebは第1引数に与えられたデータ列を10バイト分aaと比較します。つまり、第1引数に与えられたデータ列が10バイトすべてaaであるか? を確認します。青い部分がバイト単位で比較するループで、赤い部分がaaか否かを評価しています。
// level8.asm
4014cb: movzbl -0x216(%rbp),%eax
4014d2: cmp $0x61,%al
4014d4: je 4014e0 <accept@plt+0x688>
4014d6: mov $0xffffffff,%eax
4014db: jmpq 4017a5 <accept@plt+0x94d>
4014e0: mov 0x203c7a(%rip),%eax
aaの評価が終わると、次は先頭からbバイト目が61であるか否かの評価が行われます。もし61でなければエラーとなります。
以上の結果から、送信データは右図になります。これを送信するプログラムを作成します。まずは送信パケットを作成しますが、これはRC4を持ってきてもよいですが、せっかくなのでlevel8プログラム本体のコードを利用します(makepkt1.c)。
// makepkt1.c
$ gcc -Wall makepkt1.c -o makepkt
$ ./makepkt ./level8
aa aa aa aa aa aa 49 c2
c5 4d 23 92 7b ed 77 db
3f 47 68 7d 93 1d c8 d8
4a 3b 54 1b d5 c2 22 f6
05 72 ab 41 aa aa aa aa
8バイト単位で扱いたかったため、最後のaa aa aa aaが余計に出力されていますが、使用するのは先頭24バイトです。このデータを送信します。
$ ./execlog1 ./level8 5555 > execlog2.txt
// sendpkt.py
#!/usr/bin/python
from socket import *
s = socket()
s.connect(('localhost', 5555))
data = "\xaa\xaa\xaa\xaa\xaa\xaa\x49\xc2"
data += "\xc5\x4d\x23\x92\x7b\xed\x77\xdb"
data += "\x3f\x47\x68\x7d\x93\x1d\xc8\xd8"
data += "\x4a\x3b\x54\x1b\xd5\xc2\x22\xf6"
data += "\x05\x72\xab\x41"
s.send(data)
print s.recv(256)
s.close
$ python connect.py > TMP
$ hexdump -C TMP
00000000 3b 68 1c 94 fa 5c 56 4e a5 70 9f c4 ec 64 09 c0
00000010 49 4a 26 7e e7 c8 b0 69 db 3a 04 1f 91 84 09 e2
00000020 6a 57 2c f3 61 09 9c c8 31 a6 9c e5 20 d2 a9 68
00000030 62 f9 d6 c4 7b 06 58 73 a6 59 a5 c1 9d a0 6a 9f
00000040 b4 78 72 39 85 c9 94 83 eb 0a d6 51 a6 c9 b4 9e
00000050 b1 46 da ee 4f 4b fe 08 7a 60 f1 cc 7a 61 9c e1
00000060 7e ca 8b 96 c7 2c 63 39 f3 72 41 f4 ef 1c 24 5a
00000070 e8 a9 ae 8e 33 0d 13 da ee 5a fb fd 42 1b b6 cd
00000080 d5 cd b1 d8 49 cc 12 10 7c b8 fc c0 00 db ae d6
00000090 eb 88 2c d8 1f 4b 4e 01 9d 76 a6 03 7a 02 ec 5e
000000a0 51 67 70 17 f1 83 bf 00 a0 71 31 8e 8a 5f e0 a4
000000b0 d0 e4 8f 9c f6 76 c3 91 2e fa 62 77 89 d9 50 51
000000c0 5e d5 09 31 66 c3 63 5b 3f 3f 4c 47 61 ca 0a
今度はサーバからデータが送られてきました。暗号化されていますので、復号する必要がありますが、実は暗号化方法はさっきと同じです。サーバが行う暗号化処理の詳細が知りたい場合はexeclog2.txt辺りを参考にしてください。クライアントが行った暗号化処理を、今度はサーバ側が行っているだけですので、こちら側でサーバの処理を真似ればよいです。復号を行うプログラム(decodepkt1.c)を作成します。
$ gcc -Wall decodepkt1.c -o decodepkt1
$ ./decodepkt1 ./level8 > TMP
$ hexdump -C TMP
00000000 3b 68 1c 94 fa 5c aa aa aa aa aa aa aa aa aa aa |;h...\..........|
00000010 54 48 49 53 20 49 53 20 4c 45 56 45 4c 38 20 50 |THIS IS LEVEL8 P|
00000020 41 53 53 57 4f 52 44 2e 0a 00 15 99 c2 7f 00 00 |ASSWORD.........|
00000030 c8 05 40 00 00 00 00 00 00 00 00 00 01 00 00 00 |..@.............|
00000040 0a 05 00 00 01 00 00 00 03 00 00 00 00 00 00 00 |................|
00000050 77 e5 96 7c 00 00 00 00 80 a4 6e 99 c2 7f 00 00 |w..|......n.....|
00000060 f0 7b 5f 0a ff 7f 00 00 f3 72 41 f4 ef 1c 24 5a |.{_......rA...$Z|
00000070 e8 a9 ae 8e 33 0d 13 da ee 5a fb fd 42 1b b6 cd |....3....Z..B...|
00000080 d5 cd b1 d8 49 cc 12 10 7c b8 fc c0 00 db ae d6 |....I...|.......|
00000090 eb 88 2c d8 1f 4b 4e 01 9d 76 a6 03 7a 02 ec 5e |..,..KN..v..z..^|
000000a0 51 67 70 17 f1 83 bf 00 a0 71 31 8e 8a 5f e0 a4 |Qgp......q1.._..|
000000b0 d0 e4 8f 9c f6 76 c3 91 2e fa 62 77 89 d9 50 51 |.....v....bw..PQ|
000000c0 5e d5 09 31 66 c3 63 5b 3f 3f 4c 47 61 ca 0a 00 |^..1f.c[??LGa...|
000000d0
無事サーバ側の/etc/Passwordが見えました。競技中であれば、これをターゲットサーバに対して行うことで解答パスワード「THANK_YOU_FOR_PLAYING_NETAGENT_SECURITY_CONTEST_2010」が得られました。
以上でlevel8の解答は終了です。
-----
以上でNetAgent Security Contest 2010のLevel5~Level8の解答とさせていただきます。Level1~Level4に関しては「NetAgent Security Contest 2010 解答 Level1 ~ Level4」をご参照ください。また、ニコニコ動画に当コンテストの解答をアップロードされている方がいらっしゃいます。解答の流れを丁寧に動画で解説されているので、興味ある方はそちらもご参照ください(NetAgent Security Contest 2010にチャレンジしてみた)。
最後になりましたが、参加者の皆様本当にお疲れ様でした。次回はまだ未定ですが、このようなイベントがまた開催できればと考えていますので、今後ともよろしくお願いいたします。