Python逆向QQ登录协议:手机号查QQ号的技术实现与安全实践
1. 项目概述当手机号成为寻找QQ号的唯一线索在数字身份日益重要的今天QQ号不仅仅是一个聊天工具账号它往往关联着我们的游戏资产、社交关系、甚至是一些早期注册的网络服务。很多人会遇到这样的困境记得绑定的手机号却忘记了与之对应的QQ号。官方渠道通常只支持通过QQ号找回手机号反向操作则困难重重。Phone2QQ这个项目正是为了解决这个“反向查找”的痛点而生。它本质上是一个基于Python的技术实现核心思路是通过逆向工程分析QQ客户端的登录协议模拟登录过程并利用“手机号短信验证码”的登录方式从服务器返回的信息中提取出对应的QQ号。这听起来有点像“黑客技术”但其技术内核是协议分析与模拟请求属于安全研究中的“灰盒测试”范畴。对于开发者、安全研究员或是对网络协议充满好奇的技术爱好者来说这是一个绝佳的实践项目能让你深入理解一个国民级应用背后的认证逻辑和数据交互流程。2. 核心思路与技术选型解析2.1 为什么选择逆向QQ登录协议要实现手机号查QQ号摆在面前的有几条路一是利用官方未公开的API但这存在法律风险且不稳定二是通过网页爬虫但QQ的Web端防护严密验证码和动态参数处理起来非常复杂三是直接分析客户端协议。QQ客户端PC或手机经过多年迭代其与服务器通信的协议相对稳定和完整包含了完整的登录、认证、数据拉取流程。逆向客户端协议相当于直接“学习”正版客户端的沟通方式模拟出来的请求合法度最高成功率也最有保障。这个选择背后有几个关键考量点首先是协议的完整性客户端协议必然包含从开始握手到登录成功的所有必要步骤其次是加密方式的相对固定虽然QQ也使用了多种加密和混淆手段但在一个版本周期内是稳定的最后是可行性社区已有大量关于QQ协议如早期09年、13年等版本协议的逆向成果和开源代码片段可供参考降低了从零开始的门槛。2.2 技术栈与工具选型Phone2QQ项目通常基于Python实现这是因为它拥有极其丰富的网络请求和数据处理库非常适合做协议模拟和分析。网络请求库aiohttp/httpx由于登录流程涉及多次请求-响应交互且可能需要处理重定向、Cookie管理等选择一个功能强大的异步HTTP客户端库至关重要。aiohttp或httpx支持异步比传统的requests库在并发和异步控制上更有优势能更好地模拟客户端的连续操作。加密算法库hashlib,Crypto,qrcodeQQ登录协议中使用了多种加密算法包括MD5、SHA系列哈希算法以及RSA、TEA等加密算法。Python标准库的hashlib用于处理哈希Crypto或cryptography库用于处理非对称加密。此外扫码登录可能涉及QRCode生成qrcode库也会被用到。数据解析与构造struct,json,xml.etree.ElementTree服务器返回的数据可能是二进制包、JSON或XML格式。struct模块用于打包和解包二进制数据处理协议中的二进制包头json和xml库用于解析结构化的响应体。逆向分析工具非运行时依赖但为开发必备抓包工具Fiddler、Charles或Wireshark。用于捕获QQ客户端与服务器之间的真实网络流量这是分析协议的起点。需要配置代理并让QQ客户端走代理同时要处理可能遇到的SSL证书校验问题安装抓包工具的根证书到系统信任区。反编译工具对于Android客户端可以使用JADX或Ghidra进行反编译查看Java/Smali代码寻找加密函数和参数构造逻辑。调试工具IDA Pro或Ghidra用于分析PC客户端的二进制文件Frida用于动态Hook移动端App实时查看函数输入输出。注意所有逆向工程行为应仅用于学习、研究或安全测试目的且必须在法律允许的范围内针对自己拥有合法使用权的软件版本进行操作。任何未经授权对他人系统或数据进行的操作都是非法的。3. QQ登录协议核心流程逆向与关键点3.1 协议抓取与登录流程概览首先我们需要用抓包工具捕获一次完整的QQ“手机号短信验证码”登录流程。你会发现整个过程并非一次请求那么简单而是一个包含多个步骤的“握手”协议。一个简化的核心流程如下获取登录环境参数PtuiStarter客户端首先访问一个初始化接口获取本次登录会话的uin临时标识、pt_verifysession、pt_vcode_v1等关键参数以及用于后续加密的rsa公钥。发送短信验证码CheckAndSendSms客户端将手机号、经过RSA公钥加密的密码这里密码是动态的或为空因为走短信验证、以及一系列防重放攻击的salt和signature发送到服务器请求下发短信验证码。提交验证码登录Login用户输入短信验证码后客户端将验证码、手机号、以及之前步骤获取的会话参数一起提交进行最终登录验证。登录成功与票据获取服务器验证通过后会返回一个重要的skey或p_skey以及uin即QQ号。这个skey是后续访问QQ空间、邮箱等服务的通行凭证而uin就是我们最终要获取的目标。3.2 关键参数逆向与构造逆向工程中最繁琐也最核心的部分就是搞清楚每一个请求中那些长长的、看似随机的参数是如何生成的。这里列举几个典型的“拦路虎”pt_verifysession与pt_vcode_v1这两个参数通常在第一步的响应中以JavaScript变量形式返回或者隐藏在Cookie中。它们用于标识本次登录会话防止CSRF攻击。在模拟请求时必须从上一个响应中正确提取并原封不动地带入下一个请求的Cookie或Query Parameters中。RSA加密与verifycode密码或短信验证码流程中的令牌并非明文传输。客户端会从服务器获取一个RSA公钥模数n和指数e然后用这个公钥对特定格式的字符串包含密码、时间戳、uin等进行加密生成verifycode。在Python中我们需要用Crypto.PublicKey.RSA.construct来根据n和e重建公钥对象再进行加密。import binascii from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 # 假设从服务器获取了 modulus (n) 和 exponent (e) n int(E8E...很长的16进制字符串, 16) e 65537 # 通常为65537 # 构造RSA公钥 pub_key RSA.construct((n, e)) cipher PKCS1_v1_5.new(pub_key) # 待加密数据需要按QQ特定格式拼接 plaintext f{password}\\t{timestamp}\\t{uin}....encode() encrypted cipher.encrypt(plaintext) verifycode binascii.b2a_hex(encrypted).decode().upper()signature与salt防重放签名为了防止请求被简单重放QQ协议引入了基于时间、随机数和固定算法的签名。这个签名算法可能藏在客户端的代码里。通过反编译Android APK你可能会找到名为getSign或calculateSignature的Java方法其内部可能使用了MD5、SHA-256并拼接了设备标识、时间戳、固定字符串等。用FridaHook这个方法可以动态获取输入输出从而逆向出签名算法。在Python中我们就需要完全复现这个算法。import hashlib import time def qq_signature(phone, t, salt): # 这是一个示例真实算法复杂得多 raw_str f{phone}{t}{salt}some_magic_string return hashlib.md5(raw_str.encode()).hexdigest()bkn/g_tk请求令牌登录成功后获取的skey会被用于计算一个名为bkn或g_tk的令牌这个令牌在访问很多QQ内部API时是必需的。其算法是固定的遍历skey每个字符的ASCII码进行一个特定的累乘和累加操作。def get_bkn(skey: str) - int: hash_val 5381 for c in skey: hash_val (hash_val 5) ord(c) return hash_val 0x7fffffff3.3 模拟请求的细节与技巧请求头Headers的完全模拟QQ服务器会校验User-Agent、Referer、Content-Type等头部。User-Agent必须模拟成真实的QQ客户端如QQ/8.9.3 CFNetwork/1406.0.2 Darwin/22.4.0。Referer字段也经常被校验需要设置为上一个请求的URL或登录页面的URL。Cookie的持久化管理整个登录流程是状态化的依赖Cookie维持会话。需要使用像aiohttp.ClientSession这样的会话对象它会自动管理Cookie。在关键步骤可能需要手动从响应中提取特定的Cookie值如pt_verifysession并设置到会话中。处理重定向与动态参数有些登录接口在响应中不会直接返回JSON而是返回一段包含重定向URL和参数的HTML或JavaScript。你需要用正则表达式或HTML解析器如lxml从中提取出下一个请求的URL和必要的参数。4. Phone2QQ 核心功能实现步骤基于以上分析我们可以勾勒出Phone2QQ工具的核心实现步骤。请注意以下代码为概念性示例具体参数和URL已做脱敏处理。4.1 步骤一初始化会话与获取密钥import aiohttp import re async def get_login_params(session: aiohttp.ClientSession): 获取初始登录参数如pt_verifysession, rsa公钥等。 init_url https://xui.ptlogin2.qq.com/cgi-bin/xlogin async with session.get(init_url) as resp: text await resp.text() # 从返回的JS或HTML中提取参数例如 # var pt_verifysession_v1 xxx; pt_verifysession_match re.search(rpt_verifysession_v1\s*\s*([^]), text) pt_verifysession pt_verifysession_match.group(1) if pt_verifysession_match else # 提取RSA公钥通常在一个js文件或接口中 rsa_url https://.../get_rsa_key async with session.get(rsa_url) as rsa_resp: rsa_data await rsa_resp.json() modulus rsa_data[modulus] # 模数n exponent rsa_data[exponent] # 指数e return pt_verifysession, modulus, exponent4.2 步骤二请求发送短信验证码async def send_sms_code(session: aiohttp.ClientSession, phone_num: str, pt_verifysession: str, pub_key): 向指定手机号发送短信验证码。 sms_url https://.../check_and_send_sms # 1. 构造加密的verifycode这里简化实际需拼接多个参数 import time timestamp int(time.time() * 1000) # 假设密码字段在短信登录时是固定值或为空具体看协议 raw_password f{phone_num}\\t{timestamp} encrypted_password rsa_encrypt(raw_password, pub_key) # 自定义RSA加密函数 # 2. 计算签名 salt generate_salt() # 生成随机salt signature calculate_signature(phone_num, timestamp, salt) # 自定义签名函数 # 3. 构造请求载荷 payload { phone: phone_num, verifycode: encrypted_password, pt_verifysession: pt_verifysession, salt: salt, signature: signature, ts: timestamp, # ... 其他必要参数 } headers { User-Agent: 真实的QQ客户端UA, Referer: https://xui.ptlogin2.qq.com/, Content-Type: application/x-www-form-urlencoded } async with session.post(sms_url, datapayload, headersheaders) as resp: result await resp.json() # 检查result中的code成功则为0同时会返回一个用于下一步的sms_token if result.get(code) 0: return result.get(sms_token) else: raise Exception(f发送短信失败: {result})4.3 步骤三提交验证码并获取QQ号async def login_with_sms(session: aiohttp.ClientSession, phone_num: str, sms_code: str, sms_token: str, pt_verifysession: str): 使用短信验证码登录并获取最终的skey和uin(QQ号)。 login_url https://.../login payload { phone: phone_num, sms_code: sms_code, sms_token: sms_token, pt_verifysession: pt_verifysession, action: 9-28, # 特定动作码需根据协议确定 # ... 其他参数 } async with session.post(login_url, datapayload) as resp: # 登录成功可能返回JSON也可能返回一段包含重定向的HTML text await resp.text() if 登录成功 in text or ptuiCB( in text: # 从响应中提取关键信息例如通过正则匹配 # 成功响应可能包含ptuiCB(0,0,,0,你的QQ号); qq_match re.search(rptuiCB\\(\\d,\\d,,\\d,(\\d)\\), text) if qq_match: qq_number qq_match.group(1) # 同时登录成功的Cookie中会包含skey, p_skey等 cookies session.cookie_jar.filter_cookies(login_url) skey cookies.get(skey).value if cookies.get(skey) else None return qq_number, skey # 如果失败解析错误信息 error_match re.search(rptuiCB\\((\\d),(\\d),([^]*), text) if error_match: error_code error_match.group(1) error_msg error_match.group(3) raise Exception(f登录失败错误码: {error_code}, 信息: {error_msg}) raise Exception(登录响应解析失败)4.4 主函数流程整合import asyncio async def phone2qq(phone_number: str): 主函数输入手机号返回对应的QQ号。 # 用户需要手动输入收到的短信验证码 async with aiohttp.ClientSession() as session: try: print([1/3] 正在初始化登录环境...) pt_verifysession, modulus, exponent await get_login_params(session) pub_key construct_rsa_key(modulus, exponent) print(f[2/3] 正在向 {phone_number} 发送短信验证码...) sms_token await send_sms_code(session, phone_number, pt_verifysession, pub_key) sms_code input(请输入收到的短信验证码: ).strip() print([3/3] 正在验证并登录...) qq_num, skey await login_with_sms(session, phone_number, sms_code, sms_token, pt_verifysession) print(f✅ 查询成功手机号 {phone_number} 对应的QQ号为: {qq_num}) # 注意skey是敏感凭证不应保存或泄露 return qq_num except Exception as e: print(f❌ 查询失败: {e}) return None if __name__ __main__: phone input(请输入要查询的手机号: ) asyncio.run(phone2qq(phone))5. 常见问题、风险与排查技巧在实际操作中你会遇到各种各样的问题。下面是我在复现类似项目时踩过的坑和总结的经验。5.1 常见错误码与原因分析服务器返回的错误码是重要的调试信息。以下是一些可能遇到的错误码及其含义具体需根据实际协议分析错误码/提示可能原因排查方向ptuiCB(10, ...)验证码错误短信验证码输入错误或已过期。1. 确认验证码是否正确且在一分钟内输入。2. 检查sms_token是否与本次请求匹配是否已过期。ptuiCB(4, ...)会话失效pt_verifysession无效或过期。1. 确保pt_verifysession是从最新初始化请求中获取的。2. 检查整个会话的Cookie管理是否连贯请求头Referer是否正确。ptuiCB(6, ...)参数错误请求参数缺失、格式错误或签名无效。1. 逐一核对请求体Payload中的每个参数名和值是否与抓包数据完全一致。2.重点检查签名(signature)算法这是最容易出错的地方。用Frida Hook原客户端对比同一组输入下你的算法输出和客户端输出是否一致。3. 检查时间戳ts的格式可能是毫秒或秒和时区。ptuiCB(1000, ...)系统繁忙/频率过高请求频率超出服务器限制。1. 在代码中增加随机延迟如time.sleep(random.uniform(1, 3))模拟人工操作。2. 更换IP地址或使用代理池。3. 检查是否触发了设备或账号风控。无法获取RSA公钥初始化接口地址或参数已变更。1. 重新抓包确认获取公钥的接口URL和参数。2. 检查网络代理设置确保能正常访问QQ登录域名。5.2 核心避坑指南与实操心得“动态”是最大的敌人QQ的登录协议并非一成不变。客户端更新、服务器端风控策略升级都可能导致你昨天还能跑的代码今天就失效。因此不要试图写一个一劳永逸的脚本。核心价值在于掌握逆向、分析和模拟的方法论。当协议变化时你能快速定位到是哪个参数或哪个接口发生了变化。签名算法是灵魂90%的失败源于签名错误。不要试图凭空猜测算法。最有效的方法是动态调试。在Android模拟器或真机中安装QQ配置Frida脚本直接Hook关键的签名生成函数打印出输入参数和输出结果。然后在Python中复现这个函数。确保对于相同的输入你的输出和客户端的输出字节级一致。完整复现请求流不要只关注最后一个登录请求。必须从最开始的页面请求、JS加载、参数初始化开始一步步模拟并妥善管理每个步骤产生的Cookie和状态参数。漏掉任何一个看似无关的“预热”请求都可能导致后续步骤失败。设备指纹与环境模拟高级风控会检测客户端环境。除了User-Agent可能还包括屏幕分辨率、设备型号通过HTTP头或特定参数传递、甚至是一些客户端特有的计算值。在抓包时要留意请求中是否有device_id、client_type、adr_id等字段并在模拟请求时一并带上。法律与道德边界再次强调这个技术只能用于自己遗忘QQ号时的自助查询或者是在拥有明确授权的安全测试中。批量查询、侵犯他人隐私是明确的违法行为。开源项目在发布时也应明确说明用途和风险避免被滥用。5.3 项目扩展与思考实现基础功能后这个项目还可以从几个方向深化多协议支持除了短信登录还可以研究扫码登录、密码登录的协议让工具更健壮。异步与并发优化使用asyncio和aiohttp实现真正的异步请求处理多个查询任务在合法合规前提下如测试自己的多个小号。协议更新监控编写一个简单的监控脚本定期测试核心接口是否可用当协议变更时能第一时间发出警报。风控策略研究通过分析不同请求频率、不同IP、不同设备信息下的服务器响应可以管中窥豹了解QQ后端的风控策略这本身也是非常有价值的安全研究。这个项目的真正收获远不止于得到一个能查QQ号的工具。它是一次完整的、从抓包分析到代码实现的逆向工程实战。你会深刻理解网络协议是如何设计的加密和签名如何保障安全以及大型应用如何通过动态参数和风控来对抗自动化脚本。这个过程锻炼的解决问题的能力是任何教科书都无法给予的。