64ビット環境では、32ビット環境に比べ、マシン語に多少の違いがある。その違いによって、リバースエンジニアリング行為に、どの程度の影響が出るのかを調べた。今回は、その過程をテキストにまとめたものである。
なお、OSはWindowsXP(x64)、デバッガはWinDbg(x64)、コンパイラはVisualStdio2005を使用した。
とりあえず、どの程度の違いがあるのか、マシン語を読んでみる。
// prog1.cpp
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PSTR lpCmdLine,
int nCmdShow)
{
MessageBox(GetActiveWindow(),
L"This is test program.", L"Message", MB_OK);
return 0;
}
これを64ビット用にコンパイルして、WinDbgに読み込ませる。
そして、ブレイクポイントを設置して実行すると、WinMainで止まる。
// WinDbg(prog1.exe) 0:000> bp WinMain 0:000> bl 0 e 00000000`00401000 0001 (0001) 0:**** prog1!WinMain 0:000> g ModLoad: 000007ff`7d1f0000 000007ff`7d229000 C:\WINDOWS\system32\IMM32.DLL ModLoad: 000007ff`7fed0000 000007ff`7ffe4000 C:\WINDOWS\system32\ADVAPI32.dll ModLoad: 000007ff`7fd20000 000007ff`7fec9000 C:\WINDOWS\system32\RPCRT4.dll ModLoad: 000007ff`67b60000 000007ff`67b6d000 C:\WINDOWS\system32\LPK.DLL ModLoad: 000007ff`78be0000 000007ff`78c6a000 C:\WINDOWS\system32\USP10.dll Breakpoint 0 hit prog1!WinMain: 00000000`00401000 4883ec28 sub rsp,28h
ここで、アセンブルコードを出力する。
// WinDbg 00000000`00401000 4883ec28 sub rsp,28h 00000000`00401004 ff1536110000 call qword ptr [_imp_GetActiveWindow] 00000000`0040100a 4c8d05af110000 lea r8,[prog1!`string'] 00000000`00401011 488d15b8110000 lea rdx,[prog1!`string'] 00000000`00401018 488bc8 mov rcx,rax 00000000`0040101b 4533c9 xor r9d,r9d 00000000`0040101e ff1524110000 call qword ptr [_imp_MessageBoxW] 00000000`00401024 33c0 xor eax,eax 00000000`00401026 4883c428 add rsp,28h 00000000`0040102a c3 ret
よく分からないレジスタが多く使われているが、あまり気にしなくてよい。多分、すぐ慣れる。64ビット環境では、レジスタのサイズが64ビットになって、レジスタの種類が約2倍になった以外はあまり変わらないので、とりあえず、64ビット版の汎用レジスタrax、rcx、rdx、rbx、rsp、rbp、rsi、rdi辺りと、r8〜r15辺りを覚えておけばよいだろう。
まぁレジスタの話は置いておいて、関数呼び出しを見てみると、スタックが使われず、変わりにレジスタが使われている。MessageBoxWの引数と照らし合わせてみると、どのレジスタが何番目の引数か分かるだろう。まず、GetActiveWindowの戻り値はraxに格納されているはずだから、00401018より、rcxが第一引数と分かる。また、MessageBoxWの最後の引数MB_OKは、数値にすると0なので、0が入れられているレジスタを探すと、どうやらr9が該当する(r9dはr9の下位32ビット)。ここまでくると、あとはrdxが第二引数、r8が第三引数であると推測できる。つまり、次のようになる。
MessageBox(rcx, edx, r8, r9);
引数に関しては4つまでレジスタ、以後スタックという流れだが、call命令のリターン値については、スタックに積まれる。
// WinDbg 0:000> bp WinMain 0:000> g ModLoad: 000007ff`7d1f0000 000007ff`7d229000 C:\WINDOWS\system32\IMM32.DLL ModLoad: 000007ff`7fed0000 000007ff`7ffe4000 C:\WINDOWS\system32\ADVAPI32.dll ModLoad: 000007ff`7fd20000 000007ff`7fec9000 C:\WINDOWS\system32\RPCRT4.dll ModLoad: 000007ff`67b60000 000007ff`67b6d000 C:\WINDOWS\system32\LPK.DLL ModLoad: 000007ff`78be0000 000007ff`78c6a000 C:\WINDOWS\system32\USP10.dll Breakpoint 0 hit prog1!WinMain: 00000000`00401000 4883ec28 sub rsp,28h 0:000> t prog1!WinMain+0x4: 00000000`00401004 ff1536110000 call qword ptr [prog1!_imp_GetActiveWindow] 0:000> t USER32!GetActiveWindow: 00000000`78bfe020 b901000000 mov ecx,1 0:000> r rax=0000000000000000 rbx=000000000015312e rcx=0000000000400000 rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000 rip=0000000078bfe020 rsp=000000000012fe98 rbp=0000000000000000 r8=000000000015312e r9=000000000000000a r10=0000000050000161 r11=0000000000000000 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl nz na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206 USER32!GetActiveWindow: 00000000`78bfe020 b901000000 mov ecx,1 0:000>
GetActiveWindow関数呼び出し元は00401004だから、次の命令は0040100aとなる。そして、rspは000000000012fe98となっているため、このアドレス(スタック)の値を見ると、次のようになっている。
// WinDbg(Memory) 00000000`0012fe98 0a 10 40 00 00 00 00 00
スタックには、次の命令のアドレス(戻り場所)が記録されている。つまり、関数呼び出しの際に引数をpushすることはないが、call命令による戻り場所は、スタックに積まれることが分かった。続いて、引数がどこまでレジスタに頼るのかを調べる。スタック(メモリ)を使わないといっても、レジスタには限界がある。それを調べるため、次のプログラムを書いた。
// prog2.cpp
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PSTR lpCmdLine,
int nCmdShow)
{
CreateProcessAsUser(0,0,0,0,0,0,0,0,0,0,0);
return 0;
}
実行するわけではなく、マシン語が見たいだけなので、引数は適当だ。このコードをコンパイルしてマシン語にすると次のようになる。
// WinDbg(prog2.exe) 00000000`00401000 4883ec68 sub rsp,68h 00000000`00401004 33c0 xor eax,eax 00000000`00401006 4533c9 xor r9d,r9d 00000000`00401009 4533c0 xor r8d,r8d 00000000`0040100c 4889442450 mov qword ptr [rsp+50h],rax 00000000`00401011 4889442448 mov qword ptr [rsp+48h],rax 00000000`00401016 4889442440 mov qword ptr [rsp+40h],rax 00000000`0040101b 4889442438 mov qword ptr [rsp+38h],rax 00000000`00401020 89442430 mov dword ptr [rsp+30h],eax 00000000`00401024 89442428 mov dword ptr [rsp+28h],eax 00000000`00401028 33d2 xor edx,edx 00000000`0040102a 33c9 xor ecx,ecx 00000000`0040102c 4889442420 mov qword ptr [rsp+20h],rax 00000000`00401031 ff15c90f0000 call qword ptr [_imp_CreateProcessAsUserW] 00000000`00401037 33c0 xor eax,eax 00000000`00401039 4883c468 add rsp,68h 00000000`0040103d c3 ret
どうやら、レジスタで渡すのは引数4つまでで、それ以上はスタックを利用するようだ。しかし、pushやpopは使わず、ローカル変数のような形で、使用するメモリ領域を確保している。
マイクロソフト呼出規約によると、「rcx、rdx、r8、r9は整数型とポインタ型の引数に使用し、xmm0、xmm1、xmm2、xmm3は浮動小数点型引数に用いられる。そして、レジスタが足りなくなれば、スタックが使われ、戻り値はraxに格納される。」ということだ。とにかく、プロトコルと同じように、レジスタを使うにも決まり事があるのだろう。詳しく知りたい方は「Calling Convention for x64 64-Bit Environments」を参照のこと。最後に、ブログにてこれを教えてくれたiya氏サンクス!。
64ビット環境用のcrackmeを作成する。今回は、64ビットでもリバースエンジニアリングが可能かどうかを調べるのが目的なので、もっとも容易なcrackmeを作成する。
// crackme1.cpp(crackme1.cpp)
#include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PSTR lpCmdLine,
int nCmdShow)
{
SYSTEMTIME st;
GetSystemTime(&st);
if(st.wYear != 1192 || st.wMonth != 5 || st.wDay != 15){
TCHAR szErrorStr[2048];
wsprintf(szErrorStr,
L"このプログラムはUTC(世界標準時刻)で"
L"「1192年05月15日」にしか起動できません\r\n"
L"現在の日付は「%04d年%02d月%02d日」です\r\n",
st.wYear, st.wMonth, st.wDay);
MessageBox(GetActiveWindow(), szErrorStr, L"Message", MB_OK);
return 0;
}
MessageBox(GetActiveWindow(), L"OK!", L"Message", MB_OK);
return 0;
}
crackme.exeを実行すると、次のようにメッセージボックスが表示される。

