首页
社区
课程
招聘
[原创]DFA还原白盒AES密钥
2024-3-2 21:53 2571

[原创]DFA还原白盒AES密钥

2024-3-2 21:53
2571

DFA还原白盒AES密钥

本期内容是关于某app模拟登录的,涉及的知识点比较多,有unidbg补环境及辅助还原算法,ida中的md5以及白盒aes,fart脱壳,frida反调试

本章所有样本及资料均上传到了123云盘

https://www.123pan.com/s/4O7Zjv-6MFBd.html

首先抓包

image-20240301191136716'

看login请求,表单和响应都是大长串,猜测是对称加密算法或者是非对称,对称常见的有des和aes,非对称常见的有rsa.

fart脱壳

正常流程下应该是拖到jadx中反编译一下,但是目标app使用了梆梆企业加固

image-20240301191604629

我换了一部由fart脱壳机定制的pixel 4后成功脱壳,后续我会把脱壳的dex放到网盘里,所以对脱壳不了解的可以略过脱壳这个步骤

寒冰的fart脱壳机github地址:https://github.com/hanbinglengyue/FART

把脱下来的dex文件pull到电脑上

image-20240301193322261

对比下脱壳前后的反编译结果

image-20240301200805772

加密位置定位

接下来是定位加密位置了

尝试搜索"sd"

image-20240301200940767

框中的可能性比较大,其他几个类名都是android aliyun google tencent这种系统文件或者第三方厂商的,框中的包含类名以及retrofit框架

这个是目标字段的可能性很大,点进去看看,然后查找用例

image-20240301201431561

右下角框中的有一个decrypt函数,应该是响应的解密逻辑,那上面的应该是加密函数了

image-20240301201639119

点进去然后复制frida片段

1
2
3
4
5
6
7
8
9
10
11
function call(){
    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;
};
})
}

frida反调试

frida注入 frida -UF -l hook.js

以attach方式启动frida后报错无法附加进程,这里我们使用spwan方式启动即可

image-20240301202447875

换成spwan方式后还是报错了,应该还有检测frida-server

image-20240301203133715

换成葫芦娃形式的试试

image-20240301203415751

image-20240301203520112

成功了,接下来就是发个包看看有没有结果

image-20240301195916886

对比下发现结果差不多就是hook的结果把+改成空格就是sd的值了

image-20240301203709699

接着分析jadx中的函数,checkcode点进去

image-20240301203807990

可以看到目标函数返回null,和hook的结果不一样,并且jadx给出了警告,不知道是脱壳脱的不全还是jadx的问题,后续可以用jeb试试,jeb的反编译能力比jadx强

同时可以看到下面有两个native函数,checkcode,和decheckcode,尝试hook checkcode函数

image-20240301204253358

同样有结果

这两个native函数加载自libencrypt.so

image-20240301204420922

这里我选择32位的so,拖到ida32中搜索java,发现是静态注册(如果是动态注册还可以hook libart.so来找导出函数)

image-20240301204635687

unidbg搭架子

接下来是unidbg模拟执行

搭架子

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
package com;
 
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.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
 
import java.io.File;
import java.util.ArrayList;
import java.util.List;
 
public class demo2 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final Memory memory;
 
    demo2(){
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.cloudy.linglingbang").build();
        // 获取模拟器的内存操作接口
        memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/apks/llb/llb.apk"));
        // 设置JNI
        vm.setJni(this);
        // 打印日志
        vm.setVerbose(true);
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/apks/llb/libencrypt.so"), true);
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    };
        public String callByAddress(){
        // args list
        List<Object> list = new ArrayList<>(5);
        // jnienv
        list.add(vm.getJNIEnv());
        // jclazz
        list.add(0);
        // str1
        list.add(vm.addLocalObject(new StringObject(vm, "mobile=13535535353&password=fjfjfjffk&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token")));
        // int
        list.add(2);
        // str2
        list.add(vm.addLocalObject(new StringObject(vm, "1709100421650")));
        Number number = module.callFunction(emulator, 0x13A19, list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
        System.out.println("======encrypt:"+result);
        return result;
    };
    public static void main(String[] args) {
        demo2 llb = new demo2();
//      llb.callByAddress();
    }
}

补环境

运行报错,currentActivityThread 通常用于一些需要获取全局上下文或执行一些与应用程序状态相关的操作的场景

