声明:文章仅可作为学习用途,不可作为其他用途,侵权请联系,会第一时间删除。

普通AES加密,可以通过hook拿到初始密钥和iv,但是白盒AES,就不能这么做了。对于标准白盒AES,可以采取DFA差分攻击,在最后两轮之间进行故障注入,之后通过PhoneinxAes来还原轮密钥,最后通过Stark从轮密钥还原初始密钥。原理可以看看这篇文章:https://bbs.kanxue.com/thread-280335.htm

DFA还原白盒AES密钥主要流程如下:

  1. 找到故障注入位置(最后两次轮密钥加之间)

  2. 得到一个正确密文和一组故障密文,通过PhoneinxAes还原轮密钥

  3. 使用Stark从轮密钥还原初始密钥

原理概述:

  1. 为什么故障注入位置是在最后两次轮密钥加之间?

    1. 因为最后一次运算没有列混淆操作,不会导致故障密文到整个状态矩阵。

  2. 为什么通过正确密文与一组故障密文可以还原轮密钥?

    1. 因为通过系列故障注入,然后枚举出每次轮密钥的取值区间,之后取交集就可以确定轮密钥得值。

  3. 为什么可以从轮密钥还原出主密钥?

    1. 根据密钥编排算法,可以由最后一组轮密钥反推上一轮轮密钥,最终推算到第一次轮密钥加的轮密钥,第一次轮密钥加的轮密钥就是初始密钥。

1. 抓包分析

1.1 非标准SSL Pinning绕过

1. 非标准SSL Pinning定位

  1. 使用brock代理转发手机流量到charles抓包,有证书绑定问题。使用JustTruseMe也无法直接抓取数据包。推测是关键类被混淆,导致JustTrustMe没有找到okhttp的类。

  1. 使用GDA打开APK,可以观察到软件被加固,需要对软件进行脱壳处理

  1. 使用FART脱壳机运行软件,再使用repireall对脱下来的dex进行修复,完成指令回填。再将修复好的dex打成一个zip包,之后就可以继续分析了。FART主要做了两件事情,一是Dex dump,将内存中的Dex dump到文件得到.dex文件,二是通过主动调用函数,获取指令地址,dump指令到文件,得到.bin文件。之后再将bin文件中的指令回填到dex中,并修复dex头中数据。

  1. okhttp3证书校验最终会调用"com.android.org.conscrypt.NativeSsl"的"doHandshake"方法,此方法是系统提供的方法,无法被混淆。通过hook此方法,打印调用栈,找到"connectTls"方法的位置。

Java.perform(function () {  
    var NativeSsl = Java.use('com.android.org.conscrypt.NativeSsl');  
    NativeSsl.doHandshake.overload('java.io.FileDescriptor', 'int').implementation = function (a, b) {  
        console.log("参数:", a, b);  
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));  
        return this.doHandshake(a, b);  
    };  
});  

// frida -UF  -l hook_sslPinning.js

// arg[0] java.io.FileDescriptor@aa3fa37
// arg[1] 10000
// StackTrace: 
// java.lang.Throwable
//         at com.android.org.conscrypt.NativeSsl.doHandshake(Native Method)
//         at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:226)
//         at com.android.okhttp.internal.io.RealConnection.connectTls(RealConnection.java:196)
//         at com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:153)
//         at com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:116)
//         at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:186)
//...
  1. 关注第三条调用栈信息"at com.android.okhttp.internal.io.RealConnection.connectTls(RealConnection.java:196)" 这里是证书校验的关键位置。找这个函数,去定位SSL绑定位置。查看connectTls代码逻辑,确定证书校验、主机校验及pinner代码位置。

2. 绕过 SSL Pinning

  1. 编写frida hook脚本,绕过证书校验、主机校验及pinner公钥校验,注意,需要修改classloader,否则会找不到类。

