Nova Kwok's Awesome Blog

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

#Chinese #Python #PHP