Special instruction challenge チュートリアル

(動的解析してみる)

■ GDBサーバに接続する

競技サーバでは,これらの実行ファイルが,この特殊命令が実装された エミュレータ上で動作しています.
こちらを使えば,特殊命令が正常に実行できるはずです.
さらにそれらにはデバッガ(GDB)で接続して解析ができます.
いわゆる「リモートデバッグ」ができる,ということです.

そこで次は,GDBサーバに接続して,特殊命令の挙動を見てみましょう.

以下でBlackfin用のGDBを起動します.起動時に,Blackfin用の実行ファイルを 指定します.
このように実行ファイルを指定することで,実行ファイルから デバッグ用の各種情報を得て,デバッグしやすくすることができます.

$ /usr/local/cross-gcc494/bin/bfin-elf-gdb bfin-elf.x
GNU gdb (GDB) 7.12.1
...
(gdb)
起動したらGDBサーバに接続します.
接続には,しばらく時間がかかるかもしれません.
(gdb) target remote 127.0.0.1:10000
Remote debugging using 127.0.0.1:10000
...(接続まで,しばらく時間がかかる)...
0x00001400 in start ()
(gdb)
これでGDBサーバに接続できました.
先ほどはGDBに内蔵されたエミュレータ上でプログラムを実行しました.
しかし今度はGDBサーバ上のエミュレータでプログラムが実行されていて, そこにリモートで接続しています.

nputs()という関数で表示が行われていたはずなので, そこにブレークポイントを張って実行してみましょう.
GDBサーバに接続している場合には,プログラムはすでに動作開始しているため, runでなくcontinueを実行することに注意してください.

(gdb) break nputs
Breakpoint 1 at 0x1482
(gdb) continue
Continuing.

Breakpoint 1, 0x00001482 in nputs ()
(gdb)
nputs()まで来たということは,そこまでにあった random_XXX_default()や decrypt()が実行できたということです.
つまり乱数命令は,不正命令にならずに,正常に実行されています.
これはGDBサーバ上のエミュレータには,乱数命令が実装されているためです.

■ Blackfinアセンブラ基礎

nputs()では,何を表示しようとしているのでしょうか.
ここでBlackfinのアセンブラの基礎をちょっと説明します.

競技のインフォメーションページでは,アセンブラのサンプルとして 冒頭で説明した cross-gcc494-v1.0.zip を上げています.
これを展開すると,sample/bfin-elf.d というファイルがあります.
これがBlackfinのアセンブラのサンプルです.
また元となっているソースコードは sample/sample.c というファイルです.

sample.c と bfin-elf.d で,1を返すreturn_one()という関数と, 関数の第1/第2引数を返す return_arg1()/return_arg2()という関数を 比較してみます.

(sample.c)

int return_one()
{
  return 1;
}
...
int return_arg1(int a)
{
  return a;
}

int return_arg2(int a, int b)
{
  return b;
}

(bfin-elf.d)

00fe1418 <_return_one>:
  ...
  fe141e:       08 60           R0 = 0x1 (X);           /*              R0=0x1(  1) */
  fe1420:       10 00           RTS;
        ...

00fe148c <_return_arg1>:
        ...
  fe1494:       10 00           RTS;
        ...

00fe1498 <_return_arg2>:
  ...
  fe149e:       01 30           R0 = R1;
  fe14a0:       10 00           RTS;
        ...
return_one()を見ると,R0というレジスタに1を代入しています.
つまりBlackfinでは,戻り値はレジスタR0で返すようです.
また return_arg1() は何もせず戻り,return_arg2()では R1をR0に代入してから戻っています.

ということはBlackfinでは,関数の第1引数は戻り値と同様にレジスタR0, 第2引数はレジスタR1で渡されるのだと推測できます.

■ デコード結果の確認

さてここまでわかったところで,GDBでの解析に話を戻します.
nputs()でブレークしている状態でレジスタの値を見てみます.

(gdb) info registers
r0             0x1      1
r1             0x1bdc   7132
r2             0x24     36
r3             0x0      0
...
引数はレジスタR0以降で渡されるため,その付近のレジスタを見てみます.
値を見る限り,レジスタR1の値がポインタのようです. これがおそらく出力する文字列の位置でしょう.
レジスタR2は出力サイズ,レジスタR0は出力先(標準出力の1)でしょうか.

実際にnputs()の逆アセンブル結果を見てみましょう.

00001480 <_nputs>:
    1480:       67 01           [--SP] = RETS;
    1482:       a6 6f           SP += -0xc;             /* (-12) */
    1484:       ff e3 ca ff     CALL 0x1418 <___write>;
    1488:       00 60           R0 = 0x0 (X);           /*              R0=0x0(  0) */
    148a:       66 6c           SP += 0xc;              /* ( 12) */
    148c:       27 01           RETS = [SP++];
    148e:       10 00           RTS;

__write()という関数を呼んでいます.writeシステムコールの呼び出しでしょうか. たしかに,出力が行われるようです.
また戻り値としてレジスタR0にゼロが代入されているので,戻り値も返すようです.
そう考えると,nputs()は以下のような仕様の関数のようです.

int nputs(int fd, char *str, int len);
つまり出力文字列は第2引数として,レジスタR1で渡されているように思われます.

ということでレジスタR1の指す先のメモリを読んでみましょう. ここに出力文字列があります.

(gdb) x/48c $r1
0x1bdc: 83 'S'  69 'E'  67 'C'  67 'C'  79 'O'  78 'N'  123 '{' 89 'Y'
0x1be4: 111 'o' 117 'u' 85 'U'  115 's' 101 'e' 77 'M'  97 'a'  110 'n'
0x1bec: 121 'y' 68 'D'  83 'S'  80 'P'  87 'W'  104 'h' 105 'i' 108 'l'
0x1bf4: 101 'e' 78 'N'  111 'o' 116 't' 75 'K'  110 'n' 111 'o' 119 'w'
0x1bfc: 105 'i' 110 'n' 103 'g' 125 '}' 0 '\000'        0 '\000'        0 '\000'
        0 '\000'
0x1c04: 0 '\000'        0 '\000'        0 '\000'        0 '\000'        0 '\000'
        0 '\000'        0 '\000'        0 '\000'
(gdb)
これがデコードされたキーワードです.
以下のようになっていますね.
SECCON{YouUseManyDSPWhileNotKnowing}

■ おしまい