image-20240301205841135

补上,这里没什么好说的,孰能生巧

1
2
3
4
5
6
7
8
9
@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);
    }

接着运行,SystemProperties中的get像是在获取系统的某个属性

image-20240301210325785

1
2
3
4
case "android/os/SystemProperties->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
                String arg = varArg.getObjectArg(0).getValue().toString();
                System.out.println("SystemProperties get arg:"+arg);
            }

image-20240301210629362

获取手机序列号的

image-20240301210845607

adb shell getprop ro.serialno

image-20240301210921431

完整的补上

1
2
3
4
5
6
7
case "android/os/SystemProperties->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
                String arg = varArg.getObjectArg(0).getValue().toString();
                System.out.println("SystemProperties get arg:"+arg);
                if(arg.equals("ro.serialno")){
                    return new StringObject(vm, "9B131FFBA001Y5");
                }
            }

后面的环境不说了,大概也是这样的流程,遇到不会的就google一下或者问问ai,我这里就直接贴一下代码了

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
package com;
 
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 java.io.File;
import java.util.ArrayList;
import java.util.List;
 
public class demo2 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final Memory memory;
 
    demo2(){
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.cloudy.linglingbang").build();
        // 获取模拟器的内存操作接口
        memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/apks/llb/llb.apk"));
        // 设置JNI
        vm.setJni(this);
        // 打印日志
        vm.setVerbose(true);
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/apks/llb/libencrypt.so"), true);
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    };
        public String callByAddress(){
        // args list
        List<Object> list = new ArrayList<>(5);
        // jnienv
        list.add(vm.getJNIEnv());
        // jclazz
        list.add(0);
        // str1
        list.add(vm.addLocalObject(new StringObject(vm, "mobile=13535535353&password=fjfjfjffk&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token")));
        // int
        list.add(2);
        // str2
        list.add(vm.addLocalObject(new StringObject(vm, "1709100421650")));
        Number number = module.callFunction(emulator, 0x13A19, list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
        System.out.println("======encrypt:"+result);
        return result;
    };
    public static void main(String[] args) {
        demo2 llb = new demo2();
        llb.callByAddress();
    }
    @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);
            }
            case "android/os/SystemProperties->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
                String arg = varArg.getObjectArg(0).getValue().toString();
                System.out.println("SystemProperties get arg:"+arg);
                if(arg.equals("ro.serialno")){
                    return new StringObject(vm, "9B131FFBA001Y5");
                }
            }
        }
        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;":{
//                System.out.println("22222");
                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);
            }
            case "android/app/ContextImpl->getSystemService(Ljava/lang/String;)Ljava/lang/Object;":{
                String arg = varArg.getObjectArg(0).getValue().toString();
//                System.out.println("getSystemService arg:"+arg);
                return vm.resolveClass("android.net.wifi").newObject(signature);
            }
            case "android/net/wifi->getConnectionInfo()Landroid/net/wifi/WifiInfo;":{
                return vm.resolveClass("android/net/wifi/WifiInfo").newObject(null);
            }
            case "android/net/wifi/WifiInfo->getMacAddress()Ljava/lang/String;":{
                return new StringObject(vm, "02:00:00:00:00:00");
            }
        }
        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);
    }
}

再次运行下,出结果了

image-20240301211314753

但是怎么验证结果是否正确呢,我这里想着是把结果拿去解密看看,代码如下

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
package com;
 
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 java.io.File;
import java.util.ArrayList;
import java.util.List;
 
