超星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.fun


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