「最も簡単な逆アセンブル - あしのあしあと」の続き。今回は、分岐を入れてみる。
int __cdecl gt(int a, int b) { if (a >= b) { return a; } else { return b; } } int main(void) { return gt(0x07, 0x0A); }
逆アセンブルして出力されたコードが次。今回も、とりあえず、全部載せてみた。環境も前回と変わらず、Windows 10 上の Visual Studio Community 2017。
_gt: 009F16A0 push ebp ; (01) 009F16A1 mov ebp,esp ; (02) 009F16A3 sub esp,0C0h ; (03) 009F16A9 push ebx ; (04) 009F16AA push esi ; (05) 009F16AB push edi ; (06) 009F16AC lea edi,[ebp+FFFFFF40h] ; [ebp-0C0h] ; (07) 009F16B2 mov ecx,30h ; (08) 009F16B7 mov eax,0CCCCCCCCh ; (09) 009F16BC rep stos dword ptr es:[edi] ; (10) 009F16BE mov eax,dword ptr [ebp+8] ; (11) 009F16C1 cmp eax,dword ptr [ebp+0Ch] ; (12) 009F16C4 jl 009F16CD ; (13) 009F16C6 mov eax,dword ptr [ebp+8] ; (14) 009F16C9 jmp 009F16D0 ; (15) 009F16CB jmp 009F16D0 ; (16) 009F16CD mov eax,dword ptr [ebp+0Ch] ; (17) 009F16D0 pop edi ; (18) 009F16D1 pop esi ; (19) 009F16D2 pop ebx ; (20) 009F16D3 mov esp,ebp ; (21) 009F16D5 pop ebp ; (22) 009F16D6 ret ; (23)
(10) 行目までは、前回と全く変わらず。(11) 〜 (17) 行目が分岐の処理になる。
- (11) : 0x07 を EAX に転送
- (12) : EAX の値(0x07)と EBP+0Ch にある値(0x0A)とを比較する(0x07-0x0A=-3)
- (13) : JL(Jump if Less)で、(17) 行目 009F16CD までジャンプ(EIP が 009F16CD)
- (17) : EAX に 0x0A が入る
なお、(12) 行目が条件を満たさなければ、(14) 行目で EAX に 0x07 が入り、(18) 行目にジャンプする。
(18) 行目からも、前回と全く変わらず。ところで、(16) 行目は、いったい何のためにあるの?
ここで大事になるのが、フラグレジスタ(EFLAGS)。Visual Studio(表では VS としている)で表示できるフラグは、次のようになる(レジスタウィンドウに出力されるもの)。
VS | x86 | 名称「○○フラグ」 | セットされる条件・セットされた結果 |
---|---|---|---|
OV | OF | オーバーフロー | 桁あふれ(符号付き演算)のときセットされる |
UP | DF | ディレクション | セットされているとポインタが増加(ストリング操作命令) |
EI | IF | インタラプト・イネーブル | セットされていると外部割り込みを受け付ける |
PL | SF | サイン | 演算結果が負(符号付き演算)のときセットされる |
ZR | ZF | ゼロ | 演算結果がゼロのときセットされる |
AC | AF | 補助キャリー | BCD 演算(二進化十進)で使用? |
PE | PF | パリティ | "1" が偶数個のときセットされる |
CY | CF | キャリー | 桁上がりのときセットされる |
(3) 行目、(12) 行目の処理で、それぞれのフラグは、次のように変化した。違和感はない。ちなみに、EI(インタラプト・イネーブルフラグ)は、ずっと立ちっぱなし。
(03) 行目の sub 実行後
OV = 0 | UP = 0 | EI = 1 | PL = 0 | ZR = 0 | AC = 0 | PE = 1 | CY = 0 |
(12) 行目の cmp 実行後
OV = 0 | UP = 0 | EI = 1 | PL = 1 | ZR = 0 | AC = 1 | PE = 0 | CY = 1 |
JL(Jump if Less)は、「サインフラグ、オーバーフローフラグのどちらか 0 で、他方が 1 の場合」に、指定されたアドレスにジャンプする。今回は、「桁あふれなしで、演算結果が負」なので、アドレス 009F16CD へジャンプしたということ。
2017/9/20 追記
もう少し、エッセンスを、まとめられないか。IDA で、どう表現されるか、示せないか。
と思ったのだ。
そこで、もう一度、分岐に特化して実験してみる。以下は、2 つの引数の値を比較するだけの、簡単なプログラム。分岐があって、シンプルであれば、何でもよかった。
ちなみに、a と b を固定値にすると、IDA が分岐をつくってくれない。とてもかしこく、処理されない方の分岐を、勝手に除去してくれる。なので、値を、引数からもってくるようにした。これなら、どっちの分岐に入るか、わからないだろう。
int main(int argc, char *argv[]) { if (argc != 3) return -1; char *a, *b; a = argv[1]; b = argv[2]; if (a == b) { printf("a = b = %s\r\n", a); return 0; } else { printf("a = %s, b = %s\r\n", a, b); return 1; } }
逆アセンブルした結果が次。
_main: 00041040 push ebp ; (01) ベースポインタを退避 00041041 mov ebp,esp ; (02) ベースポインタを設定 00041043 cmp dword ptr [argc],3 ; (03) ● 分岐: argc を 3 と比較 00041047 je main+0Eh (04104Eh) ; (04) ● 分岐: 等しければジャンプ 00041049 or eax,0FFFFFFFFh ; (05) EAX に -1 を設定 0004104C pop ebp ; (06) ベースポインタを復元 0004104D ret ; (07) リターン 0004104E mov eax,dword ptr [argv] ; (08) EAX をベースにする 00041051 mov ecx,dword ptr [eax+4] ; (09) ECX ← 引数 1 00041054 mov eax,dword ptr [eax+8] ; (10) EAX ← 引数 2 ; いよいよメインの分岐 00041057 cmp ecx,eax ; (11) ● 分岐: 引数 1, 2 を比較 00041059 jne main+2Dh (04106Dh) ; (12) ● 分岐: 0 でなければジャンプ 0004105B push ecx ; (13) printf 第 2 引数 0004105C push offset string "a = b = %s\r\n" (0420F8h) ; (14) printf 第 1 引数 00041061 call printf (041010h) ; (15) printf コール 00041066 add esp,8 ; (16) スタックポインタを戻す 00041069 xor eax,eax ; (17) EAX に 0 を設定 0004106B pop ebp ; (18) ベースポインタを復元 0004106C ret ; (19) リターン 0004106D push eax ; (20) printf 第 3 引数 0004106E push ecx ; (21) printf 第 2 引数 0004106F push offset string "a = %s, b = %s\r\n" (042108h) ; (22) printf 第 1 引数 00041074 call printf (041010h) ; (23) printf コール 00041079 add esp,0Ch ; (24) スタックポインタを戻す 0004107C mov eax,1 ; (25) EAX に 1 を設定 00041081 pop ebp ; (26) ベースポインタを復元 00041082 ret ; (27) リターン
どこがエッセンス?
全行にコメント書いてしまっているではないか。なので、少しだけ補足する。
分岐の箇所は、「cmp → je」 と 「cmp → jne」 の 2 箇所。比較した後に、下にジャンプする。これ。比較した後に、下にジャンプする。ちなみに、ループのときは、上にもジャンプする。
今回は、分岐の後、全て return しているが、処理を続行する場合には、jmp(無条件分岐)となる。ちょうど、こんな感じ。
なお、cmp は、sub と同じ計算を行う。ただし、cmp はフラグを設定するだけで、オペランドを変更しない。ムダに貴重なレジスタを使わない。
で、最後に、exe ファイルを IDA に食わせてやると、次のようなグラフビューを出力してくれる。
とにかく分岐が見やすい。
加えて、ローカル変数やパラメータ(引数)も見やすくなっている。画像の一番上のハコの先頭に「; Attributes: bp-based frame」とある。これは、ローカル変数やパラメータを、EBP レジスタ基準に表示するという意味らしい。さらに、ローカル変数には、var_ というプレフィックスがつき、パラメータには、arg_ というプレフィックスがつく。サフィックスは、EBP からのオフセット値がつく。