車輪の再発明

いったい何番煎じだよ!

Hello World!を逆アセンブル①

こんな本を買ったので、最近はバイナリの勉強をしていたりします。

Hacking: 美しき策謀 第2版 ―脆弱性攻撃の理論と実際

Hacking: 美しき策謀 第2版 ―脆弱性攻撃の理論と実際

まだちょっとしか読めていないんですが、すごく面白いです。 攻撃手法が解説されているだけではなくて、プログラミングとは何かからはじまり、コンピュータの理解が深まる良書だと思います。

というわけで(?)、もはや何番煎じかわからないですが、今日はHello Worldアセンブリを見ていきます。 上記の本だと32bit版で解説されていますが、今回は64bitの環境で試したので、レジスタの名前が違ったりしました。

環境

Ubuntu 16.04を使っています。 gcc(コンパイラ)のバージョンは以下の通り。

$ gcc --version
gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

プログラムの作成

どんなC言語の入門書でも必ず最初に出てくるような次のコードをhelloWorld.cとして保存。

#include <stdio.h>

int main(){
    printf("Hello World!\n");
    return 0;
}

コンパイルします。

$ gcc helloWorld.c -o helloWorld

出来上がったものを実行すると、標準出力に文字列が返ります。

$ ./helloWorld
Hello World!

アセンブルしてみる

早速objdumpで逆アセンブルしてみます。

$ objdump -M intel -S helloWorld
#中略
000000000000063a <main>:
 63a:   55                      push   rbp
 63b:   48 89 e5                mov    rbp,rsp
 63e:   48 8d 3d 9f 00 00 00    lea    rdi,[rip+0x9f]        # 6e4 <_IO_stdin_used+0x4>
 645:   e8 c6 fe ff ff          call   510 <puts@plt>
 64a:   b8 00 00 00 00          mov    eax,0x0
 64f:   5d                      pop    rbp
 650:   c3                      ret    
 651:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
 658:   00 00 00 
 65b:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
#中略

アセンブリAT&T記法とIntel記法がありますが、後者の方が見やす気がするのでそちらで表示しています。 -Mオプションで指定します。

スタック領域の切り替え

最初の2行は関数が実行されるときに最初に実行される処理です。 main()関数を開始するためにスタック領域の切り替えを行います。

 63a:    55                      push   rbp
 63b:   48 89 e5                mov    rbp,rsp

rbp(ベースポインタ)がスタックの最古(底)のアドレスで、rsp(スタックポインタ)が最新(一番上)のアドレスを指しています。

pushは指定したレジスタの値をスタックに積む(保存する)命令で、こうすることで現在(main()の呼び出し元)のスタックのアドレスをバックアップします。 バックアップしたアドレスはmain()関数が終了し、main()関数の呼び出し元の処理を再開するときに使用します。

バックアップできたら、movrspの値をrbpへ書き込みます。 最新のスタックのアドレスがmain()のスタックのはじめ(底)のアドレスとして使用されるようになるわけです。

printf()の実行

続いてprintf()の呼び出しに関する部分です。

 63e:    48 8d 3d 9f 00 00 00    lea    rdi,[rip+0x9f]        # 6e4 <_IO_stdin_used+0x4>
 645:   e8 c6 fe ff ff          call   510 <puts@plt>

learip(インストラクションポインタ)に0x9f足したアドレスをrdi(ディスティネーションインデックス)へ書き込んでいます。 詳しくは次回に回しますが、rip+0x9fは"Hello World!"の先頭アドレスで、rdiを使用することで引数としてprintf()へ渡しているようです。

続いてprintf()の実行。これは関数の先頭アドレスをcallするだけなので単純ですね。

戻り値を返す

ソースで言うとreturn 0の部分です。

 64a:    b8 00 00 00 00          mov    eax,0x0

eax(アキュムレータ)は戻り値の格納に使用するレジスタで、このレジスタ0movで書き込んでいます。

スタックをもとに戻す

一通りの処理が終わったので、最初に切り替えたスタックをもとに戻します。

 64f:    5d                      pop    rbp

main()の開始時にスタックへ呼び出し元関数のスタックのベースアドレス(底のアドレス)を保存しました。 この処理ではそのアドレスをrbpへ戻しています。今回はスタックを使用していないので、ありがたみが感じられないですね。

呼び出し元へ戻る

main()関数の処理がすべて完了したので、呼び出し元へ処理を戻します。

 650:    c3                      ret    

何のためか不明な命令

最後の3行は何のためのものかよくわかりませんでした。

 651:    66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
 658:   00 00 00 
 65b:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

nopはNo Operation、つまり何もしないという命令らしいですが、何のために入っているかよくわからず、、、パディングかな?

次回はgdb(デバッガ)を使用して、レジスタやメモリの値の変化を追っていきます。