一、前言
從事移動安全行業以來,一直在做Android方面的安全及逆向,也曾想過了解下iOS的機制,奈何總是對自己下不了決心,一方面覺得精力有限,Android上好多東西自己也并沒有完全熟練掌握。另一方面可能自己太懶,不太想花費太多時間成本,何況如果沒有實操,所學的一切很快就會忘記,所以之前也僅僅是在心里埋下了這個種子而已。不過大概幾周前,筆者實屬有幸,因機緣巧合,向iOS逆向大佬貓大人好好請教了一番,至此也算是入了個iOS的小門。
因此本文也算是一個從Android視角來看待iOS逆向,iOS相關的深入點不會進行闡述。也僅以文本紀念下和大佬們在一起做安全逆向的時光~
二、入門基礎
從這里開始,是比較基礎的東西,但是由于我也是剛剛學習到,所以就列出來記錄一下,有基礎可以直接跳過第二節,從第三節/或第四節閱讀。
2.1 工具
2.1.1手機選型
在Android上通常用于逆向的手機是pixel/nexus系列,Android系統6~13都可,然后自己刷入magisk進行root。而iOS呢肯定是iPhone了,但是如何選系統如何自己越獄呢?因此在了解相關版本后,為了方便,在某寶買了一臺二手的iphone8(800左右),ios14左右的系統,商家已經幫你越獄好了(unc0ver,14系統上每次手機重啟還需點擊軟件進行越獄,不麻煩)。
當然其他手機也可以,按照大佬的建議,iOS系統最好不要最新,像13,14左右就可以,手機的話像年頭久一點的iPhone 6(大概3,4百左右),iphone8/X/SE,也都可以做逆向。切記!買手機時要問有無id鎖,是否可以刷機。如果手機來源不正規,自然就會被鎖住,也不方便逆向用了。
2.1.2 手機軟件
手機選好后,自然就要安裝一些相關軟件,就像android逆向root之后要按各種插件,比如抓包用AlwaysTrustUserCerts信任證書之類的。iOS也是一樣,iOS越獄后有個Cydia的商店,里邊可以下載安裝各種越獄插件,包括自己寫的越獄開發插件也會在這里進行管理。
常用的iOS插件:
◆Apple File Conduit"2":用于激活助手類工具對iOS設備所有路徑的訪問權限。
◆AppSync Unified:用來繞過系統對應用的簽名驗證,可以隨意安裝和運行脫殼后的ipa
◆Filza File Manager :手機上的文件管理器(簡稱“Filza”),用來瀏覽手機文件、修改文件的權限等
◆SSL Kill Switch 2: ios版的 justtrustme
◆OpenSSH: 鏈接電腦
當然以上的插件,是我在買好手機后,就已經安裝好了,自己并沒有額外做什么操作,除了frida。
◆frida:frida的安裝在cydia里默認是最新的,因此可以去frida官網的release(https://github.com/frida/frida/releases)下載對應的包,這里和android不同的是,要下載deb的包,然后通過如ifunbox的工具,安裝到手機的目錄,然后在手機上通過Filza點擊deb包進行安裝。安裝好后,frida由cydia進行管理,frida-server默認開啟,類似Android上的MagiskFrida

2.1.3 電腦工具
◆ifunbox:上圖所使用的iPhone管理軟件就是ifunbox,進行文件管理挺好用的
◆frida:和Android一樣,電腦上也要有frida
◆frida-ios-dump: 一鍵脫殼工具。使用:./dump.py 包名 --> 在當前文件夾下生成 包名.ipa 脫殼后的文件
◆class-dump: 提取所有的頭文件,方便查看一個類中的方法。使用:class-dump -H 脫殼后的主包 -o 導出頭文件需要存放路徑
◆iproxy:端口轉發,通過用于ssh鏈接手機。安裝方法:brew install usbmuxd。使用:iproxy 2222 22轉發后
2.2 一些問題
這里記錄一下遇到過的問題,及一些雜項。比如手機越獄后,發現開不開機無法進入主界面,有可能是注入的插件有問題。可以通過ssh進入手機目錄:$ sshroot@127.0.0.1-p 2222 ,默認密碼是alpine。然后可以進入插件列表 cd /Library/MobileSubstrate/DynamicLibraries,這里是所有安裝過的插件列表,比如我這里是這樣的,也可以看到我這個二手手機可能也是用了好幾年淘汰下來的。

所以如果你懷疑哪個插件有問題,可以重命名這個插件,然后在上述目錄重啟系統進程:killall -9 SpringBoard; killall -9 backboard。
或者可以重啟所有進程:ldrestart 。總之到這里,我開不開機的問題是解決了。
三、開始逆向
通過上邊的知識,即便沒有ios逆向基礎,也可以開始準備逆向了。
3.1 準備
3.1.1 脫殼
由于軟件要上架appstore,蘋果市場是默認會對應用進行加殼的。因此我們的第一步在appstore下載好相關軟件后,就可以進行脫殼。
手機上安裝好軟件后,電腦開啟端口轉發:iproxy 2222 22。然后進入frida-ios-dump腳本的目錄直接執行./dump 包名。
./dump.py com.xxx Dumping xxx to /var/folders/rl/6nvyvpmj3z352q0m8xvm0db40000gn/T [frida-ios-dump]: ZmFFmpeg.framework has been loaded. [frida-ios-dump]: libswift_Concurrency.dylib has been dlopen. ... libswift_Concurrency.dylib.fid: 100%|█████████████████████████████| 408k/408k [00:00<00:00, 5.97MB/s] Validated.plist: 251MB [00:14, 18.2MB/s] 0.00B [00:00, ?B/s]Generating "xxx.ipa"
稍等片刻,在當前目錄會生成脫殼后的ipa文件。然后我們需要找到該應用的主包,以便拖入ida分析:
1.首先將脫殼后的.ipa文件改后綴為.zip(和Android APK一樣,也是個壓縮包),解壓后進入Payload,會有一個.app的文件包。

2.點擊顯示包內容,通常我們要拖入ida里分析的包,名字和上層xxx.app是相同的,然后就可以拖進ida,由于包比較大,ida分析時間會很慢。
3.1.2 class-dump
這里也使用class-dump將頭文件導出class-dump -H ./osee2unifiedRelease.app/osee2unifiedRelease -o ./osee2unifiedReleaseH,導出頭文件的作用是,方便我們查看OC中類的所有方法/屬性。
3.1.3 抓包
這里和Android沒什么區別,在手機上安裝charles證書,信任證書,然后抓包。我們關注下相關的登錄接口。

發現body是加密的,于是看看body是如何加密的。
3.2 逆向分析
3.2.1 通過關鍵字回溯(未定位到算法)
首先在ida里搜索登錄相關字符串/api/account/prod/sign_in,發現可以直接找到,查看相關交叉引用。

發現很多,隨便找幾個先看看,都是調用了同一個函數sub_1063DF0A8(),但是奇怪的是這個函數的第二個參數/api/account/prod/sign_in,在F5里并沒有看到。

但是在匯編里是能看到的,我不知道這樣做的目的是什么,看了很多iOS逆向的帖子也沒有看到這樣的情況,或者說這是ida反混淆的問題?總之,這不重要。(如果有大佬知道,煩請解答)

然后通過回溯堆棧(console.log(Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join(''));),看能否定位到關鍵信息。
0x106913598 osee2unifiedRelease!0x63d7598 (0x1063d7598) 0x1069131fc osee2unifiedRelease!0x63d71fc (0x1063d71fc) 0x1068e27d0 osee2unifiedRelease!0x63a67d0 (0x1063a67d0) 0x10690767c osee2unifiedRelease!0x63cb67c (0x1063cb67c) 0x1020724bc osee2unifiedRelease!0x1b364bc (0x101b364bc) 0x10207256c osee2unifiedRelease!0x1b3656c (0x101b3656c) 0x102061e10 osee2unifiedRelease!0x1b25e10 (0x101b25e10) 0x100608f90 osee2unifiedRelease!0xccf90 (0x1000ccf90) 0x1a0ec1298 libdispatch.dylib!_dispatch_call_block_and_release
這里和Android逆向so也完全一樣,稍有區別的是,iOS查找基址填入的是整個庫名,如:
var base = Module.getBaseAddress("osee2unifiedRelease");
console.log("base: ",base);
回溯出來堆棧之后,可以對整個堆棧鏈路的函數進行分析及hook,不過遺憾的是,或許是對iOS網絡框架不熟,我并沒有辦法僅憑查找url,就能定位到加密算法。不過逆向有意思的地方也在這里,當一條路走不通了,放松下自己換一條。
3.2.2 通過hook base64
我們觀察body其實可以發現他是個base64,那我們大概猜一下,他使用系統庫的方式。
經查資料,可以hook OC中NSData的base64EncodedStringWithOptions方法,在OC的語法中函數調用的方式可以用[類名 方法名:參數],hook的方式發現網上大多采用frida-trace。在我印象里好像沒什么印象,即便有,也是聽了個名詞,因為在Android中我基本沒用到過。于是使用這個命令進行hook,減號代表實例方法,相反加號代表類方法,只是個格式而已,也可以用*匹配。
frida-trace -UF -m "-[NSData base64EncodedStringWithOptions:]"
這個腳本會在當前目錄生成./__handlers__/文件夾,并生成對應函數的js代碼,發現其實這就是Interceptor.attach的那個回調函數,只不過frida-trace幫你自動生成好了,方便你改腳本。
{
onEnter(log, args, state) {
this.self = args[0];
},
onLeave(log, retval, state) {
var before = ObjC.classes.NSString.alloc().initWithData_encoding_(this.self, 4);
var after = new ObjC.Object(retval);
log(`-[NSData base64EncodedStringWithOptions:]before=${before}=`);
log(`-[NSData base64EncodedStringWithOptions:]after=${after}=`);
if(after.toString().indexOf("sEn8t")>=0){
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE) .map(DebugSymbol.fromAddress).join(''));
}
}
}
當然到這里,運氣也比較好,發現返回值的確可以跟抓包的body對應上,于是打堆棧。
0x102e0a7d8 osee2unifiedRelease!+[ZHWhiteBoxEncryptTool encryptDataBase64String:] 0x102e0a6b8 osee2unifiedRelease!+[ZHWhiteBoxEncryptTool encryptData:] 0x10444b098 osee2unifiedRelease!+[NSURLRequest zh_whiteBoxEncryptRegisterLoginURLHTTPBody:] 0x10447c3f0 osee2unifiedRelease!+[ZHURLProtocol canonicalRequestForRequest:] 0x1a1863ffc CFNetwork!0x3ffc (0x180a47ffc) ...
3.2.3 尋找加密算法
進入[ encryptDataBase64String:]函數看看,發現密鑰(93020...)是寫死的360位的hex字符串(hex轉換為bytes后長度是180)。

