agefans视频解析逆向

前言

最近在做爬虫项目,发现一个动漫网站挺好,可以这个视频解析似乎有加密,最近正好有空,看了一下这个解密是怎么做出来的

准备

介绍一下今天的对象 agefans

那么首先开F12看看http有哪些请求。 然后这网站甚至还开了清console和不停debug来阻止我们使用开发者工具。

2021-10-14_084109.jpg
2021-10-14_084120.jpg

但是没有关系,把breakpoint关掉就行了。

2021-10-14_084200.png

简单的分析

先搜索一下视频文件在哪里出现了,

可以发现视频地址是在 _getplay2 这个api中得到的,

进一步分析可以得到_getplayer2是通过_getplay 302 跳转的。

2021-10-12_143227.jpg

比如, https://www.agefans.cc/_getplay?aid=20210249&playindex=2&epindex=1, 会302跳转到 https://www.agefans.cc/_getplay2?kp=xxxxxxxxxxxxxx 最后获得视频地址。

但是,直接访问 https://www.agefans.cc/_getplay?aid=20210249&playindex=2&epindex=1 无法得到302跳转,反而会返回 err:timeout. 所以猜测有一个鉴权。

仔细的分析

搜索 _getplay?, 在 s_playpre.js 中发现了他的踪迹

1
2
3
4
5
6
7
8
9
10
11
12
function __cb_getplay_url(){
//
const _url = window.location.href;
const _rand = Math.random();
var _getplay_url = (_url.replace(/.*\/play\/(\d{8})-(\d+?)-(\d+?)\.html.*/, '/_getplay?aid=$1&playindex=$2&epindex=$3') + '&r=' + _rand);
//
var re_resl = _getplay_url.match(/[&?]+epindex=(\d+)/);
const hf_epi = ('' + FEI2(re_resl[1]));
const t_epindex_ = 'epindex=';
_getplay_url = _getplay_url.replace(t_epindex_ + re_resl[1], t_epindex_ + hf_epi);
return _getplay_url;
}

继续看哪些地方调用了__cb_getplay_url,

找到发送请求的地方,

