最も簡単な逆アセンブル(ループ)

最も簡単な逆アセンブル(分岐) - あしのあしあと」の続き。今回は、ループを入れてみる。
a から b までを足し合わせる sum という関数*1で実験する。

int __cdecl sum(int a, int b) {
    /* TODO: 引数のチェック */

    int sum = 0;

    int i;
    for (i = a; i <= b; i++) {
        sum = sum + i;
    }
    return sum;
}

int main(void) {
    return sum(0x07, 0x0A);
}

アセンブルして出力されたコードが次。ちょっと量がでてきた。とはいっても、まだまだ短いので、とりあえず、全部載せてみる。環境も初回と変わらず、Windows 10 上の、Visual Studio Community 2017。

  _sum:
  00363B20  push        ebp                     ; (01)
  00363B21  mov         ebp,esp                 ; (02)
  00363B23  sub         esp,0D8h                ; (03)
  00363B29  push        ebx                     ; (04)
  00363B2A  push        esi                     ; (05)
  00363B2B  push        edi                     ; (06)
  00363B2C  lea         edi,[ebp+FFFFFF28h]     ; (07) [ebp-0D8h]
  00363B32  mov         ecx,36h                 ; (08)
  00363B37  mov         eax,0CCCCCCCCh          ; (09)
  00363B3C  rep stos    dword ptr es:[edi]      ; (10)
  00363B3E  mov         dword ptr [ebp-8],0     ; (11)
  00363B45  mov         eax,dword ptr [ebp+8]   ; (12)
  00363B48  mov         dword ptr [ebp-14h],eax ; (13)
  00363B4B  jmp         00363B56                ; (14)
  00363B4D  mov         eax,dword ptr [ebp-14h] ; (15)
  00363B50  add         eax,1                   ; (16)
  00363B53  mov         dword ptr [ebp-14h],eax ; (17)
  00363B56  mov         eax,dword ptr [ebp-14h] ; (18)
  00363B59  cmp         eax,dword ptr [ebp+0Ch] ; (19)
  00363B5C  jg          00363B69                ; (20)
  00363B5E  mov         eax,dword ptr [ebp-8]   ; (21)
  00363B61  add         eax,dword ptr [ebp-14h] ; (22)
  00363B64  mov         dword ptr [ebp-8],eax   ; (23)
  00363B67  jmp         00363B4D                ; (24)
  00363B69  mov         eax,dword ptr [ebp-8]   ; (25)
  00363B6C  pop         edi                     ; (26)
  00363B6D  pop         esi                     ; (27)
  00363B6E  pop         ebx                     ; (28)
  00363B6F  mov         esp,ebp                 ; (29)
  00363B71  pop         ebp                     ; (30)
  00363B72  ret                                 ; (31)

(10) 行目まで、および (26) 行目以降は、これまでと全くいっしょ。詳細は、依然としてよくわからないまま。
メモリの状況は、次の図の通り。グレーの部分は、初回同様。main 関数が、引数である「0x07」と「0x0A」をスタックに入れて、sum 関数をコールしている。なお、EBX, ESI, EDI の退避(push)については、毎度おなじみなので、省略している。

EBP は、(2) 行目で、関数がコールされたときのスタックポインタを保持するので、sum 関数では、EBP が基準となる。[EBP - 8h] が、変数 sum の値が格納されるアドレスを、[EBP - 14h] が、変数 i の値が格納されるアドレスを表している。なぜ、とびとびのアドレスになっているのかは、よくわからない。
(11) 行目で、sum が初期化され、(13) 行目で、i に 0x07 が入る。この値は、(16) 行目でカウントアップされ、(17) 行目で i に格納される。(22) 〜 (23) 行目で、sum の値に、i の値が足される。


これを踏まえて、ループの処理を 1 行ずつ、詳しくみていく。(11) 行目から (25) 行目の処理は、次のようになっている。

とにかく、ひたすら EAX を使う。EAX の値を入れ替えて、演算。この繰り返し。
ここまで丁寧に絵にしなくても、さすがに処理内容は理解できる。少々やりすぎたか。

2017/09/20 追記

初めは、もっともっと、簡単な処理を試せばよかったのだよ、おそらくは。
ということで、0 から 99 まで表示するだけのプログラムを試してみる。

int main(void) {
    for (int i = 0; i < 100; i++) {
        printf("%d\r\n", i);
    }
    return;
}

アセンブルすると、次のようになる。あれ?
あまり簡単になっていないような。

  01004060  push        ebp                                ; (01)
  01004061  mov         ebp,esp                            ; (02)
  01004063  sub         esp,0CCh                           ; (03)
  01004069  push        ebx                                ; (04)
  0100406A  push        esi                                ; (05)
  0100406B  push        edi                                ; (06)
  0100406C  lea         edi,[ebp-0CCh]                     ; (07)
  01004072  mov         ecx,33h                            ; (08)
  01004077  mov         eax,0CCCCCCCCh                     ; (09)
  0100407C  rep stos    dword ptr es:[edi]                 ; (10)

  0100407E  mov         dword ptr [ebp-8],0                ; (11) 初期化: int i = 0;
  01004085  jmp         main+30h (01004090h)               ; (12) ループに入る
  01004087  mov         eax,dword ptr [ebp-8]              ; (13) インクリメント
  0100408A  add         eax,1                              ; (14) インクリメント
  0100408D  mov         dword ptr [ebp-8],eax              ; (15) インクリメント
  01004090  cmp         dword ptr [ebp-8],64h              ; (16) 比較: i < 100;
  01004094  jge         main+49h (010040A9h)               ; (17) 比較: 100 以上ならループを抜ける
  01004096  mov         eax,dword ptr [ebp-8]              ; (18)
  01004099  push        eax                                ; (19) printf: 第 2 引数
  0100409A  push        offset string "%d\r\n" (01006BCCh) ; (20) printf: 第 1 引数
  0100409F  call        _printf (01001361h)                ; (21) printf: コール
  010040A4  add         esp,8                              ; (22) スタックポインタを戻す
  010040A7  jmp         main+27h (01004087h)               ; (23) ループする

  010040A9  xor         eax,eax                            ; (24)
  010040AB  pop         edi                                ; (25)
  010040AC  pop         esi                                ; (26)
  010040AD  pop         ebx                                ; (27)
  010040AE  add         esp,0CCh                           ; (28)
  010040B4  cmp         ebp,esp                            ; (29)
  010040B6  call        __RTC_CheckEsp (01001118h)         ; (30)
  010040BB  mov         esp,ebp                            ; (31)
  010040BD  pop         ebp                                ; (32)
  010040BE  ret                                            ; (33)