function unpinning(){
    //hook 动态加载的dex
    Java.enumerateClassLoaders({   //枚举classLoader
        onMatch: function (loader) {
            try {
                if (loader.findClass("okhttp3.g")) {
                    console.log(loader);
                    Java.classFactory.loader = loader;      //切换classloader
                }
            } catch (error) {
            }
        }, onComplete: function () {
        }
    });

    
	// 这里会去调用客户端证书校验的方法,不执行,就是不去校验(直接通过)。
	var Platform = Java.use('com.android.org.conscrypt.Platform');
    Platform.checkServerTrusted.overload('javax.net.ssl.X509TrustManager', '[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'com.android.org.conscrypt.AbstractConscryptSocket').implementation = function (x509tm, chain, authType, socket) {
        console.log('\n[+] checkServer  ',x509tm,JSON.stringify(x509tm) );
        
        //return this.checkServerTrusted(x509tm, chain, authType, socket);
    };
    // pinner
    let g = Java.use("okhttp3.g");
    g["a"].overload('java.lang.String', 'kotlin.jvm.a.a').implementation = function(str, aVar) {
        console.log('a is called' + ', ' + 'str: ' + str + ', ' + 'aVar: ' + aVar);
        return ;//直接返回
    };

    // host name verify
    let HostnameVerifier = Java.use("javax.net.ssl.HostnameVerifier");
    HostnameVerifier["verify"].implementation = function (str, sSLSession) {
        console.log('verify is called' + ', ' + 'str: ' + str + ', ' + 'sSLSession:' + sSLSession);
        let ret = this.verify(str, sSLSession);
        console.log('verify ret value is ' + ret);
        return true;
    };
}
  1. 绕过后即可抓取到流量

2. 关键算法定位与分析

2.1 算法定位

算法定位方法有很多种,比如

  • 通过jadx、ida阅读代码静态分析,之后通过hook方式验证。

  • 通过trace快速定位。

这里采取第一种,通过静态分析代码逻辑,动态hook验证方式来定位,后面的文章会有采取trace快速定位的方式。

1. Java层分析

  1. 在jadx中搜索请求中的字符串"request",去定位java层代码逻辑

  2. 请求内容由·checkCodeUtil.checkcode("F" + b, 1);·生成。

  1. 向上跟踪,是一个native函数 public native String checkcode(String str, int i, String str2);

2. SO层分析

  1. 定位到"libencrypt.so",使用IDA进行分析,先考虑是不是静态注册的,在导出函数表中搜索一下"checkcode"。若没有再考虑是不是动态注册的,hook RegisterNatives打印注册的函数信息。这里可以直接搜索到,是静态注册的。

  1. IDA中跳转到指定位置,没有函数具体逻辑,so被加壳,无法直接静态分析。

  1. 通过Frida hook java层checkcode与native 层 checkcode,比较参数与返回值,判断是否真的是这个函数。

function hook_java_checkcode() {
  Java.perform(function () {
    //hook 动态加载的dex
    Java.enumerateClassLoaders({   //枚举classLoader
      onMatch: function (loader) {
        try {
          if (loader.findClass("com.xxx.aeri.caranywhere.MyApplication")) {
            console.log(loader);
            Java.classFactory.loader = loader;      //切换classloader
          }
        } catch (error) {
        }
      }, onComplete: function () {
      }
    });

    let CheckCodeUtil = Java.use("com.bangcle.comapiprotect.CheckCodeUtil");
    CheckCodeUtil["checkcode"].overload('java.lang.String', 'int', 'java.lang.String').implementation = function (str, i, str2) {
      console.log('java checkcode is called' + ', ' + 'arg0: ' + str + ', ' + 'arg1: ' + i + ', ' + 'arg2: ' + str2);
      let ret = this.checkcode(str, i, str2);
      console.log('java checkcode ret value is ' + ret);
      return ret;
    };
  });
}

