首页
社区
课程
招聘
[原创]五菱AES DFA。如今弯道今犹在,不见当年过弯人 !
发表于: 2024-11-29 09:48 2536

[原创]五菱AES DFA。如今弯道今犹在,不见当年过弯人 !

2024-11-29 09:48
2536

前言

本篇文章主要是对这个APP 的白盒AES 使用 DFA 攻击最终获取到 key 和 iv的整个流程的分析记录。在论坛上有很多大佬都对这个app进行过分析了。但是我发现挺多细节的流程,大佬都懒得写。对于我这种野生码仔来说,复现案例的时候就经常找不着北,根据正态分布原理来看,牛逼的大佬终究是少数,大部分人还是处于普通水平,我就是那普通的一员,在自己稍微有点摸到点逆向的门槛的时候,也希望分享点经验让大家能少走弯路就少走点吧。于是就出现了本篇文章,我想尽可能的展现出每个步骤。至少在复现的时候,无法跟进下去的时候。知道自己是哪个地方的知识点缺失。去补足之后,继续跟进。

目标基本信息

版本:8.2.4

是否有壳:有

目标接口:登录接口

参数:sd

so : 32位

资料下载地址

链接:https://pan.quark.cn/s/08601adeb0f0
提取码:35HF

确定参数

基操抓包,简单明了就一个 sd 。

img

Java

嗯?这硬-邦-邦-的ke?又给app上套啊。现在不带套都不安全了!!问题我就想不戴套看看里面有什么好东西啊!

img

img

脱壳

基于寒冰大佬开源的 FART 修改的,很多加固厂商都对FART的特征进行检测。所以我自己改了 fart 的特征编译了一个sailfish的 FART10,还可以指定app进行脱壳,也可以指定黑名单,黑名单上面的类就不主动调用,用起来目前还是很丝滑。我把它命名成 XRT10(狗头保命) 。哈哈哈!! 再次感谢寒冰大佬。这个app要脱壳挺久的,虽然我定制的rom过滤掉了很多包,但是也是脱了十几分钟,可能是 saifish 不太行?先不管了,能用就行。

img

img

目标登录页面

img

搜一下在哪里。然后把 dump 下来的 codeitem 回填到 dex 中

img

回填codeitem

img

导入回填好的 comp.dex 然后搜索 "sd"

img

红框内的都是 app 中的 sd 。第一个一看就是个定义变量的,直接跟进。再搜索一波。没搜到,好吧!

img

那就跟进下面的。

img

其实就在刚才那个值下面。然后我们看到了 encrypte 。搞逆向的都知道吧,找到 encrypt 字眼,就说明没找错位置。

img

继续跟进

img

好嘛,也没啥弯弯绕绕,直接干进 native 层了。so 名字也知道了。

img

那就继续盘它咯 。hook 一下获取参数

1
2
3
4
5
6
7
8
9
10
11
function call_java() {
    Java.perform(function () {
        let CheckCodeUtils = Java.use("com.cloudy.linglingbang.model.request.retrofit2.CheckCodeUtils");
        CheckCodeUtils["encrypt"].implementation = function (str, i) {
            console.log(`CheckCodeUtils.encrypt is called: str=${str}, i=${i}`);
            let result = this["encrypt"](str, i);
            console.log(`CheckCodeUtils.encrypt result=${result}`);
            return result;
        };
    })
}

img

Native

Uinidbg 补环境

补环境的话,真没啥好说的,就缺啥补啥吧。慢慢的就熟练了

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
@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);
        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<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
    switch (signature) {
        case "android/os/Build->MODEL:Ljava/lang/String;":
            return new StringObject(vm, "Pixel 4 XL");
        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, "29");
        }
    }
    return super.getStaticObjectField(vm, dvmClass, signature);
}

