Compartilhar via


楽しいハック講座(3) マルチメディア タイマー その 2

こんにちははらだんです。今回は以前予告した timeSetEvent API の動作をハックしていきます。(注:クラックではありません)

 

今回紹介する内容は、弊社から無償で公開されているデバッグ専用のツール Debugging Tools for Windows に同梱されているデバッガ (windbg) と公開デバッグ シンボルを使えば誰でも確認することができます。

 

調査する内容の概要は以下になります。詳細については以前の記事「timeSetEvent の制限事項と不具合について」を読んでいただけたらと思います。

 

お題:

Windows XP で timeSetEven() を uDelay に 429,497 ms 以上かつ TIME_PERIODIC を指定して呼ぶと期待する時間が経過する遥か前にイベントが発火する。Windows Vista/Windows 7 ではこの現象は起きていない。この現象についてデバッグを行い動作を確認する。

 

詳細は以前の記事に譲るとして、このお題について以下の 4 点を確認します。

A. uDelay の上限が 1000 秒であることを実装から確認する

B. Windows XP では 429496 ミリ秒を超えるとオーバーフローすることを確認する

C. TIME_ONESHOT の場合なぜ B. の現象は再現しないかを確認する

D. Windows Vista ではこれが発生しないことを実装から確認する

 

こちらが本日の実験用のプログラムです。

#include "windows.h"#include "mmsystem.h"#pragma comment(lib, "winmm.lib")void CALLBACK TimerCallback(UINT wTimerID, UINT msg, DWORD dwUser, DWORD dw1, DWORD dw2) { std::cout << "timer" << std::endl; return;} int _tmain(int argc, _TCHAR* argv[]){ timeSetEvent(429497, 0, TimerCallback, 1, TIME_PERIODIC); timeSetEvent(429497, 0, TimerCallback, 2, TIME_ONESHOT); return 0;}オーバーフローする uDelay 値を設定し、TIME_PRIODIC/TIME_ONESHOT での動作を確認する。

 

プログラムの実行ファイルの名称は Hack_timeSetEvent.exe としました。

早速このプログラムを Windows XP SP2/SP3 と Windows Vista SP1/SP2 環境でデバッグしてみましょう。

 

※今回は Windbg の使い方は申し訳ございませんが省略させていただきました。別の機会で紹介させていただきます。

 

A. uDelay の上限が 1000 秒であることを実装から確認する

これは関数を追っていくとすぐにわかりますので手始めにここから確認して行きます。

 

(1) timeSetEvent 関数:引数 fuEvent のチェックをして Internal 関数を呼ぶ

Windbg で [File]-[Open Executable] を選択し、プログラムを指定します。その後以下のブレーク ポイントを設定しプログラムを実行します。

 

0:000> bp WINMM!timeSetEvent0:000> g

 

すぐに timeSetEvent 関数でブレークします。この関数は第 5 引数の fuEvent フラグのバリデーションを行うだけで、すぐに timeSetEventInternal と呼ばれる関数を呼び出します。u コマンドで timeSetEvent のディスアセンブラ コードを見ると以下のように引数をそのまま渡しています。このような呼び出しパターンは実は API にはよくあります。実装は timeSetEvent にはないので timeSetEventInternal に進みます。

 

76b0b0dc ff7518 push dword ptr [ebp+18h]76b0b0df ff7514 push dword ptr [ebp+14h]76b0b0e2 ff7510 push dword ptr [ebp+10h]76b0b0e5 ff750c push dword ptr [ebp+0Ch]76b0b0e8 ff7508 push dword ptr [ebp+8] <=== uDelay76b0b0eb e8f8feffff call WINMM!timeSetEventInternal (76b0afe8)

※ebp+8 ~ ebp+18h は今見ている関数の引数です。この辺のお約束については当 Blog ではこれまで紹介したことがありませんでした。参考になる書籍もありますので、次回ご紹介します。

 

(2) timeSetEventInternal 関数:1000 秒の上限処理

timeSetEvent から呼び出された timeSetEventInternal 関数のディスアセンブラ コードを見ていきます。この関数の先頭で早速 uDelay が上限 1000 秒(=1,000,000 ミリ秒)を超えていないかチェックし、超えている場合にはタイマー イベントは作成せずに終了します。

 

