IWebBrowser2使用F12开发者工具

IE11带来了非常完善的开发者工具,但是IE控件默认是没法使用的,这给我们页面调试带来了极大的不便,不过好在Win10系统有了IEChooser工具,也勉强能用。无意中发现360安全浏览器可以使用F12开发者工具,所以抽空研究了一下。本文仅针对Win10下的IE11浏览器,对于Win7及以下系统,可以使用一个叫做SuperF12的项目。

工具准备

此次研究使用了x64dbg调试器(配合DbgChild插件)用于动态调试,IDA用于静态反汇编,Process Hacker用于进程信息查看,360安全浏览器12.2.1362.0版本。

调试分析

通过Process Hacker可以发现,在360安全浏览器的IE内核中,按下F12键,会创建一个新的子进程,并且命令行参数带有-windows10-f12=的字样,在IDA中按下Alt-Twindows10-f12为关键字进行搜索,一共能找到两处,都是在函数sub_40101F中调用的,其FA#41f,该函数比较长,下面贴出核心代码:

*(_DWORD *)LibFileName = "windows10-f12";
v183 = sub_4CF6A0("windows10-f12");
sub_42CF50((char *)v10, &v177[1], (int)LibFileName);
v28 = v180;
v29 = v178;
v30 = v180;
v31 = v180;
if ( (v180 & 0x80u) != 0 )
    v31 = v178;
if ( v31 )
{
    v32 = (DWORD *)v177[1];
    v33 = (DWORD *)((char *)&v177[1] + v180);
    if ( (v180 & 0x80u) != 0 )
        v33 = (DWORD *)(v177[1] + v178);
    v34 = &v177[1];
    if ( (v180 & 0x80u) != 0 )
        v34 = (DWORD *)v177[1];
    if ( v34 != v33 )
    {
        v35 = (DWORD *)((char *)v33 - 1);
        if ( v35 > v34 )
        {
            v36 = (unsigned int)v34 + 1;
            do
            {
                v37 = *(_BYTE *)(v36 - 1);
                *(_BYTE *)(v36 - 1) = *(_BYTE *)v35;
                *(_BYTE *)v35 = v37;
                v35 = (DWORD *)((char *)v35 - 1);
                v38 = v36++ < (unsigned int)v35;
            }
            while ( v38 );
            v28 = v180;
            v32 = (DWORD *)v177[1];
            v29 = v178;
            v30 = v180;
        }
    }
    if ( (v28 & 0x80u) != 0 )
        v30 = v29;
    HIDWORD(v174) = 0;
    if ( (v28 & 0x80u) == 0 )
        v32 = &v177[1];
    sub_443D30(v32, v30, (signed int *)&v174 + 1);
    v39 = (HWND)HIDWORD(v174);
    if ( IsWindow((HWND)HIDWORD(v174)) )
    {
        if ( sub_458950() >= 7 )
        {
            dwProcessId[1] = 0;
            GetWindowThreadProcessId(v39, &dwProcessId[1]);
            sub_4B89C0(&Filename[2], 0, 260);
            sub_402DBE((int)&Filename[2], (const char *)dword_4E26E0, dwProcessId[1], HIDWORD(v174));
            sub_4B89C0(LibFileName, 0, 520);
            GetSystemDirectoryW(LibFileName, 0x104u);
            PathAppendW(LibFileName, &pMore);
            if ( PathFileExistsW(LibFileName) )
            {
                v40 = LoadLibraryW(LibFileName);
                if ( v40 )
                {
                    v41 = GetProcAddress(v40, "AttachTools");
                    if ( v41 )
                        ((void (__cdecl *)(_DWORD, _DWORD, WCHAR *, _DWORD))v41)(0, 0, &Filename[2], 0);
                }
            }
        }
    }
    v28 = v176;
    v42 = 1;
}

前面是参数解析部分,直接跳过不细究了,从函数sub_443D30调用的地方开始看,v174_int64类型,所以(signed int *)&v174 + 1HIDWORD表示的意思一样,结合这两行,我们可以猜到,函数sub_443D30是获取窗口句柄,拿到之后赋值给v39,到这里,v39HIDWORD(v174)一样,都是存储着目标窗口句柄。接下来是两个if语句,其中sub_458950可能是拿到本身的主版本号,这个没有细看,在实际调试中发现,两个if都会进入。然后,通过GetWindowThreadProcessId拿到目标窗口所在进程的PID,存储在dwProcessId[1]中。sub_4B89C0memset,将缓冲区置零,sub_402DBEsprintf函数,通过动态调试发现,sub_402DBE相当于sprintf(&Filename[2], "%d %x", dwProcessId[1], HIDWORD(v174)),这句是在构造命令行参数,构造完之后Filename[2]的值就类似于"123 456",这里要注意的是,其中的456是十六进制的。LibFilenamec:\Windows\SysWOW64\F12\F12AppFrame.dll,后面的逻辑就很清晰了,加载dll,然后调用对应的函数,需要用到前面sub_402DBE拿到的命令行参数,这里有一点要修改的是,AttachTools函数的签名为void(__stdcall*)(HWND, HINSTANCE, char*, int),不知道360中为何使用了__cdecl调用。