再来个主动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    private void call_checkcode() {
        List<Object> args = new ArrayList<>();
        args.add(vm.getJNIEnv());
        args.add(0);//jobj
 
//        args.add(vm.addLocalObject(new StringObject(vm, "lvdouzhou")));
        args.add(vm.addLocalObject(new StringObject(vm, "mobile=157696969691&password=996007icu&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token")));
        args.add(2);
        args.add(vm.addLocalObject(new StringObject(vm, "1709100421650")));
 
        Number retNum = module.callFunction(emulator, 0x13480 + 1, args.toArray());
        String ret = vm.getObject(retNum.intValue()).getValue().toString();
        System.out.println("结果->" + ret);
        //25250
    }

加密函数分析与定位

img

img

img

img

img

img

img

接下来的函数无法跟进,需要到汇编看看是怎么跳转的。

img

Hook 一看。咦?R12 再哪呢?可能是我汇编知识掌握不够,确实不知道哪个是R12。

img

那只能换种方式咯。通过代码输出寄存器地址。

1
2
3
4
5
6
7
8
9
debugger.addBreakPoint(module.base + 0x5AFC+1, new BreakPointCallback() {
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        Arm32RegisterContext context1 = emulator.getContext();
        long r12 = context1.getR12Long();
        System.out.println("r12 "+r12);
        return false;
    }
});

得到 1073762617 这个是个十进制的地址。转换成 16 进制就是

img

其中 5139 就是偏移地址,IDA 中按 G 跳转。

img

Nice! 一次就中!

Hook 查看一下 入参

img

1
2
3
4
5
6
7
8
9
10
// hook 最里面的 CWAESCipher_Auth::WBACRAES_EncryptOneBlock 也就是上面获取到的地址
debugger.addBreakPoint(module.base + 0x5138 + 1, new BreakPointCallback() {
    RegisterContext context = emulator.getContext();
 
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        
        return false;
    }
});

第二个参数是我们的入参(这里的入参被我修改掉了,参数与之前不同请忽略,下面会讲怎么修改的)

img

入参的数据太长了。不利于我们后续的分析,所以这里我们 hook 它然后修改它的数据,放入一个新的 16字节的数据。在最开始进入函数的位置就修改它的入参。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
        debugger.addBreakPoint(module.base + 0x5CF4 + 1, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
              
                String fakeInput = "0123456789abcdef";
//                String fakeInput = "lvdouzhou";
                MemoryBlock fakeInputBlock = emulator.getMemory().malloc(fakeInput.length(), true);
                fakeInputBlock.getPointer().write(fakeInput.getBytes(StandardCharsets.UTF_8));
                //修改 r0 为指向新字符串地址的zhiz
                emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, fakeInputBlock.getPointer().peer);
                return true;
            }
        });

重新 hook 0x5138 看入参是否修改成功。

img

注意! 0x538这个地方的函数,参数 par2 我们要重点关注,AES加密完成之后,indata 里面就是加密好的数据。因为我们在前面修改了入参为 0123456789abcdef ,所以我们在这个函数加密完成之后,要专门去拦截看看 AES 加密的结果。DFA 攻击的结果,也要通过这个参数去获取。

img

通过分析可以知道,在这里开始处理明文。AES 第一步就是要把明文进行处理。

img

Hook 看看进入的数据和出来的数据

img

img

1
2
3
debugger.addBreakPoint(module.base +  0x4A10 + 1);   //      CSecFunctProvider::PrepareAESMatrix 对明文进行操作
//  end 
debugger.addBreakPoint(module.base + 0x4A36 + 1); 

mr1 为入参没问题,mr2 位处理后的数据,刚进入函数是 0 也是正确的。注意要记住 r2 的地址 0xbffff468 ,之后的 AES 运算是读取这个地址的数据,DFA 故障注入也是通过这个地址。

img

img

按 c 让程序继续执行到函数返回处的断点。读取 m0xbffff468

img

经过对比,确实是明文处理之后的数据。之后就是要找到 DFA 攻击点了。为什么内从中明文转换的数据会增加 0 我目前也不太清楚,如果有人知道可以在评论区告诉我。

img

寻找 DFA 攻击点

