吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5133|回复: 27
上一主题 下一主题
收起左侧

[Web逆向] 【2023-11-26】分析 A+1 站的获取 UP 视频投稿接口变动

  [复制链接]
跳转到指定楼层
楼主
LoveCode 发表于 2023-11-27 00:11 回帖奖励
本帖最后由 LoveCode 于 2023-11-28 01:10 编辑

问:等等??这 A+1 站 是什么意思?

答:这……(踱步靠近,又见四下无人,小声说)你对着英文字母表看一看,对!就这个,你看 A 后面是谁。

【2023-11-28】说明
具体见我在评论区的紧急补充……是我想的太复杂了,没有详细测试,咳咳……失误失误,思维惯性误导了。
不过文章还是有可取之处的吧……大概……。

风云又起:接口变化

今天修炼完成后准备看点好看的放松一下,结果我的 A+1 站 小脚本报错了。

猜测是请求的接口有了变化。

爬虫不能爬太狠,开发网站的人会掉头发,做爬虫的人也掉头发,最后大家都掉头发

随便找一个 up 主页的链接:https://space.A+1站.com/up 的 mid/video,在无痕浏览器中打开,然后抓包、快速定位到相关请求。

发现接口的确变化了:多了 3 大天王

为什么上面的 dm_img_list[],又为什么称它们为 3 大天王 呢??

  • 如果不是在无痕浏览器中直接打开网址测试,那么 dm_img_list 的值如下图,咳咳,没必要给自己找麻烦了,就默认 dm_img_list 这样吧。
  • 至于另外的两大王,也不简单,具体见后文。

当然啦,A+1站 又不是小网站,大佬肯定早已发现、解决了。

  1. 先去 A+1哩A+1哩-API收集整理 项目查看 用户空间相关 | BAC Document,发现文档还没有更新,可能网站才刚刚更新那 3 大天王,文档稍微滞后一些。

  2. 在其 github 项目的 网页端接口加了新的校验 · Issue #868 中大家有所讨论。

为什么头像、用户名打码?

嗯……参考新闻联播,所以还是打码吧,不过内容是无法打码了。感兴趣可以去对应 issue 查看。

其中一个大佬给出了代码,并且经过测试得到了相同的信息……厉害呀!

还有一位道友给出了本站的链接,不过不知道是什么原因,两天了这个帖子还打不开……(小声说)所以你会看到本文的图片对敏感信息已经打码了。

总之,问题其实已经解决了。不过——因为我自己在用这个接口,自然要了解它的一些变化以便将来应对它的其它变化啦。而在上面的 issues 中大家并没有说分析过程,所以我试着分析一下,发现其中弯弯绕绕 “有点意思”,才有了这篇文章。


诡异身法:找不到的它

直接搜索关键字并没有结果。

如果使用 xhr 断点,哼哼,好长好长的调用栈,并且夹杂了很多异步操作…………我拒绝分析!!

当然,如果全局搜索请求的部分内容,如搜索字符串 wbi/arc/search 倒是能找到几个地方,但还需要分析很久。

以前碰到过这种情况,通常情况下 xhr 断点、搜索请求的 url 这两个方法基本能找到参数的位置,如今遭遇到打击了。

经过在必应搜索关键词:js 逆向搜不到关键字,查看了一些文章,主要是在JS逆向|加密参数定位的另一种方法及风控的一些思考-CSDN博客受到启发:使用浏览器的内存工具来分析。

仙器 chatGPT

如下,在 xhr 断点处生成内存快照,然后搜索 dm_img_str 参数的值 ,可以确定它是 base64 encode 的字符串。

进行 base64 decode,得到的是 一个和 webgl 有关的、很像是版本号的字符串

通过一个名为 openAi 的神秘组织锻造的仙器 chatGPT,得知 webgl 获取其版本信息的相关函数,从而定位到 dm_img_str 的生成位置!

这比我去 MDN 看 webgl 文档、或者必应搜索好多了,不愧是仙器。

同理,也能定位到 dm_cover_img_str 参数的生成位置!

运气也是实力嘛~

