DFA攻击白盒AES
关于上一篇的数字加固的分析,图片没有处理好,之后会更新加固(中)
通过网盘分享的文件:llb.apk 链接: https://pan.baidu.com/s/1YPf2yyC-nFvxRKsweiNVGg?pwd=vbvm 提取码: vbvm
--来自百度网盘超级会员v4的分享
包名:com.cloudy.linglingbang
加固:梆梆
这里可以直接用FART来脱壳
frida检测:
应该是对于frida-server的特征检测,这里可以换上葫芦侠的frida就绕过了
这个APP在登录的过程中是没有抓包检测的,所以是可以直接去得到抓包结果的
user:12345678900 password:password777
请求包
POST /llb/oauth/llb/ucenter/login HTTP/1.1
channel: yingyongbao
platformNo: Android
appVersionCode: 1481
version: V8.0.14
imei: a-4a674abf3d88252a
imsi: unknown
deviceModel: Pixel XL
deviceBrand: google
deviceType: Android
accessChannel: 1
oauthConsumerKey: 2019041810222516127
timestamp: 1734844726087
nonce: ypjWhicBQp
signature: b9114db2915d2611c075e8dcc85d1108
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Content-Length: 412
Host: api.00bang.cn
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/4.9.0
sd=MLWVhswVAEjWdmSR3ypZ1P1WDq5OGNPucy60sGAPwAGoDF3K0RyeumBVEeGpOtHv7865YfI9iDCfXcUEvN96RBv9FlAqULcndldQZYML0lmq1TORKsx50wzN21QfhMzRmI8FY5Jo7XOhpEXd7Yp6+ejw5L+QDI2qUfM/Xmay1TCEa7El9Gfq/uyfgxkHAuM7D4bz5YeAYxQnLbXjdyJst6bUOM1V7YePwSUmi0qQoDSYmTLK1n9d0RHzhqvK/qOrpBDxSho4c+di9p8yRar5pnQobZ5ErVnR5uUGWgh7Ap44oeKpLudkD9gK+O6E8gtD1R6/besf8zXt+lxE26QOfQIVOS/DBVobGJy/ReKJOQE6HC5WLQiwRqXY13bTdDoNJ3HYmatUVnQNANbIAS1tinA==
响应包
HTTP/1.1 200 OK
Date: Sun, 22 Dec 2024 05:18:47 GMT
Content-Type: application/json; charset=UTF-8
Content-Length: 458
Connection: keep-alive
Set-Cookie: acw_tc=ac11000117348447275828166efe3b6a04af5c6ec8ae3fd340bf080fd9a293;path=/;HttpOnly;Max-Age=1800
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Strict-Transport-Security: max-age=15724800; includeSubDomains
{"sd":"Mat4pSIuqCrzFWQE7ITVRYMZD4siD2ApWWETEZxDdSgG1F9pvAraW67KQ5iPdwYS3y6S1ff9X7DZ4p+AP8z9cxojG7xK/g+bIUWDPtAkmWbURumT2FRMiNCK8sdvQDlp7uuxHlKzUR7nmlktwx2JLfEihyGNimqumr7+lOqoAhJfgzk4M7r1p/r327SM/sZbRj9QsNooK4v9bgUAVcCoblD1dLtYIXA700ZodZ4cmhjmZWArG/cWhd2dSht0JZgN7vAHm8nIFQg5svnIx18sNu0QAk3ChsmWSx2Qk6RvDs25S0dLrTV9BF3jNuv1ucAz5OaOH82/ZeJ/t1Qaw0GE929+o8BpYL0olO/SFhQ8XH6jA8W/Y2cpd07tkiYAAYfB5lxkRUhWRCKMz3JJjcU43Vgew7Vy0Qc6mHpzbPjecOvv0ltkorGA0/TcEfMcKjjsb"}
这么长的请求和响应,肯定不是摘要啥的,多半就是AES,DES之类的了
我在这里通过通杀算法去查看了一些相关的请求包的数据,但是sd的信息是没有找到的,唯独只有signature这个签名有信息
复现 signature
signature: b9114db2915d2611c075e8dcc85d1108
这里可以看到的是,signature是直接的MD5的结果,然后根据堆栈去看看这个Signature是怎么生成的
可以看到的是这里的MD5之前的数据,其实就是固定值+时间戳+随机数+一个addHeader的结果
MD5 update data Utf8: 20190418102225161271734844726087ypjWhicBQpc5ad2a4290faa3df39683865c2e10310a14f0be589630ff5b16d35de3b0b7190
经过多次的抓包发现,其实最后的addHeader也是一个固定值,所以这里的signature是很好得到的
复现 sd值
由于我们的请求包里有sd变量,这里我们去搜索"sd"值,定位到了CheckCodeUtils类
查看哪里有对应的值的调用
看到这里append了"sd"以及encrypt,那么我们去HOOK一下这个值,来看看这个值的传入是和返回值是什么?
function HOOK_encryptfunction(){
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;
};
})
}
CheckCodeUtils.encrypt is called: str=mobile=12345678900&password=password777&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=F5bZmBhmPJ&response_type=token, i=2
CheckCodeUtils.encrypt result=MLWVhswVAEjWdmSR3ypZ1P1WDq5OGNPucy60sGAPwAGoDF3K0RyeumBVEeGpOtHv7865YfI9iDCfXcUEvN96RBv9FlAqULcndldQZYML0lmq1TORKsx50wzN21QfhMzRmI8FY5Jo7XOhpEXd7Yp6+eu5g+46JW+icyQKS5lxXghApbG7B7VuAJOZK3zXGkh0cOSZ7cn8cVi1BL0xKTerAHZcQ7KB8sr7dGtDrdCnqyzeXFqCkNVP3KXD9DwULBbokyA/BTDauO8kXjkyso6w6sKt8HkaBTk3fUOvGWjv/ZqvbBaB/YFabiBjbG7PsF7zV/oLlgEVd/2QJYPKAjmpuBtcvjUZsygcNVB/HEKLytAS6FQ3NyaccAHI6WEPCLiblgu/HZaVgjcCjEr7TdziS3Q==
发现这里传入的就是我们的username和password,以及一些相关的参数
再往里走,发现是native函数
于是我们再去HOOK一下这个native函数看看
!
function Hook_checkcode(){
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(`start [Method] CheckCodeUtil.checkcode is called: str=${str}, i=${i}, str2=${str2}`);
let result = this["checkcode"](str, i, str2);
console.log(`end [Method] CheckCodeUtil.checkcode result=${result}`);
return result;
};
}
CheckCodeUtils.encrypt is called: str=mobile=12345678900&password=password777&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=F5bZmBhmPJ&response_type=token, i=2
start [Method] CheckCodeUtil.checkcode is called: str=mobile=12345678900&password=password777&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=F5bZmBhmPJ&response_type=token, i=2, str2=1734847397441
end [Method] CheckCodeUtil.checkcode result=MLWVhswVAEjWdmSR3ypZ1P1WDq5OGNPucy60sGAPwAGoDF3K0RyeumBVEeGpOtHv7865YfI9iDCfXcUEvN96RBv9FlAqULcndldQZYML0lmq1TORKsx50wzN21QfhMzRmI8FY5Jo7XOhpEXd7Yp6+eu5g+46JW+icyQKS5lxXghApbG7B7VuAJOZK3zXGkh0cOSZ7cn8cVi1BL0xKTerAHZcQ7KB8sr7dGtDrdCnqyzeXFqCkNVP3KXD9DwULBbokyA/BTDauO8kXjkyso6w6sKt8HkaBTk3fUOvGWjv/ZqvbBaB/YFabiBjbG7PsF7zV/oLlgEVd/2QJYPKAjmpuBtcvjUZsygcNVB/HEKLytAS6FQ3NyaccAHI6WEPCLiblgu/HZaVgjcCjEr7TdziS3Q==
CheckCodeUtils.encrypt result=MLWVhswVAEjWdmSR3ypZ1P1WDq5OGNPucy60sGAPwAGoDF3K0RyeumBVEeGpOtHv7865YfI9iDCfXcUEvN96RBv9FlAqULcndldQZYML0lmq1TORKsx50wzN21QfhMzRmI8FY5Jo7XOhpEXd7Yp6+eu5g+46JW+icyQKS5lxXghApbG7B7VuAJOZK3zXGkh0cOSZ7cn8cVi1BL0xKTerAHZcQ7KB8sr7dGtDrdCnqyzeXFqCkNVP3KXD9DwULBbokyA/BTDauO8kXjkyso6w6sKt8HkaBTk3fUOvGWjv/ZqvbBaB/YFabiBjbG7PsF7zV/oLlgEVd/2QJYPKAjmpuBtcvjUZsygcNVB/HEKLytAS6FQ3NyaccAHI6WEPCLiblgu/HZaVgjcCjEr7TdziS3Q==
发现这里的CheckCodeUtil.checkcode的结果就是请求包的结果
POST /llb/oauth/llb/ucenter/login HTTP/1.1
channel: yingyongbao
platformNo: Android
appVersionCode: 1481
version: V8.0.14
imei: a-4a674abf3d88252a
imsi: unknown
deviceModel: Pixel XL
deviceBrand: google
deviceType: Android
accessChannel: 1
oauthConsumerKey: 2019041810222516127
timestamp: 1734847438319
nonce: zT75O2UArt
signature: 808ab5ac781f83b6439b1e0f3c218d51
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Content-Length: 412
Host: api.00bang.cn
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/4.9.0
sd=MLWVhswVAEjWdmSR3ypZ1P1WDq5OGNPucy60sGAPwAGoDF3K0RyeumBVEeGpOtHv7865YfI9iDCfXcUEvN96RBv9FlAqULcndldQZYML0lmq1TORKsx50wzN21QfhMzRmI8FY5Jo7XOhpEXd7Yp6+eu5g+46JW+icyQKS5lxXghApbG7B7VuAJOZK3zXGkh0cOSZ7cn8cVi1BL0xKTerAHZcQ7KB8sr7dGtDrdCnqyzeXFqCkNVP3KXD9DwULBbokyA/BTDauO8kXjkyso6w6sKt8HkaBTk3fUOvGWjv/ZqvbBaB/YFabiBjbG7PsF7zV/oLlgEVd/2QJYPKAjmpuBtcvjUZsygcNVB/HEKLytAS6FQ3NyaccAHI6WEPCLiblgu/HZaVgjcCjEr7TdziS3Q==
那么我们就去看看这个SO函数
全是JNI函数,我们尝试用unidbg来调用这个so来加载看看,结果所以我们要去补环境
uidbg
主动调用会报错,这样就要去补环境来启动Java_com_bangcle_comapiprotect_CheckCodeUtil_checkcode
同时这里是32位程序,记得HOOK的时候地址+1
补字段
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature){
case "android/os/Build->MODEL:Ljava/lang/String;":{
return new StringObject(vm, "Pixel 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);
}
补callObjectMethod
@Override
public 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);
}
补callStaticObjectMethod
这里的"ro.serialno":序列号,随便填也行
adb shell getprop ro.serialno
@Override
public 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, "HT7650200010");
}
}
}
return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}
总体代码:(这里是如烟大佬的代码,我的之前因为那个字段补环境的地方已经改了很多了)
package com.linglingbang;
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("E:\\android\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\linglingbang\\llb.apk"));
// 设置JNI
vm.setJni(this);
// 打印日志
vm.setVerbose(true);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(new File("E:\\android\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\linglingbang\\libencrypt.so"), true);
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
// 调用JNI OnLoad
dm.callJNI_OnLoad(emulator);
};
public String callByAddress(){
// args list
List<Object> list = new ArrayList<>(5);
// jnienv
list.add(vm.getJNIEnv());
// jclazz
list.add(0);
// str1
list.add(vm.addLocalObject(new StringObject(vm, "mobile=12345678900&password=password777&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=F5bZmBhmPJ&response_type=token")));
// int
list.add(2);
// str2
list.add(vm.addLocalObject(new StringObject(vm, "1734847397441")));
Number number = module.callFunction(emulator, 0x13A19, list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
System.out.println(" CheckCodeUtils.encrypt result encrypt ="+result);
return result;
};
public static void main(String[] args) {
demo2 llb = new demo2();
llb.callByAddress();
}
@Override
public 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);
}
@Override
public 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);
}
@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature){
case "android/os/Build->MODEL:Ljava/lang/String;":{
return new StringObject(vm, "Pixel 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);
}
}
这里是算法通杀HOOK得到的结果
再来看看我们unidbg得到的结果
可以看到的是,结果是一样的,那么也就是说这里native函数完成之后的返回值就是这个了
如画这里同时去调用了Java_com_bangcle_comapiprotect_CheckCodeUtil_decheckcode,所以我也去看了看调用
public void call_decrypto(){
ArrayList<Object> list = new ArrayList<>(5);
String str = "MLWVhswVAEjWdmSR3ypZ1P1WDq5OGNPucy60sGAPwAGoDF3K0RyeumBVEeGpOtHv7865YfI9iDCfXcUEvN96RBv9FlAqULcndldQZYML0lmq1TORKsx50wzN21QfhMzRmI8FY5Jo7XOhpEXd7Yp6+eu5g+46JW+icyQKS5lxXghApbG7B7VuAJOZK3zXGkh0cOSZ7cn8cVi1BL0xKTerAHYLxENn4IHSAkILI6kZfeRuEjaSrpUA6KgEkR96849oPfbphCeHESmH12gIqnuJoTTXxjwDfKMy0kplSVK/GJwid6z6fkxUwUGP4tw43TkyqE+XiWflyamfvLKNOlycj9gKvOjmH5swX89TeaNCfk9JG93uHZ7zT2XBx8bFmRy5zazj2hmSD5+TCYIA/eh7iMFzdguMrfygLLpt7MwDG6xY=";
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(new StringObject(vm,str)));
Number number = module.callFunction(emulator, 0x0165E1, list.toArray());
String result = vm.getObject(number.intValue()).getValue().toString();
System.out.println(" CheckCodeUtils.call_decrypto result encrypt ="+result);
}
按道理来说我们加密传入的是
mobile=12345678900&password=password777&client_id=2019041810222516127&client_secret=c5ad2a4290faa3df39683865c2e10310&state=F5bZmBhmPJ&response_type=token&ostype=ios&imei=9B131FFBA001Y5&mac=02:00:00:00:00:00&model=Pixel XL&sdk=29&serviceTime=1734847397441&mod=Google&checkcode=0c30ff815f9539478b978a39c9c95d91
那我们解密得到的就应该也是这个才对,但是这里的值一看就大概率是MD5之后的值,怎么解密之后得到的是MD5呢?
我们可以看到这里也会返回值,那么大概率就是返回v5的值了,v5又是加入了sub_138AC函数的
同时在这函数里面又调用了其他函数,在这里我们找到了类似于MD5的64轮加密的算法
所以这里我们patch一些代码,让程序不走MD5的分支,看看能够得到什么
结果就直接解密了
复现加密算法
从现在开始我们就来复现加密算法了,直接去Java_com_bangcle_comapiprotect_CheckCodeUtil_checkcode看加密算法了
这里的代码一共有大概2900行的代码,其中有很多都是MD5加密的过程(大概从1300行-2700行之间全是MD5算法)
在2787行的位置出现了AES的加密函数,但是里面是bss段的信息
我们通过交叉引用来看看能不能得到对应位置的代码
其实能够看都这里的是有对应位置的加密函数的,大概率是aes_encrypt1
这里我们也可以通过汇编去看看代码,发现这么其实是跳转到的R3的对应寄存器的位置的值,那么我们就可以去HOOK对应位置得到对应的R3的值
确定了就是在aes_encrypt1的位置,然后我们就发现了WBACRAES128_EncryptCBC的字眼,大概率就是白盒CBC模式的AES128了
往里走可以看到对应的填充的函数以及,加密一个块的函数
来到这里的CWAESCipher::WBACRAES_EncryptOneBlock
发现是一个看不了的函数,有点类似于SMC,我们也去看汇编
]
发现跳转是R4的位置,我们添加断点,查看对应寄存器R4的值
public void HOOK_unline()
{
attach.addBreakPoint(module.base+0x163FE);//得到aes_encrypt1的地址005a35-1
attach.addBreakPoint(module.base+0x0005836);//得到WBACRAES_EncryptOneBlock的地址04dcd-1
}
在CWAESCipher_Auth::WBACRAES_EncryptOneBlock函数中有明显的表面AES轮数的位置
在九轮之后就不会进行列混淆了,而我们要得到密钥就是通过DFA差分攻击来实现得到密钥,https://bbs.kanxue.com/thread-280335.htm这里有一篇对于AES的DFA差分攻击很详细的帖子。
DFA
在AES的state的正常执行流中,替换错误的一个字节的数据,导致处理错误。其中
-
如果故障早于倒数第二个列混淆,那么会影响结果中的十六个字节
-
如果故障发生在倒数两个列混淆之间,那么会影响结果中的四个字节
-
如果故障晚于最后一个列混淆,那么会影响结果中的一个字节
这里说的倒数两个列混淆也就是在 第八个和第九个循环之间的事情。
其中主要构成能够进行差分攻击DFA的主要原因是在固定了输出差分,也就是我们原本的加密结果和故障加密结果之间的差值(异或值),而导致我们可以去约束输入差分的范围
同时由于输出差分的固定,而且Y0和Z又是0-256之间的值,通过这样的算式我们同样实现了约束Y0,进而约束了K10,0的范围
在state的错误位置被修改的时候,也就是导致结果不同位置被故障之后,但是K10,0也就是第十个扩展密钥的值却是不变的,通过多次对于State故障位置的改变,一直去约束K10,0的范围,直到可以实现解密K10,0的值,同理便实现了K10的解密,进而得到密钥,这就是DFA的原理。通过state的故障位置的改变,增大约束范围,实现值的确定,进而得到真正的密钥结果。
这里为了使得伪造输入和查看填充方式,就跟着如画一样,把输入的传值修改了,我们选择的位置是在int __fastcall aes_encrypt1的位置
attach.addBreakPoint(module.base + 0x5A34, new BreakPointCallback() { //修改输入为hello
@Override
public boolean onHit(Emulator<?> emulator, long address) {
String fackInput = "hello";
// String fackInput = "helloworldDDDDDDD";
MemoryBlock fackInputBlock = emulator.getMemory().malloc(fackInput.length(), true);
fackInputBlock.getPointer().write(fackInput.getBytes(StandardCharsets.UTF_8));
emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0,fackInputBlock.getPointer().peer);
return true;
}
});
先看一下在不进行DFA之前的输入为hello的加密结果
然后我们开始进行DFA的差分攻击,随机的在state的16个字节的位置去随机替换一个值,注意这里的时机是在第八轮的列混淆之后,第九轮之前
attach.addBreakPoint(module.base+0x004E1A, new BreakPointCallback() { //star to encrypto 故障
int round = 0;
final UnidbgPointer statePointer = memory.pointer(0xE4FFF458L);
@Override
public 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 就不会在控制台断住
}
});
public static int randInt(int min, int max) {
Random rand = new Random();
return rand.nextInt((max - min) + 1) + min; // min 到 max 之间的随机数
}
这里的选择的state的替换的地址是要自己去找的
attach.addBreakPoint(module.base+0x5888, new BreakPointCallback() {
RegisterContext context = emulator.getContext();
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Inspector.inspect("CWAESCipher::WBACRAES128_EncryptCBC \n0x5888 args[1] painText addrs : ", (int) context.getPointerArg(1).peer);
Inspector.inspect("0x4DCC args[2] encrypto date addrs : ", (int) context.getPointerArg(2).peer);
emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
//onleave
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Inspector.inspect("加密函数结束 Onleave ",0x000000 );
return false;
}
});
return false;
}
});
是在CWAESCipher::WBACRAES128_EncryptCBC的三个参数的指针地址的位置。这样就可以在找到对应位置的地方进行差分攻击了
就这样的修改state的一个字节,然后约束范围,得到密钥
import phoenixAES
with open('tracefile', 'wb') as t: # 第一行是正确密文 后面是故障密文
t.write("""57b0d60b1873ad7de3aa2f5c1e4b3ff6
57b0630b18d8ad7de8aa2f5c1e4b3f73
57b0d65b1873e07de3752f5c804b3ff6
578bd60b2d73ad7de3aa2fa51e4b47f6
adb0d60b1873ad50e3aa225c1e983ff6
57e4d60b0173ad7de3aa2f061e4b17f6
17b0d60b1873ad02e3aa235c1efb3ff6
57b0460b185aad7d76aa2f5c1e4b3f16
5704d60bfd73ad7de3aa2fc21e4b4ef6
57b0870b186fad7d3baa2f5c1e4b3fd7
c3b0d60b1873add4e3aa745c1e103ff6
57b0d6531873af7de3302f5c964b3ff6
""".encode('utf8'))
phoenixAES.crack_file('tracefile', [], True, False, 3)
这里的数据我重新进行了修改,把自己找到数据填上去了,结果也是一样的
8A6E30D74045AE83634D6ECDE1516CA1
通过K10得到K0 https://github.com/SideChannelMarvels/Stark 执行获得exe
这样就可以通过k10得到k0了,也就是我们的密钥了(原理在之前的AES的文章里面也有提到)
F6F472F595B511EA9237685B35A8F866
细节推理:
首先是算法是什么模式,白盒的AES,CBC模式的填充方式大概率是pkcs7。那么我们先尝试去实现一下我们请求数据的加密过程看看,但是这里的CBC模式是不行的,因为我们还没有IV。但是不过是我们假设输入的hello和原始数据都不能被解密,那我们应该从哪里入手,这里站着了巨人的肩膀上才能够看到使用ECB模式也得到输入
但是为什么能够得到hello的字眼的结果,按照正常来说,假如是CBC模式,明文是要先进行IV异或之后再进行AES加密,之后的第一个块去异或下一个明文,也就是说按照道理,我们得到的也应该是明文和IV的异或值,正常来看,我们只能当成ECB模式来看了,那么我们就要去看看填充方式是什么了,因为EBC模式有No padding和PKCS7。
在padding之后设置断点就好了,不过先可以去得到a2的内存地址
attach.addBreakPoint(module.base+0x058A0);
attach.addBreakPoint(module.base+0x4DCC, new BreakPointCallback() {
RegisterContext context = emulator.getContext();
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Inspector.inspect("实际加密函数CWAESCipher_Auth::WBACRAES_EncryptOneBlock \n0x4DCC args[1] painText addrs : ", (int) context.getPointerArg(1).peer);
Inspector.inspect("0x4DCC args[2] encrypto date addrs : ", (int) context.getPointerArg(2).peer);
emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
//onleave
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Inspector.inspect("加密函数结束 Onleave ",0x000000 );
return false;
}
});
return true;
}
});
这里直接是填充的00 00 00,那么可以认为是No padding了
那我们就去加密
MLWVhswVAEjWdmSR3ypZ1P1WDq5OGNPucy60sGAPwAGoDF3K0RyeumBVEeGpOtHv7865YfI9iDCfXcUEvN96RBv9FlAqULcndldQZYML0lmq1TORKsx50wzN21QfhMzRmI8FY5Jo7XOhpEXd7Yp6+eu5g+46JW+icyQKS5lxXghApbG7B7VuAJOZK3zXGkh0cOSZ7cn8cVi1BL0xKTerAHYLxENn4IHSAkILI6kZfeRuEjaSrpUA6KgEkR96849oPfbphCeHESmH12gIqnuJoTTXxjwDfKMy0kplSVK/GJwid6z6fkxUwUGP4tw43TkyqE+XiWflyamfvLKNOlycj9gKvOjmH5swX89TeaNCfk9JG93uHZ7zT2XBx8bFmRy5zazj2hmSD5+TCYIA/eh7iMFzdguMrfygLLpt7MwDG6xY=
按道理来这里的结果应该是这样的,但是没有第一个'M'字符,这个可以理解,在ida中可以看到对于前缀不同的地方做了不同的处理,其中就有'M'的分支,但是 'LWVhswVAEjWdmSR3ypZ1P' 只有这里对上了 ,那我们只能去实现解密了看看哪里不一样了
只能解密前面的数据,不能往后了。那是哪里错了,能解密肯定AES的key是对的,base64也没错,那只能是模式出错了,难道还是CBC吗,但是为什么是CBC解密又可以在没有IV的情况下直接把明文给解出来???
IV是异或操作,假如真的考虑是CBC,那么只能是异或之后的结果还是明文了,那只能IV是16字节的0了。所以我们去看看
厉害的,IV是16字节的0。
这里就可以看到了,前面就差了一个"M"了
至于最后,为什么会因为这里CBC模型的AES,而且默认的填充方式是PKCS7,但是结果看到的却是NO padding的0填充,在如画的文章里面说是因为修改r0为指向新字符串的新指针有很大关系,导致的大概是指向地址不同了,并且使用了nopadding对于数据加密,也会得到hello的结果是最终的结果。
这里我们恢复了之前传入的参数,看了看再padding之后的内存存储的数据是什么
其实是可以看到这里的数据是有304个字节的,其中在最后一个16字节的块中是填充了 0c 的字节的,其实能够看到的是这里就是PKCS7的填充方式。这里的AES的KEY和IV都有了,算法已经是可以复现了。本来想着不贴如画佬的复现代码了,但是还是贴上去了。
import base64
from Crypto.Cipher import AES
import requests
import hashlib
from Crypto.Util.Padding import unpad
def __pkcs7padding(plaintext):
block_size = 16
text_length = len(plaintext)
bytes_length = len(plaintext.encode('utf-8'))
len_plaintext = text_length if (bytes_length == text_length) else bytes_length
return 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_string
key = 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()
本文章中所有内容仅供学习交流使用,不用于其他任何目的,擅自使用本文讲解的技术而导致的任何意外,与作者不负责