前言
光学不练假把式,当我们接触一个新知识的时候并非会认为这个知识很有用。只有当我们实际使用后才知道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(&sttmTime, &tmTime);
error_t = asctime_s(strMsgTime, 120, (const tm*)&sttmTime);
WordsLen = ntohs(*(unsigned short*)(Pack + 108));
SplitInfor(Pack, PackLen, Pack + 112 + WordsLen, strMsg, strNikeName);
if (strlen(strMsg) > 0)
{
int iLen = wsprintfA(inforTemp, "HookQQMsg: group&cf&%u&cf&%u&cf&%s&cf&%u&cf&%s&cf&%s\r\n", hostQQ, iSenderQQ, strNikeName, iToQQ, strMsg, strMsgTime);
OutputDebugStringA(inforTemp);
return true;
}
else
{
return false;
}
}
else
{
if (*(Pack + 123) == '\x86')//otm
{
hostQQ = ntohl(*(unsigned int*)(Pack));
iToQQ = ntohl(*(unsigned int*)(Pack + 4));
iSenderQQ = ntohl(*(unsigned int*)(Pack + 58));
time_t tmTime = ntohl(*(unsigned long*)(Pack + 109));
tm sttmTime;
errno_t error_t = localtime_s(&sttmTime, &tmTime);
error_t = asctime_s(strMsgTime, 120, (const tm*)&sttmTime);
WordsLen = ntohs(*(unsigned short*)(Pack + 125));
SplitInfor(Pack, PackLen, Pack + 129 + WordsLen, strMsg, strNikeName);
if (strlen(strMsg) > 0)
{
int iLen = wsprintfA(inforTemp, "HookQQMsg: otm&cf&%u&cf&%u&cf&%u&cf&%s&cf&%s\r\n", hostQQ, iSenderQQ, iToQQ, strMsg, strMsgTime);
OutputDebugStringA(inforTemp);
return true;
}
else
{
return false;
}
}
else if (*(Pack + 87) == '\x86') //mto
{
hostQQ = ntohl(*(unsigned int*)(Pack));
iToQQ = ntohl(*(unsigned int*)(Pack + 4));
iSenderQQ = ntohl(*(unsigned int*)(Pack + 22));
time_t tmTime = ntohl(*(unsigned long*)(Pack + 73));
tm sttmTime;
errno_t error_t = localtime_s(&sttmTime, &tmTime);
error_t = asctime_s(strMsgTime, 120, (const tm*)&sttmTime);
WordsLen = ntohs(*(unsigned short*)(Pack + 89));
SplitInfor(Pack, PackLen, Pack + 93 + WordsLen, strMsg, strNikeName);
if (strlen(strMsg) > 0)
{
int iLen = wsprintfA(inforTemp, "HookQQMsg: mto&cf&%u&cf&%u&cf&%u&cf&%s&cf&%s\r\n", hostQQ, iSenderQQ, iToQQ, strMsg, strMsgTime);
OutputDebugStringA(inforTemp);
return true;
}
else
{
return false;
}
}
else if (*(Pack + 92) == '\x86')
{
hostQQ = ntohl(*(unsigned int*)(Pack));
iToQQ = ntohl(*(unsigned int*)(Pack + 4));
iSenderQQ = ntohl(*(unsigned int*)(Pack + 27));
time_t tmTime = ntohl(*(unsigned long*)(Pack + 78));
tm sttmTime;
errno_t error_t = localtime_s(&sttmTime, &tmTime);
error_t = asctime_s(strMsgTime, 120, (const tm*)&sttmTime);
WordsLen = ntohs(*(unsigned short*)(Pack + 94));
SplitInfor(Pack, PackLen, Pack + 98 + WordsLen, strMsg, strNikeName);
if (strlen(strMsg) > 0)
{
int iLen = wsprintfA(inforTemp, "HookQQMsg: mto&cf&%u&cf&%u&cf&%u&cf&%s&cf&%s\r\n", hostQQ, iSenderQQ, iToQQ, strMsg, strMsgTime);
OutputDebugStringA(inforTemp);
return true;
}
else
{
return false;
}
}
else
return false;
}
return false;
}
解析数据给个思路,从数据第一个“86”这个字节往后固定格式为字体标识符一个字节,然后字体编码字节长度占两个字节,紧接着两个空字符。到此!!!!后面的格式为标识符一个字节,数据长度两个字节后面跟着数据…
功能函数完成之后就需要编写两个代理函数了。
2.代理函数
为了保证QQ程序的稳定性,不论什么情况下都需要调用原来的加解密函数。数据长度用来筛选非MSG消息的解析,减少不必要的时间开销。
//加解密代理函数
void __cdecl Myoi_symmetry_decrypt2(char* rawData, unsigned int rawLength, char* pwd, char* handleData, int* handleLength)
{
Realoi_symmetry_decrypt2(rawData, rawLength, pwd, handleData, handleLength);
if (rawLength<=32)
{
return;
}
ParseQQDataPack(handleData, *handleLength);
}
void __cdecl Myoi_symmetry_encrypt2(char* rawData, unsigned int rawLength, char* pwd, char* handleData, int* handleLength)
{
if (rawLength <= 32)
{
Realoi_symmetry_encrypt2(rawData, rawLength, pwd, handleData, handleLength);
return;
}
ParseQQDataPack(handleData, *handleLength);
Realoi_symmetry_encrypt2(rawData, rawLength, pwd, handleData, handleLength);
}
3.代码整合
#include "pch.h"
#include <winsock2.h>
#include <time.h>
#include "detours.h"
#pragma comment(lib,"./libX86/detours.lib")
#pragma comment(lib,"Ws2_32.lib")
#define MAX_MESSAGE_LENGTH 0x1000
#define QQ_NAME_MAX_SIZE 0x100
int __stdcall ParseQQDataPack(char* Pack, int PackLen);
/*
<gettencentqqmsg.void __cdecl My_oi_symmetry_decrypt2(char *, unsigned int, char *, char *, int *)>
<gettencentqqmsg.void __cdecl My_oi_symmetry_encrypt2(char *, unsigned int, char *, char *, int *)>
*/
typedef void(__cdecl* Poi_symmetry_decrypt2)(char*, unsigned int, char*, char*, int*);
typedef void(__cdecl* Poi_symmetry_encrypt2)(char*, unsigned int, char*, char*, int*);
Poi_symmetry_decrypt2 Realoi_symmetry_decrypt2 = NULL;
Poi_symmetry_encrypt2 Realoi_symmetry_encrypt2 = NULL;
//加解密代理函数
void __cdecl Myoi_symmetry_decrypt2(char* rawData, unsigned int rawLength, char* pwd, char* handleData, int* handleLength)
{
Realoi_symmetry_decrypt2(rawData, rawLength, pwd, handleData, handleLength);
if (rawLength<=32)
{
return;
}
ParseQQDataPack(handleData, *handleLength);
}
void __cdecl Myoi_symmetry_encrypt2(char* rawData, unsigned int rawLength, char* pwd, char* handleData, int* handleLength)
{
if (rawLength <= 32)
{
Realoi_symmetry_encrypt2(rawData, rawLength, pwd, handleData, handleLength);
return;
}
ParseQQDataPack(handleData, *handleLength);
Realoi_symmetry_encrypt2(rawData, rawLength, pwd, handleData, handleLength);
}
//获取dll里面的函数地址
unsigned int __stdcall GetApiAddrFromDll(unsigned int pIdh, char* lpApiName, int iApiNameLen);
//获取挂钩函数地址
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
char* __stdcall ConvertUtf8ToGBK(char* strUtf8, char* strPlat);
//解析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(&sttmTime, &tmTime);
error_t = asctime_s(strMsgTime, 120, (const tm*)&sttmTime);
WordsLen = ntohs(*(unsigned short*)(Pack + 108));
SplitInfor(Pack, PackLen, Pack + 112 + WordsLen, strMsg, strNikeName);
if (strlen(strMsg) > 0)
{
int iLen = wsprintfA(inforTemp, "HookQQMsg: group&cf&%u&cf&%u&cf&%s&cf&%u&cf&%s&cf&%s\r\n", hostQQ, iSenderQQ, strNikeName, iToQQ, strMsg, strMsgTime);
OutputDebugStringA(inforTemp);
return true;
}
else
{
return false;
}
}
else
{
if (*(Pack + 123) == '\x86')
{
hostQQ = ntohl(*(unsigned int*)(Pack));
iToQQ = ntohl(*(unsigned int*)(Pack + 4));
iSenderQQ = ntohl(*(unsigned int*)(Pack + 58));
time_t tmTime = ntohl(*(unsigned long*)(Pack + 109));
tm sttmTime;
errno_t error_t = localtime_s(&sttmTime, &tmTime);
error_t = asctime_s(strMsgTime, 120, (const tm*)&sttmTime);
WordsLen = ntohs(*(unsigned short*)(Pack + 125));
SplitInfor(Pack, PackLen, Pack + 129 + WordsLen, strMsg, strNikeName);
if (strlen(strMsg) > 0)
{
int iLen = wsprintfA(inforTemp, "HookQQMsg: otm&cf&%u&cf&%u&cf&%u&cf&%s&cf&%s\r\n", hostQQ, iSenderQQ, iToQQ, strMsg, strMsgTime);
OutputDebugStringA(inforTemp);
return true;
}
else
{
return false;
}
}
else if (*(Pack + 87) == '\x86')
{
hostQQ = ntohl(*(unsigned int*)(Pack));
iToQQ = ntohl(*(unsigned int*)(Pack + 4));
iSenderQQ = ntohl(*(unsigned int*)(Pack + 22));
time_t tmTime = ntohl(*(unsigned long*)(Pack + 73));
tm sttmTime;
errno_t error_t = localtime_s(&sttmTime, &tmTime);
error_t = asctime_s(strMsgTime, 120, (const tm*)&sttmTime);
WordsLen = ntohs(*(unsigned short*)(Pack + 89));
SplitInfor(Pack, PackLen, Pack + 93 + WordsLen, strMsg, strNikeName);
if (strlen(strMsg) > 0)
{
int iLen = wsprintfA(inforTemp, "HookQQMsg: mto&cf&%u&cf&%u&cf&%u&cf&%s&cf&%s\r\n", hostQQ, iSenderQQ, iToQQ, strMsg, strMsgTime);
OutputDebugStringA(inforTemp);
return true;
}
else
{
return false;
}
}
else if (*(Pack + 92) == '\x86')
{
hostQQ = ntohl(*(unsigned int*)(Pack));
iToQQ = ntohl(*(unsigned int*)(Pack + 4));
iSenderQQ = ntohl(*(unsigned int*)(Pack + 27));
time_t tmTime = ntohl(*(unsigned long*)(Pack + 78));
tm sttmTime;
errno_t error_t = localtime_s(&sttmTime, &tmTime);
error_t = asctime_s(strMsgTime, 120, (const tm*)&sttmTime);
WordsLen = ntohs(*(unsigned short*)(Pack + 94));
SplitInfor(Pack, PackLen, Pack + 98 + WordsLen, strMsg, strNikeName);
if (strlen(strMsg) > 0)
{
int iLen = wsprintfA(inforTemp, "HookQQMsg: mto&cf&%u&cf&%u&cf&%u&cf&%s&cf&%s\r\n", hostQQ, iSenderQQ, iToQQ, strMsg, strMsgTime);
OutputDebugStringA(inforTemp);
return true;
}
else
{
return false;
}
}
else
return false;
}
return false;
}
void StartDetourHook()
{
if (!GetHookProcAddress())
return;
//1.保存detour的事务
DetourRestoreAfterWith();
//2.开始处理detour的事务
DetourTransactionBegin();
//3.更新线程信息-执行事务的线程
DetourUpdateThread(GetCurrentThread());
//4.设置需要拦截的代理函数
//第一个参数二级函数指针-原函数地址
//第二个参数函数地址-代理函数的地址
DetourAttach((PVOID *)&Realoi_symmetry_decrypt2, Myoi_symmetry_decrypt2);
DetourAttach((PVOID *)&Realoi_symmetry_encrypt2, Myoi_symmetry_encrypt2);
//5.提交事务
DetourTransactionCommit();
}
void ExitDetourHook()
{
if (!Realoi_symmetry_decrypt2)
{
return;
}
//1.开始处理detour的事务
DetourTransactionBegin();
//2.更新线程信息-执行事务的线程
DetourUpdateThread(GetCurrentThread());
//3.撤销hook
//第一个参数二级函数指针-原函数地址
//第二个参数函数地址-代理函数的地址
DetourAttach((PVOID*)&Realoi_symmetry_decrypt2, Myoi_symmetry_decrypt2);
DetourAttach((PVOID*)&Realoi_symmetry_encrypt2, Myoi_symmetry_encrypt2);
//4.提交事务
DetourTransactionCommit();
}
总结
Detour Hook框架相关内容到这期就完结了,但是学习永不止步。
接下来的时间博主可能就没那么空闲了,需要苦逼的开始写毕业论文啦。下一期主题为:跟着麋鹿申请软著
希望能够从最真实的软件著作权申请流程给到各位正准备和想要申请软著的朋友们一些帮助,因为是从编写软件开始逐步每个过程进行解析,那么时间跨度将会和实际申请时长一致,敬请耐心等待啦!
当然,编译好的dll和视频教程以及工具 获取戳我