最も簡単な逆アセンブル(分岐)

最も簡単な逆アセンブル - あしのあしあと」の続き。今回は、分岐を入れてみる。

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 からのオフセット値がつく。