DFA 攻击原理就是要倒数后两轮的列混淆之前注入故障文,然后得到有故障文的加密结果。不断循环这个步骤得到几十上百个故障的加密结果。最终通过 phoneixAES 计算出第10轮秘钥。再使用 stark 通过轮秘钥推到出 k0 秘钥。根据 AES 特性,K00 就是我们的最终目标主密钥。

所以这里我们就是要找到第 9 轮加密开始的地方

img

分析代码之后我们看到了这个特征 9 ,在代码的最下面也看到了 10 这个特征,到10 这里应该就是最后一轮运算,也就是没有列混淆的最后一轮。

img

这里就 hook i==9 这里,看调用了多少次。

img

1
2
3
4
5
6
7
8
9
10
debugger.addBreakPoint(module.base + 0x5194 + 1, new BreakPointCallback() {
    int count = 0;
 
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        count++;
        System.out.println(" 5194 count " + count);
        return true;
    }
});

img

虽然是运行了 10 ,但也是我们的目标地址 。只要在第9轮注入故障文即可。

DFA 攻击

通过前面我们知道了明文地址是 0xbffff468 ,所以只要修改这个地址上的数据来注入故障文。

  1. 修改内存地址中 state 数据

修改传入的 state 明文 ,明文在内存中 0xbffff468 ,所以我们这里直接修改对应内存地址中的数据。

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 dfaAttack() {
    emulator.attach().addBreakPoint(module.base + 0x5194, new BreakPointCallback() {
        int round = 0;
        // 前面找到的处理过的明文的地址 0xbfffeb10L
        UnidbgPointer statePointer = emulator.getMemory().pointer(0xbffff468L );
 
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            round += 1;
            if (round % 9 == 0) {
                System.out.println("inject  error ");
                // 在 0- 16 个字节中随机选一个位置,注入随机故障
                statePointer.setByte(randInt(0, 15), (byte) randInt(0, 15));//随机注入
            }
            return true;//返回true 就不会在控制台断住
        }
    });
 
}
public static int randInt(int min, int max) {
    Random rand = new Random();
    return rand.nextInt((max - min) + 1) + min;
}

确认修改之后数据是否正确。首先获取正常加密之后的密文。注意,不能使用整个so运行完成之后的结果。因为后面还对这个数据做了其他处理。这里我们直接 hook WBACRAES_EncryptOneBlock 这个函数的返回结果。函数的返回地址一般都存放在LR 寄存器中,直接 LR 就能在函数返回之前查看加密结果。

0xbffff468L 是要被处理的数据的地址,我们在 LR 返回的时候通过 Inspector 输出对应加密结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
debugger.addBreakPoint(module.base + 0x5138 + 1, new BreakPointCallback() {
    RegisterContext context = emulator.getContext();
 
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        Arm32RegisterContext context1 = emulator.getContext();
        Pointer r1prt = context1.getR1Pointer();
         //输出传入的明文
        System.out.println("r1str->" + r1prt .getString(0));
         
        // 保存第三个参数的地址
        Pointer r2ptr = context1.getR2Pointer();
        // hook  LR ,LR 是程序准备返回到的地址,在这个看看函数执行的结果
        emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                    //输出 aes 加密完成后的结果
                   Inspector.inspect(r2ptr.getByteArray(0, 16), "AES finish ->");
                return true;
            }
        });
        return true;
    }
});

得到 AES 加密结果

5039f8f250d9a10688806593cefc4f80

img

然后注入故障密文,再输出注入故障文之后的加密结果。这里我们只在第一个字节注入故障文,以确定注入时机是否选择正确。 0xbffff468L 就是AESMatrix 的 indata_state 的地址 。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public void dfaAttack() {
        emulator.attach().addBreakPoint(module.base + 0x5194, new BreakPointCallback() {
            int round = 0;
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                round += 1;
                if (round % 9 == 0) {         
                    UnidbgPointer statePointer = emulator.getMemory().pointer(0xbffff468L);
                    int randnum = randInt(0, 255);
                    long injIndex = randInt(0, 15);
                    statePointer.setByte(0,(byte) randnum );//随机注入
                }
                return true;//返回true 就不会在控制台断住
            }
        });
 
    }
     
