一、前言

從事移動安全行業以來,一直在做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下載最新版就行。

最后感謝觀看,謝謝!