声明:文章仅可作为学习用途,不可作为其他用途,侵权请联系,会第一时间删除。
普通AES加密,可以通过hook拿到初始密钥和iv,但是白盒AES,就不能这么做了。对于标准白盒AES,可以采取DFA差分攻击,在最后两轮之间进行故障注入,之后通过PhoneinxAes来还原轮密钥,最后通过Stark从轮密钥还原初始密钥。原理可以看看这篇文章:https://bbs.kanxue.com/thread-280335.htm
DFA还原白盒AES密钥主要流程如下:
找到故障注入位置(最后两次轮密钥加之间)
得到一个正确密文和一组故障密文,通过PhoneinxAes还原轮密钥
使用Stark从轮密钥还原初始密钥
原理概述:
为什么故障注入位置是在最后两次轮密钥加之间?
因为最后一次运算没有列混淆操作,不会导致故障密文到整个状态矩阵。
为什么通过正确密文与一组故障密文可以还原轮密钥?
因为通过系列故障注入,然后枚举出每次轮密钥的取值区间,之后取交集就可以确定轮密钥得值。
为什么可以从轮密钥还原出主密钥?
根据密钥编排算法,可以由最后一组轮密钥反推上一轮轮密钥,最终推算到第一次轮密钥加的轮密钥,第一次轮密钥加的轮密钥就是初始密钥。
1. 抓包分析
1.1 非标准SSL Pinning绕过
1. 非标准SSL Pinning定位
使用brock代理转发手机流量到charles抓包,有证书绑定问题。使用JustTruseMe也无法直接抓取数据包。推测是关键类被混淆,导致JustTrustMe没有找到okhttp的类。
使用GDA打开APK,可以观察到软件被加固,需要对软件进行脱壳处理
使用FART脱壳机运行软件,再使用repireall对脱下来的dex进行修复,完成指令回填。再将修复好的dex打成一个zip包,之后就可以继续分析了。FART主要做了两件事情,一是Dex dump,将内存中的Dex dump到文件得到.dex文件,二是通过主动调用函数,获取指令地址,dump指令到文件,得到.bin文件。之后再将bin文件中的指令回填到dex中,并修复dex头中数据。
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)
//...
关注第三条调用栈信息"at com.android.okhttp.internal.io.RealConnection.connectTls(RealConnection.java:196)" 这里是证书校验的关键位置。找这个函数,去定位SSL绑定位置。查看connectTls代码逻辑,确定证书校验、主机校验及pinner代码位置。
2. 绕过 SSL Pinning
编写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;
};
}
绕过后即可抓取到流量
2. 关键算法定位与分析
2.1 算法定位
算法定位方法有很多种,比如
通过jadx、ida阅读代码静态分析,之后通过hook方式验证。
通过trace快速定位。
这里采取第一种,通过静态分析代码逻辑,动态hook验证方式来定位,后面的文章会有采取trace快速定位的方式。
1. Java层分析
在jadx中搜索请求中的字符串"request",去定位java层代码逻辑
请求内容由·checkCodeUtil.checkcode("F" + b, 1);·生成。
向上跟踪,是一个native函数 public native String checkcode(String str, int i, String str2);
2. SO层分析
定位到"libencrypt.so",使用IDA进行分析,先考虑是不是静态注册的,在导出函数表中搜索一下"checkcode"。若没有再考虑是不是动态注册的,hook RegisterNatives打印注册的函数信息。这里可以直接搜索到,是静态注册的。
IDA中跳转到指定位置,没有函数具体逻辑,so被加壳,无法直接静态分析。
通过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);
});
通过控制台输出结果判断,是一致,说明静态分析确定的函数是加密函数。
dump 内存中的so,之后使用IDA进行分析。由于是直接从内存中dump的so,需要对elf头、加载基址、符号表等信息进行修复(粗略理解为elf文件加载的逆操作),使用soFixer进行修复后,再拖入ida查看流程图。函数是被混淆过的,后面使用undibg和frida配合分析。
2.2 Unidbg分析
1. Unidbg补环境
接下来使用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();
}
}
运行代码,会抛出异常,读取了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;
}
补函数调用
// 根据报错信息来补函数调用
@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主动调用
使用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
编写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);
}
补一下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);
}
主动调用结果如下:
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. 算法分析
根据上面控制台输出“was called from RX@0x40026c64[libencrypt.so]0x26c64”,去0x26c64看一下
使用Unidbg console debugger在0x26C60处下断,关注寄存器x0和x1, 使用m打印对应寄存器中保存的内存处的数据,发现结果来源于x1寄存器。
emulator.attach().addBreakPoint(module.base + 0x26C60);
我们对结果来源进行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
查看LR寄存器保存的地址,可以看到是调用了memcpy后一条地址
在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;
}
});
对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
ida中去看libencrcpy.so的0x1a1c8出代码逻辑, 通过符号可以看出是base64_encode:
在cyberchef上查看,还原一下base64前数据
在base64_encode函数处下断,查看数据来源
来自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
ida中跳转到0x98ec对应位置去看,可以从函数符号来推测是AES128,WB可能是write box(白盒),CBC:加密模式
CBC模式会用IV和明文块0进行异或,所以我们需要确定的IV是什么,还有key,白盒key可以通过DFA差分攻击来提取,后面会详说。 查看CWAESCipher::WBACRAES128_EncryptCBC的代码逻辑,有调用CSecFunctProvider::InsertCBCPadding。
在函数执行前和执行后分别下断点,发现数据并没有改变,确定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);
}
ida直接搜索这个函数名,看他重载参数是一致的那个函数
两个参数一致,两个都hook上,看哪一个调用了,最终确认是 CWAESCipher_Auth::WBACRAES_EncryptOneBlock函数。
3. DFA 还原密钥
3.1 白盒AES分析
ida跳转CWAESCipher_Auth::WBACRAES_EncryptOneBlock函数。,查看逻辑
PrepareAESMatrix函数,通过代码逻辑很明显是将明文转为状态矩阵
回顾一下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
首先是确定“轮”,我们发现,有一个地方是判断了循环次数,而且条件还比较接近10,而且看条代码后面的执行逻辑,相比前面的循环中,少了MixColumns的逻辑。这样就实现了“轮”上的对应。
确定state,数据以state的形式计算、中间存储和传输,也可以反过来说,负责计算、中间存储和传输功能的那个变量就是 state。可以看v55这个变量,参与了很多次运算,从上面PrepareAesMatrix函数也可以看出来。
我们可以通过在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
在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还原密钥
在第一次写入后正确的密文后,我们需要进行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;
}
});
}
之后将我们获取到的正确密文与故障密文使用phoneinxAES来还原轮密钥,第一行是正确密文,之后是故障密文。
import phoenixAES
with open('tracefile', 'wb') as t:
t.write("""3B893D2C58B4473CF8BCBB0323D2C119
3B653D2CF6B4473CF8BCBB2123D2FB19
...
C1893D2C58B447D2F8BC3E0323F8C119
""".encode('utf8'))
phoenixAES.crack_file('tracefile', [], True, False, 3)
通过stark从轮密钥还原主密钥
cyberchef验证