Author: JieJiSS

在百度网盘里私密分享文件的时候,百度网盘会给你生成一个随机四位数字和大小写英文字母混杂的密码作为分享密码。稍微分析一下在点击分享按钮之后的网络请求,就会发现密码是本地生成好了之后再发送给服务器留存。既然是本地生成的,就给了我们可调整的余地。只要发送给服务器的密码长度为 4,服务器并不能判断出这个密码是不是随机生成的——任何一个四位密码都有可能是随机生成的,不论它看上去多么不随机(例如:1234,mdzz)。

0x00 架构分析

百度网盘作为一个大型的 Web APP,必然有着较为完整的项目模块化规范,否则极高的耦合将导致每次迭代都异常艰难。稍微在 Chrome 的开发者工具的 Source 栏里搜索一下几种浏览器端常用的模块化方法的关键词,可以发现源代码里出现了很多 definerequire,例如登录后主页面第 198 行:

1
var yunData = require('disk-system:widget/data/yunData.js');

和第 204 行:

1
var context = require('system-core:context/context.js').instanceForSystem;

看上去像是 requireJS 的路子,不过是不是的都不影响,知道怎么用就行了。我们来看一下它的目录结构:

百度网盘目录结构.jpg

这里的文件夹名称 disk-systemsystem-core 是不是看上去有点眼熟?

再往下展开试试:

百度网盘目录结构展开.jpg

找到了之前看到的 require 的函数定义,轻松愉快有木有

0x01 定位密码生成函数

