首页
社区
课程
招聘
[原创]记一次某汽车app白盒aes还原过程
发表于: 2024-2-21 16:22 24780

[原创]记一次某汽车app白盒aes还原过程

2024-2-21 16:22
24780

本文仅作学习移动安全交流 请勿用于非法用途

目标app:5LqU6I+x5rG96L2m
包名:Y29tLmNsb3VkeS5saW5nbGluZ2Jhbmc=
版本:8.2.1
加固:梆梆企业版

(1)抓取应用点击登录的接口 可以看到请求体和返回体被加密了 加密的字段名都为sd图片.png(2) 使用xposed尝试注入自吐脚本:发现没有需要的结果 猜测这是一个native层函数
不管静态注册还是动态注册,最终都要走RegisterNative 这个函数 直接使用frida hook RegisterNative 看看有哪些native层函数图片.png发现应用注册了非常多的native层函数 搜索一下encrypt 其中注意到有一个名为encrypt的so文件 注册了几次一个名为checkcode的方法 我们hook一下这个方法 看看是不是我们想要的
(3)编写frida脚本 hook一下 com.bangcle.comapiprotect.CheckCodeUtil.checkcode 这个方法

注入后 在手机上点一下登录 发现控制台输出了内容 我们与重新抓包的内容比对一下 看看是不是我们需要的结果
图片.png可以看到 这个checkcode函数 传入的参数中有我们在应用中输入的手机号 且函数返回的内容与我们抓包抓到的结果一致 至此 定位加密的结果有了
ps:这个加密参数的定位 十分投机取巧 能找到纯属运气 正常情况下 应该对应用进行脱壳 再一层层通过调用栈进行定位 (有点繁琐就偷懒了)

(1)在应用包lib/arm64-v8a下拿到libencrypt.so 放到ida中 在导出函数中搜索checkcode
图片.png有两个有关checkcode的函数 根据函数名 另外一个应该就是解密函数了
(2)跟入checkcode函数 按下f5看伪代码
图片.png图片.png
发现有大量的控制流混淆 好在混淆的不算特别严重 认真分析下还是能看出一个大概的
(3)把函数的几个入参改一下类型 和名字 方便ida识别出JNI结构体 也方便我们后续分析图片.png
往下看 开始先判断了传入的第一个参数的ascii码判断采用哪个加密函数图片.png接着就是取了一些指纹信息图片.png取完指纹信息后 把字符串进行拼接 最后加密 并把结果base64编码
图片.png(4)拿到加密的函数了 写个代码hook下看看入参和返回

图片.png
可以看到打印出来的是几个地址 再hexdump看看 是什么内容图片.png可以看到 第一个是我们要加密的明文 后面两个看不出是什么 先不管
(5)跟入这个aes_encrypt1函数
图片.png
可以看到 这个函数貌似是一个write box(白盒aes加密) 先是初始化了一个CWAESCipher对象 然后进行表转换,最后加密并返回 跟入encryptCBC
图片.png可以看到这个函数先进行了填充 然后进行了一些不知道东西的异或 再往下看
图片.png这里作了一个循环 根据函数名字判断是进行一个块加密 并判断是否全部加密完成 就跳出循环
(6)再跟入EncryptOneBlock函数
图片.png这里应该就是白盒加密的核心部分了 有一些有关AES加密流程的相关符号
图片.png最后把当前块加密的结果放入a3数组中 并返回结果

因为是白盒aes 密钥被隐藏在一个大表中 没办法直接获得加密的key 又有控制流混淆 所以先考虑模拟执行 再进行分析
(1)先搭个架子:

跑一下 看看加载so 调用JNIOnload有没有什么问题:
图片.png报了个环境异常 补上:

图片.png接着补

图片.png补上补上

补完不报错了 这样so加载就没问题了 unidbg还帮我们把jni的调用细节打印出来了
图片.png
(2) 跑目标函数--checkcode
写一个call_checkcode()

调用一下图片.png叕叕叕叕报错了 这里是我们前面在ida中分析到的 一些环境指纹 补上补上:

图片.png结果出来了 也不知道对不对 前面提到有decheckcode方法 把我们模拟执行的结果调用一下解密 看看有没有问题:

调用一下
图片.png发现结果并不正确 应该是环境补的有问题 这时候得向上排错了
(3)补环境排错
好在unidbg很贴心的在控制台中打印了JNI的调用细节 可以看到最后一行 0x2c604 这个地址调用了一个jni函数之后 程序就结束了 ida跳过去看看:图片.png发现程序在走到LABEL_71这个代码块这里就直接退出了 按x看看这个代码块是从哪里被调用了图片.png这里貌似是做了一个有关签名校验 或者包名校验的东西 ,一旦有一个不匹配的 就会跳转到LABEL_71 代码块
图片.png
除此之外 这个地方的判断也会让程序的控制流走向LABEL_71 代码块
图片.pngv6,v7这两个参数在上面sub_1B2F0中有引用 进去看看
图片.png
又是长长的恶心人的控制流混淆
图片.pngsub_1B2F0 这个函数大概就是取到当前应用的一个包名和签名 再看看sub_1AB74 函数
图片.png
sub_1AB74 函数 应该是做了一个文件的读取 进shell cat一下这个文件 看看里面什么内容
图片.png
可以看到 这个文件是储存了当前进程的包名 到这里 我们就能理解为什么
图片.pngunidbg会在控制台抛出一条提示 程序有进行文件读取的操作 写代码把这个文件访问补上:

(4)再跑一下decheckcode函数:图片.png发现没问题了 解密能解出来了 至此 unidbg 调用checkcode函数完成

(1)根据前面在ida中对libencrypt.so进行的静态分析 判断函数WBACRAES_EncryptOneBlock应该是整个白盒加密的关键部位 在ida中找到这个函数的地址0x86F8 unidbg下个断点 看看入参

图片.pngunidbg在0x86F8 处断下 注意到x0和x1都是指针 在控制台输mx0和mx1 看看这两个地址存的什么内容图片.png看起来x0处存的还是一个指针 根据ida中的分析来看 应该是CWAESCipher结构体的指针
而x1存的是我们输入的明文(为了方便分析 我把加密的明文改成了aaaaa)
我们记住x1存的地址0x40559020 这个地址在我们每次重新调用程序进行分析都是不变的 这也是unidbg在算法还原方面的一个优势所在
(2)利用unidbg中的emulator.traceRead api 追踪一下0x40559020-0x40559030 这段存放了明文的地址 看看哪里对明文进行了读取

图片.png这里对这段地址进行了十六次的读取 刚好对应了我们前面下断点读取到的明文 ida跳到0x7888 看看怎么个事
图片.png
这看起来好像是对明文进行了某种排序 我们hook下看看 在函数入口0x7874下个断点 看看a3的地址 在函数结束后读一下 看看是什么内容

图片.png在0x7874处拿到x2寄存器的地址 0xbfffeb10 再让程序运行到函数尾部 看看这个地址存的内容变成什么样图片.png
根据对这个地址内存的查看 我们知道了 PrepareAESMatrix 这个函数就是对明文进行排序 应该是我们aes加密中的plaintext->state阶段 我们再对这段地址进行trace read

图片.pngida跳到0x8c00看看图片.png根据数组符号和前面state转换传入的参数 确定了 这一部分就是进行aes加密中的轮运算的地方 有几个do..while循环嵌套
(3)走到这里 发现 几个do..while 循环的嵌套 单单静态分析还是很难看出哪个循环是单独走完了一轮加密 所以对几个do..while循环 进行hook 看看哪里是只走了9次(对应aes中的前九轮运算)

图片.png结果很明显 0x877C就是一轮计算开始的位置 共循环了九次
(4)前九轮计算循环的位置找到了 接下来就是要找第十轮计算的位置(因为aes加密中第十轮计算少了一个列混淆的步骤 所以程序应该有一个单独的代码块来进行第十轮计算)图片.png运气很好 因为符号没有抹去 根据符号判断 这里进行了最后一轮计算 有三个控制流 都进行hook一下 最终确定最下面的控制流是最终轮计算

