中少快乐阅读期刊 Flash 阅览器分析解密
声明 本文仅用于学习和研究,本文及本人不提供任何工具、文件,不以任何形式用于商业用途,阅读本文也应遵守相关法律法规,维护版权,不得侵犯他人合法权益。
信息失去时效性提醒: 部分站点已经完成了系统升级,可能无法根据本文提供的 URL 链接找到对应的内容,但截止 2024 年 12 月,其分析方法没有变化,后续请自行判断。
工具
JEPXS-decompiler releases JEPXS 是一款开源的 SWF 文件反编译工具,支持 Windows、Linux、MacOS 等多个平台,支持多种语言
CEF Flash Browser Flash 浏览器,不需要安装 Flash 插件
SWFTools SWFTools 是一组用于创建和操作 SWF 文件的工具,包括 SWF 文件的解析和生成,以及 SWF 文件中的图片和音频的提取等功能
Ruffle Ruffle 是一个 Flash Player 的 Rust 实现,可以在浏览器中运行 SWF 文件,但是目前还不支持所有的 Flash 功能,其 exporter 工具可以将 SWF 导出为图片
序言 最开始是在贴吧上看到了可以在在线阅读中国少年儿童出版社历年期刊的网站,包括《儿童文学》,《少年文摘》,《知心姐姐》等童年超喜欢的期刊们,为了方便阅读,最好能下载下来离线看,这样就可以在没有网络的情况下看到自己喜欢的期刊了,所以有了后续的一系列内容。
中少快乐阅读(国图) 、 中少快乐阅读(杭州) 是其中两个网站,也是最具代表性的两个,本文全文以《儿童文学》为例。
分析从国图版开始,国图版能更方便说明情况。
国图版默认是只有 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <!DOCTYPE HTML > <html lang ="zh-CN" > <head > <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <title > 儿童文学总第971期</title > <script type ="text/javascript" nonce ="789b5b3975c14de9a64ee243f87" src ="//local.adguard.org?ts=1708947648105& type=content-script& dmn=202.96.31.36:8888& url=http%3A%2F%2F202.96.31.36%3A8888%2FReading%2FShow%2F8add12c7-34dc-2dc0-00d6-9ee2f5772d26& app=chrome.exe& css=3& js=1& rel=1& rji=1& sbe=1& stealth=1& st-wrtc& st-loc& st-flash& st-dnt" > </script > <script type ="text/javascript" nonce ="789b5b3975c14de9a64ee243f87" src ="//local.adguard.org?ts=1708947648105& name=AdGuard%20Extra& name=AdGuard%20Popup%20Blocker& type=user-script" > </script > <link rel ="stylesheet" href ="/Content/bootstrap-3.0.0/css/bootstrap.min.css" > <link rel ="stylesheet" href ="/Content/bootstrap-3.0.0/css/bootstrap-theme.min.css" > <link rel ="stylesheet" type ="text/css" href ="/Content/css/layout.css" > <link rel ="stylesheet" media ="screen" href ="/Content/css/user-defined-responsive.css" > <script type ="text/javascript" src ="/Scripts/jquery.min.js" > </script > </head > <body > <script type ="text/javascript" > function browserRedirect ( ) { var sUserAgent = navigator.userAgent .toLowerCase (); var bIsIpad = sUserAgent.match (/ipad/i ) == "ipad" ; var bIsIphoneOs = sUserAgent.match (/iphone os/i ) == "iphone os" ; var bIsMidp = sUserAgent.match (/midp/i ) == "midp" ; var bIsUc7 = sUserAgent.match (/rv:1.2.3.4/i ) == "rv:1.2.3.4" ; var bIsUc = sUserAgent.match (/ucweb/i ) == "ucweb" ; var bIsAndroid = sUserAgent.match (/android/i ) == "android" ; var bIsCE = sUserAgent.match (/windows ce/i ) == "windows ce" ; var bIsWM = sUserAgent.match (/windows mobile/i ) == "windows mobile" ; var bIsIEMobile = sUserAgent.match (/iemobile/i ) == "iemobile" ; if (bIsIpad || bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM || bIsIEMobile) { window .location .href = '/fliphtml5/password/main/qikan/etwx/2021/12/971/web/flipviewerxpress.html?pswd=6273' ; } else { } } </script > <script type ="text/javascript" > $(document ).ready (function ( ) { browserRedirect (); function fullscreen ( ) { var width = $(window ).width (); var height = $(document ).height (); $("iframe" ).attr ("height" , height); $("iframe" ).attr ("width" , width); } window .onload = fullscreen (); $(window ).resize (function ( ) { fullscreen (); } ); ) </script > <iframe id ="bookview" src ="/fliphtml5/password/main/qikan/etwx/2021/12/971/web/flipviewerxpress.html?pswd=6273" width ="" height ="" frameborder ="0" > </iframe > <script type ="text/javascript" charset ="utf-8" src ="http://fenxi.61read.com/res/zsstat.js? uid=" > </script > </body > </html >
1 /fliphtml5/password/main/qikan/etwx/{年}/{月}/{总期数}/web/flipviewerxpress.html?pswd={开书密码}';
继续看新打开的页面的源码,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 <!doctype html > <html lang ="zh" > <head > <title > 儿童文学总第971期</title > <meta name ="viewport" content ="width = 1050, user-scalable = no" /> <meta charset ="UTF-8" > <script type ="text/javascript" nonce ="789b5b3975c14de9a64ee243f87" src ="//local.adguard.org?ts=1708947648105& type=content-script& dmn=202.96.31.36:8888& url=http%3A%2F%2F202.96.31.36%3A8888%2Ffliphtml5%2Fpassword%2Fmain%2Fqikan%2Fetwx%2F2021%2F12%2F971%2Fweb%2Fflipviewerxpress.html%3Fpswd%3D6273& app=chrome.exe& css=3& js=1& rel=1& rji=1& sbe=1& stealth=1& st-wrtc& st-loc& st-flash& st-dnt" > </script > <script type ="text/javascript" nonce ="789b5b3975c14de9a64ee243f87" src ="//local.adguard.org?ts=1708947648105& name=AdGuard%20Extra& name=AdGuard%20Popup%20Blocker& type=user-script" > </script > <link rel ="stylesheet" type ="text/css" href ="/turn/css/magazine.css" > <link rel ="stylesheet" type ="text/css" href ="/turn/css/pagination.css" > <script type ="text/javascript" src ="/turn/extras/jquery.min.1.7.js" > </script > <script type ="text/javascript" src ="/turn/extras/modernizr.2.5.3.min.js" > </script > <script type ="text/javascript" src ="/turn/lib/hash.js" > </script > <script type ="text/javascript" src ="/turn/lib/vconsole.min.js" > </script > <script type ="text/javascript" src ="/turn/lib/zoom.min.js" > </script > <script type ="text/javascript" src ="/turn/lib/turn.js" > </script > <script type ="text/javascript" src ="/turn/js/magazine.js" > </script > </head > <body > <div id ="canvas" > <div class ="zoom-icon zoom-icon-in" > </div > <div class ="magazine-viewport" > <div class ="container" > <div class ="magazine" > <div ignore ="1" class ="next-button" > </div > <div ignore ="1" class ="previous-button" > </div > </div > </div > </div > <div class ="pagination-container" > <ul class ="pagination" > <li > <span id ="p-first" > 封面</span > </li > <li > <span id ="p-previous" > 上一页</span > </li > <li > <input id ="p-now" type ="text" /> </li > <li > <span id ="p-next" > 下一页</span > </li > <li id ="li8" > <span id ="p-last" > 封底</span > </li > </ul > </div > <script type ="text/javascript" > var pageFile = "html5/tablet/" ; var xmlFile="etwx971DL.xml" ; if (screen.width > 1024 ) { pageFile = "html5/tablet/" ; } else { pageFile = "html5/mobile/" ; } loadXML (pageFile+xmlFile); function loadApp ( ) { $('#canvas' ).fadeIn (1000 ); var flipbook = $('.magazine' ); if (flipbook.width () == 0 || flipbook.height () == 0 ) { setTimeout (loadApp, 10 ); return ; } flipbook.turn ({ width : 1456 , height : 1024 , duration : 1000 , acceleration : !isChrome (), gradients : true , autoCenter : true , elevation : 50 , pages : 126 , when : { turning : function (event, page, view ) { var book = $(this ), currentPage = book.turn ('page' ), pages = book.turn ('pages' ); Hash .go ('page/' + page).update (); disableControls (page); $('.pagination #p-' + currentPage).parent ().removeClass ('current' ); $('.pagination #p-' + page).parent ().addClass ('current' ); $('.pagination #p-now' ).val (page); }, turned : function (event, page, view ) { disableControls (page); $(this ).turn ('center' ); if (page == 1 ) { $(this ).turn ('peel' , 'br' ); } }, missing : function (event, pages ) { for (var i = 0 ; i < pages.length ; i++) { addPage (pages[i], $(this )); } } } }); $('.magazine-viewport' ).zoom ({ flipbook : $('.magazine' ), max : function ( ) { return largeMagazineWidth () / $('.magazine' ).width (); }, when : { swipeLeft : function ( ) { flipbook.turn ('next' ); }, swipeRight : function ( ) { flipbook.turn ('previous' ); }, resize : function (event, scale, page, pageElement ) { if (scale == 1 ) loadSmallPage (page, pageElement); else loadLargePage (page, pageElement); }, zoomIn : function ( ) { $('.made' ).hide (); $('.magazine' ).removeClass ('animated' ).addClass ('zoom-in' ); $('.zoom-icon' ).removeClass ('zoom-icon-in' ).addClass ('zoom-icon-out' ); if (!window .escTip && !$.isTouch) { escTip = true ; $('<div />' , {'class' : 'exit-message' }).html ('<div>Press ESC to exit</div>' ).appendTo ($('body' )).delay (2000 ).animate ({opacity : 0 }, 500 , function ( ) { $(this ).remove (); }); } }, zoomOut : function ( ) { $('.exit-message' ).hide (); $('.made' ).fadeIn (); $('.zoom-icon' ).removeClass ('zoom-icon-out' ).addClass ('zoom-icon-in' ); setTimeout (function ( ) { $('.magazine' ).addClass ('animated' ).removeClass ('zoom-in' ); resizeViewport (); }, 0 ); } } }); if ($.isTouch) $('.magazine-viewport' ).bind ('zoom.doubleTap' , zoomTo); else $('.magazine-viewport' ).bind ('zoom.tap' , zoomTo); $(document ).keydown (function (e ) { var previous = 37 , next = 39 , esc = 27 ; switch (e.keyCode ) { case previous : $('.magazine' ).turn ('previous' ); e.preventDefault (); break ; case next : $('.magazine' ).turn ('next' ); e.preventDefault (); break ; case esc : $('.magazine-viewport' ).zoom ('zoomOut' ); e.preventDefault (); break ; } }); Hash .on ('^page\/([0-9]*)$' , { yep : function (path, parts ) { var page = parts[1 ]; if (page !== undefined ) { if ($('.magazine' ).turn ('is' )) $('.magazine' ).turn ('page' , page); } }, nop : function (path ) { if ($('.magazine' ).turn ('is' )) $('.magazine' ).turn ('page' , 1 ); } }); $(window ).resize (function ( ) { resizeViewport (); }).bind ('orientationchange' , function ( ) { resizeViewport (); }); $('.pagination span' ).click (function (event ) { var page; if (event.target ) { if ((page = /p-([0-9]+)/ .exec ($(event.target ).attr ('id' )))) { $('.magazine' ).turn ('page' , page[1 ]); } else { switch ($(event.target ).attr ('id' )) { case "p-first" : $('.magazine' ).turn ('page' , 1 ); break ; case "p-previous" : $('.magazine' ).turn ('previous' ); break ; case "p-next" : $('.magazine' ).turn ('next' ); break ; case "p-last" : $('.magazine' ).turn ('page' , $(".magazine" ).turn ("pages" )); break ; } } } }); if ($.isTouch) { $('.magazine' ).bind ('touchstart' , regionClick); } else { $('.magazine' ).click (regionClick); } $('.next-button' ).bind ($.mouseEvents.over , function ( ) { $(this ).addClass ('next-button-hover' ); }).bind ($.mouseEvents.out , function ( ) { $(this ).removeClass ('next-button-hover' ); }).bind ($.mouseEvents.down , function ( ) { $(this ).addClass ('next-button-down' ); }).bind ($.mouseEvents.up , function ( ) { $(this ).removeClass ('next-button-down' ); }).click (function ( ) { $('.magazine' ).turn ('next' ); }); $('.previous-button' ).bind ($.mouseEvents.over , function ( ) { $(this ).addClass ('previous-button-hover' ); }).bind ($.mouseEvents.out , function ( ) { $(this ).removeClass ('previous-button-hover' ); }).bind ($.mouseEvents.down , function ( ) { $(this ).addClass ('previous-button-down' ); }).bind ($.mouseEvents.up , function ( ) { $(this ).removeClass ('previous-button-down' ); }).click (function ( ) { $('.magazine' ).turn ('previous' ); }); resizeViewport (); $('.magazine' ).addClass ('animated' ); } $('.zoom-icon' ).bind ('mouseover' , function ( ) { if ($(this ).hasClass ('zoom-icon-in' )) $(this ).addClass ('zoom-icon-in-hover' ); if ($(this ).hasClass ('zoom-icon-out' )) $(this ).addClass ('zoom-icon-out-hover' ); }).bind ('mouseout' , function ( ) { if ($(this ).hasClass ('zoom-icon-in' )) $(this ).removeClass ('zoom-icon-in-hover' ); if ($(this ).hasClass ('zoom-icon-out' )) $(this ).removeClass ('zoom-icon-out-hover' ); }).bind ('click' , function ( ) { if ($(this ).hasClass ('zoom-icon-in' )) $('.magazine-viewport' ).zoom ('zoomIn' ); else if ($(this ).hasClass ('zoom-icon-out' )) $('.magazine-viewport' ).zoom ('zoomOut' ); }); $('#canvas' ).hide (); loadApp (); </script > </body > </html >
让我们感谢 ChatGPT 为我们进行的分析:
那么直接看这个 XML 文件 吧:
完整 XML 内容:
完整的 XML 内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 <package unique-identifier ="{7A2A5428-44E4-4734-973E-DF2A4215287B}" > <script /> <metadata > <x-metadata > <meta name ="EbooksysVersion" content ="1.50" /> <meta name ="page_content_size" content ="465 649" /> <meta name ="window_size" content ="768 1024" /> <meta name ="book_background" content ="color:0x878787" /> <meta name ="flipping_sound" content ="yes" /> <meta name ="background_music" content ="yes" /> <meta name ="standard_direction" content ="yes" /> <meta name ="autoflip" content ="no direction:forward atend:repeat interval:3000ms" /> <meta name ="bookmark" content ="type:text autosize:no shape:roundrect 3deffect:yes gradienteffect:yes slope:-8 gap:3" /> </x-metadata > </metadata > <manifest > <item id ="item0" href ="normal/etwx971DL_1.jpg" href2 ="zoomone/etwx971DL_1.jpg" media-type ="image/x-flp" /> <item id ="item1" href ="normal/etwx971DL_2.jpg" href2 ="zoomone/etwx971DL_2.jpg" media-type ="image/x-flp" /> <item id ="item2" href ="normal/etwx971DL_3.jpg" href2 ="zoomone/etwx971DL_3.jpg" media-type ="image/x-flp" /> <item id ="item3" href ="normal/etwx971DL_4.jpg" href2 ="zoomone/etwx971DL_4.jpg" media-type ="image/x-flp" /> <item id ="item4" href ="normal/etwx971DL_5.jpg" href2 ="zoomone/etwx971DL_5.jpg" media-type ="image/x-flp" /> <item id ="item5" href ="normal/etwx971DL_6.jpg" href2 ="zoomone/etwx971DL_6.jpg" media-type ="image/x-flp" /> <item id ="item6" href ="normal/etwx971DL_7.jpg" href2 ="zoomone/etwx971DL_7.jpg" media-type ="image/x-flp" /> <item id ="item7" href ="normal/etwx971DL_8.jpg" href2 ="zoomone/etwx971DL_8.jpg" media-type ="image/x-flp" /> <item id ="item8" href ="normal/etwx971DL_9.jpg" href2 ="zoomone/etwx971DL_9.jpg" media-type ="image/x-flp" /> <item id ="item9" href ="normal/etwx971DL_10.jpg" href2 ="zoomone/etwx971DL_10.jpg" media-type ="image/x-flp" /> <item id ="item10" href ="normal/etwx971DL_11.jpg" href2 ="zoomone/etwx971DL_11.jpg" media-type ="image/x-flp" /> <item id ="item11" href ="normal/etwx971DL_12.jpg" href2 ="zoomone/etwx971DL_12.jpg" media-type ="image/x-flp" /> <item id ="item12" href ="normal/etwx971DL_13.jpg" href2 ="zoomone/etwx971DL_13.jpg" media-type ="image/x-flp" /> <item id ="item13" href ="normal/etwx971DL_14.jpg" href2 ="zoomone/etwx971DL_14.jpg" media-type ="image/x-flp" /> <item id ="item14" href ="normal/etwx971DL_15.jpg" href2 ="zoomone/etwx971DL_15.jpg" media-type ="image/x-flp" /> <item id ="item15" href ="normal/etwx971DL_16.jpg" href2 ="zoomone/etwx971DL_16.jpg" media-type ="image/x-flp" /> <item id ="item16" href ="normal/etwx971DL_17.jpg" href2 ="zoomone/etwx971DL_17.jpg" media-type ="image/x-flp" /> <item id ="item17" href ="normal/etwx971DL_18.jpg" href2 ="zoomone/etwx971DL_18.jpg" media-type ="image/x-flp" /> <item id ="item18" href ="normal/etwx971DL_19.jpg" href2 ="zoomone/etwx971DL_19.jpg" media-type ="image/x-flp" /> <item id ="item19" href ="normal/etwx971DL_20.jpg" href2 ="zoomone/etwx971DL_20.jpg" media-type ="image/x-flp" /> <item id ="item20" href ="normal/etwx971DL_21.jpg" href2 ="zoomone/etwx971DL_21.jpg" media-type ="image/x-flp" /> <item id ="item21" href ="normal/etwx971DL_22.jpg" href2 ="zoomone/etwx971DL_22.jpg" media-type ="image/x-flp" /> <item id ="item22" href ="normal/etwx971DL_23.jpg" href2 ="zoomone/etwx971DL_23.jpg" media-type ="image/x-flp" /> <item id ="item23" href ="normal/etwx971DL_24.jpg" href2 ="zoomone/etwx971DL_24.jpg" media-type ="image/x-flp" /> <item id ="item24" href ="normal/etwx971DL_25.jpg" href2 ="zoomone/etwx971DL_25.jpg" media-type ="image/x-flp" /> <item id ="item25" href ="normal/etwx971DL_26.jpg" href2 ="zoomone/etwx971DL_26.jpg" media-type ="image/x-flp" /> <item id ="item26" href ="normal/etwx971DL_27.jpg" href2 ="zoomone/etwx971DL_27.jpg" media-type ="image/x-flp" /> <item id ="item27" href ="normal/etwx971DL_28.jpg" href2 ="zoomone/etwx971DL_28.jpg" media-type ="image/x-flp" /> <item id ="item28" href ="normal/etwx971DL_29.jpg" href2 ="zoomone/etwx971DL_29.jpg" media-type ="image/x-flp" /> <item id ="item29" href ="normal/etwx971DL_30.jpg" href2 ="zoomone/etwx971DL_30.jpg" media-type ="image/x-flp" /> <item id ="item30" href ="normal/etwx971DL_31.jpg" href2 ="zoomone/etwx971DL_31.jpg" media-type ="image/x-flp" /> <item id ="item31" href ="normal/etwx971DL_32.jpg" href2 ="zoomone/etwx971DL_32.jpg" media-type ="image/x-flp" /> <item id ="item32" href ="normal/etwx971DL_33.jpg" href2 ="zoomone/etwx971DL_33.jpg" media-type ="image/x-flp" /> <item id ="item33" href ="normal/etwx971DL_34.jpg" href2 ="zoomone/etwx971DL_34.jpg" media-type ="image/x-flp" /> <item id ="item34" href ="normal/etwx971DL_35.jpg" href2 ="zoomone/etwx971DL_35.jpg" media-type ="image/x-flp" /> <item id ="item35" href ="normal/etwx971DL_36.jpg" href2 ="zoomone/etwx971DL_36.jpg" media-type ="image/x-flp" /> <item id ="item36" href ="normal/etwx971DL_37.jpg" href2 ="zoomone/etwx971DL_37.jpg" media-type ="image/x-flp" /> <item id ="item37" href ="normal/etwx971DL_38.jpg" href2 ="zoomone/etwx971DL_38.jpg" media-type ="image/x-flp" /> <item id ="item38" href ="normal/etwx971DL_39.jpg" href2 ="zoomone/etwx971DL_39.jpg" media-type ="image/x-flp" /> <item id ="item39" href ="normal/etwx971DL_40.jpg" href2 ="zoomone/etwx971DL_40.jpg" media-type ="image/x-flp" /> <item id ="item40" href ="normal/etwx971DL_41.jpg" href2 ="zoomone/etwx971DL_41.jpg" media-type ="image/x-flp" /> <item id ="item41" href ="normal/etwx971DL_42.jpg" href2 ="zoomone/etwx971DL_42.jpg" media-type ="image/x-flp" /> <item id ="item42" href ="normal/etwx971DL_43.jpg" href2 ="zoomone/etwx971DL_43.jpg" media-type ="image/x-flp" /> <item id ="item43" href ="normal/etwx971DL_44.jpg" href2 ="zoomone/etwx971DL_44.jpg" media-type ="image/x-flp" /> <item id ="item44" href ="normal/etwx971DL_45.jpg" href2 ="zoomone/etwx971DL_45.jpg" media-type ="image/x-flp" /> <item id ="item45" href ="normal/etwx971DL_46.jpg" href2 ="zoomone/etwx971DL_46.jpg" media-type ="image/x-flp" /> <item id ="item46" href ="normal/etwx971DL_47.jpg" href2 ="zoomone/etwx971DL_47.jpg" media-type ="image/x-flp" /> <item id ="item47" href ="normal/etwx971DL_48.jpg" href2 ="zoomone/etwx971DL_48.jpg" media-type ="image/x-flp" /> <item id ="item48" href ="normal/etwx971DL_49.jpg" href2 ="zoomone/etwx971DL_49.jpg" media-type ="image/x-flp" /> <item id ="item49" href ="normal/etwx971DL_50.jpg" href2 ="zoomone/etwx971DL_50.jpg" media-type ="image/x-flp" /> <item id ="item50" href ="normal/etwx971DL_51.jpg" href2 ="zoomone/etwx971DL_51.jpg" media-type ="image/x-flp" /> <item id ="item51" href ="normal/etwx971DL_52.jpg" href2 ="zoomone/etwx971DL_52.jpg" media-type ="image/x-flp" /> <item id ="item52" href ="normal/etwx971DL_53.jpg" href2 ="zoomone/etwx971DL_53.jpg" media-type ="image/x-flp" /> <item id ="item53" href ="normal/etwx971DL_54.jpg" href2 ="zoomone/etwx971DL_54.jpg" media-type ="image/x-flp" /> <item id ="item54" href ="normal/etwx971DL_55.jpg" href2 ="zoomone/etwx971DL_55.jpg" media-type ="image/x-flp" /> <item id ="item55" href ="normal/etwx971DL_56.jpg" href2 ="zoomone/etwx971DL_56.jpg" media-type ="image/x-flp" /> <item id ="item56" href ="normal/etwx971DL_57.jpg" href2 ="zoomone/etwx971DL_57.jpg" media-type ="image/x-flp" /> <item id ="item57" href ="normal/etwx971DL_58.jpg" href2 ="zoomone/etwx971DL_58.jpg" media-type ="image/x-flp" /> <item id ="item58" href ="normal/etwx971DL_59.jpg" href2 ="zoomone/etwx971DL_59.jpg" media-type ="image/x-flp" /> <item id ="item59" href ="normal/etwx971DL_60.jpg" href2 ="zoomone/etwx971DL_60.jpg" media-type ="image/x-flp" /> <item id ="item60" href ="normal/etwx971DL_61.jpg" href2 ="zoomone/etwx971DL_61.jpg" media-type ="image/x-flp" /> <item id ="item61" href ="normal/etwx971DL_62.jpg" href2 ="zoomone/etwx971DL_62.jpg" media-type ="image/x-flp" /> <item id ="item62" href ="normal/etwx971DL_63.jpg" href2 ="zoomone/etwx971DL_63.jpg" media-type ="image/x-flp" /> <item id ="item63" href ="normal/etwx971DL_64.jpg" href2 ="zoomone/etwx971DL_64.jpg" media-type ="image/x-flp" /> <item id ="item64" href ="normal/etwx971DL_65.jpg" href2 ="zoomone/etwx971DL_65.jpg" media-type ="image/x-flp" /> <item id ="item65" href ="normal/etwx971DL_66.jpg" href2 ="zoomone/etwx971DL_66.jpg" media-type ="image/x-flp" /> <item id ="item66" href ="normal/etwx971DL_67.jpg" href2 ="zoomone/etwx971DL_67.jpg" media-type ="image/x-flp" /> <item id ="item67" href ="normal/etwx971DL_68.jpg" href2 ="zoomone/etwx971DL_68.jpg" media-type ="image/x-flp" /> <item id ="item68" href ="normal/etwx971DL_69.jpg" href2 ="zoomone/etwx971DL_69.jpg" media-type ="image/x-flp" /> <item id ="item69" href ="normal/etwx971DL_70.jpg" href2 ="zoomone/etwx971DL_70.jpg" media-type ="image/x-flp" /> <item id ="item70" href ="normal/etwx971DL_71.jpg" href2 ="zoomone/etwx971DL_71.jpg" media-type ="image/x-flp" /> <item id ="item71" href ="normal/etwx971DL_72.jpg" href2 ="zoomone/etwx971DL_72.jpg" media-type ="image/x-flp" /> <item id ="item72" href ="normal/etwx971DL_73.jpg" href2 ="zoomone/etwx971DL_73.jpg" media-type ="image/x-flp" /> <item id ="item73" href ="normal/etwx971DL_74.jpg" href2 ="zoomone/etwx971DL_74.jpg" media-type ="image/x-flp" /> <item id ="item74" href ="normal/etwx971DL_75.jpg" href2 ="zoomone/etwx971DL_75.jpg" media-type ="image/x-flp" /> <item id ="item75" href ="normal/etwx971DL_76.jpg" href2 ="zoomone/etwx971DL_76.jpg" media-type ="image/x-flp" /> <item id ="item76" href ="normal/etwx971DL_77.jpg" href2 ="zoomone/etwx971DL_77.jpg" media-type ="image/x-flp" /> <item id ="item77" href ="normal/etwx971DL_78.jpg" href2 ="zoomone/etwx971DL_78.jpg" media-type ="image/x-flp" /> <item id ="item78" href ="normal/etwx971DL_79.jpg" href2 ="zoomone/etwx971DL_79.jpg" media-type ="image/x-flp" /> <item id ="item79" href ="normal/etwx971DL_80.jpg" href2 ="zoomone/etwx971DL_80.jpg" media-type ="image/x-flp" /> <item id ="item80" href ="normal/etwx971DL_81.jpg" href2 ="zoomone/etwx971DL_81.jpg" media-type ="image/x-flp" /> <item id ="item81" href ="normal/etwx971DL_82.jpg" href2 ="zoomone/etwx971DL_82.jpg" media-type ="image/x-flp" /> <item id ="item82" href ="normal/etwx971DL_83.jpg" href2 ="zoomone/etwx971DL_83.jpg" media-type ="image/x-flp" /> <item id ="item83" href ="normal/etwx971DL_84.jpg" href2 ="zoomone/etwx971DL_84.jpg" media-type ="image/x-flp" /> <item id ="item84" href ="normal/etwx971DL_85.jpg" href2 ="zoomone/etwx971DL_85.jpg" media-type ="image/x-flp" /> <item id ="item85" href ="normal/etwx971DL_86.jpg" href2 ="zoomone/etwx971DL_86.jpg" media-type ="image/x-flp" /> <item id ="item86" href ="normal/etwx971DL_87.jpg" href2 ="zoomone/etwx971DL_87.jpg" media-type ="image/x-flp" /> <item id ="item87" href ="normal/etwx971DL_88.jpg" href2 ="zoomone/etwx971DL_88.jpg" media-type ="image/x-flp" /> <item id ="item88" href ="normal/etwx971DL_89.jpg" href2 ="zoomone/etwx971DL_89.jpg" media-type ="image/x-flp" /> <item id ="item89" href ="normal/etwx971DL_90.jpg" href2 ="zoomone/etwx971DL_90.jpg" media-type ="image/x-flp" /> <item id ="item90" href ="normal/etwx971DL_91.jpg" href2 ="zoomone/etwx971DL_91.jpg" media-type ="image/x-flp" /> <item id ="item91" href ="normal/etwx971DL_92.jpg" href2 ="zoomone/etwx971DL_92.jpg" media-type ="image/x-flp" /> <item id ="item92" href ="normal/etwx971DL_93.jpg" href2 ="zoomone/etwx971DL_93.jpg" media-type ="image/x-flp" /> <item id ="item93" href ="normal/etwx971DL_94.jpg" href2 ="zoomone/etwx971DL_94.jpg" media-type ="image/x-flp" /> <item id ="item94" href ="normal/etwx971DL_95.jpg" href2 ="zoomone/etwx971DL_95.jpg" media-type ="image/x-flp" /> <item id ="item95" href ="normal/etwx971DL_96.jpg" href2 ="zoomone/etwx971DL_96.jpg" media-type ="image/x-flp" /> <item id ="item96" href ="normal/etwx971DL_97.jpg" href2 ="zoomone/etwx971DL_97.jpg" media-type ="image/x-flp" /> <item id ="item97" href ="normal/etwx971DL_98.jpg" href2 ="zoomone/etwx971DL_98.jpg" media-type ="image/x-flp" /> <item id ="item98" href ="normal/etwx971DL_99.jpg" href2 ="zoomone/etwx971DL_99.jpg" media-type ="image/x-flp" /> <item id ="item99" href ="normal/etwx971DL_100.jpg" href2 ="zoomone/etwx971DL_100.jpg" media-type ="image/x-flp" /> <item id ="item100" href ="normal/etwx971DL_101.jpg" href2 ="zoomone/etwx971DL_101.jpg" media-type ="image/x-flp" /> <item id ="item101" href ="normal/etwx971DL_102.jpg" href2 ="zoomone/etwx971DL_102.jpg" media-type ="image/x-flp" /> <item id ="item102" href ="normal/etwx971DL_103.jpg" href2 ="zoomone/etwx971DL_103.jpg" media-type ="image/x-flp" /> <item id ="item103" href ="normal/etwx971DL_104.jpg" href2 ="zoomone/etwx971DL_104.jpg" media-type ="image/x-flp" /> <item id ="item104" href ="normal/etwx971DL_105.jpg" href2 ="zoomone/etwx971DL_105.jpg" media-type ="image/x-flp" /> <item id ="item105" href ="normal/etwx971DL_106.jpg" href2 ="zoomone/etwx971DL_106.jpg" media-type ="image/x-flp" /> <item id ="item106" href ="normal/etwx971DL_107.jpg" href2 ="zoomone/etwx971DL_107.jpg" media-type ="image/x-flp" /> <item id ="item107" href ="normal/etwx971DL_108.jpg" href2 ="zoomone/etwx971DL_108.jpg" media-type ="image/x-flp" /> <item id ="item108" href ="normal/etwx971DL_109.jpg" href2 ="zoomone/etwx971DL_109.jpg" media-type ="image/x-flp" /> <item id ="item109" href ="normal/etwx971DL_110.jpg" href2 ="zoomone/etwx971DL_110.jpg" media-type ="image/x-flp" /> <item id ="item110" href ="normal/etwx971DL_111.jpg" href2 ="zoomone/etwx971DL_111.jpg" media-type ="image/x-flp" /> <item id ="item111" href ="normal/etwx971DL_112.jpg" href2 ="zoomone/etwx971DL_112.jpg" media-type ="image/x-flp" /> <item id ="item112" href ="normal/etwx971DL_113.jpg" href2 ="zoomone/etwx971DL_113.jpg" media-type ="image/x-flp" /> <item id ="item113" href ="normal/etwx971DL_114.jpg" href2 ="zoomone/etwx971DL_114.jpg" media-type ="image/x-flp" /> <item id ="item114" href ="normal/etwx971DL_115.jpg" href2 ="zoomone/etwx971DL_115.jpg" media-type ="image/x-flp" /> <item id ="item115" href ="normal/etwx971DL_116.jpg" href2 ="zoomone/etwx971DL_116.jpg" media-type ="image/x-flp" /> <item id ="item116" href ="normal/etwx971DL_117.jpg" href2 ="zoomone/etwx971DL_117.jpg" media-type ="image/x-flp" /> <item id ="item117" href ="normal/etwx971DL_118.jpg" href2 ="zoomone/etwx971DL_118.jpg" media-type ="image/x-flp" /> <item id ="item118" href ="normal/etwx971DL_119.jpg" href2 ="zoomone/etwx971DL_119.jpg" media-type ="image/x-flp" /> <item id ="item119" href ="normal/etwx971DL_120.jpg" href2 ="zoomone/etwx971DL_120.jpg" media-type ="image/x-flp" /> <item id ="item120" href ="normal/etwx971DL_121.jpg" href2 ="zoomone/etwx971DL_121.jpg" media-type ="image/x-flp" /> <item id ="item121" href ="normal/etwx971DL_122.jpg" href2 ="zoomone/etwx971DL_122.jpg" media-type ="image/x-flp" /> <item id ="item122" href ="normal/etwx971DL_123.jpg" href2 ="zoomone/etwx971DL_123.jpg" media-type ="image/x-flp" /> <item id ="item123" href ="normal/etwx971DL_124.jpg" href2 ="zoomone/etwx971DL_124.jpg" media-type ="image/x-flp" /> <item id ="item124" href ="normal/etwx971DL_125.jpg" href2 ="zoomone/etwx971DL_125.jpg" media-type ="image/x-flp" /> <item id ="item125" href ="normal/etwx971DL_126.jpg" href2 ="zoomone/etwx971DL_126.jpg" media-type ="image/x-flp" /> </manifest > <spine > <itemref idref ="item0" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_1.jpg" shadow ="no" numberingdescription ="1" /> <itemref idref ="item1" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_2.jpg" numberingdescription ="2" /> <itemref idref ="item2" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_3.jpg" numberingdescription ="3" /> <itemref idref ="item3" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_4.jpg" numberingdescription ="4" /> <itemref idref ="item4" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_5.jpg" numberingdescription ="5" /> <itemref idref ="item5" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_6.jpg" numberingdescription ="6" /> <itemref idref ="item6" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_7.jpg" numberingdescription ="7" /> <itemref idref ="item7" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_8.jpg" numberingdescription ="8" /> <itemref idref ="item8" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_9.jpg" numberingdescription ="9" /> <itemref idref ="item9" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_10.jpg" numberingdescription ="10" /> <itemref idref ="item10" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_11.jpg" numberingdescription ="11" /> <itemref idref ="item11" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_12.jpg" numberingdescription ="12" /> <itemref idref ="item12" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_13.jpg" numberingdescription ="13" /> <itemref idref ="item13" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_14.jpg" numberingdescription ="14" /> <itemref idref ="item14" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_15.jpg" numberingdescription ="15" /> <itemref idref ="item15" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_16.jpg" numberingdescription ="16" /> <itemref idref ="item16" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_17.jpg" numberingdescription ="17" /> <itemref idref ="item17" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_18.jpg" numberingdescription ="18" /> <itemref idref ="item18" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_19.jpg" numberingdescription ="19" /> <itemref idref ="item19" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_20.jpg" numberingdescription ="20" /> <itemref idref ="item20" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_21.jpg" numberingdescription ="21" /> <itemref idref ="item21" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_22.jpg" numberingdescription ="22" /> <itemref idref ="item22" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_23.jpg" numberingdescription ="23" /> <itemref idref ="item23" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_24.jpg" numberingdescription ="24" /> <itemref idref ="item24" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_25.jpg" numberingdescription ="25" /> <itemref idref ="item25" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_26.jpg" numberingdescription ="26" /> <itemref idref ="item26" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_27.jpg" numberingdescription ="27" /> <itemref idref ="item27" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_28.jpg" numberingdescription ="28" /> <itemref idref ="item28" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_29.jpg" numberingdescription ="29" /> <itemref idref ="item29" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_30.jpg" numberingdescription ="30" /> <itemref idref ="item30" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_31.jpg" numberingdescription ="31" /> <itemref idref ="item31" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_32.jpg" numberingdescription ="32" /> <itemref idref ="item32" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_33.jpg" numberingdescription ="33" /> <itemref idref ="item33" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_34.jpg" numberingdescription ="34" /> <itemref idref ="item34" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_35.jpg" numberingdescription ="35" /> <itemref idref ="item35" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_36.jpg" numberingdescription ="36" /> <itemref idref ="item36" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_37.jpg" numberingdescription ="37" /> <itemref idref ="item37" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_38.jpg" numberingdescription ="38" /> <itemref idref ="item38" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_39.jpg" numberingdescription ="39" /> <itemref idref ="item39" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_40.jpg" numberingdescription ="40" /> <itemref idref ="item40" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_41.jpg" numberingdescription ="41" /> <itemref idref ="item41" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_42.jpg" numberingdescription ="42" /> <itemref idref ="item42" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_43.jpg" numberingdescription ="43" /> <itemref idref ="item43" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_44.jpg" numberingdescription ="44" /> <itemref idref ="item44" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_45.jpg" numberingdescription ="45" /> <itemref idref ="item45" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_46.jpg" numberingdescription ="46" /> <itemref idref ="item46" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_47.jpg" numberingdescription ="47" /> <itemref idref ="item47" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_48.jpg" numberingdescription ="48" /> <itemref idref ="item48" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_49.jpg" numberingdescription ="49" /> <itemref idref ="item49" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_50.jpg" numberingdescription ="50" /> <itemref idref ="item50" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_51.jpg" numberingdescription ="51" /> <itemref idref ="item51" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_52.jpg" numberingdescription ="52" /> <itemref idref ="item52" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_53.jpg" numberingdescription ="53" /> <itemref idref ="item53" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_54.jpg" numberingdescription ="54" /> <itemref idref ="item54" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_55.jpg" numberingdescription ="55" /> <itemref idref ="item55" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_56.jpg" numberingdescription ="56" /> <itemref idref ="item56" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_57.jpg" numberingdescription ="57" /> <itemref idref ="item57" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_58.jpg" numberingdescription ="58" /> <itemref idref ="item58" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_59.jpg" numberingdescription ="59" /> <itemref idref ="item59" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_60.jpg" numberingdescription ="60" /> <itemref idref ="item60" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_61.jpg" numberingdescription ="61" /> <itemref idref ="item61" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_62.jpg" numberingdescription ="62" /> <itemref idref ="item62" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_63.jpg" numberingdescription ="63" /> <itemref idref ="item63" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_64.jpg" numberingdescription ="64" /> <itemref idref ="item64" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_65.jpg" numberingdescription ="65" /> <itemref idref ="item65" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_66.jpg" numberingdescription ="66" /> <itemref idref ="item66" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_67.jpg" numberingdescription ="67" /> <itemref idref ="item67" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_68.jpg" numberingdescription ="68" /> <itemref idref ="item68" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_69.jpg" numberingdescription ="69" /> <itemref idref ="item69" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_70.jpg" numberingdescription ="70" /> <itemref idref ="item70" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_71.jpg" numberingdescription ="71" /> <itemref idref ="item71" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_72.jpg" numberingdescription ="72" /> <itemref idref ="item72" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_73.jpg" numberingdescription ="73" /> <itemref idref ="item73" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_74.jpg" numberingdescription ="74" /> <itemref idref ="item74" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_75.jpg" numberingdescription ="75" /> <itemref idref ="item75" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_76.jpg" numberingdescription ="76" /> <itemref idref ="item76" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_77.jpg" numberingdescription ="77" /> <itemref idref ="item77" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_78.jpg" numberingdescription ="78" /> <itemref idref ="item78" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_79.jpg" numberingdescription ="79" /> <itemref idref ="item79" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_80.jpg" numberingdescription ="80" /> <itemref idref ="item80" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_81.jpg" numberingdescription ="81" /> <itemref idref ="item81" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_82.jpg" numberingdescription ="82" /> <itemref idref ="item82" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_83.jpg" numberingdescription ="83" /> <itemref idref ="item83" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_84.jpg" numberingdescription ="84" /> <itemref idref ="item84" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_85.jpg" numberingdescription ="85" /> <itemref idref ="item85" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_86.jpg" numberingdescription ="86" /> <itemref idref ="item86" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_87.jpg" numberingdescription ="87" /> <itemref idref ="item87" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_88.jpg" numberingdescription ="88" /> <itemref idref ="item88" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_89.jpg" numberingdescription ="89" /> <itemref idref ="item89" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_90.jpg" numberingdescription ="90" /> <itemref idref ="item90" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_91.jpg" numberingdescription ="91" /> <itemref idref ="item91" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_92.jpg" numberingdescription ="92" /> <itemref idref ="item92" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_93.jpg" numberingdescription ="93" /> <itemref idref ="item93" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_94.jpg" numberingdescription ="94" /> <itemref idref ="item94" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_95.jpg" numberingdescription ="95" /> <itemref idref ="item95" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_96.jpg" numberingdescription ="96" /> <itemref idref ="item96" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_97.jpg" numberingdescription ="97" /> <itemref idref ="item97" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_98.jpg" numberingdescription ="98" /> <itemref idref ="item98" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_99.jpg" numberingdescription ="99" /> <itemref idref ="item99" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_100.jpg" numberingdescription ="100" /> <itemref idref ="item100" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_101.jpg" numberingdescription ="101" /> <itemref idref ="item101" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_102.jpg" numberingdescription ="102" /> <itemref idref ="item102" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_103.jpg" numberingdescription ="103" /> <itemref idref ="item103" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_104.jpg" numberingdescription ="104" /> <itemref idref ="item104" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_105.jpg" numberingdescription ="105" /> <itemref idref ="item105" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_106.jpg" numberingdescription ="106" /> <itemref idref ="item106" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_107.jpg" numberingdescription ="107" /> <itemref idref ="item107" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_108.jpg" numberingdescription ="108" /> <itemref idref ="item108" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_109.jpg" numberingdescription ="109" /> <itemref idref ="item109" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_110.jpg" numberingdescription ="110" /> <itemref idref ="item110" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_111.jpg" numberingdescription ="111" /> <itemref idref ="item111" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_112.jpg" numberingdescription ="112" /> <itemref idref ="item112" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_113.jpg" numberingdescription ="113" /> <itemref idref ="item113" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_114.jpg" numberingdescription ="114" /> <itemref idref ="item114" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_115.jpg" numberingdescription ="115" /> <itemref idref ="item115" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_116.jpg" numberingdescription ="116" /> <itemref idref ="item116" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_117.jpg" numberingdescription ="117" /> <itemref idref ="item117" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_118.jpg" numberingdescription ="118" /> <itemref idref ="item118" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_119.jpg" numberingdescription ="119" /> <itemref idref ="item119" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_120.jpg" numberingdescription ="120" /> <itemref idref ="item120" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_121.jpg" numberingdescription ="121" /> <itemref idref ="item121" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_122.jpg" numberingdescription ="122" /> <itemref idref ="item122" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_123.jpg" numberingdescription ="123" /> <itemref idref ="item123" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_124.jpg" numberingdescription ="124" /> <itemref idref ="item124" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_125.jpg" numberingdescription ="125" /> <itemref idref ="item125" thumbnail ="/resources/_thumbnail_/tn_etwx971DL_126.jpg" shadow ="no" numberingdescription ="126" /> </spine > <authoring name ="FlipViewer Xpress Creator" version ="3.6.0.305" time ="2021-12-30-17-03-GMT8" nogui ="yes" /> <drm_enabled version ="4.0.0" bookid ="8add12c734dc2dc000d69ee2f5772d26" authorid ="ccppg" > <publishing title ="etwx971DL" id ="" > <issue title ="etwx971DL" id ="8add12c734dc2dc000d69ee2f5772d26" /> </publishing > <publisher name ="CCPPG-Et9t8SWt" /> <additionalcontrol signature ="4947e3b6d8febbed5aa71ed1644f3e97" /> <modifiedby name ="FlipViewer Xpress Creator" version ="3.6.0.305" time ="2021-12-30-17-03-GMT8" nogui ="yes" /> <certificate type ="2" url ="etwx971DL_license__license_.xml" alternative ="http://asp16.digitalflip.com/drm2/getCert" trial ="no" /> <searchabletext url ="etwx971DL_text_.xml" /> <linkdef url ="etwx971DL_linkdef_.xml" /> <coverimage url ="etwx971DL_cover_.jpg" /> <archive url ="etwx971DL_archive_.xml" /> <customized allow ="yes" > <ibutton show ="no" click ="http://www.flipviewer.com" /> <aboutbox allow ="yes" title ="etwx971DL" url ="http://www.ccppg.com.cn/" company ="中国少年儿童新闻出版总社." logo ="" author ="" tel ="010-57526117" address ="北京市建国门外大街丙12号(100022) ." email ="61read@ccppg.com.cn" /> <fliptopage show ="yes" /> <fliptocover show ="yes" /> <bookmark show ="yes" /> <search show ="yes" /> <help show ="yes" detailhelphidden ="yes" /> <branding show ="no" url ="http://www.flipviewer.com" logo ="" /> <shadowmargin show ="yes" /> <flipindicator show ="yes" /> <sendemail show ="no" /> <socialnetwork show ="yes" /> <printsinglepageonly show ="no" /> <thumbnail show ="yes" /> <pagetips show ="yes" /> <toolbartips show ="yes" /> <pagedescription show ="no" external ="" /> <note show ="yes" /> <winframe show ="yes" /> <close show ="no" /> <toolbar show ="yes" position ="7" color ="8882055" noshadow ="no" /> <sidebar show ="yes" position ="9" /> <saveas show ="no" /> <highlight show ="yes" style ="translucent" /> <copytext show ="no" /> <settings show ="yes" /> <autoflipping show ="yes" /> <watermark show ="no" /> <zoompage show ="yes" factor ="150" rubberband ="no" dblclick ="yes" /> <archive show ="no" /> <view_mode show ="yes" content ="landscape" fit2height ="yes" /> </customized > <digital_signature content ="50f39a0f434ac85fddb45f3b2dc5a593" checksum ="593553436E6D78575A446D70796F59532F56734E48644139794D504A652F48474270436F6B2B6B656E64633D" /> </drm_enabled > </package >
在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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <package unique-identifier ="{2FFC6E0F-48D2-45B3-A5B3-4706235FE0B1}" > <script /> <certificate version ="4.0.0" id ="8add12c734dc2dc000d69ee2f5772d26" authorid ="ccppg" > <security encryption ="yes" password ="yes" > <flipbook id ="8add12c734dc2dc000d69ee2f5772d26" > <signature content ="5acbea65386e418ec804050cd5a55211" /> <integrity content ="2c29c55815b10f67a4ef999297952490" /> </flipbook > <encryption content ="63534D4B6D5462467654694247654D3451564C3652624D667777355736374D79657655715A6B37633464343D" /> <password content ="2B644E7238513D3D" /> <constraints > <print printmaxpagenumber ="10" /> </constraints > </security > <rights > <permissions > <pdf allow ="yes" /> <embed allow ="yes" /> <display allow ="yes" /> <enlarge allow ="yes" /> <print allow ="no" /> <copytext allow ="no" /> <download allow ="yes" /> </permissions > </rights > </certificate > <certificate_digital_signature > <signature content ="a80a70215d901311939fe4765a66259d" checksum ="9732dd58833d52787b77b6073b046a39" /> </certificate_digital_signature > </package >
同时还包括了每一页的下载地址:
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 2 3 4 http://202.96.31.36:8888/fliphtml5/password/main/qikan/etwx/2021/12/971/web/html5/tablet/etwx971DL.xml http://202.96.31.36:8888/fliphtml5/password/main/qikan/etwx/2021/12/972/web/html5/tablet/etwx972DL.xml http://202.96.31.36:8888/fliphtml5/password/main/qikan/etwx/2021/12/973/web/html5/tablet/etwx973SL.xml http://202.96.31.36:8888/fliphtml5/password/main/qikan/etwx/2021/12/974/web/html5/tablet/etwx974SL.xml
通过这样的方式可以直接获取到几乎所有的 XML 文件,对于特殊的月份,需要单独进行处理,手动打开页面获取地址即可。
随后就是下载图片和合成,这里通过 Python 快速实现,虽然程序是中少版的,但国图版其实是一致的,只需要修改一下地址即可。
Python 程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import requestsfrom bs4 import BeautifulSoupimport osnowurl = "http://hangzhoushaoer.61read.com/open/main/qikan/etwx/2023/12/1070/web/html5/tablet/etwx1070SL.xml" dirname = nowurl.split('/' )[-1 ].replace('.xml' , '' ) os.makedirs(dirname, exist_ok=True ) res = requests.get(nowurl) if res.status_code == 404 : res = requests.get(nowurl.replace('SL.xml' , 'DL.xml' )) res.encoding = 'utf-8' soup = BeautifulSoup(res.text, 'lxml' ) baseurls = nowurl.split('/' )[:-1 ] imgurls = [('/' .join(baseurls) + '/' + item['href2' ]) for item in soup.find_all('item' , {'media-type' : 'image/x-flp' })] for imgurl in imgurls: imgname = imgurl.split('/' )[-1 ].split('_' )[-1 ] if imgname[0 ] == 'r' or imgname[0 ] == 'l' : imgname = imgurl.split('/' )[-1 ].split('_' )[-2 ] + imgname imgname = imgname.zfill(8 ) imgres = requests.get(imgurl) with open (dirname + '/' + imgname, 'wb' ) as f: f.write(imgres.content) from PIL import Imageimgnames = os.listdir(dirname) imgnames.sort() imgnames = [dirname + '/' + imgname for imgname in imgnames] img = Image.open (imgnames[0 ]) img.save(dirname + '.pdf' , "PDF" , resolution=100.0 , save_all=True , append_images=[Image.open (imgname) for imgname in imgnames[1 :]])
这样就可以直接下载并合成 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 2 3 4 5 6 7 8 private function onPageDataLoaded (objectByte:ByteArray, url:String, success:Boolean) : void{ if (FVDRMCertificate.getInstance().encrypted) { objectByte = FVUtil.encryptStream(objectByte,FVDRMCertificate.getInstance().encryptionKey,false ); } this ._loader.loadByteObject(objectByte,url,this .onPageObjectLoaded,this ); }
跟随找到 fv.common.FVUtil.as 中的 encryptStream:
encryptStream
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static function encryptStream (inArray:ByteArray, key:String, isEncryption:Boolean) : ByteArray{ var outArray:ByteArray = null ; var i:int = 0 ; var len:int = 1024 * 8 ; if (inArray.length <= len) { return FVCipher.getInstance().encryptStream(inArray,key,inArray.length,isEncryption); } outArray = FVCipher.getInstance().encryptStream(inArray,key,len,isEncryption); for (i = len; i <= inArray.length; i++) { outArray[i] = inArray[i]; } return outArray; }
这里发现定义了一个 10248 的长度,如果小于这个长度,就直接加密,否则只加密前 1024 8 长度的内容,也就是说,只需要对前 8k 长度的内容进行解密即可。
跟随加密方式找到了 fv.common.FVCipher.as 中的加密方式:
encryptStream
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public function encryptStream (strm:ByteArray, encryptKey:String, bufSize:int, isEncryption:Boolean) : ByteArray{ if (strm.length == 0 ) { return strm; } var szKey:ByteArray = new ByteArray(); szKey.writeUTFBytes(encryptKey); var cipherMgr:FVCipherMgr = FVCipherMgr.getInstance(); cipherMgr.initKey(szKey,encryptKey.length); var outStrm:ByteArray = new ByteArray(); if (isEncryption) { cipherMgr.encodeBuffer(strm,outStrm,bufSize); } else { cipherMgr.decodeBuffer(strm,outStrm,bufSize); } return outStrm; }
这里的加密方式是通过 FVCipherMgr 进行的,这里的加密方式是对称加密,通过 szKey 进行初始化,然后对 strm 进行加密,继续分析 encodeBuffer:
encodeBuffer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public function encodeBuffer (src:ByteArray, dest:ByteArray, dataSize:int) : void{ var i:int = 0 ; var tempBlock:ByteArray = null ; var point:int = 0 ; var plainTextBlock:ByteArray = new ByteArray(); plainTextBlock.length = this .SAFER_BLOCK_LEN; var offset:int = 0 ; var arrayPoint:int = 0 ; var safer:FVSafer = FVSafer.getInstance(); while (dataSize >= this .SAFER_BLOCK_LEN) { for (i = 0 ; i < this .SAFER_BLOCK_LEN; i++) { plainTextBlock[i] = src[arrayPoint + i] ^ this ._lastBlock[i]; } safer.saferEncryptBlock(plainTextBlock,this ._encryptKey,dest,arrayPoint); for (i = 0 ; i < this .SAFER_BLOCK_LEN; i++) { this ._lastBlock[i] ^= dest[arrayPoint + i]; } arrayPoint += this .SAFER_BLOCK_LEN; dataSize -= this .SAFER_BLOCK_LEN; } if (dataSize > 0 ) { tempBlock = new ByteArray(); tempBlock.length = this .SAFER_BLOCK_LEN; point = 0 ; safer.saferEncryptBlock(this ._lastBlock,this ._encryptKey,tempBlock,point); for (i = 0 ; i < dataSize; i++) { dest[arrayPoint + i] = src[arrayPoint + i] ^ tempBlock[i]; } for (i = 0 ; i < this .SAFER_BLOCK_LEN; i++) { this ._lastBlock[i] ^= tempBlock[i]; } } }
这里的加密方式是 SAFER 算法,通过搜索引擎并没有找到相关的解密方式,需要进一步分析,通过 FVSafer.as 找到了 SAFER 算法的实现,
saferEncryptBlock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 public function saferEncryptBlock (block_in:ByteArray, key:ByteArray, block_out:ByteArray, arrayPoint:int) : void{ var a:uint = 0 ; var b:uint = 0 ; var c:uint = 0 ; var d:uint = 0 ; var e:uint = 0 ; var f:uint = 0 ; var g:uint = 0 ; var h:uint = 0 ; var t:uint = 0 ; var round:uint = 0 ; var keyPoint:int = 0 ; a = uint(block_in[0 ]); b = uint(block_in[1 ]); c = uint(block_in[2 ]); d = uint(block_in[3 ]); e = uint(block_in[4 ]); f = uint(block_in[5 ]); g = uint(block_in[6 ]); h = uint(block_in[7 ]); if (this .SAFER_MAX_NOF_ROUNDS < (round = uint(key[0 ]))) { round = uint(this .SAFER_MAX_NOF_ROUNDS); } while (round--) { a ^= key[++keyPoint]; b += key[++keyPoint]; c += key[++keyPoint]; d ^= key[++keyPoint]; e ^= key[++keyPoint]; f += key[++keyPoint]; g += key[++keyPoint]; h ^= key[++keyPoint]; a = this ._exp_tab[a & 255 ] + key[++keyPoint]; b = uint(this ._log_tab[b & 255 ] ^ key[++keyPoint]); c = uint(this ._log_tab[c & 255 ] ^ key[++keyPoint]); d = this ._exp_tab[d & 255 ] + key[++keyPoint]; e = this ._exp_tab[e & 255 ] + key[++keyPoint]; f = uint(this ._log_tab[f & 255 ] ^ key[++keyPoint]); g = uint(this ._log_tab[g & 255 ] ^ key[++keyPoint]); h = this ._exp_tab[h & 255 ] + key[++keyPoint]; b += a; a += b; d += c; c += d; f += e; e += f; h += g; g += h; c += a; a += c; g += e; e += g; d += b; b += d; h += f; f += h; e += a; a += e; f += b; b += f; g += c; c += g; h += d; d += h; t = b; b = e; e = c; c = t; t = d; d = f; f = g; g = t; } a ^= key[++keyPoint]; b += key[++keyPoint]; c += key[++keyPoint]; d ^= key[++keyPoint]; e ^= key[++keyPoint]; f += key[++keyPoint]; g += key[++keyPoint]; h ^= key[++keyPoint]; block_out[arrayPoint + 0 ] = a & 255 ; block_out[arrayPoint + 1 ] = b & 255 ; block_out[arrayPoint + 2 ] = c & 255 ; block_out[arrayPoint + 3 ] = d & 255 ; block_out[arrayPoint + 4 ] = e & 255 ; block_out[arrayPoint + 5 ] = f & 255 ; block_out[arrayPoint + 6 ] = g & 255 ; block_out[arrayPoint + 7 ] = h & 255 ; }
同时找到了解密函数:
saferDecryptBlock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 public function saferDecryptBlock (block_in:ByteArray, key:ByteArray, block_out:ByteArray, arrayPoint:int) : void{ var a:uint = 0 ; var b:uint = 0 ; var c:uint = 0 ; var d:uint = 0 ; var e:uint = 0 ; var f:uint = 0 ; var g:uint = 0 ; var h:uint = 0 ; var t:uint = 0 ; var round:uint = 0 ; var keyPoint:int = 0 ; a = uint(block_in[0 ]); b = uint(block_in[1 ]); c = uint(block_in[2 ]); d = uint(block_in[3 ]); e = uint(block_in[4 ]); f = uint(block_in[5 ]); g = uint(block_in[6 ]); h = uint(block_in[7 ]); if (this .SAFER_MAX_NOF_ROUNDS < (round = uint(key[0 ]))) { round = uint(this .SAFER_MAX_NOF_ROUNDS); } keyPoint += this .SAFER_BLOCK_LEN * (1 + 2 * round); h ^= key[keyPoint]; g -= key[--keyPoint]; f -= key[--keyPoint]; e ^= key[--keyPoint]; d ^= key[--keyPoint]; c -= key[--keyPoint]; b -= key[--keyPoint]; a ^= key[--keyPoint]; while (round--) { t = e; e = b; b = c; c = t; t = f; f = d; d = g; g = t; a -= e; e -= a; b -= f; f -= b; c -= g; g -= c; d -= h; h -= d; a -= c; c -= a; e -= g; g -= e; b -= d; d -= b; f -= h; h -= f; a -= b; b -= a; c -= d; d -= c; e -= f; f -= e; g -= h; h -= g; h -= key[--keyPoint]; g ^= key[--keyPoint]; f ^= key[--keyPoint]; e -= key[--keyPoint]; d -= key[--keyPoint]; c ^= key[--keyPoint]; b ^= key[--keyPoint]; a -= key[--keyPoint]; h = uint(this ._log_tab[h & 255 ] ^ key[--keyPoint]); g = this ._exp_tab[g & 255 ] - key[--keyPoint]; f = this ._exp_tab[f & 255 ] - key[--keyPoint]; e = uint(this ._log_tab[e & 255 ] ^ key[--keyPoint]); d = uint(this ._log_tab[d & 255 ] ^ key[--keyPoint]); c = this ._exp_tab[c & 255 ] - key[--keyPoint]; b = this ._exp_tab[b & 255 ] - key[--keyPoint]; a = uint(this ._log_tab[a & 255 ] ^ key[--keyPoint]); } block_out[arrayPoint + 0 ] = a & 255 ; block_out[arrayPoint + 1 ] = b & 255 ; block_out[arrayPoint + 2 ] = c & 255 ; block_out[arrayPoint + 3 ] = d & 255 ; block_out[arrayPoint + 4 ] = e & 255 ; block_out[arrayPoint + 5 ] = f & 255 ; block_out[arrayPoint + 6 ] = g & 255 ; block_out[arrayPoint + 7 ] = h & 255 ; }
只需要实现这个解密函数,就可以对前 8k 的内容进行解密,就可以对文件进行解密,同时解密密钥是在 FVDRMCertificate 中的 encryptionKey 中,但和 license 文件中的 encryption 略微不同:
在加载 license 函数中有如下内容:
parseLicense
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public function parseLicense (licenseContents:String) : void{ this ._licenseContents = licenseContents; var licenseParser:XMLDocument = new XMLDocument(); licenseParser.ignoreWhite = true ; licenseParser.parseXML(this ._licenseContents); var packageNode:XMLNode = FVUtil.childNodeForName("package" ,licenseParser); var certificateNode:XMLNode = FVUtil.childNodeForName("certificate" ,packageNode); this .parseCertificateSession(certificateNode); var signatureNode:XMLNode = FVUtil.childNodeForName("certificate_digital_signature.signature" ,packageNode); this ._certificateSignature = signatureNode.attributes.content; if (this ._embeddedLicense) { if (this ._encryptionKey != "" ) { this ._encryptionKey = FVUtil.encryptString(this ._encryptionKey,FVConstants.constEncryptKeyCDViewer,false ,true ); } } else if (this ._encryptionKey != "" && this ._nodeId != "" ) { if (FVBookOpenTool.getInstance().license != "" ) { this ._encryptionKey = FVUtil.encryptString(this ._encryptionKey,this ._nodeId,false ,true ); } else { this ._encryptionKey = FVUtil.encryptString(this ._encryptionKey,FVUtil.encryptString(this ._nodeId,FVConstants.constKeyForHttp,false ,true ),false ,true ); } } var bookinterity:Boolean = this ._bookSignature == this ._tempBookSignature || FVConstants.DEBUG_MODE; if (this ._bookId != this ._tempId || !bookinterity || !this .checkIntegrityOfLicense(this ._licenseContents) && !FVConstants.DEBUG_MODE) { this ._errorCode = FVLocal.LICENSE_TAMPERED_ERROR; this ._callback.apply(this ._caller); } else { this ._errorCode = 0 ; this ._currentTime = new Date(); if (this ._embeddedLicense && FVBook.getInstance().certInfo.trial || !this ._embeddedLicense && (this .getNeedCheckServerTime() || this ._checkLicenseCondition != "" )) { this .getServerTime(); } else { this .getLicenseValidity(); } } }
这里的 encryptionKey 是通过 encryptString 进行加密的,同时还包括了一个 constEncryptKeyCDViewer,这个值是在 FVConstants 中定义的,这个值是一个固定的字符串,可以直接通过反编译找到:
1 public static const constEncryptKeyCDViewer:* = "0b8b6a4650b148a1975331bc2da63f93" ;
再次跟随找到字符串的加密函数:
encryptString
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public function encryptString (str:String, encryptKey:String, isEncryption:Boolean, isHex:Boolean) : String{ var i:int = 0 ; var outStr2:ByteArray = null ; var outStr3:ByteArray = null ; if (str == "" || str == null ) { return str; } var outStr:ByteArray = new ByteArray(); var keyLen:int = encryptKey.length; var szKey:ByteArray = new ByteArray(); szKey.writeUTFBytes(encryptKey); var cipherMgr:FVCipherMgr = FVCipherMgr.getInstance(); cipherMgr.initKey(szKey,keyLen); var outPut:String = "" ; var strBuffer:ByteArray = new ByteArray(); if (isEncryption) { strBuffer.writeUTFBytes(str); cipherMgr.encodeString(strBuffer,outStr,strBuffer.length); outStr2 = new ByteArray(); for (i = 0 ; i < outStr.length; i++) { if (outStr[i] == "0" ) { break ; } outStr2[i] = outStr[i]; } if (isHex) { outPut = this .normalToHexString(outStr2); } else { outPut = outStr2.toString(); } } else { if (isHex) { str = this .hexToNormalString(str); } strBuffer.writeUTFBytes(str); cipherMgr.decodeString(strBuffer,outStr,strBuffer.length); outStr3 = new ByteArray(); for (i = 0 ; i < outStr.length; i++) { if (outStr[i] == "0" ) { break ; } outStr3[i] = outStr[i]; } outPut = outStr3.toString(); } return outPut; }
此外还有一些函数:
encodeString,decodeString
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public function encodeString (src:ByteArray, dest:ByteArray, dataSize:int) : void{ var outPutBuffer:ByteArray = new ByteArray(); outPutBuffer.length = dataSize; this .encodeBuffer(src,outPutBuffer,dataSize); FVBase64.getInstance().base64Encode(outPutBuffer,dest,dataSize); } public function decodeString (src:ByteArray, dest:ByteArray, dataSize:int) : void{ var nOutPutBytes:int = nOutPutBytes = dataSize / 4 * 3 ; if (src[dataSize - 1 ] == 61 ) { nOutPutBytes -= src[dataSize - 2 ] == 61 ? 2 : 1 ; } var outPutBuffer:ByteArray = new ByteArray(); outPutBuffer.length = nOutPutBytes + 5 ; FVBase64.getInstance().base64Decode(src,outPutBuffer,dataSize); this .decodeBuffer(outPutBuffer,dest,nOutPutBytes); dest[nOutPutBytes] = 0 ; }
初始化密钥的函数:
initKey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function initKey (inputStr:ByteArray, nKeyBytes:int) : void{ for (var i:int = 0 ; i < this .SAFER_BLOCK_LEN; i++) { this ._lastBlock[i] = 255 ; } var MDBuf:ByteArray = new ByteArray(); MDBuf.length = this .MAX_HASH_RESULT_LEN; FVRipeMD.getInstance().ripeMD256Hash(inputStr,nKeyBytes,MDBuf); var point:int = 0 ; var safer:FVSafer = FVSafer.getInstance(); safer.saferExpandUserkey(MDBuf,MDBuf,this .SAFER_BLOCK_LEN,10 ,1 ,this ._encryptKey); safer.saferEncryptBlock(this ._lastBlock,this ._encryptKey,this ._lastBlock,point); }
saferEncryptBlock 函数前面已经实现了,继续找到的 saferExpandUserkey 函数:
saferExpandUserkey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public function saferExpandUserkey (userkey_1:ByteArray, userkey_2:ByteArray, key2Start:int, nof_rounds:uint, strengthened:int, key:ByteArray) : void{ var i:uint = 0 ; var j:uint = 0 ; var keyPoint:int = 0 ; var ka:ByteArray = new ByteArray(); var kb:ByteArray = new ByteArray(); ka.length = this .SAFER_BLOCK_LEN + 1 ; kb.length = this .SAFER_BLOCK_LEN + 1 ; if (this .SAFER_MAX_NOF_ROUNDS < nof_rounds) { nof_rounds = uint(this .SAFER_MAX_NOF_ROUNDS); } key[0 ] = nof_rounds; keyPoint = 1 ; ka[this .SAFER_BLOCK_LEN] = 0 ; kb[this .SAFER_BLOCK_LEN] = 0 ; for (j = 0 ; j < this .SAFER_BLOCK_LEN; j++) { ka[this .SAFER_BLOCK_LEN] ^= ka[j] = this .ROL(userkey_1[j],5 ); kb[this .SAFER_BLOCK_LEN] ^= kb[j] = key[keyPoint + j] = userkey_2[j + key2Start]; } keyPoint = j + 1 ; for (i = 1 ; i <= nof_rounds; i++) { for (j = 0 ; j < this .SAFER_BLOCK_LEN + 1 ; j++) { ka[j] = this .ROL(ka[j],6 ); kb[j] = this .ROL(kb[j],6 ); } for (j = 0 ; j < this .SAFER_BLOCK_LEN; j++) { if (strengthened) { key[keyPoint] = ka[(j + 2 * i - 1 ) % (this .SAFER_BLOCK_LEN + 1 )] + this ._exp_tab[this ._exp_tab[18 * i + j + 1 ]] & 255 ; keyPoint++; } else { key[keyPoint] = ka[j] + this ._exp_tab[this ._exp_tab[18 * i + j + 1 ]] & 255 ; keyPoint++; } } for (j = 0 ; j < this .SAFER_BLOCK_LEN; j++) { if (strengthened) { key[keyPoint] = kb[(j + 2 * i) % (this .SAFER_BLOCK_LEN + 1 )] + this ._exp_tab[this ._exp_tab[18 * i + j + 10 ]] & 255 ; keyPoint++; } else { key[keyPoint] = kb[j] + this ._exp_tab[this ._exp_tab[18 * i + j + 10 ]] & 255 ; keyPoint++; } } } for (j = 0 ; j < this .SAFER_BLOCK_LEN + 1 ; j++) { ka[j] = kb[j] = 0 ; } }
至此,所有需要的内容就绪,现在需要做的就是实现这个解密函数,由于 Flash 使用的是 ActionScript,如今已经几乎没有使用,FLEX 也快要被淘汰,很难找到一个合适的环境进行编译,在现代科技(AI)的帮助下,可以较为轻松的使用与其类似的语言进行改写,出于个人技术栈等原因,我选择了 Go 语言。
Go 解密实现 本着简单高效考虑,忽略了很多语法规范,RipeMD256Hash 直接使用了第三方库。
程序如下:
Go 语言程序实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 package mainimport ( "bytes" "compress/zlib" "encoding/base64" "encoding/binary" "fmt" "github.com/C0MM4ND/go-ripemd" "io" "os" "strconv" "strings" ) var lastBlock []byte var encryptKey []byte var expTab []byte = make ([]byte , 256 )var logTab []byte = make ([]byte , 256 )const constEncryptKeyCDViewer = "0b8b6a4650b148a1975331bc2da63f93" const TAB_LEN = 256 const SAFER_BLOCK_LEN = 8 const SAFER_KEY_LEN = 217 const SAFER_MAX_NOF_ROUNDS = 13 var inputfile, outputfile, encryptionKey string func main () { args := os.Args[1 :] if len (args) == 2 { inputfile = args[0 ] encryptionKey = args[1 ] } else { fmt.Println("错误输入" ) os.Exit(-1 ) } f, _ := os.Open(inputfile) defer func (f *os.File) { _ = f.Close() }(f) buf := make ([]byte , 8 *1024 *1024 ) ri, _ := f.Read(buf) r := PageLoad(buf[:ri]) outputfile = strings.Replace(inputfile, ".swf" , ".jpg" , 1 ) _ = os.Remove(outputfile) f2, _ := os.OpenFile(outputfile, os.O_CREATE, 0x644 ) defer f2.Close() errf, _ := os.Open("error.txt" ) defer errf.Close() if bytes.Equal(r[:3 ], []byte ("CWS" )) { rr, err := zlib.NewReader(bytes.NewReader(r[8 :])) if err != nil { fmt.Println(err) return } defer rr.Close() br := bytes.NewBuffer([]byte {}) br.WriteByte(70 ) br.Write(r[1 :8 ]) io.Copy(br, rr) a := TryToExportJpeg2(br.Bytes()) if a == nil { f2.Write(br.Bytes()) errf.WriteString(outputfile + "\n" ) } else { f2.Write(a) } } else if bytes.Equal(r[:3 ], []byte ("FWS" )) { a := TryToExportJpeg2(r) if a == nil { f2.Write(r) errf.WriteString(outputfile + "\n" ) } else { f2.Write(a) } } else { errf.WriteString(outputfile + "\n" ) f2.Write(r) } } func TryToExportJpeg2 (r []byte ) []byte { rd := bytes.NewReader(r[21 :]) for { var tagType uint16 = 0 var tglen uint32 = 0 err := binary.Read(rd, binary.LittleEndian, &tagType) if err == io.EOF { break } if err != nil { fmt.Println(err) return nil } tgtype := tagType >> 6 tglen = uint32 (tagType & 0x3f ) if tglen == 0x3f { binary.Read(rd, binary.LittleEndian, &tglen) } if tgtype == 21 { rbuf := make ([]byte , tglen-2 -4 +2 ) rd.Read(rbuf) cutindex := bytes.Index(rbuf, []byte {0xff , 0xd9 , 0xff , 0xd8 , 0xff , 0xe0 , 0x00 , 0x10 }) if cutindex > 0 { rbuf = append (rbuf[:cutindex], rbuf[cutindex+4 :]...) } return rbuf[2 :] } rd.Seek(int64 (tglen), io.SeekCurrent) } return nil } func PageLoad (src []byte ) []byte { encryptionKey = encryptString(encryptionKey, constEncryptKeyCDViewer) encryptKey = []byte (encryptionKey) objectByte := EncryptStream(src, encryptionKey) return objectByte } func EncryptStream (src []byte , key string ) []byte { var outArray []byte = nil if len (src) <= 8192 { return encryptStream(src, key, len (src)) } if strings.HasSuffix(inputfile, ".xml" ) { outArray = encryptStream(src, key, len (src)) out := bytes.NewBuffer(outArray) return out.Bytes() } outArray = encryptStream(src, key, 8192 ) out := bytes.NewBuffer(outArray) if 8192 < len (src) { out.Write(src[8192 :]) } return out.Bytes() } func encryptStream (src []byte , encKey string , bufSize int ) []byte { if len (src) == 0 { return src } szKey := bytes.NewBuffer([]byte {}) szKey.WriteString(encKey) initKey(szKey.Bytes(), len (encKey)) exp := 1 for i := 0 ; i < TAB_LEN; i++ { expTab[i] = byte (exp & 255 ) logTab[expTab[i]] = byte (i) exp = exp * 45 % 257 } var res = make ([]byte , bufSize) res = decodeBuffer(src, bufSize) return res } func RipeMD256Hash (input []byte ) []byte { hash := ripemd.New256() hash.Write(input) sum := hash.Sum(nil ) return sum } func ROL (x uint , n uint ) byte { return byte ((x << n) | (x >> (8 - n))) } func saferExpandUserkey (userkey1 []byte , userkey2 []byte , key2Start int , nofRounds int , strengthened int ) []byte { i := 0 j := 0 keyPoint := 0 ka := make ([]byte , SAFER_BLOCK_LEN+1 ) kb := make ([]byte , SAFER_BLOCK_LEN+1 ) key := make ([]byte , SAFER_KEY_LEN) key[0 ] = byte (nofRounds) keyPoint = 1 ka[SAFER_BLOCK_LEN] = 0 kb[SAFER_BLOCK_LEN] = 0 for j = 0 ; j < SAFER_BLOCK_LEN; j++ { ka[j] = ROL(uint (userkey1[j]), 5 ) ka[SAFER_BLOCK_LEN] ^= ka[j] key[keyPoint+j] = userkey2[j+key2Start] kb[j] = key[keyPoint+j] kb[SAFER_BLOCK_LEN] ^= kb[j] } keyPoint = j + 1 for i = 1 ; i <= nofRounds; i++ { for j := 0 ; j < SAFER_BLOCK_LEN+1 ; j++ { ka[j] = ROL(uint (ka[j]), 6 ) kb[j] = ROL(uint (kb[j]), 6 ) } for j = 0 ; j < SAFER_BLOCK_LEN; j++ { if strengthened != 0 { key[keyPoint] = ka[(j+2 *i-1 )%(SAFER_BLOCK_LEN+1 )] + expTab[expTab[18 *i+j+1 ]]&255 keyPoint++ } else { key[keyPoint] = ka[j] + expTab[expTab[18 *i+j+1 ]]&255 keyPoint++ } } for j = 0 ; j < SAFER_BLOCK_LEN; j++ { if strengthened != 0 { key[keyPoint] = kb[(j+2 *i)%(SAFER_BLOCK_LEN+1 )] + expTab[expTab[18 *i+j+10 ]]&255 keyPoint++ } else { key[keyPoint] = kb[j] + expTab[expTab[18 *i+j+10 ]]&255 keyPoint++ } } } for j = 0 ; j < SAFER_BLOCK_LEN+1 ; j++ { ka[j] = 0 kb[j] = 0 } return key } func saferEncryptBlock (blockIn []byte , key []byte ) []byte { blockOut := make ([]byte , len (blockIn)) keyPoint := 0 a := blockIn[0 ] b := blockIn[1 ] c := blockIn[2 ] d := blockIn[3 ] e := blockIn[4 ] f := blockIn[5 ] g := blockIn[6 ] h := blockIn[7 ] round := uint (key[0 ]) if uint (SAFER_MAX_NOF_ROUNDS) < round { round = uint (SAFER_MAX_NOF_ROUNDS) } for i := round; i > 0 ; i-- { keyPoint++ a ^= key[keyPoint] keyPoint++ b += key[keyPoint] keyPoint++ c += key[keyPoint] keyPoint++ d ^= key[keyPoint] keyPoint++ e ^= key[keyPoint] keyPoint++ f += key[keyPoint] keyPoint++ g += key[keyPoint] keyPoint++ h ^= key[keyPoint] keyPoint++ a = expTab[a&255 ] + key[keyPoint] keyPoint++ b = logTab[b&255 ] ^ key[keyPoint] keyPoint++ c = logTab[c&255 ] ^ key[keyPoint] keyPoint++ d = expTab[d&255 ] + key[keyPoint] keyPoint++ e = expTab[e&255 ] + key[keyPoint] keyPoint++ f = logTab[f&255 ] ^ key[keyPoint] keyPoint++ g = logTab[g&255 ] ^ key[keyPoint] keyPoint++ h = expTab[h&255 ] + key[keyPoint] b += a a += b d += c c += d f += e e += f h += g g += h c += a a += c g += e e += g d += b b += d h += f f += h e += a a += e f += b b += f g += c c += g h += d d += h t := b b = e e = c c = t t = d d = f f = g g = t } keyPoint++ a ^= key[keyPoint] keyPoint++ b += key[keyPoint] keyPoint++ c += key[keyPoint] keyPoint++ d ^= key[keyPoint] keyPoint++ e ^= key[keyPoint] keyPoint++ f += key[keyPoint] keyPoint++ g += key[keyPoint] keyPoint++ h ^= key[keyPoint] blockOut[0 ] = a & 255 blockOut[1 ] = b & 255 blockOut[2 ] = c & 255 blockOut[3 ] = d & 255 blockOut[4 ] = e & 255 blockOut[5 ] = f & 255 blockOut[6 ] = g & 255 blockOut[7 ] = h & 255 return blockOut } func decodeBuffer (src []byte , dataSize int ) []byte { i := 0 arrayPoint := 0 cipherTextBlock := make ([]byte , SAFER_BLOCK_LEN) dest := make ([]byte , dataSize) for dataSize >= SAFER_BLOCK_LEN { for i = 0 ; i < SAFER_BLOCK_LEN; i++ { cipherTextBlock[i] = src[arrayPoint+i] } decrypted := saferDecryptBlock(cipherTextBlock, encryptKey) copy (dest[arrayPoint:], decrypted) for i = 0 ; i < SAFER_BLOCK_LEN; i++ { dest[arrayPoint+i] ^= lastBlock[i] } for i = 0 ; i < SAFER_BLOCK_LEN; i++ { lastBlock[i] ^= cipherTextBlock[i] } arrayPoint += SAFER_BLOCK_LEN dataSize -= SAFER_BLOCK_LEN } if dataSize > 0 { TempBlock := make ([]byte , SAFER_BLOCK_LEN) TempBlock = saferEncryptBlock([]byte {255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 }, encryptKey) for i = 0 ; i < dataSize; i++ { dest[arrayPoint+i] = src[arrayPoint+i] ^ TempBlock[i] } for i = 0 ; i < SAFER_BLOCK_LEN; i++ { lastBlock[i] ^= TempBlock[i] } } return dest } func saferDecryptBlock (blockIn []byte , key []byte ) []byte { blockOut := make ([]byte , len (blockIn)) var keyPoint uint keyPoint = 0 a := blockIn[0 ] b := blockIn[1 ] c := blockIn[2 ] d := blockIn[3 ] e := blockIn[4 ] f := blockIn[5 ] g := blockIn[6 ] h := blockIn[7 ] round := uint (key[0 ]) if uint (SAFER_MAX_NOF_ROUNDS) < round { round = uint (SAFER_MAX_NOF_ROUNDS) } keyPoint += uint (SAFER_BLOCK_LEN) * (1 + 2 *round) h ^= key[keyPoint] keyPoint-- g -= key[keyPoint] keyPoint-- f -= key[keyPoint] keyPoint-- e ^= key[keyPoint] keyPoint-- d ^= key[keyPoint] keyPoint-- c -= key[keyPoint] keyPoint-- b -= key[keyPoint] keyPoint-- a ^= key[keyPoint] for i := round; i > 0 ; i-- { t := e e = b b = c c = t t = f f = d d = g g = t a -= e e -= a b -= f f -= b c -= g g -= c d -= h h -= d a -= c c -= a e -= g g -= e b -= d d -= b f -= h h -= f a -= b b -= a c -= d d -= c e -= f f -= e g -= h h -= g keyPoint-- h -= key[keyPoint] keyPoint-- g ^= key[keyPoint] keyPoint-- f ^= key[keyPoint] keyPoint-- e -= key[keyPoint] keyPoint-- d -= key[keyPoint] keyPoint-- c ^= key[keyPoint] keyPoint-- b ^= key[keyPoint] keyPoint-- a -= key[keyPoint] keyPoint-- h = logTab[h&255 ] ^ key[keyPoint] keyPoint-- g = expTab[g&255 ] - key[keyPoint] keyPoint-- f = expTab[f&255 ] - key[keyPoint] keyPoint-- e = logTab[e&255 ] ^ key[keyPoint] keyPoint-- d = logTab[d&255 ] ^ key[keyPoint] keyPoint-- c = expTab[c&255 ] - key[keyPoint] keyPoint-- b = expTab[b&255 ] - key[keyPoint] keyPoint-- a = logTab[a&255 ] ^ key[keyPoint] } blockOut[0 ] = a & 255 blockOut[1 ] = b & 255 blockOut[2 ] = c & 255 blockOut[3 ] = d & 255 blockOut[4 ] = e & 255 blockOut[5 ] = f & 255 blockOut[6 ] = g & 255 blockOut[7 ] = h & 255 return blockOut } func encryptString (str string , encryptKey string ) string { outStr := make ([]byte , 8192 ) zsKey := bytes.NewBuffer([]byte {}) zsKey.WriteString(encryptKey) initKey(zsKey.Bytes(), len (encryptKey)) buf2 := []byte {} strBuffer := bytes.NewBuffer(buf2) str = string (hexStringToNormal(str)) strBuffer.WriteString(str) outStr = decodeString(strBuffer.Bytes()) for i := 0 ; i < len (outStr); i++ { if outStr[i] == 0 { outStr = outStr[:i] } } outPut := string (outStr) return outPut } func hexStringToNormal (hexStr string ) []byte { var result []byte for i := 0 ; i < len (hexStr); i += 2 { value, err := strconv.ParseUint(hexStr[i:i+2 ], 16 , 8 ) if err != nil { return nil } result = append (result, byte (value)) } return result } func decodeString (src []byte ) []byte { dataSize := len (src) nOutPutBytes := dataSize / 4 * 3 if src[dataSize-1 ] == 61 { if src[dataSize-2 ] == 61 { nOutPutBytes -= 2 } else { nOutPutBytes -= 1 } } OutPutBuffer := make ([]byte , nOutPutBytes+5 ) base64.StdEncoding.Decode(OutPutBuffer, src) dest := decodeBuffer(OutPutBuffer, nOutPutBytes) return dest } func initKey (inputStr []byte , nKeyBytes int ) { lastBlock = make ([]byte , SAFER_BLOCK_LEN) for i := 0 ; i < SAFER_BLOCK_LEN; i++ { lastBlock[i] = 255 } MDBuf := RipeMD256Hash(inputStr) exp := 1 for i := 0 ; i < TAB_LEN; i++ { expTab[i] = byte (exp & 255 ) logTab[expTab[i]] = byte (i) exp = exp * 45 % 257 } encryptKey = saferExpandUserkey(MDBuf, MDBuf, SAFER_BLOCK_LEN, 10 , 1 ) lastBlock = saferEncryptBlock(lastBlock, encryptKey) }
程序根据 ActionScript 脚本,更换了变量类型和部分方法,对解密过程进行了复现,使用时候需要提供 swf 文件路径和 lisence 文件中的 encryption 密码。
程序还根据 swf 文件的格式,对 swf 文件进行了解压缩,提取了其中的图片,如果是 xml 文件,直接解密输出。
虽然程序已经实现了,但分析还是补上,对 swf 文件进行解密后还是 swf 文件,通过 JPEXS 打开可以找到里面的内容。
如果文件内只有一张图片,那可以按上面程序给的方法,直接根据 swf 文件格式,提取首个附件,但如果是像例子这样由图片和文字拼凑而成,就变得非常麻烦。
【2025 年 3 月 1 日更新】
唉?你是不是以为到这里就结束了?确实当年知道这一步,但最近有大佬找到了中少阅读的可遍历目录的路径入口,找到了更多完整的 Flash 文件,以杭州少儿为例,通过如下链接可以访问:
https://hangzhoushaoer.61read.com/open
已失效
不过在此还是再次强调,在获得官方授权之前对站点进行镜像或任何损害站点利益的行为都是不被允许的,学习和测试时应增强版权意识,同时避免给站点带来过大访问压力。
在互联网信息闭塞,Flash 过时的当时,并没有找到合适的处理 swf 文件的方法,但如今在拥有海量训练资料的生成 AI 的帮助下,找到了 swftools
这一工具,自此获取图片不再成为一个较大的难题。
这里给出 SWFTools 官网链接 ,可以通过该系列工具来处理已经解密的 swf 文件,将内容进行渲染并保存成图片。
命令行使用说明如下:
1 2 3 4 5 6 7 8 swfrender -h # -h , --help Print short help message and exit # -l , --legacy Use old rendering framework # -o , --output Output file, suffixed for multiple pages (default: output.png) # -p , --pages range Render pages in specified range e.g. 9 or 1-20 or 1,4-6,9-11 (default: all pages) # -r , --resolution dpi Scale width and height to a specific DPI resolution, assuming input is 1px per pt (default: 72) # -X , --width width Scale output to specific width (proportional unless height specified) # -Y , --height height Scale output to specific height (proportional unless width specified)
以及在 AI 帮助下完成的 Go 语言调用swfrender的程序:
Go 语言调用 swfrender
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 func swfdecoder (inputDir string , outputDir string , threads int ) { extractor := NewExtractor(outputDir, threads) if extractor == nil { fmt.Println("错误: 创建提取器失败" ) os.Exit(1 ) } if err := extractor.ProcessDirectory(inputDir); err != nil { fmt.Printf("错误: %v\n" , err) os.Exit(1 ) } } type SwfExtractor struct { OutputDir string Threads int } func NewExtractor (outputDir string , threads int ) *SwfExtractor { if err := os.MkdirAll(outputDir, 0755 ); err != nil { fmt.Printf("创建输出目录失败: %v\n" , err) return nil } return &SwfExtractor{ OutputDir: outputDir, Threads: threads, } } func (e *SwfExtractor) ExtractSwf(swfPath string ) (string , error ) { if _, err := os.Stat(swfPath); os.IsNotExist(err) { return "" , fmt.Errorf("文件不存在: %s" , swfPath) } tempDir, err := ioutil.TempDir("" , "swf-extract-" ) if err != nil { return "" , fmt.Errorf("创建临时目录失败: %v" , err) } defer os.RemoveAll(tempDir) baseName := filepath.Base(swfPath) baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) tempPngPath := filepath.Join(tempDir, "output.png" ) outputPath := filepath.Join(e.OutputDir, baseName+".png" ) cmd := exec.Command("./swfrender" , swfPath, "-o" , tempPngPath, "-r" , "200" ) if output, err := cmd.CombinedOutput(); err != nil { return "" , fmt.Errorf("渲染SWF失败: %v, 输出: %s" , err, output) } pngData, err := ioutil.ReadFile(tempPngPath) if err != nil { return "" , fmt.Errorf("读取临时PNG文件失败: %v" , err) } if err := ioutil.WriteFile(outputPath, pngData, 0644 ); err != nil { return "" , fmt.Errorf("保存PNG文件失败: %v" , err) } return outputPath, nil }
不过有一些页面渲染还是出现了一些问题,特别是一些多图层或者具有透明图层的页面,如果加上参数 -l 整体清晰度和缩放无法达到预期,苦恼的时候意外发现了 Ruffle。
它是一个使用 Rust 开发的 Flash 模拟器,可以在现代浏览器中运行 Flash 内容,通过编译命令行工具,可以用来批量处理 SWF 文件并将其转换为图像。
GitHub 地址
首先需要编译命令行工具(未能找到可以直接下载的二进制文件,如果你找到了就可以直接使用):
1 2 3 git clone https://github.com/ruffle-rs/ruffle/ cd ruffle cargo run --release --package=exporter
编译完成后,复制 target\release\exporter.exe
复制到执行目录即可。
帮助说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .\exporter.exe --help Usage: exporter.exe [OPTIONS] <swf> [output] Arguments: <swf> The file or directory of files to export frames from [output] The file or directory (if multiple frames/files) to store the capture in. The default value will either be: - If given one swf and one frame, the name of the swf + ".png" - If given one swf and multiple frames, the name of the swf as a directory - If given multiple swfs, this field is required Options: -f, --frames <FRAMES> Number of frames to capture per file [default: 1] --skipframes <SKIPFRAMES> Number of frames to skip [default: 0] -s, --silent Don't show a progress bar --scale <SCALE> The amount to scale the page size with [default: 1.0] --width <WIDTH> Optionally override the output width --height <HEIGHT> Optionally override the output height -g, --graphics <GRAPHICS> Type of graphics backend to use. Not all options may be supported by your current system. Default will attempt to pick the most supported graphics backend [default: default] [possible values: default, vulkan, metal, dx12, gl] -p, --power <POWER> Power preference for the graphics device used. High power usage tends to prefer dedicated GPUs, whereas a low power usage tends prefer integrated GPUs [default: high] [possible values: low, high] --skip-unsupported Skip unsupported movie types (currently AVM 2) -h, --help Print help -V, --version Print version
调用示例:
1 ./exporter.exe input.swf output.png --scale 4
依然是调用外部可执行文件,相关程序省略。
自此,完整的解密和提取 Flash 文件中的图片的过程就结束了,整理,合并等操作并不在本文讨论范围内,确实有需要的话,让 AI 帮帮忙,就可以很方便快速地获得需要的内容给了。
如果有什么想法,欢迎与我联系(tdh@tdh6.top ),毕竟都看到这里了,交个朋友也不赖吧?
最后的吐槽:
Flash 时代已经过去了,但那时候的记忆永远留在心中,包括众多小游戏和一些不太小的游戏(比如 EBF),借由 Flash 被大家所知晓,喜爱。
初见不识画中意,再见已是画中人。
我不是大佬。
附 本文封面图来自番剧《星灵感应》,星屑テレパス(Hoshikuzu Telepath)S01E01 00:06:04