0:000> uWINMM!timeSetEventInternal:76b0afe8 8bff mov edi,edi //関数のエントリ ポイントのお約束76b0afea 55 push ebp //スタック ベース ポインタの保存76b0afeb 8bec mov ebp,esp //現在のスタックのトップをスタック ベースにする76b0afed 83ec30 sub esp,30h //ローカル変数として 30h バイト確保76b0aff0 53 push ebx76b0aff1 8b5d08 mov ebx,dword ptr [ebp+8] <=== uDelay の読み込み76b0aff4 81fb40420f00 cmp ebx,0F4240h <=== 0F4240h = 1000000 ミリ秒と比較76b0affa 56 push esi76b0affb 57 push edi ↓超えている場合ジャンプ76b0affc 0f8794000000 ja WINMM!timeSetEventInternal+0xae (76b0b096) ジャンプ先(76b0b096)ではタイマー イベントを作成せずに終了しています。

 

この確認で timeSetEvent のタイマー遅延時間 uDelay の上限は 1000 ms とわかりました。

 

B. Windows XP では 429496 ミリ秒を超えるとオーバーフローすることを確認する

次はオーバーフローする犯行現場を押さえます。デバッガで追いかけていくだけですが、現場にたどり着くまで uDelay がどのように関数に渡されていくか、どのレジスタに読み込まれているか、常にチェックしていきます。

 

(1) timeSetTimerEvent 関数:オーバーフローの犯行現場

3 分クッキングのように先に進んでいきます。次はオーバーフローする瞬間を捕まえます。先ほどのデバッグの続きで、 timeSetEventInternal 関数を p や t コマンドを使って進めていくと、timeThreadSetEvent と呼ばれる関数が呼ばれ、さらにその関数内から timeSetTimerEvent と呼ばれる関数が呼ばれます。

 

timeSetTimerEvent が呼ばれたところでのコールスタックです。

timeSetTimerEvent が呼ばれたところでのコールスタックです。0:000> knL # ChildEBP RetAddr 00 0012fee4 76b0ad0e WINMM!timeSetTimerEvent01 0012fef8 76b0b0a8 WINMM!timeThreadSetEvent+0x8002 0012ff40 76b0b0f0 WINMM!timeSetEventInternal+0xc003 0012ff60 0040103b WINMM!timeSetEvent+0x28 <= アプリからの API 呼び出しはここ04 0012ff7c 004011de Hack_timeSetEvent!wmain+0x2b05 0012ffc0 7c816fd7 Hack_timeSetEvent!__tmainCRTStartup+0x10f06 0012fff0 00000000 kernel32!BaseProcessStart+0x23

 

timeSetTimerEvent のディスアセンブラ コードを見てます。

0:000> uWINMM!timeSetTimerEvent:76b0abda 8bff mov edi,edi76b0abdc 55 push ebp76b0abdd 8bec mov ebp,esp //ここまではさっきと同じお約束の動作76b0abdf 51 push ecx76b0abe0 51 push ecx76b0abe1 56 push esi76b0abe2 8b7508 mov esi,dword ptr [ebp+8] 76b0abe5 8b06 mov eax,dword ptr [esi] <=== uDelay76b0abe7 8b5620 mov edx,dword ptr [esi+20h] 76b0abea 69c010270000 imul eax,eax,2710h <=== eax = eax * 10000

このいちばん最後の処理ですが、 timeSetEvent で設定した uDelay に 10000 を掛けています。

10000 倍することが何を意味しているのかですが、これはミリ秒から100ナノ秒の単位に変換しているところです。しかしこの 3 オペランドを取る掛け算命令は残念なことに 64-bit の結果を返しません。imul eax,ecx などの命令であれば edx:eax の64-bit に結果が返る計算ができるのですがそうなっていません。

 