(1)上面我们找到了前九轮计算 每一轮计算的开始点0x877C 且有了排序好的state的地址0xbfffeb10 接下来就是要开始故障攻击了

(2)调用一次看看结果

很明显 最终结果的第1,8,11,14 个字节与原始加密的内容不同 符合dfa攻击成功的特征
(3)多次攻击 取不同的故障密文

(4)利用python的phoenixAES模块 对这些故障密文进行分析

图片.png
最终拿到了第十轮的密钥:8A6E30D74045AE83634D6ECDE1516CA1
(5)计算原始密钥
GitHub - SideChannelMarvels/Stark: Repository of small utilities related to key recovery
用这个开源项目 根据轮密钥计算出原始密钥 b3831630add5e91fa4ab1fce08ea507.png最终拿到了我们的key:F6F472F595B511EA9237685B35A8F866
(6)拿到逆向之友里验证一下:
图片.png没毛病 是标准的aes

学逆向一年了 今天第一次写一篇完整的文章 样本难度不高 混淆不算太严重 部分符号没有抹去 才有了攻击点
希望这篇文章能对正在学习移动安全的朋友有所帮助
最后感谢 @白龙 的公开文章 令我受益匪浅 学到了不少东西

function hook_checkcode(){
    Java.perform(function(){
        let CheckCodeUtil = Java.use("com.bangcle.comapiprotect.CheckCodeUtil");
CheckCodeUtil["checkcode"].overload('java.lang.String', 'int', 'java.lang.String').implementation = function (str1, int1, str2) {
    console.log('checkcode is called' + ', ' + 'str: ' + str1 + ', ' + 'i: ' + int1 + ', ' + 'str2: ' + str2);
    let ret = this.checkcode(str1, int1, str2);
    console.log('checkcode ret value is ' + ret);
    return ret;
};
    })
}
function hook_checkcode(){
    Java.perform(function(){
        let CheckCodeUtil = Java.use("com.bangcle.comapiprotect.CheckCodeUtil");
CheckCodeUtil["checkcode"].overload('java.lang.String', 'int', 'java.lang.String').implementation = function (str1, int1, str2) {
    console.log('checkcode is called' + ', ' + 'str: ' + str1 + ', ' + 'i: ' + int1 + ', ' + 'str2: ' + str2);
    let ret = this.checkcode(str1, int1, str2);
    console.log('checkcode ret value is ' + ret);
    return ret;
};
    })
}
function hook_aesencode(){
    let baseaddr = Module.findBaseAddress("libencrypt.so")
    Interceptor.attach(baseaddr.add(0xA5BC),{
        onEnter:function(args){
            console.log("args0:",args[0])
            console.log("args1:",args[1])
            console.log("args2:",args[2])
        },onLeave:function(retval){
            console.log("ret:",retval)
        }
    })
     
}
function hook_aesencode(){
    let baseaddr = Module.findBaseAddress("libencrypt.so")
    Interceptor.attach(baseaddr.add(0xA5BC),{
        onEnter:function(args){
            console.log("args0:",args[0])
            console.log("args1:",args[1])
            console.log("args2:",args[2])
        },onLeave:function(retval){
            console.log("ret:",retval)
        }
    })
     
}
public class CheckCodeUtil extends AbstractJni{
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
 
    private final DvmClass CheckCodeUtil;
 
    private final Memory memory;
 
