Cubic Zone

超星尔雅网课平台反作弊机制的探究 (2) - 逆向

清空浏览器缓存, 重新开始抓包, 得到 Flash 播放器的加载链接.

GET https://mooc1-1.chaoxing.com/ananas/modules/video/cxplayer/player_4.0.11.swf?v=20161025 HTTP/1.1

下载, 并使用 JPEXS Free Flash Decompiler 反编译, 查找到关键函数 onSendLog(), 里面有 enc 的生成方式:

_loc6_ = this.getPlaySecond();
_loc3_ = _loc3_ + "&view=pc&playingTime=" + _loc6_;
_loc3_ = _loc3_ + "&isdrag=" + param2;
_loc7_ = MD5.startMd("[" + param1.clazzId + "]" + "[" + param1.userid + "]" + "[" + param1.jobid + "]" 
+ "[" + param1.objectId + "]" + "[" + _loc6_ * 1000 + "]" + "[d_yHJ!$pdA~5]" 
+ "[" + int(param1.duration) * 1000 + "]" + "[" + param1.clipTime + "]");
_loc3_ = _loc3_ + "&enc=" + _loc7_;

其中 param1 的各个属性都很容易从名字推断得到, 这样我们就可以伪造请求, 向服务器提交我们的学习进度. 将其重写为一个 python 函数:

def get_enc(clazzId: str, userid: str, jobid: str, objectId: str, playingTime: int, duration: int,
            clipTime: str) -> str:
    """
    :rtype: str
    :param clazzId: 
    :param userid: 
    :param jobid: 
    :param objectId: 
    :param playingTime: 
    :param duration: 
    :param clipTime: 
    :return: enc string
    """
    import hashlib
    _string = '[{cid}][{uid}][{jid}][{oid}][{pt}][d_yHJ!$pdA~5][{d}][{ct}]' \
        .format(cid=clazzId, uid=userid, jid=jobid, oid=objectId, pt=playingTime * 1000,
                d=duration * 1000, ct=clipTime)
    md5 = hashlib.md5()
    md5.update(_string.encode('utf-8'))
    return md5.hexdigest()

可以利用这个函数生成 enc 字符串向服务器提交数据.

同时, 反编译出的代码中还看到了播放器的对外接口:

ExternalInterface.addCallback("goPlay",this.goPlay);
ExternalInterface.addCallback("goPlayForUrls",this.goPlayForUrls);
ExternalInterface.addCallback("dragMovie",this.dragMovie);
ExternalInterface.addCallback("pauseMovie",this.pauseMovie);
ExternalInterface.addCallback("playMovie",this.play);
ExternalInterface.addCallback("fastFor",this.fastFor);
ExternalInterface.addCallback("addVolNum",this.addVolNum);
ExternalInterface.addCallback("turnOff",this.turnOff);
ExternalInterface.addCallback("reloadPlayList",this.reloadPlayList);
ExternalInterface.addCallback("getPlayList",this.getPlayList);
ExternalInterface.addCallback("getPlaySecond",this.getPlaySecond);
ExternalInterface.addCallback("getPlaySize",this.getPlaySize);
ExternalInterface.addCallback("getTotalSecond",this.getTotalSecond);
ExternalInterface.addCallback("setSubtitle",this.setSubtitle);
ExternalInterface.addCallback("sendProgress",this.sendProgress);
ExternalInterface.addCallback("refreshSkin",this.refreshSkin);
ExternalInterface.addCallback("getPlayState",this.getPlayState);
ExternalInterface.addCallback("danmuState",this.danmuState);
ExternalInterface.addCallback("sendDanmu",this.sendDanmu);

这给了我们一种绕过自动暂停的可能性: 采用用户脚本不断调用播放器的 playMovie() 接口函数, 播放器在鼠标移出播放暂停后将马上恢复播放, 同时在鼠标再次移入播放器窗口时不可能再次触发鼠标移出事件. 使用 Chrome 的控制台功能查看页面元素, 发现:

photo_2017-09-15_01-40-13

我们可以将播放器对象从嵌套的 iframe 中提取出来, 并尝试调用其对外接口. 开始视频播放, 并且将鼠标移出播放窗口使视频自动停止播放. 然后在控制台内输入:

var _frame1 = document.querySelector("iframe");
var _frame2 = _frame1.contentWindow.document.querySelector("iframe");
var _player = _frame2.contentWindow.document.querySelector("object");
_player.playMovie();

视频重新开始了播放. 更进一步, 我们可以继续输入:

setInterval(function() {
    _player.playMovie();
}, 1000);

来每秒钟调用一次播放器的开始播放函数, 维持视频的持续播放.

对页面的分析到这里就基本上结束了. 我们也有了具体的实现思路: 就单纯保持视频不暂停而言, 可以编写用户脚本, 不断调用播放器的 playMovie() 函数保持视频的播放. 更进一步, 采用 Selenium + WebDriver + Packet Capture, 可以抓取到任务点习题的答案并自动点击提交, 结合题库自动解答章节测验. 或者直接抛弃原网页, 想办法获取到各个请求参数并模拟网页与服务器的交互, 上传学习进度并解答章节测验.

本篇的内容就到这里, 具体实现请期待后续文章(也可能没有).