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.mzph.cn/news/715152.shtml

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

相关文章

win11安装nodejs

一、下载安装包 链接: https://pan.baidu.com/s/1_df8s1UlgNNaewWrWgI59A?pwdpsjm 提取码: psjm 二、安装步骤 1.双击安装包 2.Next> 3.勾选之后&#xff0c;Next> 4.点击Change&#xff0c;选择你要安装的路径&#xff0c;然后Next> 5.点击Install安装 二、…

学生云服务器腾讯云_腾讯云学生学生_腾讯云学生云主机

2024年腾讯云学生服务器优惠活动「云校园」&#xff0c;学生服务器优惠价格&#xff1a;轻量应用服务器2核2G学生价30元3个月、58元6个月、112元一年&#xff0c;轻量应用服务器4核8G配置191.1元3个月、352.8元6个月、646.8元一年&#xff0c;CVM云服务器2核4G配置842.4元一年&…

基于扩散模型的图像编辑:首篇综述

AIGC 大模型最火热的任务之一——基于 Diffusion Model 的图像编辑(editing)领域的首篇综述。长达 26 页&#xff0c;涵盖 297 篇文献&#xff01;本文全面研究图像编辑前沿方法&#xff0c;并根据技术路线精炼地划分为 3 个大类、14 个子类&#xff0c;通过表格列明每个方法的…

LeetCode 热题 100 | 图论(一)

目录 1 200. 岛屿数量 2 994. 腐烂的橘子 2.1 智障遍历法 2.2 仿层序遍历法 菜鸟做题&#xff0c;语言是 C 1 200. 岛屿数量 解题思路&#xff1a; 遍历二维数组&#xff0c;寻找 “1”&#xff08;若找到则岛屿数量 1&#xff09;寻找与当前 “1” 直接或间接连接在…

Java输入输出流详细解析

Java I/O&#xff08;输入/输出&#xff09;主要被用来处理输入数据和输出结果。 在Java中&#xff0c;输入/输出操作被当作流&#xff08;Stream&#xff09;进行处理。流是一个连续的数据流入或数据流出的通道。流操作在Java中主要可以分为两种类型&#xff1a;字节流和字符…

基于ssm疫情期间高校防控系统+vue论文

摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;学生信息因为其管理内容繁杂&#xff0c;管理数量繁多导致手工进行处理不能满足广大…

‘conda‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件

如果你在运行 conda 命令时收到了 ‘conda’ 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 的错误消息&#xff0c;这可能意味着 Anaconda 并没有正确地添加到你的系统路径中。 1.你可以尝试手动添加 Anaconda 到系统路径中。以下是在 Windows 系统上添加…

19.2 DeepMetricFi:基于深度度量学习改进Wi-Fi指纹定位

P. Chen and S. Zhang, "DeepMetricFi: Improving Wi-Fi Fingerprinting Localization by Deep Metric Learning," in IEEE Internet of Things Journal, vol. 11, no. 4, pp. 6961-6971, 15 Feb.15, 2024, doi: 10.1109/JIOT.2023.3315289. 摘要 Wi-Fi RSSI指纹定位…

调用“每日诗词”在你的页面添加一句诗

概述 前几天浏览网站的时候看到页面上有句诗&#xff0c;打开调试看了下调用的是“每日诗词”的SDK。本文基于此SDK实现你的页面添加一句诗。 实现效果 实现 1. 引入SDK <script src"https://sdk.jinrishici.com/v2/browser/jinrishici.js" charset"utf-…

mysql服务治理

一、性能监控指标和解决方案 1.QPS 一台 MySQL 数据库&#xff0c;大致处理能力的极限是&#xff0c;每秒一万条左右的简单 SQL&#xff0c;这里的“简单 SQL”&#xff0c;指的是类似于主键查询这种不需要遍历很多条记录的 SQL。 根据服务器的配置高低&#xff0c;可能低端…

【BUUCTF web】通关 2.0

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …

2024年2月国内如何快速注册OnlyFans最新小白教学

前言 onlyface软件是一个创立于2016年的订阅式社交媒体平台&#xff0c;创作者可以在自己的账号发布原创的照片或视频&#xff0c;并将其设置成付费模式&#xff0c;若用户想查看则需要每月交费订阅。 需要注意的是&#xff0c;网络上可能存在非法或不道德的应用程序&#xff…

获取当前数据 上下移动

点击按钮 上下移动 当前数据 代码 // 出国境管理 登记备案人员列表 <template><a-row><a-col span"24"><a-card :class"style[a-table-wrapper]"><!-- 出国境 登记备案人员列表 --><a-table:rowKey"records >…

淘宝开放平台获取商家订单数据API接口接入流程

taobao.custom 自定义API操作 接口概述&#xff1a;通过此API可以调用淘宝开放平台的API&#xff0c;通过技术对接&#xff0c;您可以轻松实现无账号调用官方接口。进入测试&#xff01; 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&…

通过修改host文件来访问GitHub

前言&#xff1a; 由于国内环境的原因&#xff0c;导致我们无法流畅的访问GitHub&#xff0c;。 但是我们可以采取修改host文件来实现流畅访问。 缺点&#xff1a;需要不定时的刷新修改。 操作流程 一、查询IP地址 以下地址可以查询ip地址 http://ip.tool.chinaz.com/ htt…

JDK时间

Date 全世界的时间&#xff0c;有一个统一的计算标准。 世界标准时间&#xff1a;格林尼治时间/格林威治时间简称GMT&#xff0c;目前时间标准时间已经替换为&#xff1a;原子钟。 中国标准时间&#xff1a;世界时间8 时间换算单位&#xff1a; 一秒等于一千毫秒 一毫秒等于一…

CDC作业历史记录无法删除问题

背景 数据库开启CDC功能后&#xff0c;每天会生成大量的历史记录&#xff0c;即使达到参数“每个作业的最大历史记录“的阈值后也不会被删除&#xff0c;导致其它作业的历史记录被删除&#xff0c;无法查看以前的执行情况&#xff0c;非常不方便。 现象 数据库开启CDC后会创建…

【MATLAB源码-第147期】基于matlab的QPSK调制解调在AWGN信道,瑞利信道,莱斯信道理论与实际误码率对比仿真。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 四相位移键控&#xff08;QPSK&#xff0c;Quadrature Phase Shift Keying&#xff09;是一种重要的数字调制技术&#xff0c;它通过改变信号的相位来传输数据。与其他调制技术相比&#xff0c;QPSK在相同的带宽条件下能够传…

Linux命名管道

Linux匿名管道-CSDN博客 目录 1.原理 2.接口实现 3.模拟日志 Linux匿名管道-CSDN博客 这上面叫的是匿名管道&#xff0c;不要将两者搞混&#xff0c;匿名管道说的是两个有血缘关系的进程相互通信&#xff0c;但是命名管道就是两个没有关系的管道相互通信。 1.原理 和匿名…

编译链接实战(25)ThreadSanitizer检测线程安全

ThreadSanitizer&#xff08;又称为TSan&#xff09;是一个用于C/C的数据竞争检测器。在并发系统中&#xff0c;数据竞争是最常见且最难调试的错误类型之一。当两个线程并发访问同一个变量&#xff0c;并且至少有一个访问是写操作时&#xff0c;就会发生数据竞争。C11标准正式将…