Ragnarok OnlineにおけるDirectSoundの初期化、及びサウンドレート、そして高音質化へ

2013年6月4日

今回はサウンド関連というネタを提示しますが、ネタをもとに書きながら解析及び方法模索、コーディングを行っているので、長談になりがちですが、よろしくお付き合いください。

ROのexeやdllのインポートを参照してもdsound.dllは見つかりません。
サウンド関連の初期化はBinkw32.dll(Bink RAD Game Tools )というヘルパーライブラリによってdsound.dllが動的に読み込まれ初期化されます。
内部処理を詳しく知りたい方は、 デバッグやマルウェア解析等に関する有力ツール | Planetleaf.com Lab.で紹介したIDAで BinkOpenDirectSound というファンクションを見てみましょう。

BINK:300087C9                 push    offset aDsound_dll ; "DSOUND.DLL"
BINK:300087CE                 mov     edi, eax
BINK:300087D0                 call    ds:LoadLibraryA
BINK:300087D6                 push    edi             ; uMode
BINK:300087D7                 mov     ds:dword_30057AA8, eax
BINK:300087DC                 call    esi ; SetErrorMode
BINK:300087DE                 mov     eax, ds:dword_30057AA8
BINK:300087E3                 pop     edi
BINK:300087E4                 pop     esi
BINK:300087E5
BINK:300087E5 loc_300087E5:
BINK:300087E5                 cmp     eax, 20h
BINK:300087E8                 jb      short loc_300087FB
BINK:300087EA                 push    offset aDirectsoundcre ; "DirectSoundCreate"
BINK:300087EF                 push    eax             ; hModule
BINK:300087F0                 call    ds:GetProcAddress
BINK:300087F6                 mov     ds:dword_30057AAC, eax
BINK:300087FB
BINK:300087FB loc_300087FB:
BINK:300087FB                 mov     eax, ds:dword_30057AAC
BINK:30008800                 test    eax, eax
BINK:30008802                 jnz     short loc_30008790

この通り、direct soundで音響関連が実装されていることが分かります。

さて、実は本題はサウンドレートを22kHzから44kHzに引き上げようという話になります。
現在のiROのクライアントはSetupからサウンドレートの設定が削除されていますが、サウンドレートの設定自体はレジストリのDIGITALRATETYPEという値を参照しています。
サウンドレートが弄れないため、レジストリの知識が無く旧クライアントでレートを11kや8kにしてしまうと変更するすべがないという罠もあったりなかったりで、相変わらずな適当ぶりだなと・・・。

DIGITALRATETYPEは0~2でそれぞれ22kHz、11kHz、8kHzの値へ内部で振り分けられます。
レジストリの値自体は以下のようなコードで読み込まれています。

.text:0076C100                 mov     ecx, [esp+1Ch+phkResult]
.text:0076C104                 lea     edx, [esp+1Ch+cbData]
.text:0076C108                 push    edx             ; lpcbData
.text:0076C109                 push    offset dword_8F2850 ; lpData
.text:0076C10E                 lea     eax, [esp+24h+Type]
.text:0076C112                 push    eax             ; lpType
.text:0076C113                 push    0               ; lpReserved
.text:0076C115                 push    offset aDigitalratetyp ; "DIGITALRATETYPE"
.text:0076C11A                 push    ecx             ; hKey
.text:0076C11B                 call    esi ; RegQueryValueExA