    private final DalvikModule dvm;
    public CheckCodeUtil(){
        emulator = AndroidEmulatorBuilder.for64Bit()
        .setProcessName("com.cloudy.linglingbang")
        .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
        vm = emulator.createDalvikVM(new File("H:\\JavaProject\\unidbg-0.9.7\\unidbg-android\\src\\test\\java\\com\\cloudy\\linglingbang\\wbaes.apk")); // 创建Android虚拟机
        vm.setVerbose(true); // 设置是否打印Jni调用细节
        vm.setJni(this);
        dvm = vm.loadLibrary(new File("H:\\JavaProject\\unidbg-0.9.7\\unidbg-android\\src\\test\\java\\com\\cloudy\\linglingbang\\libencrypt.so"), true); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
        module = dvm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
        vm.callJNI_OnLoad(emulator,module);
        CheckCodeUtil = vm.resolveClass("com/bangcle/comapiprotect/CheckCodeUtil");
    }
    public static void main(String[] args) {
        CheckCodeUtil checkCodeUtil = new CheckCodeUtil();
    }
}
public class CheckCodeUtil extends AbstractJni{
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
 
    private final DvmClass CheckCodeUtil;
 
    private final Memory memory;
 
    private final DalvikModule dvm;
    public CheckCodeUtil(){
        emulator = AndroidEmulatorBuilder.for64Bit()
        .setProcessName("com.cloudy.linglingbang")
        .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
        memory = emulator.getMemory(); // 模拟器的内存操作接口
        memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
        vm = emulator.createDalvikVM(new File("H:\\JavaProject\\unidbg-0.9.7\\unidbg-android\\src\\test\\java\\com\\cloudy\\linglingbang\\wbaes.apk")); // 创建Android虚拟机
        vm.setVerbose(true); // 设置是否打印Jni调用细节
        vm.setJni(this);
        dvm = vm.loadLibrary(new File("H:\\JavaProject\\unidbg-0.9.7\\unidbg-android\\src\\test\\java\\com\\cloudy\\linglingbang\\libencrypt.so"), true); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
        module = dvm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
        vm.callJNI_OnLoad(emulator,module);
        CheckCodeUtil = vm.resolveClass("com/bangcle/comapiprotect/CheckCodeUtil");
    }
    public static void main(String[] args) {
        CheckCodeUtil checkCodeUtil = new CheckCodeUtil();
    }
}
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
    switch (signature) {
        case "android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread;":{
            return vm.resolveClass("android/app/ActivityThread").newObject(null);
        }
    }
    return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}
@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
    switch (signature) {
        case "android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread;":{
            return vm.resolveClass("android/app/ActivityThread").newObject(null);
        }
    }
    return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
    switch (signature) {
        case "android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;": {
            return vm.resolveClass("android/app/ContextImpl").newObject(null);
        }      
    }
    return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
    switch (signature) {
        case "android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;": {
            return vm.resolveClass("android/app/ContextImpl").newObject(null);
        }      
    }
    return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
    switch (signature) {
        case "android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;": {
            return vm.resolveClass("android/app/ContextImpl").newObject(null);
        }       
        case "android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager;": {
            return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
        }     
    }
    return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
    switch (signature) {
        case "android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;": {
            return vm.resolveClass("android/app/ContextImpl").newObject(null);
        }       
        case "android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager;": {
            return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
        }     
    }
    return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
