前言
光学不练假把式,当我们接触一个新知识的时候并非会认为这个知识很有用。只有当我们实际使用后才知道Hook是有多好“玩”,本期将带领各位进行Detour Hook框架的实战,抓取QQ消息并对抓取数据进行解析,注意这里是抓取本地消息并非获取他人的消息。博客仅供学习,不能用于其他商业和非法用途。另外,本期代码涉及到隐私内容,并不会完全公布代码,仅分享部分重要代码,再次申明本内容仅供学习!
博客重点在于讲解Detour Hook框架的使用,对于挂钩目标函数获取仅作引导并不会实际展示过程。
其余相关内容
C++ Hook钩子技术(中)——Hook按键小精灵弹窗
C++ Hook钩子技术(上)——Detour框架
一、如何获取目标挂钩函数?
我们知道,在公网进行数据传输时肯定会对数据进行加密,而且鹅厂对本地消息数据都进行了加密,但是数据显示到聊天窗口肯定不能是密文吧,它必须的是我们都认识的文字。因此思路就是对加解密函数进行挂钩,这样就能得到加密前的明文和解密后的明文了。这里有个问题啊,为啥要同时对加解密函数进行挂钩呢?一个不可以吗?这个问题就留给屏幕前的你思考吧!
搞明白要挂勾的函数之后,我们还需要知道这个函数的定义,那么就需要用到xdbg来逆向了。还是给个思路啊,第一个思路是每当我们发送QQ消息时在输入框输入消息后点击发送,此时输入框这个内存会被访问,而且会被清空,那么就可以使用CE扫描到这块内存进行追踪。第二个思路就是对send和recv函数进行下端追踪,这两个函数肯定是频繁调用的,所以这是比较考验手速的。当然思路和实际是有出入的,有兴趣的小伙伴可以去逆向一下。
二、代码实现
整体代码框架基于C++ Hook钩子技术(上)——Detour框架,本篇是Detour Hook框架的实战篇。
1.功能函数
因为加解密函数是QQ这个软件comment.dll库里面的导出函数,而且这个函数的名字后缀可能跟随版本进行较小的变化,这里直接使用loadlibrary和getprocaddres是行不通的,因为函数名字不能确定,仅仅能确定部分。这个时候需要重载getprocaddress,需要把函数名相等修改为函数名包含,如果扫描到的当前函数函数名包含了我们给定的函数名就找到并返回函数地址即可。
//获取dll里面的函数地址
unsigned int __stdcall GetApiAddrFromDll(unsigned int pIdh, char* lpApiName, int iApiNameLen)
{
//函数略
//此函数为遍历PE文件导出表的导出函数,然后将函数名相等匹配修改为包含匹配
}
因为我们需要获取加密和解密两个函数地址,这里再封装一个函数获取这两个函数地址。
//获取挂钩函数地址
bool GetHookProcAddress()
{
HMODULE hModule = GetModuleHandle(L"common.dll");
if (!hModule)
{
OutputDebugStringA("HookQQMsg: Get Common.dll base address error!");
return false;
}
Realoi_symmetry_decrypt2 = (Poi_symmetry_decrypt2)GetApiAddrFromDll((UINT)hModule, "?oi_symmetry_decrypt2", 21);
if (!Realoi_symmetry_decrypt2)
{
OutputDebugStringA("HookQQMsg: Get decrypt address error!");
CloseHandle(hModule);
return false;
}
Realoi_symmetry_encrypt2 = (Poi_symmetry_encrypt2)GetApiAddrFromDll((UINT)hModule, "?oi_symmetry_encrypt2", 21);
if (!Realoi_symmetry_encrypt2)
{
OutputDebugStringA("HookQQMsg: Get encrypt address error!");
CloseHandle(hModule);
return false;
}
CloseHandle(hModule);
return true;
}
然后还需要一个utf8字符编码转GBK,QQ传输的字符编码是utf8,而我们计算机显示的是GBK编码的文字。
char* __stdcall ConvertUtf8ToGBK(char* strUtf8, char* strPlat)
{
//略
}
即使QQ消息解密后,他的消息还是需要进一步进行解析才能获得相应的类容,这里我已经实现了一个解析数据的函数。但是并不完善,这只是包含了对文字消息以及群聊中昵称的解析,还包括了发送消息的数据以及发送消息的双方QQ号,其余的内容感兴趣的同学可以自行优化。
//解析QQ数据
void SplitInfor(char* tPack, int dataLen, char* startIndex, char* msg, char* nikeName)
{
char strTemp[MAX_MESSAGE_LENGTH] = {
0 };
DWORD fristLen, lastLen;
while (startIndex < tPack + dataLen)
{
switch (*startIndex)
{
case 0x01:
{
startIndex++;
fristLen = ntohs(*(unsigned short*)(startIndex));
lastLen = ntohs(*(unsigned short*)(startIndex + 3));
memmove(strTemp, startIndex + 5, lastLen);
ConvertUtf8ToGBK(strTemp, msg);
startIndex = startIndex + 2 + fristLen;
break;
}
case 0x12:
{
startIndex++;
fristLen = ntohs(*(unsigned short*)(startIndex));
lastLen = ntohs(*(unsigned short*)(startIndex + 3));
memmove(strTemp, startIndex + 5, lastLen);
ConvertUtf8ToGBK(strTemp, nikeName);
startIndex = startIndex + 2 + fristLen;
break;
}
default:
{
startIndex++;
fristLen = ntohs(*(unsigned short*)(startIndex));
startIndex = startIndex + 2 + fristLen;
break;
}
}
}
}
int __stdcall ParseQQDataPack(char* Pack, int PackLen)
{
char* pData = Pack;
unsigned int hostQQ = 0;
unsigned int iToQQ = 0;
unsigned int iSenderQQ = 0;
char strMsgTime[120] = {
0 };
char inforTemp[MAX_MESSAGE_LENGTH] = {
0 };
char strMsg[MAX_MESSAGE_LENGTH] = {
0 };
char strNikeName[QQ_NAME_MAX_SIZE] = {
0 };
DWORD WordsLen;
if (memcmp(Pack + 46, "\x00\x01\x00\x01\x01", 5) == 0)
{
if (*(Pack + 106) != '\x86') return false;
//群固定数据
hostQQ = ntohl(*(unsigned int*)(Pack));
iToQQ = ntohl(*(unsigned int*)(Pack + 4));
iSenderQQ = ntohl(*(unsigned int*)(Pack + 56));
time_t tmTime = ntohl(*(unsigned long*)(Pack + 92));
tm sttmTime;
errno_t error_t = localtime_s