public class demo2 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final Memory memory;
 
    demo2(){
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.cloudy.linglingbang").build();
        // 获取模拟器的内存操作接口
        memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/apks/llb/llb.apk"));
        // 设置JNI
        vm.setJni(this);
        // 打印日志
        vm.setVerbose(true);
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/apks/llb/libencrypt.so"), true);
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    };
        public String callByAddress(){
        // args list
        List<Object> list = new ArrayList<>(5);
        // jnienv
        list.add(vm.getJNIEnv());
        // jclazz
        list.add(0);
        // str1
        list.add(vm.addLocalObject(new StringObject(vm, "mobile=13535535353&password=fjfjfjffk&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token")));
        // int
        list.add(2);
        // str2
        list.add(vm.addLocalObject(new StringObject(vm, "1709100421650")));
        Number number = module.callFunction(emulator, 0x13A19, list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
        System.out.println("======encrypt:"+result);
        return result;
    };
    public static void main(String[] args) {
        demo2 llb = new demo2();
        llb.callByAddress();
        llb.decrtpy("Mhub8kSp2n38SHF4COj57zjesFrzCIB2JiH76iCwZZffL3Y4+1/fq1uEDKKWe4yAwiacSVxXNSq1sWN5TwtfHaVgxpOREVGT2+qZEZFkvjP1GaxPCPP2jwuy4x3GvPgHl2NhG2kpsfcXHHQK9HJ5iBdtO44QdDO0vtgqU9MGGb+3q+HJwKlgfWJZj24t8HOSypJNigdCXbUEC6HGEhZhAhMX+Za1lffLlxUouhVh8rzKyESEF97li1h1vTbEf6TJyMbbdEpxh355FbxV9wZgorCa93rDfu+bsVLDbQaAF1TcacxnokoS/yv92hYaqzwzSX3UdH5oQutjW6A4gH1Zk/1Yb3k+IHofvc6Lfm+cxrLHLDtsus9SM/4+2oqsE7tsbgUny37/PQXtUJEOwebDtpz5oYxPgEIbLKIHvptVKwh4=");
    }
    @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);
            }
            case "android/os/SystemProperties->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
                String arg = varArg.getObjectArg(0).getValue().toString();
                System.out.println("SystemProperties get arg:"+arg);
                if(arg.equals("ro.serialno")){
                    return new StringObject(vm, "9B131FFBA001Y5");
                }
            }
        }
        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;":{
//                System.out.println("22222");
                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);
            }
            case "android/app/ContextImpl->getSystemService(Ljava/lang/String;)Ljava/lang/Object;":{
                String arg = varArg.getObjectArg(0).getValue().toString();
//                System.out.println("getSystemService arg:"+arg);
                return vm.resolveClass("android.net.wifi").newObject(signature);
            }
            case "android/net/wifi->getConnectionInfo()Landroid/net/wifi/WifiInfo;":{
                return vm.resolveClass("android/net/wifi/WifiInfo").newObject(null);
            }
            case "android/net/wifi/WifiInfo->getMacAddress()Ljava/lang/String;":{
                return new StringObject(vm, "02:00:00:00:00:00");
            }
        }
        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);
    }
    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, 0x165E1, list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
        System.out.println("======decrypt:"+result);
    }
}

运行结果如下,好像不太正常

image-20240301211546300

从ida中的decheckcode点进去看看

image-20240301213154479

放回的结果异常,说明走了异常的分支,看样子像是返回的是26行的值,判断!v6的值是否为真,v6来自上面的sub_138AC函数,点进去看看

image-20240301213454413

中间的sub_ED04是一个很大的函数,看这像是检测某种环境

image-20240301213630891

往下滑可以看到像是md5的64轮运算,和结尾解密得到的32位数据对应上了,所以说程序大概率是走了这个分支后直接返回了数据

image-20240301213635052

如果是这样的话就好办了

image-20240301213904273

直接在v6的地方取反就好了,看下此次的汇编代码

image-20240301214001551

是一个条件跳转,CBNZ意思是如果r0寄存器的值不为0就跳到loc_16610处,取反的指令就是CBZ(少了个N not),为0就跳

拿到hex转arm网站上看看指令,20 B9对应的是cbnz r0, #0xc

image-20240301214423558

所以我们需要的就是cbz r0, #0xc

image-20240301214520185

把20 B9改成20 B1就可以了,比较原始的方式就是用ida或者010editor改,unidbg也提供了patct的方式直接在程序执行前改机器码

ida和010editor改的方式就不说了,网上有教程,unidbg中这样改

1
2
3
4
5
public void patch(){
        UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x16604);
        byte[] code = new byte[]{(byte) 0x20, (byte) 0xB1};//直接用硬编码改原so的代码:  4FF00109
        pointer.write(code);
    }

在调用callByAddress函数之前调用patch就可以了

image-20240301215035864

解密结果也是出来了,可以看到有手机号,密码还有一些设备信息

还原算法

接下来就是unidbg辅助还原算法了

前面在加密函数的位置看到了aes字眼,所有猜测使用了aes加密

image-20240301215500397

