Youtube 自动化破解
Opened this issue · 3 comments
破解目标
登录破解
GET 获取请求链接:https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fapp%3Ddesktop%26action_handle_signin%3Dtrue%26hl%3Dzh-CN%26next%3D%252F%26feature%3Dsign_in_button&passive=true&hl=zh-CN&service=youtube&uilel=3#identifier
获取 Name 为 GAPS 的 Cookie。
获取 Body 中的 gxf 值:document.querySelector('input[name="gxf"]').value
POST 表单提交到:https://accounts.google.com/signin/challenge/sl/password
带上之前获取到 GAPS Cookie,Body 中需要填写账号密码以及上面获取到 gxf 值。
请求成功后拿到登录到 Cookie,其中比较重要的是 SID 和 SSID。后续点赞,评论等操作务必带上这两个 Cookie。(后续会有一堆 302 重定向,会产生新的 SSID,确保新的 SSID 替换原先的 SSID)
点赞破解
URL: https://www.youtube.com/service_ajax
Method: POST
FormData:
- ltct
- se
- session_token
ltct 和 se 参数保存在点赞按钮的 data-post-data 属性中。
document.querySelector('button.like-button-renderer-like-button').attributes['data-post-data'].value
session_token 保存在隐藏的 input 标签中。
document.querySelector('input[name=session_token]').value 或者 全文搜索 XSRF_TOKEN 字段,对应的值就是 session_token,参考正则:const regex = /\'XSRF_TOKEN\':(.*?)\"(.*?)\"/g
评论破解
URL: https://www.youtube.com/comment_service_ajax?action_create_comment=1
Methond: POST
FormData:
- content
- params
- bgr
- session_token
params 为评论按钮 data-params 属性的值。
document.querySelector('button.comment-simplebox-submit').attributes['data-params'].value
bgr: 暂无解决方案,可能不是必填参数,没有实际测试。
session_token 保存在隐藏的 input 标签中。
document.querySelector('input[name=session_token]').value
可以尝试用getAttribute取属性值,可以避免如果节点没有该属性时的报错
Cannot read property 'value' of undefined
具体代码根据语言和平台实现,这里只是简单的给出取值的例子而已。 @XueRainey
续:
bgr 破解
抓包可以得到 watch.js 然后进行反混淆。
在 common.js 中搜索 bgr 关键字,可以看到如下 X_
方法中有这么一段代码
if ("action_reply" in c) {
var h = a.botguard.invoke();
h && (b.bgr = h)
}
可以看出 bgr 是通过调用 a.botguard.invoke()
方法得到。继续反向引用,查看调用 X_
方法的地方。
大概有四处地方调用了该方法,此时我们需要清楚的是 a 参数是由外部变量 this 传值过来。
我们再搜索关键词 botguard
,可以找到如下代码:
this.botguard = new g.eK(g.w("COMMENTS_BG_P"), g.w("COMMENTS_BG_I", ""), g.w("COMMENTS_BG_IU", ""));
此时大部分细节开始水落石出了。botguard
是由 g
变量的 eK
构造函数创建出来的,构造参数通过 g
的 w
方法生成。
继续检查第一行代码和最后一行代码:
(function (g) {
...
})(_yt_www);
这个 g
其实就是 _yt_www
变量。
为了验证我们上述过程的正确性,我们用 Chrome 打开 Youtube 视频播放页的控制台,输入如下代码:
var bot = new _yt_www.eK(
_yt_www.w("COMMENTS_BG_P"),
_yt_www.w("COMMENTS_BG_I", ""),
_yt_www.w("COMMENTS_BG_IU", "")
)
bot.invoke()
可以看到输出结果:
我们继续在控制台中输入:
_yt_www.w
点击输出结果,会跳转到 base.js
代码的函数声明的地方。w
函数的内部结构:
g.w=function(a,b){return a in g.Ca?g.Ca[a]:b};
这段代码的逻辑是:从 g.Ca
中获取指定属性(也就是 a
)的值,如果不存在,则 b
作为默认值返回。
在控制台中输入 ——yt_www.Ca
会返回一个包含各种 KeyValue 的对象,然而这个对象是怎么创建的呢?
继续全文搜索 Ca
关键字,找到 Ca
被创建的地方:
g.Ca = window.yt && window.yt.config_ || window.ytcfg && window.ytcfg.data_ || {};
我们依次打印这四个变量,发现 window.yt.config_
和 window.ytcfg.data_
符合预期的理想变量。
这时候比较奇怪的是这两个变量的值完全一样,猜想这两个变量应该是同一个对象的引用。
下面的测试代码验证了我们的想法:
window.yt.config_ === window.ytcfg.data_
// output: true
进一步明白这两个变量,我们在控制台打印 window.yfg
和 window.yt
发现更多细节。
yfg
属性非常简单,主要维护的就是 data_
数据,而 yt
相对复杂些。
那就先从简单的开始,在首页 html 中搜索 ytcfg
关键词,很容易找到 ytcfg
的创建代码,反混淆处理后:
var ytcfg = {
d: function () {
return (window.yt && yt.config_) || ytcfg.data_ || (ytcfg.data_ = {});
},
get: function (k, o) {
return (k in ytcfg.d()) ? ytcfg.d()[k] : o;
},
set: function () {
var a = arguments;
if (a.length > 1) {
ytcfg.d()[a[0]] = a[1];
} else {
for (var k in a[0]) {
ytcfg.d()[k] = a[0][k];
}
}
}
};
这份代码除了完全验证我们上述的猜想之外,好像不能再得到太多线索。
重点回到 window.yt.config_
,在首页 html 中可以找到如下反混淆后的代码:
yt.setConfig({
...
});
打印 window.yt.setConfig
,跟踪输出结果,会跳到 base.js
中 Da
的函数声明。
继续看 base.js
中的这段代码:
Ba = function (a, b) {
if (1 < b.length) a[b[0]] = b[1];
else {
var c = b[0],
d;
for (d in c) a[d] = c[d]
}
};
g.Da = function (a) {
Ba(g.Ca, arguments)
};
大致逻辑就是将传过来的对象的属性和值,全都追加到 Ca
。
那么现在问题来了,到底是在什么地方设置了 COMMENTS_BG_P
、COMMENTS_BG_I
和 COMMENTS_BG_IU
的值呢?
这个时候用 Charles
抓包后,全文搜索这几个关键字,很容易找到都藏在请求这个接口 https://www.youtube.com/watch_fragments_ajax
后的 body 。
我们可以用正则表达式进行匹配:
/COMMENTS_BG_P', \\\"(.*?)\\"/g
/COMMENTS_BG_I', \\\"(.*?)\\"/g
/COMMENTS_BG_IU', \\\"(.*?)\\"/g
OK,参数获取这一步解决,下面开始探索如何把这些变量生成一段密文。
在控制台输入 _yt_www.eK
,跟踪到一个 common.js
文件中的一段函数:
g.eK = function (a, b, c) {
this.C = null;
// 执行 g.ye 方法
c ? g.ye(c, (0, g.t)(function () {
this.C = new window.botguard.bg(a)
}, this)) : b && (eval(b), this.C = new window.botguard.bg(a))
};
此时,在分析之前,我们先打印下 COMMENTS_BG_P
、COMMENTS_BG_I
和 COMMENTS_BG_IU
的这三个 Key 对应的值:
_yt_www.w("COMMENTS_BG_P")
_yt_www.w("COMMENTS_BG_I", "")
_yt_www.w("COMMENTS_BG_IU", "")
这里第一个结果是一大堆不是人类看得懂的文字,第二个结果一般是空字符串,第三个结果是一个链接地址。
打开 COMMENTS_BG_IU
的链接地址 https://www.google.com/js/bg/OYTJEu_EAdkyof8iycdBAUNOytG0rjUgsH0FlPvF-mw.js
,可以发现是一个函数调用,里面具体的逻辑不用分析。
回过头来继续分析上述 eK
的逻辑,里面有一个 g.ye
方法和一个 g.t
方法。
在控制台打印,并跟踪:
g.ye = function (a, b) {
// a 为 COMMENTS_BG_IU
var c = g.xe(a);
window.spf.script.load(a, c, b)
};
g.t = function (a, b, c) {
// a 为回调函数
// b 为 g 的 this
// 代码简化后,其实就是 g.t = eaa
Function.prototype.bind && -1 != Function.prototype.bind.toString().indexOf("native code") ? g.t = eaa : g.t = faa;
// 等效于 eaa(a, b, c)
return g.t.apply(null, arguments)
};
eaa = function (a, b, c) {
return a.call.apply(a.bind, arguments)
};
var Jaa = /\.vflset|-vfl[a-zA-Z0-9_+=-]+/;
var Kaa = /-[a-zA-Z]{2,3}_[a-zA-Z]{2,3}(?=(\/|$))/;
var
g.xe = function (a) {
var b = "";
if (a) {
var c = a.indexOf("jsbin/"),
d = a.lastIndexOf(".js"),
e = c + 6; - 1 < c && -1 < d && d > e && (b = a.substring(e, d), b = b.replace(Jaa, ""), b = b.replace(Kaa, ""), b = b.replace("debug-", ""), b = b.replace("tracing-", ""))
}
return b
};
以上逻辑简化下来就是:
g.eK = function (a, b, c) {
window.spf.script.load(c, '', g.t(() => {
this.C = new window.botguard.bg(a)
})
};
我们打印 window.botguard.bg
可以知道这个方法就是 COMMENTS_BG_IU
地址里的方法。
最后简化到最终的代码:
window.botguard.bg(_yt_www.w("COMMENTS_BG_P")).invoke()