本期内容是关于某app模拟登录的,涉及的知识点比较多,有unidbg补环境及辅助还原算法,ida中的md5以及白盒aes,fart脱壳,frida反调试
本章所有样本及资料均上传到了123云盘
llb资料官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘
目录
首先抓包
fart脱壳
加密位置定位
frida反调试
unidbg搭架子
补环境
还原算法
DFA还原白盒AES密钥
小坑
md5
完整算法
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!
总结
最后
首先抓包
看login请求,表单和响应都是大长串,猜测是对称加密算法或者是非对称,对称常见的有des和aes,非对称常见的有rsa.
fart脱壳
正常流程下应该是拖到jadx中反编译一下,但是目标app使用了梆梆企业加固
我换了一部由fart脱壳机定制的pixel 4后成功脱壳,后续我会把脱壳的dex放到网盘里,所以对脱壳不了解的可以略过脱壳这个步骤
寒冰的fart脱壳机github地址:GitHub - hanbinglengyue/FART: ART环境下自动化脱壳方案
把脱下来的dex文件pull到电脑上
对比下脱壳前后的反编译结果
加密位置定位
接下来是定位加密位置了
尝试搜索"sd"
框中的可能性比较大,其他几个类名都是android aliyun google tencent这种系统文件或者第三方厂商的,框中的包含类名以及retrofit框架
这个是目标字段的可能性很大,点进去看看,然后查找用例
右下角框中的有一个decrypt函数,应该是响应的解密逻辑,那上面的应该是加密函数了
点进去然后复制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方式启动即可
换成spwan方式后还是报错了,应该还有检测frida-server
换成葫芦娃形式的试试
成功了,接下来就是发个包看看有没有结果
对比下发现结果差不多就是hook的结果把+改成空格就是sd的值了
接着分析jadx中的函数,checkcode点进去
可以看到目标函数返回null,和hook的结果不一样,并且jadx给出了警告,不知道是脱壳脱的不全还是jadx的问题,后续可以用jeb试试,jeb的反编译能力比jadx强
同时可以看到下面有两个native函数,checkcode,和decheckcode,尝试hook checkcode函数
同样有结果
这两个native函数加载自libencrypt.so
这里我选择32位的so,拖到ida32中搜索java,发现是静态注册(如果是动态注册还可以hook libart.so来找导出函数)
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
通常用于一些需要获取全局上下文或执行一些与应用程序状态相关的操作的场景
补上,这里没什么好说的,孰能生巧
@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像是在获取系统的某个属性
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);}
获取手机序列号的
adb shell getprop ro.serialno
完整的补上
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);}
}
再次运行下,出结果了
但是怎么验证结果是否正确呢,我这里想着是把结果拿去解密看看,代码如下
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);}
}
运行结果如下,好像不太正常
从ida中的decheckcode点进去看看
放回的结果异常,说明走了异常的分支,看样子像是返回的是26行的值,判断!v6的值是否为真,v6来自上面的sub_138AC函数,点进去看看
中间的sub_ED04是一个很大的函数,看这像是检测某种环境
往下滑可以看到像是md5的64轮运算,和结尾解密得到的32位数据对应上了,所以说程序大概率是走了这个分支后直接返回了数据
如果是这样的话就好办了
直接在v6的地方取反就好了,看下此次的汇编代码
是一个条件跳转,CBNZ意思是如果r0寄存器的值不为0就跳到loc_16610处,取反的指令就是CBZ(少了个N not),为0就跳
拿到hex转arm网站上看看指令,20 B9对应的是cbnz r0, #0xc
所以我们需要的就是cbz r0, #0xc
把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就可以了
解密结果也是出来了,可以看到有手机号,密码还有一些设备信息
还原算法
接下来就是unidbg辅助还原算法了
前面在加密函数的位置看到了aes字眼,所有猜测使用了aes加密
还原aes加密需要确认密钥 加密模式(ecb cbc等等) 是否有iv,填充方式,接下来就是漫长的猜测验证再猜测的过程了,利用unidbg可以console debugger的优点,可以非常方便的还原算法
由于加密函数快3000多行,我这里就说大概得关键位置了,如果写的太细内容就太多了
结合着ida静态分析和unidbg动态调试可以猜测2884行应该是进行aes加密的,并且后续进行了base64编码
点进去发现来到了.bss段, .bss段是用来存放程序中未初始化的全局变量的一块内存区域
看下此次的汇编代码
BLX R3 意思是跳转到寄存器 R3
中存储的地址处执行,所以在unidbg中0x163FE下断,看看R3寄存器的地址
debugger.addBreakPoint(module.base+0x163FE);
断在0x163FE处了,前面的0x400是加上了unidbg的基地址,可以看到R3的地址减去基地址也就是后面的地址是0x5a35,再减去thumb的地址加1也就是0x5a34
ida中按G跳转到0x5a34
可以看到aes的具体逻辑就在这里面的几个函数中,最后的WBACRAES128_EncryptCBC貌似是在说white box aes128 cbc模式
如果是这样的话,由于白盒aes11个轮秘钥嵌在程序里,很难直接提取出,需要用dfa(差分故障攻击)获取到第10轮的秘钥,再利用aes_keyschedule这个模块还原出主密钥
WBACRAES128_EncryptCBC点进去
可以看到首先对明文进行了填充,往下滑
WBACRAES_EncryptOneBlock视乎是运算的主体,点进去看看
这里因为我每个地址都下断看了下参数值,实际操作过程需要一步步验证才能走到这
再点进去
这里ida f5出来的看不太懂,看看汇编视图
可以看到结尾跳转到R4寄存器指向的地址,unidbg中下断看下
debugger.addBreakPoint(module.base+0x5836);
所以最终会跳到0x4dcc位置处,为什么要-1上面也说过了,跳到0x4dcc去看看
这里会判断i=9的时候跳出循环,PrepareAESMatrix中Matrix是矩阵的意思,所有这个函数应该是对state数据进行矩阵运算
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可以直接看内存中的值
所以正确的密文是57b0d60b1873ad7de3aa2f5c1e4b3ff6
接下来进行dfa攻击(差分故障攻击),这里需要熟悉aes算法的细节,我这里就不介绍了,感兴趣的去龙哥的知识星球学习一下
故障注入的时机是倒数两次列混淆之间,也就是第八轮以及第九轮运算中两次列混淆之间的时机
这里的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)
拿到结果了
最后用aes_keyschedule把主密钥也就是初始秘钥还原出来了,F6F472F595B511EA9237685B35A8F866
把刚开始的密文拿到CyberChef尝试解一下,因为cbc模式需要iv,所以先用ecb模式,cbc模式比ecb模式多的就是cbc模式需要每个明文分组先和上个分组的密文块进行异或,由于第一组没有上个分组的密文块,所以需要一个初始化向量IV
上面符号WBACRAES128_EncryptCBC说的是cbc加密模式,但这个符号不一定可信,如果使用的是cbc模式,解出来的结果就是明文块和iv异或的值(矩阵异或)
后面全是0,如果是cbc模式下,明文块和iv异或了,由于是矩阵异或,如果填充方式是pkcs7,就意味着iv的后面几位是68656c6c6f填充后的
68656c6c6f0b0b0b0b0b0b0b0b0b0b0b后面几位,也就是0b0b0b0b0b0b0b0b0b0b0b,如果这样的话明文一变填充的数据也变了,可能是01-0f中的任何一个,这样iv的值也不固定,显然在这种情况下就太复杂了.
所以我认为应该是ecb模式下使用了Zero Padding模式,全部填充0直到一个分组长度
为了验证猜想,在InsertCBCPadding函数结束时打印处理过的state块,unidbg中下断
debugger.addBreakPoint(module.base+0x58A0); //m0x40321000
改变输入后发现后面也还是0,也就验证了采用的是Zero Padding模式,并不是常见的pkcs7模式
由于CyberChef中默认是pkcs7填充,所以把模式调成nopadding,这样解密出来的结果就是未填充的一个分组长度了,也验证了上面的Zero Padding模式
这也就是说上面的cbc模式也是错误的,而是ecb模式
尝试加密一下明文和密文对比下
正常的密文是Mhub8kSp2n38SHF4COj57zjesFrzCIB2JiH76iCwZZffL3Y4+1/fq1uEDKKWe4yAwiacSVxXNSq1sWN5TwtfHaVgxpOREVGT2+qZEZFkvjP1GaxPCPP2jwuy4x3GvPgHl2NhG2kpsfcXHHQK9HJ5iBdtO44QdDO0vtgqU9MGGb+3q+HJwKlgfWJZj24t8HOSypJNigdCXbUEC6HGEhZhAhH9QOWkbD6iDkO4mpB0xjvRurFugh+t9P3AeXJeHdhF+MnCXXj3BGlfUgi2qCvoWxYajx2sUcZkXpNbAFbj7VaAlG2ytQnO/L0aZr+SlzTxb90PoLU2VBp98GXNt0ozObSaCwO41UlmZPcKZrr9sxf32nwmoEmUwoTXe14aks2nj72zo5kz8GXyfzh2f6mddZQ==
对比一下,除了正常密文前面多了个M,以及开头有一段相同的,后面都不一样,于是我尝试能不能解密一下
可以看到只解密出了前16个字节,到这里我就感觉有点不太对了,一般来说开发人员不会乱写,如果后续他维护起来也比较麻烦,除非是那种故意写出来迷惑逆向人员的,但前面的aes算法他又暴露了出来,所以我感觉上面的推论可能有点问题,也就是说可能真的是cbc模式.如果是ecb模式下由于分组加密,每个分组单独加密,互不关联,能解第一组的话按理后面的也能解.但如果是cbc模式下每个明文分组先和上个分组的密文块进行异或,直接放到ecb模式下肯定解不出来,那为什么可以解出来第一组呢?我们先看加密模式下,第一个分组下明文和iv异或后进行后续加密,如果只解密第一组则不需要在cbc模式下,ecb就可以,并且解密出来的结果是明文和iv异或的结果,也就是说明文和iv异或后还是明文,a异或b得到a,只有一种情况,b全为0,也就是说iv是00000000000000000000000000000000
看看结果完全正常,也就是说上面的推论有问题,我们再来仔细看看上面的推论
我们否定了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轮
InsertCBCPadding执行后
末尾填充了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
说明上面的推论都没有错,只不过是修改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轮运算
这里我先对明文加密了一下,但是不确定是否有盐值
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(两个杠)
最后
微信公众号:爬虫爬呀爬
如果你觉得这篇文章对你有帮助,不妨请作者喝一杯咖啡吧!