中少快乐阅读期刊 Flash 阅览器分析解密
声明
本文仅用于学习和研究,本文及本人不提供任何工具、文件,不以任何形式用于商业用途,阅读本文也应遵守相关法律法规,维护版权,不得侵犯他人合法权益。
工具
- JEPXS-decompiler releases
JEPXS 是一款开源的 SWF 文件反编译工具,支持 Windows、Linux、MacOS 等多个平台,支持多种语言 - CEF Flash Browser
Flash 浏览器,不需要安装 Flash 插件
序言
最开始是在贴吧上看到了可以在在线阅读中国少年儿童出版社历年期刊的网站,包括《儿童文学》,《少年文摘》,《知心姐姐》等童年超喜欢的期刊们,为了方便阅读,
最好能下载下来离线看,这样就可以在没有网络的情况下看到自己喜欢的期刊了,所以有了后续的一系列内容。
中少快乐阅读(国图)、 中少快乐阅读(杭州)
是其中两个网站,也是最具代表性的两个,本文全文以《儿童文学》为例。
分析从国图版开始,国图版能更方便说明情况。
国图版默认是只有 2014 年~ 2021 年的期刊,中少版除了少部分无法打开外,可以查看几乎所有的期刊,并且可以直接浏览,通常思路就是既然可以通过 HTML 直接浏览
那么,找到对应的路径就可以下载了,一开始我也这么做了,但后来我还是将 HTML5 版本的大部分内容删除,改用了 Flash 版本,也算走了不少弯路吧,后面会具体阐述。
国图版可以通过修改 URL 或使用插件插入输入框来查看更多年份的期刊,不难发现,2013 年及以前的内容并不是没有,
而是只能通过 Flash 进行访问,众所周知,Flash 已经被淘汰,不再被浏览器支持,这些内容如今几乎无法访问,对于大部分读者来说,虽然有国产的 Flash 程序,
但所谓的“Flash浏览器”其实只是旧版本浏览器套了个壳而已,广告,弹窗等内容堪比流氓软件。感谢 CEF Flash Browser,让我能够在不安装 Flash 插件的情况下,
访问和阅读,具体请查看文章开头的相关部分。
HTML 分析和下载
以国图版本总 971 期(2021年12月经典)为例,点击阅读,
这里直接给出链接,
大致链接格式是这样的:
1 | http://{IP}:{端口}/Reading/Show/{ID} |
这个 ID 是期刊的 ID,是 UUID 的格式。查看网页源代码,包括了一些系统版本跳转到HTML5版本的js,调整狂口大小的js和主要内容:一个 iframe
标签。这里的判断系统版本并跳转 H5 看似是旧版本遗留的内容,实质上目前
直接打开就是 HTML5 的内容。iframe
标签的 src
属性指向了和 js 跳转链接一样的地址 链接:
JavaScript 链接被 AdGuard 处理过,不过不影响分析。
1 |
|
1 | /fliphtml5/password/main/qikan/etwx/{年}/{月}/{总期数}/web/flipviewerxpress.html?pswd={开书密码}'; |
继续看新打开的页面的源码,
1 |
|
让我们感谢 ChatGPT 为我们进行的分析:
那么直接看这个 XML 文件吧:
完整 XML 内容:
完整的 XML 内容
1 | <package unique-identifier="{7A2A5428-44E4-4734-973E-DF2A4215287B}"> |
在XML中包括了几个其他的XML文件:
- etwx971DL_license__license_.xml
- etwx971DL_text_.xml
- etwx971DL_linkdef_.xml
- etwx971DL_archive_.xml
其中,etwx971DL_license__license_.xml
包括了现在看起来意味不明的内容,etwx971DL_text_.xml
是一个包含了书籍的部分文本,看起来应该是方便检索使用功能的,etwx971DL_linkdef_.xml
是一个没有实际内容的文件,etwx971DL_archive_.xml
指明了封面图和文本xml地址,其他并没有提供额外信息。
etwx971DL_license__license_.xml 的完整内容:
etwx971DL_license__license_.xml
1 | <package unique-identifier="{2FFC6E0F-48D2-45B3-A5B3-4706235FE0B1}"> |
同时还包括了每一页的下载地址:
1 | <item id="item0" href="normal/etwx971DL_1.jpg" href2="zoomone/etwx971DL_1.jpg" media-type="image/x-flp"/> |
实质上每一页包括了两个地址,一个是普通的图片地址,另一个是放大后的图片地址,阅读时放大图片其实是加载了另一个张更清晰的图片进行放大,直接访问对应的地址
就可以获取到相应的图片。
那么事情的脉络已经较为清晰,我们需要做的是:
- 获取到每一本书对应的 XML 文件
- 通过 XML 文件获取到每一页的图片地址
- 下载图片,合成 PDF
看起来到这里都合情合理,但不难发现,有一些额外的内容在这里特别显眼,首先是 password,其次是 license 文件,在已知有 Flash 版本的前提下,
这一系列都预示着如果要通过 Flash 版本的方式进行下载,有着复杂的过程。
直接下载 HTML 5 图片并合成 PDF
虽然每一本书对应一个 UUID,可以通过爬取索引页面获得,但其实根据链接的格式,我们可以直接通过链接猜到对应的 XML 地址,这样会方便一些,在实际过程中,
这些地址有所改动的地方基本只有 SL
和 DL
的替换,例如 2021 年 12 月的分别为:
1 | http://202.96.31.36:8888/fliphtml5/password/main/qikan/etwx/2021/12/971/web/html5/tablet/etwx971DL.xml |
通过这样的方式可以直接获取到几乎所有的 XML 文件,对于特殊的月份,需要单独进行处理,手动打开页面获取地址即可。
随后就是下载图片和合成,这里通过 Python 快速实现,虽然程序是中少版的,但国图版其实是一致的,只需要修改一下地址即可。
Python 程序
1 | import requests |
这样就可以直接下载并合成 PDF 了,这里的代码是直接下载了放大后的图片,给定 XML 地址后,即可完成下载和合成。
转折
就这样,完成了第一批的下载和合成,在和大佬交流的过程中,发现了 Flash 版本,Flash版本下载的内容大小和 HTML5 下载大小有一定偏差。
这是大佬:
然后,寻思,对比一下两个版本的内容,发现了一些细节上的问题,直接上对比图(左侧为 H5 版本,右侧为 Flash 版本)
以 2014 年 1 月 经典为例:
封面:
文章页:
很显然,Flash 版本的内容更加清晰,H5 的图片在 JPEG 小波处理的影响下,边缘变得模糊,有明显锯齿,而 Flash 版本则没有这么严重。
那么既然想要收藏,那折腾肯定是要的,那么就开始对 Flash 版本下手吧。
Flash 版本阅读器
首先依然观察链接,Flash 链接和 H5 链接的区别只在 fliphtml5 和 flipbooks 的替换上,其他一致,XML文件,图片地址都是一致的。
1 | http://202.96.31.36:8888/flipbooks/password/main/qikan/etwx/2014/01/591/web/flipviewerxpress.html?pswd=7391 |
直接下载一个 Flash 页面,
以封面
对应的 链接 为例,
下载无法打开,通过十六进制编辑器查看文件头非常混乱,明显经过了加密处理。
既然浏览器可以打开阅读,说明播放的工具有着解密功能,观察可以发现 license 文件中似乎已经提供了一部分内容,那么就需要来分析一下 Flash 阅读器了。
通过任意一本期刊都可以获取到同一个播放器的 swf 文件,
链接在这里,实际获取链接中带了参数,这些参数传递了 XML 文件的地址,暂时忽略。
获取到 swf 文件后,进行快速检查,并没有对这个 swf 文件进行额外的加密,那么就可以直接使用 JEPXS 进行反编译了。
通过 JEPXS 反编译后,得到几乎可读的源码。
其中有这么一些很令人在意:
在 FVDRMCertificate.as 中,找到了加载 license 验证 license 的部分,确定了如果手动修改拦截了 license 文件,但不对播放器进行处理是无效的,
可以利用 JEPXS 对 PCODE 进行修改来实现忽略不正确的 license 以实现相应功能,同时找到了密码所在,因为web访问自带密码,所以几乎无感,但如果是下载
后打开,就需要这一密码,这部分内容与解密下载的 swf 无关,此处不过多站开。
跟随打开书的逻辑在fv.tool.FVLoadTool.as 中找到了加载页面的逻辑:
onPageDataLoaded
1 | private function onPageDataLoaded(objectByte:ByteArray, url:String, success:Boolean) : void |
跟随找到 fv.common.FVUtil.as 中的 encryptStream:
encryptStream
1 | public static function encryptStream(inArray:ByteArray, key:String, isEncryption:Boolean) : ByteArray |
这里发现定义了一个 10248 的长度,如果小于这个长度,就直接加密,否则只加密前 10248 长度的内容,也就是说,只需要对前 8k 长度的内容进行解密即可。
跟随加密方式找到了 fv.common.FVCipher.as 中的加密方式:
encryptStream
1 | public function encryptStream(strm:ByteArray, encryptKey:String, bufSize:int, isEncryption:Boolean) : ByteArray |
这里的加密方式是通过 FVCipherMgr 进行的,这里的加密方式是对称加密,通过 szKey 进行初始化,然后对 strm 进行加密,继续分析 encodeBuffer:
encodeBuffer
1 | public function encodeBuffer(src:ByteArray, dest:ByteArray, dataSize:int) : void |
这里的加密方式是 SAFER 算法,通过搜索引擎并没有找到相关的解密方式,需要进一步分析,通过 FVSafer.as 找到了 SAFER 算法的实现,
saferEncryptBlock
1 | public function saferEncryptBlock(block_in:ByteArray, key:ByteArray, block_out:ByteArray, arrayPoint:int) : void |
同时找到了解密函数:
saferDecryptBlock
1 | public function saferDecryptBlock(block_in:ByteArray, key:ByteArray, block_out:ByteArray, arrayPoint:int) : void |
只需要实现这个解密函数,就可以对前 8k 的内容进行解密,就可以对文件进行解密,同时解密密钥是在 FVDRMCertificate 中的 encryptionKey 中,
但和 license 文件中的 encryption 略微不同:
在加载 license 函数中有如下内容:
parseLicense
1 | public function parseLicense(licenseContents:String) : void |
这里的 encryptionKey 是通过 encryptString 进行加密的,同时还包括了一个 constEncryptKeyCDViewer,
这个值是在 FVConstants 中定义的,这个值是一个固定的字符串,可以直接通过反编译找到:
1 | public static const constEncryptKeyCDViewer:* = "0b8b6a4650b148a1975331bc2da63f93"; |
再次跟随找到字符串的加密函数:
encryptString
1 | public function encryptString(str:String, encryptKey:String, isEncryption:Boolean, isHex:Boolean) : String |
此外还有一些函数:
encodeString,decodeString
1 | public function encodeString(src:ByteArray, dest:ByteArray, dataSize:int) : void |
初始化密钥的函数:
initKey
1 | public function initKey(inputStr:ByteArray, nKeyBytes:int) : void |
saferEncryptBlock 函数前面已经实现了,继续找到的 saferExpandUserkey 函数:
saferExpandUserkey
1 | public function saferExpandUserkey(userkey_1:ByteArray, userkey_2:ByteArray, key2Start:int, nof_rounds:uint, strengthened:int, key:ByteArray) : void |
至此,所有需要的内容就绪,现在需要做的就是实现这个解密函数,由于 Flash 使用的是 ActionScript,如今已经几乎没有使用,FLEX 也快要被淘汰,很难
找到一个合适的环境进行编译,在现代科技(AI)的帮助下,可以较为轻松的使用与其类似的语言进行改写,出于个人技术栈等原因,我选择了 Go 语言。
Go 解密实现
本着简单高效考虑,忽略了很多语法规范,RipeMD256Hash 直接使用了第三方库。
程序如下:
Go 语言程序实现
1 | package main |
程序根据 ActionScript 脚本,更换了变量类型和部分方法,对解密过程进行了复现,使用时候需要提供 swf 文件路径和 lisence 文件中的 encryption 密码。
程序还根据 swf 文件的格式,对 swf 文件进行了解压缩,提取了其中的图片,如果是 xml 文件,直接解密输出。
虽然程序已经实现了,但分析还是补上,对 swf 文件进行解密后还是 swf 文件,通过 JPEXS 打开可以找到里面的内容。
如果文件内只有一张图片,那可以按上面程序给的方法,直接根据 swf 文件格式,提取首个附件,但如果是像例子这样由图片和文字拼凑而成,就变得非常麻烦。
目前暂时没有再进行下一步处理,如果你有兴趣或者有更好的方法,请务必与我联系(tdh@tdh6.top,避免爬虫,不直接放通讯软件ID了),
毕竟都看到这里了,交个朋友也不赖吧?
最后的吐槽:
- Flash 时代已经过去了,但那时候的记忆永远留在心中,包括众多小游戏和一些不太小的游戏(比如 EBF),借由 Flash 被大家所知晓,喜爱。
- TMD 你加密一次只送 8 字节是什么意思???还好只加密开头的 8K,不然解密麻烦死。
- 初见不识画中意,再见已是画中人。
- 我不是大佬,谢谢。
附
本文封面图来自番剧《星灵感应》,星屑テレパス(Hoshikuzu Telepath)S01E01 00:06:04