{"id":13467639,"url":"https://github.com/VermiIIi0n/fuckZHS","last_synced_at":"2025-03-26T03:30:49.979Z","repository":{"id":50337794,"uuid":"486848551","full_name":"VermiIIi0n/fuckZHS","owner":"VermiIIi0n","description":"自动刷智慧树课程的脚本","archived":false,"fork":false,"pushed_at":"2024-10-26T11:42:07.000Z","size":13700,"stargazers_count":717,"open_issues_count":6,"forks_count":84,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-10-29T21:56:12.793Z","etag":null,"topics":["automation","zhihuishu"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/VermiIIi0n.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-04-29T05:28:36.000Z","updated_at":"2024-10-29T14:29:19.000Z","dependencies_parsed_at":"2022-09-06T08:40:12.546Z","dependency_job_id":"6339b4f8-0e52-4d83-9ef6-7da8c112f428","html_url":"https://github.com/VermiIIi0n/fuckZHS","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VermiIIi0n%2FfuckZHS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VermiIIi0n%2FfuckZHS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VermiIIi0n%2FfuckZHS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VermiIIi0n%2FfuckZHS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/VermiIIi0n","download_url":"https://codeload.github.com/VermiIIi0n/fuckZHS/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245584516,"owners_count":20639561,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","zhihuishu"],"created_at":"2024-07-31T15:00:58.754Z","updated_at":"2025-03-26T03:30:49.938Z","avatar_url":"https://github.com/VermiIIi0n.png","language":"Python","funding_links":[],"categories":["Python","智慧树 / 知到","🧑‍💻 ZJU Hacks"],"sub_categories":["素材"],"readme":"# ZHS Fucker 食用指北\n\n## NEWS\n\n我总觉得有一天这个脚本会被 `404` ，没想到以这种形式实现了，感谢各位对鸽鸽的支持。\n\u003cimg style=\"width: 100px;\" src=\"https://github.com/VermiIIi0n/fuckZHS/assets/104612478/7a285df5-7ffb-4272-b26a-cfa4f079d506\"\u003e\n\n---\n\n在 TG 上出现了倒卖开源代码的家伙, 其谎称有我的授权. [详见此处](https://github.com/VermiIIi0n/fuckZHS/discussions/19#discussioncomment-4168410).\n\n本脚本是为人节约生命的, 不是增加负担的, 况且屎山代码我也不好意思卖, 倒卖就算了别说是我授权的, 丢不起这个脸![dragon](./images/dragon.jpg)\n\n愿意支持的朋友点个 ☆ 就好, 叔叔我承诺本~~屎山~~脚本可能会下架, 但永不会变质.\n\n---\n\n## WTF?\n\n这是一个 _Python3_ 的自动脚本, 用于自动刷智慧树课堂课程, 为您节约有限的生命.\n\n### Features\n\n- 支持校内学分课与知到共享学分课\n- 自动回答弹题\n- 设定时限\n- 射后不管, 无需交互\n\n---\n\n## WHY?\n\n自从智慧树的校内学分课(i.e. hike)播放页面用了个窒息的 _JavaScript_ 混淆之后, 大部分前端的脚本都没法用了.  \n因为它会检查 _DevTools_ 是否打开, 如果打开了就无法继续运行, 要分析的话由于混淆, 解读很麻烦.  \n于是我打算直接抄家, 入它后端(\\*), 之后便有了该脚本. (虽然最后还是被逼着反混淆了前端代码...)\n\n---\n\n## 实用链接\n\n- [版本更新注意事项](https://github.com/VermiIIi0n/fuckZHS/discussions/24)\n- [常见问题](https://github.com/VermiIIi0n/fuckZHS/discussions/25)\n- [讨论区](https://github.com/VermiIIi0n/fuckZHS/discussions)\n\n---\n\n## 准备工作\n\n您还需要做以下准备:\n\n- _Python3.10_ 及以上版本(或自行改写旧版不兼容的语法)\n- 执行 `pip install -r requirements.txt` 安装依赖\n\n---\n\n## 如何使用\n\n因登陆验证改变**目前仅有二维码登陆可用**\n\n本块分为\n\n0. Ultra Quick Start: 执行 `python main.py`\n\n   - 仅以交互输入信息, 除非开 DEBUG 模式否则不留任何敏感记录\n\n1. `Login`: 输入账号与密码\n\n   - 使用配置文件\n   - 使用命令行参数\n\n2. `Fxxking`: 开干\n\n   - 使用命令行参数\n   - 拉清单\n   - 参数列表\n\n3. `API` 简易文档: 用于单独使用模块\n\n### Login\n\n_\\*如果非常用地登入（可能？）会需要短信验证, 您应该先用浏览器登入一次, 以让您的所在地列入白名单。_  \n _\\*\\*信息优先级: 命令行 \u003e 配置文件 \u003e 交互输入_\n\n### 使用配置文件\n\n配置文件 _config.json_ 中有以下字段:\n\n```JSON\n{\n  \"username\": \"\",\n  \"password\": \"\",\n  \"qrlogin\": true,\n  \"save_cookies\": true,\n  \"proxies\": {},\n  \"logLevel\": \"INFO\",\n  \"tree_view\": true,\n  \"progressbar_view\": false,\n  \"qr_extra\": {\n      \"show_in_terminal\": null,\n      \"ensure_unicode\": false\n  },\n  \"image_path\": \"\",\n  \"pushplus\": {\n      \"enable\": false,\n      \"token\": \"\"\n  },\n  \"bark\": {\n      \"enable\": false,\n      \"token\": \"https://example.com/xxxxxxxxx\"\n  },\n  \"config_version\": \"1.3.0\"\n}\n```\n\n- `username`: 账号\n- `password`: 密码\n- `qrlogin`: **目前强制启用**二维码登陆, 方便在服务器上部署, 优先级高于账号密码\n- `proxies`: 代理, 可留空, 在 _Windows_ 上还可解决 _Clash_ 等代理造成的证书错误, 详见 [_常见问题_](https://github.com/VermiIIi0n/fuckZHS/discussions/25)\n- `logLevel`: 日志等级, 可选 `NOTSET` `DEBUG` `INFO` `WARNING` `ERROR` `CRITICAL`\n- `save_cookies`: 保存cookies，*短时间*内可以自动登录。\n- `tree_view`: 课程目录结构，关闭后不显示所有课程目录。\n- `progressbar_view`: 进度条控制，关闭后不显示当前视频进度。\n- `image_path`: 登录二维码保存路径，留空则不保存。\n- `qr_extra`: QR 相关配置\n  - `show_in_terminal`: 将二维码打印至终端\n  - `ensure_unicode`: 仅使用 Unicode 字符打印二维码\n- `pushplus`: 基于 [pushplus](https://www.pushplus.plus) 的推送服务\n  - `enable`: 启用推送\n  - `token`: 推送 token\n- `bark`: 基于 [bark](https://github.com/Finb/Bark) 的推送服务\n  - `enable`: 启用推送\n  - `token`: 推送 token，例如：`https://api.day.app/xxxxxxxxxxxxx`\n\n~~填入账号密码即可无干预自动登入~~ **当前失效**  \n _\\*配置文件如果没有的话会在 main.py 执行时自动创建._\n\n### 使用命令行参数登入\n\n```bash\npython main.py -u \u003cusername\u003e -p \u003cpassword\u003e\npython main.py -q\n```\n\n- `-u` `--username`: 账号\n- `-p` `--password`: 密码, 要注意密码将会明文留在记录中, 故不推荐使用 `-p` _\\*虽然说这破站密码泄漏就泄露吧, 写配置文件里多方便, 俗话说晚泄不如早泄(¿)_\n- `-q` `--qrlogin` : 启用二维码登陆\n\n---\n\n### Fxxking\n\n### 命令行指北\n\n**以下假设您已经在配置文件中输入必要信息**  \n _\\*信息优先级: 命令行 \u003e 配置文件 \u003e 交互输入_\n\n```bash\ncd fuckZHS\npython main.py # 如果 execution.json 不存在的话, 刷所有课\n# 只刷课程 ID 为 114514 和 1919180 的课\npython main.py -c 114514 1919180\n# 想秒过可以设个很高的 SPEEEED\npython main.py -s 444 # 我就感觉到快, 有种催人ban的感觉\n# 又或者可以限制每节课学习25分钟\npython main.py -c 114514 -l 25\n# 遇到问题想开 debug 模式, 顺带加个代理?\npython main.py -c 114514 -d --proxy http://127.0.0.1:2333\n# 又比如您想只干某几个视频, 那您可以使用 -v 传入视频 ID\npython main.py -c 114514 -v 1989 604\n# 刷ai课程，ai课程号为 114514，classId为 4444\npython main.py -ai 114514 4444\n```\n\n什么？不知道课程 ID 或视频 ID? 进入课程界面就可以在网址里看到了.  \n _\\*课程 ID 为网址中的 `courseId`(校内学分课) 或 `recruitAndCourseId`(共享学分课) 参数_  \n _\\*\\*更多选项请使用 `-h` 查看._\n\n### 拉清单\n\n使用 `--fetch` 参数可从服务器获得所有课程清单, 存储到 _execution.json_ 这个小本本里, 您可以删去不想干的课程  \n当该文件存在且没有指定课程 ID 时, 会先看看这个小本本上有没有写着课程 ID  \n_\\*清单中 `id` 参数是必须的, 想额外拉清单时请注意_  \n_\\*\\*修改时请注意 JSON 语法, 不然 Exceptions 会一下突开到脸上_\n\n```bash\npython main.py --fetch\n```\n\n清单示例:\n\n```JSON\n[\n    {\n        \"name\": \"中国近现代史\",\n        \"id\": \"42\"\n    },\n    {\n        \"name\": \"思想道德与法治\",\n        \"id\": \"1919\"\n    }\n]\n```\n\n### 命令行参数列表\n\n- `-c`, `--course`: 课程 ID, `courseId` 或 `recruitAndCourseId`, 可输入多个\n- `-v`, `--videos`: 视频 ID, `fileId` 或 `videoId`, 可输入多个\n- `-u`, `--username`: 账号\n- `-p`, `--password`: 密码\n- `-q`, `--qrlogin`: 二维码登录，目前**强制开启**\n- `-s`, `--speed`: ~~**POWERR AND SPEEEEED!**~~ 指定播放速度, 想要秒过可以设个很高的值(e.g. 644), 但不推荐. 默认为浏览器观看能到的最大值\n- `-t`, `--threshold`: 完成时播放百分比, 高于该值视作完成, 想要重复刷课可以使用大于 `1.0` 的值, 例如 `2.0` 则会再刷一遍\n- `-l`, `--limit`: 单节课的时限, 如果您看得上内点习惯分就用吧\n- `-d`, `--debug`: 调试级日志记录, 会记录请求到日志 **_(可能包含账号密码, 别乱分享, 当心被盒武_**\n- `-f`, `--fetch`: 获取课程清单并存入 _execution.json_ 文件\n- `--show_in_terminal`: 将二维码打印至终端\n- `--proxy`: 代理设置, 本来用来调试的(e.g. \u003chttp://127.0.0.1:8080\u003e)\n- `--tree_view`: 课程目录结构，关闭后不显示所有课程目录。\n- `--progressbar_view`: 进度条控制，关闭后不显示当前视频进度。\n- `--image_path`: 登录二维码保存路径，留空则不保存。\n- `-ai`, `--aicourse`: AI课程, Input AI Course ID and CLASS ID to fu*k aiCourse\n- `-h` `--help`: 显示帮助\n\n运行示例如下:\n![运行示例](./images/running.png)\n\n指北就这些啦, ~~代码很少可以自己看~~ 现在不少了().\n\n---\n\n### `API` 简介\n\n详见源码\n\n#### 实例化 `Fucker`\n\n```Python\nfrom fucker import Fucker\n\nfucker = Fucker()\n# 或者更进阶一些\nfucker = Fucker(speed=1.5, end_thre=0.91)\n# 以上控制播放速度以及终止临界值\n# speed: 校内学分课播放速度正常最高为1.25, 共享学分课为1.5, 但似乎更高的服务器也接受, 不过怕暴露就谨慎些吧\n# end_thre: 智慧树把高于一定百分比的进度视为完成, 0.91 保险一点\n# 以上俩参数仅对视频有效, 其他的内容就只有进度0或1\n```\n\n### `login` Method\n\n如果您能自己得到 `Cookies` 就可以略过, 直接给 `Fucker` 实例的 `cookies` 属性赋值即可\n_Python3_ 登入样例\n\n```Python\nfucker.login(username:str, password:str)\nfucker.login(username:str, password:str, interactive=False) # 禁用交互, 在账号或密码为空时引发异常\n\n# 如果您不想使用 login\n# 也可直接传入符合 requests 库要求的 cookies\nfucker.cookies = {key: \"Angel Beats!\", \"inside Uncle's eyes\": value}\n# 该 cookies 会覆盖原有值, 同时请确保它是完整的智慧树 cookies，因为 uuid 需要从 cookies 中解析\n```\n\n### `fuckCourse` Methods Family\n\n```Python\nfucker.fuckCourse(course_id:str) # 把整个课程都干了\nfucker.fuckCourse(course_id:str, tree_view=False) # 把整个课程都干了, 但不显示树状视图\nfucker.fuckZhidaoCourse(recruitAndCourseId) # 点名就要干知到共享学分课\nfucker.fuckHikeCourse(courseId) # 点名就要干校内学分课(hike)\n```\n\n### `fuckVideo` Methods Family\n\n```Python\nfucker.fuckVideo(course_id, video_id) # 指定干某节课里某视频\nfucker.fuckZhidaoVideo(recruitAndCourseId, videoId) # 这里的 videoId 是从 网页API响应 中获得的\nfucker.fuckHikeVideo(courseId, fileId) # 这俩就在 校内学分课(hike) 网址里\n```\n\n### `getContext` Methods Family\n\n```Python\nfucker.getZhidaoContext(recruitAndCourseId)\nfucker.getHikeContext(courseId)\nfucker.getHikeContext(courseId, force=True) # 强制更新context并重置课程学习时间(本地记录)\n'''\n自动在 `fuck*Course` `fuck*Video` 中被调用\n返回一个 `dict`, 内含该 ID 对应课程的必要信息以及实例运行后学习该课的总时间, 必要信息没有的话会向服务器请求\n该 `dict` 结构请见源码\n'''\n```\n\n### `hike` API\n\n```Python\n# 自动添加时间戳和签名, 并检查返回代码\ndef hikeQuery(self, url:str, data:dict,sig:bool=False, ok_code:int=200, setTimeStamp:bool=True, method:str=\"GET\")...\n# 以下为与网页接口同名的 API, 只返回响应中的 \"rt\" 部分\ndef queryResourceMenuTree(self, course_id)...\ndef stuViewFile(self, course_id, file_id)...\ndef saveStuStudyRecord(self, course_id, file_id, played_time, prev_time, start_date)...\n```\n\n### `zhidao` API\n\n```Python\n# 自动添加时间戳和加密, 并检查返回代码\ndef zhidaoQuery(self, url:str, data:dict, encrypt:bool=True, ok_code:int=0, setTimeStamp:bool=True, method:str=\"POST\", key=VIDEO_KEY)...\n# 以下为与网页接口同名的 API, 只返回响应中的 \"data\" 部分\ndef gologin(self, RAC_id)...\ndef queryCourse(self, RAC_id)...\ndef videoList(self, RAC_id)...\ndef queryStudyReadBefore(self, course_id, recruit_id)...\ndef queryStudyInfo(self, lesson_ids:list, video_ids:list, recruit_id)...\ndef queryUserRecruitIdLastVideoId(self, recruit_id)...\ndef prelearningNote(self, RAC_id, video_id)...\ndef loadVideoPointerInfo(self, RAC_id, video_id)...\ndef lessonPopoupExam(self, RAC_id, video_id, question_ids:list)...\ndef saveLessonPopupExamSaveAnswer(self, RAC_id, video_id, question_id, answer_ids:str)...\ndef saveDatabaseIntervalTime(self, RAC_id, video_id, played_time, last_submit, watch_point, token_id=None)...\ndef saveCacheIntervalTime(self, RAC_id, video_id, played_time, last_submit, watch_point, token_id=None)\n```\n\n---\n\n## 结构介绍\n\n### 文件结构\n\n- main.py: 命令行主函数\n- sign.py: 负责生成 hike API 所需的 `signature` 参数\n- utils.py: 一些常用工具\n- logger.py: 日志工具, 将不同等级的日志写入不同文件\n- fucker.py: `Fucker` 类定义, 所有核心代码全塞一起了, 莫在意\n- ObjDict.py: `ObjDict` 类定义, 继承自 `dict`, 可以 `object` 属性形式访问 `dict`\n- zd_utils.py: 知到 API 所需的工具, 如生成 `ev` 和 `secretStr`\n- config.json: 还能是啥, 没有的话初次运行 _main.py_ 时将生成\n- meta.json: 包含版本和分支等信息, 也用于更新检查\n- decrypt: 非必要的文件夹, 内含逆向源代码的工具及源码打包\n\n### Fucker Class Structure\n\n```Python\nclass Fucker:\n    def __init__(self, cookies: dict = None,\n                 headers: dict = None,\n                 proxies: dict = None,\n                 limit: int = 0,\n                 speed: float = None,\n                 end_thre: float = None)...\n\n    @property # cannot directly manipulate _cookies property, we need to parse uuid from cookies\n    def cookies(self)...\n    @cookies.setter\n    def cookies(self, cookies: dict|requests.cookies.RequestsCookieJar)...\n\n    def login(self, username: str=None, password: str=None, interactive: bool=True)...\n    def fuckCourse(self, course_id:str, tree_view:bool=True)...\n    def fuckVideo(self, course_id, video_id:str)...\n############################################\n# for some fucking reasons\n# there are 2 sets of completely different API for hike.zhihuishu.com and studyservice-api.zhihuishu.com\n# so we need to use different methods for different API\n#############################################\n# following are methods for studyservice-api.zhihuishu.com API\n    def getZhidaoContext(self, RAC_id:str, force:bool=False)...\n    def fuckZhidaoCourse(self, RAC_id:str, tree_view:bool=True)...\n    def fuckZhidaoVideo(self, RAC_id, video_id)...\n    def answerZhidao(self, q:dict)...\n    def _zhidaoQuery(self, url:str, data:dict, encrypt:bool=True, ok_code:int=0,\n               setTimeStamp:bool=True, method:str=\"POST\")...\n# end of zhidao methods\n#############################################\n# following are methods for hike API\n    def getHikeContext(self, course_id:str, force:bool=False)...\n    def fuckHikeCourse(self, course_id:str, tree_view:bool=True)...\n    def fuckHikeVideo(self, course_id, file_id, prev_time=0)...\n    def fuckFile(self, course_id, file_id)...\n    def _traverse(self,course_id, node: ObjDict, depth=0, tree_view=True)...\n    def _hikeQuery(self, url:str, data:dict,sig:bool=False, ok_code:int=200,\n                   setTimeStamp:bool=True, method:str=\"GET\")...\n# end of hike methods\n#######################################\n# shared private methods\n    def _watchVideo(self, video_id)... # it's probably unnecessary but let's keep it to fool those idiots\n    def _apiQuery(self, url:str, data:dict, method:str=\"POST\")...\n    def _checkCookies(self)...\n    def _checkTimeLimit(self, cid)...\n    def _sessionReady(self, ctx:dict=None)...\n```\n\n我的很大, 你忍一下(指类定义)\n\n---\n\n## 后记\n\n这一段算是授之以渔吧, 毕竟网站以后更新可能会改内容, 我又没时间更新, 就先把思路写在这. 但要注意, 下文提到的混淆肯定引入了随机量, 不可完全照搬该思路.  \n过程中用到的文件样本大多都在 _decrypt_ 目录下压缩档里.\n\n**_以下内容仅针对校内学分课的 `API` (\"hike\" 开头的域名)_**\n\n### Chapter 0: Too Young Too Naive\n\n本以为从后端入手会很轻松, 目前绝大部分的脚本是在前端实现刷智慧树自动化(有例外但那些都不是对付校内学分课的), 这样做不仅鲁棒性很差而且过于复杂, 本脚本则直接绕后, 甩开前端, 可以做到不变应万变...吧?\n\n#### 捉包\n\n总之先捉个包看看前端怎么和服务器交流, 没想到一开浏览器开发者模式就遇到了问题.  \n一旦监测到 _DevTools_, 网页界面就立即停止响应.  \n但这还不简单, 手动把相关 _JavaScript_ 无脑删不就行, 源码都在我手上有什么好怕的.\n\n紧接着一看源码:\n\n![WTF](./images/wtf.png)\n_好家伙《乱码 1/2》_\n\n不过这也没什么, 我本来就只是想抓个包, 用别的工具就是了.\n\n抓包得到页面向服务器报告进度的甜言蜜语:  \n`Headers`:\n![headers](./images/headers.png)\n\n`Params`:\n![params](./images/params.png)\n_\\*敏感信息被替换了_  \n您看看这些人多不专业, 拿 `GET` 干 `POST` 的活, 而且那个 `uuid` 根本就不是真的 UUID, 七八个随机字符而已.\n\n#### 模拟请求\n\n不过我们一路顺风, 成功就在眼前(flag++).  \n这时可以看到一个正常的响应是:\n\n```JSON\n{\n  \"status\": 200,\n  \"message\": \"OK\",\n  \"rt\": 202\n}\n```\n\n`rt` 是服务器返回的观看进度.\n\n包里大部分参数都很直接, 我们照搬便是.  \n`courseId` 我们可以从浏览器链接里得知,  \n`fileId` 每个章节的都不一样, 可以从“\u003chttps://studyresources.zhihuishu.com/studyResources/stuResouce/queryResourceMenuTree\u003e” 中获取的 JSON 中得到, 源码里有, 不再赘述.\n\n剩下只有 `signature` 不明, 不过大概是个防止多次提交的随机字符串罢了, 毕竟这群人取名似乎一直不太准确.\n\n然后一试\n\n```JSON\n{\n  \"status\": 200,\n  \"message\": \"OK\",\n  \"rt\": null\n}\n```\n\n**???**  \n为何就返回个 `null`, 为了解决这个问题我快把浏览器内核用 _Python_ 实现了, 一直以为就是个 `Headers` 或者 `Cookie☆` 的问题, 我可能缺了什么.  \n结果最后才发现问题在 `Params`, 而且就是里面那个该死的 `signature` 造成的.  \n这次他们取对名字了, 这真是个签名, 必须和其他内容配套才能被视作合法请求, 并得到 `rt`.  \n想要自己生成这个 `MD5` 签名就只能从前端代码里找 salt 等数据.\n\n没办法, 开始人生第一次反混淆 _JavaScript_ 吧...\n\n---\n\n### Chapter 1: At Beginning, It's Just Chaos\n\n下图开头这个站点名就是万恶之源了, 话说为什么叫“加密”, 只是混淆而已, 和加密相差太远了, 只能说连名字都取的很 Obfuscated.\n\n首先得把这些诡异变量重命名一下, 在 [_这个站点_](https://deobfuscate.io) 可以初步反混淆, 去掉一些简单的函数 wrapping 并重命名变量, 新变量名当然是随机的, 只是好读一些.\n\n#### 初步反混淆\n\n![level0](./images/level0.png)\n这下好多了, 我们可以看 **_不_** 到开头有一行超大的列表(太长了就只截了前几个), 不过它大概有 1.4k 个**字符串**,  \n看起来是 Base64, 解码后是乱码, 是被加密了(真·加密).\n\n再观察一下可以发现一个函数出现的次数很多:\n![mapping](./images/mapping.png)\n这个 nyko 便是负责解密的函数.\n\n要反混淆 nyko 就需要手动去掉一堆无用的循环和分支, 不然真读不下去.  \n恶心的是这个混淆后版本里充斥着各种看起来有意义的字符串, 甚至有些就是混淆前代码里的, 但他们被拿来进行无意义的比较和循环, 这样做可以同时迷惑您和您的 CPU.\n\n总之反混淆出来是这样一个简单的解密程式, 用 _Python_ 重新实现了:\n\n```Python\ndef decrypt(index:str, key:str):\n    index = int(index, 16)\n    encrypted = b64dec(table[index])\n    key = key.encode()\n\n    mod_sum = 0\n    ar = list(range(256))\n    for i in range(256):\n        mod_sum = (mod_sum + ar[i] + key[i % len(key)]) % 256\n        ar[i], ar[mod_sum] = ar[mod_sum], ar[i]\n    mod_sum = 0\n    n = 0\n    decrypted = \"\"\n    encrypted = encrypted.decode()\n    for i in range(len(encrypted)):\n        n = (n + 1) % 256\n        mod_sum = (mod_sum + ar[n]) % 256\n        ar[mod_sum], ar[n] = ar[n], ar[mod_sum]\n        decrypted += chr(ord(encrypted[i]) ^ ar[(ar[mod_sum] + ar[n]) % 256])\n    return decrypted\n```\n\n_JavaScript_ 原版其实在 Base64 解码后, 还有个手动把内容 URLEncode 了, 又调用 `decodeURIComponent` 解码的诡异操作.  \n一开始我把它删了, 结果竟然生成内容不一样. 似乎是 _JavaScript_ 的 `atob` 似乎会保留非法的内容? 经过看似无意义的 URL Encode\u0026Decode 会把这些字符丢弃掉, 只能说混淆者真是钻研了折磨人的极致了, 好在 _Python_ 的 `b64decode` 默认丢弃这些内容. _\\*此部分存疑, 不确定原因是不是这个_\n\n接下来尝试对列表内容解密, 发现还是乱码...\n\n没办法, 只得继续化简开头的函数, 看看会有什么线索.  \n接着发现程式开头有一段循环, 把列表开头和结尾的俩无意义的东西扔了, 顺带把这个列表旋转了 138 位, 用 _Python_ 表示就是 `deque.rotate(-138)`  \n这个值是好像是固定的, 但保不准以后会变\n\n#### 解读解密后代码\n\n解密并替换内容后的文件:\n![level2](./images/level2.png)\n\n解密之后常量已经可以看到了, 但其中仍有很多垃圾信息.  \n比如其中有类模式, 就是混淆版本会在一个 `Object` 内包装一堆简单的函数, 比如加减和比较, 然后取个无意义的名字.  \n结合那一堆无意义的变量来做比较判断, 以此来表示单个 `Boolean` 值.  \n因为嵌套的次数太多所以自动反混淆没能解决它们, 但只要搞清这点可以手动去掉很多无意义的分支.\n\n```JavaScript\nvar Wtf = {\n       RvuT: function (dyo, bie){\n       return dyo === bie;\n       },\n       oPhT: function(Jely, Inp, Stb){\n       return Jely(Inp, Stb)\n       },\n       stEbU: \"d0gky\", RlVDj: \"rk8Eb\"\n    }\nif (Wtf.oPhT(Wtf.RvuT,Wtf.stEbU,Wtf.RlVDj){\n    console.log(\"WTF\");\n    }\n\n//以上这一大坨， 也就等价于\nif (false){\n  console.log(\"who cares what I've logged\");\n  }\n\n//也就等价于\n//do fucking nothing but consuming your CPU, storage, network bandwidth and god damn time\n\n```\n\n---\n\n### Chapter 2: Let There = Light\n\n总算看见曙光了, 但要继续解读的话, 这些乱码的函数名字太难读了, 以功能来重新命名吧, 顺便去掉无用的, 重复的部分:\n\n![level3](./images/level3.png)\n\n怎么说呢, 虽然是很繁琐的一件事, 但看着大批大批代码被删有种莫名的快感, 我当游戏玩了一久.\n\n玩得差不多的时候突然想起来我好像不必把所有代码反混淆, 找到 `MD5` 相关的代码就好啊.  \n现在代码也只有一千多行了, 挺好找的才是.  \n随手一翻发现:\n\n```JavaScript\nfunction jobany(params) {\n      var lennora = kenshin.ADD(kenshin.ADD(\n        kenshin.ADD(kenshin.ADD(kenshin.ADD(\n          kenshin.ADD(kenshin.ADD(kenshin.ADD(\n            kenshin.ADD(\"o6xpt3b#Qy$Z\", params.uuid\n          ), params.courseId),  params.fileId\n        ), params.studyTotalTime),  params.startDate\n      ), params.endDate),  params.endWatchTime\n    ), params.startWatchTime),  params.uuid);\n      console.log(lennora);\n      return kenshin.APPLY($md5, lennora);\n    }\n```\n\n~~_\"Here↘ comes ↗Jobany~\"_~~  \n诶嘿, 改函数名有效果了, 是**盐**! 它加了**盐**! (顺带特想吐槽下这个自动生成的对象名 `kenshin`，挺中二)\n\n**盐**(bushi):\n![Shio](./images/Shiochan.webp)\n\n---\n\n### Chapter 3: small step, Giant Leap\n\n为了个区区 `signature` 废了好大功夫  \n不过有了这个我们可以自己签名了, 没想到人都这么大了还靠在自己作业上签名来骗人\n\n```Python\nfrom hashlib import md5\nfrom ObjDict import ObjDict # 这是一个我从 dict 继承后魔改的玩意儿，可以用 JavaScript 风格访问 dict\n\nSALT = \"o6xpt3b#Qy$Z\"\n\ndef sign(p:dict):\n    p = ObjDict(p)\n    raw = SALT + p.uuid + p.courseId + p.fileId + p.studyTotalTime + \\\n           p.startDate + p.endDate + p.endWatchTime + p.startWatchTime + p.uuid\n    return md5(raw.encode()).hexdigest()\n```\n\n我们也算是**努力**的**靠自己**完成作业了\n\n![1000%](./images/1000.png)  \n~~_已经 1000%了()_~~\n\n---\n\n给把如此凌乱的内容看到最后您一些安慰吧  \n祝您学习像 Anya 一样好, 料理如 Yor 一般出色!\n![bonus](./images/bonus.jpg)\n(sauce: Pixiv; pid:[97128620](https://www.pixiv.net/en/artworks/97128620)) **_放 GitHub 仓库不能算转载吧?_**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FVermiIIi0n%2FfuckZHS","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FVermiIIi0n%2FfuckZHS","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FVermiIIi0n%2FfuckZHS/lists"}