【Android安全-某大厂vmp算法全参trace分析】此文章归类为:Android安全。
好久没写新的文章了,因为最近都在弄大厂的样本,所以时间会比较久,而且这种类型也比较敏感,提醒,仅学习交流,请勿非法使用.
写在最前面,侵权系删!!! yruhua0
哪个厂暂时不说,看完你应该知道,关键位置会暴露app的用xx代替.本章将会从抓包到整个算法全流程讲解,篇幅很长,如有错别字还请理解.
ida, 010editer, frida, unidbg, jadx,CyberChef,charles(抓包看你们自己,我喜欢用vpn转发,花瓶+socksdroid)
ida,010,frida,jadx的基本使用,unidbg的进阶使用,还有就是离不开的算法分析(逆向必不可少的),包括
热门算法
hash: md5,sha1,sha256,sha512,以及hamc组合,要求熟悉算法细节,能根据算法特征直接定位明文,还原出正确的密文,以及各种魔改方向
分组加密:主要是aes的熟练程度,秘钥扩展,十轮运算怎么算的,以及查表和白盒相关,然后就是加密模式,ecb,cbc,填充等等.
冷门算法
冷门算法有很多,主要是大厂在用
比如crc32,rc4,sm3的算法细节,不要求很熟悉,但要求能做到拿到算法源码仔细阅读后能达到和热门算法一样的熟练程度
抓包没什么特别的,过掉sslping就可以了,头部有一个jdgs,格式如下
抓包对比后b1是不变的,不同版本的不一样,可以认为是key,b2这个没什么,后面会有介绍,b3:2.1,版本号,b4,疑似一段base64数据,b5和b6,一直变化,长度40,b7,时间戳. 所以要分析的就是b4,b5,b6.抓包的是13.2.x版本的,最新的,算法分析用13.x.0,大概一两个月前发布的,最终算法通用,只不过有一些秘钥要从apk里拿,这里不同版本不一样.
肯定是so的啦,哪还有java层的算法给你逆?
headers或者params这种键值对的类型很喜欢用hashmap添加
1 2 3 4 5 6 7 8 9 10 | Java.perform( function (){ var hashMap = Java.use( "java.util.HashMap" ); hashMap.put.implementation = function (a, b) { if (a!= null && a.equals( "jdgs" )){ 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); } }) |
有结果,但不是最终位置,最终位置在com.xx.security.xxguard.core.Bridge这个类下,我相信你来看大厂的应该不至于堆栈跟踪找不到.下图是最终结果.
右键复制frida片段再hook
入参两个一个数字,一个obj 数组
1 2 | [B@ 99ac1f5 ,coral| - |coral| - | - | - | - | - | - | - | - | - | - | - | - | - | - §§ 0 | 1 §§ - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - §§ - | 53684973568 §§ - | - | 1.0 | - §§ 1 | 1 | - | - | - | - | - | - | - | - | - §§google / coral / coral: 10 / QD1A. 190821.007 / 5831595 :user / release - keys / | - | - | 1724741737147 | - §§ - | - | - | - | - | - | - ,eidA9d988120a5s4CYER7EnMT + + 7gzxHQ7PuFwmBE33cauicjiUlMN8hEQz9Sqr + c2MTtGVN + 7IqnBeQMy3v7E2GYbpB21iuG + SC35rf + q406ZICqmRr, 1.0 , 83 |
这个数组传了5个参数,后面4个都是字符串,第一个参数[B 是字节数组的意思,要打印可见的数组也很简单,取第一个元素,由于是对象数组,所以要先转型成[B,再输出utf8格式,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | Java.perform( function (){ let Bridge = Java.use( "com.xx.security.xxguard.core.Bridge" ); Bridge[ "main" ].implementation = function (i2, objArr) { console.log(`Bridge.main is called: i2=${i2}, objArr=${objArr}`); for ( var i = 0; i < objArr.length; i++) { if (i===0){ if (objArr[0]!= null ){ var JavaByte = Java.use( "[B" ); var buffer = Java.cast(objArr[0],JavaByte); var res = Java.array( 'byte' , buffer); //object 先转[B var ByteString = Java.use( "com.android.okhttp.okio.ByteString" ); console.log( '0=' ,ByteString.of(res).utf8()) continue } } console.log(`${i}=${objArr[i]}`) } let result = this [ "main" ](i2, objArr); console.log(`Bridge.main result=${result.toString()}`); return result; }; }) |
所以第一个参数就是请求类型拼接路径和params和data组成的字符串,接下来写主动调用,作用是有一个按钮,可以随时调用函数,后面hook的时候可以由自己来构造参数,同时也可观察相同入参,结果是否变化.
1 2 3 4 5 6 7 8 9 10 11 | function call(){ Java.perform( function (){ let Bridge = Java.use( "com.xx.security.xxguard.core.Bridge" ); var str0 = 'POST /client.action avifSupport=1&bef=1&build=99208&client=android&clientVersion=13.1.0&ef=1&eid=eidAcc518121b5sdnj7F7Uw4TcaishY7tVQB254%2Bx37JnZ6PeiUW7ppOj%2BnldGjNFrcI%2FmI54gGvpGfOVVYAnIVEgBM8ofUy0hwGx%2B7L5g9B47fttQxV&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1724385686962%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22area%22%3A%22CV83Cv81DJY3DP8m%22%2C%22d_model%22%3A%22UQv4ZWm0WOm%3D%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJK%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22Ctu4DMenDNGm%22%2C%22uuid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22aid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22openudid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&f9c28ecee0666c4febaa160e4e56038d034ec409809b23f32e2f7c22616f4a89=&functionId=uniformRecommend71&harmonyOs=0&lang=zh_CN&networkType=wifi&partner=xiaomi001&recommendSource=9&sdkVersion=29&sign=806911e114d055771f14c8bbc3f89f33&st=1724387382329&sv=101&uemps=2-2-2&x-api-eid-token=jdd01AWSOWPBNUEQE6QYLS626725F4R55GJLSPBYQWRYLDRSLNV27TXG5NK4TEDJR4XQ5TVIO3OBB6KFD3BTLMLNRB2FJLWMZKRU2ZRKWMRA01234567' var StringClass = Java.use( 'java.lang.String' ); var byteArray = StringClass.$ new (str0).getBytes(); var objArr = Java.array( 'Ljava.lang.Object;' ,[byteArray, 'coral|-|coral|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§0|1§§-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§-|53684973568§§-|-|1.0|-§§1|1|-|-|-|-|-|-|-|-|-§§google/coral/coral:10/QD1A.190821.007/5831595:user/release-keys/|-|-|1724385488732|-§§-|-|-|-|-|-|-' , 'eidAcc518121b5sdnj7F7Uw4TcaishY7tVQB254+x37JnZ6PeiUW7ppOj+nldGjNFrcI/mI54gGvpGfOVVYAnIVEgBM8ofUy0hwGx+7L5g9B47fttQxV' , '1.0' , '83' ]) let result = Bridge[ "main" ](101, objArr); console.log( "res:" ,result) }) } |
这些都是基本操作,如果不熟可以google一下
多次调用结果如下
发现b5的结果是不变的,b4和b6一直在变,因为时间戳也在变,也就是说入参固定的情况下b5的值应该要是固定的才对,接下来用unidbg模拟执行,用frida来还原算法工作量会是unidbg的好几个数量级,前面的都是一些基础操作,后面的都是需要注意的一些细节
推荐用最新版的unidbg
否则有可能报错和我的不一样,确保下载下来执行里面的demo能跑结果
疑似unidbg bug,由于下面的patch的方式不同,so中走向不同的异常分支,这里有一个unidbg的异常出现时机不一定,所以我把它写在最前面,可能是unidbg的bug,我已经发给凯神了,在等他的回复,这里只需要把unidbg-android/src/main/java/com/github/unidbg/linux/ARM64SyscallHandler.java 下的mmap函数的<< MMAP2_SHIFT 注释掉,只要这里注释下,下面的就不会有问题.这个apk的初始化异常分支很多(不懂初始化可以往下看). 注意,一定要改
异常的字眼 java.io.IOException: Negative seek offset
改好后再进行下面的,搭骨架
一定要按顺序,先把jniOnload部分执行起来,不然你直接去调用函数到时候报很多错会弄的很乱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | package com.xx; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.memory.Memory; import com.github.unidbg.virtualmodule.android.AndroidModule; import java.io.File; public class jdgs2 extends AbstractJni{ private final AndroidEmulator emulator; private final VM vm; private final Module module; jdgs2(){ // 创建模拟器实例 emulator = AndroidEmulatorBuilder.for64Bit().setProcessName( "包名自己填" ).setRootDir( new File( "target/rootfs" )).build(); // 获取模拟器的内存操作接口 final Memory memory = emulator.getMemory(); // 设置系统类库解析 memory.setLibraryResolver( new AndroidResolver( 23 )); // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作 vm = emulator.createDalvikVM( new File( "unidbg-android/src/test/java/com/xx/files//13.x.0.apk" )); // 设置JNI vm.setJni( this ); // 打印日志 vm.setVerbose( true ); // 加载目标SO DalvikModule dm = vm.loadLibrary( "jdg" , true ); //获取本SO模块的句柄,后续需要用它 module = dm.getModule(); // 调用JNI OnLoad dm.callJNI_OnLoad(emulator); }; public static void main(String[] args) { jdgs2 demo = new jdgs2(); } } |
执行下有个info,缺少libandroid.so,这个so依赖很多so,不好加载,unidbg实现了它的部分函数,可以注册虚拟模块.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | jdgs2(){ // 创建模拟器实例 emulator = AndroidEmulatorBuilder.for64Bit().setProcessName( "com.xxx.app.mall" ).setRootDir( new File( "target/rootfs" )).build(); // 获取模拟器的内存操作接口 final Memory memory = emulator.getMemory(); // 设置系统类库解析 memory.setLibraryResolver( new AndroidResolver( 23 )); // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作 vm = emulator.createDalvikVM( new File( "unidbg-android/src/test/java/com/xx/files/xx/xx13.x.0.apk" )); // 注册虚拟Android模块 new AndroidModule(emulator, vm).register(memory); // 设置JNI vm.setJni( this ); // 打印日志 vm.setVerbose( true ); // 加载目标SO DalvikModule dm = vm.loadLibrary( "jdg" , true ); //获取本SO模块的句柄,后续需要用它 module = dm.getModule(); // 调用JNI OnLoad dm.callJNI_OnLoad(emulator); }; |
执行后成功跑起来了,恭喜你,完成了第一步,同时可以看到函数在so里的偏移是0x29ce4
第二步,开始调用函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public void callByAddress(){ // args list List<Object> list = new ArrayList<>( 4 ); // jnienv list.add(vm.getJNIEnv()); // jclazz list.add( 0 ); list.add( 101 ); byte [] bytes = "POST /client.action avifSupport=1&bef=1&build=99208&client=android&clientVersion=13.1.0&ef=1&eid=eidAcc518121b5sdnj7F7Uw4TcaishY7tVQB254%2Bx37JnZ6PeiUW7ppOj%2BnldGjNFrcI%2FmI54gGvpGfOVVYAnIVEgBM8ofUy0hwGx%2B7L5g9B47fttQxV&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1724385686962%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22area%22%3A%22CV83Cv81DJY3DP8m%22%2C%22d_model%22%3A%22UQv4ZWm0WOm%3D%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJK%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22Ctu4DMenDNGm%22%2C%22uuid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22aid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22openudid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&f9c28ecee0666c4febaa160e4e56038d034ec409809b23f32e2f7c22616f4a89=&functionId=uniformRecommend71&harmonyOs=0&lang=zh_CN&networkType=wifi&partner=xiaomi001&recommendSource=9&sdkVersion=29&sign=806911e114d055771f14c8bbc3f89f33&st=1724387382329&sv=101&uemps=2-2-2&x-api-eid-token=jdd01AWSOWPBNUEQE6QYLS626725F4R55GJLSPBYQWRYLDRSLNV27TXG5NK4TEDJR4XQ5TVIO3OBB6KFD3BTLMLNRB2FJLWMZKRU2ZRKWMRA01234567" .getBytes(); ByteArray arr = new ByteArray(vm, bytes); vm.addLocalObject(arr); StringObject str_1 = new StringObject(vm, "coral|-|coral|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§0|1§§-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§-|53684973568§§-|-|1.0|-§§1|1|-|-|-|-|-|-|-|-|-§§google/coral/coral:10/QD1A.190821.007/5831595:user/release-keys/|-|-|1724385488732|-§§-|-|-|-|-|-|-" ); vm.addLocalObject(str_1); StringObject str_2 = new StringObject(vm, "eidAcc518121b5sdnj7F7Uw4TcaishY7tVQB254+x37JnZ6PeiUW7ppOj+nldGjNFrcI/mI54gGvpGfOVVYAnIVEgBM8ofUy0hwGx+7L5g9B47fttQxV" ); vm.addLocalObject(str_2); StringObject str_3 = new StringObject(vm, "1.0" ); vm.addLocalObject(str_3); StringObject str_4 = new StringObject(vm, "83" ); vm.addLocalObject(str_4); ArrayObject arrayObject = new ArrayObject(arr,str_1,str_2,str_3,str_4); list.add(vm.addLocalObject(arrayObject)); Number number = module.callFunction(emulator, 0x29CE4 , list.toArray()); ArrayObject resultArr = vm.getObject(number.intValue()); System.out.println( "result:" +resultArr); }; |
我喜欢用地址来调,当然也可通过api调,看个人习惯
直接调用没有补环境的部分,而是直接报了一个日志,这个红色的是so输出的,不是unidbg输出的,Error pp not init,意思也很明显,未初始化.
为什么要初始化?其实这个主要是用来anti unidbg的,如果你直接调就出结果了那不是一下就被破解了吗?大厂普遍都有,中小厂也并不少见.
所以该怎么办?
正确的方法就是frida hook 这个方法的初始化过程,apk怎么做的,我们就在unidbg里照做.
只有一个函数,难道初始化和调用共用同一个函数吗?没错,现在大厂基本都是这样,比如dy,阿里,mt这些都是,函数怎么知道自己是要调用出结果还是初始化呢? 很简单,传不同的参数呗,比如这个样本,第一个参数一个数字,第二个参数一个obj数组,完全可以通过传不同数字来决定,事实上,大厂就是这么干的.
接下来用frida来hook 这个初始化过程
1 2 3 4 5 6 7 8 9 | Java.perform( function (){ let Bridge = Java.use( "com.xx.security.xxguard.core.Bridge" ); Bridge[ "main" ].implementation = function (i2, objArr) { console.log(`Bridge.main is called: i2=${i2}, objArr=${objArr}`); let result = this [ "main" ](i2, objArr); console.log(`Bridge.main result=${result.toString()}`); return result; }; }) |
以spawn启动即可,如下图,最先执行两个103,返回两个0后执行101就有结果了,猜测需要先执行103,返回两个0再调用应该能出正确结果
怎么验证?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | Java.perform( function (){ let Bridge = Java.use( "com.xx.security.xxguard.core.Bridge" ); Bridge[ "main" ].implementation = function (i2, objArr) { console.log(`Bridge.main is called: i2=${i2}, objArr=${objArr}`); let result = this [ "main" ](i2, objArr); console.log(`Bridge.main result=${result.toString()}`); if (i2===103){ console.log( '===================' ) call() } return result; }; }) function call(){ Java.perform( function (){ let Bridge = Java.use( "com.xx.security.xxguard.core.Bridge" ); var str0 = 'POST /client.action avifSupport=1&bef=1&build=99208&client=android&clientVersion=13.1.0&ef=1&eid=eidAcc518121b5sdnj7F7Uw4TcaishY7tVQB254%2Bx37JnZ6PeiUW7ppOj%2BnldGjNFrcI%2FmI54gGvpGfOVVYAnIVEgBM8ofUy0hwGx%2B7L5g9B47fttQxV&ep=%7B%22hdid%22%3A%22JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw%3D%22%2C%22ts%22%3A1724385686962%2C%22ridx%22%3A-1%2C%22cipher%22%3A%7B%22area%22%3A%22CV83Cv81DJY3DP8m%22%2C%22d_model%22%3A%22UQv4ZWm0WOm%3D%22%2C%22wifiBssid%22%3A%22dW5hbw93bq%3D%3D%22%2C%22osVersion%22%3A%22CJK%3D%22%2C%22d_brand%22%3A%22H29lZ2nv%22%2C%22screen%22%3A%22Ctu4DMenDNGm%22%2C%22uuid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22aid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%2C%22openudid%22%3A%22YzG0CtOmDNczDzK5CWVtEG%3D%3D%22%7D%2C%22ciphertype%22%3A5%2C%22version%22%3A%221.2.0%22%2C%22appname%22%3A%22com.jingdong.app.mall%22%7D&ext=%7B%22prstate%22%3A%220%22%2C%22pvcStu%22%3A%221%22%2C%22cfgExt%22%3A%22%7B%5C%22privacyOffline%5C%22%3A%5C%220%5C%22%7D%22%7D&f9c28ecee0666c4febaa160e4e56038d034ec409809b23f32e2f7c22616f4a89=&functionId=uniformRecommend71&harmonyOs=0&lang=zh_CN&networkType=wifi&partner=xiaomi001&recommendSource=9&sdkVersion=29&sign=806911e114d055771f14c8bbc3f89f33&st=1724387382329&sv=101&uemps=2-2-2&x-api-eid-token=jdd01AWSOWPBNUEQE6QYLS626725F4R55GJLSPBYQWRYLDRSLNV27TXG5NK4TEDJR4XQ5TVIO3OBB6KFD3BTLMLNRB2FJLWMZKRU2ZRKWMRA01234567' var StringClass = Java.use( 'java.lang.String' ); var byteArray = StringClass.$ new (str0).getBytes(); var objArr = Java.array( 'Ljava.lang.Object;' ,[byteArray, 'coral|-|coral|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§0|1§§-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-§§-|53684973568§§-|-|1.0|-§§1|1|-|-|-|-|-|-|-|-|-§§google/coral/coral:10/QD1A.190821.007/5831595:user/release-keys/|-|-|1724385488732|-§§-|-|-|-|-|-|-' , 'eidAcc518121b5sdnj7F7Uw4TcaishY7tVQB254+x37JnZ6PeiUW7ppOj+nldGjNFrcI/mI54gGvpGfOVVYAnIVEgBM8ofUy0hwGx+7L5g9B47fttQxV' , '1.0' , '83' ]) let result = Bridge[ "main" ](101, objArr); console.log( "res:" ,result) }) } |
很简单
1 2 3 4 | if (i2===103){ console.log( '===================' ) call() } |
每执行一次103后都主动调用一次,看看是否出结果
如上,调一次103后直接call返回一个数字,第二次调103后call就是结果,注意,这两个调103的过程是app自己完成的,我这样写最简洁,又可以判断出到底哪些过程是初始化.
所以需要先call两次103,第一次传0,第二次传1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public void callInit(String arg){ // args list List<Object> list = new ArrayList<>( 4 ); // jnienv list.add(vm.getJNIEnv()); // jclazz list.add( 0 ); list.add( 103 ); StringObject arg0 = new StringObject(vm, arg); vm.addLocalObject(arg0); ArrayObject arrayObject = new ArrayObject(arg0); list.add(vm.addLocalObject(arrayObject)); Number number = module.callFunction(emulator, 0x29CE4 , list.toArray()); ArrayObject arrayObject2 = vm.getObject(number.intValue()); System.out.println(arrayObject2.getValue()[ 0 ].getValue()); } public static void main(String[] args) { jdgs2 demo = new jdgs2(); demo.callInit( "0" ); demo.callInit( "1" ); // demo.callByAddress(); } |
代码如上,调用后结果如下,但是第二次call的时候返回的是一个负数,正常应该是0,和app不同千万不要着急调用函数,要先排除出错误
很多同学到这里不知道怎么办了.别着急,如果我告诉你上面的jni日志中有一个地方有问题,它是专门用来反制unidbg的,仔细从上往下观察3遍,如果你能找出来说明你unidbg基础很扎实,如果你发现了,也可以打在评论区哦,这个我们后面揭晓,我当时也没看出来,因为这个坑有点费眼神,不过没关系,我们一步一步排查.肯定要去so里排查,所以先把so反编译一下
只有64位的,用ida64打开,当然9.0的不分32和64了,我用的是8.3
打开先看jniOnload,有花指令,再正常不过了,大厂的都有花
上面那块有没有很熟悉?
某手的kwsgmain的花
1 2 3 4 5 6 7 8 9 10 | .text: 000000000004ABB4 E0 07 BE A9 STP X0, X1, [SP, #-32]! .text: 000000000004ABB8 E2 7B 01 A9 STP X2, X30, [SP, #16] .text: 000000000004ABBC 01 01 00 10 ADR X1, dword_4ABDC .text: 000000000004ABC0 21 10 00 F1 SUBS X1, X1, #4 .text: 000000000004ABC4 E0 03 01 AA MOV X0, X1 .text: 000000000004ABC8 00 D0 00 B1 ADDS X0, X0, #0x34 ; '4' .text: 000000000004ABCC E0 0F 00 F9 STR X0, [SP, #24] .text: 000000000004ABD0 E2 27 41 A9 LDP X2, X9, [SP, #16] .text: 000000000004ABD4 E0 07 C2 A8 LDP X0, X1, [SP], #0x20 .text: 000000000004ABD8 20 01 1F D6 BR X9 |
不能说一模一样,十有八九了吧.
还是匹配开头部分的 E0 07 BE A9 E2 7B 01 A9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | import keystone from keystone import * import ida_bytes import idaapi import idc import flare_emu # 来自叶谷雨的代码 O(∩_∩)O def binSearch(start, end, pattern): matches = [] addr = start if end = = 0 : end = idc.BADADDR if end ! = idc.BADADDR: end = end + 1 while True : addr = ida_bytes.bin_search(addr, end, bytes.fromhex(pattern), None , idaapi.BIN_SEARCH_FORWARD, idaapi.BIN_SEARCH_NOCASE) if addr = = idc.BADADDR: break else : matches.append(addr) addr = addr + 1 return matches myEH = flare_emu.EmuHelper() def getJumpAddress(addr): myEH.emulateRange(startAddr = addr, endAddr = addr + 36 ) return myEH.getRegVal( "X9" ) def makeInsn(addr): if idc.create_insn(addr) = = 0 : idc.del_items(addr, idc.DELIT_EXPAND) idc.create_insn(addr) idc.auto_wait() def generate(code, addr): ks = Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN) encoding, _ = ks.asm(code, addr) return encoding matches = binSearch( 0 , 0 , "E0 07 BE A9 E2 7B 01 A9" ) for addr in matches: print ( "try:" + hex (addr)) makeInsn(addr) targetAddr = getJumpAddress(addr) code = f "B {hex(targetAddr)}" bCode = generate(code, addr) nopCode = generate( "nop" , 0 ) ida_bytes.patch_bytes(addr, bytes(bCode)) ida_bytes.patch_bytes(addr + 4 , bytes(nopCode) * 9 ) print ( 'finish' ) |
还是之前的代码,由龙哥编写,如果不用ida脚本,直接patch也是可以的,只不过麻烦些,具体的可以看龙哥这篇文章https://www.yuque.com/lilac-2hqvv/zfho3g/issny5?#yGLEd
patch回填后重新打开,全部正常了
结果异常明显是走了异常分支,这个时候从出现异常的地方往上回溯是比较高效的方法,结尾返回了一个-3301,就以这个为突破口,首先找到目标函数地址0x29ce4,进来先转换jniEnv,看到清晰些
流程图长这样,这只是ollvm过了,vmp还在后头
前面是一些变量定义,然后就是一个switch case分发
转汇编视图可以看到上面的case e对应101分支,101我们还没执行,我们要找的是103分支
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | case 'g' : dword_B0154 = 0 ; dword_B0150 = 0 ; v15 = ( * env) - >GetObjectArrayElement(env, a4, 0LL ); v16 = ( * env) - >ExceptionCheck(env); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( v16 = = 1 ) { ( * env) - >ExceptionClear(env); v17 = 0LL ; v6 = 0LL ; dword_B0154 = 0 ; dword_B0150 = 0 ; if ( !v15 ) goto LABEL_220; goto LABEL_68; } dword_B0154 = 0 ; dword_B0150 = 0 ; if ( !v15 ) { v28 = - 3104 ; LABEL_36: v6 = sub_2D43C(env, v28); LABEL_162: dword_B0154 = 0 ; dword_B0150 = 0 ; LABEL_220: dword_B0154 = 0 ; dword_B0150 = 0 ; break ; } sub_32E70(env, v15); if ( (v101 & 1 ) ! = 0 ) v23 = v103; else v23 = v102; v24 = atoi(v23); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( v24 ! = 1 ) { if ( v24 ) { v17 = 0LL ; LABEL_65: dword_B0154 = 0 ; dword_B0150 = 0 ; if ( (v101 & 1 ) ! = 0 ) j__free(v103); dword_B0154 = 0 ; dword_B0150 = 0 ; LABEL_68: ( * env) - >DeleteLocalRef(env, v15); v6 = v17; goto LABEL_220; } dword_B0154 = 0 ; dword_B0150 = 0 ; do v25 = __ldaxr(&dword_AC900); while ( __stlxr(v25 | 1 , &dword_AC900) ); v26 = env; v27 = 0 ; LABEL_64: v17 = sub_2D43C(v26, v27); goto LABEL_65; } dword_B0154 = 0 ; dword_B0150 = 0 ; v32 = sub_43954(env); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( v32 ) { v27 = - 3301 ; LABEL_63: v26 = env; goto LABEL_64; } sub_358E0(env); if ( (s & 1 ) ! = 0 ) { v37 = * (&s + 1 ); j__free(v105); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( !v37 ) { LABEL_51: memset(&s, 0 , 0x90uLL ); sub_2F9DC(env); sub_1E228(&s, &v87); if ( (v87 & 1 ) ! = 0 ) j__free(ptr); sub_30494(env); sub_1E228(&v108[ 1 ], &v87); if ( (v87 & 1 ) ! = 0 ) j__free(ptr); sub_3180C(env); sub_1E228(&v110, &v87); if ( (v87 & 1 ) ! = 0 ) j__free(ptr); sub_30F9C(env); sub_1E228(&v105 + 8 , &v87); if ( (v87 & 1 ) ! = 0 ) j__free(ptr); v36 = sub_30CD0(env); sub_6487C(v36); if ( (v107 & 1 ) ! = 0 ) { * v108[ 0 ] = 0 ; * (&v107 + 1 ) = 0LL ; if ( (v107 & 1 ) ! = 0 ) { j__free(v108[ 0 ]); * &v107 = 0LL ; } } else { LOWORD(v107) = 0 ; } v108[ 0 ] = ptr; v107 = v87; sub_316D4(env); sub_1E228(v113, &v87); if ( (v87 & 1 ) ! = 0 ) j__free(ptr); sub_30238(env); sub_31298(env); sub_3159C(env); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( (s & 1 ) ! = 0 ) v42 = * (&s + 1 ); else v42 = s >> 1 ; if ( !v42 ) goto LABEL_121; dword_B0154 = 0 ; dword_B0150 = 0 ; if ( !((v108[ 1 ] & 1 ) ! = 0 ? v109 : LOBYTE(v108[ 1 ]) >> 1 ) ) goto LABEL_121; dword_B0154 = 0 ; dword_B0150 = 0 ; if ( !((v110 & 1 ) ! = 0 ? v111 : v110 >> 1 ) ) goto LABEL_121; dword_B0154 = 0 ; dword_B0150 = 0 ; if ( !((BYTE8(v105) & 1 ) ! = 0 ? v106[ 0 ] : (BYTE8(v105) >> 1 )) ) goto LABEL_121; dword_B0154 = 0 ; dword_B0150 = 0 ; if ( !((v107 & 1 ) ! = 0 ? * (&v107 + 1 ) : v107 >> 1 ) ) goto LABEL_121; dword_B0154 = 0 ; dword_B0150 = 0 ; if ( !((v113[ 0 ] & 1 ) ! = 0 ? v114 : v113[ 0 ] >> 1 ) ) goto LABEL_121; dword_B0154 = 0 ; dword_B0150 = 0 ; v48 = (v99[ 0 ] & 1 ) ! = 0 ? v99[ 1 ] : (LOBYTE(v99[ 0 ]) >> 1 ); if ( !v48 || ((dword_B0154 = 0 , dword_B0150 = 0 , (endptr[ 0 ] & 1 ) ! = 0 ) ? (v49 = endptr[ 1 ]) : (v49 = (LOBYTE(endptr[ 0 ]) >> 1 )), !v49 || ((dword_B0154 = 0 , dword_B0150 = 0 , (v94 & 1 ) ! = 0 ) ? (v50 = v95) : (v50 = v94 >> 1 ), !v50)) ) { LABEL_121: dword_B0154 = 0 ; dword_B0150 = 0 ; v17 = sub_2D43C(env, 0xFFFFF3E0 ); LABEL_122: dword_B0154 = 0 ; dword_B0150 = 0 ; if ( (v94 & 1 ) ! = 0 ) j__free(v96); if ( (endptr[ 0 ] & 1 ) ! = 0 ) j__free(v98); if ( (v99[ 0 ] & 1 ) ! = 0 ) j__free(v100); if ( (v113[ 0 ] & 1 ) ! = 0 ) j__free(v115); if ( (v110 & 1 ) ! = 0 ) j__free(v112); if ( (v108[ 1 ] & 1 ) ! = 0 ) j__free( * (&v109 + 1 )); if ( (v107 & 1 ) ! = 0 ) j__free(v108[ 0 ]); if ( (BYTE8(v105) & 1 ) ! = 0 ) j__free(v106[ 1 ]); if ( (s & 1 ) ! = 0 ) j__free(v105); goto LABEL_65; } dword_B0154 = 0 ; dword_B0150 = 0 ; v92 = 0LL ; v90 = 0LL ; v51 = sub_324B8(v99, endptr, &v94, &v91, &v90, &v93, &v92); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( (v51 & 1 ) ! = 0 ) { v52 = v93; v53 = v91; dword_B0154 = 0 ; dword_B0150 = 0 ; if ( v93 && v92 && v91 && v90 ) { ptr = v91; v89 = v90; * &v87 = v93; * (&v87 + 1 ) = v92; v54 = sub_1F688(env, &s, &v87); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( v54 ) { v17 = sub_2D43C(env, v54); } else { if ( (byte_ACAAA & 1 ) = = 0 ) { sub_19DDC(asc_A04F5, &unk_A0E38, 0xAu , 2u ); byte_A04F7 = 0 ; } byte_ACAAA = 1 ; sub_1E0E4(v85, asc_A04F5); sub_1DE30(v85, v86); if ( (v85[ 0 ] & 1 ) ! = 0 ) j__free(v85[ 2 ]); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( (v86[ 0 ] & 1 ) ! = 0 ) v78 = v86[ 1 ]; else v78 = LOBYTE(v86[ 0 ]) >> 1 ; if ( v78 ) { v79 = sub_23250(&v108[ 1 ], v86); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( (v79 & 1 ) ! = 0 ) { sub_3E634(env); do v80 = __ldaxr(&dword_AC900); while ( __stlxr(v80 | 2 , &dword_AC900) ); v81 = 0 ; } else { v81 = - 3106 ; } } else { v81 = - 3105 ; } v17 = sub_2D43C(env, v81); dword_B0154 = 0 ; dword_B0150 = 0 ; if ( (v86[ 0 ] & 1 ) ! = 0 ) j__free(v86[ 2 ]); } dword_B0154 = 0 ; dword_B0150 = 0 ; free(v53); free(v52); goto LABEL_241; } v76 = - 3101 ; } else { v76 = - 3102 ; } v17 = sub_2D43C(env, v76); LABEL_241: dword_B0154 = 0 ; dword_B0150 = 0 ; goto LABEL_122; } } else { dword_B0154 = 0 ; dword_B0150 = 0 ; if ( s < 2u ) goto LABEL_51; } v27 = - 3302 ; goto LABEL_63; |
搜索发现-3301经过v32真假判断后被赋值了,所以这个v32的来源很重要,来自sub_43954这个的返回值
如下图,返回值来自env findclass,第二个参数就是要找的类,v32的结果为1,说明v2为1,也就是说这个类找到了,但是由于字符串加密了,stru_A81C6点过去看不出来是什么,最简单的办法unidbg在这行汇编下个断看下
鼠标放在findcalss按tab转汇编视图
你想要知道哪个是参数你得懂汇编的意思,比如00 01 3F D6 BLR X8, 你只需记住B Branch,跳转,剩下的都是它的变体,只是略有区别,所以这行汇编就是要跳到findclass函数执行了,参数呢?根据arm64调用约定,参数1X7 寄存器中 ,剩下的参数从右往左一次入栈,被调用者实现栈平衡,返回值存放在 X0 中。所以x1就是要找的类,下断看下43A14
1 2 3 4 | public void HookByConsoleDebugger() { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module.base+ 0x43A14 ); } |
看到这个java/lang/string 应该明白了吧 java压根没有这个类啊,正确的应该是大写的S String,所以这是安全人员专门给unidbg挖的坑,就是让你跑不起来.还有种方法看这个加密的字符串就是用dump下来的so,内存中的是解密的.
但是跑unidbg最好不要用dump后的
然后知道问题所在就很好办了
第一种ida patch so,但是不推荐,最好不要直接改so
第二种 hook它的返回值,让它返回0
1 2 3 4 5 | public void patch(){ UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x2a148 ); byte [] code = new byte []{( byte ) 0x40 , ( byte ) 0x05 , 0x00 , ( byte ) 0xB5 }; // 400500B5 find class找不到 直接走向错误分支 pointer.write(code); } |
第三种 unidbg也想到了这个, 可以在vm注册后 加一句vm.addNotFoundClass("java/lang/string");
因为unidbg判断不了一个类是否真的存在,所以默认只要你找这个类,就是存在的,安全人员可以设一个隐蔽的坑,修改一个不存在的类,真机找不到返回的是0,但是unidbg返回的是1,而且看jni日志很难分辨出来.
排查掉这个分支后运行下
接下来就是补环境的过程了,不过上面有189条警告,最好处理下再补环境,以免走入新的异常
点ARM64SyscallHandler跳过去看看,上面的NR都是49
unidbg没有实现这个case的syscall,所以弹警告了,这个49是什么指令?可以在https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#arm64-64_bit 查看
chdir 也就是常说的cd app初始化的时候切换目录干啥?看看它要切到哪些目录去,LR指向的是0x23ab8,它的上一行就是cd,到ida中看看
23aB4下断
明白了吧,检测模拟器
在dump下的so里可以看到这些路径,都是模拟器相关的,unidbg没有这个case,所以不用管,直接补环境就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @Override public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case "com/xx/security/xxguard/core/Bridge->getAppContext()Landroid/content/Context;" : return vm.resolveClass( "android/content/Context" ).newObject( null ); case "com/xx/security/xxguard/core/Bridge->getAppKey()Ljava/lang/String;" :{ return new StringObject(vm, "6bbc8976-d0fe-44ef-9776-ab9da0c127e8" ); } case "com/xx/security/xxguard/core/Bridge->getJDGVN()Ljava/lang/String;" :{ return new StringObject(vm, "3.2.8.4" ); } case "com/xx/security/xxguard/core/Bridge->getPicName()Ljava/lang/String;" :{ return new StringObject(vm, "6bbc8976-d0fe-44ef-9776-ab9da0c127e8.jdg.jpg" ); } case "com/xx/security/xxguard/core/Bridge->getSecName()Ljava/lang/String;" :{ return new StringObject(vm, "6bbc8976-d0fe-44ef-9776-ab9da0c127e8.jdg.xbt" ); } } return super .callStaticObjectMethodV(vm, dvmClass, signature, vaList); } @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature){ case "android/content/Context->getPackageCodePath()Ljava/lang/String;" :{ return new StringObject(vm, "/data/app/com.xxx.app.mall-NMPcDqBzdYwQrAm-23cOyQ==/base.apk" ); } } return super .callObjectMethodV(vm, dvmObject, signature, vaList); } |
都是些基础操作
补完返回另一个负值-3102,还是上面那样的排查,很容易就定位到它读apk资源没读到,jni中也有体现
把下面框中的补上就可以看到它读了哪些
Open File:/data/app/com,读了apk就把本地的apk给它
1 2 3 4 5 6 7 8 9 10 | @Override public FileResult resolve(Emulator emulator, String pathname, int oflags) { System.out.println( "Open File:" + pathname); switch (pathname) { case "/data/app/com.xxx.app.mall-NMPcDqBzdYwQrAm-23cOyQ==/base.apk" : { return FileResult.success( new SimpleFileIO(oflags, new File( "unidbg-android/src/test/java/com/xx/files/xx/xx13.1.0.apk" ), pathname)); } } return null ; } |
然后就正常了
直接出结果,但是是一直变的,猜测随机数,时间戳等等,测试只要改unidbg-android/src/main/java/com/github/unidbg/linux/ARM64SyscallHandler.java下的gettimeofday64的currentTimeMillis()改成固定13位就可以了,基本都是改这里,32的就改gettimeofday
1 | { "b1" : "6bbc8976-d0fe-44ef-9776-ab9da0c127e8" , "b2" : "3.2.8.4_0" , "b3" : "2.1" , "b4" : "Y+0jkt2FIdCIV7z0mmcidio1XB/pDmDb6Wq9lXKhNiJvGwwHAPoKQJ8EJhQEF0Ssf1M9komvy2ufFocI2HvAcoq93b3n1kyR1xQyPFFteqltXADYy/z8oUryNr7y/8Tx8voe13qVNa+1HeTf4MzL+AP3GJTh79lGgyShIR1m8VGkmoAcK8j9Bf5z77hsTkAnMBwYjq9jq5clngeRfv2ZRyBs16/ckyGrj4FVKqSi7YS3OCK/ieXbw8N8wh6a7lAsc1UbobM04P+JBRq6ScQ8FI2IBYc+2u4MoV2UeYA5Vl7f7M3Q27uHGM61zN20+ynScTKp28nrMyioBR9CAW5EVTFFXaM8Buvx6RwpkZf2dmT1Q2mFvShO4r8sBk0l84AaNkLKXd4ihQbanZe9zePfFUjJTG8/YKzLK6wTt53k" , "b5" : "8d56fc55ced7dfeae5da682b850782d67736ae3b" , "b7" : "1714398197968" , "b6" : "96d7b46ed8e5c2814672b92d1190330201bd6c8c" } |
结果是固定下来了,但是有个问题frida call的b5是固定的761b422627f7872479184ba1d5036b3780ea06ce,和这里不一样,说明还存在暗桩,这个时候别着急去分析b4和b6,如果b5走了异常分支,b4和b6同样有可能走向异常,所以正确的做法就是分析b5的算法,并找出这个暗桩
我比较喜欢从后往前分析,这个方法适用大部分人.
先用ida的findcrypt找找可能有哪些算法
单看b5长度40位来说,如果没有自定义算法的话好像没有匹配上的,40位首选sha1,说到sha1,应该马上想到前4个魔数和md5一样,第5个魔数0xC3D2E1F0 是sha1独有的,利用unidbg 天然的可下断,内存检索,高效的trace是做算法还原的首选.
打算做两组trace,一组从从init开始,另一组从call fun开始,下面这组代码是通用的
1 2 3 4 5 6 7 8 9 10 | public void trace(){ String traceFile = "unidbg-android/src/test/java/com/xx/biji/trace5.txt" ; PrintStream traceStream = null ; try { traceStream = new PrintStream( new FileOutputStream(traceFile), true ); } catch (FileNotFoundException e) { e.printStackTrace(); } emulator.traceCode(module.base,module.base+module.size).setRedirect(traceStream); } |
init开始trace的3430万行,耗时8min,call fun开始的23万行,耗时5s,多复制几份出来(直接复制文件),全部扔到010里,多弄几个文件的目的是方便做定位,一份搜了之后可以搜另一份,结合着定位更快更准.做init trace的原因是有些数据来自原始的初始化过程,这个留着备用,主要是看call fun那份.
上面说的sha1,先看看有没有0xC3D2E1F0 这个魔数
稳妥点搜一下k值0x5A827999,一共4个 80轮中每20轮用一个
有的,说明sha1肯定参与了运算,而且看左边的热点图,应该是进行了两次sha1,正好b6也是40位,说不好b6也使用了sha1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | def sha1(data): def left_rotate(n, b): return ((n << b) | (n >> ( 32 - b))) & 0xFFFFFFFF # 初始化哈希值 h0 = 0x67452301 h1 = 0xEFCDAB89 h2 = 0x98BADCFE h3 = 0x10325476 h4 = 0xC3D2E1F0 # 预处理 original_byte_len = len (data) original_bit_len = original_byte_len * 8 data + = b '\x80' while ( len (data) % 64 ) ! = 56 : data + = b '\x00' data + = original_bit_len.to_bytes( 8 , 'big' ) # 附加消息长度 大端序 # 处理每个512-bit块 for i in range ( 0 , len (data), 64 ): w = [ 0 ] * 80 chunk = data[i:i + 64 ] # 将块划分为16个32-bit字 for j in range ( 16 ): w[j] = int .from_bytes(chunk[ 4 * j: 4 * j + 4 ], 'big' ) # 扩展到80个字 for j in range ( 16 , 80 ): w[j] = left_rotate(w[j - 3 ] ^ w[j - 8 ] ^ w[j - 14 ] ^ w[j - 16 ], 1 ) # 初始化hash值 a = h0 b = h1 c = h2 d = h3 e = h4 # 主循环 for j in range ( 80 ): if 0 < = j < = 19 : f = (b & c) | (~b & d) k = 0x5A827999 elif 20 < = j < = 39 : f = b ^ c ^ d k = 0x6ED9EBA1 elif 40 < = j < = 59 : f = (b & c) | (b & d) | (c & d) k = 0x8F1BBCDC elif 60 < = j < = 79 : f = b ^ c ^ d k = 0xCA62C1D6 temp = (left_rotate(a, 5 ) + f + e + k + w[j]) & 0xFFFFFFFF e = d d = c c = left_rotate(b, 30 ) b = a a = temp # 增加到当前的hash值 h0 = (h0 + a) & 0xFFFFFFFF h1 = (h1 + b) & 0xFFFFFFFF h2 = (h2 + c) & 0xFFFFFFFF h3 = (h3 + d) & 0xFFFFFFFF h4 = (h4 + e) & 0xFFFFFFFF # 生成最终的哈希值 return ' '.join(f' {x: 08x }' for x in [h0, h1, h2, h3, h4]) message = "yangruhua" hash_value = sha1(message.encode()) print (hash_value) |
上面是一份标准的sha1 python版
下面我演示一下怎么实现开头说的"要求熟悉算法细节,能根据算法特征直接定位明文,还原出正确的密文",此为扩展篇,不看这里的不影响后面的.我得先解释一下上面的代码,不然下面你可能听不懂.
第一步:明文预处理
1 2 3 4 5 6 7 8 9 | # 预处理 original_byte_len = len (data) original_bit_len = original_byte_len * 8 data + = b '\x80' while ( len (data) % 64 ) ! = 56 : data + = b '\x00' data + = original_bit_len.to_bytes( 8 , 'big' ) # 附加消息长度 大端序 |
明文(下面说的都是16进制)不管长度多少,首先填充0x80,接着填充00,然后sha1的分组长度是512位,64字节, while (len(data) % 64) != 56:为什么这里要判断%64后要是56的倍数?因为结尾要填充8字节的附加消息长度,注意这里的是大端序,也就是正常的字节序,如果是md5则是小端序.md5,sha1,sha256都是512分组,sha512是1024分组.
第二步:扩展64块
1 2 3 | # 扩展到80个字 for j in range ( 16 , 80 ): w[j] = left_rotate(w[j - 3 ] ^ w[j - 8 ] ^ w[j - 14 ] ^ w[j - 16 ], 1 ) |
前面说的512分组,64字节,4字节一块,一共16块,sha1 80轮运算需要80个块,所以这里有64个循环,同时观察到这里有一个循环左移,你知道为什么这个算法叫sha1吗?如果你了解sha0算法的话,这个sha1就是在sha0的基础上多了一个循环左移.NSA 1993年发布,两年后SHA-0 被撤回,因为发现了该算法存在安全漏洞,可能使得生成哈希碰撞,SHA-1 对 SHA-0 进行了一个小的修改,主要是通过增加循环左移操作来增强算法的安全性,从而使得寻找哈希碰撞变得更困难.这块也很容易理解.
第三步:核心80轮
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | for j in range ( 80 ): if 0 < = j < = 19 : f = (b & c) | (~b & d) k = 0x5A827999 elif 20 < = j < = 39 : f = b ^ c ^ d k = 0x6ED9EBA1 elif 40 < = j < = 59 : f = (b & c) | (b & d) | (c & d) k = 0x8F1BBCDC elif 60 < = j < = 79 : f = b ^ c ^ d k = 0xCA62C1D6 temp = (left_rotate(a, 5 ) + f + e + k + w[j]) & 0xFFFFFFFF e = d d = c c = left_rotate(b, 30 ) b = a a = temp # 增加到当前的hash值 h0 = (h0 + a) & 0xFFFFFFFF h1 = (h1 + b) & 0xFFFFFFFF h2 = (h2 + c) & 0xFFFFFFFF h3 = (h3 + d) & 0xFFFFFFFF h4 = (h4 + e) & 0xFFFFFFFF |
核心的80轮运算,一共4个k,每20轮用一个,如果实现开头说的"要求熟悉算法细节,能根据算法特征直接定位明文,还原出正确的密文",只需要看明文可能在哪些地方参与了运算,这里说的主要是和特殊值的运算,比如k,体现在这一行temp = (left_rotate(a, 5) + f + e + k + w[j]) & 0xFFFFFFFF,看上去是与k相加的是明文,但不能想的那么简单如果so里是w[j]+left_rotate(a, 5)+f+e+k,这样的话与k相加的就不是明文了,所以需要so里的代码辅助分析,事实上很多算法可操作性很大,明明是一个标准的,就是让你找不到key.比如后面说的aes.
我们来010里搜一下0x5A827999,原始明文与他进行计算
先看第一轮 15a64,ida中看一眼
v15 = v10 + v666 + (v7 | v5) + 0x5A827999; v10 = v6 + (v4 >> 27); v666 = a1[4]; 替代一下 v15 = v6 + (v4 >> 27) + a1[4] + (v7 | v5) + 0x5A827999; // v6 = bswap32(*a2); left_rotate(a, 5) + f + e + k + w[j] // f = (b & c) | (~b & d) e=最开始的魔数[4]
到这里你能理清这些对应关系吗?
v4对应a,a1[4]对应e,(v7 | v5)对应f,0x5A827999对应k,v6对应w[j],这些都是伪代码,才会看起来很奇怪,看多了就觉得正常了,或者从汇编层面直接看寄存器也是一样的.
所以a1(参数0)就是context(拿c语言中的形容,我也不知道叫啥),可以理解为新的魔数.a2就是参数2(明文),v15 = v6 + (v4 >> 27) + a1[4] + (v7 | v5) + 0x5A827999;所以这里与k相加的肯定不是明文,可以在unidbg中下断看下,外层函数的地址是0x15974
验证确实参数0是魔数,注意是小端序
参数1确实是明文
先验证下是否是标准sha1,结果就是下一次的魔数
1 2 3 4 5 6 7 8 | 明文 0000 : 50 4F 53 54 20 2F 63 6C 69 65 6E 74 2E 61 63 74 POST / client.act 0010 : 69 6F 6E 20 61 76 69 66 53 75 70 70 6F 72 74 3D ion avifSupport = 0020 : 31 26 62 65 66 3D 31 26 62 75 69 6C 64 3D 39 39 1 &bef = 1 &build = 99 0030 : 32 30 38 26 63 6C 69 65 6E 74 3D 61 6E 64 72 6F 208 &client = andro unidbg中的结果 0000 : A0 93 31 E3 9B 7E ED A8 FE BA CA 89 5E 36 46 82 .. 1. .~......^ 6F . 0010 : 79 EB B5 05 |
不要直接把明文拿到cyberchef中sha1,那里面默认是有填充的,这里只是sha1的一个分组,为什么明文理论上可以无限长,就是这里每一分组的结果都作为下一分组的魔数,验证的话也很简单,只要把填充部分注释掉
明文输入POST /client.action avifSupport=1&bef=1&build=99208&client=andro,结果e33193a0a8ed7e9b89cabafe8246365e05b5eb79,字节序反转下就是了,所以没有魔改,是标准的.
对比下可知,与k值相加的确实不是明文.,如果一开始直接看这个函数的两个入参也能猜出来,为什么要介绍这些细节呢?这里我只是想说明熟悉算法的作用性很大,后面不会再那么介绍了.
如果这个函数走了很多遍,一遍遍断下来看内存不是很麻烦.
是的,unidbg提供了更简单的方法
这里有一个上面说的花,所以其实15974和159ac是同一块地址,你hook哪个都是一样的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Debugger debugger = emulator.attach(); debugger.addBreakPoint(module.base + 0x159ac , new BreakPointCallback() { RegisterContext context = emulator.getContext(); // sha1 int num = 0 ; @Override public boolean onHit(Emulator<?> emulator, long address) { num+= 1 ; UnidbgPointer src = context.getPointerArg( 0 ); Inspector.inspect(src.getByteArray( 0 , 0x20 ), "0x159ac onEnter arg0 " +num); UnidbgPointer src2 = context.getPointerArg( 1 ); Inspector.inspect(src2.getByteArray( 0 , 0x40 ), "0x159ac onEnter arg1 " +num); debugger.addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { Inspector.inspect(src.getByteArray( 0 , 0x20 ), "0x159ac onLeave arg0 " +num); return true ; } }); return true ; } |
使用Inspector的inspect的可以直接监控一块内存的数据,后面的参数是一个tag,利用context对象可以直接获取到当前位置的寄存器
最后一组就是b6 96d7b46ed8e5c2814672b92d1190330201bd6c8c,内存中的是小端序
1 2 | 6E B4 D7 96 81 C2 E5 D8 2D B9 72 46 02 33 90 11 8C 6C BD 01 |
往上搜b5
b5由uri拼接0x20字节神秘数据后sha1得到
1 | { "b1" : "6bbc8976-d0fe-44ef-9776-ab9da0c127e8" , "b2" : "3.2.8.4_0" , "b3" : "2.1" , "b4" : "Y+0jkt2FIdCIV7z0mmcidio1XB/pDmDb6Wq9lXKhNiJvGwwHAPoKQJ8EJhQEF0Ssf1M9komvy2ufFocI2HvAcoq93b3n1kyR1xQyPFFteqltXADYy/z8oUryNr7y/8Tx8voe13qVNa+1HeTf4MzL+AP3GJTh79lGgyShIR1m8VGkmoAcK8j9Bf5z77hsTkAnMBwYjq9jq5clngeRfv2ZRyBs16/ckyGrj4FVKqSi7YS3OCK/ieXbw8N8wh6a7lAsc1UbobM04P+JBRq6ScQ8FI2IBYc+2u4MoV2UeYA5Vl7f7M3Q27uHGM61zN20+ynScTKp28nrMyioBR9CAW5EVTFFXaM8Buvx6RwpkZf2dmT1Q2mFvShO4r8sBk0l84AaNkLKXd4ihQbanZe9zePfFUjJTG8/YKzLK6wTt53k" , "b5" : "8d56fc55ced7dfeae5da682b850782d67736ae3b" , "b7" : "1714398197968" } |
b6由除去b6的数据拼接0x20字节的神秘数据sha1得到
由此可以猜测b5和b6应该是同一套算法,只是最开始的明文不一样.事实也确实如此,所以后面不再介绍b6
但注意这个是走向异常分支的结果,所以只能说有一定参考价值.frida call和hook区别就在于最后面0x20字节
正确的应该是
1 2 | 2D 75 86 0E 3C 71 84 27 7E 2D B6 B2 A5 AA C7 45 71 74 2E BB 94 89 6C A1 ED 0A 0A 5A 15 1C 84 79 |
所以需要向上层寻找异常点,最开始的做法是跟踪这0x20字节的生成,就是利用emulator.traceWrite和下断跟踪,最终发现这个是在142F4函数生成的,一共3个参数,第三个不变如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 0000 : 6D AA 32 E2 4B 3A 1E FD 2E D2 20 DF 9F 3E DD 76 m. 2.K :.... ..>.v 0010 : 55 71 80 22 1E 4B 9E DF 30 99 BE 00 AF A7 63 76 Uq.".K.. 0. ....cv 0020 : 6D 08 DC DB 73 43 42 04 43 DA FC 04 EC 7D 9F 72 m...sCB.C....}.r 0030 : 2D C6 23 04 5E 85 61 00 1D 5F 9D 04 F1 22 02 76 - . #.^.a.._...".v 0040 : 15 67 B0 7B 4B E2 D1 7B 56 BD 4C 7F A7 9F 4E 09 .g.{K..{V.L...N. 0050 : 14 3B 6B 44 5F D9 BA 3F 09 64 F6 40 AE FB B8 49 .;kD_..?.d.@...I 0060 : 2F DF 64 08 70 06 DE 37 79 62 28 77 D7 99 90 3E / .d.p.. 7yb (w...> 0070 : 9D D1 8A 28 ED D7 54 1F 94 B5 7C 68 43 2C EC 56 ...(..T...|hC,.V 0080 : 2C CB FB 66 C1 1C AF 79 55 A9 D3 11 16 85 3F 47 ,..f...yU.....?G 0090 : 8C 8C 6C 08 4D 90 C3 71 18 39 10 60 0E BC 2F 27 ..l.M..q. 9. `.. / ' 00A0 : 40 27 09 2B 0D B7 CA 5A 15 8E DA 3A 1B 32 F5 1D @'. + ...Z...:. 2. . 第二个参数用来存结果的,最终结果就是上图中的 0010 : 5C 97 10 A8 75 A9 55 E2 55 E6 36 9A 46 87 39 66 \...u.U.U. 6.F . 9f 0020 : F3 5F F1 C2 6E 5B D0 51 1D 1B 3E 03 74 E5 36 E7 ._..n[.Q..>.t. 6. 第一个参数的两次入参 0000 : F6 4F F2 16 6B E6 A2 1D CE 17 ED 14 EE 7B 3B 93 .O..k........{;. 0010 : 30 31 30 32 30 33 30 34 30 35 30 36 30 37 30 38 0102030405060708 0000 : 4C 87 00 B8 65 B9 45 F2 45 F6 26 8A 56 97 29 76 L...e.E.E.&.V.)v 0010 : 5C 97 10 A8 75 A9 55 E2 55 E6 36 9A 46 87 39 66 \...u.U.U. 6.F . 9f |
ida中已经识别出了aes字眼
入参3 16*11 怀疑是扩展的秘钥,可以用第一组秘钥扩展试试,以下代码也是龙哥提供的
Sbox = ( 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16, ) Rcon = (0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36) def text2matrix(text): matrix = [] for i in range(16): byte = (text >> (8 * (15 - i))) & 0xFF if i % 4 == 0: matrix.append([byte]) else: matrix[i // 4].append(byte) return matrix def shiftRound(array, num): ''' :param array: 需要循环左移的数组 :param num: 循环左移的位数 :return: 使用Python切片,返回循环左移num个单位的array ''' return array[num:] + array[:num] def g(array, index): ''' g 函数 :param array: 待处理的四字节数组 :index:从1-10,每次使用Rcon中不同的数 ''' # 首先循环左移1位 array = shiftRound(array, 1) # 字节替换 array = [Sbox[i] for i in array] # 首字节和rcon中对应元素异或 array = [(Rcon[index] ^ array[0])] + array[1:] return array def xorTwoArray(array1, array2): ''' 返回两个数组逐元素异或的新数组 :param array1: 一个array :param array2: 另一个array :return: ''' assert len(array1) == len(array2) return [array1[i] ^ array2[i] for i in range(len(array1))] def showRoundKeys(kList): for i in range(len(kList)): print("K%02d:" %i +"".join("%02x" % k for k in kList[i])) def keyExpand(key): master_key = text2matrix(key) round_keys = [[0] * 4 for i in range(44)] # 规则一(图中红色部分) for i in range(4): round_keys[i] = master_key[i] for i in range(4, 4 * 11): # 规则二(图中红色部分) if i % 4 == 0: round_keys[i] = xorTwoArray(g(round_keys[i - 1], i // 4), round_keys[i - 4]) # 规则三(图中橙色部分) else: round_keys[i] = xorTwoArray(round_keys[i - 1], round_keys[i - 4]) # 将轮密钥从44*4转成11*16,方便后面在明文的运算里使用 kList = [[] for i in range(11)] for i in range(len(round_keys)): kList[i//4] += round_keys[i] showRoundKeys(kList) return kList key = 0x6DAA32E24B3A1EFD2ED220DF9F3EDD76 kList = keyExpand(key)
扩展完竟然对不上,莫非是魔改了秘钥扩展?当时我也不知道怎么办,研究了下两组参数
1 2 3 4 5 6 | 第一个参数的两次入参 0000 : F6 4F F2 16 6B E6 A2 1D CE 17 ED 14 EE 7B 3B 93 .O..k........{;. 0010 : 30 31 30 32 30 33 30 34 30 35 30 36 30 37 30 38 0102030405060708 0000 : 4C 87 00 B8 65 B9 45 F2 45 F6 26 8A 56 97 29 76 L...e.E.E.&.V.)v 0010 : 5C 97 10 A8 75 A9 55 E2 55 E6 36 9A 46 87 39 66 \...u.U.U. 6.F . 9f |
第二组有没有感觉很奇怪,为什么后面的数字前后都是一样的.往加密模式和填充方式那块想,如果是cbc然后填充都是10是不是有可能这种情况,而且5C 97 10 A8 75 A9 55 E2 55 E6 36 9A 46 87 39 66这个就是第一组的密文,填充16个0x10再异或第一组的密文是不是有可能是4C 87 00 B8 65 B9 45 F2 45 F6 26 8A 56 97 29 76,验证下是正确的,那按理 30 31 30 32 30 33 30 34 30 35 30 36 30 37 30 38就是最开始的iv,F6 4F F2 16 6B E6 A2 1D CE 17 ED 14 EE 7B 3B 93明文和iv异或的结果,所以初始明文是c67ec2245bd59229fe22dd22de4c0bab
还差一个key,既然他可能魔改秘钥扩展那直接用扩展的11轮秘钥加密,结果不正确,然后就想着dfa出来key,但是这个查表用的很独特
这是标准的10轮,每一轮的某个方法都是这16个字节一起处理的,这里的每4字节一组,这4组都是分开处理的,不在连续的地址上,感觉根本dfa不了,想了挺久的,最后发现这个6D AA 32 E2 4B 3A 1E FD 2E D2 20 DF 9F 3E DD 76秘钥按小端序来扩展竟然对得上,所以真正的秘钥是0xE232AA6DFD1E3A4BDF20D22E76DD3E9F,拿到cyberchef里加密下
还真是这个结果,但是我用frida hook这个aes的地址发现根本就不走这个位置,白忙活了,这还不是最气人的,最后把异常分支打通,然后跟踪这组数据是在vmp里面.这;流程图看上去好像很小,但其实分支很多,也不是ollvm,待会会说怎么区别ollvm和vmp,气人的是我以为这会是ollvm的aes,一直在里面找aes的特征,结果这是个sha256
前面说了frida不走这,所以这个分支也是错误的,要往上回溯
在刚开始aes断下的地方按bt back trace
frida去hook直到相邻两个地址一个走,一共不走为止,问题就出在这发现215c0往下都是走的,22b00往上都不走,说明出在215c0的内部,它不应该走22b00分支
用流程视图看更直观,往上找,最终发现是在20144的返回值 hook的和unidbg中的不一样
1 2 3 4 5 6 7 8 9 10 11 12 13 | var soAddr = Module.findBaseAddress( "libjdg.so" ); var funcAddr = soAddr.add(0x20144) //32位的话记得+1 Interceptor.attach(funcAddr,{ onEnter: function (args){ console.log( 'onEnter arg[]: ' ,args[0]) this .arg0 = args[0] }, onLeave: function (retval){ console.log( 'onLeave result: ' ,retval) } }); |
1 2 3 4 5 6 7 8 9 10 11 | onLeave result: 0x0 onEnter arg[]: 0x46ddc60f onLeave result: 0x0 onEnter arg[]: 0x36fbbfa1 onLeave result: 0x0 onEnter arg[]: 0x665c9199 onLeave result: 0x0 onEnter arg[]: 0xa203ef4 onLeave result: 0x0 onEnter arg[]: 0xfb95b21 onLeave result: 0x0 |
真机返回的全0,unidbg部分会返回1,这个地方引用很多,就改aes引用的一处会走向另一个分支,改成全0后结果后b5的结果和真机一致,b4不一样有可能就是真机的随机和unidbg的随机数实现不同,肯定是做不到随机相同的,而且不同手机也无法做到.
排除的过程比较耗时,hook上b5就正常了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public void patch2() { Debugger debugger = emulator.attach(); debugger.addBreakPoint(module.base + 0x20144 , new BreakPointCallback() { RegisterContext context = emulator.getContext(); @Override public boolean onHit(Emulator<?> emulator, long address) { debugger.addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() { @Override public boolean onHit(Emulator<?> emulator, long address) { emulator.getBackend().reg_write(Unicorn.UC_ARM64_REG_X0, 0 ); return true ; } }); return true ; } }); } |
这个异常怎么引起的不是很明白,因为有时候返回0有时候返回1,当时还认为是不是低版本不走vmp,高版本走vmp,因为unidbg是安卓6,真机是安卓10,没有安卓6的手机所以用的模拟器,模拟器的安卓6 call的和真机一样,怀疑是一个很隐秘的 anti unidbg,大概是一些系统库和真机不同导致的.也可能是安全人员留下的备用方案,vmp走不通的话走这条,当时测的搜索接口这个也是能用的.不过最好和真机保持一致.这种备用方案很正常,比如某音quic协议都有https的降级通道.
patch后正常了,和真机一致
重新trace一份,从call fun开始,这次240万行,耗时25s,不patch是20多万行,5s,对比可知patch后的算法明显复杂的多.某音的从call fun开始大概1300万行vmp,对比某音还是弱了很多.
1 2 3 | 要分析的数据 0010 : 2D 75 86 0E 3C 71 84 27 7E 2D B6 B2 A5 AA C7 45 - u..<q.'~ - .....E 0020 : 71 74 2E BB 94 89 6C A1 ED 0A 0A 5A 15 1C 84 79 qt....l....Z...y |
b6同b5,这里不介绍了,上面分析到只是最后0x20字节不一样,前面说了这是一个vmp版的sha256,接下来按照我最开始的思路来续写.
上面说了这0x20字节不走aes,不确定它是什么算法的情况下,直接trace write+下断跟踪(循环几次就能找到最开始赋值的位置)
最终赋值的位置在24918
可以发现都是一些左移右移,模运算,取反,异或,写虚拟机需要把这些基础的运算实现.
ollvm与vmp有什么区别?
这是最开始的call的目标函数0x29CE4
截了部分,对比可知,ollvm只是混淆的厉害,但基础的逻辑还是可以看到的,比如函数调用,jni,vmp是只有基本的运算指令.
这是某音的metasec_ml的vmp
伪代码也是只有基础运算,Helios和Ladon Argus Medusa这4个都在vmp里,Gorgon不在,Gorgon简单的trace write就可以解决,新版的还多了一个参数soter,7神了
新版Ladon和Argus很短,即将弃用,ladon和Helios同一个算法,所以没必要留两个,Argus收集的指纹远没有Medusa多,弃用也正常,至于最新的soter,还没来得及研究,后面会试试
这是我朋友修的metasec_ml,对比上面我修的好看多了!不过最终trace都一样.
两种方法
第一种,还原虚拟机指令,这块难度很大,综合性很强,需要有多年的逆向vmp经验,最好是有win vmp的经验,因为安卓的vmp很多是借鉴的pc的.这块我也正在学.
第二种,就是这篇文章的主题,trace还原,主要体现是在010里搜关键常量,密文,从后往前推,根据对应关系直接还原算法,这提供了一个弯道超车的机会,但是技巧性很高,而且很费精力,想象一下,一整天就盯着一堆汇编,十六进制看,是啥感受,而且复杂点的样本要看几个星期.
010里搜0x40024918,最后赋值的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "eor w9, w10, w9" w10 = 0x61 w9 = 0x4c = > w9 = 0x2d "eor w9, w10, w9" w10 = 0xe5 w9 = 0x90 = > w9 = 0x75 "eor w9, w10, w9" w10 = 0xfc w9 = 0x7a = > w9 = 0x86 "eor w9, w10, w9" w10 = 0x18 w9 = 0x16 = > w9 = 0xe "eor w9, w10, w9" w10 = 0x27 w9 = 0x1b = > w9 = 0x3c "eor w9, w10, w9" w10 = 0xb8 w9 = 0xc9 = > w9 = 0x71 "eor w9, w10, w9" w10 = 0x32 w9 = 0xb6 = > w9 = 0x84 "eor w9, w10, w9" w10 = 0x4e w9 = 0x69 = > w9 = 0x27 "eor w9, w10, w9" w10 = 0x4 w9 = 0x7a = > w9 = 0x7e "eor w9, w10, w9" w10 = 0x48 w9 = 0x65 = > w9 = 0x2d "eor w9, w10, w9" w10 = 0x32 w9 = 0x84 = > w9 = 0xb6 "eor w9, w10, w9" w10 = 0xc1 w9 = 0x73 = > w9 = 0xb2 "eor w9, w10, w9" w10 = 0x27 w9 = 0x82 = > w9 = 0xa5 "eor w9, w10, w9" w10 = 0x69 w9 = 0xc3 = > w9 = 0xaa "eor w9, w10, w9" w10 = 0xc3 w9 = 0x4 = > w9 = 0xc7 "eor w9, w10, w9" w10 = 0x54 w9 = 0x11 = > w9 = 0x45 "eor w9, w10, w9" w10 = 0xe5 w9 = 0x94 = > w9 = 0x71 "eor w9, w10, w9" w10 = 0xdf w9 = 0xab = > w9 = 0x74 "eor w9, w10, w9" w10 = 0xc w9 = 0x22 = > w9 = 0x2e "eor w9, w10, w9" w10 = 0x60 w9 = 0xdb = > w9 = 0xbb "eor w9, w10, w9" w10 = 0xf8 w9 = 0x6c = > w9 = 0x94 "eor w9, w10, w9" w10 = 0x64 w9 = 0xed = > w9 = 0x89 "eor w9, w10, w9" w10 = 0xf1 w9 = 0x9d = > w9 = 0x6c "eor w9, w10, w9" w10 = 0x21 w9 = 0x80 = > w9 = 0xa1 "eor w9, w10, w9" w10 = 0x1c w9 = 0xf1 = > w9 = 0xed "eor w9, w10, w9" w10 = 0x78 w9 = 0x72 = > w9 = 0xa "eor w9, w10, w9" w10 = 0xca w9 = 0xc0 = > w9 = 0xa "eor w9, w10, w9" w10 = 0x29 w9 = 0x73 = > w9 = 0x5a "eor w9, w10, w9" w10 = 0xfe w9 = 0xeb = > w9 = 0x15 "eor w9, w10, w9" w10 = 0x5b w9 = 0x47 = > w9 = 0x1c "eor w9, w10, w9" w10 = 0xce w9 = 0x4a = > w9 = 0x84 "eor w9, w10, w9" w10 = 0x w9 = 0x = > w9 = 0x79 / / 最后一组不给,因为含秘钥,测试过不同版本秘钥一致 |
往下滑 发现b7的结果也与这些数据异或,说明w9很有可能是固定的,暂时理解固定的,只是这里是固定的,某音的就不会这样,他是这样操作的至少嵌套几层才会有秘钥的出现.所以重点关注w10的来源,直接搜"eor w9, w10, w9" w10=0x61 w9=0x4c => w9=0x2d 这里的0x61,往上找最有可能的(需要经验),因为会有很多
下面的全是经验了,有经验快些,最好找经过运算得到0x61的,而不是找 ldr(load 加载) str(stor 存)这种,因为c语言传参取参需要通过地址来实现,所以会有很多这种指令,而且很多时候这些不是最开始运算的地方,所以一句话,还是经验.
最后发现其实是0x161,w9也是秘钥,这次是加运算,需要多个文件联合定位,核心的就是这两组秘钥,仅学习交流,所以秘钥不公开.这组秘钥不同版本也是定值.
所以需要再次寻找w10 e9 5b f8 0f,搜0xe95bf80f
还是上面说的找运算得到结果的,别找赋值的,第一个就是了w10=0x6a09e667 w9=0x7f5211a8 => w9=0xe95bf80f,0x6a09e667 什么常量?sha256,前面说的熟悉算法到这就有用了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | import struct # 常量 K = [ 0x428a2f98 , 0x71374491 , 0xb5c0fbcf , 0xe9b5dba5 , 0x3956c25b , 0x59f111f1 , 0x923f82a4 , 0xab1c5ed5 , 0xd807aa98 , 0x12835b01 , 0x243185be , 0x550c7dc3 , 0x72be5d74 , 0x80deb1fe , 0x9bdc06a7 , 0xc19bf174 , 0xe49b69c1 , 0xefbe4786 , 0x0fc19dc6 , 0x240ca1cc , 0x2de92c6f , 0x4a7484aa , 0x5cb0a9dc , 0x76f988da , 0x983e5152 , 0xa831c66d , 0xb00327c8 , 0xbf597fc7 , 0xc6e00bf3 , 0xd5a79147 , 0x06ca6351 , 0x14292967 , 0x27b70a85 , 0x2e1b2138 , 0x4d2c6dfc , 0x53380d13 , 0x650a7354 , 0x766a0abb , 0x81c2c92e , 0x92722c85 , 0xa2bfe8a1 , 0xa81a664b , 0xc24b8b70 , 0xc76c51a3 , 0xd192e819 , 0xd6990624 , 0xf40e3585 , 0x106aa070 , 0x19a4c116 , 0x1e376c08 , 0x2748774c , 0x34b0bcb5 , 0x391c0cb3 , 0x4ed8aa4a , 0x5b9cca4f , 0x682e6ff3 , 0x748f82ee , 0x78a5636f , 0x84c87814 , 0x8cc70208 , 0x90befffa , 0xa4506ceb , 0xbef9a3f7 , 0xc67178f2 ] # 初始哈希值 H = [ 0x6a09e667 , 0xbb67ae85 , 0x3c6ef372 , 0xa54ff53a , 0x510e527f , 0x9b05688c , 0x1f83d9ab , 0x5be0cd19 ] def right_rotate(value, bits): return ((value >> bits) | (value << ( 32 - bits))) & 0xffffffff def sha256(data): # 步骤 1: 填充消息 original_byte_len = len (data) original_bit_len = original_byte_len * 8 data + = b '\x80' data + = b '\x00' * (( 56 - (original_byte_len + 1 ) % 64 ) % 64 ) data + = struct.pack( '>Q' , original_bit_len) # 步骤 2: 解析消息为512-bit块 blocks = [] for i in range ( 0 , len (data), 64 ): blocks.append(data[i:i + 64 ]) # 步骤 3: 初始化工作变量 hash_pieces = H[:] # 步骤 4: 处理每一个块 for block in blocks: W = list (struct.unpack( '>16L' , block)) + [ 0 ] * 48 for i in range ( 16 , 64 ): s0 = right_rotate(W[i - 15 ], 7 ) ^ right_rotate(W[i - 15 ], 18 ) ^ (W[i - 15 ] >> 3 ) s1 = right_rotate(W[i - 2 ], 17 ) ^ right_rotate(W[i - 2 ], 19 ) ^ (W[i - 2 ] >> 10 ) W[i] = (W[i - 16 ] + s0 + W[i - 7 ] + s1) & 0xffffffff a, b, c, d, e, f, g, h = hash_pieces for i in range ( 64 ): S1 = right_rotate(e, 6 ) ^ right_rotate(e, 11 ) ^ right_rotate(e, 25 ) ch = (e & f) ^ (~e & g) temp1 = (h + S1 + ch + K[i] + W[i]) & 0xffffffff S0 = right_rotate(a, 2 ) ^ right_rotate(a, 13 ) ^ right_rotate(a, 22 ) maj = (a & b) ^ (a & c) ^ (b & c) temp2 = (S0 + maj) & 0xffffffff h = g g = f f = e e = (d + temp1) & 0xffffffff d = c c = b b = a a = (temp1 + temp2) & 0xffffffff hash_pieces = [(x + y) & 0xffffffff for x, y in zip (hash_pieces, [a, b, c, d, e, f, g, h])] # 步骤 5: 拼接哈希值 return ' '.join(f' {piece: 08x }' for piece in hash_pieces) hash_value = sha256( 'yangruhua' .encode()) print (f 'SHA-256: {hash_value}' ) |
这是一份python版的sha256,写的很清晰,按照我上面说的方法很容易就能找到明文,找到明文后往上回溯就脱离vmp了,是一个md5,剩下的都不难了.
这点我觉得某音做的很好
第一:首先不会把秘钥那么早就暴露出来,中间会有很多复杂的运算流程才会到最终秘钥出场,这里至少花几天.
第二:算法不要只有一个分支,多弄几组,根据特殊值走不同的算法,这样逆向人员很难找规律,比如medusa的16组算法
第三:不让逆向人员trace出来,这就是反制unidbg的内容了
如果你前面都完整看明白了,b4就不可能拦住你,又不在vmp里.
需要注意的是会调用外部的一个compress 压缩算法,根据压缩特征知道是zlib
1 2 | gzip 1f 8b 08 H4sI / / 格式 16 进制 base64 zlib 78 9c eJw |
b4可以直接解密出明文,vmp里需要两组秘钥,除此以外还需要解密apk里的资源获取一个cJson,这里同样有两组秘钥a6,和a11,如果不走vmp算法,a6用a7替换. b4用秘钥a11,b5 b6用秘钥a6
1 2 3 4 5 6 7 8 9 10 11 12 | { "a1" : 0 , "a10" : 400 , "a2" : "com.xxx.app.mall" , "a11" : "966479bf34cb1eaxxxxxxxxbfce0421893f29a24" , / / 解密b4所需要的RC4的key的一部分 "a3" : "E0D1A70367Cxxxxxxxx4678DFD05F84F" , / / 解密b4会有这个结果出来 "a4" : "99208" , "a5" : "13.x.0" , "a6" : "iy0yIVKJzyT7f5cAwMXGVpQQ1azrxxxxxxxxxxxh4rlgfCgP4T5RqgN5DZ57yu8Y" , "a7" : "eWnsSes/ypEXkWOvqYWbWYruuFz2xxxxxxxxxxx6UNnXqO/CbpA8VT37cF4ap9pg" , "a8" : 1717412177879 } |
1 2 3 | 最新版本的a6和a11,版本不方便透露,你看看时间关系就知道什么版本了 a11 = '250130793xxxxxxxx8d84931fdfa4163d22cf3b4' a6 = 'MiOdPNIQ22pOxxxxxxxxxxMx6FqVzvcjWsINvJyZKahqMRH3f1M2PVCfBNv9urfO' |
感兴趣的可以尝试一下,适合做大厂的第一个入门案例,后续会更新al系,某团,需要些时间,可以期待一下
如果你只做自己能力范围之内的事情,就永远没法进步.或者你能把一件事做到极致.
可是
更多【Android安全-某大厂vmp算法全参trace分析】相关视频教程:www.yxfzedu.com