还原aes加密需要确认密钥 加密模式(ecb cbc等等) 是否有iv,填充方式,接下来就是漫长的猜测验证再猜测的过程了,利用unidbg可以console debugger的优点,可以非常方便的还原算法

由于加密函数快3000多行,我这里就说大概得关键位置了,如果写的太细内容就太多了

image-20240301220533659

结合着ida静态分析和unidbg动态调试可以猜测2884行应该是进行aes加密的,并且后续进行了base64编码

image-20240301220808339

点进去发现来到了.bss段, .bss段是用来存放程序中未初始化的全局变量的一块内存区域

看下此次的汇编代码

image-20240301220929018

BLX R3 意思是跳转到寄存器 R3 中存储的地址处执行,所以在unidbg中0x163FE下断,看看R3寄存器的地址

1
debugger.addBreakPoint(module.base+0x163FE);

image-20240301221300745

断在0x163FE处了,前面的0x400是加上了unidbg的基地址,可以看到R3的地址减去基地址也就是后面的地址是0x5a35,再减去thumb的地址加1也就是0x5a34

ida中按G跳转到0x5a34

image-20240301221547257

可以看到aes的具体逻辑就在这里面的几个函数中,最后的WBACRAES128_EncryptCBC貌似是在说white box aes128 cbc模式

如果是这样的话,由于白盒aes11个轮秘钥嵌在程序里,很难直接提取出,需要用dfa(差分故障攻击)获取到第10轮的秘钥,再利用aes_keyschedule这个模块还原出主密钥

WBACRAES128_EncryptCBC点进去

image-20240301222610632

可以看到首先对明文进行了填充,往下滑

image-20240301222847886

WBACRAES_EncryptOneBlock视乎是运算的主体,点进去看看

image-20240301222954843

这里因为我每个地址都下断看了下参数值,实际操作过程需要一步步验证才能走到这

再点进去

image-20240301223140975

这里ida f5出来的看不太懂,看看汇编视图

image-20240301223338118

可以看到结尾跳转到R4寄存器指向的地址,unidbg中下断看下

1
debugger.addBreakPoint(module.base+0x5836);

image-20240301223518940

所以最终会跳到0x4dcc位置处,为什么要-1上面也说过了,跳到0x4dcc去看看

image-20240301224003849

这里会判断i=9的时候跳出循环,PrepareAESMatrix中Matrix是矩阵的意思,所有这个函数应该是对state数据进行矩阵运算

image-20240301224108139

aes的1-9轮和第10轮不一样,第十轮少了一个列混淆运算

为了方便分析秘钥,我让unidbg在aes输入明文的地方修改寄存器的值,这样加密的结果就是16字节的,如果直接修改unidbg的入参的话,由于后续会拼上环境参数二导致参数太长

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

接下来在aes加密结束后的结果是多少

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

由于这个WBACRAES_EncryptOneBlock函数结束的时候寄存器中的地址已经不是原先的用来存返回值的地址了,所有需要提前hook看一下入参时目标参数的地址,代函数执行完直接打印这个地址就是结果了,这里是0xbffff50c m0xbffff50c可以直接看内存中的值

image-20240301225229367

所以正确的密文是57b0d60b1873ad7de3aa2f5c1e4b3ff6

接下来进行dfa攻击(差分故障攻击),这里需要熟悉aes算法的细节,我这里就不介绍了,感兴趣的去龙哥的知识星球学习一下

故障注入的时机是倒数两次列混淆之间,也就是第八轮以及第九轮运算中两次列混淆之间的时机

image-20240301230244942

这里的s应该就是state块

1
2
3
4
5
6
7
8
9
10
11
12
13
debugger.addBreakPoint(module.base+0x4E2A, new BreakPointCallback() {
    int round = 0;
    UnidbgPointer statePointer = memory.pointer(0xbffff458);
    @Override
    public boolean onHit(Emulator<?> emulator, long address) {
        round += 1;
        System.out.println("round:"+round);
        if (round % 9 == 0){
            statePointer.setByte(randInt(0, 15), (byte) randInt(0, 0xff));
        }
        return true;//返回true 就不会在控制台断住
    }
});

DFA还原白盒AES密钥

