x-zse-96,android端,伪dex加固,so加固,白盒AES,字符串加密

2024-03-21 12:04

本文主要是介绍x-zse-96,android端,伪dex加固,so加固,白盒AES,字符串加密,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

x-zse-96,android端,伪dex加固,so加固,白盒AES,字符串加密

上一篇某招聘软件的sig及sp参数被和谐掉了,所以懂得都懂啊!
在这里插入图片描述
因为web的api没有那么全,所以来看了下app的,ios的防护几乎没有,纸糊的一样,android端的有点复杂了,到最后我也没能完整的实现整个加密过程,我也只复现到DFA还原出了秘钥,iv也找到了,就是结果不对,也许是魔改AES的程度比较高,后续搞出来了的话再发下文吧.
网上找了下,都是分析web的,几乎没有分析app的,所以这篇应该算是首篇比较详细的文章了.

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

流程

抓包

抓包的话可以发现头部有个x-zse-96参数,和web是一样的名字.web的之前我也搞过,没什么难度说实话.
在这里插入图片描述

关键代码定位

正常来说是先查壳看看有没有加固,加固的脱壳,没加固的直接拖到jadx里反编译,因为我觉得这应该算是个大厂吧,凭着经验大厂很少加壳,所以我也没有查壳,直接扔到jadx里反编译了.
尝试搜索一下字符串x-zse-96
在这里插入图片描述
什么也没有找到,其实是字符串被加密了
这个时候就有很多方法可以选择了,比如hook java层的系统hashmap,x-zse-96像是base64过的,也可以hook base64,又像是1.0_拼接后面一串得来的,也可以hook StringBuilder的tostring方法,甚至如果你觉得它是so层的加密也可以直接hook NewStringUTF函数.
这里我根据习惯先hook了hashmap