見ての通り、時間指定の制限だ。GetSystemTime関数の戻り値で評価しているので、この部分を変更してやればOK。というわけで、さっそくデバッガで開く。
// WinDbg(crackme1.exe) ntdll!DbgBreakPoint: 00000000`78ed3320 cc int 3 0:000< bp WinMain 0:000< bl 0 e 00000000`00401000 0001 (0001) 0:**** crackme1!WinMain 0:000< g ModLoad: 000007ff`7d1f0000 000007ff`7d229000 C:\WINDOWS\system32\IMM32.DLL ModLoad: 000007ff`7fed0000 000007ff`7ffe4000 C:\WINDOWS\system32\ADVAPI32.dll ModLoad: 000007ff`7fd20000 000007ff`7fec9000 C:\WINDOWS\system32\RPCRT4.dll ModLoad: 000007ff`67b60000 000007ff`67b6d000 C:\WINDOWS\system32\LPK.DLL ModLoad: 000007ff`78be0000 000007ff`78c6a000 C:\WINDOWS\system32\USP10.dll Breakpoint 0 hit crackme1!WinMain: 00000000`00401000 b858100000 mov eax,1058h 0:000<
WinMainでブレイクポイントを設置して実行する。当然WinMainで停止する。ここで逆アセンブルする。
// WinDbg crackme1!WinMain: 00000000`00401000 b858100000 mov eax,1058h 00000000`00401005 e8060a0000 call crackme1!__chkstk 00000000`0040100a 482be0 sub rsp,rax 00000000`0040100d 488b05ec1f0000 mov rax,qword ptr [crackme1!__security_cookie] 00000000`00401014 4833c4 xor rax,rsp 00000000`00401017 4889842440100000 mov qword ptr [rsp+1040h],rax 00000000`0040101f 488d4c2430 lea rcx,[rsp+30h] 00000000`00401024 ff15d60f0000 call qword ptr [crackme1!_imp_GetSystemTime] 00000000`0040102a 0fb7542430 movzx edx,word ptr [rsp+30h] 00000000`0040102f 440fb75c2436 movzx r11d,word ptr [rsp+36h] 00000000`00401035 6681faa804 cmp dx,4A8h 00000000`0040103a 0fb74c2432 movzx ecx,word ptr [rsp+32h] 00000000`0040103f 751c jne crackme1!WinMain+0x5d 00000000`00401041 6683f905 cmp cx,5 00000000`00401045 7516 jne crackme1!WinMain+0x5d 00000000`00401047 664183fb0f cmp r11w,0Fh 00000000`0040104c 750f jne crackme1!WinMain+0x5d 00000000`0040104e ff15f4100000 call qword ptr [crackme1!_imp_GetActiveWindow] 00000000`00401054 488d151d120000 lea rdx,[crackme1!`string'] 00000000`0040105b eb2d jmp crackme1!WinMain+0x8a 00000000`0040105d 440fb7c9 movzx r9d,cx 00000000`00401061 440fb7c2 movzx r8d,dx 00000000`00401065 410fb7c3 movzx eax,r11w 00000000`00401069 488d1560110000 lea rdx,[crackme1!`string'] 00000000`00401070 488d4c2440 lea rcx,[rsp+40h] 00000000`00401075 89442420 mov dword ptr [rsp+20h],eax 00000000`00401079 ff15d9100000 call qword ptr [crackme1!_imp_wsprintfW] 00000000`0040107f ff15c3100000 call qword ptr [crackme1!_imp_GetActiveWindow] 00000000`00401085 488d542440 lea rdx,[rsp+40h] 00000000`0040108a 4c8d05d7110000 lea r8,[crackme1!`string'] 00000000`00401091 4533c9 xor r9d,r9d 00000000`00401094 488bc8 mov rcx,rax 00000000`00401097 ff15b3100000 call qword ptr [crackme1!_imp_MessageBoxW] 00000000`0040109d 33c0 xor eax,eax 00000000`0040109f 488b8c2440100000 mov rcx,qword ptr [rsp+1040h] 00000000`004010a7 4833cc xor rcx,rsp 00000000`004010aa e821000000 call crackme1!__security_check_cookie 00000000`004010af 4881c458100000 add rsp,1058h 00000000`004010b6 c3 ret
普段OllyDbgを使っているせいなのか、レジスタが多いせいなのか分からないが、少し読みにくい。しかし、慣れればそんなでもないだろう。重要な部分は以下だ。
// 条件分岐 00000000`0040101f 488d4c2430 lea rcx,[rsp+30h] 00000000`00401024 ff15d60f0000 call qword ptr [crackme1!_imp_GetSystemTime] 00000000`0040102a 0fb7542430 movzx edx,word ptr [rsp+30h] 00000000`0040102f 440fb75c2436 movzx r11d,word ptr [rsp+36h] 00000000`00401035 6681faa804 cmp dx,4A8h 00000000`0040103a 0fb74c2432 movzx ecx,word ptr [rsp+32h] 00000000`0040103f 751c jne crackme1!WinMain+0x5d 00000000`00401041 6683f905 cmp cx,5 00000000`00401045 7516 jne crackme1!WinMain+0x5d 00000000`00401047 664183fb0f cmp r11w,0Fh 00000000`0040104c 750f jne crackme1!WinMain+0x5d
GetSystemTime関数が呼び出されているが、この関数の引数はひとつで、構造体のアドレスだ。よって、0040101fにて、rcxに構造体のアドレスが入れられたと分かる。さらに、呼び出されたあと、dxレジスタに値が転送されている。よって、アドレス00401035で、dxレジスタと0x04A8が比較されている。ちなみに04A8hは十進数で1192。同じく、0040102fにて、r11wへ、GetSystemTime関数にて取得された値が転送されている。ちなみに、r11は64ビットサイズで、r11dはr11の下位32ビット、r11wはr11dの下位16ビットだ。
このような感じで、レジスタへの転送と比較が行われ、jne命令でジャンプ先が示されている。しかし、ジャンプしてしまったら、エラーメッセージが表示されるため、ここはジャンプさせないようにバイナリを変更しなければならない。よって、これら3つのjne命令すべてを逆転させてやれば、クラックは完了となる。
続いてバイナリを変更する。crackme1.exeをバイナリエディタで開くと、アドレス00000400以下から、コードが書かれてあるのが分かる。もしかしたらPEフォーマットも大幅な変更が加えられているかもしれない、と思ったが、コードセクションのアドレスなどはいたって普通だった。64ビット環境のEXEファイルも、少々興味があるので、見てみたい気もするが、とりあえずはjne命令の変更を行うことにする。
// crackme1.exe(変更前)(crackme1.exe) 00000430 0F B7 5C 24 36 66 81 FA A8 04 0F B7 4C 24 32 75 00000440 1C 66 83 F9 05 75 16 66 41 83 FB 0F 75 0F FF 15
0000043Fから「75」が3回ほど出現しているのが分かる。よって、これら3つを「74」に変更する。
// crackme1.exe(変更後)(crackme1c.exe) 00000430 0F B7 5C 24 36 66 81 FA A8 04 0F B7 4C 24 32 74 00000440 1C 66 83 F9 05 74 16 66 41 83 FB 0F 74 0F FF 15
変更後のcrackme1.exeを保存して、実行する。すると次のように表示される。

見事ジャンプ先を変更し、「OK」というメッセージボックスが表示された。
今回は、64ビット環境でも、マシン語解析を中心としたリバースエンジニアリングが行えるのか、を試した。32ビット環境と比べると、レジスタのサイズが64ビットになっていたり、アドレス(ポインタ)が64ビットになっていたりで、慣れるまでは少々苦労するかもしれないが、やろうと思えば十分可能であることが分かった。
32ビット環境だと、どうしても搭載するメモリは(例外はあるが一般的な考え方としては)4GBまでなので、今後、コンピュータをさらに高性能化していくためには、64ビット化は必須なのかもしれない。しかし、その場合においても、マシン語解析によるリバースエンジニアリングが可能ならば、まだ、アセンブラをやる意味もあるのではないかと思う。興味があれば、ぜひともx64のアセンブラもやってみてほしい。