还是回到我们今天的主题上——尾巴,什么的尾巴?当然是鼎鼎大名“QQ尾巴病毒”。七、八两个月的《程序员》杂志上一直在讨论病毒和杀病毒的工作原理,当然人家讨论的都是那些很底层的病毒。说句实话有些看不太懂,唯一的收获就是对PE文件格式有些了结了。正好赶上这两天寝室同学的QQ中了“QQ尾巴”。呵呵,我真不知道因改怎么说他了,这个风靡于2003到2004年间的病毒,现在居然还有人青睐于他……废话,中病毒也不是他的错。他在痛苦的杀毒过程中,我有犯上了“妇科医生”的病,“QQ尾巴”到底是怎么工作的呢?
拿起听诊器——“Windows任务管理器”,打开X关机——“Spy++”,顺手在抄起手术刀——“Visual C++”,让我来主观臆断一下:
尾巴从哪里来的?
尾巴从哪里来的?这似乎是个很好解释的问题,WIN32的系统有一种很有用的东西,之所以说他有用,就是只要能好好利用他能出现很多惊人的效果。“QQ尾巴”当然就是用了“剪贴板”这个好东西!其实他就是通过剪贴板向QQ消息的那个RichEdit“贴”上一句话而已。之所以说是RichEdit,当然不是我猜出来的了,大家可以用Spy++查查看。

