DFA还原白盒AES密钥

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

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

llb资料官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘

目录

首先抓包

fart脱壳

加密位置定位

frida反调试

unidbg搭架子

补环境

还原算法

DFA还原白盒AES密钥

小坑

md5

完整算法

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

总结

最后

首先抓包

image-20240301191136716

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

fart脱壳

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

image-20240301191604629

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

寒冰的fart脱壳机github地址:GitHub - hanbinglengyue/FART: ART环境下自动化脱壳方案

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

image-20240301193322261

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

image-20240301200805772

加密位置定位

接下来是定位加密位置了

尝试搜索"sd"

image-20240301200940767

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

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

image-20240301201431561

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

image-20240301201639119

点进去然后复制frida片段

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模拟执行

搭架子

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"));// 设置JNIvm.setJni(this);// 打印日志vm.setVerbose(true);// 加载目标SODalvikModule dm = vm.loadLibrary(new File("unidbg-android/apks/llb/libencrypt.so"), true);//获取本SO模块的句柄,后续需要用它module = dm.getModule();// 调用JNI OnLoaddm.callJNI_OnLoad(emulator);};public String callByAddress(){// args listList<Object> list = new ArrayList<>(5);// jnienvlist.add(vm.getJNIEnv());// jclazzlist.add(0);// str1list.add(vm.addLocalObject(new StringObject(vm, "mobile=13535535353&password=fjfjfjffk&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token")));// intlist.add(2);// str2list.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

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

@Overridepublic 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

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

完整的补上

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,我这里就直接贴一下代码了

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"));// 设置JNIvm.setJni(this);// 打印日志vm.setVerbose(true);// 加载目标SODalvikModule dm = vm.loadLibrary(new File("unidbg-android/apks/llb/libencrypt.so"), true);//获取本SO模块的句柄,后续需要用它module = dm.getModule();// 调用JNI OnLoaddm.callJNI_OnLoad(emulator);};public String callByAddress(){// args listList<Object> list = new ArrayList<>(5);// jnienvlist.add(vm.getJNIEnv());// jclazzlist.add(0);// str1list.add(vm.addLocalObject(new StringObject(vm, "mobile=13535535353&password=fjfjfjffk&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token")));// intlist.add(2);// str2list.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();}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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

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

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"));// 设置JNIvm.setJni(this);// 打印日志vm.setVerbose(true);// 加载目标SODalvikModule dm = vm.loadLibrary(new File("unidbg-android/apks/llb/libencrypt.so"), true);//获取本SO模块的句柄,后续需要用它module = dm.getModule();// 调用JNI OnLoaddm.callJNI_OnLoad(emulator);};public String callByAddress(){// args listList<Object> list = new ArrayList<>(5);// jnienvlist.add(vm.getJNIEnv());// jclazzlist.add(0);// str1list.add(vm.addLocalObject(new StringObject(vm, "mobile=13535535353&password=fjfjfjffk&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=eu4acofTmb&response_type=token")));// intlist.add(2);// str2list.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=");}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic 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 listList<Object> list = new ArrayList<>(5);// jnienvlist.add(vm.getJNIEnv());// jclazzlist.add(0);// strlist.add(vm.addLocalObject(new StringObject(vm, str)));// intNumber 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中这样改

public void patch(){UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x16604);byte[] code = new byte[]{(byte) 0x20, (byte) 0xB1};//直接用硬编码改原so的代码:  4FF00109pointer.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寄存器的地址

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中下断看下

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的入参的话,由于后续会拼上环境参数二导致参数太长