public static int randInt(int min, int max) {
    Random rand = new Random();
    return rand.nextInt((max - min) + 1) + min;
}

img

对比两次的结果。发现的确符合 DFA 故障注入之后的特征。在最后一次列混淆注入故障密文,只会影响最终结果的 4 字节,且影响的位置出于0 7 10 13 字节的位置上 。

img

获取大量故障文加密结果

在确定注入位置正确后,就需要获取大量的注入故障文之后的加密结果以反推第10轮秘钥。根据 DFA 原理,我们在地 0 - 3 字节,分别注入 5次故障,一共 生成 20 个故障文。理论上就可以通过 phoenixAES 推到出 k10 也就是第 10 轮秘钥了。

剩下就是苦力活了,各种复制粘贴结果

1
2
3
4
5
# 注意,不是同时写下面的代码。每次用一个就行了,这里只是列举出来
statePointer.setByte(0,(byte) randnum );//随机注入
statePointer.setByte(1,(byte) randnum );//随机注入
statePointer.setByte(2,(byte) randnum );//随机注入
statePointer.setByte(3,(byte) randnum );//随机注入

还有一种写法是在明文矩阵中 0 - 15 字节位置随机注入随机注入

1
statePointer.setByte(randInt(0,15),(byte)  randnum ) 

具体用那种写法,就看你实际测试那种效果好了。这里我用了第一种。

最终得到一批故障文,用下面的格式放入一个文件中。第一行放入正常的密文。后面放故障文

img

然后推导密文

1
2
3
import phoenixAES
 
phoenixAES.crack_file(r".\dfa32new.log",[],True,False,verbose=2)

Nice! 一次就中!得到了 k10 8A6E30D74045AE83634D6ECDE1516CA1

img

推导 k00 也就是主密钥 。得到 主密钥 F6F472F595B511EA9237685B35A8F866

img

寻找 IV

CBC 模式需要使用一个 IV ,且在最开始的时候需要与处理过的明文进行异或操作。异或操作是可以做逆运算的。

也就是 5^6 = 3 反过来,我们可以知道 5^3 = 6 ,这里假设 6 就是我们的目标 iv 。通过这一层操作我们可以在真正进行加密操作前把查看一下传入的数据。把传入加密的数据和我们的原始明文再做一个异或。理论上就可以得到 iv了。

img

结合 hook 结果我们可以在在进入 WBACRAES_EncryptOneBlock 函数的时候,传入的还是没有处理过的明文数据。

img

我们前面 DFA 的注入时机选择在这里

img

所以最有可能进行了 iv 异或的操作位置就在 CSecFunctProvider::PrepareAESMatrix 。直接 hook 这个函数,在函数返回的时候查看第 3 个参数的结果。在 DFA 的时候已经知道了 第三个参数的地址是 0xbffff468 。

1
2
3
4
5
6
7
8
9
10
11
12
debugger.addBreakPoint(module.base + 0x4A10 + 1, new BreakPointCallback() {
     @Override
     public boolean onHit(Emulator<?> emulator, long address) {
         emulator.attach().addBreakPoint(emulator.getContext().getLRPointer().peer, new BreakPointCallback() {
             @Override
             public boolean onHit(Emulator<?> emulator, long address) {
                 return false;
             }
         });
         return false;
     }
 });

img

先看看入参 mr0 没问题

img

按 c 继续执行到函数返回的地方,然后读取 0xbffff468 的数据,理论上就是明文和 iv异或之后的数据 。

img

嗯?? 似乎没有变化?

img

所以 明文^iv=明文 。这个时候就想起了一个异或的定律,任何数与0异或都等于 0 。其实在 DFA的公式推导中就使用了这个神奇的定律

img

掏出计算器验证一下,是了!! 数学就是这么神奇!!amazing!!

img

所以我们这里就可以推到出 iv = 00000000000000000000000000000000