首先想到的是模拟一次分享,看看有哪些函数被调用了。结果一路下断点,跟到了一个事件钩子池里(

就是说,它把所有的注册的事件和对应的 handler 都搁到了一个大数组里,对接受到的每个事件都依次判断一下这些 handler 的触发条件是否满足,如果满足就调用。往里跟了两三层,不想接着跟了……倒也不是跟不了,之前研究学校的综评系统的时候一个 click 事件跟了十几层,最后还是把注册的处理函数给揪出来了(

于是打算另辟蹊径:上百度搜搜有没有别人干过类似的事情(逃

这叫:用百度家的产品来搞百度家的产品,真是一点负罪感也没有啊 2333333

搜到了这里有一篇教程,它提供的代码是:

1
2
var Share = require(["function-widget-1:share/util/service/createLinkShare.js"]);
Share.prototype.makePrivatePassword = function () { return "1234"; };

放到百度网盘页面的 Chrome Inspector's Console 里执行,不出意外地报错了。(要是不报错,哪来的这篇博文呢)

看一眼报的错是什么:

1
2
3
Uncaught Error: Cannot find module `function-widget-1:share/util/service/createLinkShare.js`
at require (mod_455fd55.js:1)
at <anonymous>:1:1

很明显,网上资料里给的模块路径跟实际所在的路径已经不一样了,require 函数根据给出的路径找不到对应的模块。我瞅了一眼那篇博文的发表时间,2017 年 4 月份……两年前的了,路径变了也正常。但是至少 function-widget-1 这个文件夹还在(参见上文插图),所以先从这个文件夹入手。点开之后发现目录结构如下:

1
2
3
4
5
+ function-widget-1/pkg
|-- async-all_<hash>.js
|-- sync-all_<hash>.js
|-- upDataTips-all_<hash>.js
|-- ...

从文件名来判断的话,头两个文件分别负责异步操作和同步操作,那么看上去创建分享链接这种需要和服务器通信的就应该被归类到负责异步的 async-all 里去了。打开 async-all_<hash>.js 一搜:

百度网盘async-all.jpg

于是利用 Chrome 提供的 beautify 功能格式化一下代码,稍微阅读一下它 define 的东西,就在 createPrivateShareLink 函数里定位到了最终的密码生成函数 makePrivatePassword

百度网盘分享密码生成函数.jpg

在第 2626 行下个断点,实际操作一次,在断点处就可以跟到 makePrivatePassword 的定义了。

百度网盘分享密码生成函数实现.jpg

最终的模块路径是 function-widget-1:share/util/shareFriend/createLinkShare.js

0x02 Hook 密码生成函数

光是知道这个 makePrivatePassword 在哪没用,我们还得想办法能控制它返回的函数值,这样才能实现自定义密码这个需求。再看一眼函数定义(已经进行了一定处理,方便阅读),

1
2
3
4
5
6
7
8
9
10
11
12
makePrivatePassword: function() {
var chars = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "m", "n", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"], // chars.length = 9 + 26 - 1 - 1 = 33
rndint = function(start, end) {
return Math.round((end - start) * Math.random() + start) // [start, end]
},
generatePswd = function(len) {
for (var arr = [], i = 1; i <= len; i++)
arr.push(chars[rndint(0, chars.length - 1)]); // end = 32, end - start = 32
return arr.join("")
};
return generatePswd(4)
}

于是自然而然的想到可以魔改 Math.random,从而控制 makePrivatePassword

1
2
3
4
5
6
let i = 0, indexOfChars = [0, 1, 2, 3];
window.Math.random = function () {
const index = indexOfChars[i] / 32; // end - start = 32
i++;
return index;
};

但是这样的话,首先有一点问题就是原本的 chars 数组(改名之后)就不包括 "l""o""0"(我猜测是因为 l 容易和 1 搞混、o0 互相容易搞混),所以魔改后还是没法用这三个字符,有点不完美;其次,这样 tricky 的操作封装起来比较麻烦,百度网盘的程序员哪天改了 makePrivatePassword 的实现的话,封装的函数还得跟着变。很差劲。

所以得另辟蹊径。翻回来看看 createPrivateShareLink 的实现:

1
2
3
4
createPrivateShareLink: function(e, i) {
var t = this,
o = t.makePrivatePassword();
// ...

发现了一个很有意思的地方:makePrivatePasswordt 的属性,而 t 指向了 this!换言之,我们直接修改

1
require(["path/to/module"]).makePrivatePassword

无法覆盖所有的 makePrivatePassword(因为这只是 require 出来的一个实例),但是我们可以修改:

1
require(["path/to/module"]).prototype.makePrivatePassword

相当于覆盖掉原型链上的函数定义,那么以后每次 require 出来的实例在调用 makePrivatePassword 时就会调用我们的函数,完美实现覆盖声明。(这个时候就可以理解为什么刚才在百度上搜到的教程要修改 prototype 了)

最终成型的、封装好的函数如下:

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
/** 
* @author JieJiSS
* @description 用 Babel 转译过的代码,前面一大堆都是当时闲的没事干写的 AssertionError 类
*/

(function runBeforeShare(pswd) {
"use strict";
var _MOD_PATH = "function-widget-1:share/util/shareFriend/createLinkShare.js";
pswd = String(pswd);

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) {
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
}
var AssertionError = (function AssertionErrorFactory(_Error) {
_inherits(AssertionError, _Error);

function AssertionError(reason) {
_classCallCheck(this, AssertionError);

var _this = _possibleConstructorReturn(this, (AssertionError.__proto__ || Object.getPrototypeOf(AssertionError)).call(this, reason));

_this.name = "AssertionError";
return _this;
}
AssertionError.prototype.valueOf = function valueOf() {
return this;
}

return AssertionError;
})(Error);
function _assert(a, b, description) {
if(a !== b) {
throw new AssertionError(
"Assert failed: " +
String(a) + " !== " + String(b) +
description ? (" (" + description + ")") : ""
);
}
}

_assert(pswd.length, 4);
_assert(/^[0-9A-Za-z]+$/.test(pswd), true, "regexp /^[0-9A-Za-z]+$/ test failed");
require([_MOD_PATH]).prototype.makePrivatePassword = function () { return pswd };
})(prompt("Set Password:"));

来源:https://blog.jiejiss.com/