0x00 前言
工具:burp、frida、ida
难点:Flutter
小区在很早之前就换了某科技的门禁系统
支持手机开门、门卡开门、人脸开门
所有信息均已脱敏和打马赛克
手机开门软件及其逆天,广告满天飞,一气之下就逆了
0x01 准备
获取安装包
下载最新版发现加壳了,是 蛮犀加固企业壳
很久之前给这软件写过去广告模块,所以已知旧版本是无壳的
所以只用往回找没壳的版本,就能使用 Frida Hook 了
SSL 验证
使用代{过}{滤}理抓包发现无法抓到,并且有 SSL 验证
使用 IDA 打开 libflutter.so
搜索 ssl_client
获取到 SSL 验证 函数为 0x3E0F74
libapp.so 逆向
使用 Blutter 一把梭哈
把脚本导入 IDA
可以看到函数名也全部都还原了
0x02 抓包分析
初始化抓包
导出 Burp 的 SSL 证书,并存到 /data/local/tmp/cert-der.crt
使用 Blutter 提供的 frida 脚本
复制代码 隐藏代码
/*
Android SSL Re-pinning frida script v0.2 030417-pier
$ adb push burpca-cert-der.crt /data/local/tmp/cert-der.crt
$ frida -U -f it.app.mobile -l frida-android-repinning.js --no-pause
https://techblog.mediaservice.net/2017/07/universal-android-ssl-pinning-bypass-with-frida/
UPDATE 20191605: Fixed undeclared var. Thanks to @oleavr and @ehsanpc9999 !
*/
setTimeout(function () {
Java.perform(function () {
console.log("");
console.log("[.] Cert Pinning Bypass/Re-Pinning");
var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
var FileInputStream = Java.use("java.io.FileInputStream");
var BufferedInputStream = Java.use("java.io.BufferedInputStream");
var X509Certificate = Java.use("java.security.cert.X509Certificate");
var KeyStore = Java.use("java.security.KeyStore");
var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
var SSLContext = Java.use("javax.net.ssl.SSLContext");
// Load CAs from an InputStream
console.log("[+] Loading our CA...");
var cf = CertificateFactory.getInstance("X.509");
try {
var fileInputStream = FileInputStream.$new(
"/data/local/tmp/cert-der.crt"
);
} catch (err) {
console.log("[o] " + err);
}
var bufferedInputStream = BufferedInputStream.$new(fileInputStream);
var ca = cf.generateCertificate(bufferedInputStream);
bufferedInputStream.close();
var certInfo = Java.cast(ca, X509Certificate);
console.log("[o] Our CA Info: " + certInfo.getSubjectDN());
// Create a KeyStore containing our trusted CAs
console.log("[+] Creating a KeyStore for our CA...");
var keyStoreType = KeyStore.getDefaultType();
var keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
console.log(
"[+] Creating a TrustManager that trusts the CA in our KeyStore..."
);
var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
var tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
console.log("[+] Our TrustManager is ready...");
console.log("[+] Hijacking SSLContext methods now...");
console.log("[-] Waiting for the app to invoke SSLContext.init()...");
SSLContext.init.overload(
"[Ljavax.net.ssl.KeyManager;",
"[Ljavax.net.ssl.TrustManager;",
"java.security.SecureRandom"
).implementation = function (a, b, c) {
console.log("[o] App invoked javax.net.ssl.SSLContext.init...");
SSLContext.init
.overload(
"[Ljavax.net.ssl.KeyManager;",
"[Ljavax.net.ssl.TrustManager;",
"java.security.SecureRandom"
)
.call(this, a, tmf.getTrustManagers(), c);
console.log("[+] SSLContext initialized with our custom TrustManager!");
};
}, 0);
var libflutter = null;
function onLibFlutterLoaded() {
const ssl_pinning_addr = 0x3e0f74;
Interceptor.attach(libflutter.add(ssl_pinning_addr), {
onEnter: function (args) {
console.log("Disabling SSL validation");
},
onLeave: function (retval) {
console.log("Retval: " + retval);
retval.replace(0x1);
},
});
}
function tryLoadLibFlutter() {
libflutter = Module.findBaseAddress("libflutter.so");
if (libflutter === null) setTimeout(tryLoadLibFlutter, 500);
else onLibFlutterLoaded();
}
tryLoadLibFlutter();
加上 Bypass SSL 验证
开始抓包
禁止检查更新
将 /v1/appVersion 地址替换为空即可绕过更新检查
发送验证码
Host: aHR0cHM6Ly9nYXRld2F5Mi5xaW5saW5rZWppLmNvbQ==
URL: /member/sms/sendSecurityCode
Header
Param
{
"mobile": "" // 手机号
}
数据解释
Header
下列头必须添加
Param
下面为签名所需数据
登录
Host: bW9iaWxlYXBpMy5xaW5saW5rZWppLmNvbQ==
URL: /api/app/v1/login
Param
{
"smsCode": "", // 验证码
"mobile": "", // 加密手机号
"sign": "", // 签名
"version": "4.9.9", // 软件版本
"nonce": "", // 随机数
"appChannel": 1, // 未知 软件渠道?
"timestamp": 0 // 时间戳
}
检查登录
Host: bW9iaWxlYXBpMy5xaW5saW5rZWppLmNvbQ==
URL: /api/app/v1/checkLogin?sessionId=令牌
Param
{
"method": "checkLogin", // 调用方法
"sign": "", // 签名
"version": "4.9.9", // 软件版本
"nonce": "0", // 随机数
"timestamp": 0 // 时间戳
}
获取所有门
Host: bW9iaWxlYXBpMy5xaW5saW5rZWppLmNvbQ==
URL: /api/app/user/v2/queryAllUserDoorByCache?sessionId=令牌
Param
类型为 form data
communityId社区 ID
开门
Host: bW9iaWxlYXBpMy5xaW5saW5rZWppLmNvbQ==
URL: /api/open/doorcontrol/v2/open?sessionId=令牌
Param
签名分析
function functionCallLog(
address,
argsLength = 1,
func_name = ""
) {
Interceptor.attach(libapp.add(address), {
onEnter: function () {
init(this.context);
for (let i = 0; i < argsLength; i++) {
try {
const arg = getArg(this.context, i);
const [tptr, cls, values] = getTaggedObjectValue(arg);
this.info = this.info || [];
this.info.push(
`(${i}) ${cls.name}@${tptr.toString().slice(2)} = ${JSON.stringify(
values,
null,
2
)}`
);
} catch (e) {
this.info = this.info || [];
this.info.push(`(${i}) ${e}`);
}
}
},
onLeave: function (result) {
if (this.skip) {
return;
}
const [tptr, cls, values] = getTaggedObjectValue(result);
var stamp = new Date().getTime() + 8 * 60 * 60 * 1000;
var beijingTime = new Date(stamp).toISOString();
let debug_info = [
`Time: ${beijingTime}`,
`Function name: ${func_name || "unknown"}`,
`Function ptr: 0x${address.toString(16)}`,
`Args:`,
this.info.join("n"),
`Return:`,
`${cls.name}@${tptr.toString().slice(2)} = ${JSON.stringify(values, null, 2)}`,
"RegisterNatives called from:n" +
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
.join("n")
];
console.log("");
console.log("==========================");
console.log(debug_info.join("n"));
console.log("==========================");
console.log("");
},
});
}
添加一个辅助函数
API 签名分析
根据关键词找到 qinlin_utils_Encryption_Encryption::getHeaderSign_c52af0 函数
查看函数发现调用了 common_base_service_task_qapp_service_task__QappEncrypt::md5Encrypt_65fce0 函数
function onLibappLoaded() {
functionCallLog(0x65fce0, 1, "QappEncrypt::md5Encrypt");
functionCallLog(0xc52af0, 2, "Encryption::getHeaderSign");
}
把这两个函数都 Hook 了
可以看到签名生成是请求参数排序后添加 &key=qiAnlPinP 使用 MD5 生成的签名
发送验证码签名分析
可以看到签名生成是请求参数排序后添加 &appsecret=OKFoQ9MmNXQtcyXROo4PnaFkfDPTHuDg 使用 MD5 生成的签名
登录分析
通过关键词搜索可以发现有 AES 加密,猜测手机号加密是使用的 AES
Hook 后也是获取到了加密密钥 FBC213C4C7BEEBD2AA4EDBF0F681C41B
0x03 密码和蓝牙开门
密码开门
搜索关键词后找到获取开门密码的函数
第一个参数 社区ID
第二个参数 设备MAC地址
查看伪代码,发现是把一串文本传入获取MD5然后进行一些操作后获取密钥,分别Hook这两个函数
可以发现生成是使用的
md5(MAC地址 + 当前时间整分时间戳 + 社区ID)
然后替换全部 a-z 字符,取后4位作为密码
注:整分是指 当前时间 2025-01-19 18:15:58 整分 2025-01-19 18:10:00
蓝牙开门
使用 Xposed Hook 蓝牙读写函数,发现只有写入,没有数据返回,因此可以判定,蓝牙开门只向门发送数据而不校验是否开门成功
使用的服务是 0xFFF0
使用的特征是 0xFFF1
使用 ESP32 模拟蓝牙
使用官方的 BLE server multiconnect 改改就行
设置服务UUID
设置特征UUID
设置蓝牙名称
数据接收处理
添加一个替换,把在线替换成离线
让设备离线,使用蓝牙开门而不使用网络开门
由于 ESP32 无法设置MAC地址为组播地址,需要在Burp中添加一个替换规则
把设备MAC地址替换成ESP32的地址
可以看到已经模拟成功
数据分析
搜索关键词找到蓝牙连接处理函数
可以发现,使用了3des进行加密
经过几次对比发现数据和密钥都是动态生成的
密钥生成 设备MAC地址 + 年份 + 月份 + 日期 + 小时 + 分钟 + 55AA5A5AA5
数据生成 年份 + 月份 + 日期 + 小时 + 分钟 + 设备名后6位
蓝牙发送数据
截取 3DES 加密返回的前16位
拼接发送数据,具体如下 固定前缀(30) + 前16位数据 + 年份 + 月份 + 日期 + 小时 + 分钟 + 固定后缀(FA34DD0001) + 校验码(xor校验)
其他
如何确认是固定数据的
在反编译出来的 Dart 机器码中可以直接搜索到
0x04 结语
至此,所有常用功能均已逆向完毕,需要相关源码的请自行到 Github 搜索
· 今 日 推 荐 ·
本文内容来自网络,如有侵权请联系删除
原文始发于微信公众号(逆向有你):安卓逆向 -- 某开门软件签名算法、蓝牙开门、密码开门分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论