什么是去花指令的最高境界?莫過于去花之后替換進apk中,依然正常運行,這對匯編功底無疑是一種挑戰。

今天就獻丑拿某流量第一的APK樣本做一下IDA腳本一鍵去花指令分析。

獻上對比圖:

當然還有替換進去的運行圖,替換進去apk運行不成功那算什么去花。

講完效果就開干!

簡單分析

◆異常B指令 -寄存器間接跳轉

分析一下,使用了寄存器間接跳,干擾反匯編引擎讓其無法正確函數尾部,也不能什么都指望IDA。

◆上IDA腳本,此代碼為片段代碼

ARM64(也被稱為AArch64)是ARM架構的64位版本。ARM64的調用約定定義了函數如何傳遞參數和返回結果。在ARM64的調用約定中,參數是通過寄存器和堆棧傳遞的,具體如下:

參數傳遞:前八個整型或指針類型的參數通過寄存器X0至X7傳遞。前八個浮點型參數通過寄存器V0至V7傳遞。如果函數有更多的參數,那么超出的參數將通過堆棧傳遞。

返回值:函數的返回值通過寄存器X0(和X1,如果需要)或者V0(浮點數和SIMD類型)返回。

保留寄存器:某些寄存器在函數調用中需要被保留。這意味著如果函數修改了這些寄存器的值,那么它需要在返回前恢復它們的原始值。這些寄存器包括:X19至X28,以及棧指針SP。

調用者保存寄存器:一些寄存器在函數調用前后的值可以不同,如果調用者函數希望保留這些寄存器的值,那么它需要在調用其他函數前保存這些寄存器的值。這些寄存器包括:X0至X18,和所有的V寄存器(V0至V31)。

由上文可知,函數在開頭必然要分配函數所需的棧空間以及保存調用者保存寄存器,并且在函數尾部恢復保存寄存器以及棧空間,這就是下圖腳本原理。

ins_map = {"sub": "add", "str": "ldr", "stp": "ldp"}#因為入棧出棧是對應的 所以我們只需要吧 基礎的三個對應opcode替換下就可以
def find_func_end(self):
        encodings = [0xC0, 0x03, 0x5F, 0xD6]  # ret 的指令編碼
        for i in md.disasm(ida_bytes.get_bytes(self.func_start, 12), 0): #arm64前三條指令分配棧空間保存寄存器
            if i.op_str.find('!') == -1:
                encodings = ks_disasm(self.get_inv_opcode(i)) + encodings
            else:
                s_new = i.op_str.replace("]!", "")
                s_new = s_new.replace(", #-", "],")
                dis_str = '{} {}'.format(self.ins_map.get(i.mnemonic), s_new)  # 調用約定的棧平衡指令
                encodings = ks_disasm(dis_str) + encodings
        opcodelist = list(ida_bytes.get_bytes(self.func_start, 0x8000))
        index = find_sublist(opcodelist, encodings)
        self.func_end = self.func_start + index  # 函數結尾的地址
        print("func end addr : ", hex(self.func_end))

由圖中我們也能看出這是標準的入棧出棧格式,運行后得到正確的函數尾部地址 0x32ea8。

但是當我們強行把JNI_OnLoad的函數結尾地址改成0X32EA8是不行的,因為IDA還是有點倔脾氣的,必須讓他心服口服的認為才行!

◆讀匯編

現在找到了正確的函數尾部地址,那么經過上上圖我們看到0X32804明顯是個不應該在函數中出現的函數,但是他確實有完整的棧平衡格式,這個有完美起跳動作的狼人我們應該怎么處理呢?

SP+8???經過分析這個函數返回X0竟然是LR寄存器,哦!明白了,小狼一頭!

手動計算下LR的地址明顯是0x327FC + 0x34 =0x32830那么BR X1的位置就是0x32830。

看下0x32830地址的OPCDE OMG嵌套狼-

 
SVC             0
 DCD 0x477001DE
   ****
 BL              sub_32804
 ADD             X6, X0, #8
 BR              X6
  ****
 B.NE            loc_32858
 CLREX
 BRK             #3

明顯的花指令嘍,CPU要是執行了SVC不得崩潰才怪,但是花指令花就花在它執行不到,這這這!那就處理唄,反正能算出他的真實執行地址,把能NOP的都NOP唄。

◆方案1 Unicorn

其實我是寫了兩個版本的,最后Unicorn arm32版本被我PASS掉了,因為都搞64了,但是放個圖紀念一下。

◆方案2 大膽干,早點散

經過分析所有的基礎花都是基于那個獲取LR的函數來的,那就通過他定位花的位置,直接nop其位置就得了。如果在函數中找到特征碼存在的地方,會自動遍歷其上下N個地址,直到找到花特征進行NOP。

這樣就可以了嗎?

經過處理并且刪除掉內部函數以及無用分支流我們確實得到了一個能夠F5的函數,但是這對我們的要求來說遠遠不夠,我們的要求可以替換進真機正常運行!

現在F5正常,但是為什么我們替換到真機里就閃退呢?

無非兩個問題,第一就是花指令去除到了真實代碼塊,二就是檢測了代碼塊是否被更改!

◆動態排查

那就調試唄,首先找到崩潰位置,跟就完了。

根據筆者跟了百條指令,發現崩潰點位在:

竟然把0給了SP寄存器,并且循環清空棧空間,這不崩才怪!

◆原理分析

其實如果安全人員想讓它崩潰有更簡單的辦法,但是為什么用這種方法呢,大家可以看到,這一系列的操作是為了清空原本的棧空間,那么棧空間有什么不可告人的秘密呢?如果熟悉匯編代碼,畫過堆棧圖的大佬們應該知道,棧空間可是保存著完整的調用鏈條的,這也是frida之類打印調用堆棧的原理,當我們調試的時候真機崩潰了,打印出的調用堆棧確是1-2-3-4-5-6那你懵不懵呢?

放個對比圖-上面的是正常的崩潰截圖,frida可以準確的打印出崩潰地址!可以更清晰的供調試人員分析是因為那里崩潰,方便排查!同時,這也方便了逆向人員定位檢測代碼!

下面的就是清空棧空間后的崩潰截圖,什么都打印不出來就直接Crash掉了。

◆向上溯源

繼續排查發現,樣本對代碼塊進行了移位亦或操作。

因為v14指向的是本函數的代碼位置,所以當我們開心的NOP掉時,必然觸發了它的檢測機制,這才是隱藏在最后的狼人,讓我們干掉它把。

直接讓他RET就好,pathch一個函數也不好玩,把整個so patch了把,RegisterNatives成功得到 !一個1000kb多的so,500kb都是業務無關代碼,冤冤相報何時了,不抗了不抗了。