申明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
抓包
1. 抓包没什么说的,往上滑或往下滑都能刷新抓到包
2.翻页后文本对比下发现只有两个时间戳是不同的然后加密得到不同的sign,其他参数可以暂时固定
定位sing的位置
3.首先把apk拖到jadx反编译一下,这个时候大部分都会去搜字符串,但是由于很多地方都使用了sign关键字,直接搜的话太麻烦了,所以也可以搜查询参数中比较特殊的字符串,比如ad_extra,banner_hash,statistics,他们最终肯定会参与params的组装,然后可以顺着找到sign 我这里采用的是hook hashmap的put方法,这样比搜索快一些,代码如下
[JavaScript] 纯文本查看 复制代码
Java.perform(function (){
function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b) {
if(a.equals("ad_extra")){
showStacks();
console.log("hashMap.put: ", a, b);
}
return this.put(a, b);
}
}
)
4.只有一处,顺着堆栈找可以找到com.bilibili.okretro.f.a.d
5.如下图
6.点击h这个方法里
7.再点进g这个方法 ,g加载s这个native方法
8.s来源于libbili.so这个文件
9.找到对应的so文件,只有32位的arm架构,下面那个是模拟器的,所以选择第一个里面的libbili.so
用32位的ida把它转为汇编
10.在导出表里面没有发现java的字眼,所以是动态注册,同时可以看到标志JNI_OnLoad,代表动态注册
11.点进去JNI_OnLoad 发现里面嵌套了很多函数,找到RegisterNatives动态注册很麻烦,这里可以用hook脚本,输出当前类下的所有native方法对应c中方法的偏移量,然后再找到c中的函数代码
[Python] 纯文本查看 复制代码
// 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。
var addrRegisterNatives = null;
// 列举 libart.so 中的所有导出函数(成员列表)
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols;
console.log(symbol.name)
//_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
if (symbol.name.indexOf("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("RegisterNatives") >= 0 &&
symbol.name.indexOf("CheckJNI") < 0) {
addrRegisterNatives = symbol.address;
console.log("RegisterNatives is at ", symbol.address, symbol.name);
//break
}
}
if (addrRegisterNatives) {
// RegisterNatives(env, 类型, Java和C的对应关系,个数)
Interceptor.attach(addrRegisterNatives, {
onEnter: function (args) {
var env = args[0]; // jni对象
var java_class = args[1]; // 类
var class_name = Java.vm.tryGetEnv().getClassName(java_class);
var taget_class = "com.bilibili.nativelibrary.LibBili";
if (class_name === taget_class) {
//只找我们自己想要类中的动态注册关系
console.log("\n[RegisterNatives] method_count:", args[3]);
var methods_ptr = ptr(args[2]);
var method_count = parseInt(args[3]);
for (var i = 0; i < method_count; i++) {
// Java中函数名字的
var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
// 参数和返回值类型
var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
// C中的函数内存地址
var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
var name = Memory.readCString(name_ptr);
var sig = Memory.readCString(sig_ptr);
var find_module = Process.findModuleByAddress(fnPtr_ptr);
// 地址、偏移量、基地址
var offset = ptr(fnPtr_ptr).sub(find_module.base);
//console.log("name:", name, "name:", sig, "module_name:", find_module.name, "offset:", offset);
console.log("name:", name, "name:", sig, "offset:", offset);
}
}
}
});
}
12.这里因为app一开始加载的时候已经加载了所有的so文件,所以需要以重新启动app的方式来hook,而不能通过附加形式 hook,如下图
frida -U -f tv.danmaku.bili -l hook_code.js
13.这里在ida里按G就能弹出运行框,输入偏移量就可以调到指定函数位置 14.转化JNIEnv对象后如下图 15.往下滑看返回值 这里其实是进行了一堆md5加密,参数1是url路径后面的参数(除了sign),参数2就是加密的sign
16.通过java层hook s方法即可看出
[JavaScript] 纯文本查看 复制代码
Java.perform(function (){
let LibBili = Java.use("com.bilibili.nativelibrary.LibBili");
LibBili["s"].implementation = function (sortedMap) {
console.log(`LibBili.s is called: sortedMap=${sortedMap}`);
var map = Java.use("java.util.TreeMap")
var dict = Java.cast(sortedMap,map)
console.log('传入的对象====>',dict)
let result = this["s"](sortedMap);
console.log(`LibBili.s result=${result}`);
return result;
};
}
)
算法
17.最终sign python算法,z最终生成的与params中的一样,验证算法是正确的
[Python] 纯文本查看 复制代码
import hashlib
from urllib.parse import quote, unquote
params = {
"ad_extra": "E86F4CFF1F8FA890A75155EEAA51E6AE4FA9DBE62FCE708186D0CE5EF37B86948620D8BA1D991685B1288E2EDE09C6D52F8C2D33D59872EAE1EB776D11F71523CE1AF2112D8A950B98F6A1A48F848BC6871A849C3ED14308F46431A85625726A929A8906FA0C16FEE2CEB33209AE6F1E0C6856961045F53A0FE3470E4E223F48DAE7923040EAC4541BE6F728DEA350329AC40887CB773083BB4D6D91DCCCDF8D16C5672A5E344293F5EFD2F3654B88602781A8869076E96FF8359FC76D3CD5851A733D0CF38E11DC869D660D1624928815C2A13497B215CCEA52053B302039B9B93DFABDD6A71A16AC8898285A37C7DEB5AB5ADD788C2456B5D9B2F8FDB1ACD334E8127D56B144B523155DE8AB49A1D1173CB590E379CCF33EFAE8C388100D5CEA7AD220E2AAA2256FF16D4BE28C8AA3D7BAE19B1FE6AA860276BB86B27ACCA34B8E081D67E8C699CF4ED4D7A45E8556B05584B35B1E11E80B9B41DC51C47B260C602E07B1936C73DDB8D7FFBBD148894822C5F7C9A688C5A25DB2CA92D77CA7C35E3AFD807D0AE95967943A42B30D0F0EF8EAD9D2E74A20BB4EA72014B5A3BFD53B2ECFB15B47455D97A4FBFDDFB4A3E30853E0B9CBF16AC70F25CB6B939540328256BF42AB6DF9D3DD4649E3F0B340376B162F859D5EE92D99A778FB313E28BCAD195FB59A30EC374436735A9732BF013A78FE3F606425B48A74137C267DAB91A00962C5FFECB7E798AC130FCF7F9428A05082C6D717D13F129F809818E05DB11EA3DE2EA80728D30DB7EECAE1231085C4E3B47B98506F261D89D15997AC09FB46DBA3444A438F43A59D232385F3C5548DFF3F51733A80A80880E7945035A18DCDDCDEB85A2DCCF755CD1AEBCA759CB2BE4AF6D5AB3A9FA8F7429DD37B740E33D80E1F11B8BD4DD312DEAECEEBAB7DC6FF57EFC5A81D3D7D02E798AA5CDCD387EAD885EE8D89368FA301463658FA52",
"appkey": "1d8b6e7d45233436",
"autoplay_card": "11",
"autoplay_timestamp": "0",
"build": "7500300",
"c_locale": "zh-Hans_CN",
"channel": "alifenfa",
"column": "2",
"column_timestamp": "0",
"device_name": "Pixel 4",
"device_type": "0",
"disable_rcmd": "0",
"flush": "8",
"fnval": "464",
"fnver": "0",
"force_host": "0",
"fourk": "1",
"guidance": "0",
"https_url_req": "0",
"idx": "1698066410",
"inline_danmu": "2",
"inline_sound": "1",
"interest_id": "0",
"login_event": "0",
"mobi_app": "android",
"network": "wifi",
"open_event": "",
"platform": "android",
"player_net": "1",
"pull": "false",
"qn": "32",
"recsys_mode": "0",
"s_locale": "zh-Hans_CN",
"splash_id": "",
"statistics": "{\"appId\":1,\"platform\":3,\"version\":\"7.50.0\",\"abtest\":\"\"}",
"ts": "1698066416",
"video_mode": "1",
"voice_balance": "0",
# "sign": "002c2395f37e8800095c41e08b652517"
}
sorted_params = sorted(params.items(), key=lambda x: x[0])
sorted_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
original_string = sorted_str
# 分割字符串,然后仅对值进行编码
parts = original_string.split("&")
encoded_parts = []
for part in parts:
key, value = part.split("=")
encoded_value = quote(value)
encoded_parts.append(f"{key}={encoded_value}")
# 重新组合编码后的部分
encoded_string = "&".join(encoded_parts)
# 盐值 560c52ccd288fed045859ed18bffd973
str = encoded_string+'560c52ccd288fed045859ed18bffd973'
sign = hashlib.md5(str.encode('utf-8')).hexdigest()
print(sign)
出于安全考虑,本章未提供完整流程,调试环节省略较多,只提供大致思路,具体细节要你自己还原: