前言
大家好呀,欢迎来到我的博客!!! 本期我将带来b站so层sign算法实现
设备: pixel4 android10
下载地址: aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzI4MTI5MS9oaXN0b3J5X3Y2MTgwNTAw
版本: 6.18.0
工具: charles(抓包) socksdroid(流量转发) jadx(反编译dex) ida(反编译so)
我看了下b站这算法并没有更新,但是so层做了混淆,新版的不太好分析,所以干脆分析老版本的,最新版的so层混淆如下图
目录
前言
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
抓包
定位sign位置
用32位的ida把它转为汇编
算法
总结
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
抓包
1 抓包没什么说的,往上滑或往下滑都能刷新抓到包
2 翻页后文本对比下发现只有两个时间戳是不同的然后加密得到不同的sign,其他参数可以暂时固定
定位sign位置
3 首先把apk拖到jadx反编译一下,这个时候大部分都会去搜字符串,但是由于很多地方都使用了sign关键字,直接搜的话太麻烦了,所以也可以搜查询参数中比较特殊的字符串,比如ad_extra,banner_hash,statistics,他们最终肯定会参与params的组装,然后可以顺着找到sign 我这里采用的是hook hashmap的put方法,这样比搜索快一些,代码如下
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中的函数代码
// 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。
var addrRegisterNatives = null;
// 列举 libart.so 中的所有导出函数(成员列表)
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
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方法即可看出
Java.perform(function (){
let LibBili = Java.use("com.bilibili.nativelibrary.LibBili");
LibBili["s"].implementation = function (sortedMap) {
console.log(`LibBili.s is called: sortedMap=${sortedMap}`);
let result = this["s"](sortedMap);
console.log(`LibBili.s result=${result}`);
return result;
};
}
)
算法
17 最终sign python算法,z最终生成的与params中的一样,验证算法是正确的
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)
总结
1出于安全考虑,本章未提供完整流程,调试环节省略较多,只提供大致思路,具体细节要你自己还原,相信你也能调试出来.
2本人写作水平有限,如有讲解不到位或者讲解错误的地方,还请各位大佬在评论区多多指教,共同进步,也可加本人微信lyaoyao__i(两个_)