咳咳,上述情况只能说 “凑巧”,谁让它最后多了两个 == 号呢……不过这样的事情我也有点不满意,下次碰到了没有那两个 == 号让我辨认该怎么办??而且这种内存分析方式我还是第一次见到,多试一试。

下面再说明一下这种内存分析的方式

注意,如果关键字的名称很短,比如 "d",那么这种方法也可能失效,就像平时全局搜索关键字时一样,关键字很短时效果就差很多啦。

在本文的番外篇中讨论这个问题。

在内存快照中搜索参数的名字 dm_img_str(名字越长效果越好,就像普通的全局搜索关键字一样),找到了它的马甲:webglStr

然后全局搜索 webglStr: 或者 webglStr=,统统打上断点,因为我们需要找到对它赋值的地方,那里可能就是 dm_img_str 的来历。

其实打断点调试多处的 webglStr 时,就可以看到一些 weglStr 包含的就是 dm_img_str 的值。

大概是如下这种情况

  • 曾经有某个 x 对象, x = { webglStr: "dm_img_str"}
  • 然后有一个变量也名为 webglStr,它存储 dm_img_str 参数的值
  • 另一个对象 y 有这种操作: y[x.webglStr] = webglStr
  • 就是因为全部都用了同一个名字,导致全局搜索 webglStr 时顺便把真正的 dm_img_src 值给搜出来了。嘿~!真凑巧呢!

    不过都到这地步了还是多多讲一些

如下,见到了其来历。

可是,即便知道了 dm_img_str 名字的由来 ,那又如何根据这个名字找到 dm_img_str 参数值生成的地方呢,上述代码中也没见到和它参数值有关的代码呀。

下面就是重点了,仔细观察这个名为 dm_img_str 的字符串,它有几个非常重要的特点:

  1. 它是一个字符串。(废话,大家都看不出来吗??)。
  2. 它是作为名称来使用的,也就是说它将来会作为请求中参数的名字。
  3. 正因为它会作为 “名字” 使用,反而对它的处理就会非常简单:通常只有取值操作而已,谁还能对一个本该作为名称使用的字符串做些奇奇怪怪的操作,计算机性能强也不是用在这里的嘛。
  4. 那么将该字符串替换成 Proxy 对象,找到取它的值的地方不就好了!并且那么所有关于该字符串赋值地方都会复制同一个 Proxy

所以,准备如下 hook 函数!

// 快速根据字符串创建代{过}{滤}理,从而跟踪它的取值
window.create_str_proxy = function (str) {
    let temp = { "v": str };

    return new Proxy(temp, {
        get(target, property, receiver) {
            console.log("访问代{过}{滤}理字符串的属性: ", property);

            if (typeof temp["v"][property] === "function") {
                if (property === "toString" || property === "valueOf") {
                    // 到取值的地方就暂停!!!
                    debugger;
                }
                // 如果访问底层字符串方法还需要绑定 this 哟
                return temp["v"][property].bind(temp["v"]);
            }
            // 此时就是访问字符串的普通属性啦
            return temp["v"][property];
        }
    })
}

点击运行,找到了~~~!

而上图中 s.webglStr = B(s.webglStr), 的  B 函数如下,嘿!还真把末尾的两个等号去掉了。

根据上面的分析,只要继续找所有 webglStr 就可以找到了!

同理,利用上面的 Proxy 原理,也可以找到 "dm_cover_img_str" 的参数值啦!!

结束了吗??

也就是说:dm_img_str 是当前浏览器的 webgl 的版本(具体我也不清楚,没用过 webgl),dm_cover_img_str 是显卡的信息,这两个值几乎可以认为是固定的,所以直接把浏览器实际请求使用的这两个参数值拿过来添加到以前的代码中即可。

至于 dm_img_list 设置为空就行,它太长了,反正没有它照样可以请求。

另外,经过测试,w_rid(另一个重要的参数,不在本文讨论范围之类)的生成并没有依赖上面的 3 大天王

至此,算是勉强解决了。


试试看 dm_img_list

都到这里了,干脆也就试着分析它吧(虽然它的用处不大……)。

如下,可以确定它是利用 JSON.stringify 转换而来。

全局搜索关键词 JSON.stringify 会有很多结果,这时候就需要通过 hook 了。