0:000> ? 0x00068db9 * 0x2710Evaluate expression: 4294970000 = 00000001`00000a90

 

計算するとこのように 33bit 目に繰り上がりが発生し、 32bit の eax レジスタでは桁あふれが発生します。

 

p コマンドでステップ実行していくと以下のようにその瞬間を捕らえられます。

0:000> peax=00068db9 ebx=76b10200 ecx=00000020 edx=9267821e esi=76b11760 edi=00000001eip=76b0abea esp=0012fed8 ebp=0012fee4 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246WINMM!timeSetTimerEvent+0x10:76b0abea 69c010270000 imul eax,eax,2710h0:000> peax=00000a90 ebx=76b10200 ecx=00000020 edx=9267821e esi=76b11760 edi=00000001eip=76b0abf0 esp=0012fed8 ebp=0012fee4 iopl=0 ov up ei pl zr na pe cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000a47WINMM!timeSetTimerEvent+0x16:76b0abf0 33c9 xor ecx,ecx

この計算結果がそのまま使われてタイマー イベントが作成されてしまいます。

 

この確認で Windows XP の timeSetEvent には 429497 ms の上限があることがわかりました。

しかし TIME_ONESHOT 動作の時にはこの現象は発生しないことが確認できています。

 

C. TIME_ONESHOT の場合なぜ B. の現象は再現しないかを確認する

ここまでの処理で TIME_ONESHOT と TIME_PERIODIC の動作に差はありません。したがってこの後追跡していくと TIME_ONESHOT の時だけミリ秒の値を使う、または 64-bit で再度計算しなおしているところがあるはずです。続きのディスアセンブラ コードを確認します。

 

(1) TIME_ONESHOT の時にだけ実行されるコードを確認する。

 

0:000> uWINMM!timeSetTimerEvent+0xd:76b0abe7 8b5620 mov edx,dword ptr [esi+20h]76b0abea 69c010270000 imul eax,eax,2710h <=== 犯行現場76b0abf0 33c9 xor ecx,ecx76b0abf2 57 push edi76b0abf3 8b7e24 mov edi,dword ptr [esi+24h]76b0abf6 03c2 add eax,edx76b0abf8 13cf adc ecx,edi76b0abfa 894620 mov dword ptr [esi+20h],eax0:000> uWINMM!timeSetTimerEvent+0x23:76b0abfd 894e24 mov dword ptr [esi+24h],ecx76b0ac00 33ff xor edi,edi76b0ac02 f6461401 test byte ptr [esi+14h],176b0ac06 7413 je WINMM!timeSetTimerEvent+0x41 (76b0ac1b)76b0ac08 53 push ebx76b0ac09 e8037ffeff call WINMM!soundPlay+0x3e (76af2b11)76b0ac0e 8b4e20 mov ecx,dword ptr [esi+20h]76b0ac11 8b5e24 mov ebx,dword ptr [esi+24h]0:000> uWINMM!timeSetTimerEvent+0x3a:76b0ac14 2bc1 sub eax,ecx76b0ac16 1bd3 sbb edx,ebx76b0ac18 5b pop ebx76b0ac19 eb0f jmp WINMM!timeSetTimerEvent+0x50 (76b0ac2a)76b0ac1b 6aff push 0FFFFFFFFh <=TIME_ONESHOT の場合ここに飛ぶ76b0ac1d 68f0d8ffff push 0FFFFD8F0h = -10000 です。76b0ac22 57 push edi76b0ac23 ff36 push dword ptr [esi]0:000> uWINMM!timeSetTimerEvent+0x4b:76b0ac25 e8bc02ffff call WINMM!_allmul (76afaee6)76b0ac2a 3bd7 cmp edx,edi76b0ac2c 7c0e jl WINMM!timeSetTimerEvent+0x62 (76b0ac3c)76b0ac2e 7f04 jg WINMM!timeSetTimerEvent+0x5a (76b0ac34)76b0ac30 3bc7 cmp eax,edi76b0ac32 7608 jbe WINMM!timeSetTimerEvent+0x62 (76b0ac3c)76b0ac34 897df8 mov dword ptr [ebp-8],edi76b0ac37 897dfc mov dword ptr [ebp-4],edi0:000> peax=e3c3fcac ebx=76b10200 ecx=00000000 edx=e3c3f21c esi=76b11790 edi=00000000eip=76b0ac02 esp=0012fdf4 ebp=0012fe04 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246WINMM!timeSetTimerEvent+0x28:76b0ac02 f6461401 test byte ptr [esi+14h],1 ds:0023:76b117a4=000:000> dd esi76b11790 00068db9 00000001 0041111d 0000000276b117a0 00000031 00000000 00000798 00000c28

 

最初の緑のコードに注目します。test 命令により 1 と比較しているところは今回の呼び出しが TIME_ONESHOT(=1) かどうかをチェックしているところです。TIME_PERIODIC 指定の場合にはオレンジのコードが実行されたのちにピンクのコード以降にジャンプします。TIME_ONESHOT の場合にのみ実行される2番目の緑のコードを見てみます。_allmul という関数を呼ぶためにスタックに引数を保存しています。

 

 

(2) _allmul 関数:64-bit の掛け算

_allmul 関数内に入って動作をトレースすると以下のように uDelay の再計算を行っていました。

0:000> peax=00068db9 ebx=fff97247 ecx=ffffd8f0 edx=00068db8 esi=76b11790 edi=00000000eip=76afaf12 esp=0012fddc ebp=0012fe04 iopl=0 nv up ei ng nz na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000286WINMM!_allmul+0x2c:76afaf12 f7e1 mul eax,ecx 429497 * (-10000)0:000> peax=fffff570 ebx=fff97247 ecx=ffffd8f0 edx=00068db7 esi=76b11790 edi=00000000eip=76afaf14 esp=0012fddc ebp=0012fe04 iopl=0 ov up ei ng nz na po cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000a83WINMM!_allmul+0x2e:76afaf14 03d3 add edx,ebx0:000> peax=fffff570 ebx=fff97247 ecx=ffffd8f0 edx=fffffffe esi=76b11790 edi=00000000eip=76afaf16 esp=0012fddc ebp=0012fe04 iopl=0 nv up ei ng nz na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000282WINMM!_allmul+0x30:76afaf16 5b pop ebx0:000> peax=fffff570 ebx=76b10200 ecx=ffffd8f0 edx=fffffffe esi=76b11790 edi=00000000eip=76afaf17 esp=0012fde0 ebp=0012fe04 iopl=0 nv up ei ng nz na po nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000282WINMM!_allmul+0x31:76afaf17 c21000 ret 10h0:000> ? fffffffefffff570Evaluate expression: -4294970000 = fffffffe`fffff570

 