debugger.addBreakPoint(module.base+0x5A34, new BreakPointCallback() {@Overridepublic 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加密结束后的结果是多少

    debugger.addBreakPoint(module.base+0x4DCC, new BreakPointCallback() {RegisterContext context = emulator.getContext();@Overridepublic boolean onHit(Emulator<?> emulator, long address) {emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {//onleave@Overridepublic boolean onHit(Emulator<?> emulator, long address) {return false;}});return true;}});

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

image-20240301225229367

所以正确的密文是57b0d60b1873ad7de3aa2f5c1e4b3ff6

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

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

image-20240301230244942

这里的s应该就是state块

debugger.addBreakPoint(module.base+0x4E2A, new BreakPointCallback() {int round = 0;UnidbgPointer statePointer = memory.pointer(0xbffff458);@Overridepublic 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密钥

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

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中下断

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寄存器指向新指针的操作,看下面的代码

debugger.addBreakPoint(module.base+0x5A34, new BreakPointCallback() {@Overridepublic 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

再来看上面的明文块

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

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

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

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

6be9743e 9f528df4 cd9465a9 7cb645a1
7cb645a1 9f528df4 cd9465a9 6be9743e

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

完整算法

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

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
import base64
from Crypto.Cipher import AES
import requests
import hashlib
from Crypto.Util.Padding import unpaddef __pkcs7padding(plaintext):block_size = 16text_length = len(plaintext)bytes_length = len(plaintext.encode('utf-8'))len_plaintext = text_length if (bytes_length == text_length) else bytes_lengthreturn 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_stringkey = 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/267600.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaWeb--JDBC

一&#xff1a;JDBC概述 1.概念 JDBC 就是使用Java语言操作关系型数据库的一套API 全称&#xff1a;( Java DataBase Connectivity ) Java 数据库连接 2.本质 官方&#xff08; sun 公司&#xff09;定义的一套操作所有关系型数据库的规则&#xff0c;即接口&#xff1b;各个…

【C语言】熟悉文件基础知识

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 文件 为了数据持久化保存&#xff0c;使用文件&#xff0c;否则数据存储在内存中&#xff0c;程序退出&#xff0c;内存回收&#xff0c;数据就会丢失。 程序设计中&…

代码随想录算法训练营第46天| 139.单词拆分、背包问题总结

139.单词拆分 完成 思路&#xff1a; 本题可以用背包问题的思路解决&#xff0c;单词是物品&#xff0c;字符串是背包&#xff0c;要求物品能否把背包装满。 dp[j] 字符串长度为j时&#xff0c;能否拆分为一个或多个在字典中出现的单词。 递推公式为&#xff1a;if([i, j] 这个…

“平民化”非结构数据处理

在全球信息产业高速发展的背景下&#xff0c;IDC预测&#xff0c;2018 到 2025 年之间&#xff0c;全球产生的数据量将会从 33 ZB 增长到 175 ZB&#xff0c; 复合增长率27%&#xff0c;其中超过 80%的数据都会是处理难度较大的非结构化数据&#xff0c;如文档、文本、图形、图…

备战蓝桥杯---状态压缩DP基础1之棋盘问题

它只是一种手段&#xff0c;一种直观而高效地表示复杂状态的手段。 我们先来看一道比较基础的&#xff1a; 直接DFS是肯定不行&#xff0c;我们发现对某一行&#xff0c;只要它前面放的位置都一样&#xff0c;那么后面的结果也一样。 因此我们考虑用DP&#xff0c;并且只有0/…

WEB服务器-Tomcat(黑马学习笔记)

简介 服务器概述 服务器硬件 ● 指的也是计算机&#xff0c;只不过服务器要比我们日常使用的计算机大很多。 服务器&#xff0c;也称伺服器。是提供计算服务的设备。由于服务器需要响应服务请求&#xff0c;并进行处理&#xff0c;因此一般来说服务器应具备承担服务并且保障…

flutter简单的MethodChannel通道Demo(引入调用小红书sdk)

flutter端创建MethodChannel类 import package:flutter/services.dart;//MethodChannel const methodChannel const MethodChannel(com.flutter.demo.MethodChannel);class FlutterMethodChannel {/** MethodChannel flutter给原生发信息* 在方法通道上调用方法invokeMethod*…

用冒泡排序模拟C语言中的内置快排函数qsort!

目录 ​编辑 1.回调函数的介绍 2. 回调函数实现转移表 3. 冒泡排序的实现 4. qsort的介绍和使用 5. qsort的模拟实现 6. 完结散花 悟已往之不谏&#xff0c;知来者犹可追 创作不易&#xff0c;宝子们&#xff01;如果这篇文章对你们有帮助的话&#xff0c;别忘了给个免…

《TCP/IP详解 卷一》第9章 广播和组播

目录 9.1 引言 9.2 广播 9.2.1 使用广播地址 9.2.2 发送广播数据报 9.3 组播 9.3.1 将组播IP地址转换为组播MAC地址 9.3.2 例子 9.3.3 发送组播数据报 9.3.4 接收组播数据报 9.3.5 主机地址过滤 9.4 IGMP协议和MLD协议 9.4.1 组成员的IGMP和MLD处理 9.4.2 组播路由…

继电器测试中需要注意的安全事项有哪些?

继电器广泛应用于电气控制系统中的开关元件&#xff0c;其主要功能是在输入信号的控制下实现输出电路的断开或闭合。在继电器测试过程中&#xff0c;为了确保测试的准确性和安全性&#xff0c;需要遵循一定的安全事项。以下是在进行继电器测试时需要注意的安全事项&#xff1a;…

汽车大灯尾灯划痕裂缝破洞破损掉角崩角等如何修复?根本没必要换车灯换总成,使用无痕修UV树脂胶液即可轻松搞定。

TADHE车灯无痕修复专用UV胶是一种经过处理的UV树脂胶&#xff0c;主要成份是改性丙烯酸UV树脂。应用在车灯的专业无痕修复领域。 车灯修复UV树脂有以下优点&#xff1a; 1. 快速修复&#xff1a;此UV树脂是一种用UV光照射在10秒内固化的材料。 2. 高强度&#xff1a;UV树脂固…

LabVIEW流量控制系统

LabVIEW流量控制系统 为响应水下航行体操纵舵翼环量控制技术的试验研究需求&#xff0c;通过LabVIEW开发了一套小量程流量控制系统。该系统能够满足特定流量控制范围及精度要求&#xff0c;展现了其在实验研究中的经济性、可靠性和实用性&#xff0c;具有良好的推广价值。 项…

抖音视频批量下载软件|视频评论采集工具

抖音视频评论采集软件是一款基于C#开发的高效、便捷的工具&#xff0c;旨在为用户提供全面的数据采集和分析服务。用户可以通过关键词搜索抓取视频数据&#xff0c;也可以通过分享链接进行单个视频的抓取和下载&#xff0c;从而轻松获取抖音视频评论数据。 批量视频提取模块&a…

数学建模函数插值与拟合

1.脑图 2.介绍 我们自己找到的函数&#xff0c;在已知点处的函数值和要求的函数在这些点处的函数值相等&#xff0c;这个函数 就叫做未知函数的插值函数&#xff1b; 多项式函数构成的插值函数的集合叫做函数类&#xff1b; 3.拉格朗日插值法 基函数的求法和插值函数的构造…

Java SPI机制详解

Java SPI机制详解 1. 定义接口2. 实现接口4. 创建配置文件5. 加载实现类6.Java SPI机制在MySQL中的使用 总结 SPI 全称为 (Service Provider Interface) &#xff0c;是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制&#xff0c; 当我们有个接口&#xff0c;想在…

性别和年龄的视频实时监测项目

注意&#xff1a;本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站 &#xff08;[www.aideeplearning.cn]&#xff09; 性别和年龄检测 Python 项目 首先介绍性别和年龄检测的高级Python项目中使用的专业术语 什么是计算机视觉&#xff1f; 计算机视觉是使计算机能…

golang学习5,glang的web的restful接口

1. //返回json r.GET("/getJson", controller.GetUserInfo) package mainimport (/*"net/http"*/"gin/src/main/controller""github.com/gin-gonic/gin" )func main() {r : gin.Default()r.GET("/get", func(ctx *…

【K8S类型系统】一文梳理 K8S 各类型概念之间的关系(GVK/GVR/Object/Schema/RestMapper)

参考 k8s 官方文档 https://kubernetes.io/zh-cn/docs/reference/https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/ 重点 Kubernetes源码学习-kubernetes基础数据结构 - 知乎 重点 Kubernetes类型系统 | 李乾坤的博客 重点 k8s源码学习-三大核心数…

Java学习--学生管理系统(残破版)

代码 Main.java import java.util.ArrayList; import java.util.Scanner;public class Main {public static void main(String[] args) {ArrayList<Student> list new ArrayList<>();loop:while (true) {System.out.println("-----欢迎来到阿宝院校学生管理系…

Stable Cascade-ComfyUI中文生图、图生图、多图融合基础工作流分享

最近 ComfyUI对于Stable Cascade的支持越来越好了一些&#xff0c;官方也放出来一些工作流供参考。 这里简单分享几个比较常用的基础工作流。 &#xff08;如果还没有下载模型&#xff0c;可以先阅读上一篇Stable Cascade升级&#xff0c;现在只需要两个模型&#xff09; &a…