let my_json_stringify = JSON.stringify;

JSON.stringify = function (a) {
    console.log("========== JSON stringify Start ==========");

    // 根据上图的分析,传给 stringify 的是一个数组哟
    // 而数组的元素是一个对象,含有 x、y、z 等属性,根据这些特征进行详细地判断
    if (Array.isArray(a) && a[0]?.hasOwnProperty("x") && a[0]?.hasOwnProperty("y") && a[0]?.hasOwnProperty("z") && a[0]?.hasOwnProperty("timestamp")) {
        debugger;
    }
    console.log(a);

    console.log("========== JSON stringify End ==========");

    return my_json_stringify(a);
}

如下,在进行翻页时看到了目标!

上述的 t 其实就是下图中的 s.userLog,并且意外的看到了其它 2 大王。

所以关键数据来自于上面的 this.getLog(o, i) 方法,最后经过查找,它取了 this.logStack 的值进行处理。

而要往数组添加数据有 .push、unshift 等等,找到了它的代码。此处并没有详细分析,只是看了个大概,毕竟没有它也能访问接口啦,只是好奇

  • 在翻页的时候会记录鼠标、键盘操作,然后发给服务器。

  • 我也可以在浏览器中输入网址直接进入对应页数的网页,所以这个 dm_img_list 参数可以为空,直接默认它为 [] 即可。


尾声

修为越高,斗法的对手也越强,就更需要好的灵器、并且熟练运用它们。

其实,利用合适的灵器,今天的 3 大天王 能很快突破。

  • 比如有写好的 hook 能直接将各种常用的解密函数的结果输出
  • 只要将这些输出和 dm_img_str 的值稍微一对比就能知道其位置的了。

如下,在 v_jstools 中启动这些东西。

然后在控制台搜索相关参数名字是能搜索到的,不过 A+1 站自己定义了 btoa 函数,导致插件没有将其 hook 掉,故无法直接搜索 dm_img_str 的参数值啦,不过知道了 hook 原理也能直接修改网站的 js 文件啦。


番外:搜索的关键字太短

无论是以前的全局搜索关键字,还是今天的在内存分析界面中搜索关键字都有一个缺点:如果关键字太短则搜索结果太多。

此时可以试一试 浏览器内存漫游 的方法,可以理解为它是一种大型的 hook,据作者所说可以做到 “通杀”,具体见 GitHub - JSREI/ast-hook-for-js-RE: 浏览器内存漫游解决方案(探索中...)

我并没有尝试,现有的灵器还用不顺手呢,也许当关键字隐藏太深时,可以一试。

免费评分

参与人数 5吾爱币 +7 热心值 +5 收起 理由
笙若 + 1 + 1 谢谢@Thanks!
三滑稽甲苯 + 2 + 1 用心讨论,共获提升!
ceyowa + 1 + 1 非常实用的方法,第一次学到了
Tonyha7 + 2 + 1 用心讨论,共获提升!
熊大熊二汪汪队 + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

来自 9#
 楼主| LoveCode 发表于 2023-11-28 01:05 |楼主
本帖最后由 LoveCode 于 2023-11-28 02:23 编辑

紧急补充

咳咳,这个……(有点不好意思)之前因为接口的链接中多了 3 个参数(也就是 dm 开头的那 3 个),而我把这些参数添加到代码中时就可以运行了,这才以为它们是关键。

不过,之后我发现陷入误区了,经过测试发现不需要那 3 个参数也可以。

不清楚是不是挑时间还是什么情况

如下是尽可能将参数省略了,甚至省略了本文的 3 大王。连续请求 10 次,偶尔失败。

import requests

