超星 MOOC 视频课程跳过 (刷课) 原理及 Python,PHP 实现

TO 读者:本文仅从技术层面分享超星 MOOC 心跳包的实现原理,我不鼓励也不支持通过这个脚本或者我搭建的网站 (chaoxing.fun) 来进行逃课或刷课等行为, 且我理解这次分享出来势必加速官方修复此问题,所以当你看到本文时可能这个方法已经不再起作用了.
TO 超星:无意冒犯,如有侵权,请 联系 我删除本文.

这个是一个比较早之前就实现的一个项目,不过由于担心收到超星方面的律师函,就一直没有在互联网上公开过,最近在回忆一次技术分享的时候又一次想到了这个,Google 查了一下发现还是有一些网友发布了相关教程. 善意的推测这样做对超星官方没有损失,遂提取一部分来分享一下. 由于相隔时间较长且做完 Demo 之后就再也没去用过,所以这里基本只讲我能回忆上来的重点部分.

单纯从 MOOC 视频页面源代码来看,我无法判断出超星对于视频是否播放完成的服务端反馈,但是通过数据包判断有心跳包的存在.

首先研究心跳包的组成,是一个 GET 请求,网址类似:https://mooc1-1.chaoxing.com/multimedia/log/17f2ce4e123456db326a1234564be8b6 ?otherInfo=nodeId_101234501&rt=0.9&userid=12345678&dtype=Video&clazzId=12345678&clipTime=0_1799&jobid=1504425996893136&duration=1799&objectId=a9f47a42b8e7f59f1234567c5b7ced33&view=pc&playingTime=1124&isdrag=3&enc=c9f8584360936c7b6752e19154f44ec7
拆分一下传入 param 大概有如下部分:

1
2
3
4
5
6
7
8
9
10
?otherInfo=nodeId_101234501
&rt=0.9
&userid=12345678
&clazzId=12345678
&clipTime=0_1799
&jobid=1504425996893136
&duration=1799
&objectId=a9f47a42b8e7f59f1234567c5b7ced33
&playingTime=1124
&enc=c9f8584360936c7b6752e19154f44ec7

服务端的返回为一个 json 数据:

1
isPassed: false

一些机智的小伙伴很快就会发现,那个 playingTime 对应的就是自己的播放时间,如果播放时间到了视频的最后时间的话,这个视频就通过了,于是开始改 URL 地址,但是发现无论怎么改返回的都是 false,这里直接猜测 enc 的作用就是服务端校验值,但是找遍了 JS 脚本都没有发现计算 enc 的部分,遂认为计算部分实现在他们自己的 Flash 播放器中.

通过对 Flash 播放器的逆向工程(其实就是随意在 BaiDu 上面找了一个 Flash 解包工具)可以发现在 4500 行(好像是这个)发现如下代码:

1
2
3
var loc2:*=(loc2 = loc2 +"&view=pc&playingTime="+ arg1 +"-"+ arg2) + "&isdrag=1";
loc5 = com.chaoxing.player.util.MD5.startMd("["+ arg4.clazzId +"]" + "["+ arg4.userid +"]" + "["+ arg4.jobid +"]" + "["+ arg4.objectId +"]" + "["+ arg2 * 1000 +"]" + "[d_yHJ!$pdA~5]" + "["+ int(arg4.duration) * 1000 + "]" + "["+ arg4.clipTime +"]");
loc2 = (loc2 = loc2 +"&enc="+ loc5).substring(1);

这样一来事情就很明了了,其本质就是一个字符串拼接加盐操作,而且盐居然还是硬编码在里面的,值为 d_yHJ!$pdA~5… 很有意思,,所以只需要重新实现一遍 enc 计算算法并给出新的满足 enc 值的 URL 就可以骗过服务端提交伪造的 “视频已播放完” 的心跳包了.

Python3 代码实现

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
import hashlib
import random
import math
import json
import sys

url = sys.argv[1]
temp = url.split('?')
jsonStr = '{"'+ temp[1].replace('=','":"').replace('&','","') +'"}'
js = json.loads(jsonStr)
time = js["duration"]
clazzId = js["clazzId"]
userid = js["userid"]
jobid = js["jobid"]
n = (int(time)-random.randint(1,10)) * 1000 # playing time minus a random value to avoid detection
clipTime = js["clipTime"]
duration = int(js["duration"]) * 1000
objectId = js["objectId"]

salt = "d_yHJ!$pdA~5"

pwdStr = "[%s][%s][%s][%s][%s][%s][%s][%s]" % (clazzId, userid, jobid, objectId, n, salt, duration, clipTime)

hashed = hashlib.md5(pwdStr.encode('utf-8'))
#js["playingTime"] = (int(time)-5)
js["playingTime"] = int(n/1000)
js["enc"] = hashed.hexdigest()
param = "?"
for j in js:
param += "%s=%s&" % (j,js[j])

url_new = temp[0] + param

print(url_new)

PHP 代码实现

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
function cal($url){
#process url
$url = parse_url($url);
parse_str($url['query'], $array);

#update enc
$n = ($array['duration'] - rand(1,10)) * 1000;
$salt = 'd_yHJ!$pdA~5';
$duration = $array['duration'] * 1000;

$pwdStr = sprintf("[%s][%s][%s][%s][%s][%s][%s][%s]",
$array['clazzId'],
$array['userid'],
$array['jobid'],
$array['objectId'],
$n, $salt, $duration,
$array['clipTime']);
$array['enc'] = md5($pwdStr);

#update playing Time
$array['playingTime'] = floor($n/1000);

#make url
$query = http_build_query($array);
$url = sprintf("%s://%s%s?%s",$url['scheme'],$url['host'],$url['path'],$query);

return $url;
}

当然,如果你还嫌麻烦的话,可以试试我搭建的一个服务 chaoxing.fun


我的博客使用了Disqus评论框,如果你看不到评论框,那么多半Disqus服务在你所在的地区被墙,请使用代理访问。