XueSeason/Hacker

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 构造函数创建出来的,构造参数通过 gw 方法生成。

继续检查第一行代码和最后一行代码:

(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.yfgwindow.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.jsDa 的函数声明。

继续看 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_PCOMMENTS_BG_ICOMMENTS_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_PCOMMENTS_BG_ICOMMENTS_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()