Java.perform(function (){var hashMap = Java.use("java.util.LinkedHashMap");  //LinkedHashMap HashMap
hashMap.put.implementation = function (a, b) {if(a!=null && a.equals("X-Zse-96")){ //X-Zse-96 x-zse-96console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))console.log("hashMap.put: ", a, b);}return this.put(a, b);
}}

什么也没有hook到,看来不是通过hashmap来添加的
接着我hook了StringBuilder的tostring方法,因为做过web的就知道,这个参数就是前面的1.0_拼接后面的得到的.事实上hook base64也是可以hook到的

var sb = Java.use("java.lang.StringBuilder");
sb.toString.implementation = function () {
var retval = this.toString();
if (retval.indexOf("1.0_") != -1) {console.log("StringBuilder.toString: ", retval);console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
}
return retval;
}

在这里插入图片描述
从堆栈来看这个参数的生成是最后通过拦截器添加上去的,tostring的下面一行点进去看看
在这里插入图片描述
按堆栈的意思就是说这个sb.tostring就是结果了,hook了下b函数看看传进去的是不是x-zse-96,结果并不是,是一个32位的md5的结果,堆栈后面有一句native method,这里我也是比较困惑为什么按照堆栈里找的和反编译出来的结果不一样.
这里我以为是反编译出了什么问题,扔到pkid里查个壳看看
在这里插入图片描述
好家伙,不按常理出牌,正常来说大厂都不加壳的,看了一眼so的名字,dexhelper确实是梆梆企业版的
在这里插入图片描述
这个时候我拿出了我的px4定制的fart脱壳机,结果直接运行不起来,由于fart脱壳机太热门了,很多厂商对这个都有检测,我这个还是去过特征版的!
但是我记得这个梆梆加固主要是指令抽取,正常来说反编译的代码都是空的函数,为什么和jadx中的不一样,并且里面的dex都是有数据的.
在这里插入图片描述
用mt管理器看一下
在这里插入图片描述
发现是个伪加固,app虚晃一枪,要是正常人估计还在想办法怎么脱壳,这样的话也不用脱壳了,正好挺省事的.
在这里插入图片描述

接着上面的逻辑,b2不为空,所以走了下面的逻辑,并且有一个addHeader的方法,这个像是往请求头里添加键值对.并且可以看到是添加了两组,hook一下

Java.perform(function(){
let Builder = Java.use("okhttp3.Request$Builder");
Builder["addHeader"].implementation = function (str, str2) {let result = this["addHeader"](str, str2);if(str=='X-Zse-96' || str=='X-Zse-93'){console.log(`Builder.addHeader is called: str=${str}, str2=${str2}`);console.log(`Builder.addHeader result=${result}`);}return result;
};
})

有结果,并且入参是x-zse-96的值
在这里插入图片描述
这里的H.d(“G51CEEF09BA7DF27F”)其实就是字符串加密了,执行后结果就是x-zse-96,点进去是一个native方法.在so层加密了,现在很多app都弄成这样了,通过搜索几乎定位不到关键代码.
接着看
H.d(“G38CD8525”)就是1.0_了,hook H.d方法同样可以得到结果.真正的加密结果来自new String(this.c.a(a2)),a2是查询参数md5后的结果转为了字节数组.
接着一路跟下来到了native方法
在这里插入图片描述
看名字应该是一个aes.中间的str是一个很长的字符串,这个字符串每次都是一样的,但加密结果不一样,所以不用管这个,主要是1和3参数,分别是两个字节数组,1是params md5后的字节数组,第3个数组初步可以当成是aes的秘钥,如果是ecb模式的话(初步认为,其实是iv,一般人都会认为是秘钥的,因为标题说了白盒AES,秘钥内嵌在程序里)
在这里插入图片描述

so模块没有出现在代码里,前面我hook了jni方法NewStringUTF发现是libbangcle_crypto_tool.so,还可以hook libart.so来找动态注册以及libdl.so找静态注册.
接下来看libbangcle_crypto_tool.so,拖到ida里反汇编
在这里插入图片描述
可以看到都是seg段,没有text段
在这里插入图片描述
并且导出函数中也是有java中的静态注册函数
在这里插入图片描述
点过去也是这样,这里你应该感觉到不对劲了,似乎ida无法识别这个so,其实是so被加固了.

dump so

so加固的碰到的比较少,这里我能想到的办法就是dump内存中的so,再修复一下.

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("libbangcle_crypto_tool.so")

dump下来后拖到电脑上修复一下就好了,工具https://github.com/F8LEFT/SoFixer
不过这样dump下来的效果不是很好,unidbg压根跑不起来这个so,里面还是有些数据无法访问,偏偏这些就是白盒aes中的关键数据s盒.折腾了大半天,一路打patch,最后还是放弃了unidbg这条路.
还是老老实实看so里的内容把,修复后的so至少可以看到代码段了.
在这里插入图片描述
进来后先转jnienv对象,这样代码会好分析些.
这个函数稍微有点控制流混淆以及虚假指令
在这里插入图片描述
不过从后往前推就可以看出来走的那个函数了,v12是返回值,v12来自v10创建的字节数组,v10来着上面的8E74函数,这个函数就是核心的加密流程了,点进去瞧瞧
v9是a1赋值过来的,所以也是env对象在这里插入图片描述
进来后看左下角的流程图,用了好几个switch case来判断走哪个流程,还好我们知道我们的函数是laes,并且从提示来看知道是cbc模式,也就是说需要一个初始化iv,这个laes的意思应该是local aes,字面意思就是本地的aes.从这个函数进去看看.进去后再进入一个laes就到了下面这个图
在这里插入图片描述
前面那些内容函数最好hook看一下入参,其实我都hook过了,只是没写,写了篇幅太长了,后面还有白盒AES,这里有个a3>15,a3是明文的长度,aes分组长度都是16个字节,所以这里应该是判断明文的长度来决定加密几轮.这个a6是个函数
在这里插入图片描述
直接点进去是这样,不知道是不是加固的原因,这里只好看汇编了,鼠标放到a6那,按tab转汇编视图
在这里插入图片描述

blx R12,意思是跳到R12寄存器中的地址执行,这里直接看看不出跳到哪,可以用frida 的inlinehook看看

function inlineHook() {var nativePointer = Module.findBaseAddress("libbangcle_crypto_tool.so");var hookAddr = nativePointer.add(0x6140); // 6140	Interceptor.attach(hookAddr, {onEnter: function (args) {console.log(nativePointer)console.log("onEnter: ", this.context.r12);}, onLeave: function (retval) {}});
}
inlineHook()

这个so是32位的,正常情况下地址是要加1的,但是因为这个so是从内存中dump下来的,所以不需要加1了,这个需要注意下.结果是0x6420.这个app的好几年都是用的这个so,一点都没变,偏移也没变,属于是so加固了以为高枕无忧了.
在这里插入图片描述
进来后看到一个名字WB white box,白盒的意思,现在的安卓逆向java层的加密已经几乎没有了,so的逆向上来就是各种白盒,各种魔改算法以及自写算法,不如直接去搞ios逆向,现在的ios逆向很多都是调用的系统函数,可以直接hook整个系统函数,类似安卓的那个算法助手,而且ios逆向的人少,对抗少,系统也闭源,风控也比安卓小得多,现在从0开始搞安卓逆向难度很大,就算你有天赋至少也需要半年的时间(ios的可以看看沐阳老哥的课程,当然只是题外话,后面也会出几篇文章从0开始搞ios逆向.)
之前我发过一篇白盒aes的文章,很多js逆向的反馈压根没听过这个词,感觉很高级的样子,正常,js逆向你可以直接扣js啊,ida中的这个伪c代码扣下来很难跑通,对c的要求有点高.我是扣了一下,跑不起来,放弃了.
看下面的内容前你最好了解下什么是白盒AES,这是我从网上找的一篇文章https://blog.csdn.net/qq_37638441/article/details/128968233
接着正题,前面java层传入了一个参数3,16个字节,如果是白盒aes的话,那这个大概率就是iv了.

function callAES(){var base_addr = Module.findBaseAddress("libbangcle_crypto_tool.so");var real_addr = base_addr.add(0x6420);var wbaes_encrypt_ecb_func = new NativeFunction(real_addr, "void", ["pointer", "pointer", "pointer", "pointer","pointer"]);inputPtr = Memory.alloc(0x10);var inputArray = hexToBytes("b11812121a8886852e2e868786252e2e");Memory.writeByteArray(inputPtr, inputArray)var inputPtr3 = Memory.alloc(0x10);// var inputArray = hexToBytes("803d17b5b00000000400000080000000");var inputArray = hexToBytes("401948d4b00000000400000080000000");Memory.writeByteArray(inputPtr3, inputArray)var inputPtr4 = Memory.alloc(0x10);var inputArray = hexToBytes("00000000000000000000000000000000");Memory.writeByteArray(inputPtr4, inputArray)var inputPtr5 = Memory.alloc(0x10);var inputArray = hexToBytes("00000000000000000000000000000000");Memory.writeByteArray(inputPtr5, inputArray)wbaes_encrypt_ecb_func(inputPtr, inputPtr, inputPtr3, inputPtr4,inputPtr5);console.log(hexdump(inputPtr,{length: 0x10}));
}

so主动调用这个函数,其实它有5个参数,ida识别成了3个.前两个是同一个参数,iv和明文异或的结果,cbc模式下比ecb模式多了个iv,后两个参数作用不大,不用管,关键是第3个参数,有点奇怪,现在我也没搞清楚它的作用是什么,后面再说吧.

DFA攻击

调用后有结果,结果也是正确的.接下来寻找dfa攻击点,第8次列混淆到第9次列混淆之间,并且需要寻找到state块
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
按aes的算法应该是这样,最后面的第十轮少了列混淆,所以不在这个循环里面.
加下来寻找哪个是state块,初看选了result,每次核心运算都有他,一hook发现经过9轮他的值都不变,直到最后赋值结果的时候才变了.
后来仔细看了下v20也有可能,但是这个v20不太好hook他的变化,因为这里没有什么函数调用,所有没有入参什么的,要hook到这个v20的地址有些困难
在这里插入图片描述

这里我寻找九轮循环最开始的地方hook上,这个赋值写的也有点复杂.鼠标放到v20的地方,转汇编更方便看.
在这里插入图片描述
在这里插入图片描述
这里鼠标放到var_34按h可以转成立即数.STRB R3, [R11,#-52]
这条指令的意思就是将寄存器 R3 中的值存储到距离 R11 寄存器所指向的内存位置偏移为 -52 的地方。所以v20的地址就是R11的地址减去52的偏移的位置处.

function hookwb(){var count = 0;var base_addr = Module.findBaseAddress("libbangcle_crypto_tool.so");var real_addr = base_addr.add(0x6580)Interceptor.attach(real_addr, {onEnter: function (args) {count += 1;console.log("onEnter: ",count, hexdump(this.context.r11.sub(0x24),{length:0x10}));console.log("start:"+count);}});
}

结果也确实是9轮,接下来进行dfa攻击.

function hexToBytes(hex) {for (var bytes = [], c = 0; c < hex.length; c += 2)bytes.push(parseInt(hex.substr(c, 2), 16));return bytes;
}
var inputPtr;
function callAES(){var base_addr = Module.findBaseAddress("libbangcle_crypto_tool.so");var real_addr = base_addr.add(0x6420);var wbaes_encrypt_ecb_func = new NativeFunction(real_addr, "void", ["pointer", "pointer", "pointer", "pointer","pointer"]);inputPtr = Memory.alloc(0x10);var inputArray = hexToBytes("b11812121a8886852e2e868786252e2e");Memory.writeByteArray(inputPtr, inputArray)var inputPtr3 = Memory.alloc(0x10);// var inputArray = hexToBytes("803d17b5b00000000400000080000000");var inputArray = hexToBytes("401948d4b00000000400000080000000");Memory.writeByteArray(inputPtr3, inputArray)var inputPtr4 = Memory.alloc(0x10);var inputArray = hexToBytes("00000000000000000000000000000000");Memory.writeByteArray(inputPtr4, inputArray)var inputPtr5 = Memory.alloc(0x10);var inputArray = hexToBytes("00000000000000000000000000000000");Memory.writeByteArray(inputPtr5, inputArray)wbaes_encrypt_ecb_func(inputPtr, inputPtr, inputPtr3, inputPtr4,inputPtr5);// var output = Memory.readByteArray(inputPtr, 0x10);// console.log(bufferToHex(output))console.log(hexdump(inputPtr,{length: 0x10}));
}
function bufferToHex (buffer) {return [...new Uint8Array (buffer)].map (b => b.toString (16).padStart (2, "0")).join ("");
}
function hookwb(){var count = 0;var base_addr = Module.findBaseAddress("libbangcle_crypto_tool.so");var real_addr = base_addr.add(0x6580)Interceptor.attach(real_addr, {onEnter: function (args) {count += 1;console.log("onEnter: ",count, hexdump(this.context.r11.sub(0x24),{length:0x10}));if(count===9){this.context.r11.sub(0x24).add(randomNum(0,15)).writeS8(randomNum(0, 0xff));console.log("onEnter: ", hexdump(this.context.r11.sub(0x24),{length:0x10}));}console.log("start:"+count);}});
}
function randomNum(minNum,maxNum){if (arguments.length === 1) {return parseInt(Math.random() * minNum + 1, 10);} else if (arguments.length === 2) {return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);} else {return 0;}
}
function dfa(){for(var i=0;i<300;i++){hookwb()callAES()}
}

连续调用300次后取得故障密文再用phoenixAES拿到第10轮秘钥E48E8AA0E4449B580CF7ECC13CC17CD3,接着用aes_keyschedule还原出主密钥6BA6737912D31F3A1B53066645FABEA3
在这里插入图片描述
秘钥6BA6737912D31F3A1B53066645FABEA3,iv99303a3a32343a3992923a3b3a999292(java层的)都有了.事实上只有1轮的话,也可以直接用ecb模式,因为入参1就是明文和iv异或的结果,拿去加密一下发现结果不对…
前面我就说了这个参数3不太清楚是什么,因为这个入参这个时刻下主动调用是这个结果,重开app主动调用逻辑不变结果取变了.这里有个很大的问号?
这里先分析到这里吧,先埋个坑,后面有空再看看吧.

最后

微信公众号
在这里插入图片描述
知识星球
在这里插入图片描述
如果你觉得这篇文章对你有帮助,不妨请作者喝杯咖啡吧!
在这里插入图片描述

这篇关于x-zse-96,android端,伪dex加固,so加固,白盒AES,字符串加密的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/832592

相关文章

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

python修改字符串值的三种方法

《python修改字符串值的三种方法》本文主要介绍了python修改字符串值的三种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录第一种方法:第二种方法:第三种方法:在python中,字符串对象是不可变类型,所以我们没办法直接

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

Java中的密码加密方式

《Java中的密码加密方式》文章介绍了Java中使用MD5算法对密码进行加密的方法,以及如何通过加盐和多重加密来提高密码的安全性,MD5是一种不可逆的哈希算法,适合用于存储密码,因为其输出的摘要长度固... 目录Java的密码加密方式密码加密一般的应用方式是总结Java的密码加密方式密码加密【这里采用的

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

C#中字符串分割的多种方式

《C#中字符串分割的多种方式》在C#编程语言中,字符串处理是日常开发中不可或缺的一部分,字符串分割是处理文本数据时常用的操作,它允许我们将一个长字符串分解成多个子字符串,本文给大家介绍了C#中字符串分... 目录1. 使用 string.Split2. 使用正则表达式 (Regex.Split)3. 使用

使用Python制作一个PDF批量加密工具

《使用Python制作一个PDF批量加密工具》PDF批量加密‌是一种保护PDF文件安全性的方法,通过为多个PDF文件设置相同的密码,防止未经授权的用户访问这些文件,下面我们来看看如何使用Python制... 目录1.简介2.运行效果3.相关源码1.简介一个python写的PDF批量加密工具。PDF批量加密

Java中JSON字符串反序列化(动态泛型)

《Java中JSON字符串反序列化(动态泛型)》文章讨论了在定时任务中使用反射调用目标对象时处理动态参数的问题,通过将方法参数存储为JSON字符串并进行反序列化,可以实现动态调用,然而,这种方式容易导... 需求:定时任务扫描,反射调用目标对象,但是,方法的传参不是固定的。方案一:将方法参数存成jsON字

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo