リバースエンジニアリング

Last modified: 2005/02/02 17:10:01

はじめに

最初に断っておきますが、私はKrackerではありませんし、またリバースエンジニアリングについてさほど詳しいわけでもありません。そんな人が「リバースエンジニアリング」などと銘打って文章を書くこと自体がそもそもおかしいですが、今回は、私がリバースエンジニアリングについていろいろと調べた結果をテキストとしてまとめてみようということでこの文章を書き上げました。よって、この文章は私がここ1ヶ月くらいで学んだ過程を書き綴っています。ただ、まとめたといっても、デバッガの使い方といった初歩の部分から書いているわけではないので、少なくともアセンブリ言語を理解していることが前提となります。また場合によっては、WindowsやDLLの仕組み、そして暗号アルゴリズムに関してもある程度の知識が必要かもしれません。

実験を行った私の環境はWinXP、コンパイラはVC++.NET、デバッガはOllyDbgですので、その他の環境の方はいろいろと内容に違いがでてくるかもしれません。ご了承ください。

リバースエンジニアリングとは

リバースエンジニアリングとは、ソフトウェアやハードウェアなどを分解、あるいは解析し、その仕組みや仕様、目的、構成部品、要素技術などを明らかにすること。プログラムの分野では、モジュール間の関係の解明やシステムの基本仕様の分析といった行為を含む。

一般にはあまり良いイメージがないが、仕様書と実装の食い違いを指摘したり、セキュリティホールやバグの発見につながるなど、システム保守やセキュリティ強化の面で役立つこともある。(IT用語辞書「e-Words」より)

crackme.exe

では、とりあえず以下のプログラムをDLしてください。

crackme.exe

「crackme.exe」ですが、その名の通りクラックするためのEXEファイルです。それで、さっそく実行してみると、どうやら1192年5月15日にしか実行できないというダイアログが表示され、勝手にプログラムが終了してしまいます。

crackme.exeのエラーメッセージ

1192年は確か鎌倉幕府ができた年です。今年の大河ドラマは「義経」なので、ちょうどこの時代に作成されたEXEファイルということになります。面白くない冗談はさておき、この時間制限をどうやってクリアするか? まずは、Windowsの時刻設定を変更して対応する、といったもっともシンプルな方法が思いつきますが、さすがのWindowsXPも1980年以降の時間しか設定することはできません。1192年に設定することはできないので、却下となります。

次に、デバッガ(OllyDbgなど)を使って、内容を解析するといったことが考えられます。では、とりあえずOllyDbgで中身を見てみることにしましょう。OllyDbgはフリーのデバッガでありOllyDbgのページからダウンロードすることができます。OllyDbgに関してはDigital Travesiaがとても参考になります。その他、日本語化パッチや、DokoDonさん作成のOllyDbgプラグイン「OllyDump」やその他有用なツール盛りだくさんですのでぜひ参考にしてください。