總體是進行了三種加密,分別是
sub_106B3E2A0 -> preDataIn160:secureKey:iv:
void *__fastcall sub_106B3E2A0(void *a1, void *a2, void *a3, void *a4, void *a5)
{
return _objc_msgSend(a1, "preDataIn160:secureKey:iv:", a3, a4, a5);
}
sub_106B1CA20 -> laesEncryptData:secureKey:iv:
void *__fastcall sub_106B1CA20(void *a1, void *a2, void *a3, void *a4, void *a5)
{
return _objc_msgSend(a1, "laesEncryptData:secureKey:iv:", a3, a4, a5);
}
sub_106B3E2E0 -> preDataOut160:secureKey:iv:
void *__fastcall sub_106B3E2E0(void *a1, void *a2, void *a3, void *a4, void *a15)
{
return _objc_msgSend(a1, "preDataOut160:secureKey:iv:", a3, a4, a15);
}
這里的_objc_msgSend是OC底層通過發送消息,來進行函數調用的,其中a1是類,第二個參數是方法名,其余是參數。我們也可以到之前class-dump出來的頭文件里看看,還是很清晰的。

然后我們hook這個三個函數的入參和出參,就可以得到整個從明文到密文的加密鏈路。當然,這里需要注意的,雖然我們還是可以使用Android frida hook的方式(基址+偏移),但是我們打印參數時,卻不能脫離OC的方式。
比如我們hook 這個函數laesEncryptData,即便我們知道真正的參數從a3開始(類似OC的調用約定吧,從第三個參數開始傳參),但是我們像在Android那樣,僅通過hexdump是無法打印出預期的值的。打印OC有點類似打印JNI,需要使用對應的方法,比如在輸出a3時,可以先使用new ObjC.Object(this.arg2)打印下對象,如果輸出的類似這種{length = 32, bytes = 0x36666161 39316535 38616339 63346661 ... 37363438 38323730 }(如是字符串類型直接能輸出)就可以使用Memory.readByteArray(data.bytes(),data.length())來進行hexdump了,其余的沒什么區別。
id __cdecl +[BangcleCryptoTool laesEncryptData:secureKey:iv:](BangcleCryptoTool_meta *self, SEL a2, id a3, id a4, id a5)