public void checkcode(){
    //这里的参数是前面hook java层得到的
    String str1 = "mobile=13288888888&password=123456&client_id=2019041810299999999&client_secret=a72a27b1e11b63d8161f0dfd3cab8cef&state=V6g2Lm8888&response_type=token&ostype=ios&imei=00&mac=00:00:00:00:00:00&model=Pixel 4&sdk=29&serviceTime=1706188888888&mod=Google&checkcode=dd9766a6e55044b08d6880c2430fa6eb";
    String str3 = "1706172888888";
    DvmObject ret = CheckCodeUtil.callStaticJniMethodObject(emulator, "checkcode(Ljava/lang/String;ILjava/lang/String;)Ljava/lang/String;",str1,1,str3);
    String strOut = (String)ret.getValue();
    System.out.println("\ncall checkcode: " + strOut);
 
}
public void checkcode(){
    //这里的参数是前面hook java层得到的
    String str1 = "mobile=13288888888&password=123456&client_id=2019041810299999999&client_secret=a72a27b1e11b63d8161f0dfd3cab8cef&state=V6g2Lm8888&response_type=token&ostype=ios&imei=00&mac=00:00:00:00:00:00&model=Pixel 4&sdk=29&serviceTime=1706188888888&mod=Google&checkcode=dd9766a6e55044b08d6880c2430fa6eb";
    String str3 = "1706172888888";
    DvmObject ret = CheckCodeUtil.callStaticJniMethodObject(emulator, "checkcode(Ljava/lang/String;ILjava/lang/String;)Ljava/lang/String;",str1,1,str3);
    String strOut = (String)ret.getValue();
    System.out.println("\ncall checkcode: " + strOut);
 
}
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
    switch (signature) {
        case "android/os/Build->MODEL:Ljava/lang/String;": {
            return new StringObject(vm, "Pixel");
        }
        case "android/os/Build->MANUFACTURER:Ljava/lang/String;": {
            return new StringObject(vm, "Google");
        }
        case "android/os/Build$VERSION->SDK:Ljava/lang/String;": {
            return new StringObject(vm, "23");
        }
    }
    return super.getStaticObjectField(vm, dvmClass, signature);
}
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
    switch (signature) {
        case "android/os/Build->MODEL:Ljava/lang/String;": {
            return new StringObject(vm, "Pixel");
        }
        case "android/os/Build->MANUFACTURER:Ljava/lang/String;": {
            return new StringObject(vm, "Google");
        }
        case "android/os/Build$VERSION->SDK:Ljava/lang/String;": {
            return new StringObject(vm, "23");
        }

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2024-2-21 16:25 被劫__编辑 ,原因:
收藏
免费 14
支持
分享
最新回复 (13)
雪    币: 498
活跃值: (4186)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
6
2024-2-21 18:48
0
雪    币: 4428
活跃值: (5577)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
666
2024-2-21 19:15
0
雪    币: 2399
活跃值: (2852)
能力值: ( LV5,RANK:61 )
在线值:
发帖
回帖
粉丝
4
这里有要补充一点的是:
在作者的:5.开始攻击 还原密钥下的dfaAttack()的statePointer.setByte(0,(byte)randInt(0,15));//随机注入,这种情况下无论执行到0xff次,最多只能还原4位置
也就是:K0: ['D9', 'E2'] K7: ['D4', 'C1'] Ka: ['A5', 'E0'] Kd: ['E6', 'F0']
Round key bytes recovered:
8A............83....6E....51....
FI: good candidate for encryption!
98dfeec304eb147e3ce25a689da2d41c: gr
到这:(3)多次攻击 取不同的故障密文
下,需要改一下,改成为:
statePointer.setByte(randInt(0, 15), (byte) randInt(0, 0xff));//随机注入
。即可复现!

以及感谢大佬的分享
2024-2-22 01:03
1
雪    币: 1037
活跃值: (1780)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
老哥漏了app名字unidbg补环境的地方
2024-2-22 11:56
0
雪    币: 1329
活跃值: (1430)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
强啊。要学习点什么东西才能跟大佬一样?
2024-2-22 14:55
0
雪    币: 2141
活跃值: (4522)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
mb_fidppcok 强啊。要学习点什么东西才能跟大佬一样?
看白龙文章就行
2024-2-22 17:38
0
雪    币: 3004
活跃值: (30866)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
感谢分享
2024-2-23 09:50
1
雪    币: 387
活跃值: (887)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
9
给大佬点赞
2024-2-23 18:04
0
雪    币:
活跃值: (345)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
给大佬点赞,爱来自中国。
2024-2-23 19:16
0
雪    币: 442
活跃值: (864)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
11
给大佬点赞
2024-2-29 16:45
0
雪    币: 241
活跃值: (320)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
小黄鸭爱学习 看白龙文章就行
在哪看呢大佬,我看csdn好像只有几篇了
4天前
0
雪    币: 542
活跃值: (3004)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
感谢分享
2天前
0
雪    币: 1379
活跃值: (2796)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
感谢分享
2天前
0
游客
登录 | 注册 方可回帖
返回
//