-----  crackme.exe (OllyDbg)
00402900 >/$ 81EC 14080000  SUB ESP,814
00402906  |. A1 78934000    MOV EAX,DWORD PTR DS:[__security_cookie]
0040290B  |. 338424 1408000>XOR EAX,DWORD PTR SS:[ESP+814]
00402912  |. 898424 1008000>MOV DWORD PTR SS:[ESP+810],EAX
00402919  |. 8D4424 00      LEA EAX,DWORD PTR SS:[ESP]
0040291D  |. 50             PUSH EAX                                 ; /pSystemTime
0040291E  |. FF15 10704000  CALL DWORD PTR DS:[<&KERNEL32.GetSystemT>; \GetSystemTime
00402924  |. 66:8B4424 00   MOV AX,WORD PTR SS:[ESP]
-----

OllyDbgで開くこのような場所からスタートするのが分かります。その場所から少し下へ進んでいくと、GetSystemTimeというAPIが表示されています。さらにその下にはDialogBoxParamが呼び出されています。

-----  crackme.exe (OllyDbg)
00402945  |. 8B8C24 1808000>MOV ECX,DWORD PTR SS:[ESP+818]
0040294C  |. 6A 00          PUSH 0                                   ; /lParam = NULL
0040294E  |. 68 A0274000    PUSH crackme.Dlg_Proc                    ; |DlgProc = crackme.Dlg_Proc
00402953  |. 6A 00          PUSH 0                                   ; |hOwner = NULL
00402955  |. 6A 65          PUSH 65                                  ; |pTemplate = 65
00402957  |. 51             PUSH ECX                                 ; |hInst
00402958  |. FF15 CC704000  CALL DWORD PTR DS:[<&USER32.DialogBoxPar>; \DialogBoxParamA
0040295E  |. EB 38          JMP SHORT crackme.00402998
-----

よって、この辺りに時間条件判定のコードがあることが分かります。というか、GetSystemTime関数で現在の時間を取得して、「1192年5月15日」ではなかったらプログラムを終了するという処理が行われているのだろうと推測できます。さらに、その判定処理は、GetSystemTimeより後であり、DialogBoxParamAより前であることが予想できます。なのでその間の処理を調べてみます。

-----  crackme.exe (OllyDbg)
00402924  |. 66:8B4424 00   MOV AX,WORD PTR SS:[ESP]
00402929  |. 66:3D A804     CMP AX,4A8
0040292D  |. 66:8B4C24 06   MOV CX,WORD PTR SS:[ESP+6]
00402932  |. 66:8B5424 02   MOV DX,WORD PTR SS:[ESP+2]
00402937  |. 75 27          JNZ SHORT crackme.00402960
00402939  |. 66:83FA 05     CMP DX,5
0040293D  |. 75 21          JNZ SHORT crackme.00402960
0040293F  |. 66:83F9 0F     CMP CX,0F
00402943  |. 75 1B          JNZ SHORT crackme.00402960
-----

重要なのは、3つのJNZ命令であり、すべて00402960へジャンプしようとしています。追ってみれば分かりますが、その先はエラーを知らせるメッセージボックスの処理となります。つまり、ここでジャンプしないように進めればプログラムが実行できるということです。JMP命令と同じくCMP命令も3つあります。これで比較を行っているわけですが、GetSystemTime関数呼出し後、スタックから取り出してきた値をレジスタに格納しています。AXレジスタにはおそらく「年」が、DXレジスタには「月」が、CXレジスタには「日」が入るだろうことが予測できます。3つのCMP命令による比較と、3つのJNZ命令によるジャンプです。これらのデータからどの部分を書き換えれば、エラーメッセージを出さずに正常にプログラムを起動できるかを考えると、答えは明白です。3つのJNZ命令は、すべてSHORTジャンプなので2バイトで表現されています。よって3つのJNZ命令、計6バイトのデータをNOPに書き換えることで、プログラムを正常に起動できそうです。

しかし、ソフトウェアのKrackingパッチは、変更するデータをなるべく少なくした方がエレガントであるらしいので、なるべく変更箇所を少なくしましょう。ということで、最初のJNZ命令のジャンプ先を00402960から00402945へ変更することにしましょう。これなら1バイトを書き換えるだけで、プログラムを実行できるようになります。

では、最初のJNZ命令があるアドレス00402937の「75 27」の2バイトを「75 0C」に変更してください。OllyDbgでは、ポインタを変更箇所に合わせて「右クリック→バイナリ→編集」で任意の場所を編集することができます。あとはそれを実行ファイルへ書き込まなければならないので、「右クリック→実行ファイルへコピー」を選択して、実行ファイルへ書き込むと、無事、書き込み後のEXEファイルが作成されます。そして、作成されたEXEファイルをダブルクリックすると、エラーが表示されずにプログラムは終了せずに、ウィンドウが表示されます。

crackme.exeの実行

変更前と変更後の値である「7527」と「750C」は何を意味しているのか? 実は「75 27」はアセンブリ言語で「jnz 27」を意味します。そして、「75 0C」は「jnz 0C」です。つまり、「75 27」は相対アドレスで27バイトの位置にジャンプすることを意味しています。それを、相対アドレスで0Cバイトの位置にジャンプさせるように変更することで、時間判定処理をジャンプさせているわけです。これは、アセンブリ言語に対応したマシン語ですので、理解するためにはアセンブリ言語の知識が必要となります。

パッキング

パッキングとは、EXEファイルを実行できる状態のまま圧縮することです。そして、それを行うツールのことをパッカーと呼びます。パッカーとは、本来、EXEファイルを実行できる状態のまま圧縮してくれるというとても便利なツールです。ただ、最近では、圧縮してくれるだけではなく、リバースエンジニアリングを行いにくくするためにも使用されています。というより、むしろこのような「アンチKrack対策」として利用される方が多いようです。では実際どのようなものなのかを理解するために、使ってみることにしましょう。パッカーにはいくつもの種類が存在しますが、今回は、オープンソースで有名なUPXを使ってcrackme.exeのパッキングを行ってみます。UPXのサイトからDLしてください。私は「UPX ver1.24」をDLしました。

前回使ったcrackme.exeはすでに実行できるようにバイナリを書き換えてしまったので、新しくcrackme.exeをDLするか、もしくは、変更箇所を元の値である「27」に戻してください。そして、そのcrackme.exeをUPXを使ってパッキングしてください。パッキングするのがメンドクサイという方は以下にcrackme.exeをパッキングしたpcrackme.exeをDLしてください。ただし、以後パッキングされたものをcrackme.exeと呼ぶので、DLされた方はファイル名をpcrackme.exeからcrackme.exeに変更してください。

pcrackme.exe
-----  コマンドプロンプト
C:\>upx crackme.exe
                     Ultimate Packer for eXecutables
         Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002
UPX 1.24w        Markus F.X.J. Oberhumer & Laszlo Molnar         Nov 7th 2002

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
     26624 ->     15872   59.61%    win32/pe     crackme.exe

Packed 1 file.
C:\>
-----

パッキングが完了したら、まずはcrackme.exeのサイズをみてください。明らかにファイルサイズが小さくなっています。そして、もちろんファイルサイズが小さくなっても実行することができます。ただ、お馴染みの「1192年5月15日にしか実行できません」というようなメッセージボックスが表示されてプログラムが終了してしまいます。なので、もう一度OllyDbgからバイナリを変更することにしましょう。ということで、OllyDbgでUPXパッキングがなされたcrackme.exeを開いてください。変なエラーメッセージができるかもしれませんが、無視して構いません。

-----  crackme.exe [UPX版] (OllyDbg)
0040CC80 > $ 60             PUSHAD
0040CC81   . BE 00904000    MOV ESI,crackme.00409000
0040CC86   . 8DBE 0080FFFF  LEA EDI,DWORD PTR DS:[ESI+FFFF8000]
0040CC8C   . 57             PUSH EDI
0040CC8D   . 83CD FF        OR EBP,FFFFFFFF
0040CC90   . EB 10          JMP SHORT crackme.0040CCA2
-----

するとパッキング前とは全然違うコードになっています。どこにもGetSystemTime関数はありませんし、何かわけの分からないことを行っており、意図したプログラムのソースコードになっていません。しかし、実行することは可能なのです。つまり、パッカーを使うと実行ファイルのサイズが小さくなり、かつ、内部構造を簡単に解析させないようにさせることが可能なのです。うーん、なんて便利なんだろう。つまり、これはリバースエンジニアリングを行う者にとってはやっかいな代物となるわけです。最初にパッカーを作ったのが誰かは知りませんが、さぞかし「Krackerのやつらめ、ざまーみろ!」と思ったに違いありません(ホントか?(笑))。

しかし、天下の解析屋Krackerも負けてはいません。彼らはこのようなパッキングされたEXEファイルを解析し、元のEXEファイルに戻す手法を開発(?)しました。それを「アンパッキング」と呼びます。どのような複雑なパッキングがなされていても、彼らはそれを破り、元のEXEファイルを復元させてしまうのです。さらに「アンパッカー」と呼ばれるパッキングを解除するツールまで開発しました。まさに彼らは「ソフトウェアに不可能は無い」という、その言葉通りのことを証明したのでした。めでたし、めでたし(完)。

アンパッキング

えっと、何の話でしたっけ? ああーそうそう、パッキングですね。それで、今度はそのパッキングされたcrackme.exeをアンパッキングしてみたいと思います。といっても、そもそもUPXは「-d」コマンドを使えば復元(アンパック)できるので、わざわざ手動でアンパッキングするのもどうかと思いますが、何事も練習と思ってやってみます。

UPXにてパッキングされたcrackme.exeをOllyDbgで開くと、以下のように表示されます。

-----  crackme.exe [UPX版] (OllyDbg)
0040CC80 > $ 60             PUSHAD
0040CC81   . BE 00904000    MOV ESI,crackme.00409000
0040CC86   . 8DBE 0080FFFF  LEA EDI,DWORD PTR DS:[ESI+FFFF8000]
0040CC8C   . 57             PUSH EDI
0040CC8D   . 83CD FF        OR EBP,FFFFFFFF
0040CC90   . EB 10          JMP SHORT crackme.0040CCA2
-----

パッキングとは、簡単に言えば「EXEファイルを圧縮しているだけ」です。ただ、圧縮するだけだったらZIPやLHAと同じですが、実行できる状態で圧縮してる部分が大きく違います。それで、実行できる状態で圧縮するわけですから、当然パッキングされたcrackme.exeには、オリジナルなcrackme.exeの処理の前に展開(解凍)ルーチンが付加されていることになります。ZIPで圧縮されたEXEファイルは、まず展開(解凍)してから生成されたファイルをダブルクリックで実行しますよね。考え方はそれと同じです。ただ、パッキングされたEXEファイルの場合は、EXEファイルの中に展開(解凍)する処理も組み込まれているだけなのです。つまり、EXEファイルが実行されたら、最初に、圧縮されたオリジナルcrackme.exeをメモリ上に展開(解凍)する処理を実行します。そして展開(解凍)後、オリジナルcrackme.exeの先頭アドレス(「Original Entry Point」略して「OEP」)へジャンプして、実際のcrackme.exeの処理を行うわけです。すっごい分かりにくい説明のような気がしますが、私の文章力ではこの辺りが限界ですので、あとは、googleで調べてください(^^;。

要するに、パッキングを行ったら「本来のデータが圧縮されて、さらにその圧縮されたデータの展開(解凍)を行う処理が最初に追加される」ということです。ということは、パッキングされたEXEファイルから本来の圧縮されてないデータ部分を取り出すためには、展開(解凍)処理が行われたすぐあとのメモリ状態をダンプすればよいということになります。なので「OEP」を探す必要が出てくるというわけなんです。よって、アンパッキングを行う際に、最初にやらなければならないこととは、その「オリジナルcrackme.exeの先頭アドレス」である「OEP」を探すこととなります。

では、早速探しましょう。OllyDbgの最初の1行は「PUSHAD」命令です。レジスタの内容をすべてスタックに退避するといった便利な命令ですが、では、これに対応する「POPAD」を見つけてください。PUSHADをずっと下に検索していった先にあると思います。

-----  crackme.exe [UPX版] (OllyDbg)
0040CDC6   .^EB E1          JMP SHORT crackme.0040CDA9
0040CDC8   > FF96 9CC00000  CALL DWORD PTR DS:[ESI+C09C]
0040CDCE   > 61             POPAD
0040CDCF   .-E9 505CFFFF    JMP crackme.00402A24
-----

見事にみつけたら、そのPOPAD以下のJMP命令にブレークポイントを仕掛け、プログラムを実行します。実行したら、当然のごとくこの位置で止まりますので、F8で処理を1回進めてください。

-----  crackme.exe [UPX版] (OllyDbg)
00402A24   6A 60            PUSH 60
00402A26   68 78724000      PUSH crackme.00407278
00402A2B   E8 70030000      CALL crackme.00402DA0
00402A30   BF 94000000      MOV EDI,94
....(省略)....
00402A43   56               PUSH ESI
00402A44   FF15 48704000    CALL DWORD PTR DS:[407048]               ; kernel32.GetVersionExA
00402A4A   8B4E 10          MOV ECX,DWORD PTR DS:[ESI+10]
-----

するとアドレス「00402A24」にジャンプします。ちょっと下に「kernel32.GetVersionExA」というありふれたAPI名がありますので、どうやらここがOEPだと決めうちします。実際、OEPを探しますと言いましたが、OEPを探す一定の法則は無いそうです。まぁ考えてみれば当たり前ですよね。展開(解凍)を行う処理がどこで終わるのかなんて分かるわけありません。そういうことはパッカーを作成した人に聞いてください。なので、地道にマシン語を追いかけるしかありません。ただPUSHADやPOPADが少なからず手がかりになるようなので、それを元に運と勘とニオイで探し当ててください。アンパッキングは予想以上にメンドクサイ作業なのです。

OEPで止まったら、当然のごとくプラグインとしてインストールされているOllyDumpを使って、メモリ状態をファイルに落とします。とりあえずファイル名は「crackme_dump.exe」としておきます。その時に「get EIP as OEP」ボタンをクリックして算出されたアドレス「2A24」を覚えておきます。

OllyDump

「OllyDumpって何?」という方はココからg_ollydump221b.zipというファイルをDLして、プラグインとしてOllyDbgに組み込んでおいてください。Digital Travesiaから辿ることができます。

アンパッキングはまだまだ続きます。

現在、手元にあるのは、OllyDumpを使ってダンプした「crackme_dump.exe」と「get EIP as OEP」ボタンを押したときに算出されたアドレス「2A24」です。それで、メモリ状態をダンプしてきた「crackme_dump.exe」早速実行しても、残念ながら実行することができません。以下のようなエラーメッセージがでます。

crackme_dump.exeのエラーメッセージ

 それで、次にIATの再構築というものをやらなければなりません。IATというのは「Import Address Table」の略だそうです。「何それ?」という方はPEフォーマットについて勉強しましょう。マシン語大研究というサイトにPEフォーマットの詳細というものがありますので、それを参考にしてください。

それで、通常はこのIATの再構築をツールを使って行います。例えば、スペシャルねこまんま57号です。汎用プロセスメモリエディタ兼デバッガと銘打ってますが、ものすごい高機能なツールで、汎用プロセスメモリエディタ兼デバッガにとどまりません。IATの再構築なんて朝飯前っぽい雰囲気を漂わせているので、使ってみてください。他にもいろいろと使い道があり、本当にすばらしいツールのようです。

ただ、読者の方には、「ハードディスクの容量が少ないので、新しくツールをDLするなんて夢のまた夢です」という人や「ツールなんてコンパイラとデバッガがあれば十分、仕組みを教えてくれりゃーあとは自分で作るぜ!」という人もおられるかもしれません。なので、今回はIATの再構築をバイナリエディタでやってみようと思います。バイナリエディタに私はStirlingを使用しています。

では、Stirlingを起動して「crackme_dump.exe」を読み込んでください。まずは「Import Address Table」を探します。探しますといってもPEフォーマットを知っている人だったら簡単に探せるんですが、知らない人だったら、ただの数字の羅列にしか見えませんので、ここでOllyDbgを使います。OllyDbgでパッキングされた「crackme.exe」を開きます。別にメモリイメージをダンプした「crackme_dump.exe」でもよいですが、Stirlingで読み込んでいる状態だとファイルの排他制御が働いて、Stirlingからファイルへ書き込めなくなる恐れがあるので、「crackme.exe」の方がよいです。

OllyDbgで「右クリック→検索→ラベル名」もしくは「Ctrl + N」とすると、新しくウィンドウが開きます。すると以下のように表示されていると思います。

-----  現在のモジュール内検索(OllyDbg)
0040D09C   .rsrc      Import  (  KERNEL32.ExitProcess
0040D098   .rsrc      Import  (  KERNEL32.GetProcAddress
0040D094   .rsrc      Import  (  KERNEL32.LoadLibraryA
0040D0A4   .rsrc      Import  (  USER32.wsprintfA
00402A24   UPX0       Export     <モジュールエントリーポイント>
-----

それでタイプに「Import」となっているAPIのアドレスを見てみるとKERNEL32.ExitProcessの場合だと「0040D09C」となっています。400000はベースアドレスですので無視します。すると0000D09Cというアドレスが算出されます。バイナリエディタで0000D09Cの部分を調べてください。

-----  crackme_dump.exe(変更前)(stirling)
0000D090    00 00 00 00 D8 05 E4 77 FD A5 E3 77 B5 5C E3 77
0000D0A0    00 00 00 00 6A C9 CF 77 00 00 00 00 4B 45 52 4E
0000D0B0    45 4C 33 32 2E 44 4C 4C 00 55 53 45 52 33 32 2E
0000D0C0    64 6C 6C 00 00 00 4C 6F 61 64 4C 69 62 72 61 72
0000D0D0    79 41 00 00 47 65 74 50 72 6F 63 41 64 64 72 65
0000D0E0    73 73 00 00 45 78 69 74 50 72 6F 63 65 73 73 00
0000D0F0    00 00 77 73 70 72 69 6E 74 66 41 00 00 00 00 00
-----

0000D09Cからの4バイトは「B5 5C E3 77」となっています。それを正しいアドレスである「E2 D0 00 00」に変更してください。そのままバイナリエディタを見てください。crackme_dump.exeのアドレス0000D0E2には、ExitProcessという文字列が存在しているのが確認できます。次に進みます。OllyDbgを見ると次はGetProcAddressのアドレス「0040D098」ですね。ベースアドレス400000を省いて0000D098として、バイナリエディタからこのアドレスの位置にあるデータを調べると「FD A5 E3 77」です。そのデータを正確なGetProcAddress文字列のアドレス「D2 D0 00 00」に変更します。文字列のアドレスといっても実際には、文字列の先頭より2バイト前のアドレスとなっていますが、この説明は割愛します。これについてはPEフォーマットを学んでください。このようにして、OllyDbgを見ながらcrackme_dump.exeのIATを変更していきます。この例だと4箇所、計16バイトありますね。すべて変更すると、以下のようになります。

-----  crackme_dump.exe(変更後)(stirling)
0000D090    00 00 00 00 C4 D0 00 00 D2 D0 00 00 E2 D0 00 00
0000D0A0    00 00 00 00 F0 D0 00 00 00 00 00 00 4B 45 52 4E
0000D0B0    45 4C 33 32 2E 44 4C 4C 00 55 53 45 52 33 32 2E
0000D0C0    64 6C 6C 00 00 00 4C 6F 61 64 4C 69 62 72 61 72
0000D0D0    79 41 00 00 47 65 74 50 72 6F 63 41 64 64 72 65
0000D0E0    73 73 00 00 45 78 69 74 50 72 6F 63 65 73 73 00
0000D0F0    00 00 77 73 70 72 69 6E 74 66 41 00 00 00 00 00
-----

上から2行目(32ビット)までに変更データがあります。下5行は変更していません。これでIATの再構築は終了です。では、「crackme_dump.exe」を実行してみてください。無事実行されて、時間うんぬんのエラーメッセージがでたら成功です。これでアンパッキングは終了となります。が、あくまでもアンパッキングが終了しただけで、ここから一般的な解析が始まるわけです。

IATの再構築を行った「crackme_dump.exe」をOllyDbgで開いてください。変な警告がいくつか表示されるかもしれませんが、無視して構いません。

-----  crackme_dump.exe (OllyDbg)
00402A24 > 6A 60            PUSH 60
00402A26   68 78724000      PUSH crackme_.00407278
00402A2B   E8 70030000      CALL crackme_.00402DA0
00402A30   BF 94000000      MOV EDI,94
00402A35   8BC7             MOV EAX,EDI
-----

こんな感じになっているはずです。では「右クリック→検索→同上全モジュール内」を選択して、GetSystemTimeにブレークポイントを仕掛けます。そして「解析→実行」で実行させると、

-----  crackme_dump.exe (OllyDbg)
77E21608 > 55               PUSH EBP
77E21609   8BEC             MOV EBP,ESP
77E2160B   83EC 18          SUB ESP,18
77E2160E   A1 1800FE7F      MOV EAX,DWORD PTR DS:[7FFE0018]
-----

で、止まります。ここはおそらく関数内なので、「F8」ボタンを押してRET命令まで処理を進めます。

-----  crackme_dump.exe (OllyDbg)
77E21673   66:8948 0E       MOV WORD PTR DS:[EAX+E],CX
77E21677   C9               LEAVE
77E21678   C2 0400          RETN 4
77E2167B > A1 1800FE7F      MOV EAX,DWORD PTR DS:[7FFE0018]
-----

アドレス77E21678がRET命令なので、そこでさらにもう一度「F8」を押すと関数を抜けます。そして

-----  crackme_dump.exe (OllyDbg)
00402924   66:8B4424 00     MOV AX,WORD PTR SS:[ESP]
00402929   66:3D A804       CMP AX,4A8
0040292D   66:8B4C24 06     MOV CX,WORD PTR SS:[ESP+6]
00402932   66:8B5424 02     MOV DX,WORD PTR SS:[ESP+2]
00402937   75 27            JNZ SHORT crackme_.00402960
00402939   66:83FA 05       CMP DX,5
0040293D   75 21            JNZ SHORT crackme_.00402960
0040293F   66:83F9 0F       CMP CX,0F
00402943   75 1B            JNZ SHORT crackme_.00402960
-----

このような場所に辿り着きます。ここは以前見たことがありますね。そうです、3つのJNZ命令と3つのCMP命令です。前回と同じように、アドレス00402937の「75 27」を「75 0C」に変更して、保存してください。保存したEXEファイルを実行すると、見事プロテクトが外れてウィンドウが表示されます。

crackme.exeの実行

「アンパッキングってややこしい!」というのが私の感想です。なんかいろいろなツールを使い分けて、あーだこーだとやらなければならないわけです。さらにパッキングがひとつだけしか施されてないならばまだ良いのですが、2重3重とパッキングされていたら、もうやる気でません(笑)。ということで、次は別のアプローチで、このパッキングされたcrackme.exeのプロテクトを破っていくことにします。

APIフック

WindowsのAPIは、kernel32.dllやuser32.dllといったような、DLLファイルとして実装されています。よって、プログラム(EXEファイル)は、実行時にこれらのDLLを暗黙のうちにリンクしなければなりません。これはすなわち、APIはEXEファイルに実装されているわけではない、というなんとも当たり前な結論に行き着きます。

そこで、例えば時間を取得するAPIであるGetLocalTimeやGetSystemTimeを使って、ソフトウェアの使用期限を設けているツールがあるとします(crackme.exeですね)。このようなツールは、GetLocalTimeやGetSystemTimeから渡されたデータ(時間)が正しいということを前提としています。しかし、そもそもGetLocalTimeやGetSystemTimeといったAPIはEXEファイルではなく、外部のDLLファイルにて実装されているわけであり、このデータを信頼することは必ずしも安全ではないと考えられます。何故なら、kernel32.dllとまったく同じエクスポート関数を持っており、まったく同じ挙動を示すlernel32.dllを作成してしまえば、そのDLLは、kernel32.dllに取って代わることができるからです。

kernel32.dllとまったく同じエクスポート関数を持っているDLLを作成すれば、それはEXEファイルにkernel32.dllと認識させることが可能となります。しかし、本来kernel32.dllやuser32.dllといったWindowsの動作に直接関係するDLLは、プログラムの実行時に特別なカタチでリンクされることになるので、本物のkernel32.dllがリンクされる前に、偽物のkernel32.dllをリンクさせることはできません。よって、EXEファイルのバイナリを多少変更する必要がでてきます。

UPXでパッキングされたcrackme.exeをバイナリエディタ(stirling)で開き「KERNEL32.DLL」をキーワードに文字列検索を行います。すると、以下のアドレスで見つかります。

-----  crackme.exe (stirling)
000042A0    00 00 00 00 F0 D0 00 00 00 00 00 00 4B 45 52 4E
000042B0    45 4C 33 32 2E 44 4C 4C 00 55 53 45 52 33 32 2E
-----

ここでヒットします。では、試しに「KERNEL32.DLL」の「K」の部分を「L」に変更してください。そして、保存しプログラムを実行してみます。

-----  crackme.exe (stirling) 変更後
000042A0    00 00 00 00 F0 D0 00 00 00 00 00 00 4C 45 52 4E
000042B0    45 4C 33 32 2E 44 4C 4C 00 55 53 45 52 33 32 2E
-----

「LERNEL32.dll」というようにバイナリを変更した状態でプログラムを実行すると、「LERNEL32.dllが見つかりません」というダイアログボックスが表示され、プログラムが起動しません。これは当たり前で、どのフォルダを探しても「LERNEL32.dll」なんていうDLLは存在しませんので、このようなダイアログが表示されるわけです。

DLLが存在しないエラーメッセージ

さて、ここからがポイントです。lernel32.dllが無いならば、作りましょう。作るといっても、適当なDLLファイルではいけません。EXEファイルはkernel32.dllと思ってlernel32.dllをリンクするわけですから、これから作るlernel32.dllは、kernel32.dllとエクスポート関数、挙動が同じでなければなりません。そのためには、まずkernel32.dllのエクスポート関数を調べる必要があります。そんなものはいったいどこに書かれてあるのか? それは、もちろんkernel32.dllに書かれてあります。というわけで、Windowsシステムフォルダからkernel32.dllをコピーしてください。間違ってもシステムフォルダからkernel32.dllを消さないように。

さて、エクスポート関数を調べるということですが、根性のある人はバイナリエディタでkernel32.dllを開いて、エクスポート関数のようなものを、ひとつずつ手動でコピーしていけばOKです。でも、kernel32.dllのエクスポート関数は約1000個ほどあるので、とてもじゃないですが、根性のない私にはできません(^^;。なので、エクスポート関数を集めるツールを作りました。

仲介DLL生成補助ツール

システムフォルダのkernel32.dllをlistexport.exeと同じフォルダにコピーして、コマンドプロンプトから以下のように実行します。

-----  コマンドプロンプト
C:\..\wb15>listexport kernel32.dll > kernel32exp.txt
C:\..\wb15>
-----

すると、同じフォルダにkernel32exp.txtというテキストファイルが作成されますので、それをテキストエディタで開くと、kernel32.dllのエクスポート関数が列挙されています。

-----  kernel32exp.txt
序数: 0001    名前: ActivateActCtx
序数: 0002    名前: AddAtomA
序数: 0003    名前: AddAtomW
序数: 0004    名前: AddConsoleAliasA
......(省略)......
序数: 039D    名前: lstrcpynW
序数: 039E    名前: lstrlen
序数: 039F    名前: lstrlenA
序数: 03A0    名前: lstrlenW
-----

listexport.cppがソースコードですが、解説は割愛させていただきます。というのも、このソースはどんなに説明しても、PEヘッダフォーマットを理解していないと読めないからです。そして、逆に理解していれば解読は容易にできます。つまり、解説したとしても、ソースの解説ではなくPEヘッダフォーマットの解説になってしまい、この記事とは主旨がずれてしまうからです。というわけで、興味のある方は、読んでみてください。コメントも不必要なほどつけてますので(笑)。

さて、現在crackme.exeのバイナリをLERNEL32.dllにしてしまったので、crackme.exeが実行できなくなっています。なので、これからlernel32.dllを作成することにします。再びlistexport.exeを使います。-fileオプションをつけて以下のように実行してください。

-----  コマンドプロンプト
C:\..\wb15>listexport -file kernel32.dll lernel32
ソースファイル lernel32.cpp, lernel32.def を生成しました
C:\..\wb15>
-----

すると「ソースファイル lernel32.cpp, lernel32.def を生成しました」を表示されます。これでlistexport.exeと同じフォルダにこれらのファイルが生成されています。あとはこの2つのファイルをVC++などでビルドするだけで、kernel32.dllとまったく同じエクスポート関数を持ったlernel32.dllを作成することができます。

まず、VC++で「lernel32」という空のDLLプロジェクトを作成します。そして、これら2つのファイルをプロジェクトに追加し、あとはビルドを行うだけです。ただし、VC++.NETに関しては少し違います。VC++.NETはとても不親切で、.defファイルをプロジェクトに追加しても、ビルド時のリンカオプションに.defファイルを追加してくれません。これはバグなんじゃないのかと私は思いますが、とにかくダメなので、VC++.NETに関しては、「ファイル→新しい項目の追加」で新しくlernel32.defファイルを作成して、そこにlistexportで生成されたlernel32.defの内容をコピーしてください。.cppに関しては問題ありませんので、そのままプロジェクトに追加してください。

あと、ひとつlistexportを利用する上で重要なこととして、.defファイルはその性質上「@マーク」を関数名に含んだ関数を扱えません。その他特殊文字も同様です。よって、そのような関数名を持つDLLを偽装する場合は、あらかじめ「@」を「*」などに置換しておいてビルドを行い、DLLになった状態で、再度バイナリレベルで「*」を「@」に置換するといった方法をとってください。「crtdll.dll」などがそのようなDLLに該当します。

さて、山あり谷ありでなんとかlernel32.dllが作成できたら、crackme.exeと同じフォルダにコピーしてください。無事crackme.exeが実行できたら、成功となります。ただ、実行といっても年月日が違うので、すぐに終了してしまうんですけどね。

無事、lernel32.dllが作成できました。さて、問題はここからです。今度は、いよいよcrackme.exeの時間制限プロテクトを破らなければなりません。時間取得に利用していたAPIはGetSystemTimeでした。まず、このAPIをMSDNなどで検索して、戻り値や引数などを調べます。そしてこのAPIとまったく同じ挙動のエクスポート関数を作成します。

最初に、lernel32.cppの以下の行をコメントアウトします。おそらく1257行目辺りです。そして変わりの自作GetSystemTime関数を定義します。

-----  lernel32.cpp
//__declspec( naked ) void WINAPI d_GetSystemTime() { _asm{ jmp p_GetSystemTime } }
__declspec(dllexport) VOID WINAPI d_GetSystemTime(LPSYSTEMTIME t);
-----

この定義はMSDNなどで調べることで分かります。そして次に、自作GetSystemTimeを作成します。なので、lernel32.cppの一番下に以下のソースを追加します。

-----  lernel32.cpp
__declspec(dllexport) VOID WINAPI d_GetSystemTime(LPSYSTEMTIME t)
{	
	t->wYear         = 1192;
	t->wMonth        = 5;
	t->wDay          = 15;
	t->wDayOfWeek    = 0;
	t->wHour         = 1;
	t->wMinute       = 1;
	t->wSecond       = 1;
	t->wMilliseconds = 1000;
}
-----

とりあえず、年月日を「1192年5月15日」として、他の値は適当に設定します。これで完了です。プロジェクトを再度ビルドして、lernel32.dllをcrackme.exeと同じフォルダにコピーしてください。偽装kernel32.dllであるlernel32.dllを作成し、GetSystemTime APIを偽装しました。これでcrackme.exeを実行してください。見事実行されたら成功です。

crackme.exeの実行

最終的に作成した「lernel32」の.cppファイル、.defファイル、そして.dllファイルは、以下のZIPファイルにまとめました。

lernel32.zip

さて、このように、場合によってはアンパッキングしなくても破ることができることもあります。要するに物事は考え方次第ということです。ただ、もちろんこのテクニックは時間関連のプロテクトのみに有効です。なので、パスワードが必要となるプロテクトでは、残念ながらこのテクニックは使えませんので、地道にアンパッキングを行って破ってください。

実行されたcrackme.exe

さて、さまざまな方法でKrackされ、見事にプロテクトを解除されたcrackme.exeは、次にユーザー名とパスワードを必要とします。この実行されたcrackme.exeは、私が考えうる「Krackされないソフトウェア」を実現しています。リバースエンジニアリングのスキルに自信のある方はぜひ解いてみてください。その概要を次の章に書きます。

絶対にKrackされないソフトウェアの作り方を考える

絶対にKrackされないソフトウェアの作り方を考えます。ただし本当に、絶対にKrackされないソフトウェアを作るのは、まず無理です。ただし限りなくそれに近づくことは可能です。多数のパッカーを使用するのもひとつの方法ではありますが、今回はあえてパッカーを使用しない方法を考えてみます。

まず、リバースエンジニアリングにかけて、ものすごいスキルの持ち主である某氏の格言を紹介します。彼は「メモリ上にCPUが解釈できるマシン語が展開される限り、Krackできないことなど有り得ない!」と仰いました。かっこよすぎです! 感動しました。マジですごすぎです(汗)。つまり、「どんなに圧縮(パッキング)されてようが、CPUが解釈できるならば人間に解釈できないわけはない」という意味だと思います。もちろん、この格言はパッキングに限らず、すべてのアンチKrack対策に当てはまることだと思います。結局、CPUが解釈できるわけですから、人間にだって解釈できるわけです。よって、某氏からみると、CPUが解釈できるプログラムを組んでしまった時点で、すでにそのソフトはKrackできてしまうものですよ、というわけです。では、どうしましょうか?

ならばいっそ、CPUが解釈できないプログラムを組むってのはどうでしょう? バイナリエディタを開いて、自分の好きな数字を適当にどんどん書き込んでいきます。そしてファイル名hogehoge.exeで保存します。そして、そのファイルをダブルクリックしてみてください。まずCPUは解釈できません。これなら、絶対にKrackできないと思います。絶対にKrackできないソフトウェアの完成です。でも実行もできないと思います。なのでソフトウェアじゃないですね。ってゆーか、それただのバイナリデータやん。

ただ、この考えは一理あると思います。つまりCPUが解釈できないプログラムを書けばよいわけです。何が問題であるかというと、要するに「入力されたパスワードを判定する部分が存在する」からダメなのです。もっというと、この部分をCPUが解釈できてしまうからダメなのです。よって、パスワードを判定しないというのはどうでしょう? パスワードが正しいかどうかを判定しないわけです。ということで、私が考えた方法を次に示します。

まずは、正確なパスワードが入力されたあとの処理部分をすべて暗号化しておきます。つまり、正確なパスワードが入力されたことによって使用可能になるあらゆるプログラムを暗号化しておきます。そして、復号化のためのキーをパスワードに割り当てます。パスワードが入力されたら、正確なパスワードであるかどうかを確認せずに、そのパスワード(キー)を使って、以後の処理部分の復号化を行います。そして、復号化されたあとの処理部分へジャンプします。

もし、正確なパスワードが入力されていたら、復号化されたデータは正常なプログラムとなっているので問題ありません。そのまま処理が実行されます。しかし、間違ったパスワードが入力されていたら、間違ったキーで以後のプログラムの復号化処理を行ったことになるので、復号化は失敗します。復号化は失敗しているので、もちろん以後のプログラムは正常な動作を保障できません。つまり、処理部分にジャンプしたら間違いなく、プログラムがフリーズするでしょう。でもそれは仕方ないです。間違ったパスワードは入力しちゃダメだよ、ということです(ぉぃ。ただ、さすがにフリーズさせるのはまずいので、あらかじめ復号化されたデータのチェックサムを取っておきます。そしてそのチェックサムと比較して正しければ、99%の確立で正しいパスワードが入力されたと判断して、復号化したプログラムを実行します。

大抵の暗号アルゴリズムは、アルゴリズムと暗号文からキーと平文を求めるのは不可能であると数学的に証明されています。なので、パスワードを探しあてるには、アルゴリズムを再現して、総当りでキーと平文を探すしかありません。しかし、アルゴリズムを再現している時点でEXEファイルのソースコードを復元しているようなものですので、そこまでやってしまうKrackerの方にはお手上げということで、パスワードを教えても構わないでしょう(笑)。

この方法は、パスワード入力以後のプログラムをデータとしてマシン語で持っておかなければならないので、大きなプログラムだとまず実用的ではありません。というかこんなややこしい方法するくらいなら、ソフトウェアの質をもっと上げろ! と思います。ただ、例えばEXEファイル自体を実行できない形式に暗号化、あるいは圧縮を行い、別の認証EXEファイルを起動して、パスワードを入力させ、そこから暗号化、あるいは圧縮されたファイルをEXEファイルとして復元するといったような芸当は可能だと思います。というかパスワード認証をそなえたZIP圧縮のファイルが、おそらくそういうことをやっているんじゃないかなぁと推測しただけなのですが。でもこのZIP圧縮ファイルもKrackされたらしいですので、結局、絶対にKrackされないソフトウェアというのは作れないということでしょう。いやはや、リバースエンジニアリングも奥が深いです。

さいごに

さて、いかがだったでしょうか。今回は、リバースエンジニアリング繋がりで個人的に気になる内容を解説しました。実際に、PEフォーマットやDLLの仕組み、そしてリバースエンジニアリングや暗号論といった多方面の知識が必要になるものを目指したつもりですが、幅広い分だけそれぞれが浅かったりします。まさに、私のスキル不足を露呈しているようで、読者の方には申し訳ないです。ただ、何かを実現するときに多方面の知識が必要になることはよくあります。そのような場合に、取っ掛かりだけでも知っていると、案外考えが広がるものです。というわけで、今回はこのへんで終わりたいと思います。

では、また会う日まで...


Copyright (C) 2003-2005 Kenji Aiko All Rights Reserved