代码编写

到这里,我们就分析完了启动参数带了-windows10-f12后,360安全浏览器的处理逻辑,很容易可以在我们自己的项目中加入下面的代码

static BOOL AttachF12Tools(HWND hIE)
{
	auto hMod = ::LoadLibrary(L"F12\\F12AppFrame.dll");
	if (!hMod)
	{
		return FALSE;
	}
	using AttachToolsType = void(__stdcall*)(HWND, HINSTANCE, char*, int);
	auto AttachTools = (AttachToolsType)::GetProcAddress(hMod, "AttachTools");
	if (AttachTools)
	{
		DWORD dwPid = 0;
		::GetWindowThreadProcessId(hIE, &dwPid);
		if (dwPid == 0)
		{
			::FreeLibrary(hMod);
			return FALSE;
		}
		char name[64] = { 0 };
		_snprintf_s(name, _countof(name), "%d %x", dwPid, hIE);
		AttachTools(NULL, NULL, name, 0);
		return TRUE;
	}
	::FreeLibrary(hMod);
	return FALSE;
}

但是此刻还无法启动开发者工具,我们还需要继续分析AttachTools函数

void __stdcall AttachTools(HWND a1, HINSTANCE a2, char *a3, int a4)
{
  int v4; // ebx
  HINSTANCE v5; // eax
  unsigned int v6; // ecx
  LPCSTR v7; // esi
  std::_Ref_count_base *v8; // edi
  DWORD v9; // esi
  HWND v10; // edi
  unsigned int v11; // [esp+0h] [ebp-44h]
  unsigned __int16 v12; // [esp+4h] [ebp-40h]
  int v13; // [esp+10h] [ebp-34h]
  int v14; // [esp+14h] [ebp-30h]
  int v15; // [esp+18h] [ebp-2Ch]
  int v16; // [esp+1Ch] [ebp-28h]
  std::_Ref_count_base *v17; // [esp+20h] [ebp-24h]
  HRESULT v18; // [esp+24h] [ebp-20h]
  int v19; // [esp+28h] [ebp-1Ch]
  int pNumArgs; // [esp+2Ch] [ebp-18h]
  LPCSTR lpMultiByteStr; // [esp+30h] [ebp-14h]
  LPCWSTR lpCmdLine; // [esp+34h] [ebp-10h]
  int v23; // [esp+40h] [ebp-4h]

  v4 = 0;
  v13 = 0;
  v14 = 0;
  v15 = 0;
  v23 = 0;
  ATL::CSimpleStringT<char,0>::CSimpleStringT<char,0>(&lpMultiByteStr, &ATL::g_strmgr);
  LOBYTE(v23) = 1;
  if ( !a3 )
  {
    v6 = 0;
    goto LABEL_7;
  }
  if ( (unsigned int)a3 & 0xFFFF0000 )
  {
    v6 = strlen(a3);
LABEL_7:
    ATL::CSimpleStringT<char,0>::SetString(&lpMultiByteStr, a3, v6);
    goto LABEL_8;
  }
  v5 = ATL::AtlFindStringResourceInstance(v11, v12);
  if ( v5 )
    ATL::CStringT<char,ATL::StrTraitATL<char,ATL::ChTraitsCRT<char>>>::LoadStringW(
      (unsigned int)&lpMultiByteStr,
      (int)v5,
      (unsigned __int16)a3);
LABEL_8:
  LOBYTE(v23) = 2;
  ATL::CSimpleStringT<char,0>::CSimpleStringT<char,0>(&lpCmdLine, &ATL::g_strmgr);
  LOBYTE(v23) = 3;
  v7 = lpMultiByteStr;
  if ( !lpMultiByteStr || (unsigned int)lpMultiByteStr & 0xFFFF0000 )
    ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>::operator=(lpMultiByteStr);
  else
    ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>::LoadStringW(
      &lpCmdLine,
      (unsigned __int16)lpMultiByteStr);
  v8 = (std::_Ref_count_base *)CommandLineToArgvW(lpCmdLine, &pNumArgs);
  v17 = v8;
  LOBYTE(v23) = 5;
  if ( v8 )
  {
    if ( pNumArgs > 0 )
    {
      do
      {
        ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>(*((_DWORD *)v8 + v4));
        LOBYTE(v23) = 6;
        std::vector<ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>,std::allocator<ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>>>::push_back(&v19);
        LOBYTE(v23) = 5;
        ATL::CStringData::Release((ATL::CStringData *)(v19 - 16));
        ++v4;
      }
      while ( v4 < pNumArgs );
    }
    LocalFree((HLOCAL)v8);
    ATL::CStringData::Release((ATL::CStringData *)(lpCmdLine - 8));
    LOBYTE(v23) = 0;
    ATL::CStringData::Release((ATL::CStringData *)(v7 - 16));
    if ( pNumArgs == 2 )
    {
      v9 = __o__wtoi(*(_DWORD *)v13);
      v10 = (HWND)_wcstol(*(const wchar_t **)(v13 + 4), 0, 16);
      v18 = CoInitializeEx(0, 2u);
      LOBYTE(v19) = 0;
      v16 = 0;
      v17 = 0;
      std::shared_ptr_long_::_Setpd_long____lambda_e57c4cc13d2c92dd5aaa8f479b84dfdd___(&v18, v19);
      LOBYTE(v23) = 7;
      if ( !v18 )
      {
        lpMultiByteStr = 0;
        LOBYTE(v23) = 8;
        if ( !F12::GetDocumentFromHwnd(v10, &lpMultiByteStr) && !IEConfiguration_SetBool(536870925, 1) )
          InjectTools(v9, (int *)&lpMultiByteStr);
        ATL::CComPtrBase<SHObjIdl::IPackageDebugSettings>::~CComPtrBase<SHObjIdl::IPackageDebugSettings>(&lpMultiByteStr);
      }
      if ( v17 )
        std::_Ref_count_base::_Decref(v17);
    }
  }
  else
  {
    ATL::CStringData::Release((ATL::CStringData *)(lpCmdLine - 8));
    ATL::CStringData::Release((ATL::CStringData *)(v7 - 16));
  }
  std::vector<ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>,std::allocator<ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>>>::~vector<ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>,std::allocator<ATL::CStringT<unsigned short,ATL::StrTraitATL<unsigned short,ATL::ChTraitsCRT<unsigned short>>>>>(&v13);
}

