-
-
[分享]某车联网五##光DFA攻击白盒AES协议算法分析
-
发表于: 2024-12-26 13:14 2139
-
DFA攻击白盒AES
通过网盘分享的文件:llb.apk
链接: https://pan.baidu.com/s/1YPf2yyC-nFvxRKsweiNVGg?pwd=vbvm 提取码: vbvm
--来自百度网盘超级会员v4的分享
包名:com.cloudy.linglingbang , 加固:梆梆
![]
这里可以直接用FART来脱壳
应该是对于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一下这个值,来看看这个值的传入是和返回值是什么?
1 2 3 4 5 6 7 8 9 10 11 | 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函数看看
1 2 3 4 5 6 7 8 9 | 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
补字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @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); } |
总体代码:(这里是如烟大佬的代码,我的之前因为那个字段补环境的地方已经改了很多了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | 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,所以我也去看了看调用
1 2 3 4 5 6 7 8 9 10 | 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); } |
按道理来说我们加密传入的是
1 | 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的值
1 2 3 4 5 | 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的位置
1 2 3 4 5 6 7 8 9 10 11 | 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个字节的位置去随机替换一个值,注意这里的时机是在第八轮的列混淆之后,第九轮之前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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的替换的地址是要自己去找的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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的一个字节,然后约束范围,得到密钥
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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的内存地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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都有了,算法已经是可以复现了。本来想着不贴如画佬的复现代码了,但是还是贴上去了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | 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() |
本文章中所有内容仅供学习交流使用,不用于其他任何目的,擅自使用本文讲解的技术而导致的任何意外,与作者不负责