質問の長さについては、ここで事前にお詫び申し上げます。
C# で記述された Windows サービスに統合しようとしているクローズド ソースと文書化されていない COM オブジェクト (アンマネージ DLL) があります。COM オブジェクトは、サービスが対話する必要がある一部のハードウェアへのアクセスをラップします。
オブジェクトのインターフェイス ドキュメントまたはソースを取得できません。私が続けなければならないのは、オブジェクト自体、COM オブジェクトと対話する 3 つの [クローズド ソース、文書化されていない] クライアント、およびかなりの量のドメイン固有の知識だけです。
これまでのところ、これを解読するのは非常に困難でした.1週間と数える.
レジストリからオブジェクトの CLSID を取得できました。これにより、サービスでオブジェクトをインスタンス化できました。
次のステップは、使用する必要があるインターフェイスの IID を見つけることでした。私が探していた特定のメソッドはエクスポートされません。私はPDBを持っていません。typelib 情報が表示されず、OLE-COM オブジェクト ビューアが COM オブジェクトを開くことを拒否します。IDispatch も実装されていないため、掘り下げる必要がありました。最終的に、バイナリで GUID を手動で検索し、一意の GUID や既知の GUID を削除することで、2 つの IID を特定することに成功しました。この時点で、IID が正しいと確信しています。
IID は、対応するメソッド情報がなければ明らかに役に立ちません。そのために、IDA を使用したリバースに頼らざるを得ませんでした。GUID への参照を、ハードウェア機能に関する知識と大まかな逆アセンブリと関連付けることで、インターフェイスの構造と目的について知識に基づいた推測を行うことができました。
今、私はインターフェイスを使用してハードウェアと対話する必要があるところにいます...そして、これが私が立ち往生している場所です。
逆アセンブルから、最初に呼び出さなければならないメソッドは次のようになっていることがわかります。
HRESULT __stdcall SetStateChangeCallback(LPVOID callback);
コールバック シグネチャは次のようになります。
HRESULT (__stdcall *callbackType)(LPVOID data1, LPVOID data2)
これが私のサービスコードです:
[ComImport, System.Security.SuppressUnmanagedCodeSecurity,
Guid(...),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface AccessInterface
{
[PreserveSig]
int SetStateChangeCallback(IntPtr callbackPtr);
...
}
[UnmanagedFunctionPointerAttribute(CallingConvention.StdCall)]
private delegate int OnStateChangeDelegate(IntPtr a, IntPtr b);
private int OnStateChange(IntPtr a, IntPtr b)
{
Debug("***** State change triggered! *****");
}
private Guid _typeClsid = new Guid(...);
private Guid _interfaceIid = new Guid(...);
private object _comObj = null;
private AccessInterface _interface = null;
private OnStateChangeDelegate _stateChangeDelegate = null;
private IntPtr _functionPtr = IntPtr.Zero;
private void InitHardware()
{
Type t = Type.GetTypeFromCLSID(_typeClsid);
_comObj = Activator.CreateInstance(t);
if (_comObj == null)
{
throw new NullReferenceException();
}
_interface = _comObj as AccessInterface;
if (_interface == null)
{
throw new NullReferenceException();
}
_stateChangeDelegate = new OnStateChangeDelegate(OnStateChange);
_functionPtr = Marshal.GetFunctionPointerForDelegate(_stateChangeDelegate);
int hr = _interface.SetStateChangeCallBack(_functionPtr);
// hr (HRESULT) == 0, indicating success
}
これで、IntPtr.Zero を SetStateChangeCallBack() に渡した場合にのみ、このコードを正常に実行できます。実際の参照を渡すと、SetStateChangeCallBack() を呼び出した後、おそらく COM オブジェクトが初めてコールバックを呼び出そうとしたときに、サービスが数秒以内に例外コード 0xc0000005 でクラッシュします。
断層オフセットは一貫しています。IDA と以前に生成された逆アセンブリの助けを借りて、問題が発生している領域を特定できました。
06B04EF7 loc_6B04EF7: ; CODE XREF: 06B04F49j
06B04EF7 lea eax, [esp+0Ch]
06B04EFB push eax
06B04EFC mov ecx, ebx
06B04EFE call near ptr unk_6B06660
06B04F03 test eax, eax
06B04F05 jl short loc_6B04F4B
06B04F07 mov esi, [esp+0Ch]
06B04F0B test esi, esi
06B04F0D jz short loc_6B04F45
06B04F0F push 36h
06B04F11 lea ecx, [esp+18h]
06B04F15 push 0
06B04F17 push ecx
06B04F18 call near ptr unk_6B0F960
06B04F1D mov edx, [esp+1Ch]
06B04F21 push edx
06B04F22 lea eax, [esp+24h]
06B04F26 push esi
06B04F27 push eax
06B04F28 call near ptr unk_6B0F9E0
06B04F2D push esi
06B04F2E call near ptr unk_6B0C8D2
06B04F33 mov eax, [edi+4]
06B04F36 mov ecx, [eax]
06B04F38 add esp, 1Ch
06B04F3B lea edx, [esp+14h]
06B04F3F push edx
06B04F40 push eax
06B04F41 mov eax, [ecx] ; Crash here!
06B04F43 call eax
06B04F45
06B04F45 loc_6B04F45: ; CODE XREF: 06B04F0Dj
06B04F45 cmp dword ptr [edi+28h], 0
06B04F49 jnz short loc_6B04EF7
06B04F4B
06B04F4B loc_6B04F4B: ; CODE XREF: 06B04F05j
06B04F4B pop esi
06B04F4C pop ebx
06B04F4D pop edi
06B04F4E add esp, 40h
06B04F51 retn
クラッシュはオフセット 0x06B04F41 (つまり、"mov eax, [ecx]") にあります。
逆アセンブリからの対応する疑似コード関数 (上記のアセンブラは do ループで開始することに注意してください):
void __thiscall sub_10004EE0(int this)
{
int v1; // edi@1
void *v2; // esi@4
void *v3; // [sp+4h] [bp-40h]@3
int v4; // [sp+8h] [bp-3Ch]@5
char v5; // [sp+Ch] [bp-38h]@5
v1 = this;
if ( *(_DWORD *)(this + 4) )
{
if ( *(_DWORD *)(this + 40) )
{
do
{
if ( sub_10006660(v1 + 12, (int)&v3) < 0 )
break;
v2 = v3;
if ( v3 )
{
memset(&v5, 0, 0x36u);
unknown_libname_44(&v5, v2, v4);
j_j__free(v2);
// Crash on this statement!
(*(void (__stdcall **)(_DWORD, char *))**(void (__stdcall ****)(_DWORD, _DWORD))(v1 + 4))(
*(_DWORD *)(v1 + 4),
&v5);
}
}
while ( *(_DWORD *)(v1 + 40) );
}
}
}
関数ポインタを COM オブジェクトに正しく渡していないと確信していますが、正しく行う方法がわかれば胸がいっぱいです。私は試してみました[必死の順序で!]:
- _functionptr
- _functionPtr.ToPointer() [void* パラメータとして]
- _functionPtr.ToInt32() [int パラメータとして]
- _stateChangeDelegate [OnStateChangeDelegate パラメータとして]
- OnStateChange [OnStateChangeDelegate パラメータとして]
- デリゲートに CallingConvention.Cdecl を使用する
- 変数と関数に静的修飾子を追加する
- コールバックのシグネチャの変更 (戻り値の削除、パラメーターの int への変更、パラメーター数の変更を含む)
- 間接的なレベルを追加する [_functionPtr.ToInt32() を Marshal.AllocCoTaskMem() で割り当てられたメモリ ブロックに格納することにより]
場合によっては、変更により、ntdll や 06B04F36 でのクラッシュなど、さまざまなクラッシュの場所がトリガーされました。ほとんどの場合、クラッシュは上記の 06B04F41 で説明したとおりです。
IDA Pro をプロセスにアタッチすると、コールバックのアドレスが 06B04F40 で EAX に入るように見え、COM オブジェクトが使用しようとするアドレスにはそこからの固定オフセットがあります。例えば:
EAX (正しいアドレス) = 000A1392 ECX (使用アドレス) = 0A1378B8
ECX の下 4 桁は常に 78B8 です。
繰り返しますが、デリゲートまたは関数ポインターを正しく渡していないと思いますが、その方法がわかりません。サービスが WOW64 環境で動作していることも影響しているのではないでしょうか。
私の質問: (1) 問題についてより多くの情報を得る、および/または (2) 問題を解決するために、何をすることをお勧めしますか?
C# サービスの完全なコード以外のソース コードにはアクセスできないことに注意してください。私は IDA Pro の無料版を使用しているので、疑似コードに逆戻りするか、プロセスにアタッチしてクラッシュ例外をキャッチするよりも便利なことはできないようです。デバッグ モードで VS からサービスを実行することはできないため、実際にはその側でしかログを記録していません。コンパイル可能でないアンマネージ コードで問題が発生しているため、それほど良いとは思いません。 /読みやすいソース。たぶん私は間違っています?
アドバイスをいただき、誠にありがとうございます!
編集:
さて、C# から成功できないと考えた問題に頭を悩ませた後、サービスが実行する必要があることを実行するための最小限の C++ テスト アプリケーションを作成しようとしました...そして成功しました!
IAccessInterface : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE SetCallback(
/* [in] */ LPVOID pCallBack) = 0;
virtual HRESULT STDMETHODCALLTYPE SetDevice(
/* [in] */ char* context1,
/* [in] */ LPVOID context2,
/* [in] */ LPVOID context3) = 0;
virtual HRESULT STDMETHODCALLTYPE CloseDevice() = 0;
};
IAccessInterface* pInterface;
int __stdcall CallbackImpl(char* context, char* data)
{
printf("Callback succeeded!\r\n");
return 0;
}
void CleanUp(bool deviceOpen)
{
if (pInterface != NULL)
{
if (deviceOpen)
{
pInterface->SetCallback(NULL);
pInterface->CloseDevice();
}
pInterface->Release();
pInterface = NULL;
}
CoUninitialize();
}
int _tmain(int argc, _TCHAR* argv[])
{
GUID objClsid = GUID();
GUID interfaceIid = GUID();
CoInitialize(NULL);
int hr = CoCreateInstance(objClsid, 0, 1, interfaceIid, (void**)&pInterface);
if (!pInterface || !SUCCEEDED(hr))
{
CleanUp(false);
return 1;
}
LPVOID ptr = &callbackImpl;
LPVOID ptr2 = &ptr;
hr = pInterface->SetCallback(&ptr2);
if (!SUCCEEDED(hr))
{
CleanUp(false);
return 1;
}
char* context1 = "a_device_identifier";
hr = pInterface->SetDevice(context1, NULL, NULL);
if (!SUCCEEDED(hr))
{
CleanUp(false);
}
Sleep(30000); // give time for device to initialise and trigger callbacks (testing only)
// clean up
CleanUp(true);
return 0;
}
したがって、次の 3 行を同等の C# で複製する方法を見つける必要があります。
LPVOID ptr = &CallbackImpl;
LPVOID ptr2 = &ptr;
hr = pInterface->SetCallback(&ptr2);
非常に多くのレベルの間接化が必要になることは不必要に思われます (疑わしくさえあります)。たぶん、私は分解を完全に理解していません。この時点で最も重要なことは、それが機能することです。
したがって、C# からこれを実現する方法についてのコメントは大歓迎です!