function hook_native_checkcode(){
  Java.perform(function() {
    var base_addr = Module.findBaseAddress("libencrypt.so");
    var real_addr = base_addr.add(0x253AC);
    console.log(real_addr)
    var obj = Java.use("java.lang.Object");
    var String_java = Java.use('java.lang.String');
    var ByteString = Java.use("com.android.okhttp.okio.ByteString");

    Interceptor.attach(real_addr, {
      onEnter: function (args) {
        console.log("hook success!")
        console.log("JNIEnv:" + args[0]) //JNIENV
        console.log("jclazz:" + args[1] ) //jclazz
        console.log("arg0:"+Java.cast(args[2],String_java));
        console.log("arg1:"+args[3].toInt32());
        console.log("arg2:"+Java.cast(args[4],String_java));

        // regs
        //console.log(JSON.stringify(this.context))

      },onLeave: function(retval) {
        console.log("native checkcode ret:"+Java.cast(retval,String_java))
      }
    });
  })
}
setImmediate(function() {
  //延迟1秒调用Hook方法
  setTimeout(hook_java_checkcode, 1000);
  setTimeout(hook_native_checkcode, 1000);
}); 
  1. 通过控制台输出结果判断,是一致,说明静态分析确定的函数是加密函数。

  1. dump 内存中的so,之后使用IDA进行分析。由于是直接从内存中dump的so,需要对elf头、加载基址、符号表等信息进行修复(粗略理解为elf文件加载的逆操作),使用soFixer进行修复后,再拖入ida查看流程图。函数是被混淆过的,后面使用undibg和frida配合分析。

2.2 Unidbg分析

1. Unidbg补环境

  1. 接下来使用Unidbg进行分析,先搭一个undbg环境,先跑起来,加载我们脱壳修复后的so。