(01) 〜 (10) 行目と、(24) 〜 (33) 行目は、「最も簡単な逆アセンブル - あしのあしあと」 の最後、『あと、一応 main 関数も書いておくことにした。』に書いた通り。(11) 〜 (23) 行目が本丸。「EBP - 8」が、「i」を表している。演算は EAX で行うため、「EBP - 8」と「EAX」の間で、頻繁に値の入れかえをしている。
なるほど。構造は見えた。

こんな for ループであれば、逆アセンブルすると、次のような構造をしている。

ループの構造のバリエーションが、たくさんあるとは思えない。こんなものだと思う。
で、最後に、exe ファイルを IDA に食わせてやると、次のようなグラフビューを出力してくれる。

な、なんと。こんなにシンプルにしてくれるとは。やはり、簡単な処理を試しておいてよかった。

2017/09/23 追記

で、もともとの、a から b までの合計を求める処理に戻る。a と b が固定値だと、IDA では、合計値を事前に計算してくれるだろう。これはありがたいことではあるが、グラフビューがループの構造にならず、ループの勉強にならない。そこで、やはり a と b を引数で渡すことにした。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc != 3) return -1;

    int a = atoi(argv[1]);
    int b = atoi(argv[2]);

    int sum = 0;

    int i;
    for (i = a; i <= b; i++) {
        sum = sum + i;
    }

    printf("%d\r\n", sum);
    return 0;
}

これなら、ループの構造になるだろう。きっと。

え。読めないんですけど。なんでこんなことに?

2017/09/24 追記

while 文を忘れていた。おそらく、すごいシンプルになるだろうが、一応やっておく。

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int i = 0;
    while (i < 100) {
        printf("%d\r\n", i);
        i++;
    }
    return;
}

アセンブルすると、次のようになる。ほらね。

  00234060  push        ebp                                ; (01)
  00234061  mov         ebp,esp                            ; (02)
  00234063  sub         esp,0CCh                           ; (03)
  00234069  push        ebx                                ; (04)
  0023406A  push        esi                                ; (05)
  0023406B  push        edi                                ; (06)
  0023406C  lea         edi,[ebp-0CCh]                     ; (07)
  00234072  mov         ecx,33h                            ; (08)
  00234077  mov         eax,0CCCCCCCCh                     ; (09)
  0023407C  rep stos    dword ptr es:[edi]                 ; (10)
  0023407E  mov         dword ptr [ebp-8],0                ; (11) i = 0;
  00234085  cmp         dword ptr [ebp-8],64h              ; (12) ● 条件 0x64 以上?
  00234089  jge         main+47h (02340A7h)                ; (13) ■ ジャンプ
  0023408B  mov         eax,dword ptr [ebp-8]              ; (14) 第 2 引数 & ESP - 4
  0023408E  push        eax                                ; (15) 第 1 引数 & ESP - 4
  0023408F  push        offset string "%d\r\n" (0236BCCh)  ; (16) _printf
  00234094  call        _printf (0231361h)                 ; (17) _printf
  00234099  add         esp,8                              ; (18) ESP + 8(もとに戻す)
  0023409C  mov         eax,dword ptr [ebp-8]              ; (19)
  0023409F  add         eax,1                              ; (20)
  002340A2  mov         dword ptr [ebp-8],eax              ; (21)
  002340A5  jmp         main+25h (0234085h)                ; (22) ● 無条件ジャンプ
  002340A7  xor         eax,eax                            ; (23) ■
  002340A9  pop         edi                                ; (24)
  002340AA  pop         esi                                ; (25)
  002340AB  pop         ebx                                ; (26)
  002340AC  add         esp,0CCh                           ; (27)
  002340B2  cmp         ebp,esp                            ; (28)
  002340B4  call        __RTC_CheckEsp (0231118h)          ; (29)
  002340B9  mov         esp,ebp                            ; (30)
  002340BB  pop         ebp                                ; (31)
  002340BC  ret                                            ; (32)

(22) 行目から (12) 行目への無条件(アンコンディショナル)ジャンプが、ループのキモ。
for 文のときと違って、インクリメントのセクションがない。もちろん、(19) 〜 (21) 行目で、i をインクリメントしているのだが。
そして、(13) 行目のコンディショナルジャンプで、ループを抜ける。

*1:引数の名前は、これまでとそろえるために a, b のままにした。本当は、from, to とかにすればよいのだろうけど。