用了很久 panda 这个软件了,感觉还是非常不错的,速度、延迟都还是非常好的。但三天的免费使用时长确实有点短,于是有了付费使用的想法。但是一看价格,每月 9.9 美元直接把我劝退。但作为重度白嫖用户,我网上搜了一下发现并没有破解版的 panda,于是,我决定用自己贼菜的技术尝试破解一下。
# 尝试 1:采用脚本批量注册试用账号
首先,先尝试进行抓包。直接抓包并没有显示证书问题,说明并该软件并没有使用 sslpinning 反抓包技术,这就简单多了,直接软件数据,使用应用变量重新生成一个设备码,然后开启抓包,打开 app,得到抓包结果。
分析抓包得到的数据,发现注册逻辑如下:
- 首先向接口 post 请求设备信息,查询该设备是否注册过账号。
- 如果注册过账号,直接获得该设备绑定的 userNumber 和 authorization,并标记为登录状态。如果未注册过,则进入注册流程。
- 注册流程:向接口请求获取 userNumber,获取到后再次向接口进行一次携带 userNamber 值的 account-auto-generation 请求,且本次请求会自动获取并记录 authorization。
分析后发现,默认账户密码为系统生成的设备 ID(非 IMEI 码)。于是尝试用 PHP 写了一个注册脚本,如下:
<?php | |
// 请求模拟设置区域 | |
$id = randomkeys(33); | |
// 获取 userNumber | |
$time = time(); | |
$headers = \[ | |
"device-identifier: ".$id, | |
"api-version: v1.0", | |
"accept-language: zh-CN", | |
"accept: application/json", | |
"user-Agent: okhttp/3.8.0 android/10(OnePlus) panda/5.3.0(71)", | |
"device-type: ANDROID", | |
"product-identifier: panda", | |
"x-timestamp:".$time, | |
"content-type: application/json; charset=UTF-8" | |
\]; | |
$userNumberData = '{"clientVersion":"5.3.0","deviceToken":"'.$id.'","deviceType":"ANDROID"}'; | |
$userNumberResponse = post('https://api.panhvhg.xyz/api/register/user-number', $userNumberData, $headers); | |
$userBumberJson = json\_decode($userNumberResponse,true); | |
$userNumber = $userBumberJson\['data'\]; | |
// 获取 authorization | |
$time = time(); | |
$headers = \[ | |
"device-identifier: ".$id, | |
"api-version: v1.0", | |
"accept-language: zh-CN", | |
"accept: application/json", | |
"user-Agent: okhttp/3.8.0 android/10(OnePlus) panda/5.3.0(71)", | |
"device-type: ANDROID", | |
"product-identifier: panda", | |
"x-timestamp:".$time, | |
"content-type: application/json; charset=UTF-8" | |
\]; | |
$authData = '{"clientVersion":"5.3.0","deviceName":"Android 10 SDK 29 OnePlus ONE A2001","deviceToken":"'.$id.'","deviceType":"ANDROID","number":'.$userNumber.',"password":""}'; | |
$authResponse = post('https://api.panhvhg.xyz/api/register/trier-account-auto-generation', $authData, $headers); | |
$authJson = json\_decode($authResponse,true); | |
$auth = $authJson\['data'\]\['accessToken'\]; | |
//var\_dump($authResponse); | |
/\* | |
// 访问 order | |
$time = time(); | |
$headers = \[ | |
"device-identifier: ".$id, | |
"api-version: v1.0", | |
"accept-language: zh-CN", | |
"accept: application/json", | |
"user-Agent: okhttp/3.8.0 android/10(OnePlus) panda/5.3.0(71)", | |
"device-type: ANDROID", | |
"product-identifier: panda", | |
"x-timestamp:".$time, | |
"content-type: application/json; charset=UTF-8", | |
"authorization:Bearer ".$auth | |
\]; | |
//$order = get('https://api.panhvhg.xyz/api/orders/active',$headers); | |
// 设置密码 | |
$time = time(); | |
$headers = \[ | |
"device-identifier: ".$id, | |
"api-version: v1.0", | |
"accept-language: zh-CN", | |
"accept: application/json", | |
"user-Agent: okhttp/3.8.0 android/10(OnePlus) panda/5.3.0(71)", | |
"device-type: ANDROID", | |
"product-identifier: panda", | |
"x-timestamp:".$time, | |
"content-type: application/json; charset=UTF-8", | |
"authorization:Bearer ".$auth | |
\]; | |
$password=randomkeys(8); | |
$passwordData = '{"newPassword":"'.$password.'","oldPassword":"'.$id.'"}'; | |
$passwordResponse = postHeard('https://api.panhvhg.xyz/api/users/change-password', $passwordData, $headers); | |
$result = \['code'=>1,'msg'=>'获取成功!','userNumber'=>$userNumber,'password'=>$password\]; | |
$result = json\_encode($result); | |
\*/ | |
$time = time(); | |
$headers = \[ | |
"device-identifier: ".$id, | |
"api-version: v3.0", | |
"accept-language: zh-CN", | |
"accept: application/json", | |
"user-Agent: okhttp/3.8.0 android/10(OnePlus) panda/5.3.0(71)", | |
"device-type: ANDROID", | |
"product-identifier: panda", | |
"x-timestamp:".$time, | |
"content-type: application/json; charset=UTF-8", | |
"authorization:Bearer ".$auth, | |
"request\_raw\_response\_body\_tag\_header: 8" | |
\]; | |
$fina = post('https://api.panhvhg.xyz/api/v3/channels/738/connect','',$headers); | |
echo $fina; | |
// 设备 ID 生成 | |
function randomkeys($length) | |
{ | |
$key = ''; | |
$pattern = '1234567890ABCDEFGHIJKLOMNOPQRSTUVWXYZ'; | |
for($i=0;$i<$length;$i++) | |
{ | |
$key .= $pattern{mt\_rand(0,35)}; // 生成 php 随机数 | |
} | |
return $key; | |
} | |
//post-curl 函数封装 | |
function post($url, $data, $headers) { | |
// 初使化 init 方法 | |
$ch = curl\_init(); | |
// 指定 URL | |
curl\_setopt($ch, CURLOPT\_URL, $url); | |
// 设定请求后返回结果 | |
curl\_setopt($ch, CURLOPT\_RETURNTRANSFER, 1); | |
// 声明使用 POST 方式来进行发送 | |
curl\_setopt($ch, CURLOPT\_POST, 1); | |
// 发送什么数据呢 | |
curl\_setopt($ch, CURLOPT\_POSTFIELDS, $data); | |
// 忽略证书 | |
curl\_setopt($ch, CURLOPT\_SSL\_VERIFYPEER, false); | |
curl\_setopt($ch, CURLOPT\_SSL\_VERIFYHOST, false); | |
curl\_setopt($ch, CURLOPT\_HTTPHEADER,$headers); | |
// 设置超时时间 | |
curl\_setopt($ch, CURLOPT\_TIMEOUT, 10); | |
// 发送请求 | |
$output = curl\_exec($ch); | |
// 关闭 curl | |
curl\_close($ch); | |
// 返回数据 | |
return $output; | |
} | |
//post-curl 获取响应头 | |
function postHeard($url, $data, $headers) { | |
// 初使化 init 方法 | |
$ch = curl\_init(); | |
// 指定 URL | |
curl\_setopt($ch, CURLOPT\_URL, $url); | |
// 设定请求后返回结果 | |
curl\_setopt($ch, CURLOPT\_RETURNTRANSFER, 1); | |
// 返回 response\_header, 该选项非常重要,如果不为 true, 只会获得响应的正文 | |
curl\_setopt($ch, CURLOPT\_HEADER, true); | |
// 是否不需要响应的正文,为了节省带宽及时间,在只需要响应头的情况下可以不要正文 | |
curl\_setopt($ch, CURLOPT\_NOBODY, false); | |
// 声明使用 POST 方式来进行发送 | |
curl\_setopt($ch, CURLOPT\_POST, 1); | |
// 发送什么数据呢 | |
curl\_setopt($ch, CURLOPT\_POSTFIELDS, $data); | |
// 忽略证书 | |
curl\_setopt($ch, CURLOPT\_SSL\_VERIFYPEER, false); | |
curl\_setopt($ch, CURLOPT\_SSL\_VERIFYHOST, false); | |
curl\_setopt($ch, CURLOPT\_HTTPHEADER,$headers); | |
// 设置超时时间 | |
curl\_setopt($ch, CURLOPT\_TIMEOUT, 10); | |
// 发送请求 | |
$output = curl\_exec($ch); | |
// 获得响应结果里的:头大小 | |
$headerSize = curl\_getinfo($ch, CURLINFO\_HEADER\_SIZE); | |
// 关闭 curl | |
curl\_close($ch); | |
// 返回数据 | |
return $output; | |
} | |
function get($url, $headers) { | |
// 初使化 init 方法 | |
$ch = curl\_init(); | |
// 指定 URL | |
curl\_setopt($ch, CURLOPT\_URL, $url); | |
// 设定请求后返回结果 | |
curl\_setopt($ch, CURLOPT\_RETURNTRANSFER, 1); | |
// 声明使用 POST 方式来进行发送 | |
curl\_setopt($ch, CURLOPT\_POST, false); | |
// 忽略证书 | |
curl\_setopt($ch, CURLOPT\_SSL\_VERIFYPEER, false); | |
curl\_setopt($ch, CURLOPT\_SSL\_VERIFYHOST, false); | |
curl\_setopt($ch, CURLOPT\_HTTPHEADER,$headers); | |
// 设置超时时间 | |
curl\_setopt($ch, CURLOPT\_TIMEOUT, 10); | |
// 发送请求 | |
$output = curl\_exec($ch); | |
// 关闭 curl | |
curl\_close($ch); | |
// 返回数据 | |
return $output; | |
} | |
?> |
本以为到此就大功告成,但发现注册的账号登录时提示 “试用账号不允许其他设备登录”,于是宣告这个思路失败。
# 尝试 2:直接获取节点信息
此类 npv 软件无非就是使用常用的类 sockets5 代理协议来实现的富强。于是决定换一种思路,直接提取线路信息。
首先,同样先用抓包来进行尝试。打开抓包软件,打开 APP,点击连接按钮,返回后查看抓包信息,如下:
首先,根据返回的 json 数据中的 “meta” 中 “protocol” 的值发现使用的是 “shadowsocks” 协议。在其他键值对中并没有发现 ss 链接或节点信息,于是推测,节点信息保存在被加密的 “data” 值中。根据被加密字符串的特点来看,推测是使用了 aes 加密,但由于密钥我们无从得知,所以只能通过反编译来查看加密方式和密钥。
打开 MT 管理器,点击该安装包发现该 APP 并未加固。那我们直接点击查看即可。使用 dex 编辑器打开 dex 文件,我们直接尝试搜索 “parser”,搜索类型选择 “类”,惊喜的发现,得到了一个结果。
打开该类,直接便发现了显眼的 “AES/ECB/PKCS5Padding” 字样,以及 “key” 字样与相邻的 “panda&beta#12345” 字样,这这这.... 就这么简单就找到加密方式与密钥了?于是打开一个 aes 的解密工具,尝试破解一下 data 数据。
直接得到了节点信息。于是尝试进行 ss 的连接。打开 v2rayNG,选择手动输入 [shadowsocks]。连接,然后发现无网。仔细观察解密后的 data 数据,发现有一个键为 “timeout”,并且连续多次请求返回的端口值都不相同,不同账户的 password 值都相同。于是可以推测该软件的连接思路如下:连接时通过 post 请求节点的 connect 信息,同时 ss 服务端开放相应端口 “timeout” 值的时间,如果没有在规定时间内发起连接,端口就会拒绝本次连接。由于端口值一直在变且有连接时间限制,所以我们无法直接获取有效的节点信息。
本次尝试再次以失败告终。
# 最后尝试:修改 API + 反代 connect 接口
推测出该软件的连接原理后,又有了一种新的破解思路。我们可以直接修改该软件的 API 接口,然后在服务器上搭建一个返回虚假数据的 API 来达到破解的目的,节点信息的话可以直接反代真实 API 的 connect 接口来实现。
直接使用 thinkphp 框架快速搭建一个接口,然后完善路由和返回数据,由于没啥技术含量,在此就不再赘述。
修改 API 我们还是使用 mt 管理器,编辑 dex 文件,搜索关键字 “API”,然后排除 sdk 中的 API 类信息,找到了 API 所在位置:com.pandavpn.androidproxy.ApiConst
根据抓包抓到的接口,我们修改接口为搭建的虚假接口,然后保存,签名,安装即可。
然后打开,点击连接,成功!