分からない人のために解説すると、C言語のファンクションの呼び出し引数はスタックに積まれます。つまりこの場合は、
RegQueryValueExA( ecx , “DIGITALRATETYPE" , null , eax , &dword_8F2850 ,edx );という呼び出しが行われることになります。
このコードから、(DWORD)dword_8F2850に、DIGITALRATETYPEの値が格納される事が分かります。
あとはこの、インデックスを参照している場所を捜索すると、実際のサウンドレートの値を設定しているコードが判明します。

.text:0065F956 loc_65F956
.text:0065F956                 mov     eax, dword_8F2850
.text:0065F95B                 sub     eax, ebp
.text:0065F95D                 jz      short loc_65F97C
.text:0065F95F                 sub     eax, 1
.text:0065F962                 jz      short loc_65F970
.text:0065F964                 mov     dword_8E3594, 1F40h ; 8000 Hz
.text:0065F96E                 jmp     short loc_65F986
.text:0065F970 ; ---------------------------------------------------------------------------
.text:0065F970
.text:0065F970 loc_65F970:
.text:0065F970                 mov     dword_8E3594, 2B11h ; 11025 Hz
.text:0065F97A                 jmp     short loc_65F986
.text:0065F97C ; ---------------------------------------------------------------------------
.text:0065F97C
.text:0065F97C loc_65F97C:
.text:0065F97C                 mov     dword_8E3594, 5622h ; 22050 Hz
.text:0065F986
.text:0065F986 loc_65F986:
.text:0065F986
.text:0065F986                 mov     eax, dword_8F2858
.text:0065F98B                 sub     eax, ebp
.text:0065F98D                 mov     dword_8E3598, 8
.text:0065F997                 jnz     short loc_65F99F
.text:0065F999                 mov     dword_8E3598, esi
.text:0065F99F
.text:0065F99F loc_65F99F:
.text:0065F99F                 mov     eax, dword_8F285C
.text:0065F9A4                 push    eax
.text:0065F9A5                 push    0Fh
.text:0065F9A7                 call    ds:_AIL_set_preference@8 ; AIL_set_preference(x,x)
.text:0065F9AD                 mov     ecx, dword_8E359C
.text:0065F9B3                 mov     edx, dword_8E3598
.text:0065F9B9                 mov     eax, dword_8E3594 ; soud rate
.text:0065F9BE                 push    1
.text:0065F9C0                 push    ecx
.text:0065F9C1                 push    edx
.text:0065F9C2                 push    eax ; soud rate
.text:0065F9C3                 call    ds:_AIL_open_digital_driver@16 ; AIL_open_digital_driver(x,x,x,x)

これでサンプルレートがdword_8E3594に格納されることが判明しました。
その後、AIL_open_digital_driverというMSS32.dll(Miles Sound System RAD Game Tools )内のファンクションをコールしていることが分かります。サンプルレートは1番目の引数です。ROのメモリ上のコードを書き換えるのはリスクが大きいため、ここではMSS32.dllのAIL_open_digital_driverファンクションをフックすることを考えます。

では実際MSS32.dllの該当ファンクションを見ていきましょう。

.text:21103640 loc_21103640:
.text:21103640                 dec     word ptr dword_21150E64
.text:21103647                 retn
.text:21103647 _AIL_release_all_timers@0 endp
.text:21103647
.text:21103647 ; ---------------------------------------------------------------------------
.text:21103648                 align 10h
.text:21103650 ; Exported entry 120. _AIL_open_digital_driver@16
.text:21103650
.text:21103650 ; =============== S U B R O U T I N E =======================================
.text:21103650
.text:21103650
.text:21103650 ; __stdcall AIL_open_digital_driver(x, x, x, x)
.text:21103650                 public _AIL_open_digital_driver@16
.text:21103650 _AIL_open_digital_driver@16 proc near
.text:21103650                                         ; DATA XREF: .rdata:off_21147E28o
.text:21103650
.text:21103650 pMem            = word ptr -10h
.text:21103650 var_E           = dword ptr -0Eh
.text:21103650 var_8           = dword ptr -8
.text:21103650 var_4           = dword ptr -4
.text:21103650 arg_0           = dword ptr  4
.text:21103650 arg_4           = dword ptr  8
.text:21103650 arg_8           = dword ptr  0Ch
.text:21103650 arg_C           = dword ptr  10h
.text:21103650
.text:21103650                 sub     esp, 10h
.text:21103653                 inc     word ptr dword_21150E64
.text:2110365A                 cmp     word_21150E60, 0

都合がいいことに、ファンクションの下位アドレスに8バイトのパディングが存在するので、これを利用して処理を挟み込みます。今どき、超低レートでの再生を考える必要はないと思えるので単純にレートを2倍化するコードを挟み込むことにします。
以下は可読性を上げるため、成形したコードを提示します。

                21103648 90                   nop
                21103649 90                   nop 
                2110364a 90                   nop
                2110364b 90                   nop
                2110364c 90                   nop
                2110364d 90                   nop
                2110364e 90                   nop
                2110364f 90                   nop
Export__AIL_open_digital_driver:
                21103650 83ec10               sub     esp,byte +010h
                21103653 66ff05640e1521       inc     word [L21150e64]

スタックフレームの都合上通常のプロクシ関数呼び出しというわけにはいかないので、プロクシファンクションをスタックフレームなしの純粋なアセンブラコードとして記述することにします。

ProxyAIL_open_digital_driver:
					処理
					.
					.
					sub     esp,byte +010h
					jmp     ProxyResume

ProxyFuncSub:
                2110364c e9********           jmp     ProxyAIL_open_digital_driver
Export__AIL_open_digital_driver:
                21103650 ebf9                 jmp     short ProxyFuncSub
                21103652 90                   nop
ProxyResume:
                21103653 66ff05640e1521       inc     word [L21150e64]

そして、肝心の今回書き起こしたコードはこちら、

void *pResumeAIL_open_digital_driverFunction;
void __declspec(naked) ProxyAIL_open_digital_driver(void)
{
	__asm mov   eax,[esp+0x04] // eax = soundrate
	__asm add   [esp+0x04],eax // soundrate + soundrate ( soundrate x 2 )
	__asm sub   esp,0x010
	__asm jmp   pResumeAIL_open_digital_driverFunction
}

BOOL RagexeSoundRateFixer(void)
{
	BOOL result = FALSE;
	CString fullpath;

	TCHAR currentdir[MAX_PATH];
	HINSTANCE hDll;
	::GetCurrentDirectoryA( MAX_PATH,currentdir );

	fullpath.Format(_T("%s¥¥Mss32.dll"), currentdir);
	hDll = ::LoadLibrary(fullpath);

	if( !hDll ){
		return result;
	}

	BYTE *p = (BYTE*)::GetProcAddress( hDll, "_AIL_open_digital_driver@16");

	if( p )
	{
		if( p[ 0] == 0x83 && p[ 1] == 0xec && p[ 2] == 0x10 )
		{
			// find hotpatch structure.
			DWORD flOldProtect, flDontCare;
			if ( ::VirtualProtect(   (LPVOID)&p[-5], 7 , PAGE_READWRITE, &flOldProtect) )
			{
				p[-5] = 0xe9;              // jmp
				p[ 0] = 0xeb; p[ 1] = 0xf9;// jmp short [pc-7]
				p[ 2] = 0x90; // nop
				pResumeAIL_open_digital_driverFunction = &p[3];
				*((DWORD*)&p[-4])  = (DWORD)ProxyAIL_open_digital_driver - (DWORD)&p[-5] -5;

				::VirtualProtect( (LPVOID)&p[-5], 7 , flOldProtect, &flDontCare);
				result = TRUE;
			}
		}
	}
	::FreeLibrary(hDll);

	return result;
}

今回のコードは、CBTフックにより、ウインドウ作成時に実行することで、簡単に導入が出来る物です。
ただし、同じような事をするアプリケーションとの協調性は無いので、注意したいところです。

Ragnarok Online

Posted by redchat