3.3 主動調用
在我們定位好關鍵算法之后,通常為了測試方便,往往是需要主動調用函數的,和Android無異。比如這個app,他總是有線程在做加密,即便把網關掉了也不行,這對于我們分析輸出日志是很不方便的。我們可以通過Interceptor.replace函數替換掉某個方法,而我們自己主動調用時,調其內部的方法即可。
比如,我們這里分析到laesEncryptData函數內部會調用sub_1000902A8方法,這個sub_1000902A8方法內部會調用sub_100090420這個方法,因此我們可以主動調用sub_100090420,替換掉sub_1000902A8,就可以去除干擾(另兩個函數preDataIn160,preDataOut160不是核心算法,也不復雜,這里不做過多闡述)。
__int64 __fastcall sub_1000902A8(__int64 a1, unsigned int a2, __int64 a3, __int64 a4, __int64 a5, unsigned int a6, __int64 a7, unsigned int a8, int a9)
{
v24 = a1;
v23 = a2;
v22 = a3;
v21 = a4;
v20 = a5;
v19 = a6;
a7a = a7;
a8a = a8;
v16 = a9;
LODWORD(v10) = 1;
HIDWORD(v10) = 4;
v14 = 1;
v15 = a9;
v12 = 1;
v13 = 0;
v11 = 0;
return sub_100090420(a1, a2, a3, (int *)a4, a5, a6, a7, a8, &v10);
}
這里還有一點和Android不一樣,就是地址偏移,在iOS中,使用基址+偏移的方式hook時,ida中的地址要減去10000000。下為替換算法。
function replace(){
var base = Module.getBaseAddress("osee2unifiedRelease");
Interceptor.replace(base.add(0x902A8),new NativeCallback(function(a,b,c,d,e,f,g,h,i){
return 0;
},'int',['pointer','int','pointer','int','pointer','int','pointer','int','pointer']));
}
當然,在這里我遇到了一點小坑,其實如上的反匯編代碼是不準確的,該函數共有9個參數,根據arm64的調用約定,超過8個參數,會通過棧傳遞,也就是最后一個參數v10,并不是如偽代碼那樣直接傳遞的。
__text:00000001000902A8 sub_1000902A8 __text:00000001000902A8 STP X29, X30, [SP,#-0x10]! __text:00000001000902AC MOV X29, SP __text:00000001000902B0 SUB SP, SP, #0x70 __text:00000001000902B4 LDR W8, [X29,#0x10] __text:00000001000902B8 ADD X9, SP, #0x10 ; 賦值給x9,作為參數傳遞,后面又將x9賦值給sp __text:00000001000902BC MOV W10, #1;這個a9的一個參數 __text:00000001000902C0 MOV W11, #4;這也是一個參數 __text:00000001000902C4 STUR X0, [X29,#-8] __text:00000001000902C8 STUR W1, [X29,#-0xC] __text:00000001000902CC STUR X2, [X29,#-0x18] __text:00000001000902D0 STUR X3, [X29,#-0x20] __text:00000001000902D4 STUR X4, [X29,#-0x28] __text:00000001000902D8 STUR W5, [X29,#-0x2C] __text:00000001000902DC STR X6, [SP,#0x38] __text:00000001000902E0 STR W7, [SP,#0x34] __text:00000001000902E4 STR W8, [SP,#0x30] __text:00000001000902E8 STR W10, [SP,#0x10] ; a11 傳遞到棧上 ,也就是傳到x9上 __text:00000001000902EC STR W11, [SP,#0x14] __text:00000001000902F0 STR W10, [SP,#0x28] ; 傳遞到棧上 __text:00000001000902F4 LDR W8, [SP,#0x30] __text:00000001000902F8 STR W8, [SP,#0x2C] __text:00000001000902FC STR W10, [SP,#0x20]; 傳遞到棧上 __text:0000000100090300 STR WZR, [SP,#0x24] __text:0000000100090304 STR WZR, [SP,#0x18] __text:0000000100090308 LDUR X0, [X29,#-8] __text:000000010009030C LDUR W1, [X29,#-0xC] __text:0000000100090310 LDUR X2, [X29,#-0x18] __text:0000000100090314 LDUR X3, [X29,#-0x20] __text:0000000100090318 LDUR X4, [X29,#-0x28] __text:000000010009031C LDUR W5, [X29,#-0x2C] __text:0000000100090320 LDR X6, [SP,#0x38] ; a7 __text:0000000100090324 LDR W7, [SP,#0x34] ; a8 __text:0000000100090328 STR X9, [SP] ; a9 a9參數通過sp傳遞 __text:000000010009032C BL sub_100090420 __text:0000000100090330 MOV SP, X29 __text:0000000100090334 LDP X29, X30, [SP+var_s0],#0x10 __text:0000000100090338 RET
另外一種定位a9是如何傳值的:跳轉進函數后,查看a9的交叉引用,可以發現v20是個數組,最多用到了v20[7]。

結合frida hook的結果。

可以判斷,a9是一個int數組,長度為4(int占4字節空間)*8=32(0x20)大小。
因此使用frida構造a9參數時使用:
var dword = Memory.alloc(32);
Memory.writeUInt(dword,1);
Memory.writeUInt(dword.add(4),4);
Memory.writeUInt(dword.add(4*2),0);
Memory.writeUInt(dword.add(4*3),1);
Memory.writeUInt(dword.add(4*4),1);
Memory.writeUInt(dword.add(4*5),0);
Memory.writeUInt(dword.add(4*6),1);
Memory.writeUInt(dword.add(4*7),1);
主動調用的代碼如下,其中sub_100090420這個函數的前8個參數分別的,輸入/長度,輸出/長度,iv/長度,key/長度,輸入隨便找的hook時真實的數據。
function call_aes(){
var base = Module.getBaseAddress("osee2unifiedRelease");
console.log("base: ",base);
var aes = new NativeFunction(base.add(0x90420),'int',['pointer','int','pointer','pointer','pointer','int','pointer','int','pointer']);
//輸入
var data_len = 0x20;
const data = Memory.alloc(data_len);
Memory.writeByteArray(data,[0xca,0xcc,0x6e,0x68,0x64,0x63,0xc6,0x6e,0x60,0xc2,0x66,0xc4,0xc8,0x6c,0xc4,0xc6,0xca,0xc2,0x60,0xc4,0x6c,0x64,0x61,0x61,0x61,0xc4,0xc6,0xc4,0xc2,0xc8,0x6c,0x62]);
//輸出:空的byte數組,函數返回后,有值
var result_len = 16 * (data_len / 16 + 1);
var result = Memory.alloc(result_len);
var result_len_ptr = Memory.alloc(Process.pointerSize);
result_len_ptr.writeUInt(result_len);
//iv
var iv_len = 0x10;
const iv = Memory.alloc(iv_len);
Memory.writeByteArray(iv,[0x4c,0x41,0xb2,0xc9,0xb4,0xba,0xff,0x8a,0x6a,0x69,0xa5,0x99,0x02,0x5f,0x03,0x15]);
//key
var key_len = 0xb4; //長度180
const key = Memory.alloc(key_len);
Memory.writeByteArray(key,[0x93,0x02,0x01,0x9f,0xbf,0xa1,0xbb,0x6b,0xdb,0x9f,0xca,0x46,0x84,0xb3,0xe7,0xf6,0x38,0x30,0x44,0x18,0x14,0x06,0x35,0x60,0x29,0x7e,0x4f,0x00,0xde,0x63,0x69,0x41,0x66,0x4f,0x7e,0xa3,0x94,0x29,0xb2,0x60,0x4e,0x4f,0x93,0xa7,0x84,0x0e,0xcf,0x12,0x54,0xcb,0xa8,0xd9,0xea,0x29,0xcd,0xf4,0xf7,0xe4,0x01,0x97,0xb5,0x0d,0xf7,0x7e,0x19,0xfb,0x07,0xf2,0xf9,0x74,0xe7,0x87,0xcf,0x87,0x32,0xa6,0x2a,0x1e,0x2e,0x0f,0xcb,0xfa,0x2a,0xcb,0xac,0x63,0x76,0xc8,0x32,0xc0,0x82,0x39,0xa0,0xb5,0xd9,0xe0,0xe7,0x06,0xeb,0x27,0xb8,0x31,0xe5,0xef,0xfc,0xdb,0x3d,0x00,0x08,0x7e,0x62,0xa6,0x02,0x92,0x31,0xf6,0x4a,0x2b,0x30,0x99,0x72,0x07,0x59,0xe3,0x1f,0x9d,0xfa,0x12,0x8b,0xc7,0xe9,0x6a,0x83,0xd7,0x1a,0xf7,0x9a,0xa4,0x89,0xb9,0xe5,0x6f,0xfd,0xd5,0xe2,0xf1,0x42,0xa3,0xf9,0xac,0x11,0xe4,0xab,0xce,0x01,0xc6,0xf2,0xfb,0xca,0x01,0xb7,0x59,0xac,0x84,0x2f,0x14,0x91,0xa1,0xa5,0x8d,0x74,0xea,0xdd,0x2b,0x38,0x09,0x1e,0xb8,0x21,0x16])
//最后一個參數
var dword = Memory.alloc(32);
Memory.writeUInt(dword,1);
Memory.writeUInt(dword.add(4),4);
Memory.writeUInt(dword.add(4*2),0);
Memory.writeUInt(dword.add(4*3),1);
Memory.writeUInt(dword.add(4*4),1);
Memory.writeUInt(dword.add(4*5),0);
Memory.writeUInt(dword.add(4*6),1);
Memory.writeUInt(dword.add(4*7),1);
//主動調用
var aes_r = aes(data,data_len,result,result_len_ptr,iv,iv_len,key,key_len,dword);
console.log("aes_r",aes_r,hexdump(result,{length:result_len}));
}
至此主動調用成功,后要詳細分析sub_100090420算法。

四、核心算法分析
在我們正式分析魔改的aes算法之前,我想應該是要介紹下aes標準算法的原理,就當是回顧下知識點,所以這一節可能會比較枯燥,不過這里還是只介紹下相關的概念,不會太深入細節。因此這一節可以粗略的過下,甚至跳過,后面的內容如果迷惑了,可以返回來看看。下面我們大概概述下標準AES算法的加密流程。
4.1 AES標準算法
AES-128接收16字節的明文輸入,16字節的密鑰,輸出16字節的密文結果。且每增加64位,AES-128/192/256算法的循環會增加2輪。
以AES-128為例,共加密10輪,其中包含的操作為:
1.SubBytes:字節替換(通過查aes固定S-Box替換)
2.ShiftRows:行移位(矩陣第1行不變,第2行左移1個字節,第3行左移2個字節,第4行左移3個字節)
3.MixColumns:列混淆(通過左乘一個固定矩陣)
4.AddRoundKey:輪密鑰加(通過密鑰編排得來,首次使用為主密鑰)

其中初始變換只執行AddRoundKey,算法循環第1~9輪依次執行SubBytes,ShiftRows,MixColumns,AddRoundKey。最終輪(第10輪)不包含MixColumns。算法完畢。
上邊就是AES的整體流程,和要用到的知識點。
4.2 使用trace
回過頭來,接著看sub_100090420這個函數,確實已經脫離了OC,進入了熟悉的C環境,雖然沒有混淆,但是其內部分支跳轉太多,靜態看起來也不是很方便。

于是我用到了Virenz大佬寫好的stalker腳本(https://github.com/Virenz/frida-js),分函數trace和指令trace,格式非常清晰方便,推薦使用。
分別對sub_100090420進行函數/指令trace,首先看下function trace,發現調用的函數并不多,這里先將關鍵函數的作用寫出,后續將詳細分析該算法是如何魔改aes的。
function trace: [函數地址]([調用地址]) -- 調用層級 [0x100091054]( [0x10009047c] ) -- 0 //1. 密鑰編排后的處理 [0x106a9b340]( [0x100091074] ) -- 1 //malloc [0x100091b3c]( [0x100090520] ) -- 0 [0x100091bcc]( [0x100090548] ) -- 0 [0x106a9a530]( [0x100090614] ) -- 0 //calloc [0x106a9a1e8]( [0x10009062c] ) -- 0 //memcpy [0x100091c7c]( [0x100090664] ) -- 0 //類似pkcs填充 [0x106a9a1f4]( [0x100091cf8] ) -- 1 //使用memset進行填充 [0x106a9a1f4]( [0x100091d6c] ) -- 1 //使用memset進行填充 [0x100094360]( [0x100090b18] ) -- 0 //2.關鍵函數,CBC模式,明文異或 [0x100091fac]( [0x10009440c] ) -- 1 //3.真正魔改aes的加密,測試時輸出明文為32個字節,通過填充后,輸出為48個字節,且aes128每輪循環加密16字節,故48/16=3,0x100091fac函數循環3輪 [0x100091fac]( [0x10009440c] ) -- 1 [0x100091fac]( [0x10009440c] ) -- 1 [0x106a9aaa0]( [0x100091038] ) -- 0 //free [0x100091ef4]( [0x100091040] ) -- 0 [0x106a9aaa0]( [0x100091f18] ) -- 1 //free
通過function trace打印了函數執行流程后,可以查看密文result的交叉引用,并hook相關函數(從function trace來看并不多),打印輸入輸出,最終定位到了這里。

看下sub_100094360這個函數。
__int64 __fastcall sub_100094360(__int64 a1, __int64 a2, int a3, __int64 a4, __int64 a5, void (__fastcall *a6)(__int64, __int64, __int64, unsigned int *))
{
__int64 iv_1; // [xsp+0h] [xbp-40h]
signed int i; // [xsp+8h] [xbp-38h]
unsigned int v9; // [xsp+Ch] [xbp-34h]
void (__fastcall *v10)(__int64, __int64, __int64); // [xsp+10h] [xbp-30h]
__int64 keyptr; // [xsp+18h] [xbp-28h]
__int64 iv; // [xsp+20h] [xbp-20h]
int result_len; // [xsp+2Ch] [xbp-14h]
__int64 result; // [xsp+30h] [xbp-10h]
__int64 data; // [xsp+38h] [xbp-8h]
data = a1;
result = a2;
result_len = a3;
iv = a4;
keyptr = a5;
v10 = a6;
v9 = 0;
iv_1 = a4;
while ( result_len >= 16 )
{
for ( i = 0; i < 16; ++i )
*(result + i) = *(data + i) ^ *(iv_1 + i);
(v10)(result, result, keyptr, &v9); // aes加密算法
iv_1 = result;
result_len -= 16;
data += 16LL;
result += 16LL;
}
return v9;
}
發現這個函數將result分割16個字節,每次循環首先將明文與iv異或并作為sub_100091FAC(v10)的參數,調用完后,將結果重新賦值給iv,并進行下一輪循環。
這里其實就是分組密碼常見的CBC模式,因為aes也是分組密碼,在進行加密之前,先將明文分組,如果不夠分了,就進行相應規則填充數據。過程就是將明文分組與前一個密文分組進行XOR異或運算,首輪的話就與iv異或,上述代碼ida反編譯的很好了,對照下圖,應該就可以理解了。

4.3 trace分析
接下來我們要分析核心算法sub_100091fac,先看下好像并不多。
__int64 __fastcall sub_100091FAC(__int64 result, __int64 a2, __int64 *key2)
{
int v3; // w9
signed int i; // [xsp+14h] [xbp-6Ch]
signed int k; // [xsp+14h] [xbp-6Ch]
signed int l; // [xsp+14h] [xbp-6Ch]
signed int m; // [xsp+14h] [xbp-6Ch]
signed int n; // [xsp+14h] [xbp-6Ch]
signed int ii; // [xsp+14h] [xbp-6Ch]
int j; // [xsp+18h] [xbp-68h]
__int64 key2_1; // [xsp+20h] [xbp-60h]
char v12; // [xsp+48h] [xbp-38h]
char v13; // [xsp+49h] [xbp-37h]
char v14; // [xsp+4Ah] [xbp-36h]
char v15; // [xsp+4Bh] [xbp-35h]
char v16; // [xsp+4Ch] [xbp-34h]
char v17; // [xsp+4Dh] [xbp-33h]
char v18; // [xsp+4Eh] [xbp-32h]
char v19; // [xsp+4Fh] [xbp-31h]
char v20; // [xsp+50h] [xbp-30h]
char v21; // [xsp+51h] [xbp-2Fh]
char v22; // [xsp+52h] [xbp-2Eh]
char v23; // [xsp+53h] [xbp-2Dh]
char v24; // [xsp+54h] [xbp-2Ch]
char v25; // [xsp+55h] [xbp-2Bh]
char v26; // [xsp+56h] [xbp-2Ah]
char v27; // [xsp+57h] [xbp-29h]
char v28; // [xsp+58h] [xbp-28h]
char v29; // [xsp+59h] [xbp-27h]
char v30; // [xsp+5Ah] [xbp-26h]
char v31; // [xsp+5Bh] [xbp-25h]
char v32; // [xsp+5Ch] [xbp-24h]
char v33; // [xsp+5Dh] [xbp-23h]
char v34; // [xsp+5Eh] [xbp-22h]
char v35; // [xsp+5Fh] [xbp-21h]
char v36; // [xsp+60h] [xbp-20h]
char v37; // [xsp+61h] [xbp-1Fh]
char v38; // [xsp+62h] [xbp-1Eh]
char v39; // [xsp+63h] [xbp-1Dh]
char v40; // [xsp+64h] [xbp-1Ch]
char v41; // [xsp+65h] [xbp-1Bh]
char v42; // [xsp+66h] [xbp-1Ah]
char v43; // [xsp+67h] [xbp-19h]
unsigned __int8 v44; // [xsp+68h] [xbp-18h]
unsigned __int8 v45; // [xsp+69h] [xbp-17h]
unsigned __int8 v46; // [xsp+6Ah] [xbp-16h]
unsigned __int8 v47; // [xsp+6Bh] [xbp-15h]
unsigned __int8 v48; // [xsp+6Ch] [xbp-14h]
unsigned __int8 v49; // [xsp+6Dh] [xbp-13h]
unsigned __int8 v50; // [xsp+6Eh] [xbp-12h]
unsigned __int8 v51; // [xsp+6Fh] [xbp-11h]
unsigned __int8 v52; // [xsp+70h] [xbp-10h]
unsigned __int8 v53; // [xsp+71h] [xbp-Fh]
unsigned __int8 v54; // [xsp+72h] [xbp-Eh]
unsigned __int8 v55; // [xsp+73h] [xbp-Dh]
unsigned __int8 v56; // [xsp+74h] [xbp-Ch]
unsigned __int8 v57; // [xsp+75h] [xbp-Bh]
unsigned __int8 v58; // [xsp+76h] [xbp-Ah]
unsigned __int8 v59; // [xsp+77h] [xbp-9h]
key2_1 = *key2;
v3 = *(key2 + 4) + (*(key2 + 4) < 0 ? 0x1F : 0);
for ( i = 0; i < 16; ++i )
*(&v44 + i) = (byte_106EF9068[*(key2_1 + i) & 0xF ^ 16 * (*(result + i) & 0xF)] >> 4) & 0xF ^ 16
* ((byte_106EF9068[(*(key2_1 + i) >> 4) & 0xF ^ 16 * ((*(result + i) >> 4) & 0xF)] >> 4) & 0xF);
for ( j = 1; j < (v3 >> 5) + 6; ++j )
{
v28 = dword_106EF9168[v44] >> 24;
v29 = dword_106EF9168[v44] >> 16;
v30 = LOWORD(dword_106EF9168[v44]) >> 8;
v31 = dword_106EF9168[v44];
v32 = dword_106EF9168[v48] >> 24;
v33 = dword_106EF9168[v48] >> 16;
v34 = LOWORD(dword_106EF9168[v48]) >> 8;
v35 = dword_106EF9168[v48];
v36 = dword_106EF9168[v52] >> 24;
v37 = dword_106EF9168[v52] >> 16;
v38 = LOWORD(dword_106EF9168[v52]) >> 8;
v39 = dword_106EF9168[v52];
v40 = dword_106EF9168[v56] >> 24;
v41 = dword_106EF9168[v56] >> 16;
v42 = LOWORD(dword_106EF9168[v56]) >> 8;
v43 = dword_106EF9168[v56];
v12 = dword_106EF9568[v49] >> 24;
v13 = dword_106EF9568[v49] >> 16;
v14 = LOWORD(dword_106EF9568[v49]) >> 8;
v15 = dword_106EF9568[v49];
v16 = dword_106EF9568[v53] >> 24;
v17 = dword_106EF9568[v53] >> 16;
v18 = LOWORD(dword_106EF9568[v53]) >> 8;
v19 = dword_106EF9568[v53];
v20 = dword_106EF9568[v57] >> 24;
v21 = dword_106EF9568[v57] >> 16;
v22 = LOWORD(dword_106EF9568[v57]) >> 8;
v23 = dword_106EF9568[v57];
v24 = dword_106EF9568[v45] >> 24;
v25 = dword_106EF9568[v45] >> 16;
v26 = LOWORD(dword_106EF9568[v45]) >> 8;
v27 = dword_106EF9568[v45];
for ( k = 0; k < 16; ++k )
*(&v28 + k) = (byte_106EF9968[*(&v12 + k) & 0xF ^ 16 * (*(&v28 + k) & 0xF)] >> 4) & 0xF ^ 16
* ((byte_106EF9968[(*(&v12 + k) >> 4) & 0xF ^ 16 * ((*(&v28 + k) >> 4) & 0xF)] >> 4) & 0xF);
v12 = dword_106EF9A68[v54] >> 24;
v13 = dword_106EF9A68[v54] >> 16;
v14 = LOWORD(dword_106EF9A68[v54]) >> 8;
v15 = dword_106EF9A68[v54];
v16 = dword_106EF9A68[v58] >> 24;
v17 = dword_106EF9A68[v58] >> 16;
v18 = LOWORD(dword_106EF9A68[v58]) >> 8;
v19 = dword_106EF9A68[v58];
v20 = dword_106EF9A68[v46] >> 24;
v21 = dword_106EF9A68[v46] >> 16;
v22 = LOWORD(dword_106EF9A68[v46]) >> 8;
v23 = dword_106EF9A68[v46];
v24 = dword_106EF9A68[v50] >> 24;
v25 = dword_106EF9A68[v50] >> 16;
v26 = LOWORD(dword_106EF9A68[v50]) >> 8;
v27 = dword_106EF9A68[v50];
for ( l = 0; l < 16; ++l )
*(&v28 + l) = (byte_106EF9968[*(&v12 + l) & 0xF ^ 16 * (*(&v28 + l) & 0xF)] >> 4) & 0xF ^ 16
* ((byte_106EF9968[(*(&v12 + l) >> 4) & 0xF ^ 16 * ((*(&v28 + l) >> 4) & 0xF)] >> 4) & 0xF);
v12 = dword_106EF9E68[v59] >> 24;
v13 = dword_106EF9E68[v59] >> 16;
v14 = LOWORD(dword_106EF9E68[v59]) >> 8;
v15 = dword_106EF9E68[v59];
v16 = dword_106EF9E68[v47] >> 24;
v17 = dword_106EF9E68[v47] >> 16;
v18 = LOWORD(dword_106EF9E68[v47]) >> 8;
v19 = dword_106EF9E68[v47];
v20 = dword_106EF9E68[v51] >> 24;
v21 = dword_106EF9E68[v51] >> 16;
v22 = LOWORD(dword_106EF9E68[v51]) >> 8;
v23 = dword_106EF9E68[v51];
v24 = dword_106EF9E68[v55] >> 24;
v25 = dword_106EF9E68[v55] >> 16;
v26 = LOWORD(dword_106EF9E68[v55]) >> 8;
v27 = dword_106EF9E68[v55];
for ( m = 0; m < 16; ++m )
*(&v28 + m) = (byte_106EF9968[*(&v12 + m) & 0xF ^ 16 * (*(&v28 + m) & 0xF)] >> 4) & 0xF ^ 16
* ((byte_106EF9968[(*(&v12 + m) >> 4) & 0xF ^ 16 * ((*(&v28 + m) >> 4) & 0xF)] >> 4) & 0xF);
for ( n = 0; n < 16; ++n )
*(&v44 + n) = (byte_106EF9968[*(key2_1 + n + 16 * j) & 0xF ^ 16 * (*(&v28 + n) & 0xF)] >> 4) & 0xF ^ 16 * ((byte_106EF9968[(*(key2_1 + n + 16 * j) >> 4) & 0xF ^ 16 * ((*(&v28 + n) >> 4) & 0xF)] >> 4) & 0xF);
}
v28 = byte_106EFA268[v44];
v29 = byte_106EFA268[v49];
v30 = byte_106EFA268[v54];
v31 = byte_106EFA268[v59];
v32 = byte_106EFA268[v48];
v33 = byte_106EFA268[v53];
v34 = byte_106EFA268[v58];
v35 = byte_106EFA268[v47];
v36 = byte_106EFA268[v52];
v37 = byte_106EFA268[v57];
v38 = byte_106EFA268[v46];
v39 = byte_106EFA268[v51];
v40 = byte_106EFA268[v56];
v41 = byte_106EFA268[v45];
v42 = byte_106EFA268[v50];
v43 = byte_106EFA268[v55];
for ( ii = 0; ii < 16; ++ii )
*(a2 + ii) = (byte_106EFA368[*(key2_1 + ii + 16 * j) & 0xF ^ 16 * (*(&v28 + ii) & 0xF)] >> 4) & 0xF ^ 16 * ((byte_106EFA368[(*(key2_1 + ii + 16 * j) >> 4) & 0xF ^ 16 * ((*(&v28 + ii) >> 4) & 0xF)] >> 4) & 0xF);
return result;
}
他的第一個參數和第二個參數是相同的,都是明文,在函數結束后也都變成了密文,第三個參數是密鑰key,從上面來看也并不是16位的。
其實逆向到這里,我一直懷疑著,就是bangcle算法究竟把aes魔改到什么程度?雖然最外層的算法名寫的是laes,而且上層函數也的確明文分組與iv異或,并且根據trace及分析來看,中間也的確是9輪循環。那么他是否僅僅改了碼表而已?還是說不僅改了碼表,甚至連aes內部算法也重寫了?我能否對照標準的aes來還原他?以及他的key為什么是180位,而標準的aes僅僅是16位,又如何用key呢?帶著這些個疑問,我開始了進入了使用trace還原算法的世界。
4.3.1 還原首個循環
key2_1 = *key2;
v3 = *(key2 + 4) + (*(key2 + 4) < 0 ? 0x1F : 0); //(1)
for ( i = 0; i < 16; ++i ) //(2)
*(&v44 + i) = (byte_106EF9068[*(key2_1 + i) & 0xF ^ 16 * (*(result + i) & 0xF)] >> 4) & 0xF ^ 16 * ((byte_106EF9068[(*(key2_1 + i) >> 4) & 0xF ^ 16 * ((*(result + i) >> 4) & 0xF)] >> 4) & 0xF);
首先我這里還原的方式,是通過之前的指令trace日志+clion還原代碼時調試一步步分析。
1.如上邊這一段代碼(1)處,把ida里的偽代碼拷貝進去,并控制好和trace時一樣的入參。

發現這里的v3返回0x80,查看ida匯編地址,找到對應的trace結果,ida的偽碼分析沒錯。

2.接下來(2)處是個16位的循環,還是仿照第一步,先將偽碼拷貝出來,但是這次ida反編譯的代碼都在一行,我們進行下拆分。其次,ida偽碼里返回給個局部變量v44,那我們先姑且先malloc出一個自己的空間用于存儲。

那這里判斷算法是否輸出正確,有兩種方式。首先是傳統的方式,這個循環里最后生成的值是*(&v44 + i),而這個值最終是通過異或得來,因此我們查看ida里匯編的地址。

也即w12,也就是這16次for循環的結果,因此去trace里對照1000920B8地址。

第二種方式,得益于frida stalker在trace時可以定制化輸出,比如在大佬的trace腳本中,我們可以將readCString()改成hexdump出了兩行內容。

于是我們也可以直接去trace搜整個for循環的結果4c da e9 c4 5a a1 0f 28 1e a2 01 ed 5b b6 62 b9,發現內存里有很多地方都有,也即證明了此步還原準確。

4.3.2 還原9輪循環中的混合算法
接下來,又進入了一個大循環中for ( j = 1; j < (v3 >> 5) + 6; ++j )因為上一步中已經還原出v3=0x80,因此手動計算下(v3 >> 5) + 6 = (0x80 >> 5) + 6 = 10。也即aes標準算法中的9輪循環。
其實上一步的算法還原,還算容易,只需要照抄ida代碼即可,但是這一輪算法里,雖然看起來偽碼很整潔很規律,4個一組4個一組。

但我遇到了很疑惑的問題,甚至還懷疑了ida是不是有問題。
首先就是,這一部分代碼里,無論是輸入還是輸出都是局部變量。比如像這一行還好說v28 = dword_106EF9168[v44] >> 24;v44也就是上一個算法的結果,但v28是誰呢?甚至于下一個4組v32 = dword_106EF9168[v48] >> 24;v32,v48都是局部變量,這又該如何還原呢?
首先還是先猜,最開始計算了16個字節的v44的值,那就先嘗試下使用v44,于是我還原的代碼如下:

和之前一樣去ida找地址,在trace里查結果,像這個計算查右移相關的指令lsr即可。很幸運,嘗到了一絲甜頭。
//trace日志 [0x100092118] 0x104c42118 lsr w10, w10, #0x18; # x10: 0xb07a7a6d --> 0xb0, ... [0x100092138] 0x104c42138 lsr w10, w10, #0x10; # x10: 0xb07a7a6d --> 0xb07a, [0x10009213c] 0x104c4213c and w10, w10, #0xff; # x10: 0xb07a --> 0x7a, ...
于是,第一個四組的計算已經成功。可是到了下一個四組。

我嘗試將v48認為是v44+1的值(0xda)來進行計算(v44 = 4c da e9 c4 5a a1 0f 28 1e a2 01 ed 5b b6 62 b9,最最初計算的16字節的結果,在第一組v44等于4c),但遺憾的是我得出來的值卻無法與trace結果相對應。

我計算出來的v32/v33是0x18/0x67,但trace的結果卻是0x2d/0x37 ??? 看來事情并沒有我所猜測這么簡單。于是分析trace,看看值是怎么來的。
//trace結果 [0x100092188] 0x104c42188 ldurb w10, [x29, #-0x14]; # x10: 0x6d --> 0x5a, [0x10009218c] 0x104c4218c mov x11, x10; # x11: 0x10baa9298 --> 0x5a, [0x100092190] 0x104c42190 orr x12, xzr, #4; # [0x100092194] 0x104c42194 mul x11, x11, x12; # x11: 0x5a --> 0x168, [0x100092198] 0x104c42198 add x11, x9, x11; # x11: 0x168 --> 0x10baa92d0 ( //這個是碼表在內存里的值 10baa92d0 af 37 37 2d fa 73 73 d2 3e c7 c7 48 d3 a4 a4 0c .77-.ss.>..H.... 10baa92e0 ff 77 77 dd 59 51 51 5e d1 8b 8b 4d 10 52 52 16 .ww.YQQ^...M.RR.), [0x10009219c] 0x104c421bc ldr w10, [x11]; # x10: 0x5a --> 0x2d3737af, //取值dword_106EF9168[v48] [0x1000921a0] 0x104c421a0 lsr w10, w10, #0x18; # x10: 0x2d3737af --> 0x2d, //2d值的由來 ... [0x1000921c0] 0x104c421c0 lsr w10, w10, #0x10; # x10: 0x2d3737af --> 0x2d37, [0x1000921c4] 0x104c421c4 and w10, w10, #0xff; # x10: 0x2d37 --> 0x37, //37值的由來
發現兩個值都是從0x2d3737af偏移而來,而0x2d3737af也是魔改后碼表里的值dword_106EF9168[v48];,也就是說真正要看的是v48如何等于5a。跟到5a最初被賦值的地方ldurb w10, [x29, #-0x14]; # x10: 0x6d --> 0x5a發現是從x29-0x14的地方取值,按正常邏輯,只要搜,誰往[x29, #-0x14]的地方賦值就行,不過trace里搜不到。所以到這里差不多就比較懵,值跟不下去了。
于是我又換了另一種猜想,如果v44不是一個char數組呢?假設他是一個int指針,那么如果v48=*(v44+1),那么v48的值應該是v44往后偏移4個字節,于是查看完整的v44: 4c da e9 c4 5a a1 0f 28 1e a2 01 ed 5b b6 62 b9,第一個值是4c沒問題,第一個四組驗證過了。如果按照剛剛的猜想,往后偏移4個字節,那么v48應該是?5a!發現對上了!那趕緊趁熱打鐵,驗證接下來的兩個值是不是1e和5b,也就是v49=1e,v50=5b。查看下trace。
//trace [0x100092210] 0x104c42210 ldurb w10, [x29, #-0x10]; # x10: 0xaf --> 0x1e, [0x100092214] 0x104c42214 mov x11, x10; # x11: 0x10baa92d0 --> 0x1e, ... [0x1000922b8] 0x104c422b8 ldurb w10, [x29, #-0xc]; # x10: 0xd2 --> 0x5b, [0x1000922bc] 0x104c422bc mov x11, x10; # x11: 0x10baa92d4 --> 0x5b,
漂亮!那么到這里我心中大概有點數了,接下來的4組應該是繼續從下一個偏移0xda開始,然后分別使用da,a1,a2,b6。也即他把這個16字節的數組"立"了過來。

然后繼續驗證,發現猜想中本應是da的值,但是卻變成了a1?本應是a1的值,卻變成了a2?
//trace 碼表的index [0x100092320] 0x104c42320 ldurb w10, [x29, #-0x13]; # x10: 0xfa --> 0xa1, ... [0x1000923a8] 0x104c423a8 ldurb w10, [x29, #-0xf]; # x10: 0x9e --> 0xa2, ... [0x100092430] 0x104c42430 ldurb w10, [x29, #-0xb]; # x10: 0xf1 --> 0xb6, ... [0x1000924b8] 0x104c424b8 ldurb w10, [x29, #-0x17]; # x10: 0x1 --> 0xda,
咦?這難道,就是AES的行移位算法?!沒錯。
我們最初介紹了aes的標準流程時,提到了aes的內部小算法,這個就是行移位算法。aes將16個字節先看成是一個4*4的矩陣,然后分別對矩陣進行變化,所謂的行移位算法也是固定的一種模式,如下圖:

也就是說,我們這16個字節真正的使用方式是,先進行ShiftRows行移位,然后在進行SubBytes字節替換(魔改碼表里取值),這也是bangcle_laes的一個混合小算法。
那么我們還原算法時,就要自己寫一個行移位了,而之前猜想v44是一個int指針也完全不對,他仍是一個char指針,只不過取值之前,已經對里邊的內容進行了變換!
其次在將行移位后的矩陣進行轉置。

因此,我們還原算法時,就可以按照標準aes那樣,先將16字節轉成一個4*4的矩陣,然后對矩陣進行行移位操作等變換。
//clion算法還原部分
uint8_t *shiftp = (uint8_t *)*v44;
uint8_t state[4][4] = {0};
//轉成二維數組state
convert_array(shiftp,state);
///進行行移位
ShiftRow(state);
uint8_t * p = (uint8_t *)state;
最終在內存里的格式變換為:
(lldb) x *v44 0x1327041c0: 4c da e9 c4 5a a1 0f 28 1e a2 01 ed 5b b6 62 b9 L???Z?.(.?.?[?b? --> (lldb) x p 0x16f18b2a8: 4c 5a 1e 5b a1 a2 b6 da 01 62 e9 0f b9 c4 28 ed LZ.[????.b?.??(?
還原過程中最困難的部分已經完成,其余部分按照之前的思路也都可以對照,結果不對就跟trace分析。
至此,9輪循環里的混合算法還原完畢。
其實到這里,雖說算法還原成功,但是過程卻極其艱難。我也抱怨過ida里為什么不把算法的過程表現出來呢?看來還是ida反匯編有問題?這確實是我當時的疑惑。后來與Virenz大佬討論一番,發現并不是ida沒有表現,而是因為你并不理解ida的"想法"。

回過頭來看,發現ida早已清清楚楚的告訴了你,雖然他不能精準的將代碼全部還原,僅僅以一些局部變量表示。但是他會告訴你他反匯編的內在邏輯。比如上圖中可分為兩塊,v28~v43這16個字段是順序的,通過結果來看其在內存中也是連續的,也即可以表示為一個數組。
再比如v44,v48,v52這些碼表里的索引值,也都清清楚楚告訴你他們的關系,仔細觀察的話,其實是可以看出行移位的,內存值里為:
v44 v45 v46 v47 v48 v49 v50 v51 v52 v53 v53 v55 v56 v57 v58 v59
上圖第1塊里取的4個索引為,v44,v48,v52,v56
到了第2塊里取的4個索引為,v49,v53,v57,v45
可以明顯發現,的確進行了行移位操作。如果還原時能了解這一點,可能就不用費勁追trace,或許只用看也能看出大概了。
4.3.3 還原最終輪算法
通過上邊的分析,這里其實也大差不差,唯一有些注意的點就是看好每一步小算法的入參出參,也就是誰進行了運算,又返回給了誰。
最后我們對照下完整算法的返回結果:
(lldb) x result 0x1327047b0: 77 1a 85 29 14 b6 3c 51 c5 5c b2 2d 52 19 ce 73 w..).?
還是得益于frida trace時的定制化,我們可以直接在結果中搜即可。

也就證明了算法還原成功。
4.4 算法回顧
通過上邊的分析,發現這個bangcle的AES魔改的很厲害,基本就是一個AES的架子,內部已經完全混亂了。其次還有他的密鑰key我們還沒有分析,也在這里說明下。我們都知道,正常的AES key是16位的,他的主要作用就是在AES算法中進行AddRoundKey(輪密鑰加)的過程。
AddRoundKey的算法就是將16字節的“輸入”與16字節子密鑰進行異或得到輸出數據,而子密鑰的獲取是通過密鑰拓展編排算法得來(密鑰編排算法就不做過多介紹,較復雜)。從之前的AES算法流程中也可以看到,從初始變換到10輪加密計算,總共用到了11次AddRoundKey,也就是說,密鑰擴展編排后,總共會占11*16=176個字節的內存空間。
而bangcle的AES原本傳入的密鑰就是180位的,也可以說,他把密鑰編排的算法前移了。那可能有小伙伴就問了,你不是說密鑰編排后,總共是176位嗎,那多出來的4位呢?其實在進行加密算法前,他也對密鑰key進行了處理。我們看下相關的計算。

可以清楚的看到,他key的前4位( key[i%3] )實際上是用于"解密"后邊176位的密鑰,也就是說原始的key實際是加密(異或)過的。這樣做的目的,我也只是有個猜想,那就是他解密后的176字節的key真的是用密鑰編排算法算出來的,而不是沒有規則的key。因為密鑰編排算法編出來的子密鑰,實際上是能逆推出主密鑰的,有興趣的小伙伴可以去了解下DFA差分故障攻擊的原理,也是會用到這一點。
至此,我們完整的分析并還原了魔改的aes算法,想必如果這個算法再加了混淆,難度可想而知。
五、總結
到這里,本文也已經結束了,也許各位已經看的很累了,但總之還是希望對你有所幫助!本文的樣本相信仔細看的小伙伴都能看出是哪個app,想練手的話就在AppStore下載最新版就行。
最后感謝觀看,謝謝!
安全圈
看雪學苑
安全牛
嘶吼專業版
FreeBuf
安全圈
安全牛
嘶吼專業版
商密君
看雪學苑
安全圈
黑白之道