# 前言
最近萌生了想去漫展的想法,毕竟这么久了还没有去过一次漫展嘞,听说 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 |
注入进去之后不出意料的卡在主界面不动了
去看看是在哪一个 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
里面是纯的检测逻辑
我对 libmsaoaidsec.so
里面的控制流平坦化还是很感兴趣的,之后会去专门分析一下这个 so:)
现在我们就用最简单的方法去反调试好啦,就是不让 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 成功了
# webview chrome 调试
用 Device Monitor
看一下购票的页面,发现是套了一个 WebView
想要在 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
按钮配置端口转发
然后 选一个端口点击 Done
但是这样做并没有什么网页可以 inspect
通过 device monitor
来看,这个 b 站肯定是调用了 Webview
的,那为什么这个脚本没有 hook 到 Webview 的创建呢?我觉得可能的原因就是 webview 并不是在主进程被创建的,而是在子进程被创建的,我们打印一下 bilibili 建立的进程来看看情况
的确,除了主进程之外,还多了其他的四个进程,感觉这个 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 到子进程啦
打开了 webview 的 debug 之后,终于可以 inspect 了!
之后的过程就很简单啦
- 点击漫展详情,抓个包
- 点击立即购票,抓个包
- 点击提交订单,抓个包
我写了一个小脚本还挺好用的,用这个 js 脚本可以读取剪切板里的 curl 请求并转换为 python 的 request 请求复制到剪切板上 :)
剪切板里的 curl 请求是从这里来的
记得安装一下包就好了
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") | |
}) |
分析一下接口,写一下脚本就抢票成功啦
waiting for cp30!