超星 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 大概有如下部分:
?otherInfo=nodeId_101234501
&rt=0.9
&userid=12345678
&clazzId=12345678
&clipTime=0_1799
&jobid=1504425996893136
&duration=1799
&objectId=a9f47a42b8e7f59f1234567c5b7ced33
&playingTime=1124
&enc=c9f8584360936c7b6752e19154f44ec7
服务端的返回为一个 json 数据:
isPassed: false
一些机智的小伙伴很快就会发现,那个 playingTime
对应的就是自己的播放时间,如果播放时间到了视频的最后时间的话,这个视频就通过了,于是开始改 URL 地址,但是发现无论怎么改返回的都是 false,这里直接猜测 enc 的作用就是服务端校验值,但是找遍了 JS 脚本都没有发现计算 enc 的部分,遂认为计算部分实现在他们自己的 Flash 播放器中。
通过对 Flash 播放器的逆向工程(其实就是随意在 BaiDu 上面找了一个 Flash 解包工具)可以发现在 4500 行(好像是这个)发现如下代码:
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 代码实现
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 代码实现
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.nova.moe