前言
声明,本文与该软件仅用于学习技术交流,请勿将软件用于非法途径,本作者不承担一切法律责任,望使用者须知。若影响到贵公司正常运行,请联系本人删除。
使用语言
有人会很好奇这个软件到底是怎么运行的,为啥可以实现自动完成视频 ,作业。借此写个完整的该软件开发过程,供各位学习,整个开发过程真不算的难,听我慢慢道来(尽可能详细),但你看完后,写不出来,大概率也不会想写。
既然是写软件,那怎么能不说说编程语言,首先这个软件是基于易语言开发的,初学易语言的三个月所写的练手项目,本是写来给我自用的,不过确实好用,那为啥不分享出去呢。
实际上我也考虑过开源(在精益论坛),奈何多次审核不通过,乏了,便不开源了。
首先,说说为啥会选择易语言,有一部分原因是因为我那时候正好在学易语言,哪怕现在如果要开发一个类似于这样的软件,我也会优选易语言(在不考虑兼容与报毒情况下)。原因其实非常简单,好写,太好写了,我记得那时候的第一版超星刷课只花了一周时间,实现了自动完成视频,那还只是我没什么开发经验的前提下(借鉴了外面的一份源码)。再者也是最主要的一部分,写的软件是基于什么平台,很显然,桌面级应用 Windows 平台。那就少不了交互界面了,而正是这个交互界面,让我劝退了 javascrpit 与 python。不是说他们不行,而是写起来绝对比易语言复杂。如果你有接触过这两者相关的估计会知道,尤其还是实现自动完成任务的功能,基本上是不提供界面而言。也就让代码的可操作性少了非常多,这还不是最致命的,致命的是使用者不是人人都学程序了,即使发你一个 python 文件,但他大概率是不会听你大费周章的安装,输入,原因就是麻烦(如果这份代码好用的话当我没说过)。
说这些都不如直接来一个 exe 可执行文件,让用户去点击操作,然后通过一个日志输出显示给用户,告知用户当前程序执行进度。可能有人又会问,那为啥不用 C#,VB.net,QT 等,我 tm 要是会的 话,也不会用易语言来写了,易语言敲代码体验很差,如果用过其他的文本编辑器,就特别不想用易语言(反正我是这样,真的难用),毕竟易语言都是 20 年前的产物了,能活到现在就不错了。但不得不说,易语言是真的好写,好用,好上手。
语言不分贵贱,能写出好的程序都是好语言,所以本文都是以本人从易语言开发角度来讲述,如果你恰好有程序开发经验,或有想接触的,本文或许能给予你一些帮助。
找源码
既然介绍完所用语言,那么就开始编写代码吧,不过在此之前先别急,这一步尤为关键,能极大的节省你所开发的效率,那就是搜索是否的相关源码或者软件。最好是与自己所开发的语言一致。
这是我当时编写软件前在吾爱破解论坛上搜索到的相关源码,如下:
看看软件源码的界面
在比对一下我的修改了数十次的。
没错,我就是基于这个软件改的,还是有点相似之处的。但事实上这个原作者的代码在我翻阅到时就已经不能用了,并且还有很多弊端,例如还需要输入学校名称,输入验证码,这对用户体验来说的是非常烦人的。
同时在这份源码上只能说是一份临时品,几乎没有维护可言(虽然易语言写的软件多半都 不好维护),不过有一个核心加密算法,也就是最终提交视频的一个核心算法,让我省去 JS 逆向分析的时间(后文会说到,不过以我那时候来看,这个 JS 自行解决也不成问题)
那时候搜索到的还有其他的相关脚本,例如大多数人都了解过的油猴插件。后文有简单讲述到,因为和本文涉及到的不相关。
执行流程
找到相关源码或软件,就已经离项目完成快了一半了,接着只需要在该软件上进行修改,已达到自己的目的。当然,如果要补充一些功能还需要花费很多时间的。
页面设计
我优先做的就是修改 UI 界面,做到竟可能的不丑,且符合个人风格。而这部分就是拖拽组件,移动组件,微调组件,平行垂直等操作,没啥可言的。我所用的都是 windows 自带的组件,加上我不会自绘组件,只好借助皮肤模块来美化界面了。美化的效果如下图
实际上,页面设计相关就到此结束了,我能做的也只是尽量不丑,毕竟不会自绘组件,用原生自带的组件就这样了。当然,后面关于怎么数据渲染到组件这些会写到的。
登录
接着就是要说实现原理,首先回想一下,我们如果手 动去一个个看视频,答题,需要干嘛,那肯定是登录了,不登录学习通那边怎么知道是你,那么在浏览器中,登录只是输入下账号,密码,然后点击登录按钮就完事了。然而实际原理不只是点击按钮这么简单,实则是发送一个 http 请求给后端,后端进行效验结果比对,返回结果,我简单叙述一下,放 js 代码来看看:
具体看图片
完整关键代码如下:(已删除不必要代码)
//手机号+密码登录
function loginByPhoneAndPwd() {
var phone = $('#phone').val().trim()
var pwd = $('#pwd').val()
var fid = $('#fid').val()
var refer = $('#refer').val()
if (util.isEmpty(phone)) {
util.showMsg(true, 'phoneMsg', '请输入手机号', true)
return
}
if (util.isEmpty(pwd)) {
util.showMsg(true, 'pwdMsg', '请输入密码', true)
return
}
var t = $('#t').val()
if (t == 'true') {
pwd = $.base64.btoa(pwd, 'UTF-8')
}
// --------------------------------------------------------
$.ajax({
url: '/fanyalogin',
type: 'post',
dataType: 'json',
data: {
fid: fid,
uname: phone,
password: pwd,
refer: refer,
t: t,
},
success: function (data) {
if (data.status) {
var url = ''
if (data.tochaoxing) {
var path = window.location.protocol + '//' + window.location.host
url =
path +
'/towriteother?name=' +
encodeURIComponent(data.name) +
'&pwd=' +
encodeURIComponent(data.pwd) +
'&refer=' +
data.url
} else {
url = decodeURIComponent(data.url)
}
if (top.location != self.location && $('#_blank').val() == '1') {
top.location = url
} else {
window.location = url
}
} else {
if (data.weakpwd) {
window.location =
'/v11/updateweakpwd?uid=' +
data.uid +
'&oldpwd=' +
encodeURIComponent($('#pwd').val()) +
'&refer=' +
refer
} else {
var msg = util.isEmpty(data.msg2) ? '登录失败' : data.msg2
msg = '密码错误' == msg || '用户名或密码错误' == msg ? '手机号或密码错误' : msg
util.showMsg(true, 'err-txt', msg)
}
}
},
})
}
代码并不长,一个很简单的 post 登录,这里我会一一进行分析
var phone = $('#phone').val().trim()
var pwd = $('#pwd').val()
var fid = $('#fid').val()
var refer = $('#refer').val()
if (util.isEmpty(phone)) {
util.showMsg(true, 'phoneMsg', '请输入手机号', true)
return
}
if (util.isEmpty(pwd)) {
util.showMsg(true, 'pwdMsg', '请输入密码', true)
return
}
var t = $('#t').val()
if (t == 'true') {
pwd = $.base64.btoa(pwd, 'UTF-8')
}
在分割符的前一部分,获取我们表单中的手机号(phone),密码(pwd),学校 id(fid),以及不重要的 refer,同时判断手机号,密码是否为空,并给出相应提示,同时这里的 pwd 还进行了 base64.btoa,也就是 Base64 编码处理过。这里我就模拟一下这些数据
phone = '15212345678'
pwd = 'a123456'
pwd = 'YTEyMzQ1Ng==' // Base64编码后的结果
fid = '12345' // 也可以不指定学校 填-1
refer = 'http://passport2.chaoxing.com'
然后再看剩余的一部分,主要就关注这些:
$.ajax({
url: '/fanyalogin',
type: 'post',
dataType: 'json',
data: {
fid: fid,
uname: phone,
password: pwd,
refer: refer,
t: t,
},
success: function (data) {
//....
}
}
也正是因为这几行代码,将我们的数据发送给了学习通的服务端,并将数据返回给我们,这里我抓个数据包看看数据是怎么样的
POST /fanyalogin HTTP/1.1
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded; Charset=UTF-8
Accept: */*
Referer: https://passport2.chaoxing.com/login?&newversion=true
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Origin: https://passport2.chaoxing.com
x-requested-with: XMLHttpRequest
Host: passport2.chaoxing.com
Content-Length: 94
fid=12345&uname=15212345678&password=YTEyMzQ1Ng==&refer=http://passport2.chaoxing.com&t=true
认真看上面最后一行,有没有发现这些数据不就是我们刚刚上面模拟的数据。再来看返回数据
HTTP/1.1 200 OK
Server: Tengine
Date: Fri, 25 Sep 2020 11:50:30 GMT
Content-Type: text/html;charset=utf-8
Connection: keep-alive
Set-Cookie: JSESSIONID=625EBEBB8C9A307910975B8A6306EE13; Path=/; HttpOnly
Set-Cookie: lv=3; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/
Set-Cookie: uf=b2d2c93beefa90dcc0dd308bdb4e3ac7c15d612bf3f08318fdb57793f3e0b0e8e06d6354b86e5f8c6733e63a87a57410913b662843f1f4ad6d92e371d7fdf644cb407fe2f4a1b7e3102289339c6dea121471850d8bf7e34cbde8ab62ef4efbfc29d661c57520821b; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/
Set-Cookie: _d=1601034630583; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/
Set-Cookie: vc=D117659BD1295E4489AED8ED14E8A8D8; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/; HttpOnly
Set-Cookie: vc2=5B73047EF8C636D19D282B878FC42D4A; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/; HttpOnly
Set-Cookie: vc3=Lptknj6gO2EVnnAWOW0A1O0d0RGWzgO1jVDtKiGtxlqX7dH5uAz84KoWDf2Y9v%2Biw2V3RyKd2gNXf%2BMVKt2HKmJzYK1vt%2BBHu%2B%2BXwG3NtJAWvXygxxcRYlSwCt%2BDv0r8JkrhqgJxJQV2VkMVMon8PABIuJdJKudVTQR%2FP6u2pfY%3D2dd5ab18abc4e7b47fdeb363a13a7c64; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/; HttpOnly
Set-Cookie: xxtenc=0fd26095d768e519a53edd2f4ba4c9e9; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/
Set-Cookie: DSSTASH_LOG=C_38-UN_9502-US_42736002-T_1601034630584; Domain=.chaoxing.com; Expires=Sun, 25-Oct-2020 11:50:30 GMT; Path=/
Set-Cookie: route=1b37a788fe3a8c39de935217be0d9f7a;Path=/
Content-Length: 56
{"url":"http%3A%2F%2Fi.mooc.chaoxing.com","status":true}
现在只要注意最后一行,这是登录成功的返回结果,那如果密码错误呢,返回的结果如下
{ "msg2": "用户名或密码错误", "status": false }
那么既然简单了明白了一下基本的登录实现原理,能不能模拟一下这样的请求,替换一下其他人的账号密码,然后发送给服务端,当然可以,看看用易语言代码是怎么实现的这样的登录功能。
注意我划线的两个部分,实际上在 POST 请求中,要做的也就是替换一系列的数据,已达到模拟请求,并接受服务端接收的数据。
不过这里我还得小提一句,看到上面的返回响应中,有好几个 Set-Cookie,这里 Cookie 是服务端返回的,并且加密处理了,而正是这个 Cookie,保存了我们的登录凭证,使得下次我们请求的时候会将这些 Cookie 携带上去,这样服务端才能知道哪个请求是谁发送的。这样才能获取到哪个学生的信息,如 课程信息,作业信息等等。
凭什么我发送一个请求给服务端,然后就能登录成功,就可以不用借用浏览器,来实现登录账号?或者说本质上,浏览器数据交互也是通过 HTTP 协议,而 HTTP 协议就是这样,至于实现原理,这里我不涉及太多相关的,你只需要知道可以就行,但有时候并不像上面这么简单(请接着看下文例子)。
从上面的例子中,也许你已经能知道,只要发送一个请求的事情,就可以登录,就能获取对应的信息,实际上,你只要知道下面这一句话:
浏览器本质的操作,就是向服务器发送请求,而软件所做的就是模拟请求,已达到获取数据。
而操作流程,先抓包获取到发送的数据,然后再模拟数据发送给服务端,就可以达到相对应的操作后文都将以这样的操作为例,以至于实现原理,就涉及到相关专业的知识了,同时模拟请求,能绕过浏览器自身的限制(JS 的限制).
获取课程列表
这里我就以获取课程列表数据,并通过 DOM 解析,并将其显示在软件上来演示。
现在已经登录成功了,那么我现在就向服务器发送获取课程信息的请求,在这里我封装成一个易语言的函数,如下几个部分:
url = “http://mooc1-1.chaoxing.com/visit/courses/study?isAjax=true&debug=false”
http.Open (“GET”, url)
http.Send ()
response = http.GetResponseTextU2A ()
此时 response 为 html 文件文本,像这样的
<ul class="clearfix">
<li style="position:relative;" class="courseItem curFile">
<input type="hidden" name="courseId" value="215497310" />
<input type="hidden" name="classId" value="34423626" />
<div class="Mcon1img httpsClass" >
<a href='/visit/stucoursemiddle?courseid=215497310&clazzid=34423626&vc=1&cpi=166457817' target="_blank" >
<img src= https://p.ananas.chaoxing.com/star3/270_160c/56d94e43e4b0dfadae7a3437.jpg onerror="nofind(event)" imagedata='https://p.ananas.chaoxing.com/star3/270_160/56d94e43e4b0dfadae7a3437.jpg' width="270" height="169" />
</a>
<a href="javascript:void(0)" class="move" style="right: 0px;" title="移动到">
移动到 </a>
</div>
<div class="Mconright httpsClass">
<h3 class="clearfix" >
<a class="courseName" href='/visit/stucoursemiddle?courseid=215497310&clazzid=34423626&vc=1&cpi=166457817' target="_blank" title="创业基础">创业基础</a>
<i> </i>
</h3>
<p title="王艳茹">王艳茹 </p>
<p title="中国 青年政治学院">中国青年政治学院 </p>
<p title="默认班级">默认班级</p>
<p class="">课程时间:2020年11月12日-2021年01月10日</p>
<p class="Mconrightp3" style="display:none;">
</p>
</div>
</li>
<li class="addLi">
<div class="">
<a href=http://www.fanya.chaoxing.com/schoolcourse/zixuan2?fid=11231 target="_top" class="Mdelc2dt" style="height: 202px;padding-top: 76px;" title="添加课程"><span></span></a>
</div>
</li>
</ul>
可以看到课程信息以及相关链接,接下来要做的就是根据 DOM 对象,提取这些课程数据,下为我那时候解析的代码:
根据 CSS 类名 courseItem,获取课程单元块以及课程数,然后通过遍历 courseItemList,同时通过 CSS 选择器选择到对应的 HTML 标签,获取到我们想要的数据,通过一个自定义数据类型(这里非对象),将其存在课程列表数组内,最后将这些数据通过超级列表框设置到页面上。也就是如下图这样
同样的获取章节列表,作业列表,考试列表,甚至是一些评论列表,也都是通过 DOM 解析,获取其数据,存储到数组内,然后根据章节名或者 id 来获取数组成员,已达到指定课程完成任务。
开始刷课(重点)
如果只是获取数据那怎么能够,而刷课才是软件的主要目的,首先要刷课,就必须要指定课程,这里指定课程也就是 列表框中选中即可,此时点击开始刷课便能开始任务,这里来看看刷课的代码
就是判断用户有无登录,有无任务在执行,有无选课,然后将配置写入到配置文件,方便下次打开还是上次配置,同时设置模式,是要完成那一部分任务,最后将按钮设置为禁止,不可点击(所以为啥我不一开始就直接禁止开始刷课为假呢),最后启动一个线程来执行,在主线程执行会导致窗口卡顿等现象。然后下面才是正在的执行逻辑了。
获取章节列表
添加了注释,就懒得在打字一个个说明了,执行逻辑并不难,也就是判断,然后执行,接着要到刷视频和题这个方法,因为最主要还是这个代码在干什么。(后面也会将写上代码注释,方便大家理解)
开始刷视频和题
开始循环访问选择夹,接下来代码有点多,执行流程也就是循环,判断,我简化了很多,认真看
这里我要提一下,上面的 mArg 数据是什么,是一段 JSON 数据文本,长下面这样
{
"attachments": [
{
"headOffset": 663000,
"jobid": "1596706035431101",
"otherInfo": "nodeId_349140314-cpi_159793445",
"isPassed": true,
"property": {
"jobid": "1596706035431101",
"switchwindow": "true",
"size": 443706433,
"fastforward": "true",
"hsize": "423.15 MB",
"module": "insertvideo",
"name": "12.13.mp4",
"mid": "8562913227181596706035139",
"type": ".mp4",
"doublespeed": 1,
"objectid": "902ca19c673c7fa256702b6211c9df07",
"_jobid": "1596706035431101"
},
"mid": "8562913227181596706035139",
"playTime": 663000,
"type": "video",
"aid": 600168224,
"objectId": "902ca19c673c7fa256702b6211c9df07"
}
],
"defaults": {
"fid": "1617",
"ktoken": "ac0308fc1f7a84019740f1bebfd0b733",
"mtEnc": "a70ce1e7dac8d0d2e6082e2a95d002a3",
"isFiled": 0,
"ignoreVideoCtrl": 0,
"reportUrl": "https://mooc1-2.chaoxing.com/multimedia/log/a/159793445",
"chapterCapture": 0,
"userid": "157041903",
"reportTimeInterval": 60,
"initdataUrl": "https://mooc1-2.chaoxing.com/richvideo/initdatawithviewer",
"knowledgeid": 349140314,
"schooldoublespeed": 0,
"qnenc": "b2a727eed024085321062c005680e1ef",
"defenc": "bca5e389669154f0fd1fb0208b2ad655",
"clazzId": 34189060,
"cardid": 311180449,
"imageUrl": "https://p.ananas.chaoxing.com/star3/270_169c/f01bc30632e023f83b3e8879cdeea2c7.jpg",
"lastmodifytime": 1609462354000,
"state": 0,
"courseid": 215403857,
"cpi": 159793445,
"subtitleUrl": "https://mooc1-2.chaoxing.com/richvideo/subtitle"
},
"control": true
}
通过 JSON 解析工具,可以获取到章节下的课件内容信息。比如视频时长,视频通过状态,视频 id,等等
取到我们想要的数据,并存为变量即可,接着才是关键所在,获取到了课件信息,同时判断课件类型为视频,并且视频的通过状态为 false,那么接下来就是要提交视频了。相关代码如下
提交视频
其中这里提交视频就一个请求,也就是这个请求,服务端才知道你视频看了多少,并且将你的观看时长记录到数据库中,最终拼接的 url 比如这样的 https://mooc1-2.chaoxing.com/multimedia/log/a/159793445/66ee5f706ccc9e58ab0ea383a83e665c?clazzId=34189060&playingTime=935&duration=935&clipTime=0_935&objectId=34ad66ae9778a00c6bfde810f12431ac&otherInfo=nodeId_349140316-cpi_159793445&jobid=1595816983931186&userid=257041903&isdrag=4&view=pc&enc=f26c618c0e0af1147fe2f4ce7b5e8f95&rt=0.9&dtype=Video&_t=160954150532
当然这个请求没这么好伪装出来,你在上面这几行就可以看到这些参数的复杂了,并且还有相关的加密,如果你随便发送一个请求,服务器鸟都不会鸟你的。伪装还算简单,照葫芦画瓢就完事了,而加密你就需要了解对应的加密算法和 JS 逆向了。这里如果我改掉其中一个必要的请求,那么将会出现如下界面
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>403</title>
<style>
.main{height:255px;margin:0 auto;margin-top:15%;font-size:14px;color:#999; width: 350px;}
.font_top{padding-top:45px;display:inline;}
</style>
</head>
<body>
<div class="main">
<p class="font_top">>_< 很抱歉,您没有权限访问这个页面! (403)</p>
<p style="font-size: 12px; color: #ddd;">112.48.28.255<br/>mooc-2166199849-8f1c1</p>
</div>
</body>
</html>