headers = {
    # "authority": "api.bilibili.com",
    # "accept": "*/*",
    # "accept-language": "en,zh-CN;q=0.9,zh;q=0.8",
    # "cache-control": "no-cache",
    "origin": "https://space.bilibili.com",
    # "pragma": "no-cache",
    "referer": "https://space.bilibili.com/2142762/video",
    # "sec-ch-ua": "^\\^Chromium^^;v=^\\^118^^, ^\\^Microsoft",
    # "sec-ch-ua-mobile": "?0",
    # "sec-ch-ua-platform": "^\\^Windows^^",
    # "sec-fetch-dest": "empty",
    # "sec-fetch-mode": "cors",
    # "sec-fetch-site": "same-site",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.76",
}
cookies = {
    "buvid3": "58CBD4ED-4D82-666C-8EFE-337ABCED53DB95963infoc",
    "b_nut": "1701088795",
    # "_uuid": "65342583-1E84-E6105-F872-1A9C6FE2E38797168infoc",
    # "buvid4": "99832353-6338-2D1C-7AB7-F89A1A5DA21297090-023112712-",
    # "bili_ticket": "eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDEzNDc5OTcsImlhdCI6MTcwMTA4ODczNywicGx0IjotMX0.UcheAgY8MK1JqO6yqWO9vm0fJKjI06-F5BMUVucj2fk",
    # "bili_ticket_expires": "1701347937",
    # "buvid_fp": "094f1315a3795ae7695094f66ba4518e",
    # "b_lsid": "D810BE96C_18C1193F406",
    # "PVID": "1",
    # "innersign": "0",
    # "enable_web_push": "DISABLE",
    # "header_theme_version": "CLOSE",
    # "home_feed_column": "4",
    # "browser_resolution": "945-671"
}
url = "https://api.bilibili.com/x/space/wbi/arc/search"
params = {
    "mid": "2142762",
    "ps": "30",
    "tid": "0",
    "pn": "1",
    "keyword": "",
    "order": "pubdate",
    "platform": "web",
    "web_location": "1550101",
    "order_avoided": "true",
    # "dm_img_list": "^\\[^\\]",
    # "dm_img_str": "V2ViR0wgMS4wIChPcGVuR0wgRVMgMi4wIENocm9taXVtKQ",
    # "dm_cover_img_str": "QU5HTEUgKEludGVsLCBJbnRlbChSKSBIRCBHcmFwaGljcyA2MzAgKDB4MDAwMDU5MUIpIERpcmVjdDNEMTEgdnNfNV8wIHBzXzVfMCwgRDNEMTEpR29vZ2xlIEluYy4gKEludGVsKQ",
    "w_rid": "0ea56ea45305ee113a85d1742ff3be12",
    "wts": "1701105288",
}
for i in range(10):
    response = requests.get(
        url, headers=headers, cookies=cookies, params=params
    )
    print(response.text[:50])

并且随着请求次数越多,这个失败的次数也越多。

但是在请求头中加入 "accept-language": "en,zh-CN;q=0.9,zh;q=0.8" 就可以连续请求了。

headers = {
    "accept-language": "en,zh-CN;q=0.9,zh;q=0.8",     # 取消它的注释,即启用它就可以了
    # ... 其它代码都不变化
}

ok = 0
for i in range(50):
    response = requests.get(
        url, headers=headers, cookies=cookies, params=params
    )
    t = response.text[:50]
    print(t)
    if ('"code":0' in t):
        ok += 1

print(f"{ok} / 50")

直接请求 50 次!(请原谅我的粗鲁)

点评

请把这个部分也添加到正文里,方便精华集(如果有的话)采集和整理。也方便整体食用  详情 回复 发表于 2023-12-4 10:59

免费评分

参与人数 2吾爱币 +3 热心值 +1 收起 理由
阿斯顿 + 1 谢谢@Thanks!
fortytwo + 2 + 1 深入浅出,值得学习!

查看全部评分

推荐
bzgl666 发表于 2023-11-27 22:55
沙发
xixicoco 发表于 2023-11-27 17:12
3#
Tonyha7 发表于 2023-11-27 17:52
太强了!完美的分析过程
4#
mingdia 发表于 2023-11-27 18:09
真的好强,膜拜
5#
james180 发表于 2023-11-27 19:55
膜拜大佬
6#
贵宾 发表于 2023-11-27 21:53
{"code":-352,"message":"风控校验失败","ttl":1,"data":{"v_voucher

SESSDATA=   添加这个值就正常了
7#
havealook 发表于 2023-11-27 22:32
茅塞顿开,技术真强
10#
ruancc 发表于 2023-11-29 19:00
日常打卡,支持楼主。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-5 09:20

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表