Windowsで電卓を起動するアセンブリコードを書いてみる
限定的な環境において電卓を起動するアセンブリコードを書き、シェルコードとして実行してみる。
環境
Windows 8.1 Pro 64 bit版、Visual Studio Community 2013 with Update 4
>systeminfo OS 名: Microsoft Windows 8.1 Pro OS バージョン: 6.3.9600 N/A ビルド 9600 OS ビルドの種類: Multiprocessor Free システムの種類: x64-based PC プロセッサ: 1 プロセッサインストール済みです。 [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz >cl Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86 >ml Microsoft (R) Macro Assembler Version 12.00.31101.0 >dumpbin Microsoft (R) COFF/PE Dumper Version 12.00.31101.0
電卓を起動するプログラムを書いてみる
まずは、C言語で電卓を起動するプログラムを書いてみる。
Windowsにおいて、特定のコマンドを実行するにはWinExec関数、プロセスを終了させるにはExitProcess関数が利用できる。 これらはちょうど、glibcにおけるsystem関数とexit関数に対応する。 これらを使い、calc.exeを起動するプログラムコードを書くと次のようになる。
/* execcalc.c */ #include <windows.h> int main() { WinExec("calc", SW_SHOWNORMAL); ExitProcess(0); }
コンパイルして、実行してみる。
>cl execcalc.c Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86 Copyright (C) Microsoft Corporation. All rights reserved. execcalc.c Microsoft (R) Incremental Linker Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:execcalc.exe execcalc.obj >execcalc.exe
実行の結果、電卓が起動することが確認できる。
API関数のアドレスを調べてみる
アセンブリコードにおいてAPI関数を呼び出すためには、その実際のアドレスを得る必要がある。 そこで、実際のアドレスを調べるために次のようなプログラムコードを書いてみる。
/* getaddr.c */ #include <windows.h> #include <stdio.h> int main(int argc, char *argv[]) { if (argc < 3) { fprintf(stderr, "Usage: %s lpFileName lpProcName\n", argv[1]); exit(1); } HMODULE hmod = LoadLibrary(argv[1]); if (!hmod) { fprintf(stderr, "library not found: %s\n", argv[1]); exit(1); } FARPROC fproc = GetProcAddress(hmod, argv[2]); if (!fproc) { fprintf(stderr, "function not found: %s\n", argv[2]); exit(2); } printf("%s!%s = %p\n", argv[1], argv[2], fproc); return 0; }
上のコードは、LoadLibrary関数で第一引数に与えたモジュールをロードし、GetProcAddress関数で第二引数に与えた関数のアドレスを取得して表示する。 このようなコードは、一般にはarwinという名前で知られている。
上のコードをコンパイルし、WinExec関数、ExitProcess関数のアドレスを調べてみる。 なお、MSDNのドキュメントより、これらはどちらもkernel32.dllにて実装されていることが調べられる。
>cl getaddr.c Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86 Copyright (C) Microsoft Corporation. All rights reserved. getaddr.c Microsoft (R) Incremental Linker Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:getaddr.exe getaddr.obj >getaddr kernel32 WinExec kernel32!WinExec = 75802927 >getaddr kernel32 ExitProcess kernel32!ExitProcess = 757E7F64
上の結果から、それぞれの関数の実際のアドレスが0x75802927、0x757E7F64であることがわかる。 なお、64bit版のWindowsではWOW64(Windows 32-bit On Windows 64-bit)と呼ばれる仕組みにより32bitアプリケーションをそのまま動かすことができ、ここでのコードもすべて32bitアプリケーションとしてコンパイルされている。
アセンブリコードを書いてみる
Visual Studioでは、Microsoft Macro Assembler(MASM)がmlというコマンド名で使えるようになっている。 MASMの文法に従い、電卓を起動するアセンブリコードを書くと次のようになる。
; execcalc.asm .386 .model flat, stdcall .code start: xor eax, eax push eax push 636c6163h ; "calc" mov eax, esp push 1 push eax mov eax, 75802927h ; ret = WinExec("calc", SW_SHOWNORMAL) call eax push eax mov eax, 757E7F64h ; ExitProcess(ret) call eax end start
MASMでは16進定数の表記に0x prefixではなくh suffixを用いる。 また、x86の32bitアプリケーションなので、レジスタにはeaxなどを用い、関数の引数はスタックに積むことによって渡される。
上のコードを、mlを使ってアセンブルするには次のようにする。
>ml execcalc.asm /link /subsystem:console Microsoft (R) Macro Assembler Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: execcalc.asm Microsoft (R) Incremental Linker Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. /OUT:execcalc.exe execcalc.obj /subsystem:console
なお、/link
以降はリンカオプションとなるため、ファイル名は先に指定しておく必要がある。
生成されたshellcode.exeを実行すると、電卓が起動することが確認できる。
>execcalc.exe
コンパイルされたexeファイルをディスアセンブルするには、dumpbinコマンドが利用できる。
>dumpbin /disasm execcalc.exe Microsoft (R) COFF/PE Dumper Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file execcalc.exe File Type: EXECUTABLE IMAGE 00401000: 33 C0 xor eax,eax 00401002: 50 push eax 00401003: 68 63 61 6C 63 push 636C6163h 00401008: 8B C4 mov eax,esp 0040100A: 6A 01 push 1 0040100C: 50 push eax 0040100D: B8 27 29 80 75 mov eax,75802927h 00401012: FF D0 call eax 00401014: 50 push eax 00401015: B8 64 7F 7E 75 mov eax,757E7F64h 0040101A: FF D0 call eax Summary 1000 .text >dumpbin /rawdata execcalc.exe Microsoft (R) COFF/PE Dumper Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file execcalc.exe File Type: EXECUTABLE IMAGE RAW DATA #1 00401000: 33 C0 50 68 63 61 6C 63 8B C4 6A 01 50 B8 27 29 3ÀPhcalc.Äj.P¸') 00401010: 80 75 FF D0 50 B8 64 7F 7E 75 FF D0 .uÿÐP¸d.~uÿÐ Summary 1000 .text
シェルコードとして実行してみる
上の出力結果から得られる機械語コードをC形式の文字列に変換し、これにジャンプするC言語コードを書いてみると、次のようになる。
/* loader.c */ #include <stdio.h> #include <string.h> int main() { char code[] = "\x33\xC0\x50\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x01\x50\xB8\x27\x29\x80\x75\xFF\xD0\x50\xB8\x64\x7F\x7E\x75\xFF\xD0"; printf("strlen(code) = %d\n", strlen(code)); (*(void (*)())code)(); return 0; }
WindowsにおけるDEPにはHardware-enforced DEPとSoftware-enforced DEPの二つがあり、スタックにおけるコード実行はHardware-enforced DEPにより禁止される(参考)。 つまり、上のコードをそのままコンパイルしてもスタック上のコードにジャンプした直後に落ちるようになっている。 そこで、ここではリンカオプションにて意図的にDEPを無効にした上でコンパイルしてみる。
>cl loader.c /link /nxcompat:no Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86 Copyright (C) Microsoft Corporation. All rights reserved. loader.c Microsoft (R) Incremental Linker Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:loader.exe /nxcompat:no loader.obj
/link /nxcompat:no
を指定することで、コンパイルされた実行ファイルにおいてDEPが無効となる。
生成されたloader.exeを実行すると、シェルコードが実行され電卓が起動することが確認できる。
>loader.exe strlen(code) = 28
WindowsにおけるASLRとシェルコード
Windows 8.1ではkernel32.dll含む主要なシステムライブラリにおいてASLR(Address space layout randomization)が有効となっているため、kernel32.dllが読み込まれるタイミング(OS起動時)ごとに得られるアドレスが変化する。 つまり、上で書いたアセンブリコードは再起動すると動かなくなる。 このため、WindowsのシェルコードではProcess Environment Block(PEB)などの構造体からkernel32.dllのベースアドレスを特定するといった処理が含まれることが多い。
また、Windowsの場合、内部的に用いられるシステムコールよりもドキュメント化されているWindows APIのほうが仕様変更されにくいため、シェルコードではAPI関数の呼び出しが行われることが一般的である。