QQ中有一个断网探测功能,当网络断开时,QQ在1~2秒钟内就可以知道并且刷新用户状态,当网络畅通时又可以迅速连接服务器。对这个功能,网上有多种猜测实现的方法,有说不停的ping,有说访问dns的,有说是心跳包保持连接的。这次通过逆向的方式一探究竟。
研究的QQ版本:
操作系统平台:win7 32位
Od附加QQ ,选择od菜单->查看->可执行模块,从列表框中选择common.dll模块
在od反汇编窗口选择查找->当前模块中的名称
在util::network::isNetConnectionOK2下断点
代码比较简单,只有几句话
Call 67e58107是关键call, F7进入67e58107
这段代码od调试时直接执行
mov eax,dword ptr[68017530] ;
test eax,eax,rea;
jnz 跳转
经过阅读主要意思就是判断68017530这个地址的指针指向的内容是否为0,
可以肯定这是读取标示位确定如何返回断网还是联网的。
查看内存地址如下图:
对01位置下内存写入断点,程序立刻断在如下函数位置
经过多次跟踪,发现程序的关键call是 call 67E58D96,继续od F8跟流程,最后会发现程序进入 call 67e58268,这里开始调用winapi函数,拿出IDA直接加载common.dll,F5阅读伪代码。 代码如下
void __cdecl sub_67E58268(int a1, int a2, int a3, LPLONG Target)
{
struct _MIB_IPFORWARDTABLE *v4; // eax@4
struct _MIB_IPFORWARDTABLE *v5; // esi@5
unsigned int v6; // edi@7
unsigned int v7; // edx@7
int v8; // esi@8
unsigned int v9; // ecx@11
int v10; // ecx@14
const wchar_t *v11; // edi@21
const wchar_t *v12; // esi@21
int v13; // eax@22
__int32 **v14; // edi@22
int v15; // edx@22
int v16; // ecx@22
int **v17; // eax@22
int v18; // ecx@22
int v19; // edx@22
void *v20; // eax@25
char *v21; // esi@29
DWORD v22; // edx@29
int v23; // ecx@30
char *v24; // edi@30
int v25; // eax@38
int v26; // edx@38
int v27; // ecx@38
int **v28; // eax@38
int v29; // edx@38
int v30; // ecx@38
int v31; // edx@38
int v32; // ecx@38
int **v33; // eax@38
int v34; // edx@38
int v35; // eax@39
int v36; // [sp-14h] [bp-4Ch]@38
int *v37; // [sp-10h] [bp-48h]@22
int v38; // [sp-Ch] [bp-44h]@22
int *v39; // [sp-8h] [bp-40h]@22
__int32 *v40; // [sp-4h] [bp-3Ch]@22
char v41; // [sp+Ch] [bp-2Ch]@38
char v42; // [sp+10h] [bp-28h]@38
char v43; // [sp+14h] [bp-24h]@38
int **v44; // [sp+18h] [bp-20h]@22
int v45; // [sp+1Ch] [bp-1Ch]@7
ULONG v46; // [sp+20h] [bp-18h]@3
unsigned int v47; // [sp+24h] [bp-14h]@7
ULONG pdwSize; // [sp+28h] [bp-10h]@3
void *v49; // [sp+2Ch] [bp-Ch]@3
PMIB_IPADDRTABLE pIpAddrTable; // [sp+30h] [bp-8h]@3
unsigned int v51; // [sp+34h] [bp-4h]@1
__int32 **v52; // [sp+40h] [bp+8h]@38
*(_DWORD *)a1 = 0;
*(_DWORD *)a2 = 0;
*(_DWORD *)a3 = -1;
CTXStringW::Empty(Target);
v51 = -1;
if ( !sub_67E5812D((unsigned __int32 *)&v51) || v51 == -1 )
{
*(_DWORD *)a1 = 1;
return;
}
pdwSize = 0;
v46 = 0;
v49 = 0;
pIpAddrTable = 0;
GetIpForwardTable(0, &pdwSize, 1);
if ( pdwSize && (v4 = (struct _MIB_IPFORWARDTABLE *)malloc(pdwSize), (v49 = v4) != 0) )
{
v5 = v4;
memset(v4, 0, pdwSize);
if ( !GetIpForwardTable(v5, &pdwSize, 1) )
{
v6 = v5->dwNumEntries;
v7 = -1;
v45 = -1;
v47 = 0;
if ( v6 )
{
v8 = (int)&v5->table[0].dwForwardMask;
while ( 1 )
{
if ( v51 > 0 && v51 != -1 )
{
v9 = *(_DWORD *)(v8 - 4);
if ( v9 > 0 && v9 != -1 && *(_DWORD *)v8 > 0u )
{
v10 = *(_DWORD *)v8 & v9;
if ( v10 == (v51 & *(_DWORD *)v8) )
break;
}
}
if ( !*(_DWORD *)(v8 - 4) && !*(_DWORD *)v8 && (v7 == -1 || *(_DWORD *)(v8 + 32) < v7) )
{
v7 = *(_DWORD *)(v8 + 32);
v45 = v47;
}
++v47;
v8 += 56;
if ( v47 >= v6 )
{
v11 = L"NetDetect";
v12 = L"func";
goto LABEL_23;
}
}
v40 = *(__int32 **)v8;
v39 = (int *)&v44;
v13 = Util::Network::IPToString(v10, v7);
v38 = *(_DWORD *)(v8 - 4);
v14 = (__int32 **)v13;
v37 = &v45;
v17 = (int **)Util::Network::IPToString(v16, v15);
v40 = *v14;
v39 = *v17;
v38 = v18;
Util::Network::IPToString(v18, v19);
v11 = L"NetDetect";
v12 = L"func";
sub_67DEBB47(
(int)L"file",
240,
(int)L"func",
3,
L"NetDetect",
(const char *)L"dwTargetIP[%s] forwardRaw.dwForwardDest[%s] forwardRaw.dwForwardMask[%s]",
v38);
CTXStringW::~CTXStringW((CTXStringW *)&v45);
CTXStringW::~CTXStringW((CTXStringW *)&v44);
v45 = v47;
LABEL_23:
if ( v45 != -1 )
{
v40 = (__int32 *)1;
*(_DWORD *)a1 = 1;
GetIpAddrTable(0, &v46, (BOOL)v40);
if ( v46 && (v20 = malloc(v46), (pIpAddrTable = (PMIB_IPADDRTABLE)v20) != 0) )
{
memset(v20, 0, v46);
if ( GetIpAddrTable(pIpAddrTable, &v46, 1) )
{
sub_67E2D52E((int)L"file", 272, (int)v12, 3, (int)v11, (const char *)L"%s", L"internal err");
}
else
{
v21 = (char *)v49 + 56 * v45 + 4;
v22 = 0;
if ( pIpAddrTable->dwNumEntries )
{
v23 = *((_DWORD *)v49 + 14 * v45 + 5);
v24 = (char *)pIpAddrTable->table;
while ( *((_DWORD *)v24 + 1) != v23 )
{
++v22;
v24 += 24;
if ( v22 >= pIpAddrTable->dwNumEntries )
goto LABEL_43;
}
if ( dword_67F5656C && *((_DWORD *)v49 + 14 * v45 + 4) != dword_67F5656C
|| dword_68017534 && *(_DWORD *)v24 != dword_68017534 )
{
v40 = *(__int32 **)v24;
v39 = (int *)&v41;
v25 = Util::Network::IPToString(v23, v22);
v38 = *((_DWORD *)v21 + 3);
v52 = (__int32 **)v25;
v37 = (int *)&v42;
v28 = (int **)Util::Network::IPToString(v27, v26);
v36 = dword_68017534;
v44 = v28;
v45 = Util::Network::IPToString(v30, v29);
v33 = (int **)Util::Network::IPToString(v32, v31);
v40 = *v52;
v39 = *v44;
v38 = *(_DWORD *)v45;
v37 = *v33;
v36 = v45;
Util::Network::IPToString(v45, v34);
sub_67DEBB47(
(int)L"file",
292,
(int)L"func",
2,
L"NetDetect",
(const char *)L"TargetIP=%s, PrevNextHop=%s, PrevIf=%s, CurrentNextHop=%s, CurrentIf=%s",
v36);
CTXStringW::~CTXStringW((CTXStringW *)&v47);
CTXStringW::~CTXStringW((CTXStringW *)&v43);
CTXStringW::~CTXStringW((CTXStringW *)&v42);
CTXStringW::~CTXStringW((CTXStringW *)&v41);
*(_DWORD *)a2 = 1;
}
v35 = *((_DWORD *)v21 + 3);
v40 = Target;
dword_67F5656C = v35;
v39 = *(int **)v24;
dword_68017534 = (int)v39;
*(_DWORD *)a3 = v39;
sub_67E57D1C((const wchar_t *)L"file", (int)v39, v40);
}
}
}
else
{
v40 = (__int32 *)L"internal err";
sub_67E2D52E((int)L"file", 265, (int)v12, 3, (int)v11, (const char *)L"%s", L"internal err");
}
}
}
goto LABEL_43;
}
sub_67E2D52E((int)L"file", 222, (int)L"func", 3, (int)L"NetDetect", (const char *)L"%s", L"internal err");
}
else
{
sub_67E2D52E((int)L"file", 213, (int)L"func", 3, (int)L"NetDetect", (const char *)L"%s", L"internal err");
}
*(_DWORD *)a1 = 1;
LABEL_43:
if ( v49 )
free(v49);
if ( pIpAddrTable )
free(pIpAddrTable);
}
发现QQ使用了GetIpForwardTable这个windows api,围绕GetIpForwardTable进行状态判断,经过查询,此函数说明如下:The GetIpForwardTable function retrieves the IPv4 routing table.
在网络上找到这样一篇文章《[Windows]如何判断指定的IP是否能访问?》
http://www.qdac.cc/?cat=51
把代码放上:
/*判断是否能够路由到指定的IP
Parameters
ARemote : 用来确定网卡的远程地址
Returns
在以下情况下,返回false:
1.无法路由到远程地址;
2.网卡被禁用或网卡断开连接
否则,返回true
*/
bool __fastcall CanRouteTo(const in_addr &ARemote)
{
MIB_IFTABLE *ATable;
DWORD ASize=0;
int ARetVal=0;//Unknown
if(GetIfTable(NULL,&ASize,false)==ERROR_INSUFFICIENT_BUFFER)
{
ATable=(MIB_IFTABLE *)malloc(ASize);
PMIB_IPFORWARDTABLE pIpForwardTable=NULL;
try
{
if(GetIfTable(ATable,&ASize,false)==ERROR_SUCCESS)
{
ASize=0;
int ALastMatric;
if(GetIpForwardTable(pIpForwardTable, &ASize, false)==ERROR_INSUFFICIENT_BUFFER)
{
pIpForwardTable = (PMIB_IPFORWARDTABLE)malloc(ASize);
if(GetIpForwardTable(pIpForwardTable, &ASize, false)==ERROR_SUCCESS)
{
for(int j=0; (j<pIpForwardTable->dwNumEntries)&&(ARetVal==0); j++)
{
if((pIpForwardTable->table[j].dwForwardMask&ARemote.S_un.S_addr)==pIpForwardTable->table[j].dwForwardDest)
{
if(pIpForwardTable->table[j].dwForwardType>=3)
{
for(DWORD i=0; i<ATable->dwNumEntries; i++)
{
if(pIpForwardTable->table[j].dwForwardIfIndex==ATable->table[i].dwIndex)
{
switch(ATable->table[i].dwOperStatus)
{
case MIB_IF_OPER_STATUS_OPERATIONAL:
case MIB_IF_OPER_STATUS_CONNECTED:
ARetVal=1;//Connected
break;
default:
ARetVal=2;//Disconnected
break;
}
break;
}
}
}
}
}
}
}
}
}
__finally
{
free(ATable);
if(pIpForwardTable)
free(pIpForwardTable);
}
}
return (ARetVal==1);
}
经过对比网上代码和ida伪代码对比,可以发现他们使用的原理是完全相同的,
最后总结一下QQ断网判断流程: QQ另起一定时器线程,
每一秒调用一次GetIpForwardTable这个window api,根据返回的状态判断当前是否断网,或是联网。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!