```Java
package com.xxx;
import java.io.File;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
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.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;

public class xxx extends AbstractJni implements IOResolver{

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    public xxx(){
        // 创建模拟器实例
        emulator = AndroidEmulatorBuilder.for64Bit()
        .setProcessName("com.xxx.aeri.caranywhere")
        .build();

        // 获取模拟器内存操作接口
        final Memory memory = emulator.getMemory();

        // 设置系统库类解析
        memory.setLibraryResolver(new AndroidResolver(23));
        emulator.getSyscallHandler().addIOResolver(this);

        // 创建Android虚拟机,传入APK
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/xxx/xxx_v7.7.0.apk"));
        // 设置JNI
        vm.setJni(this);
        // 打印日志
        vm.setVerbose(true);
        new AndroidModule(emulator, vm).register(memory);

        // 加载so
        //DalvikModule dm = vm.loadLibrary("encrypt", true);
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/xxx/libencrypt_fixed.so"), true);

        // 获取so句柄
        module = dm.getModule();

        // 调用JNI Onload
        dm.callJNI_OnLoad(emulator);

    }

    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        // TODO Auto-generated method stub
        System.out.println("get path:"+pathname);
        return null;
    }

    public static void main(String[] args){
        xxx xxx = new xxx();

    }
}
  1. 运行代码,会抛出异常,读取了maps文件,从手机中直接dump一个正常的maps文件,然后在unidbg中访问。

//get path:/proc/stat
//get path:/dev/__properties__
//get path:/proc/self/maps
//主类继承IOResolver,补上dump的maps
    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        // TODO Auto-generated method stub
        // throw new UnsupportedOperationException("Unimplemented method 'resolve'");
        System.out.println("get path:"+pathname);
        if("/proc/self/maps".equals(pathname) || ("/proc/" + emulator.getPid()+"/maps").equals(pathname)){
            return FileResult.success(new SimpleFileIO(oflags,new File("unidbg-android/src/test/resources/xxx/maps"),pathname));
        }
        return null;
    }
  1. 补函数调用

// 根据报错信息来补函数调用
    @Override
    public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
        // TODO Auto-generated method stub

        switch(signature){
            // java.lang.UnsupportedOperationException: android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread;
			// at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:432)
            case "android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread;":
                return dvmClass.newObject(null);

            // java.lang.UnsupportedOperationException: android/os/SystemProperties->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
            // at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:432)
            case "android/os/SystemProperties->get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
                String arg0 = varArg.getObjectArg(0).getValue().toString();
                String arg1 = varArg.getObjectArg(1).getValue().toString();
                System.out.println("SystemProperties.get["+arg0+", "+arg1+"]");
                // SystemProperties.get[ro.serialno, unknown]
                String ret = "";
                if (arg0.equals("arg0")){
                    ret = "unknown";
                }

                return new StringObject(vm, ret);
            }
        }


        return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
    }

@Override
    public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
        // TODO Auto-generated method stub
        switch(signature){
            // java.lang.UnsupportedOperationException: android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;
            //     at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:921)
            case "android/app/ActivityThread->getSystemContext()Landroid/app/ContextImpl;":
                return vm.resolveClass("android/app/ContextImpl").newObject(null);

            // java.lang.UnsupportedOperationException: android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager;
            //         at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:921)
            case "android/app/ContextImpl->getPackageManager()Landroid/content/pm/PackageManager;":
                return vm.resolveClass("android/content/pm/PackageManager").newObject(null);

            // java.lang.UnsupportedOperationException: android/app/ContextImpl->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
            // at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:921)
            case "android/app/ContextImpl->getSystemService(Ljava/lang/String;)Ljava/lang/Object;":{
                StringObject serviceName = varArg.getObjectArg(0);
                assert serviceName != null;
                System.out.println("getSystemService["+serviceName+"]");
                return new SystemService(vm, serviceName.getValue());
            }

            // java.lang.UnsupportedOperationException: android/net/wifi/WifiManager->getConnectionInfo()Landroid/net/wifi/WifiInfo;
            // at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:921)
            case "android/net/wifi/WifiManager->getConnectionInfo()Landroid/net/wifi/WifiInfo;":
                return vm.resolveClass("WifiInfo").newObject(null);

            // java.lang.UnsupportedOperationException: WifiInfo->getMacAddress()Ljava/lang/String;
            // at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:921)
            // cat /sys/class/net/wlan0/address
            case "WifiInfo->getMacAddress()Ljava/lang/String;":
                return new StringObject(vm, "9e:99:ed:0b:6f:f7");
        
            }
        
        return super.callObjectMethod(vm, dvmObject, signature, varArg);
    }

2. Unidbg主动调用

  1. 使用Frida hook一下checkcode函数,看一下输出

function hook_checkcode(){
    Java.perform(function() {
        var base_addr = Module.findBaseAddress("libencrypt.so");
        var real_addr = base_addr.add(0x253AC); // checkcode function addr
        console.log(real_addr);

        var obj = Java.use("java.lang.Object");
        var obj = Java.use("java.lang.Object");
		var String_java = Java.use('java.lang.String');
        var ByteString = Java.use("com.android.okhttp.okio.ByteString");

        Interceptor.attach(real_addr, {
            onEnter: function(args){
                console.log("hook success!")
                console.log("JNIEnv :" + args[0]) //JNIENV
                console.log("Jobject :" + args[1] ) //jclazz
	            console.log("arg0:"+Java.cast(args[2],String_java));
	            console.log("arg1:"+args[3].toInt32());
	            console.log("arg2:"+Java.cast(args[4],String_java));

            },onLeave: function(){

            }
        });

    });
}
function main(){
    setTimeout(hook_checkcode, 5000);

}

setImmediate(main)

//arg0:F{"deviceType":"0","identifier":"","networkOperator":"无","city":"","latitude":"39.90923","version":"221","appChannel":"1","timeStamp":"1738923448642","random":"8D6A79B0D0594AE997F1596531FEFA34","imeiMD5":"DEEE68CF927351EF55EFB937CE9FF503","sceneValue":"1","devicename":"GOOGLEPIXEL 2 XL","functionType":"0","networkType":"WiFi","longitude":"116.397428"}
//arg1:1
//arg2:1738923448669
  1. 编写undibg主动调用checkcode

public String checkcode(){
    // arg list
    ArrayList<Object> params = new ArrayList<>(10);
    //JNIEnv
    params.add(vm.getJNIEnv());
    //jclazz
    params.add(0);
    //arg1 
    StringObject arg1 = new StringObject(vm, "F{\"deviceType\":\"0\",\"identifier\":\"\",\"networkOperator\":\"无\",\"city\":\"\",\"latitude\":\"39.90923\",\"version\":\"221\",\"appChannel\":\"1\",\"timeStamp\":\"1738923448642\",\"random\":\"8D6A79B0D0594AE997F1596531FEFA34\",\"imeiMD5\":\"DEEE68CF927351EF55EFB937CE9FF503\",\"sceneValue\":\"1\",\"devicename\":\"GOOGLEPIXEL 2 XL\",\"functionType\":\"0\",\"networkType\":\"WiFi\",\"longitude\":\"116.397428\"}");
    params.add(vm.addLocalObject(arg1));
    //arg2
    params.add(0);
    //arg3
    StringObject arg2 = new StringObject(vm, "1738923448669");
    params.add(vm.addLocalObject(arg2));

    Number number = module.callFunction(emulator, 0x253ac, params.toArray());
    StringObject res = vm.getObject(number.intValue());
    return res.toString();

}

public static void main(String[] args){
    xxx xxx = new xxx();
    String encryptdata = xxx.checkcode();
    System.out.println(encryptdata);

}
  1. 补一下Build相关文件信息

@Override
    public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
        switch (signature) {
        // java.lang.UnsupportedOperationException: android/os/Build->MODEL:Ljava/lang/String;
        // at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103)
        // adb shell getprop ro.product.model
            case "android/os/Build->MODEL:Ljava/lang/String;":
                return new StringObject(vm, "Pixel 2 XL");

        // java.lang.UnsupportedOperationException: android/os/Build->MANUFACTURER:Ljava/lang/String;
        // at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103)
        // adb shell getprop ro.product.manufacturer
            case "android/os/Build->MANUFACTURER:Ljava/lang/String;":
                return new StringObject(vm, "Google");

        // java.lang.UnsupportedOperationException: android/os/Build$VERSION->SDK:Ljava/lang/String;
        // at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103)
        // 
            case "android/os/Build$VERSION->SDK:Ljava/lang/String;":
                return new StringObject(vm, "29");

        }
        return super.getStaticObjectField(vm, dvmClass, signature);
    }
  1. 主动调用结果如下:

JNIEnv->ReleaseStringUTFChars("1738923448669") was called from RX@0x40026c04[libencrypt.so]0x26c04
JNIEnv->NewStringUTF("FO4k9LFi0Rzz4vLsDI9LBGZrPnS2v85bMdCAqX675wwSDs1cZfiL423RxOjouI3c+bQXC8RojGfLpw/H4wyhnAd1oXnBoHAE36oLItIgkB2ynqV71zOXuJuJDnf34XNXkGm/wffoAEisu2W3+nu8O0nM3hQk3Eyw1rZpYV98nSGJq91pwNi2xealUjBinmR/iiFDz3IJ/ovXxoglhQnGAjPZmHm3iw90wpzcmqWADUkgKFJvXjlTjYJLE+0aw7prcxf+9kYTfvdDTswKDHxTOM0YYvX4Pd9yhjVdxbMQPd5KVhcyOauiTzML1MSvJznpsZWJUb1+aAJyJuENprM/mn1vWMbSpAByUFvuckKWeRJW8NNlAQM9JrODLQnC2ihLpnbGdIWdWx5MXP/zIYBsBxQHoQZGE/1IR0QfNyMQyAaPumFXievJ/ipQKB+FVIxHmwNzkhTz8aORIShv062OIwusy6KYA6IhOw7hii7yphy8=") was called from RX@0x40026c64[libencrypt.so]0x26c64
"FO4k9LFi0Rzz4vLsDI9LBGZrPnS2v85bMdCAqX675wwSDs1cZfiL423RxOjouI3c+bQXC8RojGfLpw/H4wyhnAd1oXnBoHAE36oLItIgkB2ynqV71zOXuJuJDnf34XNXkGm/wffoAEisu2W3+nu8O0nM3hQk3Eyw1rZpYV98nSGJq91pwNi2xealUjBinmR/iiFDz3IJ/ovXxoglhQnGAjPZmHm3iw90wpzcmqWADUkgKFJvXjlTjYJLE+0aw7prcxf+9kYTfvdDTswKDHxTOM0YYvX4Pd9yhjVdxbMQPd5KVhcyOauiTzML1MSvJznpsZWJUb1+aAJyJuENprM/mn1vWMbSpAByUFvuckKWeRJW8NNlAQM9JrODLQnC2ihLpnbGdIWdWx5MXP/zIYBsBxQHoQZGE/1IR0QfNyMQyAaPumFXievJ/ipQKB+FVIxHmwNzkhTz8aORIShv062OIwusy6KYA6IhOw7hii7yphy8="

3. 算法分析

  1. 根据上面控制台输出“was called from RX@0x40026c64[libencrypt.so]0x26c64”,去0x26c64看一下

  1. 使用Unidbg console debugger在0x26C60处下断,关注寄存器x0和x1, 使用m打印对应寄存器中保存的内存处的数据,发现结果来源于x1寄存器。

emulator.attach().addBreakPoint(module.base + 0x26C60);

  1. 我们对结果来源进行tracewrite一下,看一下输出,查看PC寄存器,是libc+0x1c208

emulator.traceWrite(0x40576000, 0x40576010);
//[16:52:05 155] Memory WRITE at 0x40576001, data size = 8, data value = 0x3069464c396b344f, PC=RX@0x4033c208[libc.so]0x1c208, LR=RX@0x40378dc4[libc.so]0x58dc4
//[16:52:05 155] Memory WRITE at 0x40576009, data size = 8, data value = 0x44734c76347a7a52, PC=RX@0x4033c208[libc.so]0x1c208, LR=RX@0x40378dc4[libc.so]0x58dc4
  1. 查看LR寄存器保存的地址,可以看到是调用了memcpy后一条地址

  1. 在LR寄存器保存地址的上一条指令下断,关注数据来源

        emulator.attach().addBreakPoint(0x40378dc0, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                RegisterContext registerContext = emulator.getContext();
                int length = registerContext.getIntArg(2);
                UnidbgPointer destStr = registerContext.getPointerArg(0);
                UnidbgPointer srcStr = registerContext.getPointerArg(1);
                Inspector.inspect(srcStr.getByteArray(0,length),"memcpy src==" + srcStr + " dest == " + destStr);
                return true;
            }
        });

  1. 对0x40571000进行tracewrite,看看是在哪里对这个地址进行写入操作。

        emulator.traceWrite(0x40571000, 0x40571002);
        // [11:43:58 512] Memory WRITE at 0x40571001, data size = 1, data value = 0x34, PC=RX@0x4001a1c8[libencrypt.so]0x1a1c8, LR=RX@0x40026b00[libencrypt.so]0x26b00
        // [11:43:58 512] Memory WRITE at 0x40571002, data size = 1, data value = 0x6b, PC=RX@0x4001a150[libencrypt.so]0x1a150, LR=RX@0x40026b00[libencrypt.so]0x26b00
  1. ida中去看libencrcpy.so的0x1a1c8出代码逻辑, 通过符号可以看出是base64_encode:

  1. 在cyberchef上查看,还原一下base64前数据

  1. 在base64_encode函数处下断,查看数据来源

  1. 来自x0寄存器, 对0x4056e000进行tracewrite,确定写入位置

        emulator.traceWrite(0x4056e000, 0x4056e002);
//          [13:22:26 013] Memory WRITE at 0x4056e000, data size = 8, data value = 0x656369766564227b, PC=RX@0x4002a030[libencrypt.so]0x2a030, LR=RX@0x40026918[libencrypt.so]0x26918
//          [13:22:26 013] Memory WRITE at 0x4056e000, data size = 8, data value = 0x656369766564227b, PC=RX@0x40009998[libencrypt.so]0x9998, LR=RX@0x40009a10[libencrypt.so]0x9a10
//          [13:22:26 015] Memory WRITE at 0x4056e000, data size = 8, data value = 0x3c47b4582c3d893b, PC=RX@0x400098ec[libencrypt.so]0x98ec, LR=RX@0x400098e4[libencrypt.so]0x98e4
  1. ida中跳转到0x98ec对应位置去看,可以从函数符号来推测是AES128,WB可能是write box(白盒),CBC:加密模式

  1. CBC模式会用IV和明文块0进行异或,所以我们需要确定的IV是什么,还有key,白盒key可以通过DFA差分攻击来提取,后面会详说。 查看CWAESCipher::WBACRAES128_EncryptCBC的代码逻辑,有调用CSecFunctProvider::InsertCBCPadding。

  1. 在函数执行前和执行后分别下断点,发现数据并没有改变,确定iv=0;继续往下跟,会注意到CWAESCipher::WBACRAES_EncryptOneBlock函数。

__int64 __fastcall CWAESCipher::WBACRAES_EncryptOneBlock(CWAESCipher *this, unsigned __int8 *a2, unsigned __int8 *a3)
{
  return (**(__int64 (__fastcall ***)(CWAESCipher *, unsigned __int8 *, unsigned __int8 *, __int64))this)(
           this,
           a2,
           a3,
           10LL);
}
  1. ida直接搜索这个函数名,看他重载参数是一致的那个函数

  1. 两个参数一致,两个都hook上,看哪一个调用了,最终确认是 CWAESCipher_Auth::WBACRAES_EncryptOneBlock函数。

3. DFA 还原密钥

3.1 白盒AES分析

  1. ida跳转CWAESCipher_Auth::WBACRAES_EncryptOneBlock函数。,查看逻辑

  1. PrepareAESMatrix函数,通过代码逻辑很明显是将明文转为状态矩阵

  1. 回顾一下AES128逻辑,伪代码如下:

state <- plaintext
AddRoundKey(state, k0)
for r=1 ... 9
	SubBytes(state)
	ShiftRows(state)
	MixColumns(state)
	AddRoundKey(state, kr)
SubBytes(state)
ShiftRows(state)
AddRoundKey(state, kr)
ciphertext <- state
  1. 首先是确定“轮”,我们发现,有一个地方是判断了循环次数,而且条件还比较接近10,而且看条代码后面的执行逻辑,相比前面的循环中,少了MixColumns的逻辑。这样就实现了“轮”上的对应。

  1. 确定state,数据以state的形式计算、中间存储和传输,也可以反过来说,负责计算、中间存储和传输功能的那个变量就是 state。可以看v55这个变量,参与了很多次运算,从上面PrepareAesMatrix函数也可以看出来。

  1. 我们可以通过在PrepareAesMatrix函数处下断,打印x2寄存器的值,这个是状态矩阵的地址,之后我们进行DFA差分攻击时需要用到, unidbg中地址不会改变,获取到的x2=0xbfffeb10。

// .text&ARM.extab:0000000000007FB8                 BL              _ZN17CSecFunctProvider16PrepareAESMatrixEPhiPA8_h ;
emulator.attach().addBreakPoint(module.base+0x7FB8);
//         debugger break at: 0x40007fb8 @ Runnable|Function64 address=0x400253ac, arguments=[unidbg@0xfffe1640[libandroid.so]0x640, 0, 1058634310, 0, 1668016508]
// >>> x0=0x4056e000 x1=0x10 x2=0xbfffeb10 x3=0xa x4=0x0 x5=0x180 x6=0x4056dff0 x7=0xe0e0e0e0e0e0e0e x8=0x4056e170 x9=0xaaaaaaaaaaaaaaab x10=0x0 x11=0x405421c0 x12=0x0 x13=0x0 x14=0x1
  1. 在CWAESCipher::WBACRAES_EncryptOneBlock函数返回时,获取X1的值,那个就是加密后的密文

debugger.addBreakPoint(module.base + 0x98b4, new BreakPointCallback() {
            int num = 0;

            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                RegisterContext context = emulator.getContext();
                num += 1;

                final long x1 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X1).longValue();
                emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
                    @Override
                    public boolean onHit(Emulator<?> emulator, long address) {
                        if (num == 1) {
                            Backend backend = emulator.getBackend();
                            final byte[] bytes = backend.mem_read(x1, 0x10);
                            StringBuilder hexString = new StringBuilder();
                            for (byte b : bytes) {
                                hexString.append(String.format("%02X", b & 0xFF));
                            }
                            String filename = "unidbg-android/src/test/java/com/xxx/dfaAes.txt"; // 文件名
                            System.out.println("hexstring===" + hexString);
                            try {
                                FileWriter writer = new FileWriter(filename, true);
                                writer.write(hexString + "\n"); // 写入字符串
                                writer.close();
                            } catch (IOException e) {
                                System.err.println("写入文件时出现错误:" +
                                        e.getMessage());
                            }
                        }
                        return true;
                    }
                });
                return true;
            }
        });

3.2 DFA还原密钥

  1. 在第一次写入后正确的密文后,我们需要进行DFA攻击,由于“经过两次列混淆,故障会扩散倒整个状态矩阵”,所以进行DFA的位置可以考虑是在第九轮。

public void callDfa() {
        Debugger debugger = emulator.attach();
        // 循环开始的位置
        debugger.addBreakPoint(module.base + 0x7ffc, new BreakPointCallback() {
            UnidbgPointer pointer;
            int num = 1; // m0xbfffeb10

            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                pointer = UnidbgPointer.pointer(emulator, 0xbfffeb10L);
				// 第九轮
                if (num % 9 == 0) {
                    System.out.println("===num " + num);
                    Random random = new Random();
                    // 随机改变一个字节
                    int randomLine = random.nextInt(4); // 生成 0 到 3 之间的随机数
                    switch (randomLine) {
                        case 0:
                            pointer.setByte(randInt(0, 3), (byte) randInt(0, 0xff));
                            break;
                        case 1:
                            pointer.setByte(randInt(8, 11), (byte) randInt(0, 0xff));
                            break;
                        case 2:
                            pointer.setByte(randInt(16, 19), (byte) randInt(0, 0xff));
                            break;
                        case 3:
                            pointer.setByte(randInt(24, 27), (byte) randInt(0, 0xff));
                            break;
                    }
                }
                num += 1;
                return true;
            }
        });
    }
  1. 之后将我们获取到的正确密文与故障密文使用phoneinxAES来还原轮密钥,第一行是正确密文,之后是故障密文。

import phoenixAES

with open('tracefile', 'wb') as t:
    t.write("""3B893D2C58B4473CF8BCBB0323D2C119  
3B653D2CF6B4473CF8BCBB2123D2FB19
...
C1893D2C58B447D2F8BC3E0323F8C119
     """.encode('utf8'))
 
phoenixAES.crack_file('tracefile', [], True, False, 3)

  1. 通过stark从轮密钥还原主密钥

  1. cyberchef验证