[Win32] [C++] 独自のコントロールパネル アプリケーションを作る

IP アドレスの設定ををするときに、[ファイルを名を指定して実行] から ncpa.cpl を実行して、ネットワーク接続の一覧を開いている人は多いかと思います。%systemroot%\system32 フォルダには control.exe というプログラムがあり、こいつがコントロールパネルの正体なわけですが、cpl ファイルは control.exe に読み込まれるファイルなんだろう、と適当に考えていました。

しかし、偶々 cpl ファイルについて書かれたウェブサイトを見て、これが DLL ということを知りました。独自ダイアログボックスを持つ適当なコントロールパネル アプリケーション (「日付と時刻」など) を実行し、タスク マネージャーからプロセスを見てみると、rundll32.exe が動いています。ncpa.cpl は独自のダイアログボックスを持たず、シェルに統合されているようなので、explorer.exe からロードされます。

とりあえず作ってみようということで、簡単なコントロールパネル アプリケーションを作ってみました。MSDN にサンプルがあったので、それをもっとシンプルに書き換えました。これで十分動きます。リソースは、IDI_ICON1 というアイコンと IDD_DIALOG1 というダイアログを追加しただけです。

http://msdn.microsoft.com/en-us/library/ms914264.aspx

//
// main.cpp
//

#include <Windows.h>
#include <Cpl.h>
#include <strsafe.h>

#include "resource.h"

#define DLLEXPORT __declspec(dllexport)
#define DLLIMPORT __declspec(dllimport)

HINSTANCE g_hDll= NULL;

const WCHAR g_CplTitle[]= L"MyCPL";
const WCHAR g_CplInfo[]= L"Hello";

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        g_hDll= hModule;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

INT_PTR CALLBACK DlgProc(HWND hWnd, UINT msg, WPARAM w, LPARAM l) {
    switch ( msg ) {
    case WM_INITDIALOG:
        return TRUE;
        break;
    case WM_COMMAND:
        if ( w==IDOK || w==IDCANCEL ) {
            EndDialog(hWnd, w);
        }
        break;
    }
    return 0;
}

LONG CALLBACK CPlApplet(HWND hwndCPL, UINT message, LPARAM lParam1, LPARAM lParam2) {
    switch (message) {
    case CPL_INIT:
        // Perform global initializations, especially memory
        // allocations, here.
        // Return 1 for success or 0 for failure.
        // Control Panel does not load if failure is returned.

        return 1;

    case CPL_GETCOUNT:
        // The number of actions supported by this Control
        // Panel application.

        return 1;

    case CPL_NEWINQUIRE:
        // This message is sent once for each dialog box, as
        // determined by the value returned from CPL_GETCOUNT.
        // lParam1 is the 0-based index of the dialog box.
        // lParam2 is a pointer to the NEWCPLINFO structure.

        if ( lParam2 ) {
            NEWCPLINFO *pNewCpl= (NEWCPLINFO*)lParam2;
            pNewCpl->dwSize = sizeof(NEWCPLINFO);
            pNewCpl->dwFlags = 0;
            pNewCpl->dwHelpContext = 0;
            pNewCpl->lData = 0; // user-defined

            // The large icon for this application. Do not free this
            // HICON; it is freed by the Control Panel infrastructure.

            pNewCpl->hIcon = LoadIcon(g_hDll, MAKEINTRESOURCE(IDI_ICON1));
           
            StringCbCopy(
              pNewCpl->szName, sizeof(pNewCpl->szName), g_CplTitle);
            StringCbCopy(
              pNewCpl->szInfo, sizeof(pNewCpl->szInfo), g_CplInfo);
            pNewCpl->szHelpFile[0]= 0;

            return 0;
        }
        return 1;  // Nonzero value means CPlApplet failed.

    case CPL_DBLCLK:
        // The user has double-clicked the icon for the
        // dialog box in lParam1 (zero-based).

        DialogBox(g_hDll, MAKEINTRESOURCE(IDD_DIALOG1), hwndCPL, DlgProc);
        return 0;

    case CPL_STOP: // Called once for each dialog box. Used for cleanup.
    case CPL_EXIT: // Called only once for the application. Used for cleanup.
    default:
        return 0;
    }

    return 1;  // CPlApplet failed. (norun)
}

エクスポートする関数は一つだけですが、定義ファイルでエクスポート。序数はつけてもつけなくても OK。

;
; mycpl.def
;

LIBRARY mycpl
EXPORTS
    CPlApplet    @100

ビルドすると DLL ファイルができるので、拡張子を cpl に変更して system32 フォルダに放り込んでおけば、次回起動時にコントロールパネルに表示されます。

image

ダブルクリックするとダイアログが開きます。

広告

[x86 Assembler] [Windows] _RTC_CheckStackVars とは

前回の記事で、__security_check_cookie 周りのアセンブラを調べていたら、_RTC_CheckStackVars という、コンパイラが差し込む他のセキュリティ コードを発見してしまいました。__security_check_cookie に比べて、_RTC_CheckStackVars については、日本語の解説が全然ない。コンパイル エラーで _RTC_CheckStackVars のシンボルが出てくることは多いらしいけど。

このセキュリティ コードは、コンパイル オプション /RTC1 や /RTCs をつけたときに差し込まれます。Visual Studio でいうところの [C/C++ > Code Generation > Basic Runtime Checks] です。ちなみに前回の /GS オプションは [C/C++ > Code Generation > Buffer Security Check ] でした。それぞれ独立した機構となっているようです。

