1

在这里提前为问题的长度道歉。

我有一个封闭源代码和未记录的 COM 对象 - 一个非托管 DLL - 我正在尝试将其集成到用 C# 编写的 Windows 服务中。COM 对象封装了对服务需要与之交互的某些硬件的访问。

我无法获取该对象的接口文档或源代码。我所要做的就是对象本身、与 COM 对象交互的三个 [封闭源代码未记录] 客户端,以及大量特定领域的知识。

到目前为止,这是一个非常难以破解的难题——一个星期,而且还在继续。

我能够从注册表中获取对象的 CLSID——这允许我在服务中实例化它。

下一步是找到我需要使用的接口的 IID。我正在寻找的特定方法没有导出。我没有 PDB。似乎没有任何类型库信息,并且 OLE-COM 对象查看器拒绝打开 COM 对象。IDispatch 也没有实现,所以这是一个挖掘的问题。通过手动在二进制文件中搜索 GUID 并消除唯一和/或已知的 GUID,我最终成功地识别了两个 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
  • 向变量和函数添加静态限定符
  • 更改回调的签名(包括删除返回值、将参数更改为整数、修改参数个数)
  • 添加间接级别 [通过将 _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;
}

所以现在我只需要找到一种方法来用等效的 C# 复制以下三行:

LPVOID ptr = &CallbackImpl;
LPVOID ptr2 = &ptr;
hr = pInterface->SetCallback(&ptr2);

似乎没有必要(甚至可疑)需要如此多的间接级别。可能我还没有完全理解反汇编。在这一点上,最重要的是它有效。

因此,欢迎任何关于如何从 C# 实现这一目标的评论!

4

0 回答 0