做设计素材网站宁德市房价
之前分析过某物h5的以及小程序的搜索接口,就是一个aes,秘钥不固定,表单里把秘钥以及密文一起发过去,服务器解密后再把数据加密返回,客户端解密展示到页面上.
这期是关于app的登录,密码登录
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
我创建了一个js 安卓 ios逆向的技术交流群,刚兴趣的可以加一下,在最后面.
抓包
可以看到表单里有newSign,password,userName加密了,都是32位的.并且响应也是加密的.
再来看看头部,框中的那几个都是可能需要分析的,其中有一个shumeiid,这个是数美sdk里的.这个难度估计有点大,这个我们下期分析,SK和edk和ltk自行研究,后面看了下so的结构,就算是so的难度应该也不会太大.这几个貌似都没变,但是ltk是一直变化的.这个也留着后面分析.
反编译
先查下壳,没加固,总算碰到一个没加固的,前几期都是加固的,看来是对自己的产品比较有信心.
尝试搜索下字符串username
有点多,这样一个个点过去太麻烦了,尝试hook hashmap
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b) {if(a!=null && a.equals("userName")){console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))console.log("hashMap.put: ", a, b);}return this.put(a, b);
}
frida反调试
frida启动后以attach注入
直接断开,同时app会重启,这里用的已经是去过特征版的frida了,葫芦娃编译的,还是被检测到了.
以spawn方式启动试试
直接断开并没有重启,来看看它加载了哪些so.肯定是so的检测
var dlopen = Module.findExportByName(null, "dlopen");
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");Interceptor.attach(dlopen, {onEnter: function (args) {var path_ptr = args[0];var path = ptr(path_ptr).readCString();console.log("[dlopen:]", path);},onLeave: function (retval) {}
});
Interceptor.attach(android_dlopen_ext, {onEnter: function (args) {var path_ptr = args[0];var path = ptr(path_ptr).readCString();console.log("[dlopen_ext:]", path);},onLeave: function (retval) {}
});
libmsaoaidsec.so mobile security alliance open aid security,字面意思,看着就不是什么好东西,网上有分析libmsaoaidsec.so的文章,自行检索,我这里是直接把它删掉,有些app可以这样过,有些删了会闪退,闪退的话只能把它过掉.
删掉后frida可以正常使用,直接attach即可.
看到mobileLogin字眼,jadx中点过去看看
f.c方法点过去看看
看到了AES ECB字眼,应该是java层的,这样的话其他的几个也有可能是java层的,但是在上面图中的hashmap中没有看到newsign字眼,这个app用的okhttp3框架,极有可能是用的okhttp3.Interceptor添加的.因为有可能是java层的,有3个参数,当然优先考虑java通杀方案.
Java.perform(function () {function showStacks() {console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));}var ByteString = Java.use("com.android.okhttp.okio.ByteString");function toBase64(tag, data) {//logOutPut(tag + " Base64: " + ByteString.of(data).base64());console.log(tag + " Base64: " + ByteString.of(data).base64());}function toHex(tag, data) {//logOutPut(tag + " Hex: " + ByteString.of(data).hex());console.log(tag + " Hex: " + ByteString.of(data).hex());}function toUtf8(tag, data) {//logOutPut(tag + " Utf8: " + ByteString.of(data).utf8());console.log(tag + " Utf8: " + ByteString.of(data).utf8());}var messageDigest = Java.use("java.security.MessageDigest");messageDigest.update.overload('byte').implementation = function (data) {console.log("MessageDigest.update('byte') is called!");showStacks();return this.update(data);}messageDigest.update.overload('java.nio.ByteBuffer').implementation = function (data) {console.log("MessageDigest.update('java.nio.ByteBuffer') is called!");showStacks();return this.update(data);}messageDigest.update.overload('[B').implementation = function (data) {console.log("MessageDigest.update('[B') is called!");showStacks();var algorithm = this.getAlgorithm();var tag = algorithm + " update data";toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);console.log("=======================================================");return this.update(data);}messageDigest.update.overload('[B', 'int', 'int').implementation = function (data, start, length) {console.log("MessageDigest.update('[B', 'int', 'int') is called!");showStacks();var algorithm = this.getAlgorithm();var tag = algorithm + " update data";toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);console.log("=======================================================", start, length);return this.update(data, start, length);}messageDigest.digest.overload().implementation = function () {console.log("MessageDigest.digest() is called!");showStacks();var result = this.digest();var algorithm = this.getAlgorithm();var tag = algorithm + " digest result";toHex(tag, result);toBase64(tag, result);console.log("=======================================================");return result;}messageDigest.digest.overload('[B').implementation = function (data) {console.log("MessageDigest.digest('[B') is called!");showStacks();var algorithm = this.getAlgorithm();var tag = algorithm + " digest data";toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);var result = this.digest(data);var tags = algorithm + " digest result";toHex(tags, result);toBase64(tags, result);console.log("=======================================================");return result;}messageDigest.digest.overload('[B', 'int', 'int').implementation = function (data, start, length) {console.log("MessageDigest.digest('[B', 'int', 'int') is called!");showStacks();var algorithm = this.getAlgorithm();var tag = algorithm + " digest data";toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);var result = this.digest(data, start, length);var tags = algorithm + " digest result";toHex(tags, result);toBase64(tags, result);console.log("=======================================================", start, length);return result;}var mac = Java.use("javax.crypto.Mac");mac.init.overload('java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function (key, AlgorithmParameterSpec) {console.log("Mac.init('java.security.Key', 'java.security.spec.AlgorithmParameterSpec') is called!");return this.init(key, AlgorithmParameterSpec);}mac.init.overload('java.security.Key').implementation = function (key) {console.log("Mac.init('java.security.Key') is called!");var algorithm = this.getAlgorithm();var tag = algorithm + " init Key";var keyBytes = key.getEncoded();toUtf8(tag, keyBytes);toHex(tag, keyBytes);toBase64(tag, keyBytes);console.log("=======================================================");return this.init(key);}mac.update.overload('byte').implementation = function (data) {console.log("Mac.update('byte') is called!");return this.update(data);}mac.update.overload('java.nio.ByteBuffer').implementation = function (data) {console.log("Mac.update('java.nio.ByteBuffer') is called!");return this.update(data);}mac.update.overload('[B').implementation = function (data) {console.log("Mac.update('[B') is called!");var algorithm = this.getAlgorithm();var tag = algorithm + " update data";toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);console.log("=======================================================");return this.update(data);}mac.update.overload('[B', 'int', 'int').implementation = function (data, start, length) {console.log("Mac.update('[B', 'int', 'int') is called!");var algorithm = this.getAlgorithm();var tag = algorithm + " update data";toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);console.log("=======================================================", start, length);return this.update(data, start, length);}mac.doFinal.overload().implementation = function () {console.log("Mac.doFinal() is called!");var result = this.doFinal();var algorithm = this.getAlgorithm();var tag = algorithm + " doFinal result";toHex(tag, result);toBase64(tag, result);console.log("=======================================================");return result;}var cipher = Java.use("javax.crypto.Cipher");cipher.init.overload('int', 'java.security.cert.Certificate').implementation = function () {console.log("Cipher.init('int', 'java.security.cert.Certificate') is called!");return this.init.apply(this, arguments);}cipher.init.overload('int', 'java.security.Key', 'java.security.SecureRandom').implementation = function () {console.log("Cipher.init('int', 'java.security.Key', 'java.security.SecureRandom') is called!");return this.init.apply(this, arguments);}cipher.init.overload('int', 'java.security.cert.Certificate', 'java.security.SecureRandom').implementation = function () {console.log("Cipher.init('int', 'java.security.cert.Certificate', 'java.security.SecureRandom') is called!");return this.init.apply(this, arguments);}cipher.init.overload('int', 'java.security.Key', 'java.security.AlgorithmParameters', 'java.security.SecureRandom').implementation = function () {console.log("Cipher.init('int', 'java.security.Key', 'java.security.AlgorithmParameters', 'java.security.SecureRandom') is called!");return this.init.apply(this, arguments);}cipher.init.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec', 'java.security.SecureRandom').implementation = function () {console.log("Cipher.init('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec', 'java.security.SecureRandom') is called!");return this.init.apply(this, arguments);}cipher.init.overload('int', 'java.security.Key', 'java.security.AlgorithmParameters').implementation = function () {console.log("Cipher.init('int', 'java.security.Key', 'java.security.AlgorithmParameters') is called!");return this.init.apply(this, arguments);}cipher.init.overload('int', 'java.security.Key').implementation = function () {console.log("Cipher.init('int', 'java.security.Key') is called!");var algorithm = this.getAlgorithm();var tag = algorithm + " init Key";var className = JSON.stringify(arguments[1]);if(className.indexOf("OpenSSLRSAPrivateKey") === -1){var keyBytes = arguments[1].getEncoded();toUtf8(tag, keyBytes);toHex(tag, keyBytes);toBase64(tag, keyBytes);}console.log("=======================================================");return this.init.apply(this, arguments);}cipher.init.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function () {console.log("Cipher.init('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec') is called!");var algorithm = this.getAlgorithm();var tag = algorithm + " init Key";var keyBytes = arguments[1].getEncoded();toUtf8(tag, keyBytes);toHex(tag, keyBytes);toBase64(tag, keyBytes);var tags = algorithm + " init iv";var iv = Java.cast(arguments[2], Java.use("javax.crypto.spec.IvParameterSpec"));var ivBytes = iv.getIV();toUtf8(tags, ivBytes);toHex(tags, ivBytes);toBase64(tags, ivBytes);console.log("=======================================================");return this.init.apply(this, arguments);}cipher.doFinal.overload('java.nio.ByteBuffer', 'java.nio.ByteBuffer').implementation = function () {console.log("Cipher.doFinal('java.nio.ByteBuffer', 'java.nio.ByteBuffer') is called!");showStacks();return this.doFinal.apply(this, arguments);}cipher.doFinal.overload('[B', 'int').implementation = function () {console.log("Cipher.doFinal('[B', 'int') is called!");showStacks();return this.doFinal.apply(this, arguments);}cipher.doFinal.overload('[B', 'int', 'int', '[B').implementation = function () {console.log("Cipher.doFinal('[B', 'int', 'int', '[B') is called!");showStacks();return this.doFinal.apply(this, arguments);}cipher.doFinal.overload('[B', 'int', 'int', '[B', 'int').implementation = function () {console.log("Cipher.doFinal('[B', 'int', 'int', '[B', 'int') is called!");showStacks();return this.doFinal.apply(this, arguments);}cipher.doFinal.overload().implementation = function () {console.log("Cipher.doFinal() is called!");showStacks();return this.doFinal.apply(this, arguments);}cipher.doFinal.overload('[B').implementation = function () {console.log("Cipher.doFinal('[B') is called!");showStacks();var algorithm = this.getAlgorithm();var tag = algorithm + " doFinal data";var data = arguments[0];toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);var result = this.doFinal.apply(this, arguments);var tags = algorithm + " doFinal result";toHex(tags, result);toBase64(tags, result);console.log("=======================================================");return result;}cipher.doFinal.overload('[B', 'int', 'int').implementation = function () {console.log("Cipher.doFinal('[B', 'int', 'int') is called!");showStacks();var algorithm = this.getAlgorithm();var tag = algorithm + " doFinal data";var data = arguments[0];toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);var result = this.doFinal.apply(this, arguments);var tags = algorithm + " doFinal result";toHex(tags, result);toBase64(tags, result);console.log("=======================================================", arguments[1], arguments[2]);return result;}var signature = Java.use("java.security.Signature");signature.update.overload('byte').implementation = function (data) {console.log("Signature.update('byte') is called!");return this.update(data);}signature.update.overload('java.nio.ByteBuffer').implementation = function (data) {console.log("Signature.update('java.nio.ByteBuffer') is called!");return this.update(data);}signature.update.overload('[B', 'int', 'int').implementation = function (data, start, length) {console.log("Signature.update('[B', 'int', 'int') is called!");var algorithm = this.getAlgorithm();var tag = algorithm + " update data";toUtf8(tag, data);toHex(tag, data);toBase64(tag, data);console.log("=======================================================", start, length);return this.update(data, start, length);}signature.sign.overload('[B', 'int', 'int').implementation = function () {console.log("Signature.sign('[B', 'int', 'int') is called!");return this.sign.apply(this, arguments);}signature.sign.overload().implementation = function () {console.log("Signature.sign() is called!");var result = this.sign();var algorithm = this.getAlgorithm();var tag = algorithm + " sign result";toHex(tag, result);toBase64(tag, result);console.log("=======================================================");return result;}
});
不过我比较喜欢用算法助手,原理是一样的.先在lsp里把目标app勾上,算法助手页面把这个app打开
全部勾上后重启app,登录一下,抓包同时开着,这个包里的
{"cipherParam": "userName","countryCode": 86,"loginToken": "","newSign": "614a0f4d2ef049ff1d0145b0a96d2ef0","password": "ca8f119a27ec17f98b463807cd0b6b62","platform": "android","timestamp": "1711714586437","type": "pwd","userName": "83c7358c94e3a24e11f0fde1cbf5d559_1","v": "5.38.5"
}
userName
日志里搜一下83c7358c94e3a24e11f0fde1cbf5d559,后面的_1自己添加上去的.
有结果
aes ecb nopadding,无填充是因为已经填充过了,看明文后面那一串,直接用pkcs7就可以了.userName解决
password
直接搜ca8f119a27ec17f98b463807cd0b6b62
密码加一个盐值,没什么好说的.
newSign
搜一下614a0f4d2ef049ff1d0145b0a96d2ef0
md5签的一串base64值.搜索这个值有没有结果,搜一部分dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t
没有搜到,这个值是base64过的,尝试hook一下base64的encodeToString方法
var base64 = Java.use("android.util.Base64");
base64.encodeToString.overload('[B', 'int').implementation = function (a, b) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
console.log("base64.encodeToString: ", JSON.stringify(a));
var result = this.encodeToString(a, b);
console.log("base64.encodeToString result: ", result)
return result;}
没有hook到,估计是so base64后返回的,所以没有hook到.
先看下先前的堆栈
let i0 = Java.use("gf.i0");
i0["c"].implementation = function (map, j, str) {console.log(`i0.c is called: map=${map}, j=${j}, str=${str}`);let result = this["c"](map, j, str);console.log(`i0.c result=${result}`);return result;
};
hook一下这个c方法,入参的str是空值
中间添加了些东西,这里直接跟这个aesencrypt.encode方法,一路跟到
encodebyte方法,写这个方法的主动调用
function call2(){Java.perform(function (){let AESEncrypt = Java.use("com.duapp.aesjni.AESEncrypt");var str='010110100010001010010010000011000111001011101010101000101110111010011010101101101010001000101100010110100010001010011010110011001111001011100010101000100100110010110010100010101011110010111100'var bArr = [99,105,112,104,101,114,80,97,114,97,109,117,115,101,114,78,97,109,101,99,111,117,110,116,114,121,67,111,100,101,56,54,108,111,103,105,110,84,111,107,101,110,112,97,115,115,119,111,114,100,99,97,56,102,49,49,57,97,50,55,101,99,49,55,102,57,56,98,52,54,51,56,48,55,99,100,48,98,54,98,54,50,112,108,97,116,102,111,114,109,97,110,100,114,111,105,100,116,105,109,101,115,116,97,109,112,49,55,49,49,55,48,52,48,56,49,52,55,54,116,121,112,101,112,119,100,117,115,101,114,78,97,109,101,56,51,99,55,51,53,56,99,57,52,101,51,97,50,52,101,49,49,102,48,102,100,101,49,99,98,102,53,100,53,53,57,95,49,117,117,105,100,49,48,49,51,48,48,102,98,51,57,48,51,48,56,52,101,118,53,46,51,56,46,53]var res = AESEncrypt["encodeByte"](bArr, str)console.log(res)})
}
des 3des aes关系
第一个字符串转16进制后是24字节,写文章之前不记得前面有个aes了,以为这个是3des,3des秘钥就是24字节,3des由des进化过来的,因为des不安全,那个时候aes还没出来,aes中的a是advanced,高级的,由des进化而来,aes的原始名称是Rijndael算法,是由创造这个算法的两位作者名字组成的.因为是在des基础上改进的,所以大家都称它aes.
des秘钥8字节太短了,aes有16字节,24字节,32字节的,密钥的长度在一定程度上决定着算法的安全性,而DES密钥长度过短,也就导致了它的安全性较低;而AES则更加安全,这也是AES能够取代DES的重要原因之一.
在aes还没出之前为了解决des安全性的问题,有专家推出了3des,cyberchef中名称是TripleDES,Triple有3的意思,秘钥24字节,由3把秘钥组成,每8个字节算一把秘钥,加密流程如图所示,如果前两把秘钥相同,就相当于只有一次des加密,3des目前已被证明安全性不足,目前用的都是aes,如果没记错的话某书的x-s就是用的3des,好吧扯得有点远了,回到正文.
用3des加密一遍结果不对,只能看so了.
第一个数组转utf-8
import base64
byte_list = [99,105,112,104,101,114,80,97,114,97,109,117,115,101,114,78,97,109,101,99,111,117,110,116,114,121,67,111,100,101,56,54,108,111,103,105,110,84,111,107,101,110,112,97,115,115,119,111,114,100,99,97,56,102,49,49,57,97,50,55,101,99,49,55,102,57,56,98,52,54,51,56,48,55,99,100,48,98,54,98,54,50,112,108,97,116,102,111,114,109,97,110,100,114,111,105,100,116,105,109,101,115,116,97,109,112,49,55,49,49,55,48,52,48,56,49,52,55,54,116,121,112,101,112,119,100,117,115,101,114,78,97,109,101,56,51,99,55,51,53,56,99,57,52,101,51,97,50,52,101,49,49,102,48,102,100,101,49,99,98,102,53,100,53,53,57,95,49,117,117,105,100,49,48,49,51,48,48,102,98,51,57,48,51,48,56,52,101,118,53,46,51,56,46,53]
num_list = bytearray()
for item in byte_list:if item < 0:item = item + 256num_list.append(item)
utf8_string = num_list.decode()
print('utf8_string:',utf8_string)
结果就是表单里除了newsign的东西cipherParamuserNamecountryCode86loginTokenpasswordca8f119a27ec17f98b463807cd0b6b62platformandroidtimestamp1711704081476typepwduserName83c7358c94e3a24e11f0fde1cbf5d559_1uuid101300fb3903084ev5.38.5,再拼了一个uuid和版本号.
so加固
上图中so的名字是libJNIEncrypt.so,只有64位的,用ida64打开
提示文件结果被破坏,点yes
警告这个so有无意义或者无效的节.点ok.
刚进来两处爆红,上面一条都是灰色的,没有代码段,不用想,加固了.
so dump
dump内存中的so,yang神的脚本
function dump_so(so_name) {Java.perform(function () {var currentApplication = Java.use("android.app.ActivityThread").currentApplication();var dir = currentApplication.getApplicationContext().getFilesDir().getPath();var libso = Process.getModuleByName(so_name);console.log("[name]:", libso.name);console.log("[base]:", libso.base);console.log("[size]:", ptr(libso.size));console.log("[path]:", libso.path);var file_path = dir + "/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";var file_handle = new File(file_path, "wb");if (file_handle && file_handle != null) {Memory.protect(ptr(libso.base), libso.size, 'rwx');var libso_buffer = ptr(libso.base).readByteArray(libso.size);file_handle.write(libso_buffer);file_handle.flush();file_handle.close();console.log("[dump]:", file_path);}});
}
dump_so("libJNIEncrypt.so")
dump下来拉到电脑上用sofixer修复一下.
dump下来的没有前面的警告了,上面的"进度条"也变成了蓝色,同时看到了Jnionload,动态注册,以及base64和aes128,难怪之前hook不到base64,原来是so base64的
接下来找动态注册函数.hook libart
var addrRegisterNatives = null;
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {var symbol = symbols[i];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.duapp.aesjni.AESEncrypt"; //111 某个类中动态注册的soif (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, "sig:", sig,'module_name:',find_module.name ,"offset:", offset);}}}});
}
以spawn启动
0x174c,ida中跳过去看看,然后转换一下jnienv对象
看返回值,来自v18,v18来自j_AES_128_ECB_PKCS5Padding_Encrypt,很明显了,ecb无iv 16字节秘钥,pkcs5填充,直接frida hook这个函数打印两个入参.
var soAddr = Module.findBaseAddress("libJNIEncrypt.so");
var funcAddr = soAddr.add(0x182C) //32位的话记得+1Interceptor.attach(funcAddr,{onEnter: function(args){console.log('onEnter arg[0]: ',hexdump(args[0]))console.log('onEnter arg[1]: ',hexdump(args[1]))this.arg0 = args[0]},onLeave: function(retval){// console.log('onLeave arg[]: ')console.log('onLeave result: ',retval)}});
入参1是java层传来的明文,参数2是key d245a0ba8d678a61,验证一下,和主动调用结果一致,最后md5就是newsign了,至此,newsign分析完毕,没什么难度的.
技术交流+lyaoyao__i(两个_),群聊7天有效
微信公众号
知识星球