接下来就是取多次故障密文了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import phoenixAES
with open('tracefile', 'wb') as t:  # 第一行是正确密文 后面是故障密文
    t.write("""57b0d60b1873ad7de3aa2f5c1e4b3ff6
57b0d6a41873737de3892f5c2a4b3ff6
5720d60baf73ad7de3aa2f9b1e4b02f6
57b0f20b18f3ad7daeaa2f5c1e4b3f86
8db0d60b1873ad2fe3aa365c1eab3ff6
e2b0d60b1873ad5be3aafa5c1e1b3ff6
57b04e0b1812ad7d89aa2f5c1e4b3fa7
57d1d60b3773ad7de3aa2f8b1e4b2ff6
bcb0d60b1873ad21e3aa155c1e3d3ff6
57b0bb0b1885ad7d4aaa2f5c1e4b3f29
3ab0d60b1873ad67e3aac65c1e193ff6
57b0d6531873af7de3302f5c964b3ff6
     """.encode('utf8'))
phoenixAES.crack_file('tracefile', [], True, False, 3)

image-20240301231448670

拿到结果了

最后用aes_keyschedule把主密钥也就是初始秘钥还原出来了,F6F472F595B511EA9237685B35A8F866

image-20240301231607021

把刚开始的密文拿到CyberChef尝试解一下,因为cbc模式需要iv,所以先用ecb模式,cbc模式比ecb模式多的就是cbc模式需要每个明文分组先和上个分组的密文块进行异或,由于第一组没有上个分组的密文块,所以需要一个初始化向量IV

image-20240302135202423

上面符号WBACRAES128_EncryptCBC说的是cbc加密模式,但这个符号不一定可信,如果使用的是cbc模式,解出来的结果就是明文块和iv异或的值(矩阵异或)

image-20240302135745654

后面全是0,如果是cbc模式下,明文块和iv异或了,由于是矩阵异或,如果填充方式是pkcs7,就意味着iv的后面几位是68656c6c6f填充后的

68656c6c6f0b0b0b0b0b0b0b0b0b0b0b后面几位,也就是0b0b0b0b0b0b0b0b0b0b0b,如果这样的话明文一变填充的数据也变了,可能是01-0f中的任何一个,这样iv的值也不固定,显然在这种情况下就太复杂了.

image-20240302135949572

所以我认为应该是ecb模式下使用了Zero Padding模式,全部填充0直到一个分组长度

为了验证猜想,在InsertCBCPadding函数结束时打印处理过的state块,unidbg中下断

1
debugger.addBreakPoint(module.base+0x58A0); //m0x40321000

改变输入后发现后面也还是0,也就验证了采用的是Zero Padding模式,并不是常见的pkcs7模式

image-20240302141241131

由于CyberChef中默认是pkcs7填充,所以把模式调成nopadding,这样解密出来的结果就是未填充的一个分组长度了,也验证了上面的Zero Padding模式

image-20240302141627145

这也就是说上面的cbc模式也是错误的,而是ecb模式

尝试加密一下明文和密文对比下

image-20240302142229008

正常的密文是Mhub8kSp2n38SHF4COj57zjesFrzCIB2JiH76iCwZZffL3Y4+1/fq1uEDKKWe4yAwiacSVxXNSq1sWN5TwtfHaVgxpOREVGT2+qZEZFkvjP1GaxPCPP2jwuy4x3GvPgHl2NhG2kpsfcXHHQK9HJ5iBdtO44QdDO0vtgqU9MGGb+3q+HJwKlgfWJZj24t8HOSypJNigdCXbUEC6HGEhZhAhH9QOWkbD6iDkO4mpB0xjvRurFugh+t9P3AeXJeHdhF+MnCXXj3BGlfUgi2qCvoWxYajx2sUcZkXpNbAFbj7VaAlG2ytQnO/L0aZr+SlzTxb90PoLU2VBp98GXNt0ozObSaCwO41UlmZPcKZrr9sxf32nwmoEmUwoTXe14aks2nj72zo5kz8GXyfzh2f6mddZQ==

image-20240302142653707

对比一下,除了正常密文前面多了个M,以及开头有一段相同的,后面都不一样,于是我尝试能不能解密一下

image-20240302165238312

