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関数の呼び出しが行われることが一般的である。

関連リンク