楽しいハック講座(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!timeSetEvent
0: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]     <=== uDelay

76b0b0eb e8f8feffff      call    WINMM!timeSetEventInternal (76b0afe8)


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


 


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


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


 








0:000> u
WINMM!timeSetEventInternal:
76b0afe8 8bff            mov     edi,edi   //
関数のエントリ ポイントのお約束
76b0afea 55              push    ebp      
//スタック ベース ポインタの保存
76b0afeb 8bec            mov     ebp,esp  
//現在のスタックのトップをスタック ベースにする
76b0afed 83ec30          sub     esp,30h  
//ローカル変数として 30h バイト確保
76b0aff0 53              push    ebx
76b0aff1 8b5d08          mov     ebx,dword ptr [ebp+8]  
<=== uDelay の読み込み
76b0aff4 81fb40420f00    cmp     ebx,0F4240h 
<=== 0F4240h = 1000000 ミリ秒と比較
76b0affa 56              push    esi
76b0affb 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!
timeSetTimerEvent

01 0012fef8 76b0b0a8 WINMM!
timeThreadSetEvent+0x80
02 0012ff40 76b0b0f0 WINMM!timeSetEventInternal+0xc0
03 0012ff60 0040103b WINMM!timeSetEvent+0x28  <=
アプリからの API 呼び出しはここ
04 0012ff7c 004011de Hack_timeSetEvent!wmain+0x2b
05 0012ffc0 7c816fd7 Hack_timeSetEvent!__tmainCRTStartup+0x10f
06 0012fff0 00000000 kernel32!BaseProcessStart+0x23


 


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








0:000> u
WINMM!timeSetTimerEvent:
76b0abda 8bff            mov     edi,edi
76b0abdc 55              push    ebp
76b0abdd 8bec            mov     ebp,esp  //
ここまではさっきと同じお約束の動作
76b0abdf 51              push    ecx
76b0abe0 51              push    ecx
76b0abe1 56              push    esi
76b0abe2 8b7508          mov     esi,dword ptr [ebp+8]
76b0abe5 8b06            mov     eax,dword ptr [esi]  
<=== uDelay
76b0abe7 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 * 0x2710
Evaluate expression: 4294970000 = 00000001`00000a90


 


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


 


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








0:000> p
eax=00068db9
ebx=76b10200 ecx=00000020 edx=9267821e esi=76b11760 edi=00000001
eip=76b0abea esp=0012fed8 ebp=0012fee4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!timeSetTimerEvent+0x10:
76b0abea 69c010270000  
 imul    eax,eax,2710h

0:000> p
eax=00000a90 ebx=76b10200 ecx=00000020 edx=9267821e esi=76b11760 edi=00000001
eip=76b0abf0 esp=0012fed8 ebp=0012fee4 iopl=0         ov up ei pl zr na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000a47
WINMM!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> u
WINMM!timeSetTimerEvent+0xd:
76b0abe7 8b5620          mov     edx,dword ptr [esi+20h]
76b0abea 69c010270000    imul    eax,eax,2710h  <===
犯行現場
76b0abf0 33c9            xor     ecx,ecx
76b0abf2 57              push    edi
76b0abf3 8b7e24          mov     edi,dword ptr [esi+24h]
76b0abf6 03c2            add     eax,edx
76b0abf8 13cf            adc     ecx,edi
76b0abfa 894620          mov     dword ptr [esi+20h],eax
0:000> u
WINMM!timeSetTimerEvent+0x23:
76b0abfd 894e24          mov     dword ptr [esi+24h],ecx
76b0ac00 33ff            xor     edi,edi
76b0ac02 f6461401        test    byte ptr [esi+14h],1
76b0ac06 7413            je      WINMM!timeSetTimerEvent+0x41 (76b0ac1b)

76b0ac08 53              push    ebx
76b0ac09 e8037ffeff      call    WINMM!soundPlay+0x3e (76af2b11)
76b0ac0e 8b4e20          mov     ecx,dword ptr [esi+20h]
76b0ac11 8b5e24          mov     ebx,dword ptr [esi+24h]
0:000> u
WINMM!timeSetTimerEvent+0x3a:
76b0ac14 2bc1            sub     eax,ecx
76b0ac16 1bd3            sbb     edx,ebx
76b0ac18 5b              pop     ebx
76b0ac19 eb0f            jmp     WINMM!timeSetTimerEvent+0x50 (76b0ac2a)

76b0ac1b 6aff            push    0FFFFFFFFh   <=TIME_ONESHOT の場合ここに飛ぶ
76b0ac1d 68f0d8ffff      push    0FFFFD8F0h = -10000
です。
76b0ac22 57              push    edi
76b0ac23 ff36            push    dword ptr [esi]
0:000> u
WINMM!timeSetTimerEvent+0x4b:
76b0ac25 e8bc02ffff      call    WINMM!_allmul (76afaee6)
76b0ac2a 3bd7            cmp     edx,edi

76b0ac2c 7c0e            jl      WINMM!timeSetTimerEvent+0x62 (76b0ac3c)
76b0ac2e 7f04            jg      WINMM!timeSetTimerEvent+0x5a (76b0ac34)
76b0ac30 3bc7            cmp     eax,edi
76b0ac32 7608            jbe     WINMM!timeSetTimerEvent+0x62 (76b0ac3c)
76b0ac34 897df8          mov     dword ptr [ebp-8],edi
76b0ac37 897dfc          mov     dword ptr [ebp-4],edi

0:000> p
eax=e3c3fcac ebx=76b10200 ecx=00000000 edx=e3c3f21c esi=
76b11790 edi=00000000
eip=76b0ac02 esp=0012fdf4 ebp=0012fe04 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!timeSetTimerEvent+0x28:
76b0ac02 f6461401        test    byte ptr [esi+14h],1       ds:0023:76b117a4=00
0:000> dd esi
76b11790  00068db9 00000001 0041111d 00000002
76b117a0  00000031
00000000 00000798 00000c28


 


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


 


 


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


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








0:000> p
eax=00068db9 ebx=fff97247 ecx=ffffd8f0
edx=00068db8 esi=76b11790 edi=00000000
eip=76afaf12 esp=0012fddc ebp=0012fe04 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
WINMM!_allmul+0x2c:
76afaf12 f7e1            mul     eax,ecx 
429497 * (-10000)

0:000> p
eax=fffff570 ebx=fff97247 ecx=ffffd8f0 edx=00068db7 esi=76b11790 edi=00000000
eip=76afaf14 esp=0012fddc ebp=0012fe04 iopl=0         ov up ei ng nz na po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000a83
WINMM!_allmul+0x2e:
76afaf14 03d3            add     edx,ebx
0:000> p
eax=
fffff570 ebx=fff97247 ecx=ffffd8f0 edx=fffffffe esi=76b11790 edi=00000000
eip=76afaf16 esp=0012fddc ebp=0012fe04 iopl=0         nv up ei ng nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000282
WINMM!_allmul+0x30:
76afaf16 5b              pop     ebx
0:000> p
eax=fffff570 ebx=76b10200 ecx=ffffd8f0 edx=fffffffe
esi=76b11790 edi=00000000
eip=76afaf17 esp=0012fde0 ebp=0012fe04 iopl=0         nv up ei ng nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000282
WINMM!_allmul+0x31:
76afaf17 c21000          ret     10h

0:000> ? fffffffefffff570
Evaluate expression: –
4294970000 = fffffffe`fffff570


 


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


 


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


 


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


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


 


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


 








