# 前言

最近萌生了想去漫展的想法,毕竟这么久了还没有去过一次漫展嘞,听说 cp30 好像不错,不过问题就是票特别的难抢😔

趁现在 cp30 买票还没开始,今天也没有什么其他的事情,所以就简单分析了一下 b 站的 app,写了个抢票脚本自娱自乐一下

感觉这个 app 还是很有意思的,它把主要的 webview 业务逻辑放到子进程里运行我觉得还是很不错滴,这样 frida 注入到主进程就 hook 不到了(我就被卡了快一个小时!)

但这个抢票脚本写的相当的简陋,我就不好意思放出来啦😂不过相信大家在 github 上应该可以找到更加完善的项目吧 :)

还有就是为什么不直接去 b 站会员购网页端分析呢,这样购票接口不是一下子就看到了嘛就不需要绕各种检测了

just for fun!

# 基本信息

包名: tv.danmaku.bili

入口: tv.danmaku.bili.MainActivityV2

# frida 反调试

先看看会员购页面是哪一个 activity

PS D:\work\analysis\bilibili> adb shell "dumpsys activity top | grep ACTIVITY"
  ACTIVITY com.google.android.apps.nexuslauncher/.NexusLauncherActivity ad36d3f pid=2139
  ACTIVITY tv.danmaku.bili/.MainActivityV2 e753a0a pid=12505

用 frida 写个简单的 hook 脚本注入进去看看情况

function hook(){
    Java.perform(function(){
        const activity = Java.use("com.mall.ui.page.base.MallWebFragmentLoaderActivity");
        activity.onCreate.overload('android.os.Bundle').implementation = function(x){
            const retv = this.onCreate(x);
            return retv;
        }
    })
}
setImmediate(hook,0);
oriole:/data/local/tmp # /data/local/tmp/fs16.3.3 -l 0.0.0.0:1234
adb forward tcp:1234 tcp:1234
frida -H 127.0.0.1:1234 -l .\hook.js -f tv.danmaku.bili

注入进去之后不出意料的卡在主界面不动了

image-20240713222856973

去看看是在哪一个 so 卡住的

function hook_dlopen() {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    console.log("load " + path);
                }
            }
        }
    );
}
setImmediate(hook_dlopen)

哈哈没想到是 libmsaoaidsec.so , 这个 so 我在很多的 apk 里面都看到过了,按照以前逆向的经验这个 so 里面没有任何的业务代码,在 init_proc 里面是纯的检测逻辑

image-20240713222907116

我对 libmsaoaidsec.so 里面的控制流平坦化还是很感兴趣的,之后会去专门分析一下这个 so:)

image-20240713222916985

现在我们就用最简单的方法去反调试好啦,就是不让 app 加载这个 so, 具体做法就是在打开这个 so 时,把要加载的 so 的字符串置空

function hook_dlopen() {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                if(path.indexOf('libmsaoaidsec.so') >= 0){
                    ptr(pathptr).writeUtf8String("");
                }
                console.log("load " + path);
            }
        }
    });
}
setImmediate(hook_dlopen_anti)

再次注入代码之后就 hook 成功了

image-20240713222925964

# webview chrome 调试

Device Monitor 看一下购票的页面,发现是套了一个 WebView

image-20240713222934485

想要在 android 中使用 chrome 的 devtool 开启 webview debug, 需要注入下面的 frida 脚本

function webview_debug() {
    Java.perform(function () {
        var WebView = Java.use('android.webkit.WebView');
        WebView.$init.overloads.forEach(function(init) {
            init.implementation = function() {
                // 调用原始构造方法
                var instance = init.apply(this, arguments);
                // 打开 WebView 的调试功能
                WebView.setWebContentsDebuggingEnabled(true);
                console.log('[*] WebView debug open~');
                // 返回实例
                return instance;
            };
        });
    });
}

然后使用 USB 将电脑和手机相连

在电脑端的 chrome 打开 chrome://inspect/#devices

之后我们点击这个 Port forwarding 按钮配置端口转发

image-20240713222956316

然后 选一个端口点击 Done

image-20240713223022350

但是这样做并没有什么网页可以 inspect

image-20240713223028761

通过 device monitor 来看,这个 b 站肯定是调用了 Webview 的,那为什么这个脚本没有 hook 到 Webview 的创建呢?我觉得可能的原因就是 webview 并不是在主进程被创建的,而是在子进程被创建的,我们打印一下 bilibili 建立的进程来看看情况

image-20240713223040869

的确,除了主进程之外,还多了其他的四个进程,感觉这个 web 进程很可疑呀

那写个 python 脚本让 frida 也去注入这个 tv.danmaku.bili:web 子进程好啦

import codecs
import frida
import sys
import threading
device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")
pending = []
sessions = []
scripts = []
event = threading.Event()
jscode = open('./hook.js', 'r', encoding='utf-8').read()
pkg = "tv.danmaku.bili"  #包名
def spawn_added(spawn):
    event.set()
    if spawn.identifier == pkg or spawn.identifier == f"{pkg}:web":
        print('spawn_added:', spawn)
        session = device.attach(spawn.pid)
        script = session.create_script(jscode)
        script.on('message', on_message)
        script.load()
    device.resume(spawn.pid)
def spawn_removed(spawn):
    print('spawn_removed:', spawn)
    event.set()
def on_message(spawn, message, data):
    print('on_message:', spawn, message, data)
def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)
device.on('spawn-added', spawn_added)
device.on('spawn-removed', spawn_removed)
device.enable_spawn_gating()
event = threading.Event()
print('Enabled spawn gating')
pid = device.spawn([pkg])
session = device.attach(pid)
print("[*] Attach Application id:", pid)
device.resume(pid)
sys.stdin.read()

这样就 hook 到子进程啦

image-20240713223048889

打开了 webview 的 debug 之后,终于可以 inspect 了!

image-20240713223059764

之后的过程就很简单啦

  1. 点击漫展详情,抓个包
  2. 点击立即购票,抓个包
  3. 点击提交订单,抓个包

我写了一个小脚本还挺好用的,用这个 js 脚本可以读取剪切板里的 curl 请求并转换为 python 的 request 请求复制到剪切板上 :)

剪切板里的 curl 请求是从这里来的

image-20240713225910636

记得安装一下包就好了

npm i curlconverter
npm i copy-paste
import * as curlconverter from 'curlconverter';
import ncp from 'copy-paste'
let cmd = ncp.paste()
var res = curlconverter.toPython(cmd);
console.log(res)
ncp.copy(res, function () {
  console.log("OK")
})

分析一下接口,写一下脚本就抢票成功啦

image-20240713225457049

waiting for cp30!

更新于 阅读次数