简单分析一下这个用来处理权限问题的函数应该为getplay_pck() 以及 getplay_pck2().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function __getset_play(_in_id, cb_getplay_url, cb_cnt){
//
const _url = window.location.href;
const _rand = Math.random();
const _getplay_url = cb_getplay_url();
if(dettchk()){
$.get(_getplay_url, function(_in_data, _in_status){
if('err:timeout' == _in_data){
if(cb_cnt > 0){
__getplay_pck();
__getplay_pck2();
return __getset_play(_in_id, cb_getplay_url, cb_cnt-1);
}else {
return false;
}
}
// ignored

同理,搜索getplay_pck() 以及 getplay_pck2(), 发现他们分别出现在 s_runtimelib.jss_dett.js

但是这两个是加密的, 我们得对他们进行一波解密。

特殊的分析

在进行解密之前,我们先大概看一下服务器是如何鉴定正常的请求

如下是一个请求

在经过几次试验后,可以发现:

  • 这个请求可以正常获取到数据一定时间,过了一定时间后就会失效并且返回err:timeout.
  • 除了referer之外, 要获取到正常数据,还需要cookie,可能是是t1,k2,k2,t2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /_getplay?aid=20210249&playindex=2&epindex=1&r=0.7315117942661356 HTTP/1.1
Host: www.agefans.cc
Connection: close
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99"
Accept: */*
X-Requested-With: XMLHttpRequest
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36
sec-ch-ua-platform: "Windows"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://www.agefans.cc/play/20210249?playid=2_1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: ck_volume=0.18; fa_c=1; Hm_lvt_7fdef555dc32f7d31fadd14999021b7b=1634075310,1634227432; Hm_lpvt_7fdef555dc32f7d31fadd14999021b7b=1634227432; t1=1634227492738; k1=372490656; k2=71052386557371; t2=1634227447871; fa_t=1634227447913

同时,失败的请求会返回 set-cookie, 如下

set-cookie设置了t1以及k1,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HTTP/1.1 200 OK
Date: Thu, 14 Oct 2021 16:07:43 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 11
Connection: close
Set-Cookie: t1=1634227713016; Path=/
Set-Cookie: k1=382912592; Path=/
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Access-Control-Allow-Origin: https://web.age-spa.com:8443
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization
Access-Control-Allow-Credentials: true
Cache-Control: no-store
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
CF-Cache-Status: DYNAMIC
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Server: cloudflare
CF-RAY: 69e20ff0ae286040-SEA

err:timeout

所以,综合分析可以得知,首先我们需要获取t1, k2. 然后通过一定的算法,生成t2,k2并保存在cookie中,再次发送请求就可以获得数据了

加密的分析

先来看一下这两文件长什么样

2021-10-14_091306.jpg

简单分析后得知, 加密后的代码大概分为三个部分

  • 数据段,包括对数据进行进一步处理
  • 解密函数
  • 加密代码段

那么要解密也很简单了,只要把加密的地方,例如_0x1705("xx","bbb")通过解密函数解密即可。

当然函数名是是还原不了了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function __getplay_pck2() {
;var encode_version = '加密类型版本'
, fwiba = '__0x9ecf8'
, __0x9ecf8 = ['数据'];
// 处理数据
(function(_0x25c7a5, _0x422073) {
var _0x25cb8a = function(_0x355576) {
while (--_0x355576) {
_0x25c7a5['push'](_0x25c7a5['shift']());
}
};
_0x25cb8a(++_0x422073);
}(__0x9ecf8, 0x1db));
// 解密函数
var _0x1705 = function(_0x501411, _0x17d72d) {}

// 加密后的原始代码
xxx[_0x1705(xxx,xxx)] = xxx
}

知道了解密方法之后我们来看一下用来做验证的两个函数到底干了什么

getplay_pck()

首先是犯下首先之罪的 __getplay_pck(),

猜测一下f大概就是获得cookie的值,然后f2大概就是设置cookie的值。

如下图分析,这段算法首先获取了t1的值,然后把t1除 0x3e8 (1000) 四舍五入取整,最后右移5位

k2可通过一段不明意义的计算获取

t2为当前时间

1
2
3
4
5
6
7
8
9
10
11
12
// get cookie t1 
// t1 = Math.round(Number(f('t1')) / 0x3e8) >> 0x5;
t1 = Math['round'](Number(f('t1')) / 0x3e8) >> 0x5;

// set cookie k2
// k2 = (t1 * (t1 % 0x1000) * 0x3 + 0x1450f) * (t1 % 0x1000) + t1
f2('k2', (t1 * (t1 % 0x1000) * 0x3 + 0x1450f) * (t1 % 0x1000) + t1);

// set cookie t2
// k2 = new Date().getTime()+""
f2('t2', new Date()[_0x1705('0x12', 'bPNz')]());

getplay_pck2()

然后是是犯下然后之罪的 __getplay_pck2()

通过分析得知,首先获取k2的最后一个数字,然后获取一个t2,使这个t2的最后三个数字包含k2的最后一个数字

结束

1
2
3
4
5
6
7
8
9
10
11
12
13
// ksub = k2[-1]
ksub = f('k2')['slice'](-0x1);
// while true
while (!![]) {
// t2 = new Date().getTime()
t2 = new Date()['getTime']();
// if t2.toString().slice(-0x3).indexOf(ksub) >= 0
if (t2['toString']()['slice'](-0x3)[_0x1691('0x12', '9f@X')](ksub) >= 0x0) {
// set cookie t2 = t2
f2('t2', t2);
break;
}
}

"keygen"

那么知道了是如何完成验证的,那么就可以开始写解析了,先来整理一下步骤

其实k1没什么用

  1. 访问_getplay,获得t1
  2. 通过以上方式获得cookie
  3. 加上cookie再次访问获得链接
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
import math
import re
import time

import requests


def getcookie(t1:int):
time_now = int(time.time()*1000)
print("t1 is %d, now is %s, t1-time_now = %d"%(t1,time_now,t1-time_now))
t1_tmp = math.floor(t1 / 1000 + 0.5) >> 0x5
k2 = (t1_tmp * (t1_tmp % 0x1000) * 0x3 + 0x1450f) * (t1_tmp % 0x1000) + t1_tmp
t2 = time_now
k2_s = str(k2)
t2_s = str(t2)
t2_s = t2_s[:-1:]+k2_s[-1]
print("k2=%s" % k2_s)
print("t2=%s" % t2_s)
return {"t2":t2_s,"k2":k2_s}

url = "https://www.agefans.cc/_getplay?aid=20210249&playindex=2&epindex=1"
data = requests.head(url,headers={"referer":"https://www.agefans.cc/",})
print(data)
setcookies = data.headers.get("set-cookie")
print(setcookies)
t1 = int(re.compile(r"t1=[^;]*;").search(setcookies).group()[3:-1:])
print(t1)
cookies = getcookie(t1)
cookies["t1"] = str(t1)
data = requests.get(url,headers={"referer":"https://www.agefans.cc/",},cookies=cookies)
print(data.text)

成功获得直链~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Response [200]>
t1=1634231445902; Path=/, k1=586623324; Path=/
1634231445902
t1 is 1634231445902, now is 1634231395254, t1-time_now = 50648
k2=99036993616128
t2=1634231395258

{
"purl":"/age/player/ckx1/?url=",
"purlf":"https://play.agefans.cc:8443/age/player/dpx/?url=",
"vurl":"https%3a%2f%2fkol%2dfans%2efp%2eps%2enetease%2ecom%2ffile%2f6159a40954eace75cb975131raYEy7we03?from%3Dysjdm.com%7Cysjdm.net",
"playid":"<play>web_mp4</play>",
"vurl_bak":"",
"purl_mp4":"/age/player/ckx1/?url=",
"ex":""
}

碎碎念

整这么麻烦干什么。