代码依旧很长,不过没关系,我们只需要从CommandLineToArgvW开始阅读即可,lpCmdLine是我们上面传递的第三个参数,即"123 456",所以pNumArgs值为2,这里很明显看到__o__wtoi_wcstol,也解释了参数中前一个值为十进制,后一个为十六进制的原因(微软任性),然后是一个CoInitializeEx(NULL, 2u);的调用,2uCOINIT_APARTMENTTHREADEDv18保存了返回值,可以看到,只有当v18为0(S_OK)时,才会进入后面的处理逻辑,否则直接报错了,所以我们需要确保当前进程没有初始化COM组件,最好的办法就是创建一个新进程

static HWND IsF12DevTool(LPTSTR lpstrCmdLine)
{
	int nArgs = 0;
	LPWSTR* ppArgList = ::CommandLineToArgvW(lpstrCmdLine, &nArgs);
	if (ppArgList == NULL)
	{
		return NULL;
	}
	constexpr wchar_t pstrF12[] = L"-windows10-f12=";
	constexpr UINT nF12 = _countof(pstrF12) - 1;
	HWND hIE = NULL;
	for (int i = 0; i < nArgs; i++)
	{
		auto pIndex = StrStrNIW(ppArgList[i], pstrF12, nF12);
		if (pIndex == NULL)
		{
			continue;
		}
		std::wstring strWnd(pIndex + nF12);
		std::reverse(strWnd.begin(), strWnd.end());
		hIE = reinterpret_cast<HWND>(StrToInt(strWnd.c_str()));
		if (hIE && !::IsWindow(hIE))
		{
			hIE = NULL;
		}
		break;
	}

	::LocalFree(ppArgList);
	return hIE;
}

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow)
{
	UNREFERENCED_PARAMETER(nCmdShow);

	int result = 0;

	auto hIE = IsF12DevTool(lpstrCmdLine);
	if (hIE && AttachF12Tools(hIE))
	{
		return result;
	}
    return 0;
}

可以看到,新进程逻辑很简单,首先检测命令行中有没有-windows10-f12参数,有的话就调用AttachTools,没有的话则直接退出。-windows10-f12的值为IE窗口的句柄,是底层类名为Internet Explorer_Server的那个窗口句柄,这里使用了std::reverse进行字符串反转,因为360安全浏览器这样玩的,所以也算是致敬吧。

最后,就是主进程的F12响应,然后获取窗口句柄,当作参数传递给新进程,代码比较简单不贴了,记得一点就是,窗口句柄转换为字符串之后记得反转。

致谢

  • SuperF12基本实现了IEChooser.exe做的事情,很厉害。
  • 360安全浏览器团队,直接参考,或者说照抄了360安全浏览器的处理方式。

ie

2658 Words

2020-08-12 10:46