プログラム中の条件分岐をバイパスするには

引き続き、思い出したときにバイナリやアセンブリの勉強をしています。
今回はバイナリを編集してプログラム中の条件分岐をバイパスする方法について勉強します。

以下のようなプログラムがあります。


#include 
#include 

int main()
{
    time_t old = time(NULL);
    time_t new = time(NULL);
    struct tm *timer = localtime(&old);

    if (old > new) {
        printf("%d-%d-%d %d:%d:%d\n",
            timer->tm_year + 1900,
            timer->tm_mon + 1,
            timer->tm_mday,
            timer->tm_hour,
            timer->tm_min,
            timer->tm_sec
            );
    }
    else {
           printf("Bye!\n");
    }

    return 0;
}

プログラムの内容を簡単に説明すると、現在時刻を2回取得し、1回目に取得した時刻が2回目に取得した時刻よりも大きければ(新しければ)1回目の時刻を表示し、それ以外の場合は"Bye!"と表示して終了します。
1回目に取得した時刻が2回目に取得した時刻よりも大きい(新しい)ということは通常ならばありえないので、このプログラムをそのままコンパイルして実行しても"Bye!"と表示されて終わります。
if1.png

そこで、コンパイルしたバイナリを編集して条件分岐をバイパスし、1回目の時刻を表示させることが今回の目的になります。
以下はコンパイルしたバイナリを逆アセンブルしたものです。
if2.png

要所をソースコードと照らし合わせて、ざっくりコメントを入れてみました。

0000000100000ea0 <_main>:
100000ea0:	55                   	push   %rbp
100000ea1:	48 89 e5             	mov    %rsp,%rbp
100000ea4:	48 83 ec 40          	sub    $0x40,%rsp
100000ea8:	31 c0                	xor    %eax,%eax
100000eaa:	89 c7                	mov    %eax,%edi
100000eac:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)	## NULL
100000eb3:	e8 a8 00 00 00       	callq  100000f60 <_main+0xc0>	## time_t old = time(NULL); 結果は%raxに格納される
100000eb8:	31 c9                	xor    %ecx,%ecx
100000eba:	89 cf                	mov    %ecx,%edi
100000ebc:	48 89 45 f0          	mov    %rax,-0x10(%rbp)	## oldを-0x10(%rbp)にコピー
100000ec0:	e8 9b 00 00 00       	callq  100000f60 <_main+0xc0>	time_t new = time(NULL); 結果は%raxに格納される
100000ec5:	48 8d 7d f0          	lea    -0x10(%rbp),%rdi
100000ec9:	48 89 45 e8          	mov    %rax,-0x18(%rbp)	## newを-0x18(%rbp)にコピー
100000ecd:	e8 82 00 00 00       	callq  100000f54 <_main+0xb4>	## struct tm *timer = localtime(&old); 結果は%raxに格納される
100000ed2:	48 89 45 e0          	mov    %rax,-0x20(%rbp)	## tm *timerを-0x20(%rbp)にコピー
100000ed6:	48 8b 45 f0          	mov    -0x10(%rbp),%rax	## old (-0x10(%rbp))を%raxにコピー
100000eda:	48 3b 45 e8          	cmp    -0x18(%rbp),%rax	## newの値とoldの値を比較
100000ede:	0f 8e 57 00 00 00    	jle    100000f3b <_main+0x9b> ## oldとnewの値が等しいか、oldがnewより小さければアドレス100000f3bへジャンプ (else文の処理へ移る)
100000ee4:	48 8d 3d ab 00 00 00 	lea    0xab(%rip),%rdi        # 100000f96 <_main+0xf6> (ここより先はif文の処理)

上記で少しまぎらわしいのがcmp命令の部分です。cmp命令は片方のオペランドからもう片方のオペランドの値を減算します。そして減算の結果に応じて次の処理が決定されます。
AT&T構文のcmp命令は第一オペランドが減数、第二オペランドが被減数となります。つまり第二オペランドから第一オペランドの値を減算することになります。
よって


100000eda:	48 3b 45 e8          	cmp    -0x18(%rbp),%rax

上記は%rax から -0x18(%rbp)の値を減算する(oldの値 - newの値)という意味になります。
oldの値からnewの値を減算した結果、ゼロになるかマイナスの値になった場合はelse文へ処理が移ります。(oldの値とnewの値が等しい、あるいはoldの値がnewの値より小さい場合、と言い換えてもOK)


100000ede:	0f 8e 57 00 00 00    	jle    100000f3b <_main+0x9b>

条件分岐をバイパスするには上記のジャンプ命令を書き換えればOKです。今回はcmpの結果に関わらずに直後の処理(if文の直後の処理)に移りたいので、jle命令をnop命令に書き換えます。nopとはno operationの略で、「何もしない」という意味です。90というオペコードで表されます。
適当なバイナリエディタで該当箇所を編集します。具体的には0f 8e 57 00 00 00の部分を90 90 90 90 90 90と書き換えます。今回はviを使いました。

編集前
if3.png

編集後
if4.png

逆アセンブルの結果も比較してみましょう。
編集前
if5.png

編集後
if6.png

編集後のバイナリを実行すると、1回目に取得した時刻を表示して終了しているのが確認できます。
if7.png

以上
REF for cmp
http://www.hpcs.cs.tsukuba.ac.jp/~msato/lecture-note/kikaigo2008/lecture2.pdf
https://en.wikibooks.org/wiki/X86_Assembly/Control_Flow

Leave a Reply

Your email address will not be published. Required fields are marked *