至此我们 key=F6F472F595B511EA9237685B35A8F866 iv= 00000000000000000000000000000000

还差一个 padding

Padding

在IDA中我们向前查看代码可以看到在 CWAESCipher::WBACRAES128_EncryptCBC 中有写入padding 的操作

img

img

没什么说的,hook 查看返回值

img

可以看到我们传入的数据被拼接上了很多数据 。

img

注意看这里还 7 个空位。按 c 继续执行。再查看这个数据。

img

看到 结尾增加了 07 ,没跑了 pkcs7 。

验证

img

img

加密解密都没有问题

再 unidbg 的生成的数据对比一下 。除了开头的 M 其他都一样了。这个M就是个手动拼接上去的字符 。

img

MD5

寻找padding的时候我们传入原文,发现原文最后面拼接了一个checkcode

img

长度是 32 ,第一反应可能是 MD5。对前面字符串的一个签名 。试一下看,试试又不会怀孕。

img

下面的是 unidbg 产生的,上面是手工生成的,发现就是替换了位置而已。

解密函数

img

img

通过 unidbg 主动调用传入,我们刚才的加密结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void decrtpy(String str) {
    // args list
    List<Object> list = new ArrayList<>(5);
    // jnienv
    list.add(vm.getJNIEnv());
    // jclazz
    list.add(0);
    // str
    list.add(vm.addLocalObject(new StringObject(vm, str)));
    // int
    Number number = module.callFunction(emulator, 0x160B4 + 1, list.toArray());
    String result = vm.getObject(number.intValue()).getValue().toString();
    System.out.println("decrypt:" + result);
}
 
 
String ret ="MC2Flb7PBa8OyZYNpAQPQsaGqIfEAfDTdqS+7RDWEJ5mLKMZug4QMfcP1YhKf+0uMnmG5/f4n0gM6gmr4vYkcxbhA7UahLBDfkPF8Ye1G8fZaQ2Jrm9AvO05+6Zj8H10AHyzMnDlaiUDEw0H51sfeLdsInQCa4Y8Y3Og3Aj5wxlbNbutZ6aj+gyA1ONsOxIBsC1gVg5zsCwxUKHQdijg4SzmukFJJqN0gsYiZNRfoVVmkKdZmOxOmn12mi5WR0Jgc0kdUFP8wvWoFvhHPhevkCktRg6tzsdV62EVxVBMLaC+EpWQFjUlKt5mjRhn/Tibr+oerqasrAHYWeyh4Aclv4HTUiHPYf6VPB0sG2P2yRLgBrkJSEuNC3dAF/MS+/vJcJuKgpdE9jHvVBeCXEWOKgA==";
decrtpy(ret);

img

没什么问题,到此就都搞定啦!!

总结

这次案例中我们通过抓包定位到加密参数 sd 。追踪到 native 层的入口为 checkcode 。运用 unidbg 补环境,下断点分析出这是个白盒 AES 。运用 DFA 攻击得到了 AES 的 iv 和 key。这个 app 很适合入门 DFA ,难度适中。需要用到脱壳,抓包,参数定位,native层算法分析,DFA 等各项逆向技能。能够从头跟到尾,相信你对逆向的理解就更上一层楼。

整个过程涉及很多步骤,很难一一写清楚,加上我写文章的能力也一般。有什么不足的欢迎大家在评论区探讨。多谢各位大佬的时间。


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

收藏
免费 9
支持
分享
最新回复 (5)
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
支持一下
2024-11-29 10:01
0
雪    币: 940
活跃值: (9936)
能力值: ( LV13,RANK:385 )
在线值:
发帖
回帖
粉丝
3
这都不给个优秀或精华
2024-12-3 16:00
0
雪    币: 41
活跃值: (898)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
厉害,学习了
2024-12-4 09:20
0
雪    币: 1080
活跃值: (2013)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
牛的
2024-12-4 09:58
0
雪    币: 235
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
感谢你的贡献,论坛因你而更加精彩!
2024-12-8 06:57
0
游客
登录 | 注册 方可回帖
返回
//