上一篇写得有点凌乱了,所以把前面的内容再重写下....
2.编写QQ显IP插件
2.1 编写加载外挂dll的程序
这里采用VC作为开发环境,我使用的是VS2005
首先说下大体的思路:
这里仿造coralQQ一样,将真正的外挂代码写入dll,然后编写一个exe文件去加载qq.exe,并把我们写得dll注入。
这一步在前一篇文章中已经给出了具体代码。
2.2 编写插件主体
感觉自己如果一涉及具体代码就回说不来话,所以这次就先谈思路,然后给出部分代码。
dll要做的事:
将我们前面分析过的关键API挂钩,以便得到IQQCore *,Uin,以及在qq聊天窗口弹出时进行捕获以便显示IP信息。
要做到上面的要求,首先就是要在dll加载后开始做API hook的工作。
不过这件事情不能在DllMain里面写,推荐的做法是在DllMain里面创建一个新的线程,线程的执行函数我们设为:WorkerProc(VC编写的话推荐用_beginthread创建这个线程,否则无法使用CRT函数),为什么不能直接在DllMain里面写我稍候分析
然后就是在WorkerProc里面写入具体的hook代码。
1.拦截QQHelperDll.dll中的IsLogin函数
前面分析过了,IQQCore *指针可以通过拦截这个API来获取。
这里我们采用的是《windows核心编程》中推荐的拦截API hook的办法(修改入口地址跳转相对他烦了些,而且我们这里只是想做hook):通过动态修改API调用者模块的IAT表,将原先API的入口地址替换成我们函数的。具体的替换代码我们就采用书中提供的了的ReplaceIATEntryInOneMod(见前一篇)。
现在要注意一个问题,我们用来替换原先API的函数必须和原函数采用同样的调用规范,这个IsLogin本身就是cdecl得,所以只要用个形参和返回值一样的函数取替换即可:
给出实现本功能的代码:
typedef int (*OrgIsLogin)(DWORD ptrIQQCore);
int PokeIsLogin(DWORD ptrIQQCore)
{
global_ptrIQQCore = ptrIQQCore;
return ((OrgIsLogin)(PROC)OrgIsLoginProc)(ptrIQQCore);
}
////下面代码在WorkerProc函数中///
OrgIsLoginProc = GetProcAddress( GetModuleHandleA(QQHelperDll.dll),?IsLogin@@YAHPAUIQQCore@@@Z);
if (ReplaceIATEntryInOneMod(QQHelperDll.dll,OrgIsLoginProc,(PROC)&PokeIsLogin,GetModuleHandleA(qq.exe)))
{
//替换成功
}
//
今后,只要QQ.exe调用IsLogin,我们的PokeIsLogin函数就会调用,并把IQQCore纪录下来,注意替换的函数要最后去调用原函数,否则就会让程序崩溃。
2.拦截SetForegroundWindow API
拦截该API是因为每次弹出新的QQ聊天窗口时,CQQApplication.dll都会去调用它,以便把聊天窗口至于前景显示。所以拦截CQQApplication.dll对他的调用就能在由新的聊天窗口出现时调用我们插件的代码去显示IP信息,而不必傻傻的去写循环等待了
这里就要注意SetForegroundWindow 是采用stdall调用规范的,所以别忘在替换函数名前加上__stdcall(或WINAPI宏)标记。
该部分的替换代码如下:
typedef BOOL ( __stdcall *OrgSetForegrandWindow)(HWND hWnd);
extern C BOOL APIENTRY OnQQWndShow(HWND hWnd)
{
bool trueResult = ((OrgSetForegrandWindow)(PROC)OrgFuncProc)(hWnd);
return trueResult;
}
//下面代码在WorkerProc函数或由其调用的函数中
OrgFuncProc = GetProcAddress( GetModuleHandleA(user32),SetForegroundWindow);
if (ReplaceIATEntryInOneMod(user32.dll,OrgFuncProc,(PROC)&OnQQWndShow,global_hCQQAppModule))
{
//替换函数入口成功
}
其中global_hCQQAppModule是CQQApplication.dll在qq.exe中的模块句柄,可以调用GetModuleHandleA(CQQApplication.dll)来实现。不过现在存在一个问题
用od加载qq.exe就会发现实际上CQQApplication.dll并不是在qq.exe启动后加载的,而是在登录以后。所以如果我们的dll插件在被加载之后立刻调用GetModuleHandleA的话就会返回NULL,那么之后的函数替换就不可能实现了。
这里我用了个比较笨的办法,在替换SetForegroundWindow前编写循环不断的去掉用GetModuleHandleA,直到返回非null,而为了防止频率过快可以在循环中加上Sleep函数。(之前也尝试loadlibrary提前加载,但一直失败。还有中办法就是从前面的IsLogin来判断是否登录了QQ。但没有试验过。总之如果你有比较好的办法也希望告诉我)
这部分的代码如下:
bool WaitforLogon()
{
while(!bIsDllUnload)
{
global_hCQQAppModule = GetModuleHandleA(CQQApplication.dll);
if (global_hCQQAppModule) return true;
Sleep(300);
}
return false;
}
这就是为什么前面说过不能再dllmain里面作函数替换的工作了,否则会导致dllmain无法退出,从而会锁死整个qq
3获取Uin
现在IQQCore *已经获得了,同时只要在OnQQWndShow中写入得到用户IP信息的代码,再使用FindWindow的方法把原先的广告去除,再创建个Edit或者Static来显示我们的数据即可。
但之前还要得到Uin,也就是对方好友的QQ号。
其实这里有个很笨的办法:QQ聊天对话框中有对方的号码的:比如“&heaven(27605046)(后面是个性签名)。的确可以用FindWindow+GetWindowText获取,然后得到括号里面的数据就好了,但是是否有更简单办法呢?有
这里要感谢明日帝国(sunwangme)的教程,这里就是用他的方法了,具体原理还是大家去他blog看吧(google一下)
大体的方法是:在QQ准备显示聊天对话框时,上面“&heaven(27605046)(后面是个性签名)这段文字会调用位于QQBaseClassInDll.dll中的CAllInOneStatusBar::SetUin(unsigned long);这个函数,其中参数就是我们要的QQ号了。而且可以保证CQQApplication.dll仅仅在要显示对话框前才会调用它。(再次感谢明日帝国)
所以和上面一样,这次拦截CAllInOneStatusBar::SetUin(unsigned long);
OrgSetUINProc =
GetProcAddress(GetModuleHandleA(QQBaseClassInDll.dll),?SetUin@CAllInOneStatusBar@@QAEX_J@Z);
if (ReplaceIATEntryInOneMod(QQBaseClassInDll.dll,OrgSetUINProc,(PROC)&Poke_GETUIDA,global_hCQQAllInOne)){
}
今后就会先调用我们的OrgSetUINProc,其中获取参数即可。
不过要注意2个问题,第一这个QQBaseClassInDll.dll也不是一开始加载,更不是在登录后加载,而是在第一次要显示QQ聊天窗口才加载,不过比较幸运的是可以直接LoadLibrary把它先载过来。
还有一个问题就是SetUin是thiscall规范的……哎,自己对thiscall还不是很了解,所以怕出错,那个OrgSetUINProc只能用naked调用规范了,同时要自己写汇编去完成细节的事:
__declspec( naked ) int Poke_GETUIDA()
{
_asm
{
push ecx
mov ecx,[esp+8]
mov dwRecentUINA,ecx
pop ecx
jmp OrgSetUINProc
}
}
ok,这样一切都完成了
3.编写获取IP信息的代码
先整理下上面我们3个替换函数的执行顺序:
PokeIsLogin:最好执行,且今后不断被调用
Poke_GETUIDA:在将要显示QQ聊天窗口时执行
OnQQWndShow:在Poke_GETUIDA之后运行。
所以可以保证在OnQQWndShow中可以得到我们获取IP信息所有必要的参数了。
那么就开始编写获取IP的代码:
按照前面分析的,先用BasicCtr.GetFriendQQData得到当前好友的IQQData *:
DWORD MainHandle;
if (ptrBasicCtr_GetFriendQQData==NULL || global_ptrIQQCore==NULL) return 0;
_asm
{
mov edx,dwID
mov eax,global_ptrIQQCore
lea ecx,MainHandle
push ecx
push edx
push eax
call ptrBasicCtr_GetFriendQQData ; BasicCtr.GetFriendQQData
add esp,0xC
}
其中dwID是Uin,即用DWORD保存的QQ号码
global_ptrIQQCore就是前面获得的IQQCore *
ptrBasicCtr_GetFriendQQData 是GetFriendQQData的入口地址,用GetProcAddress得到
最终MainHandle将保存IQQData *,如果运行失败,会返回null
接下来就是通过QQUSER_DYNAMIC_DATA(暂时命名)函数来得到动态信息类的指针:
char *szQQUSER_DYNAMIC_DATA = QQUSER_DYNAMIC_DATA;
DWORD tmpInfo;
DWORD returnVal;
tmpInfo = 0xba863a1e;
if (mainHandle == NULL)
{
return NULL;
}
_asm
{
mov eax,mainHandle
lea edx,returnVal
push edx
lea edx,tmpInfo
push edx
mov ecx,[eax]
push szQQUSER_DYNAMIC_DATA
push eax
call [ecx+54h]
}
结果将保存在returnVal中,同样,如果执行错误returnVal=NULL
最后就是去获取IP地址了
char *szwProcotol = wProcotol;
char *szRecentip=dwRecentIP;
char *szwRecentPort=wRecentPort;
char *szdwC2CIP=dwC2CIP;
char *szwC2CPort=wC2CPort;
char *szdwIP=dwIP;
char *szwPort=wPort;
bool GetDestIPInfo(DWORD ptrClassHandle,DWORD *ptrDestIp,DWORD *ptrDestPort)
{
if (ptrClassHandle==NULL || ptrDestIp==NULL || ptrDestPort==NULL) return false;
//Using Std Info Buffer
_asm
{
pushad
pushf
mov eax,ptrClassHandle
mov ecx,[eax]
mov edx,ptrDestIp
push edx
push szRecentip
push eax
mov eax,[ecx+34h]
call eax
mov eax,ptrClassHandle
mov ecx,[eax]
mov edx,ptrDestPort
push edx
push szwRecentPort
push eax
mov eax,[ecx+30h]
call eax
popf
popad
}
(*ptrDestPort) &= 0xFFFF;
if ((*ptrDestIp) != NULL && (*ptrDestPort) != NULL) return true;
_asm
{
pushad
pushf
mov eax,ptrClassHandle
mov ecx,[eax]
mov edx,ptrDestIp
push edx
push szdwC2CIP
push eax
mov eax,[ecx+34h]
call eax
mov eax,ptrClassHandle
mov ecx,[eax]
mov edx,ptrDestPort
push edx
push szwC2CPort
push eax
mov eax,[ecx+30h]
call eax
popf
popad
}
(*ptrDestPort) &= 0xFFFF;
if ((*ptrDestIp) != NULL && (*ptrDestPort) != NULL) return true;
_asm
{
pushad
pushf
mov eax,ptrClassHandle
mov ecx,[eax]
mov edx,ptrDestIp
push edx
push szdwIP
push eax
mov eax,[ecx+34h]
call eax
mov eax,ptrClassHandle
mov ecx,[eax]
mov edx,ptrDestPort
push edx
push szwPort
push eax
mov eax,[ecx+30h]
call eax
popf
popad
}
(*ptrDestPort) &= 0xFFFF;
return ((*ptrDestIp) != NULL && (*ptrDestPort) != NULL);
}
ptrClassHandle是前面得到的returnVal,IP地址的DWORD数据和WORD的port信息将保存在后2个参数指针的地址中。(代码借鉴了CoralQQ)
到目前为止已大功告成!其他的细节代码就请各位发挥吧
4.扩展
上面并没有介绍如何得到对方QQ版本的信息,其实和获取IP一样,你在CQQApplication.dll中找到wProcotol的字符串参考,就会发现它和获取IP的代码很像,直接抄下来用就是了
CoralQQ还能在鼠标移动到好友头像上弹出的信息卡片下方显示IP,其实机制是一样的,这里就不说了,可以看看明日帝国的教程~
好了,我的教程就到此为止,希望能对大家有用~有什么错误希望能立刻指正