可以看到只解密出了前16个字节,到这里我就感觉有点不太对了,一般来说开发人员不会乱写,如果后续他维护起来也比较麻烦,除非是那种故意写出来迷惑逆向人员的,但前面的aes算法他又暴露了出来,所以我感觉上面的推论可能有点问题,也就是说可能真的是cbc模式.如果是ecb模式下由于分组加密,每个分组单独加密,互不关联,能解第一组的话按理后面的也能解.但如果是cbc模式下每个明文分组先和上个分组的密文块进行异或,直接放到ecb模式下肯定解不出来,那为什么可以解出来第一组呢?我们先看加密模式下,第一个分组下明文和iv异或后进行后续加密,如果只解密第一组则不需要在cbc模式下,ecb就可以,并且解密出来的结果是明文和iv异或的结果,也就是说明文和iv异或后还是明文,a异或b得到a,只有一种情况,b全为0,也就是说iv是00000000000000000000000000000000

image-20240302170520902

看看结果完全正常,也就是说上面的推论有问题,我们再来仔细看看上面的推论

image-20240302171310044

我们否定了pkcs7填充方式,上面用了两个如果,并不能否定cbc模式,如果是cbc模式下的zero padding模式再来看看,解密结果是68656c6c6f0000000000000000000000,这种情况下68656c6c6f(hello的hex形式)zero padding后是68656c6c6f0000000000000000000000,再和iv 00000000000000000000000000000000异或后还是它本身68656c6c6f0000000000000000000000,这样的话就说的通了.所以正确的加密模式应该是aes128-cbc模式-zero _padding填充

key为F6F472F595B511EA9237685B35A8F866,iv为00000000000000000000000000000000

小坑

这里有个坑,当我把明文用上面的加密模式加密一遍,发现结果不对,CyberChef中默认是pkcs7填充,如果能完全解密就说明就是pkcs7填充,可是我们上面的推论也每错啊!!!别急,听我细说.

我把之前的修改r0为指向新字符串的新指针注释掉,采用原始的明文进行填充,这是填充前,304字节刚好19轮

image-20240302195053533

InsertCBCPadding执行后

image-20240302195343422

末尾填充了3个03,这正是pkcs7的填充模式,那为什么上面用hello的明文填充后后面是0呢,这个我也不太清楚这个修改r0寄存器指向新指针的操作,看下面的代码

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

这里我用python aes存算计算了如果使用zero padding模式加密得到的结果也正是最开始的密文57b0d60b1873ad7de3aa2f5c1e4b3ff6

image-20240302195825492

说明上面的推论都没有错,只不过是修改r0为指向新字符串的新指针后经过InsertCBCPadding并没有完成pkcs7填充,但是正常的明文是经过了pkcs7填充的,这里我也不清楚是为什么,但肯定和这个修改r0为指向新字符串的新指针有很大关系.

所以正确的加密模式应该是aes128-cbc模式-pkcs7填充

key为F6F472F595B511EA9237685B35A8F866,iv为00000000000000000000000000000000

写到这里我本来想把上面的错误推论删掉,但是想了想,并不是只有得到正确的结果才会让人进步,所以我保留了,相信每个读者逆向的时候都会有自己的思路,我想我把自己的思路比较完整的写出来了.

md5

再来看上面的明文块

1
mobile=13535535353&password=fjfjfjffk&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token&ostype=ios&imei=unknown&mac=02:00:00:00:00:00&model=Pixel 4 XL&sdk=29&serviceTime=1709100421650&mod=Google&checkcode=6be9743e9f528df4cd9465a97cb645a1

前面几个应该是可以固定的,后面有个checkcode,按单词的意思就是检查代码,中文翻译过来可以理解为验签,防止aes被人破解的情况下如果明文被篡改需要把这个值一并改掉,否则不给通过.

接下来重点看看这个checkcode,32位首先猜md5,上面的图中也看到了疑似md5的64轮运算

image-20240302172240301

这里我先对明文加密了一下,但是不确定是否有盐值

1
2
6be9743e9f528df4cd9465a97cb645a1 明文中的结果
7cb645a19f528df4cd9465a96be9743e md5后的结果

这样一对比好像中间一串是一样的,拆分看看

1
2
6be9743e 9f528df4 cd9465a9 7cb645a1
7cb645a1 9f528df4 cd9465a9 6be9743e

明眼人都能看出来前4个字节和后4个字节调换了顺序,这样的话也不需要去ida中看代码了,直接就得到了结果,这确实有点运气的成分在,但是运气也是实力的一部分啊!

完整算法

