对有关pc qq8.7提示版本太低进行分析
本帖最后由 wai1216 于 2019-12-3 09:58 编辑好久不见,上来冒个泡
前几天在windows上登qq,被提示如下文字
因当前版本存在安全风险,将无法继续使用................
无奈,逆向之.
逆向前,先思考了下.
这样突然出现不能登录,从逻辑上来讲应该是服务器返回过来判断的.因此发包的时候会去效验了版本之类的东西.
但是呢,我又不太想通过走网络层面去patch,虽然有可能patch就不是以下地方了。
1. 进行修改
一番搜索后,发现有一款叫ntrqq的小插件,可以搞定这个限制.
下载了最新版,载入后dump了下来,看了看作者是如何实现的.
{
if ( v31 == 1 )
v31 = 0x1636;
__fuckqqloginlimit_pubno = v31;
v34 = sub_68F73290(v31, L"PreloginLogic.dll");
v35 = GetProcAddress(dword_68F3B7C4, "?GetPubNo@Version@@YAKXZ");
if ( v34 )
{
if ( !v35 )
{
v36 = v7(L"AFBase.dll");
v35 = GetProcAddress(v36, "?GetPubNo@Version@@YAKXZ");
if ( !v35 )
{
v37 = v7(L"AppUtil.dll");
v35 = GetProcAddress(v37, "?GetPubNo@Version@@YAKXZ");
if ( !v35 )
{
v38 = v7(L"PreloginLogic.dll");
v35 = GetProcAddress(v38, "?GetPubNo@Version@@YAKXZ");
}
}
}
result = sub_68EF2A30(v35, "KernelUtil.dll", v7, sub_68EE3500, v34);
}
else
{
v39 = v35;
if ( v35
|| (v40 = v7(L"AFBase.dll"), (v39 = GetProcAddress(v40, "?GetPubNo@Version@@YAKXZ")) != 0)
|| (v41 = v7(L"AppUtil.dll"), (v39 = GetProcAddress(v41, "?GetPubNo@Version@@YAKXZ")) != 0)
|| (v42 = v7(L"PreloginLogic.dll"), result = GetProcAddress(v42, "?GetPubNo@Version@@YAKXZ"), (v39 = result) != 0) )
{
LOBYTE(Buffer) = -23;
result = (sub_68EE3500 - v39 - 5);
*(&Buffer + 1) = sub_68EE3500 - v39 - 5;
if ( v39 != -1 )
{
flOldProtect = 0;
VirtualProtect(v39, 5u, 0x40u, &flOldProtect);
WriteProcessMemory(0xFFFFFFFF, v39, &Buffer, 5u, 0);
result = VirtualProtect(v39, 5u, flOldProtect, 0);
}
}
}
return result;
}
其中 v31 = GetPrivateProfileIntW("Login", "FuckQQLoginLimit", 0, "NtrQQ.ini");
作者把有调用GetPubNo这个函数的几个dll进行了patch,以至于返回NtrQQ.ini中设定好的数值,如果为1,则为0x1636
当然还在其它地方进行了修改,这个稍后再谈
现在有了大概方向就好办了,首先看了看这个函数的代码
kernelutil.dll <?GetPubNo@Version@@YAKXZ>
int Version::GetPubNo()
{
return dword_5331CCCC;
}
只是一个全局变量,在下面函数进行的初始化
kernelutil.dll<?Init@Version@@YAHXZ>// hummerengine.dll 中调用
int __thiscall Version::Init(void *ecx0)
{
//
v23 = L"vi.dat";
v22 = ecx0;
Target = 0;
NumberOfBytesRead = 0;
CTXStringW::CTXStringW((CTXStringW *)&v22, &Default);
v1 = Util::Sys::GetProgramBinDir(&v27);
operator+(&v24, v1);
CTXStringW::~CTXStringW((CTXStringW *)&v27);
v23 = 0;
v22 = 0;
v2 = (const WCHAR *)CTXStringW::operator wchar_t const *(&v24);
v3 = CreateFileW(v2, 0x80000000, 1u, 0, 3u, (DWORD)v22, (HANDLE)v23);
if ( v3 != (HANDLE)-1 )
{
FileSizeHigh = 0;
v4 = GetFileSize(v3, &FileSizeHigh);
v5 = v4;
if ( !FileSizeHigh && v4 < 0x5000 )
{
Target = unknown_libname_95(v4);
ReadFile(v3, (LPVOID)Target, v5, &NumberOfBytesRead, 0);
}
CloseHandle(v3);
}
v32 = 15;
v31 = 0;
LOBYTE(v30) = 0;
sub_532970AA(&v30, NumberOfBytesRead);
sub_53270CB5(&Target);
if ( v31 < 0x10 )
goto LABEL_20;
v6 = v30;
if ( v32 < 0x10 )
v6 = &v30;
memcpy(&byte_5331CCB4, v6, 0xCu);
v7 = *((_WORD *)v6 + 6);
v8 = v31 - 14;
v9 = (int)v6 + 14;
v29 = v7;
if ( v31 - 14 < v7 + 2 )
goto LABEL_20;
CTXStringW::CTXStringW(&FileSizeHigh, v9, v7 >> 1);
v10 = (_WORD *)(v29 + v9);
v11 = *v10;
v12 = v8 - v29 - 2;
v13 = (int)(v10 + 1);
v29 = v11;
if ( v12 < v11 )
{
CTXStringW::~CTXStringW((CTXStringW *)&FileSizeHigh);
LABEL_20:
v20 = 0;
goto LABEL_18;
}
CTXStringW::CTXStringW(&v27, v13, v11 >> 1);
v14 = CTXStringW::operator wchar_t const *(&v27);
CTXStringW::operator=(&unk_5331CCC0, v14);
v15 = CTXStringW::operator wchar_t const *(&FileSizeHigh);
CTXStringW::operator=(&unk_5331CCC4, v15);
v16 = v29 + v13;
v17 = v12 - v29;
dword_5331CCC8 = 65793;
dword_5331CCCC = 25600;
if ( (unsigned __int8)byte_5331CCB5 + 100 * (unsigned __int8)byte_5331CCB4 >= 2574 && v17 >= 8 )
{
memcpy(&dword_5331CCC8, (const void *)v16, 4u);
memcpy(&dword_5331CCCC, (const void *)(v16 + 4), 4u);
v16 += 8;
v17 -= 8;
}
if ( v17 >= 2 )
{
v18 = *(_WORD *)v16;
if ( v17 - 2 >= v18 )
{
CTXStringW::CTXStringW(&v29, v16 + 2, v18 >> 1);
v19 = CTXStringW::operator wchar_t const *(&v29);
CTXStringW::operator=(&unk_5331CCD0, v19);
CTXStringW::~CTXStringW((CTXStringW *)&v29);
}
}
CTXStringW::~CTXStringW((CTXStringW *)&v27);
CTXStringW::~CTXStringW((CTXStringW *)&FileSizeHigh);
v20 = 1;
LABEL_18:
std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(&v30, 1, 0);
CTXStringW::~CTXStringW((CTXStringW *)&v24);
return v20;
}
注意到有个call sub_532970AA,该函数首先通过CTXCommPack::GetWord得到dat文件的开头两字节判断是否为0,之后调用GetByte判断是否2了
也就是说dat头部要满足00 00 02,之后通过GetModuleFileNameW得到qq.exe计算md5,再将其md5循环20次得到的hash作为解密vi.dat的密钥(变形tea),伪代码如下
signed int __cdecl sub_53296DB9(char a1, unsigned __int8 *a2)
{
MD5_Init((struct MD5state_st *)&v14);
for ( i = ReadFile(hFile, &Buffer, 0x1000u, &NumberOfBytesRead, 0);
i && NumberOfBytesRead;
i = ReadFile(hFile, &Buffer, 0x1000u, &NumberOfBytesRead, 0) )
{
MD5_Update((struct MD5state_st *)&v14, (const unsigned __int8 *)&Buffer, NumberOfBytesRead);
}
CloseHandle(hFile);
if ( a1 != 3 )
goto LABEL_12;
v7 = GetModuleHandleW(0);
if ( v7 )
{
MD5_Update(
(struct MD5state_st *)&v14,
(const unsigned __int8 *)v7 + *(_DWORD *)((char *)v7 + *((_DWORD *)v7 + 15) + 44),
*(_DWORD *)((char *)v7 + *((_DWORD *)v7 + 15) + 28));
LABEL_12:
MD5_Final(a2, (struct MD5state_st *)&v14);
v8 = 20;
do
{
MD5_Init((struct MD5state_st *)&v13);
MD5_Update((struct MD5state_st *)&v13, a2, 0x10u);
MD5_Final(a2, (struct MD5state_st *)&v13);
--v8;
}
while ( v8 );
v3 = 1;
}
}
之后通过GetBuf和GetWord获取到加密数据的大小,而此时文件的偏移刚好是加密后数据的起始位置
那么有关vi.dat的结构就可以抽象成如下
struct s_buf{
short flag;
bytetype;
short offset;
byte unknown_like_padding;
short define_tea_encrypt_size;
byte define_tea_encrypt_data;
};
接着通过oi_symmetry_decrypt2进行解密
.text:532971D1 lea ecx,
.text:532971D4 push ecx
.text:532971D5 push eax
.text:532971D6 mov , eax
.text:532971D9 lea eax,
.text:532971DC push eax
.text:532971DD movzx eax,
.text:532971E1 push eax
.text:532971E2 push
.text:532971E5 call ds:?oi_symmetry_decrypt2@@YAHPBEH0PAEPAH@Z ; oi_symmetry_decrypt2(uchar const *,int,uchar const *,uchar *,int *)
.text:532971EB add esp, 18h
以我所使用的版本得到如下数据
0052628037 03 00 00 C9 4A 00 00 08 00 00 00 46 00 24 007...éJ......F.$.
0052629050 00 52 00 4F 00 44 00 4E 00 41 00 4D 00 45 00P.R.O.D.N.A.M.E.
005262A024 00 20 00 50 00 72 00 65 00 76 00 69 00 65 00$. .P.r.e.v.i.e.
005262B077 00 34 00 28 00 38 00 2E 00 37 00 2E 00 31 00w.4.(.8...7...1.
005262C039 00 31 00 34 00 35 00 2E 00 32 00 30 00 31 009.1.4.5...2.0.1.
005262D029 00 00 00 1C 00 38 00 2E 00 37 00 2E 00 31 00).....8...7...1.
005262E039 00 31 00 34 00 35 00 2E 00 32 00 30 00 31 009.1.4.5...2.0.1.
005262F000 00 01 01 01 00 24 68 00 00 70 01 48 00 75 00......$h..p.H.u.
005263006D 00 6D 00 65 00 72 00 45 00 6E 00 67 00 69 00m.m.e.r.E.n.g.i.
005263106E 00 65 00 2E 00 64 00 6C 00 6C 00 3A 00 46 00n.e...d.l.l.:.F.
0052632030 00 41 00 32 00 46 00 45 00 36 00 41 00 42 000.A.2.F.E.6.A.B.
0052633041 00 46 00 32 00 42 00 35 00 43 00 34 00 32 00A.F.2.B.5.C.4.2.
0052634043 00 36 00 34 00 39 00 36 00 43 00 32 00 45 00C.6.4.9.6.C.2.E.
0052635036 00 39 00 35 00 43 00 30 00 42 00 43 00 7C 006.9.5.C.0.B.C.|.
005263604C 00 6F 00 6E 00 67 00 43 00 6E 00 6E 00 2E 00L.o.n.g.C.n.n...
0052637064 00 6C 00 6C 00 3A 00 35 00 33 00 35 00 42 00d.l.l.:.5.3.5.B.
0052638042 00 33 00 39 00 43 00 39 00 45 00 42 00 35 00B.3.9.C.9.E.B.5.
0052639031 00 30 00 42 00 42 00 36 00 46 00 46 00 32 001.0.B.B.6.F.F.2.
005263A035 00 32 00 33 00 44 00 31 00 30 00 30 00 35 005.2.3.D.1.0.0.5.
005263B031 00 39 00 45 00 37 00 7C 00 4B 00 65 00 72 001.9.E.7.|.K.e.r.
005263C06E 00 65 00 6C 00 55 00 74 00 69 00 6C 00 2E 00n.e.l.U.t.i.l...
005263D064 00 6C 00 6C 00 3A 00 39 00 31 00 34 00 33 00d.l.l.:.9.1.4.3.
005263E039 00 43 00 33 00 36 00 33 00 36 00 32 00 36 009.C.3.6.3.6.2.6.
005263F041 00 37 00 32 00 30 00 42 00 46 00 42 00 33 00A.7.2.0.B.F.B.3.
0052640033 00 32 00 45 00 42 00 34 00 45 00 32 00 32 003.2.E.B.4.E.2.2.
0052641031 00 31 00 36 00 37 00 7C 00 54 00 53 00 49 001.1.6.7.|.T.S.I.
0052642050 00 2E 00 44 00 41 00 54 00 3A 00 31 00 39 00P...D.A.T.:.1.9.
0052643037 00 34 00 33 00 38 00 31 00 30 00 43 00 30 007.4.3.8.1.0.C.0.
0052644036 00 43 00 42 00 39 00 31 00 44 00 32 00 37 006.C.B.9.1.D.2.7.
0052645033 00 35 00 45 00 34 00 44 00 33 00 34 00 46 003.5.E.4.D.3.4.F.
0052646044 00 42 00 34 00 38 00 31 00 37 00 00 00 00 00D.B.4.8.1.7.....
给出分析的结构
struct s_data{
typedef struct s_ver{
byte majorver:1;
byte minorver:1;
short padding:2;
}t_ver;
dword buildver;
dword unknown;
bytevername[];
dword clienttype;
dowrd pubno;
short dll_information_size;
bytedll_information;
};
到这里就可以构造出想要的pubno号,然后调用oi_symmetry_encrypt2加密数据替换掉vi.dat.
然后打开qq却发现提示了两个初始化错误0x0d以及0x30,继续进行分析,注意到仅替换了pubno.
int Version::GetPubNo()处下断,发现是在HummerEngine.dll中进行的check,伪代码如下: //ntrqq的作者好像对此函数也进行了path
signed int sub_528CAE70()
{
//
if ( sub_528D2997()
&& ((CTXStringW::CTXStringW(&v10, L"platform"),
operator+(&v9, &v10, L"res:"),
CTXStringW::~CTXStringW(&v10),
v1 = sub_528CAD0A(&v9),
CTXStringW::~CTXStringW(&v9),
CTXStringW::CTXStringW(&v9, L"platform"),
operator+(&v10, &v9, L"xtml:"),
CTXStringW::~CTXStringW(&v9),
v8 = sub_528CAD0A(&v10),
CTXStringW::~CTXStringW(&v10),
CTXStringW::CTXStringW(&v9, L"platform"),
operator+(&v10, &v9, L"data:"),
CTXStringW::~CTXStringW(&v9),
v2 = sub_528CAD0A(&v10),
CTXStringW::~CTXStringW(&v10),
v3 = CTXStringW::CTXStringW(&v9, L"platformtheme:"),
v4 = sub_528CAD0A(v3),
CTXStringW::~CTXStringW(&v9),
v5 = Version::GetPubNo(),
v1 != v5)
|| v8 != v5
|| v2 != v5
|| v4 != v5) )
{
.....
}
}
sub_528D2997() 点进去看了看,大概是为创建另一进程起的部分参数
对赋值函数进行分析,伪代码如下
int __stdcall sub_528CAD0A(int a1)
{
//
qmemcpy(&v12, L"pubversion.dat", 0x1Eu);
v1 = CTXStringW::operator wchar_t const *(a1);
FS::CombineQNC(&v9, v1, &v12);
v11 = 0;
v2 = CTXStringW::operator wchar_t const *(&v9);
FS::CreateFileW(v2, 0, &v11);
...
CTXStringA::CTXStringA(&v6, v7, v8);
v3 = CTXStringA::GetBuffer(&v6);
v4 = StrToIntA(v3);
CTXStringA::~CTXStringA(&v6);
...
}
动态跟了跟,原来读取的安装目录下Resource.8.7.19145的四个.rdb
搜索了下网上对rdb解析的工具,发现解析不了.
无奈之下只有分析下结构对值进行修改.给出逆向后的结构
struct s_rdb{
byte feature;
dword count;
dword res_struct_offset;
dword padding;
dword res_data_offset;
};
struct s_res{
wchar name[];
dword offset;
dowrd unknown;
dword size;
};
用资源下的data.rdb来举例子
0000000035 33 31 45 39 38 32 3034 46 38 35 34 32 46 30531E98204F8542F0
0000001018 09 00 00 24 00 00 0000 00 00 00 CC 3D 02 00....$........=..
0000002000 00 00 00 62 00 74 006E 00 72 00 65 00 70 00....b.t.n.r.e.p.
000000306F 00 72 00 74 00 5C 0067 00 75 00 69 00 64 00o.r.t.\.g.u.i.d.
0000004032 00 74 00 74 00 2E 0064 00 61 00 74 00 00 002.t.t...d.a.t...
0000005000 00 00 00 00 00 00 0085 F1 01 00 00 00 00 00................
0000006070 00 75 00 62 00 76 0065 00 72 00 73 00 69 00p.u.b.v.e.r.s.i.
000000706F 00 6E 00 2E 00 64 0061 00 74 00 00 00 85 F1o.n...d.a.t.....
0000008001 00 00 00 00 00 05 0000 00 00 00 00 00 61 00..............a.
pubversion.data数据的位置应该是 0x23dcc + 0x1f185 + 0x24
00042F6BB9 DD DD F0 C2 82 18 0008 01 32 36 36 36 30 7B..........26660{
为什么需要再加0x24,见如下代码
705BD10D | 76 09 | jbe common.705BD118 |
705BD10F | 8B42 10 | mov eax,dword ptr ds: |
705BD112 | 2B45 0C | sub eax,dword ptr ss: |
705BD115 | 8945 14 | mov dword ptr ss:,eax |
705BD118 | 8B41 28 | mov eax,dword ptr ds: |
705BD11B | 0342 08 | add eax,dword ptr ds: |
705BD11E | 8B71 2C | mov esi,dword ptr ds: |
705BD121 | 1372 0C | adc esi,dword ptr ds: |
705BD124 | 0345 0C | add eax,dword ptr ss: |
705BD127 | 1375 10 | adc esi,dword ptr ss: |
705BD12A | 83C0 24 | add eax,24 |
705BD12D | 13F7 | adc esi,edi |
705BD12F | 8975 10 | mov dword ptr ss:,esi |
705BD132 | 897D FC | mov dword ptr ss:,edi |
也正是因为这个原因,导致解析失败么
得到数值,之后在进行转换.
v4 = StrToIntA(v3);
对四个rdb进行size和data的修改.
至此打开没问题,也可以进行登录了.
2.问题来了
登录后却提示,qq损坏.
发现不点提示框时,不会结束掉qq.
还有收到消息,但是不能打开好友和群的聊天框.
无奈又只有挂上调试器,进行分析.
先对打不开聊天框进行分析
先逆了半天,以为对修改的文件进行了校验或者触发了暗桩,转念一想发现不应该,回想起来有些惯性思维了.
随意找了个能断下来的地方,跟踪发现弹窗是由于版本变化了,加载时对Plugin目录下的插件版本进行check导致.
ChatFrameApp.dll:
signed int __cdecl sub_51529DC6(const wchar_t *a1, _DWORD **a2, int a3)
{
//
v80 = &v77;
sub_51526E09(v3);
CTXStringW::CTXStringW(&v81, L"com.tencent.advertisement", v78);
v77 = (int *)&v81;
v4 = *((_DWORD *)v79 + 1);
v82 = (char *)v79 + 4;
v5 = 1;
v6 = sub_51527264(v79, v4, &v81);
sub_51526E59(1);
*(_DWORD *)v82 = v6;
**(_DWORD **)(v6 + 4) = v6;
CTXStringW::~CTXStringW((CTXStringW *)&v81);
CTXStringW::CTXStringW(&v81, L"com.tencent.audiovideo", v78);
...
}
signed int __cdecl sub_51529CFF(_DWORD **a1, _DWORD **a2, int a3)
{
//
v9 = &v7;
sub_51526E09(v3);
for ( i = *(char **)v8; i != v8; i = *(char **)i )
{
sub_51527FC9(&v11, **a1, *a1, i + 8);
if ( v11 == *a1 )
{
sub_51527FC9(&v10, **a2, *a2, i + 8);
if ( v10 == *a2 )
{
v9 = (int **)CTXStringW::operator wchar_t const *(i + 8);
v7 = (int *)&v9;
sub_51511243(L"file", 120, L"func", 2, L"plc", (const char *)L"%s", &v9);
sub_515280C1(*(_DWORD *)a3, i + 8);
}
}
}
v5 = 0;
if ( !*(_DWORD *)(a3 + 4) )
v5 = 1;
sub_51528BF0(&v8);
operator delete(v8);
return v5;
}
patch了下,能打开聊天框,进行聊天了,由于时间挂太久,还T我下线了.
依法炮制干掉了AppMisc.dll对于插件的加载.
再次打开qq,进行登录.
恩,能进行聊天就行,作为基本功能满足即可.
事后发现由于插件没加载,不能传文件.
打开Com.Tencent.FileTransfer,将目录里Bundle.rdb文件中pubversion.dat有关数据进行修改.
再再次打开qq,进行登录.测试了下功能发现可行.
舒服了.
还好只是用来聊天交流.
3.反思
方式不是太好,理想方式,应该只在发包的时候对pubno和ver相关进行修改,可能hook的地方多一些.
然后在根据ntrqq中,再对一些校验的地方在进行patch.
厉害了,楼主!PYG15周年生日快乐! 感觉新版的qq除了有广告其他的也还行 这么好的技术贴,为啥没人回复呢? 学习了楼主,讲解认真 直接用国际版。 厉害了楼主,讲解认真 太强了。坐沙发学习一 QQ里面暗桩挺多的,破解登录这块我是直接IATHook把PreloginLogic.dll的导入表的GetPubNo给Hook了,我发现小于25600的版本都可以登录。
但是最近QQ服务器似乎检测pubno了,低于25600的版本都是旧版,都被禁止登录了。
我不知道是我的处理手法有问题还是说服务器那边一刀切了,不知道楼主用改vi.dat这个方法最近有没有被封号之类的?因为改vi.dat的话要改的地方挺多的,不过理论上应该比我直接勾函数要完美吧。 Lance.Moe 发表于 2020-5-7 02:36
QQ里面暗桩挺多的,破解登录这块我是直接IATHook把PreloginLogic.dll的导入表的GetPubNo给Hook了,我发现小 ...
服务器对其他地方做了check 有问题就冻结掉了 具体在哪 你可以看看发的包还包括那些地方 // 因为发的包长度肯定是不变的 所以要校验的话 就那么一些数据 哪些有关就显而易见了不过不建议搞成插件发出去 了解就好
页:
[1]
2