0:000> u
WINMM!timeSetTimerEvent:
745744ca 8bff            mov     edi,edi
745744cc 55              push    ebp
745744cd 8bec            mov     ebp,esp
745744cf 51              push    ecx
745744d0 51              push    ecx
745744d1 56              push    esi
745744d2 8b7508          mov     esi,dword ptr [ebp+8]
745744d5 8b06            mov     eax,dword ptr [esi]
0:000> u
WINMM!timeSetTimerEvent+0xd:
745744d7 b910270000      mov     ecx,2710h

745744dc f7e1           
mul     eax,ecx
745744de 014620          add     dword ptr [esi+20h],eax
745744e1 57              push    edi
745744e2 115624          adc     dword ptr [esi+24h],edx
745744e5 8b4614          mov     eax,dword ptr [esi+14h]
745744e8 33ff            xor     edi,edi
745744ea a801            test    al,1
 


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


 








0:000> p
eax=74597f00 ebx=74597d80 ecx=00000020 edx=00000000 esi=74597f00 edi=00000001
eip=745744d5 esp=0028fdbc ebp=0028fdc8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!timeSetTimerEvent+0xb:
745744d5 8b06            mov     eax,dword ptr [esi]  ds:0023:74597f00=00068db9
0:000> p
eax=00068db9 ebx=74597d80 ecx=00000020 edx=00000000 esi=74597f00 edi=00000001
eip=745744d7 esp=0028fdbc ebp=0028fdc8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!timeSetTimerEvent+0xd:
745744d7 b910270000      mov     ecx,2710h
0:000> p
eax=00068db9 ebx=74597d80 ecx=00002710 edx=00000000 esi=74597f00 edi=00000001
eip=745744dc esp=0028fdbc ebp=0028fdc8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
WINMM!timeSetTimerEvent+0x12:
745744dc f7e1            mul     eax,ecx  429497 * 10000 => edx:eax

0:000> p
eax=00000a90 ebx=74597d80 ecx=00002710 edx=00000001 esi=74597f00 edi=00000001
eip=745744de esp=0028fdbc ebp=0028fdc8 iopl=0         ov up ei pl nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000a07
WINMM!timeSetTimerEvent+0x14:
745744de 014620          add     dword ptr [esi+20h],eax ds:0023:74597f20=eb354f11

0:000> ? 00068db9 *00002710
Evaluate expression: 4294970000 = 00000001`00000a90


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


 


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


 


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


 


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


 


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


 

Comments (0)

Skip to main content