前置きは以上で、アセンブラを読みましょう。たったこれだけのコードを解読するのに半日かかりました。「今、このレジスタには何が入っているか」 を覚えるのが大変。メモリ構造もまた然り。マシンスタックなどの絵を書きながらじゃないと頭に入らない。

まずはプログラムの準備。関数は 2 つにしました。中途半端な処理をさせているのは、処理が単純過ぎると _RTC_CheckStackVars が差し込まれないためです。

//
// main.cpp
//

void bor2() {
    char buf[8];
    int *p= (int*)(buf+0);
    int *q= (int*)(buf+4);
    *p= 0x12345678;
    *q= 0x87654321;
}

void bor1() {
    char buf[64];
    buf[0]= 0;
}

int wmain(int argc, wchar_t *argv[]) {
    bor1();
    bor2();
    return 0;
}

環境は前と同じで。WOW64 や AMD64 の仕組みをちゃんと把握できていないので、念のため x86 で実行します。

コンパイラ: Visual Studio 2010
コンパイル環境: Windows 7 x64
実行環境: WIndows 7 x86

前回と同じ bor1 から見ていきます。

BOR!bor1:
00071430 55              push    ebp
00071431 8bec            mov     ebp,esp
00071433 81ec0c010000    sub     esp,10Ch
00071439 53              push    ebx
0007143a 56              push    esi
0007143b 57              push    edi
0007143c 8dbdf4feffff    lea     edi,[ebp-10Ch]
00071442 b943000000      mov     ecx,43h
00071447 b8cccccccc      mov     eax,0CCCCCCCCh
0007144c f3ab            rep stos dword ptr es:[edi]
0007144e a100700700      mov     eax,dword ptr [BOR!__security_cookie (00077000)]
00071453 33c5            xor     eax,ebp
00071455 8945fc          mov     dword ptr [ebp-4],eax
00071458 c645b800        mov     byte ptr [ebp-48h],0
0007145c 52              push    edx
0007145d 8bcd            mov     ecx,ebp
0007145f 50              push    eax
00071460 8d1580140700    lea     edx,[BOR!bor1+0x50 (00071480)]
00071466 e817fcffff      call    BOR!ILT+125(_RTC_CheckStackVars (00071082)
0007146b 58              pop     eax
0007146c 5a              pop     edx
0007146d 5f              pop     edi
0007146e 5e              pop     esi
0007146f 5b              pop     ebx
00071470 8b4dfc          mov     ecx,dword ptr [ebp-4]
00071473 33cd            xor     ecx,ebp
00071475 e89afbffff      call    BOR!ILT+15(__security_check_cookie (00071014)
0007147a 8be5            mov     esp,ebp
0007147c 5d              pop     ebp
0007147d c3              ret

青字が呼び出し部分です。ここで重要なのは 2 点です。「ecx レジスタには bor!bor1 の ebp が入っている」 ことと、「edx レジスタに bor!bor1+0x50 のアドレスが入っている」 ということです。bor!bor1+0x50 って何よ、って話ですよね。当然気になります。bor!bor1 関数のリターン コードはアドレス 0007147d にある ret 命令です。0007147d = bor!bor1 (00071430) + 0x4D なので、+0x50 は、bor!bor1 がロードされている直後の DWORD 境界の位置を示しているらしいことが分かります。

確認のため bor!bor2 についても調べてみると、やはり bor!bor2 関数の直後の DWORD 位置を edx にストアしています。bor2 の場合は偶然 DWORD 境界で関数が終わっていますが、bor!bor1 の場合は 0007147e と 0007147e の 2 バイトはのりしろになります。

BOR!bor2:
00071390 55              push    ebp
00071391 8bec            mov     ebp,esp

中略

000713d6 52              push    edx
000713d7 8bcd            mov     ecx,ebp
000713d9 50              push    eax
000713da 8d15f8130700    lea     edx,[BOR!bor2+0x68 (000713f8)]
000713e0 e89dfcffff      call    BOR!ILT+125(_RTC_CheckStackVars (00071082)
000713e5 58              pop     eax
000713e6 5a              pop     edx

中略

000713f4 8be5            mov     esp,ebp
000713f6 5d              pop     ebp
000713f7 c3              ret

で、ここには何が入っているのだろうか、ということで見てみます。bor!bor1 の直後の部分です。プロセスが起動してメモリ上にマップされるときに Windows 側が行う処理と思われるので、実際にどういう数値がセットされるのかは不明です。

; 00071460 8d1580140700    lea     edx,[BOR!bor1+0x50 (00071480)]

0:000> dd 00071470
00071470  33fc4d8b fb9ae8cd e58bffff ff8bc35d
00071480  00000001 00071488 ffffffb8 00000040
00071490  00071494 00667562 cccccccc cccccccc
000714a0  cccccccc cccccccc cccccccc cccccccc

赤字の C3 が bor!bor1 の ret 命令。00071498 から cccccccc が続いているところを見ると、意味を持つ値は 24 バイト分あるようです。便宜上、この 24 バイト分を 6 つの DWORD 値に分け、それぞれ A B C D E F という名前で呼ぶことにします。値はこんな感じ。

A= 00000001 B= 00071488 C= ffffffb8 D= 00000040 E= 00071494 F= 00667562

さて、ecx, と  edx レジスタをセットしたところで、いよいよ _RTC_CheckStackVars の処理です。適宜注釈を加えたアセンブラがこれ。

BOR!ILT+125(_RTC_CheckStackVars:
00071082 e9d9040000      jmp     BOR!_RTC_CheckStackVars (00071560)

BOR!_RTC_CheckStackVars:
; プロローグ
00071560 8bff            mov     edi,edi
00071562 55              push    ebp
00071563 8bec            mov     ebp,esp
00071565 51              push    ecx
00071566 53              push    ebx
00071567 56              push    esi
00071568 57              push    edi

; レジスタ初期化
00071569 33ff            xor     edi,edi
0007156b 8bf2            mov     esi,edx  –> esi stores BOR!bor+0x50
0007156d 8bd9            mov     ebx,ecx  –> ebx stores parent’s EBP
0007156f 897dfc          mov     dword ptr [ebp-4],edi –> loop counter
00071572 393e            cmp     dword ptr [esi],edi
00071574 7e48            jle     BOR!_RTC_CheckStackVars+0x5e (000715be)
                                           -> passed
00071576 eb08            jmp     BOR!_RTC_CheckStackVars+0x20 (00071580)

; ループ開始
BOR!_RTC_CheckStackVars+0x20:
00071580 8b4604          mov     eax,dword ptr [esi+4]
00071583 8b0c38          mov     ecx,dword ptr [eax+edi]
00071586 817c19fccccccccc cmp     dword ptr [ecx+ebx-4],0CCCCCCCCh  <比較1>
0007158e 750f            jne     BOR!_RTC_CheckStackVars+0x3f (0007159f)
                                            -> error

BOR!_RTC_CheckStackVars+0x30:
00071590 8b543804        mov     edx,dword ptr [eax+edi+4]
00071594 03d1            add     edx,ecx
00071596 813c1acccccccc  cmp     dword ptr [edx+ebx],0CCCCCCCCh  <比較2>
0007159d 7411            je      BOR!_RTC_CheckStackVars+0x50 (000715b0)

; エラールーチン呼び出し
BOR!_RTC_CheckStackVars+0x3f:
0007159f 8b4c3808        mov     ecx,dword ptr [eax+edi+8]
000715a3 8b5504          mov     edx,dword ptr [ebp+4]
000715a6 51              push    ecx
000715a7 52              push    edx
000715a8 e81bfbffff      call    BOR!ILT+195(?_RTC_StackFailureYAXPAXPBDZ) (000710c8)
000715ad 83c408          add     esp,8

BOR!_RTC_CheckStackVars+0x50:
000715b0 8b45fc          mov     eax,dword ptr [ebp-4]
000715b3 40              inc     eax
000715b4 83c70c          add     edi,0Ch
000715b7 8945fc          mov     dword ptr [ebp-4],eax
000715ba 3b06            cmp     eax,dword ptr [esi]
000715bc 7cc2            jl      BOR!_RTC_CheckStackVars+0x20 (00071580)
                                           -> loop

; エピローグ
BOR!_RTC_CheckStackVars+0x5e:
000715be 5f              pop     edi
000715bf 5e              pop     esi
000715c0 5b              pop     ebx
000715c1 8be5            mov     esp,ebp
000715c3 5d              pop     ebp
000715c4 c3              ret

数分間見つめていると分かりますが、for 文です。ループ カウンタは ebp-4 の DWORD を使います。ループ回数は、例の bor!bor1+0x50 から始まるメモリ ブロックにある最初の DWORD 値、すなわち <A> です。これは 000715ba の cmp 命令を見ると分かります。esi レジスタには、終始 <A> の値が入っていて、その値とループカウンタを比較しています。<A> は何だったでしょうか。0x00000001 でした。ここがどういうときに 2 以上になるのかは不明です。bor!bor2 でも <A> の値は 1 でした。

この関数がスタックの異常を検出している部分は 2 ヶ所あり、それぞれ <比較1> <比較2> と呼ぶことにします。

さて、比較1 です。ここからレジスタ地獄・・・。

00071580 8b4604          mov     eax,dword ptr [esi+4]
00071583 8b0c38          mov     ecx,dword ptr [eax+edi]
00071586 817c19fccccccccc cmp     dword ptr [ecx+ebx-4],0CCCCCCCCh  <比較1>

まずは ebx レジスタから見ます。これは元を辿ると、bor!bor1 で ecx に保存しておいた ebp レジスタの値です。_RTC_CheckStackVars の中では ebx レジスタは終始 bor!bor1 における ebp レジスタの値を保持しています。

ecx は、00071580 の mov 命令から順番に見ていったほうが分かりやすいです。まず [esi+4] です。esi はさっき出てきたように <A> です。ということは [esi+4] は <B> の値です。

ここでメモリの値を再掲します。

; 00071460 8d1580140700    lea     edx,[BOR!bor1+0x50 (00071480)]

0:000> dd 00071470
00071470  33fc4d8b fb9ae8cd e58bffff ff8bc35d
00071480  00000001 00071488 ffffffb8 00000040
00071490  00071494 00667562 cccccccc cccccccc
000714a0  cccccccc cccccccc cccccccc cccccccc

赤字で示した <B> の値は、自分の次の DWORD である <C> のアドレスを指しています。ついでに見てみると、青字で示した <E> は <F> のアドレスを指しています。どうしてこういう実装になっているかは不明です。<B> の値が B+4 のアドレス以外を指す場合があるのかどうかも不明です。いずれにしろ、00071580 の mov 命令の実行によって eax レジスタには <C> の「アドレス」が入ります。そして、00071583 にある mov 命令で、eax に入った <C> のアドレスに edi を加えたアドレスの値を ecx レジスタにストアします。edi は初登場です。上のアセンブラにあるように、00071569 で 0 が設定されます。後のほうを見ると、000715b4 で 12 が加算されます。ループカウンタに連動している値で、i*12 の値を示していることが分かります。初回なので edi は 0 です。というか先にも書きましたが、ループ回数は 1 なので、これが最後の実行です。何はともあれ、ecx には <C> の値がセットされました。まとめるとレジスタの値は次のようになります。

ebx = bor!bor のスタックベースポインタ
eax = <B> の値 = <C> のアドレス
ecx = <C> + i*12 の値 = i=0 なので <C> の値
ecx+ebx-4 = bor!bor のスタックベースポインタ + <C> の値 – 4

今回の場合、<C> の値は 0xffffffb8 = –72 なので bor!bor のスタックベースから 76 バイト上流にある値が 0xCCCCCCCC 以外の値だとエラーになります。これが <比較1> の処理です。

bor!bor のスタックベースから 76 バイト上流にある値とは何でしょうか。ここでヒントになるのが、bor1 関数の C++ における唯一の処理 buf[0]=0; です。これはアセンブラでも一行です。

00071458 c645b800        mov     byte ptr [ebp-48h],0

buf[0]  は ebp-0x48 にあることが分かります。そして 0x48 = 72 です。つまり、<比較1> では ebp-72-4 の値をチェックすることで、マシンスタックの上流側が初期化された 0xCCCCCCCC のままであるかどうかを確認していることが分かります。

次に <比較2> についてもレジスタの内容を順番に調べてみます。

00071590 8b543804        mov     edx,dword ptr [eax+edi+4]
00071594 03d1            add     edx,ecx
00071596 813c1acccccccc  cmp     dword ptr [edx+ebx],0CCCCCCCCh  <比較2>

ebx = bor!bor のスタックベースポインタ
edx = [eax+edi+4] + ecx
eax = <C> のアドレス
edi = i*12
ecx = <C> + i*12 の値

edi は 0 なので、[eax+edi+4] は <D> の値を示していることになります。メモリを調べると、ここは 0x00000040 です。この <D> 値に <C> の値を加算します。すると、0xffffffb8 + 0x00000040 = –8 となります。ということは、<比較2> は、bor1 における ebp-8 の値を調べていることになります。今回の場合、ebp-4 にはクッキーが入っているので、ebp-8 は、クッキーのちょうど上にある DWORD のアドレスです。今回のプログラムで buf[0] は ebp-48h でした。つまり、buf[63] は ebp-9 に保存されることになります。ちょうどクッキーと buf[63] の間の ebp-8 にある DWORD 分が残されるわけです。ここが壊されないようにチェックしているのですね。

念のため bor!bor2 も見てみると、確かに <C> + <D> = 0xfffffff8 = –8 になります。

bor!bor2
000713da 8d15f8130700    lea     edx,[BOR!bor2+0x68 (000713f8)]

0:000> dd 000713f0
000713f0  fffffc20 c35de58b 00000001 00071400
00071400  fffffff0 00000008 0007140c 00667562
00071410  cccccccc cccccccc cccccccc cccccccc

ようやく _RTC_CheckStackVars の全貌が見えてきました。ローカル変数用に確保されているマシンスタック内のメモリ領域のうち、上流部分を <比較1> 、下流部分を <比較2> でチェックし、初期化状態の 0xCCCCCCCC 以外であればエラーだと判断しているようです。こんなところに CC で埋める意味が隠されていたとは驚きです。

ここまで分かると、アセンブラを見るのも楽になってきます。今回、キーとなっていたのは、<C> と <D> にストアされている値でした。これらはともにスタックベースポインタからのオフセット値であり、ローカル変数領域のうち上流側のオフセットが <C> で、下流側のオフセットは <C>+<D> に保存されているわけです。つまり、<D> の値は、ローカル変数の合計サイズを表していることになります。確かに bor!bor1 では <D> = 64 で、bor!bor2 では <D> = 8 です。

今回はループが 1 回のみでしたが、仮に 2 回以上ループする場合は、マシンスタックのどの部分を確認することになるでしょうか。もはや簡単ですね。今回は、<C> と <D> を使ってスタックの両端を確認しましたが、ループが 2 順目に入った場合、これらに 12 を加算した値を使います。つまり、A B C D E F の後に G H I という 24 バイトが続くとして、<F> と <G> を使うことになります。複数のスタック領域を同時に調べられるようになっているようです。

もしかして、この 24 バイト領域って常識?

[x86 Assembler] [Windows] __security_check_cookie とは

最近のコンパイラは、かなり頭の使う処理を行ってくれています。VIsual Studio でも、普通に書いたプログラムなのに、いろいろなセキュリティー関連のチェック機構を埋め込んでいくれています。動作させる環境を守ってくれるのでとても有難いのですが、何も知らずにプログラムを使っていると、思わぬところで悩まされることがあります。

Visual Studio で /GS オプションを付けて C/C++ プログラムをコンパイルすると、関数呼び出しの際にバッファー オーバー ラン (BOR) を自動的に検出してくれるコードが埋め込まれます。異なるバージョンのコンパイラで作ったライブラリをリンクさせようとすると、たまに 「__security_check_cookie なんちゃらのシンボルがない」 というリンク エラーが出ることがあります。自分で書いたプログラムを見てもそんな関数は使ってないわけですが、コンパイラが勝手に埋め込んでるわけです。

このセキュリティ チェック機構に関しては、次のMSDN の ページが詳しいです。

http://msdn.microsoft.com/ja-jp/library/aa290051(v=vs.71).aspx

IT Pro でも解説がなされていました。次のページの第 4~6 回ですが、なかなかひどい文章です。作者は身元を明かせよ、と。

http://itpro.nikkeibp.co.jp/article/COLUMN/20060119/227538/?ST=develop

アセンブラ関連の投稿一発目ということで、自分でこの辺を見てみることにした。アセンブラに関してはまだまだ勉強中なので、変なことを言っているかもしれません。

環境は以下。
コンパイラ: Visual Studio 2010
コンパイル環境: Windows 7 x64
実行環境: WIndows 7 x86

とりあえずプログラムを書く。なんでもいい。

//
// main.cpp
//

void bor() {
    char buf[64];
    buf[0]= 0;
}

int wmain(int argc, wchar_t *argv[]) {
    bor();
    return 0;
}

これを /GS オプションをつけて (デフォルトでオンになっているけど) コンパイル。bor 関数のアセンブラは以下のようになっている。適当にコメント入れました。

ちなみに、表記フォーマットはこんな風になっています。
<ソースコードの行番号> <プログラムのメモリ上の位 置> <機械語> <アセンブラ>

BOR!bor [e:\visual studio 2010\projects\bor\main.cpp @ 6]:

; プロローグ(関数が呼ばれた時のおまじない)
    6 00181380 55              push    ebp
    6 00181381 8bec            mov     ebp,esp
    6 00181383 81ec0c010000    sub     esp,10Ch
    6 00181389 53              push    ebx
    6 0018138a 56              push    esi
    6 0018138b 57              push    edi

; ローカル変数用のスタックを CC で埋める
    6 0018138c 8dbdf4feffff    lea     edi,[ebp-10Ch]
    6 00181392 b943000000      mov     ecx,43h
    6 00181397 b8cccccccc      mov     eax,0CCCCCCCCh
    6 0018139c f3ab            rep stos dword ptr es:[edi]

; BOR 検出用のクッキーを保存
    6 0018139e a100701800      mov     eax,dword ptr [BOR!__security_cookie (00187000)]
    6 001813a3 33c5            xor     eax,ebp
    6 001813a5 8945fc          mov     dword ptr [ebp-4],eax

; buf[0]= 0; の処理
    8 001813a8 c645b800        mov     byte ptr [ebp-48h],0

; なんだこいつは…
    9 001813ac 52              push    edx
    9 001813ad 8bcd            mov     ecx,ebp
    9 001813af 50              push    eax
    9 001813b0 8d15d0131800    lea     edx,[BOR!bor+0x50 (001813d0)]
    9 001813b6 e8c7fcffff      call    BOR!ILT+125(_RTC_CheckStackVars (00181082)
    9 001813bb 58              pop     eax
    9 001813bc 5a              pop     edx

; エピローグ1(関数が終わる時のおまじない)
    9 001813bd 5f              pop     edi
    9 001813be 5e              pop     esi
    9 001813bf 5b              pop     ebx

; ここで BOR を検出させる
    9 001813c0 8b4dfc          mov     ecx,dword ptr [ebp-4]
    9 001813c3 33cd            xor     ecx,ebp
    9 001813c5 e84afcffff      call    BOR!ILT+15(__security_check_cookie (00181014)

; エピローグ2(関数が終わる時のおまじない)
    9 001813ca 8be5            mov     esp,ebp
    9 001813cc 5d              pop     ebp
    9 001813cd c3              ret

C++ で二行書いただけなのに、これだけの長さになってしまうところがアセンブラの面白いところ。VCでデバッグしているときに、変数が CC で埋められているのを疑問に思っていたけど、関数が呼ばれるたびにこんな風に明確にリセットされているとは思わなかった。あと、ローカル変数は 64 バイトなのに、268 (=0x10C) バイトもリセットしている。そんなに使うのだろうか。

肝心の BOR 検出処理は 2ヶ所に分かれています。まず、関数が呼ばれた段階(プロローグと、ローカル変数の初期化処理の後)で、BOR!__security_cookiee という DWORD 値(クッキー)とレジスタ esp を XOR 演算した値を ebp-4、すなわち、この関数スコープにおけるスタック領域の一番下に代入します。先の MSDN のページによると、クッキーの値は単なる乱数値で、__security_init_cookie の中で起動時に生成されるようです。

関数の最初にクッキーを保存しておいて、次に、関数の処理が終わったあとに __security_check_cookie 関数を呼び出して照合します。関数をコールする前に、ebp-4 に保存しておいたクッキーに再度 esp レジスタの値を XOR し、元々のクッキーの値を ecx レジスタに入れておきます。

__security_check_cookie のアセンブラは以下のようになっています。

BOR!__security_check_cookie [f:\dd\vctools\crt_bld\self_x86\crt\src\intel\secchk.c @ 52]:
   52 00181460 3b0d00701800    cmp     ecx,dword ptr [BOR!__security_cookie (00187000)]
   56 00181466 7502            jne     BOR!__security_check_cookie+0xa (0018146a)

BOR!__security_check_cookie+0x8 [f:\dd\vctools\crt_bld\self_x86\crt\src\intel\secchk.c @ 57]:
   57 00181468 f3c3            rep ret

BOR!__security_check_cookie+0xa [f:\dd\vctools\crt_bld\self_x86\crt\src\intel\secchk.c @ 59]:
   59 0018146a e918fcffff      jmp     BOR!ILT+130(___report_gsfailure) (00181087)

処理はかなり単純で、赤字で示した処理で、ecx レジスタの値とクッキーの値を比較するだけです。値が異なっていれば、関数の処理中に ebp-4 に保存されていたクッキーが変更された、つまり、ローカル変数用に確保されていたバッファー領域を超過して BOR が発生したか、途中で esp レジスタが変更されてしまったということで、___report_gsfailure という関数を呼びに行きます。

値が一致すれば BOR チェックとしては成功で、関数は終わります。が、単純に ret で終わるのではなく、rep ret で終わるのです。これについて調べたところ、はっきりとした理由は不明ですが、パフォーマンス向上のために rep ret を使う慣習があるらしい。

この人のブログに言及があります。
http://mikedimmick.blogspot.com/2008/03/what-heck-does-ret-mean.html

以下の怪しいページに、AMD が推奨している、と書いてある。怪しいけど、とりあえず深追いは避ける。
http://sourceware.org/ml/libc-alpha/2004-12/msg00022.html

関数 bor に戻って、BOR!ILT+125(_RTC_CheckStackVars を呼び出す処理。これはなんだろう。名前からして、マシンスタックの正常性を確認してくれる処理だろうか。

BOR!ILT+125 では、jmp 命令で BOR!_RTC_CheckStackVars に飛ばされます。で、その関数がこれ。

BOR!_RTC_CheckStackVars:
00fc14b0 8bff            mov     edi,edi
00fc14b2 55              push    ebp
00fc14b3 8bec            mov     ebp,esp
00fc14b5 51              push    ecx
00fc14b6 53              push    ebx
00fc14b7 56              push    esi
00fc14b8 57              push    edi
00fc14b9 33ff            xor     edi,edi
00fc14bb 8bf2            mov     esi,edx
00fc14bd 8bd9            mov     ebx,ecx
00fc14bf 897dfc          mov     dword ptr [ebp-4],edi
00fc14c2 393e            cmp     dword ptr [esi],edi
00fc14c4 7e48            jle     BOR!_RTC_CheckStackVars+0x5e (00fc150e)

BOR!_RTC_CheckStackVars+0x16:
00fc14c6 eb08            jmp     BOR!_RTC_CheckStackVars+0x20 (00fc14d0)

BOR!_RTC_CheckStackVars+0x20:
00fc14d0 8b4604          mov     eax,dword ptr [esi+4]
00fc14d3 8b0c38          mov     ecx,dword ptr [eax+edi]
00fc14d6 817c19fccccccccc cmp     dword ptr [ecx+ebx-4],0CCCCCCCCh
00fc14de 750f            jne     BOR!_RTC_CheckStackVars+0x3f (00fc14ef)

BOR!_RTC_CheckStackVars+0x30:
00fc14e0 8b543804        mov     edx,dword ptr [eax+edi+4]
00fc14e4 03d1            add     edx,ecx
00fc14e6 813c1acccccccc  cmp     dword ptr [edx+ebx],0CCCCCCCCh
00fc14ed 7411            je      BOR!_RTC_CheckStackVars+0x50 (00fc1500)

BOR!_RTC_CheckStackVars+0x3f:
00fc14ef 8b4c3808        mov     ecx,dword ptr [eax+edi+8]
00fc14f3 8b5504          mov     edx,dword ptr [ebp+4]
00fc14f6 51              push    ecx
00fc14f7 52              push    edx
00fc14f8 e8cbfbffff      call    BOR!ILT+195(?_RTC_StackFailureYAXPAXPBDZ) (00fc10c8)
00fc14fd 83c408          add     esp,8

BOR!_RTC_CheckStackVars+0x50:
00fc1500 8b45fc          mov     eax,dword ptr [ebp-4]
00fc1503 40              inc     eax
00fc1504 83c70c          add     edi,0Ch
00fc1507 8945fc          mov     dword ptr [ebp-4],eax
00fc150a 3b06            cmp     eax,dword ptr [esi]
00fc150c 7cc2            jl      BOR!_RTC_CheckStackVars+0x20 (00fc14d0)

BOR!_RTC_CheckStackVars+0x5e:
00fc150e 5f              pop     edi
00fc150f 5e              pop     esi
00fc1510 5b              pop     ebx
00fc1511 8be5            mov     esp,ebp
00fc1513 5d              pop     ebp
00fc1514 c3              ret

クッキーのチェックより長い。初心者にはソースコードがないとキツい・・・、というわけでこれはまた今度。

[Win32] [C++] convert hexadecimal string into decimal integer

C++ の標準関数 (というより Visual C++ の C ランタイム ライブラリ ; CRT) には 16 進から 10 進への変換するものが存在しない。10 進から 16 進だったら printf 系や _itoa 系の関数を使えば一発なのに。

で、書いてみた。幾つか方法はあるが、速さよりもコード量を短くしたいので wstringstream を使う方法でやってみる。一応このソース ファイルは CRT だけで書いています。速さ優先だと、どんなアルゴリズムがいいんですかね。

[2012/04/07]  文字整数変換の関数を書き直しました。
[C++] convert hexadecimal string into decimal integer (updated)

//
// htoi.cpp
//

#include <sstream>

using namespace std;

unsigned long htoi_32(const wchar_t*);
unsigned long long htoi_64(const wchar_t*);

struct longlong {
    long ll;
    long hl;
};

unsigned long htoi_32(const wchar_t *hex) {
    wchar_t safebuf[9];
    safebuf[8]= 0;
    memcpy(safebuf, hex, sizeof(wchar_t)*8); //rough code

    unsigned long ret= 0;
    wstringstream ss;
    ss << std::hex << safebuf;
    ss >> ret;
    return ret;
}

unsigned long long htoi_64(const wchar_t *hex) {
    unsigned long long ret= 0;

    wchar_t safebuf[18];
    safebuf[17]= 0;
    memcpy(safebuf, hex, sizeof(wchar_t)*17); // rough code

    wchar_t *p= safebuf;
    while ( *p ) {
        if ( *p==L’`’ ) {
            ((longlong*)&ret)->hl= htoi_32(safebuf);
            ((longlong*)&ret)->ll= htoi_32(p+1);
            return ret;
        }
        ++p;
    }

    int len= p-safebuf;
    if ( len>8 ) {
        ((longlong*)&ret)->ll= htoi_32(p-8);
        *(p-8)= 0;
        ((longlong*)&ret)->hl= htoi_32(safebuf);
    }
    else {
        ((longlong*)&ret)->hl= 0;
        ((longlong*)&ret)->ll= htoi_32(safebuf);
    }

    return ret;
}

“rough code” とコメントを入れたところは本当にラフ。safebuf のバッファー サイズが固定されているから、危険性は少ないという判断です。時間がないので手抜きです。お決まりですね。

バック クォートを使った書式 0x########`######## に対応させているのは、デバッガーのコンソールから数値を直接コピペできるようにしているため。で、何のためにこんな関数が必要かというと、NtQuerySystemTime という関数で取得されるシステム時間に関連するところを解析する必要があったため。

http://msdn.microsoft.com/en-us/library/ms724512(v=vs.85).aspx

GetSystemTimeAsFileTime を使えって書いてありますけどね。

この関数が返す FILETIME は構造体の形をしていますが、定義を見れば明らかなように、実体は単なる 64 bit 整数値で、UTC 西暦 1601 年の元日からのオフセットを 100 nsec 単位で表したものです。いろいろと中途半端。大体 1601 年ってどこから出てきたんだ。ちなみに NTP では基準が 1900 年のオフセット値が使われています。バラバラ。

で、その NtQuerySystemTime を呼び出すと、例えば 129,421,807,825,314,855 とかいう巨大な数値が返ってくるわけです。約 13 京。もちろん、パッと見ただけでは何月何日かチンプンカンプンです。

人間が使う暦は FILETIME ではなく SYSTEMTIME という構造体で管理されます。もちろん相互変換する API があります。そんな救世主が SystemTimeToFileTime と FileTimeToSystemTime です。

http://msdn.microsoft.com/en-us/library/ms724948(v=VS.85).aspx
http://msdn.microsoft.com/en-us/library/ms724280(v=VS.85).aspx

NtQuerySystemTime のように Nt を接頭辞とする関数のほとんどは Ntdll.dll に実装されていますが、インポート ライブラリがないため、動的リンクを使う必要があるので注意。

それなりにエラー処理もしていますが、自分で使うために書いたこともあって、あまり真面目にテストしていません。穴はいっぱいあると思います。

//
// main.cpp
//

#include <Windows.h>
#include <stdio.h>

unsigned long htoi_32(const wchar_t*);
unsigned long long htoi_64(const wchar_t*);

typedef NTSTATUS (WINAPI *PFUNC)(PLARGE_INTEGER);

/*

Usage:

  qtime

    Retrieve the current system time with NtQuerySystemTime

  qtime 0x01234567`89abcdef
  qtime 0x0123456789abcdef
  qtime 0n9223372036854775807

     Convert specified 100-nanosecond offset since Jan.1,1601 into UTC.
*/

int wmain(int argc, wchar_t *argv[]) {
    if ( argc==2 && wcscmp(argv[1],L"/?")==0 ) {
        wprintf_s(L"\nUsages:\n\n  qtime\n\n");
        wprintf_s(L"    Display the current system time with NtQuerySystemTime\n\n");
        wprintf_s(L"  qtime 0x01234567`89abcdef\n  qtime 0x0123456789abcdef\n");
        wprintf_s(L"  qtime 0n9223372036854775807\n\n     Convert specified 100-nanosecond offset since Jan.1,1601 into UTC.\n\n");
        return 0;
    }

    if ( argc==2 ) {
        LPCWCHAR p= argv[1];
        SYSTEMTIME st;
        long long ll= 0;
        if ( p[0]==L’0′ && p[1]==L’x’ )
            ll= htoi_64(p+2);
        else if ( p[0]==L’0′ && p[1]==L’n’ )
            ll= _wtoi64(p+2);
        else
            goto query_currenttime;

        if ( FileTimeToSystemTime((PFILETIME)&ll, &st) ) {
            wprintf_s(L"specified parameter: %s\n", p);
            wprintf_s(L"0x%08x`%08x = %4d/%2d/%2d %02d:%02d:%02d.%03d\n",
                ((PLARGE_INTEGER)&ll)->HighPart,
                ((PLARGE_INTEGER)&ll)->LowPart,
                st.wYear,
                st.wMonth,
                st.wDay,
                st.wHour,
                st.wMinute,
                st.wSecond,
                st.wMilliseconds);
            return 0;
        }
    }

query_currenttime:
   
    HMODULE hNtdll= LoadLibrary(L"Ntdll.dll");
    if ( hNtdll ) {
        PFUNC pNtQuerySystemTime= (PFUNC)GetProcAddress(hNtdll, "NtQuerySystemTime");
        if (  pNtQuerySystemTime ) {
            LARGE_INTEGER currenttime;
            HRESULT hr= (*pNtQuerySystemTime)(&currenttime);
            if ( SUCCEEDED(hr) ) {
                wprintf_s(L"Current   FILETIME: 0x%08x`%08x\n",
                    currenttime.HighPart, currenttime.LowPart);

                SYSTEMTIME st;
                if ( FileTimeToSystemTime((PFILETIME)&currenttime, &st) ) {
                    wprintf_s(L"Current SYSTEMTIME: %4d/%2d/%2d %02d:%02d:%02d.%03d\n",
                        st.wYear,
                        st.wMonth,
                        st.wDay,
                        st.wHour,
                        st.wMinute,
                        st.wSecond,
                        st.wMilliseconds);
                }
            }
        }
        FreeLibrary(hNtdll);
    }

    return 0;

}

最近、エラー処理のために goto 文を使うことが増えてきた。goto は使うな、と書いてある C/C++ 本が多いけど、使った方がコードが分かりやすくなる場合もあると思うのです。

[Win32] [C++] LogonUser と CreateProcessAsUser

2011/12/31~2012/1/1 にかけて、続きの記事を書きました。
—–
[Win32] [C++] CreateProcessAsUser – #4 セキュリティ記述子
[Win32] [C++] CreateProcessAsUser – #3 ソース
[Win32] [C++] CreateProcessAsUser – #2 トークン編
[Win32] [C++] CreateProcessAsUser – #1 特権編
—–

コマンド プロンプトから runas すると、他のユーザー アカウントでプロセスを起動することができます。ただしパスワードは手入力しないといけないので、これを自動化するための苦肉の策がいろいろと考案されています。VB スクリプトを使って SendKey する方法など。試してみましたが、確かに動きます。

SendKey ではあまりにも芸がないということで、プログラム的にやってみます。というか数千のプロセスを作る必要があったので、プログラムでやらないとしょうがない。

API としては LogonUser で取得したトークンを CreateProcessAsUser に渡すだけのようです。簡単ですね。が、それだと動かない。余裕で 1314 エラーです。

エラー 1314
「クライアントは要求された特権を保有していません。 」

なんですと、管理者ですぞ!? さて、真面目に MSDN を読まなければいけません。と、すぐ書いてあるし。

CreateProcessAsUser
http://msdn.microsoft.com/en-us/library/ms682429(VS.85).aspx

Typically, the process that calls the CreateProcessAsUser function must have the SE_INCREASE_QUOTA_NAME privilege and may require the SE_ASSIGNPRIMARYTOKEN_NAME privilege if the token is not assignable. If this function fails with ERROR_PRIVILEGE_NOT_HELD (1314), use theCreateProcessWithLogonW function instead. CreateProcessWithLogonW requires no special privileges

そこで SE_INCREASE_QUOTA_NAME と SE_ASSIGNPRIMARYTOKEN_NAME 特権を自分のユーザーに割り当てます。ローカル セキュリティ ポリシーの設定から、それぞれ以下の特権を割り当てます。

セキュリティの設定 > ローカル ポリシー > ユーザー権利の割り当て
プロセスのメモリ クォータの増加: SE_INCREASE_QUOTA_NAME 
プロセス レベル トークンの置き換え: SE_ASSIGNPRIMARYTOKEN_NAME

ちなみにクライアントは Windows 7 x86 ですが、クォータの方はすでに Administrators が入っているのに、トークンの置き換えの方にはLOCAL SERVICE と NETWORK SERVICE の 2 つしか入っていなかった。管理者なんて大したことないですね。やはりサービス起動ユーザーは強い。

ポリシーを反映させるために、一度ログオフしてから、再度ログオンして実行。すると CreateProcessAsUser は成功。別ユーザーで実行したかったプログラムは、ユーザー入力を必要としないプログラムだったので、要件としてはとりあえずこれで OK。

さて、ここでユーザー入力を必要とするプログラムを起動したい場合はかなり面倒です。もう一度 MSDN に戻り、該当部分を見てみるとこんな感じ。

By default, CreateProcessAsUser creates the new process on a noninteractive window station with a desktop that is not visible and cannot receive user input. To enable user interaction with the new process, you must specify the name of the default interactive window station and desktop, "winsta0\default", in the lpDesktop member of the STARTUPINFO structure. In addition, before calling CreateProcessAsUser, you must change the discretionary access control list (DACL) of both the default interactive window station and the default desktop. The DACLs for the window station and desktop must grant access to the user or the logon session represented by the hToken parameter.

  1. STARTUPINFO::lpDesktop
  2. ウィンドウ ステーションの DACL に実行ユーザーを追加
  3. デスクトップの DACL に実行ユーザーを追加

簡単そうじゃん、と思ってしまったが、これが超めんどくさい。ちなみにこれをやらずに CreateProcessAsUser を実行すると、次のようなエラーが出てダメです。

「アプリケーションを正しく起動できませんでした  (0xc0000142)。」
WS0002

ウィンドウステーションを持たないプログラムがウィンドウを表示しようとして落ちたのでしょうかね。これは今度デバッグしてみても面白いかも。

それはさておき、DACL への追加をしなければならないわけですが、これが実に実にめんどくさいです。サンプルは MSDN にありますが。

Starting an Interactive Client Process in C++
http://msdn.microsoft.com/en-us/library/aa379608(v=VS.85).aspx

Getting the Logon SID in C++
http://msdn.microsoft.com/en-us/library/aa446670(v=VS.85).aspx

(2014/12/29 修正) 2011 年末に作成したテスト プログラムのソースを GitHub 上で公開しています。このプログラムに関する記事については、本記事の冒頭にあるリンクをご参照ください。m(_ _)m

https://github.com/msmania/logue

ウィンドウ ステーションとデスクトップで、DACL を変更するときの処理が微妙に違う。サンプルでは関数を分けていたので、そのままだけど、一緒にしたほうがプログラムは短くなるかも。もともとオブジェクトが持っていた DACL をコピーするところまでは全く同じで、そのあと、トークンから取得した SID を pNewACL にコピーするときに、ウィンドウ ステーションでは 2 回に分けて AddAce する必要があるのです。だから、dwNewAclSize のサイズも微妙に異なる。このサイズについては、以下の KB に説明あり。

INFO: Computing the Size of a New ACL
http://support.microsoft.com/kb/102103/en