引き続き、思い出したときにバイナリやアセンブリの勉強をしています。
今回はバイナリを編集してプログラム中の条件分岐をバイパスする方法について勉強します。
以下のようなプログラムがあります。
#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!"と表示されて終わります。
そこで、コンパイルしたバイナリを編集して条件分岐をバイパスし、1回目の時刻を表示させることが今回の目的になります。
以下はコンパイルしたバイナリを逆アセンブルしたものです。
要所をソースコードと照らし合わせて、ざっくりコメントを入れてみました。
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を使いました。
編集後のバイナリを実行すると、1回目に取得した時刻を表示して終了しているのが確認できます。
以上
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