符号が逆なのはこの後の処理の都合ですが、この計算結果を使ってタイマー イベントが作成されます。この後 NtSetTimer 関数が呼ばれカーネル モードで処理されていきます。これについては機会があったらやってみたいと思います。

 

TIME_PERIODIC と TIME_ONESHOT では計算過程が異り、 TIME_ONESHOT では 64-bit で計算されていることがわかりました。

 

D. Windows Vista ではこれが発生しないことを実装から確認する

最後に Windows Vista の方での実装を確認してみます。

 

(1) timeSetTimerEvent 関数:オーバーフローしない

 

0:000> uWINMM!timeSetTimerEvent:745744ca 8bff mov edi,edi745744cc 55 push ebp745744cd 8bec mov ebp,esp745744cf 51 push ecx745744d0 51 push ecx745744d1 56 push esi745744d2 8b7508 mov esi,dword ptr [ebp+8]745744d5 8b06 mov eax,dword ptr [esi]0:000> uWINMM!timeSetTimerEvent+0xd:745744d7 b910270000 mov ecx,2710h745744dc f7e1 mul eax,ecx745744de 014620 add dword ptr [esi+20h],eax745744e1 57 push edi745744e2 115624 adc dword ptr [esi+24h],edx745744e5 8b4614 mov eax,dword ptr [esi+14h]745744e8 33ff xor edi,edi745744ea a801 test al,1

 

Windows XP とあまり変らないように見えますが、実際に動かしてみると違いがわかると思います。

 

0:000> peax=74597f00 ebx=74597d80 ecx=00000020 edx=00000000 esi=74597f00 edi=00000001eip=745744d5 esp=0028fdbc ebp=0028fdc8 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246WINMM!timeSetTimerEvent+0xb:745744d5 8b06 mov eax,dword ptr [esi] ds:0023:74597f00=00068db90:000> peax=00068db9 ebx=74597d80 ecx=00000020 edx=00000000 esi=74597f00 edi=00000001eip=745744d7 esp=0028fdbc ebp=0028fdc8 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246WINMM!timeSetTimerEvent+0xd:745744d7 b910270000 mov ecx,2710h0:000> peax=00068db9 ebx=74597d80 ecx=00002710 edx=00000000 esi=74597f00 edi=00000001eip=745744dc esp=0028fdbc ebp=0028fdc8 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246WINMM!timeSetTimerEvent+0x12:745744dc f7e1 mul eax,ecx 429497 * 10000 => edx:eax0:000> peax=00000a90 ebx=74597d80 ecx=00002710 edx=00000001 esi=74597f00 edi=00000001eip=745744de esp=0028fdbc ebp=0028fdc8 iopl=0 ov up ei pl nz na pe cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000a07WINMM!timeSetTimerEvent+0x14:745744de 014620 add dword ptr [esi+20h],eax ds:0023:74597f20=eb354f110:000> ? 00068db9 *00002710 Evaluate expression: 4294970000 = 00000001`00000a90

こちらは最初から 64-bit で計算を行っているために XP のようなオーバーフローは発生しません。

 

A. から D. までの 4 点について実装と動作を確認することができました。

 

今回のは鍵になる値が即値としてそのままコーディングされていたり、オーバーフローする部分が典型的な imul/mul の計算結果によるものでしたので比較的確認が容易だったと思います。シンボルも公開デバッグ シンボルで十分わかる内容でした。

 

もう少し詳細に timeSetEvent の呼び出しから関連する引数についてはすべてどのレジスタに入ったのか、何番目の引数に入れ直されて別の関数が呼ばれたのか、構造体らしいものに保存された場所など逐一解説したかったのですが、記事が非常に長くなるので省略させていただきました。

 

私たちは仕事でこのようなことを毎日やっております。また、私たちサポート エンジニアが Debugging Tools for Windows をどのように使っているかご紹介できたかなと思います。