顺手就能写出这样一段代码:
| TCHAR g_str[] = "想看BT版的编程技术就来我的Blog:http://cnxiaohai.yculblog.com/"; // 函数功能:向文本框中粘贴尾巴 void PasteText(HWND hRich) { HGLOBAL hMem; LPTSTR pStr; // 分配内存空间 hMem = GlobalAlloc(GHND | GMEM_SHARE, sizeof(g_str)); pStr = GlobalLock(hMem); lstrcpy(pStr, g_str); GlobalUnlock(hMem); OpenClipboard(NULL); EmptyClipboard(); // 设置剪贴板文本 SetClipboardData(CF_TEXT, hMem); CloseClipboard(); // 释放内存空间 GlobalFree(hMem); // 粘贴文本 SendMessage(hRich, WM_PASTE, 0, 0); } |
尾巴什么时候贴上去呢?
这是一个关键的问题,我一个想法是用计时器来控制粘贴的时间。这样的话,代码道是好写了
| void CQQTailDlg::OnTimer(UINT nIDEvent) { PasteText(hRich); } |
但是问题马上就出来了,这种方法存在的很大的局限性。就是这个计时器的间隔到底应该是多少呢?其实,无论设置的合理或者是不合理,都是不合理的。不是贴不上去,就是在不该贴的时候贴出来了。
但,病毒本身不是这样的。看看同学那台中病毒的机器。它能够准确地在你单击“发送”或按下Ctrl+Enter键的时候将文本粘贴上。我在他的机器上还做过一个试验,开了一对占用CPU的程序,其实也就是一连开了好几个Super Pi,让他的CPU一直顶在100%上,这是我能很清楚的看到这段文本被粘贴到那个RichEdit里面。
马上换一个思路,我想到介绍做游戏外挂的文章中提到过一种东西叫:钩子!——为什么我不试试用钩子去思考这个问题呢?
钩子到底是什么东西啊?所谓Win32钩子(hook)“是一段子程序,它可以用来监视、检测系统中的特定消息,并完成一些特定的功能。”这句话使用某某某的书上抄来的。我妇科医生的本性又出来了。
想当年,乾隆年二十四年六月二十七——你看这时间多准啊!其实说二十八也行,没人跟我叫这真儿——闻学使施公愚山奉亲命赴济南府东昌县访贤,在东昌县亲审卞胭脂一案,为东郭名士宿介平反。“玄烨”就是Win32;“施愚山”就是这个钩子;“卞胭脂一案”就是这个消息;“平反宿介”就完成这个特定得功能。就像施愚山能专职为宿介平反一样,程序员也可以用钩子来捕获处理Windows系统中特定的消息。
问题回到了“QQ尾巴”上边,就是我们需要一个钩子,在用户单击了“发送”按钮之后,粘贴我们的文本。我想这段钩子是可以如下实现的,至于如何挂接这个钩子,我会在稍后说明:
| // 钩子过程,监视“发送”的命令消息 LRESULT CALLBACK CallWndProc(int nCode, WPARAM wParam, LPARAM lParam) { CWPSTRUCT *p = (CWPSTRUCT *)lParam; // 捕获“发送”按钮 if (p->message == WM_COMMAND && LOWORD(p->wParam) == 1) { PasteText(g_hRich); } return CallNextHookEx(g_hProc, nCode, wParam, lParam); } |
对于么几行试验性的代码,我道是可以说说道道一些了
1、lParam是一个指向CWPSTRUCT结构的指针,这个结构的描述如下:
| typedef struct { LPARAM lParam; WPARAM wParam; UINT message; HWND hwnd; } CWPSTRUCT, *PCWPSTRUCT; |
当然这是抄MSDN的,让我乎诹一个当然行,就怕WIN32系统不认。看到这里,像我一样的SDK fans一定会会心一笑:这不是窗口回调的那四个铁杆参数么?如你所说,的确是这样,你甚至可以使用switch (p->message) { /* ... */ }这样的代码写成的钩子函数来全面接管QQ窗口。
2、g_hRich是一个全局变量,它保存的是QQ消息文本框的句柄。这里之所以采用全局变量,是因为我无法从键盘钩子回调函数的参数中获得这个句柄。至于如何获得这个句柄以及这个全局变量的特殊位置,我会在稍后说明。
3、CallNextHookEx是调用钩子链中的下一个处理过程,换了施愚山就会说:“本亲命是来调查卞胭脂一案的,来人啊,提人犯宿介”——好了,不BT了——这是书写钩子函数中很重要的一个环节,如果少了这一句,那么可能会导致系统的钩子链出现错误,某些程序就会没有响应。
4、还有就是,为什么我捕获的是WM_COMMAND消息,这个原因让我来用下面的SDK代码来说明——虽然QQ是用MFC写的,但是用SDK代码才能说明WM_COMMAND和“发送”按钮的关系,怎么说MFC和SDK本事同根生吗。
| #define IDC_BTN_SENDMSG 1 // “发送”按钮ID的宏定义 // QQ发送消息对话框回调过程·伪造版 LRESULT CALLBACK ProcSendDlg(HWND hDlg, UINT Msg, WPARAM wParam, LPARAM lParam) { switch (Msg) { case WM_CLOSE: EndDialog(hDlg, 0); break; case WM_COMMAND: { switch (LOWORD(wParam)) { case IDC_BTN_SENDMSG: // 发送消息... break; // 其它的命令按钮处理部分... } } break; // 其它的case部分... } return 0; } |
消息发送的整个过程是:当用户单击了“发送”按钮后,这个按钮的父窗口——也就是“发送消息”的对话框——会收到一条WM_COMMAND的通知消息,其中wParam的低位字(即LOWORD(wParam))为这个按钮的ID,然后再调用代码中发送的部分。所以,在此我捕获WM_COMMAND消息要比捕获其它消息或挂接鼠标钩子要有效得多。
好了,现在这个钩子已经可以胜利地完成任务了。但是请不要忘记:有更多的用户更偏爱于用“Ctrl+Enter”热键来发送消息,所以程序中还需要挂上一个键盘钩子:
| // 键盘钩子过程,监视“发送”的热键消息 LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { // 捕获热键消息 if (wParam == VK_RETURN && GetAsyncKeyState(VK_CONTROL) < 0 && lParam >= 0) { PasteText(g_hRich); } return CallNextHookEx(g_hKey, nCode, wParam, lParam); } |
至于还有人喜欢用“Enter”热键来发送消息只要再加上一条就可以了。
在这里唯一要解释的一点就是lParam >= 0子句。很明显这个if判断是在判断热键Ctrl+Enter的输入,那么lParam >= 0又是什么呢?事实上在键盘钩子的回调之中,lParam是一个很重要的参数,它包含了击键的重复次数、扫描码、扩展键标志等等的信息。其中lParam的最高位(0x80000000)则表示了当前这个键是否被按下,如果这个位正在被按下,这个位就是0,反之为1。所以lParam >= 0的意思就是在WM_KEYDOWN的时候调用PasteText,也就是说,如果去掉这个条件,PasteText将会被调用两次——连同WM_KEYUP的一次,这个消息的观察也可以通过Spy++做到。
怎么把钩子挂到QQ的窗口上呢?
对于晒衣服大家都会的,晒衣服的时候当然只要考虑两个问题:衣服往哪里晒?怎么晒?——好像又是一个可以上升到哲学高度的问题了。当然,对于挂接钩子,要解决的问题是:往哪里挂接钩子,以及如何挂接?
挂接钩子的目标,肯定是QQ“发送信息”窗口的所属线程。就是将这个窗口的句柄传入之后来进行钩子的挂接:
| // 挂接钩子 BOOL WINAPI SetHook(HWND hQQ) { BOOL bRet = FALSE; if (hQQ != NULL) { DWORD dwThreadID = GetWindowThreadProcessId(hQQ, NULL); g_hRich = GetWindow(GetDlgItem(hQQ, 0), GW_CHILD); if (g_hRich == NULL) { return FALSE; } // 挂接钩子 g_hProc = SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, g_hInstDLL, dwThreadID); g_hKey = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstDLL, dwThreadID); bRet = (g_hProc != NULL) && (g_hKey != NULL); } else { // 卸载钩子 bRet = UnhookWindowsHookEx(g_hProc) && UnhookWindowsHookEx(g_hKey); g_hProc = NULL; g_hKey = NULL; g_hRich = NULL; } return bRet; } |
到此为止,以上所有的代码都位于一个DLL的动态链接库之中,关于DLL我就不多介绍了。DLL之中已经做好了所有重要的工作——事实上这部分工作也只能由DLL来完成,这是由Windows虚拟内存机制决定的——我们只需要在EXE之中调用导出的SetHook函数就可以了。那么,SetHook的参数如何获得呢?接着做试验。
| HWND hSend; g_hQQ = NULL; SetHook(NULL); do { g_hQQ = FindWindowEx(NULL, g_hQQ, "#32770", NULL); hSend = FindWindowEx(g_hQQ, NULL, "Button", "发送(&S)"); } while(g_hQQ != NULL && hSend == NULL); if (g_hQQ != NULL) { SetHook(g_hQQ); } |
至于"#32770","Button", "发送(&S)"都是可以再Spy++中找到的。这段代码中的do-while循环就是用来查找“发送消息”的窗口的,QQ窗口的保密性越来越强了,窗口一层套一层,找起来十分不便——这也是双刃剑,QQ占用系统内存越来越大,QQ用起来越来越慢,也是多亏了这些窗口嵌套的帮忙啊。
DLL的共享数据段
| // 定义共享数据段 #pragma data_seg("shared") HHOOK g_hProc = NULL; // 窗口过程钩子句柄 HHOOK g_hKey = NULL; // 键盘钩子句柄 HWND g_hRich = NULL; // 文本框句柄 #pragma data_seg() #pragma comment(linker, "/section:shared,rws") |
这定义了一段共享的数据段,是的,我已经在注释李写得很清楚了,那么共享数据段起到了什么作用呢?在回答这个问题之前,我们把大脑变成Visual C++的编译器,在大脑中把代码中以#开头的预处理指令注释掉然后重新编译这个DLL并运行,用大脑运行一下:好像,好像,尾巴没了。
是的,添加尾巴失败!
我的解释是:这个DLL需要将一个实例映射到EXE的地址空间之中以供其调用,还需要将另一个实例映射到QQ的地址空间之中来完成挂接钩子的工作。也就是说,当钩子挂接完毕之后,整个系统的模块中,有两个DLL实例的存在!此DLL非彼DLL也,所以它们之间是没有任何联系的。拿全局变量g_hRich来说,图中左边的DLL通过EXE的传入获得了文本框的句柄,然而如果没有共享段的话,那么右边的DLL中,g_hRich仍然是NULL。共享段于此的意义也就体现出来了,就是为了保证EXE、DLL、QQ三者之间的联系。这一点,和C++中static的成员变量有些相似。
如果在钩子挂接成功之后,你可以通过一些有模块查看功能的进程管理器看一看,就会发现这个DLL也位于QQ.exe的模块之中。
再废话一些
当然就算我把我的这些想法全实现了,他也不是一个病毒。病毒的自我复制性和传播性都是没有实现的。不是我为了保护大家的QQ,是我也不知道这个因改怎么做。不过听说,它利用IE的邮件头漏洞在QQ上传播的。我还不理解这是怎么会是,不过我想,这个漏洞我们尊敬的Microsoft应该早就缝过补丁了。
对于我这样想法的“QQ尾巴”是可以手工杀毒的,其实,只要通过任务管理器,就能中止这个进程。但是现在的“QQ尾巴”增加了复活功能——在EXE被杀掉后,DLL会将其唤醒。查过一些资料,这一技术是利用CreateRemoteThread在所有的进程上各插入了一个额外的复活线程,真可谓是一石二鸟——保证EXE永远运行,同时这个正在使用中的DLL是无法被删除的。有兴趣的朋友可以参考Jeffrey Richter《Windows核心编程》的相关章节。
