本期內容是關于某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(兩個杠)
最后
微信公眾號:爬蟲爬呀爬
如果你覺得這篇文章對你有幫助,不妨請作者喝一杯咖啡吧!