调试非托管回调问题

时间:2012-12-01 09:08:54

标签: com callback reverse-engineering

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

我有一个封闭的源和未记录的COM对象 - 一个非托管DLL - 我试图集成到用C#编写的Windows服务中。 COM对象包含对服务需要与之交互的某些硬件的访问。

我无法获取该对象的界面文档或来源。我必须继续的是对象本身,与COM对象交互的三个[闭源未记录的]客户端,以及相当多的领域特定知识。

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

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

下一步是找到我需要使用的接口的IID。我正在寻找的特定方法不会导出。我没有PDB。似乎没有任何typelib信息,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()[as void * param]
  • _functionPtr.ToInt32()[as int param]
  • _stateChangeDelegate [as OnStateChangeDelegate param]
  • OnStateChange [as OnStateChangeDelegate param]
  • 使用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#实现这一点的任何评论都是受欢迎的!

0 个答案:

没有答案