替换你自己的mobile和password即可,友情提醒,本文章中所有内容仅供学习交流使用,不用于其他任何目的,请勿对目标app发生大规模请求,否则后果自负!!!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!

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
import base64
from Crypto.Cipher import AES
import requests
import hashlib
from Crypto.Util.Padding import unpad
 
def __pkcs7padding(plaintext):
    block_size = 16
    text_length = len(plaintext)
    bytes_length = len(plaintext.encode('utf-8'))
    len_plaintext = text_length if (bytes_length == text_length) else bytes_length
    return plaintext + chr(block_size - len_plaintext % block_size) * (block_size - len_plaintext % block_size)
def aes_encrypt(mobile,password):
    _str = f'mobile={mobile}&password={password}&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token&ostype=ios&imei=unknown&mac=02:00:00:00:00:00&model=Pixel 4 XL&sdk=29&serviceTime=1709100421650&mod=Google'
    checkcode = hashlib.md5(_str.encode()).hexdigest()
    swapped_string = checkcode[24:] + checkcode[8:24] + checkcode[:8]
    plaintext = _str+'&checkcode='+swapped_string
    key = bytes.fromhex('F6F472F595B511EA9237685B35A8F866')
    iv = bytes.fromhex('00000000000000000000000000000000')
    aes = AES.new(key, AES.MODE_CBC, iv)
    content_padding = __pkcs7padding(plaintext)  # 处理明文, 填充方式
    encrypt_bytes = aes.encrypt(content_padding.encode('utf-8'))  # 加密
    return 'M' + str(base64.b64encode(encrypt_bytes), encoding='utf-8'# 重新编码
def decrypt(text):
    ciphertext = base64.b64decode(text)
    key = bytes.fromhex('F6F472F595B511EA9237685B35A8F866')
    iv = bytes.fromhex('00000000000000000000000000000000')
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)
    decrypted_data = unpad(plaintext, AES.block_size, style='pkcs7')
    return decrypted_data.decode("utf-8")
def login():
    headers = {
        "channel": "yingyongbao",
        "platformNo": "Android",
        "appVersionCode": "1481",
        "version": "V8.0.14",
        "imei": "a-759f0c27ef7fe3b6",
        "imsi": "unknown",
        "deviceModel": "Pixel 4",
        "deviceBrand": "google",
        "deviceType": "Android",
        "accessChannel": "1",
        # "oauthConsumerKey": "2019041810222516127",
        "timestamp": "1709100421649",
        "nonce": "PCpLXbXts7",
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
        "Host": "api.00bang.cn",
        "User-Agent": "okhttp/4.9.0"
    }
    url = "https://api.00bang.cn/llb/oauth/llb/ucenter/login"
    mobile = ''  # 换成你自己的
    password = '' # 换成你自己的
    sd = aes_encrypt(mobile,password)
    print(sd)
    data = {
        "sd": sd
    }
    response = requests.post(url, headers=headers, data=data,verify=False)
    print('加密结果:',response.text)
    print(response)
    print('解密结果',decrypt(response.json()['sd'][1:]))
if __name__ == '__main__':
    login()

总结

1由于本节涉及知识点重多,有很多讲解不到位的地方还请在评论区指出!

2本章所涉及的材料都上传在网盘了,https://www.123pan.com/s/4O7Zjv-6MFBd.html,刚兴趣的自行还原验证,相信对你的安卓逆向水平一定会有提升!

3js逆向转安卓逆向,如有讲解错误的还请多多包涵!

4技术交流+v lyaoyao__i(两个杠)

最后

微信公众号:爬虫爬呀爬

qrcode_for_gh_c637bce93320_258

知识星球

图片描述

如果你觉得这篇文章对你有帮助,不妨请作者喝一杯咖啡吧!

img


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2024-3-3 13:48 被杨如画编辑 ,原因:
收藏
点赞8
打赏
分享
最新回复 (4)
雪    币: 7811
活跃值: (21271)
能力值: ( LV12,RANK:550 )
在线值:
发帖
回帖
粉丝
随风而行aa 10 2024-3-3 11:49
2
1
不错
雪    币: 125
活跃值: (142)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
杨如画 2024-3-3 13:21
3
1
雪    币: 19349
活跃值: (28971)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-3-3 23:00
4
1
感谢分享
雪    币: 540
活跃值: (580)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_fidppcok 2024-3-5 10:17
5
0
反正我是觉得挺强的
游客
登录 | 注册 方可回帖
返回