最近在逛某论坛时碰到了DuFile网盘的资源,对于这列靠下载限速+繁琐跳转促使用户付费的收费盘,当然要破解一下玩玩。经过几个小时的分析,笔者发现这家网盘的策略是比较有趣的,故此记录一下。
注意:此类收费盘,破解会员高速通道在理论上基本是不可能的,我们能做的只是规避掉等待时间。
打开链接,http://dufile.com/file/xxx2e80bxxxx00da.html (地址已做处理),能看出网站的模板都是那种收费网盘的大路货,直接开调试模式抓包,但这里就遇到了第一个坑。
网页直接进入无法点击的调试状态,只要不关闭调试窗口就一直这样,而且事后证明如果在访问网址前就开启调试面板则网页根本就不会加载。这实际是一种不太常见的前端反逆向措施(常见的是js代码混淆),详情可见这篇文章,破解方法见此。
破解后继续在调试工具里查看,下一个跳转的页面地址是把/file/换成/down/,这也是此类网盘地址跳转的一概路子,新页面里我们输入验证码,追踪到下载按钮绑定的函数,发现它的代码在http://dufile.com/js/file.js里,这里的代码根本没法读,不过它用的是最简单的编码加密,在线解密一下就明晰了很多;为了增加逆向难度,前端对变量名也做了随机化处理。我们关注第一行的var _0x100e列表变量,里面出现了debug,console等字样,可以猜想它们在经过某些处理后形成的语句就会对常用的前端调试手段产生干扰。
幸运的是函数名没有加密,我们找到down_file函数(其余函数都是举报,开vip以及无关紧要的xxx),然后用查找功能追踪里面涉及到的变量,发现引用关系很简单,只涉及一小段代码
var _0x100e = ['<div style="500px;h.........太长略'] var _0x5289 = function(_0x24bdbe, _0x4f9026) { _0x24bdbe = _0x24bdbe - 0x0; var _0x471092 = _0x100e[_0x24bdbe]; return _0x471092; }; function down_file(_0x5876ce, _0x3d630b) { var _0x385fa2 = document[_0x5289('0x64')](_0x5289('0x65')); _0x385fa2['id'] = 'filter'; _0x385fa2[_0x5289('0x18')] = _0x5289('0x66') + '<div id="frmmask" style="z-index:9999;display:block;background:#000;filter:alpha(opacity=0);-moz-opacity:0;opacity:0;position:absolute;visibility:hidden;"></div>' + _0x5289('0x67') + _0x5289('0x68') + _0x5289('0x69') + '<iframe id="show_down" style="margin-top:4px;" frameborder="0" scrolling="no" width="352" height="316" scrolling="auto" src="about:blank"></iframe></div></div>'; document[_0x5289('0x19')][_0x5289('0x6a')](_0x385fa2); showMask(!![], 0x15a, 0x164); document[_0x5289('0x10')](_0x5289('0x6b'))['src'] = _0x5289('0x6c') + _0x5876ce + _0x5289('0x6d') + _0x3d630b; }
将变量名作替换后就很明了
var elelist = ['<div style="500px;h.........太长略'] var getelefunc = function(index, _) { index = index - 0x0; //index=index-0,其实此行可以删除 var ele = elelist[index]; return ele; }; function down_file(fileid, '0') { //fileid就是url里那一串字母数字,0是固定的 /* 这些不重要,能看出它们功能只是在网页里渲染一些元素框体 var _0x385fa2 = document[_0x5289('0x64')](_0x5289('0x65')); _0x385fa2['id'] = 'filter'; _0x385fa2[_0x5289('0x18')] = _0x5289('0x66') + '<div id="frmmask" style="z-index:9999;display:block;background:#000;filter:alpha(opacity=0);-moz-opacity:0;opacity:0;position:absolute;visibility:hidden;"></div>' + _0x5289('0x67') + _0x5289('0x68') + _0x5289('0x69') + '<iframe id="show_down" style="margin-top:4px;" frameborder="0" scrolling="no" width="352" height="316" scrolling="auto" src="about:blank"></iframe></div></div>'; document[_0x5289('0x19')][_0x5289('0x6a')](_0x385fa2); showMask(!![], 0x15a, 0x164); */ document[getelefunc('0x10')](getelefunc('0x6b'))['src'] = getelefunc('0x6c') + fileid + getelefunc('0x6d') +'0'; //关键在于getelefunc('0x6c') + fileid + getelefunc('0x6d'),分别对应elelist[108]和elelist[109],一查便知。 }
指向了一个链接:http://dufile.com/dd.php?file_key=xxxxxxxxxxxx&p=0,而此链接GET返回的内容里就含有直接下载地址。
事实上更简单的方法是直接用第三方工具抓包,比如Charles
可以很直观的看出发包次序和包内容,于是构造Python脚本如下:
import requests from bs4 import BeautifulSoup as Bs import re class DuFile(object): def __init__(self): self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763"} def getfile(self, url, filepath): session = requests.session() #用session来维持访问中的cookie req1 = session.get(url, headers=self.headers) if req1.status_code==200: content = Bs(req1.text, 'lxml') file_size = content.select_one('#fbody1').select_one('b').text filename = content.select_one('.title').text filename = filename.split("下载")[1].strip() print("filename: %s, filesize: %sKB" %(filename, file_size)) else: return 'error' targetaddr = url.replace('/file/','/down/') self.headers.update({'Host':'dufile.com', "Referer":url}) req2 = session.get(targetaddr, headers=self.headers) file_id = re.search(r'/down/([a-z0-9]+)', targetaddr).group(1) self.headers.update({"Referer":targetaddr}) req4 = session.get("http://dufile.com/dd.php?file_key={}&p=0".format(file_id), headers=self.headers) downurl = re.search(r"id="downs" href="(http.+?)"", req4.text).group(1) host = re.search(r'http://(.+?)/down', downurl).group(1) req_file = session.get(downurl, headers={'Host':host, 'Referer':"http://dufile.com/dd.php?file_key={}&p=0".format(file_id)}) with open(filepath+filename, 'wb') as f: f.write(req_file.content) return filename
但使用中直接抛出了异常,req4返回了意外的结果,是一个地址,打开后
被认出来了......那么疏漏在哪里了呢?
回顾之前的抓包,脚本比正常访问少的只有
两处。2发包时间在问题点的后面,应该不是,那就只有1,1是验证码的相关操作,分为一个get图片请求和一个post输入值提交,如果输入值正确即返回1。因为绝大多数网盘(飞猫、讯牛等)只会用这个返回值来决定是否执行下一步的下载函数,所以根本不需要去真的填写验证码(飞猫云曾有一段时间把真正地址写在返回值里,这种是没办法跳过去的),DuFile是否在这里也做了验证?
经试验,DuFile的验证码图片分为两种,分别是4位纯数字和计算加减法,受URL里的down_code加不加下划线控制,因为我们不是搞CV的,所以没必要花时间研究验证码识别问题,直接调第三方服务就好,打码服务百度一下就能找到,一个码一般1分到几分钱,还是很便宜的,如果是那种比较白给的验证码,甚至可以试试百度免费的图片转文字服务,这里就不多做介绍了。直接上修改版代码
import requests from bs4 import BeautifulSoup as Bs import re import random import base64 import time import json import hashlib def md5(ss): fd = hashlib.md5() fd.update(ss.encode('utf-8')) return fd.hexdigest() def veryimg(img_data): link = "http://xxxx.xxxx.com/api/capreg" #这里就不放具体是哪家了 timestamp = str(int(time.time())) pd_id = YOUR_ID pd_key = YOUR_KEY sign = md5(pd_id + timestamp + md5( timestamp + pd_key)) predict_type = '10400' #4位纯数字 data = { 'user_id':pd_id, 'timestamp':timestamp, 'sign':sign, 'predict_type':predict_type, 'img_data':img_data } req = requests.post(link, data) result = json.loads(req.text)['RspData'] code = json.loads(result)['result'] return code class DuFile(object): def __init__(self): self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763"} def getfile(self, url, filepath): session = requests.session() #用session来维持访问中的cookie req1 = session.get(url, headers=self.headers) if req1.status_code==200: content = Bs(req1.text, 'lxml') file_size = content.select_one('#fbody1').select_one('b').text filename = content.select_one('.title').text filename = filename.split("下载")[1].strip() print("filename: %s, filesize: %sKB" %(filename, file_size)) else: return 'error' targetaddr = url.replace('/file/','/down/') self.headers.update({'Host':'dufile.com', "Referer":url}) req2 = session.get(targetaddr, headers=self.headers) file_id = re.search(r'/down/([a-z0-9]+)', targetaddr).group(1) self.headers.update({"Referer":targetaddr}) ''' 新增验证码提交 ''' req3 = session.get("http://dufile.com/downcode.php?{}".format(str(random.random())), headers=self.headers) # 这里的随机值是防止浏览器去读缓存而设的,我们脚本里其实不加也行 if req3.status_code==200: # 将图片二进制转为base64字符串传入接口 code = veryimg(base64.b64encode(req3.content).decode()) else: return 'error' req_p = session.post("http://dufile.com/downcode.php", data={'action':'yz','id':file_id, 'code':code}, headers=self.headers) # req4 = session.get("http://dufile.com/dd.php?file_key={}&p=0".format(file_id), headers=self.headers) downurl = re.search(r"id="downs" href="(http.+?)"", req4.text).group(1) host = re.search(r'http://(.+?)/down', downurl).group(1) req_file = session.get(downurl, headers={'Host':host, 'Referer':"http://dufile.com/dd.php?file_key={}&p=0".format(file_id)}) # 直接一次性请求,如果文件比较大建议用分块下载(不要用多线程,免费用户不支持的),写法网上教程很多这里不赘述 with open(filepath+filename, 'wb') as f: f.write(req_file.content) return filename
重新运行,搞定!
总结
笔者对Yunfile, 飞猫云,彩虹云,讯牛,城通,Busdisk等多家收费盘均进行过逆向破解,DuFile不是难度最大的一个,但它的反破解套路比较有趣,一方面用到了禁调试脚本,一方面又细心地在验证码上下了功夫,这两种在笔者看来都有些剑走偏锋的意思;特别是验证码的部分,误导性很强,如果配合上彩虹云用的js修改cookies验证,应该能拦住一大波人。而且经笔者测试,DuFile的下载间隔限制是IP级的,所以即使每次的session都是新生成,也得老老实实等上10分钟,或者上IP代理池,这点许多网盘也没能做到。
真有下载刚需的话(嘿嘿嘿~你懂的),还是建议充个会员,中国的服务器、带宽真心挺贵的,人家也要恰饭嘛。