在工作中,多多少少會遇到花指令的情況,本系列文章我會慢慢更新,把我學習過程分享給大家,從簡單的構造到如何結合手頭的工具patch。

什么是花指令

洋文叫:Junk Code 臟指令 主要作用就是用來干擾,反匯編引擎(線形掃描&遞歸下降) 和 F5 增加逆向(PJ)難度的一種技術,能屏蔽大部分腳本小子,這些指令CPU也不認識,執行到這種指令CPU直接會報錯,如何不影響CPU正常執行又能干擾反匯編引擎的花指令呢。

花指令種類與如何簡單構造

花指令的種類有很多,這里只討論移動端或者說ARM平臺,哪些團隊在用花指令呢我的知識范圍內發現的的商業級樣本中,魔獸世界懷舊服macOS版某泔水團等等等等........ 大家可以后續補充。

這里為什么沒有研究x86,第一因為我不會第二個原因是x86是非定長指令集,這會導致x86有那種雙字節的花指令不適合線形掃描,要校驗的太多了無法通過定長4字節的方式進行線性掃描。

ARM64指令集天生就是定長4字節,這里可能有人會杠ADRL x0, unk_1023d55018字節你怎么說,其實這是兩條指令adrp x0, #0xcadd x0, x0, #0x501。

第一章簡單的花指令構造與修正

◆example1

                                      _main:
0x0000000100003c84 FF8300D1               sub        sp, sp, #0x20
0x0000000100003c88 FD7B01A9               stp        fp, lr, [sp, #0x10]
0x0000000100003c8c FD430091               add        fp, sp, #0x10
0x0000000100003c90 08008052               mov        w8, #0x0
0x0000000100003c94 FF0B00B9               str        wzr, [sp, #0x8]
0x0000000100003c98                        db  0xb1 ; '.'
0x0000000100003c99                        db  0x7f ; '.'
0x0000000100003c9a                        db  0x39 ; '9'
0x0000000100003c9b                        db  0x05 ; '.'
0x0000000100003c9c                        db  0x4e ; 'N'
0x0000000100003c9d                        db  0x61 ; 'a'
0x0000000100003c9e                        db  0xbc ; '.'
0x0000000100003c9f                        db  0x00 ; '.'
0x0000000100003ca0 29048052               mov        w9, #0x21

上邊的樣本是我自己構造的簡單花指令樣本,可以看到 0x100003c98-0x100003c9f 無法被反匯編引擎正常解析并標記為數據db,這個樣本是在Hopper中解析的,在ida pro呈現的是:DCB/DCW/DWD/DCQ 這里簡單理解他們都是數據只是大小不同 B = byte,W= word (2bytes),D = dword(4bytes),Q = qword(8bytes)

ARM文檔(https://developer.arm.com/documentation/dui0473/m/directives-reference/dcb?lang=en)。

QA:那問題來了數據不是有數據段么,怎么出現在代碼段了?

DCB/DCW/DWD/DCQ 在ARM中表示定義數據的指令,正常的代碼經過編譯器怎么能犯這種錯誤呢,問題就出在二進制工程師通過某種手段構造出來故意干擾反匯編引擎腳本小子下邊公布代碼:

◆main.cpp

 
/****
   *@author 周樟壽
   */
    #include 
    
    /****
     * __attribute__((always_inline))  內聯函數
     * @return
     */
    static __attribute__((always_inline)) int case1() {
    
    #if __arm64__
         __asm__(
            "udf #0x5397FB1"
            ".long 87654321"
            ".long 12345678"
        );
    #endif
        int b = 1 * 3 + (5 * 6);
    
        return b;
    }
    int main() {
    int c = case1();
    std::cout << (c) << std::endl;
    return 0;
}

這個例子是無法正常運行執行的,因為cpu執行到了我們通過內聯匯編構造的非法指令,這里使用了兩種方式來構造非法指令。

◆.long 表示聲明是數據即我們看到DCQ

◆udf 字面意思就是未定義 具體請看文檔官方文檔-udf(https://developer.arm.com/documentation/dui0801/h/A32-and-T32-Instructions/UDF)

有了基礎知識,我們先想辦法修正這類這里使用ida python進行修正。

◆fix_example_junk_code.py

    
""
    @author 周樟壽
    ""
    import ida_bytes
    import idautils
    import idc
    arm64_nop = b"\x1f\x20\x30\xd5"
    
    if __name__ == '___main__':
        print("fix_example_junk_code.py begin.....")
        segments = idautils.Segments() #獲取所有段
        for set in segments:
            #只處理 text 段  這個腳本支持 so 和 match-o
            if idc.get_segm_name(seg) in ['__text','.text']
                #開始
                seg_start_ea = idc.get_segm_start(seg)
                #結束
                seg_end_ea = idc.get_segm_end(seg)-4
                
                #臨時變量
                current_ea = seg_start_ea
                while current_ea <= seg_end_ea:
                    #指令大小對于非指令無效
                    item_size = idc.get_item_size(current_ea)
                    #是否是asm 指令判斷
                    is_asm_code = ida_bytes.is_code(ida_bytes.get_flags(current_ea))
                    if not is_asm_code:
                        # patch nop
                        ida_bytes.patch_bytes(current_ea,arm64_nop)
                        current_ea = current_ea + 4
                    else:
                        current_ea = current_ea + item_size
                    
        print("fix script end success ........")           

◆第一章遺留下了一個問題就是,如何構造不影響cpu執行指令的花指令。

第二章構造可用的花指令: B+junkCode

這個小節需要解決的是讓CPU正常執行并且還能干擾反匯編引擎&F5,先看例子講解原理。

◆case2

/****
   *@author 周樟壽
   */
    #include 
    
    /****
     * __attribute__((always_inline))  內聯函數
     * @return
     */
    static __attribute__((always_inline)) int case2() {
    
    #if __arm64__
         __asm__(
            "b #0x10"
            "udf #0x5397FB1"
            ".long 87654321"
            ".long 12345678"
        );
    #endif
        int b = 1 * 3 + (5 * 6);
    
        return b;
    }
    int main() {
    int c = case2();
    std::cout << (c) << std::endl;
    return 0;
}

這個例子與第一個例子最大的不同就是,增加了一個B指令來讓CPU正常執行跳過我們精心構造的花指令,下面聊聊為什么用B指令和 為什么后邊的立即數是#0x10。

◆為什么用B指令 文檔(https://developer.arm.com/documentation/ddi0596/2021-12/Base-Instructions/B--Branch-?lang=en)

使用B指令是因為,B指令的尋址方式是相對尋址,相對于PC寄存器進行的,其次B是無條件跳轉無需關心LR寄存器。

◆B 指令后邊的立即數

立即數為什么是0x10這里先把轉成10進制等于16這里要回顧一下PC寄存器的知識,PC寄存器永遠存儲的是需要執行下一條指令的地址,這里就會衍生出來一個公式來計算具體跳多遠,公式: 立即數=(填充指令數量+1) * 4,套用上邊例子最終結果 16=(3+1) * 4。

◆結語

這種簡單類型的修正思路就是,發現B指令,先計算出立即數,然后判斷判斷一條是否為指令集,然后輪訓判斷下去保存總指令大小,如果發現立即數=我們掃描的區域大小直接全nop掉,

下一章我直接根據實際樣本進行學習以及修正。

◆修正腳本

fix_case2.py

""
@author 周樟壽
""
import ida_bytes
import idautils
import idc
arm64_nop = b"\x1f\x20\x30\xd5"
def get_range():
    segments = idautils.Segments()  # 獲取所有段
    for seg in segments:
        # 只處理 text 段  這個腳本支持 so 和 match-o
        if idc.get_segm_name(seg) in ['__text', '.text']:
            # 開始
            seg_start_ea = idc.get_segm_start(seg)
            # 結束
            seg_end_ea = idc.get_segm_end(seg) - 4
            break
    return seg_start_ea, seg_end_ea
if __name__ == '___main__':
    print("fix_example_junk_code.py begin.....")
    # 開始
    seg_start_ea, seg_end_ea = get_range()
    # 臨時變量
    current_ea = seg_start_ea
    while current_ea <= seg_end_ea:
        # 指令大小對于非指令無效
        item_size = idc.get_item_size(current_ea)
        # 是否是asm 指令判斷
        is_asm_code = ida_bytes.is_code(ida_bytes.get_flags(current_ea))
        if not is_asm_code:
            current_ea = current_ea + 4
        else:
            # 判斷是否是指令B
            if idc.print_insn_mnem(current_ea) in ['b', 'B']:
                # 獲取B 后邊的目標地址   立即數=目標地址-當前地址
                b_imm = idc.get_operand_value(current_ea, 0) - current_ea
                # 判斷指令B下邊是否是指令不是指令開始非指令區域大小
                if not ida_bytes.is_code(ida_bytes.get_flags(current_ea + 4)):
                    data_size = 0
                    begin_ea = current_ea + 4
                    while True:
                        if not ida_bytes.is_code(ida_bytes.get_flags(begin_ea)):
                            data_size = data_size + 4
                            begin_ea = begin_ea + 4
                        else:
                            break
                    if data_size == b_imm:
                        # todo not
                        print("nop fix")
            current_ea = current_ea + item_size
    print("fix script end success ........")

花指令的剩余套路簡介與基本構造

看了這么多代碼我相信老鳥都看煩了,不拖拉直接把基礎剩余的花指令套路大概過一遍直接上真實樣本分享我的修正思路,大家有更好的思路也可以交流。

花指令的類型還有以下幾種:

1.虛假控制流+ DCQ

2.虛假控制流 + B + 棧不平

3.B + 棧不平

4.利用x30寄存器進行 RET 跳轉

還有很多精心構造的下邊我給出具體構造的例子,不會再寫具體修正腳本大家可以自己試著去修正一下。

◆虛假控制流+B 樣本.cpp

 
/****
   *@author 周樟壽
   */
int bcf(int a) {
    return (a + a);
}
static __attribute__((always_inline)) int case3() {
    int a = 3;
    if (bcf(a) != 0) {
    #if __arm64__
        __asm__(
        "b 0xc"
        "udf #0x5397FB1"
        ".long 87654321"
        ".long 12345678"
        );
    #endif
    }
    int b = 1 * 3 + (5 * 6);
    return b;
}

這只是簡單的例子虛假控制流按我這個寫法會被編譯器優化掉實際情況遠比這復雜的多,僅供參考。

◆B + 棧不平

static __attribute__((always_inline)) int case4() {
    int a = 3;
    if (bcf(a) != 0) {
#if __arm64__
        __asm__(
        "b 0xc"
        "add sp,sp,#0x100"
        "add sp,sp,#0x100"
        );
#endif
    }
    int b = 1 * 3 + (5 * 6);
    return b;
}

例子和構造樣本就先簡單到這里我們直接結合,真實樣本樣本學習其中的套路。

開水團花指令學習與修正 xxxx.so & xxxx

看開水團系列的應用我是純抱著學習態度去的,因為平時也用但是覺得卡卡的偶爾還非常熱抱著好奇心的態度去學習,你別說有點東西下邊開始分享我的學習過程,盡量分享的細致一點和我的思路,這個學習記錄沒有參考別人的修正方案按自己的理解進行修正,稱不上完美但是可以f5了看起來清爽一點,如果哪里有腳本上的優化大家可以交流集思廣益。

◆樣本總結

包含的種類非常多我只修正了我關心的部分會講解修正過程以及他們之間的關系,開水團的花指令是有關聯的屬于嵌套關系而且還夾雜了DCQ這種數據,其中BR x8這種動態跳轉我沒有仔細去看不在我的修正范圍。

◆搜索方向

安卓從ida pro的export 窗口看 JNI_onLoad 開始看起就能發現花指令的存在了,一直跟著向下即可,同Group產品我也都看了雙平臺的花指令套路是一致的,大家不要去看iOS端修起來很費時間,如果學習盡量看安卓下邊正式開始吧。

◆樣本1之 利用 x30 (LR) 寄存器 + RET 強制 停止函數

在點進去JNI_onLoad就會看到如下:

.text:14580 E0 7B 3E A9                   STP             X0, X30,[SP,#var_20]
.text:14584 01 00 00 94                   BL              sub_14588
.text:14584                               ; End of function JNI_OnLoad
                                          sub_14588
.text:14588 60 00 00 10                   ADR             X0, loc_14594
.text:1458C FE 03 00 AA                   MOV             X30, X0
.text:14590 C0 03 5F D6                   RET

這個樣本很有意思,第一利用了ADR地址無關性把目標地址放進了X0寄存器,第二利用 X30寄存器也就是LR寄存器直接跳了過去,RET其實等同于mov pc,lr那如何修正呢,先人工修正一下看看。

修正.asm

.text:14580 E0 7B 3E A9                   STP             X0, X30,[SP,#var_20]
.text:14584 01 00 00 94                   BL              sub_14588
.text:14584                               ; End of function JNI_OnLoad
                                          sub_14588
.text:14588 60 00 00 10                   nop             
.text:1458C FE 03 00 AA                   nop             
.text:14590 C0 03 5F D6                   B      loc_14594

這里手工修正,修正了3條指令核心就在RET修正B 目標地址,這里其實有朋友會說為什么不在BL sub_14588這里修正B loc_14594因為實際情況遠比想象的復雜得多,這個ret中間的位置可能會出現 sp棧相關操作要不要保留?不清楚干嘛的都要保留,我直接放出修正腳本寫的比較low大家別噴有些沒找到函數,我就直接手工操作的。

◆修正.py

 /****
   *@author 周樟壽
   */
import ida_bytes
import idautils
import idc
import re
from keystone import *
if __name__ == '__main__':
    """
        初始化匯編 引擎 keystone
        """
    ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
    arm64_nop = b"\x1f\x20\x03\xd5"
    segments = idautils.Segments()
    for seg in segments:
        """這里只處理 代碼段"""
        x_30_ret_total = 0
        if idc.get_segm_name(seg) in ["__text", ".text"]:
            """獲取段信息"""
            seg_name = idc.get_segm_name(seg)
            seg_start_addr = idc.get_segm_start(seg)
            seg_end_addr = idc.get_segm_end(seg) - 4
            seg_size = seg_end_addr - seg_start_addr
            current_ea = seg_start_addr
            while current_ea <= seg_end_addr:
                item_size = idc.get_item_size(current_ea)
                is_asm_code = ida_bytes.is_code(ida_bytes.get_flags(current_ea))
                if is_asm_code:
                    current_mnem = idc.print_insn_mnem(current_ea)
                    """
                    優化修復邏輯 里邊有很多地方都是 通過地址-4 這種計算方式來判斷的,遇到了特殊情況就無法搜索
                    需要向上逐4 字節搜索
                    
                    """
                    new_fix = True
                    if new_fix:
                        """
                        這是原始模板,但是存在中間穿插 棧平衡相關得指令無法使用 精確 -4  判斷 第一個操作數是 x30 
                        和 -8 判斷 mnem 是 adr
                        adr x0 ,#0xdc
                        mov x30 x0
                        ret
                        
                        這里優化搜索規則  當前是RET 指令向上搜索, 如果向上搜索 4-8 字節 發現有操作 X30 (LR) 寄存器 就標
                        標記好了,x30 的 操作位置,繼續向上搜索 查找ADR 指令 找到要跳轉 得目標
                        """
                        if current_mnem == 'RET':
                            if ((idc.print_operand(current_ea - 4, 0) == 'X30') and (
                                    idc.print_insn_mnem(current_ea - 4) == 'MOV')) \
                                    or \
                                    (idc.print_operand(current_ea - 8, 0) == 'X30' and idc.print_insn_mnem(
                                        current_ea - 8) == 'MOV'
                                    ):
                                # (
                                #         (idc.print_operand(current_ea - 4, 0) == 'X30') and (
                                #         idc.print_insn_mnem(current_ea - 4) == 'MOV')
                                # ):
                                if idc.print_insn_mnem(current_ea - 4) in ['ADD', 'MOV']:
                                    print(
                                        f'current {hex(current_ea)} x30_ret  sub_4={idc.print_insn_mnem(current_ea - 4)}')
                                    """統計"""
                                    x_30_mov = 0
                                    x_30_adr = 0
                                    """
                                    nop ret
                                    """
                                    # ida_bytes.patch_bytes(current_ea, arm64_nop)
                                    location_addr = 0
                                    if idc.print_insn_mnem(current_ea - 4) == "MOV":
                                        """
                                        nop mov
                                        """
                                        # ida_bytes.patch_bytes(current_ea, arm64_nop)
                                        # ida_bytes.patch_bytes(current_ea - 4, arm64_nop)
                                        """
                                        用于確定具體跳轉目標得指令位置
                                        """
                                        location_addr = current_ea - 8
                                    elif idc.print_insn_mnem(current_ea - 4) == "ADD":
                                        # ida_bytes.patch_bytes(current_ea - 8, arm64_nop)
                                        location_addr = current_ea - 12
                                    else:
                                        print(f'未知:{hex(current_ea)}')
                                    if location_addr != 0:
                                        try:
                                            b_number = int(
                                                re.search(r'[0-9A-F]+', idc.print_operand(location_addr, 1)).group(),
                                                16)
                                            path_asm = f'b #{hex(b_number - current_ea)}'
                                            print(
                                                f' address:{hex(location_addr)}   operand:{hex(b_number - current_ea)}  asm:{path_asm}')
                                            encoding, count = ks.asm(path_asm)
                                            patch_byte = bytes([int(i) for i in encoding])
                                            print(f"------------------->{path_asm} = [", end='')
                                            for i in encoding:
                                                print("%02x " % i, end='')
                                            print(']')
                                            """
                                            修正跳轉
                                            """
                                            ida_bytes.patch_bytes(location_addr, arm64_nop)
                                            """
                                            修正ret
                                            """
                                            ida_bytes.patch_bytes(current_ea, patch_byte)
                                            if idc.print_insn_mnem(current_ea - 4) == "MOV":
                                                """
                                                nop mov
                                                """
                                                # ida_bytes.patch_bytes(current_ea, arm64_nop)
                                                ida_bytes.patch_bytes(current_ea - 4, arm64_nop)
                                            elif idc.print_insn_mnem(current_ea - 4) == "ADD":
                                                ida_bytes.patch_bytes(current_ea - 8, arm64_nop)
                                        except Exception as e:
                                            print(f'出錯地址:{hex(location_addr)}')
                                        #
                                        # asm_number = b_number - location_addr
                                        #
                                        # path_asm = f'b #{b_number}'
                                        # encoding, count = ks.asm(path_asm)
                                        # patch_byte = bytes([int(i) for i in encoding])
                                        # print(f"------------------->{path_asm} = [", end='')
                                        # for i in encoding:
                                        #     print("%02x " % i, end='')
                                        # print(']')
                                        #
                                        # ida_bytes.patch_bytes(location_addr, patch_byte)
                                    x_30_ret_total = x_30_ret_total + 1
                    if not new_fix:
                        """如果之 這種花指令跳轉規則 """
                        if (current_mnem == 'RET') and (idc.print_operand(current_ea - 4, 0) == 'X30') and (
                                idc.print_operand(current_ea - 4, 1) in ['X0', 'W0']):
                            """當前指令-8"""
                            sub_8_mnem = idc.print_insn_mnem(current_ea - 8)
                            sub_8_imm = 0
                            b_imm = 0
                            if sub_8_mnem == "ADR":
                                has_patch = False
                                sub_8_imm = idc.print_operand(current_ea - 8, 1)
                                sub_8_imm = int(re.search(r'[0-9A-F]+', sub_8_imm).group(), 16)
                                """
                                計算地址
                                立即數 = 目標地址-當前地址
                                
                                """
                                b_imm = sub_8_imm - (current_ea - 8)
                                if has_patch:
                                    ida_bytes.patch_bytes(current_ea, arm64_nop)
                                    ida_bytes.patch_bytes(current_ea - 4, arm64_nop)
                                path_asm = f'bl #{b_imm}'
                                encoding, count = ks.asm(path_asm)
                                patch_byte = bytes([int(i) for i in encoding])
                                print(f"------------------->{path_asm} = [", end='')
                                for i in encoding:
                                    print("%02x " % i, end='')
                                print(']')
                                if has_patch:
                                    ida_bytes.patch_bytes(current_ea - 8, patch_byte)
                            """統計"""
                            x_30_ret_total = x_30_ret_total + 1
                            print(
                                f'address:{hex(current_ea)}  sub_8_mnem[{sub_8_mnem}]  sub_8_imm={hex(sub_8_imm)}  b_offset:{hex(b_imm)}')
                """skip"""
                current_ea = current_ea + item_size
            print(f"total all ret30 total:{x_30_ret_total}")

整體腳本實現相對來說比較啰嗦,我講究的是能用就行剩下的交給GPT幫我優化,腳本包含我剛開始的修正代碼,當時修正的是錯誤的大家看看就好了。

◆第二種花指令,是隱藏真實跳轉地址,并交給中央轉發器統一分發

以前沒見過就是覺得非常厲害大概的跳轉思路設計的很精妙,這也導致了我剛開始無腦NOP陷入了痛苦深淵,直到寫這篇文章的時候有開源項目實現過這種隱藏真實跳轉的LLVM項目(https://github.com/amimo/goron),寫這篇文章的時候說實話沒有看過他的核心思想就是根據樣本學習和動態調試,一步一步扣出來的,下邊結合樣本和具體修正思路來看比較好,樣本種類很多大家慢慢看別急。

◆樣本1

.text:14594 E0 7B 7E A9                   LDP             X0, X30, [SP,#-0x20]
.text:14598 E0 7B 3B A9                   STP             X0, X30, [SP,#-0x50]
.text:1459C 20 00 80 D2                   MOV             X0, #1
.text:145A0 02 00 00 14                   B               loc_145A8
.text:145A4 06 00 00 00                   DCD 6
.text:145A8 15 02 00 94                   BL              sub_14DFC
.text:145AC 14 00 00 00                   DCD 0x14
.text:145B0 30 00 00 00                   DCD 0x30
.text:145B4 8C 00 00 00                   DCD 0x8C
.text:145B8 B8 00 00 00                   DCD 0xB8
.text:145BC 1F 20 03 D5                   DCD 0xD503201F

◆中央轉發器

.text:14DFC E0 07 BF A9                   STP             X0, X1, [SP,#var_10]!
.text:14E00 C0 5B 60 B8                   LDR             W0, [X30,W0,UXTW#2]
.text:14E04 DE 43 20 8B                   ADD             X30, X30, W0,UXTW
.text:14E08 E0 07 C1 A8                   LDP             X0, X1, [SP+0x10+var_10],#0x10
.text:14E0C C0 03 5F D6                   RET

這要結合中央轉發器來看,先說中央轉發器都干了什么里邊通篇就只做了一個運算操作,最終跳出到目標地址依然利用了 RET X30 的特性這里不過多介紹,核心突出的就是計算和跳轉不要關心STPLDP。

◆計算就干了一件事情 公式如下 x30=x30+load(x30+(x0<<2))

公式是這樣,讀懂需要看上下文剛開始我也很懵,最后調試幾遍就發現了規律,首先看x30x0的值是這么來的計算一遍看看。

先看X30(LR)已知條件是LR寄存器永遠指向下一條要執行的指令地址拿樣本1中來看,當前

X30=0x145AC,為什么因為調用中央轉發器用的是BL有返回的跳轉。

再看x0的值,向上溯源調用中央轉發器前就有一條MOV X0, #1這個#1是什么,這里可以理解為跳轉編號,大家可以找到中央轉發器看一下XREF引用每個調用前都會有 X0或者W0的立即數賦值操作。

◆tips

大家要注意具有迷惑性的0x145A4 DCD 0x6實際樣本中還有一種 x0 賦值方式就是

LDR w0,0xc這并不是直接把0xc賦值給 w0,LDR是基于 PC寄存器的一種地址無關性的讀取操作,說人話的意思就是加載 pc寄存器地址+0xc 存放的內容放到 w0寄存器中具體會在腳本中體現因為要考慮到。

◆根據樣本我們實際算一遍就明白了

X30 = 0x145AC +Load(0x145AC+(1<<2))
#這一步只是地址計算
0x145AC+0x4 = 0x145B0
#加載這個地址的值
Load(0x145B0)=0x30
#實際得出的地址
0x145AC+0x30 = 0x145DC

◆核心思想與總結

◆利用中央轉發起進行x30 條轉,配合花指令存儲偏移傳遞不同編號進行跳轉,干擾編譯器。

◆利用花指令保存真實跳轉偏移植,屏蔽腳本小子無腦NOP,讓花指令數據變得有意義。

◆樣本都嵌套的一層嵌套一層。

具體修正與腳本

實際修正腳本也是寫了非常多,還有很多情況需要考慮還好我都寫了中文注釋。

find_中央轉發器.py

 /****
   *@author 周樟壽
   */
import ida_bytes
import idautils
import idc
if __name__ == '__main__':
    segments = idautils.Segments()
    for seg in segments:
        """這里只處理 代碼段"""
        if idc.get_segm_name(seg) in ["__text", ".text"]:
            seg_name = idc.get_segm_name(seg)
            seg_start_addr = idc.get_segm_start(seg)
            seg_end_addr = idc.get_segm_end(seg) - 4
            seg_size = seg_end_addr - seg_start_addr
            print(
                f's_name:{seg_name} s_start:{hex(seg_start_addr)} s_end:{hex(seg_end_addr)}  s_size:{seg_size} has_align:{seg_size % 4 == 0}')
            current_ea = seg_start_addr
            while current_ea <= seg_end_addr:
                """判斷是不是 ASM"""
                is_asm_code = ida_bytes.is_code(ida_bytes.get_flags(current_ea))
                """ 獲取長度 """
                item_size = idc.get_item_size(current_ea)
                if not is_asm_code:
                    create_result = ida_bytes.create_dword(current_ea, 4, True)
                    current_ea = current_ea + 4
                else:
                    c_asm = idc.GetDisasm(current_ea)
                    if c_asm == "ADD             X30, X30, W0,UXTW":
                        print(f'全局跳轉.....address:{hex(current_ea)}  asm:{c_asm}')
                    current_ea = current_ea + item_size
    print('format script success ........')

修正中央跳轉.py

這個腳本的修正思路,通過中央轉發器的 xref 引用進行向上修復,前提是通過腳本找到,我貼心的為大家打印了日志和修正開關,還有對跳轉位置寫了備注。

import struct
import ida_bytes
import idautils
import idc
import re
from keystone import *
def op_convert(x):
    if x.startswith("0x"):
        return int(x, 16)
    else:
        return int(x)
"""
此腳本應用于 特殊花指令跳轉........
"""
if __name__ == '__main__':
    arm64_nop = b"\x1f\x20\x03\xd5"
    """
    初始化匯編 引擎 keystone
    """
    ks = Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
    """
    這里是經過分析 找到得全局分發器入口地址
    """
    # redirects = [0x1065FEBD4, 0x106D959F0]
    redirects = [0x14DFC]
    for redirect in redirects:
        for xref in idautils.XrefsTo(redirect, 0):
            print('============================================================================')
            print(xref.type, idautils.XrefTypeName(xref.type), 'from', hex(xref.frm), 'to', hex(xref.to))
            redirect_base = xref.frm + 4
            print(f'redirect base:{hex(redirect_base)}')
            for a_xref in idautils.XrefsTo(xref.frm, 0):
                """獲得 x0 后邊得立即數"""
                op1 = idc.print_operand(a_xref.frm - 4, 1).replace("=", "").replace("#", "")
                """轉換 數字"""
                xx_number = op_convert(op1)
                """計算偏移"""
                offset = xx_number << 2
                f_bytes = ida_bytes.get_bytes(redirect_base + offset, 4)
                n_tuple = struct.unpack('                hex_data = [hex(num) for num in n_tuple][0]
                index_arr = redirect_base + int(hex_data, 16)
                """添加備注 """
                ida_bytes.set_cmt(a_xref.frm, f"current redirect:{hex(index_arr)}", False)
                print(
                    f"--[{idc.print_insn_mnem(a_xref.frm - 4)}-{idc.print_insn_mnem(a_xref.frm - 8)}]---------[{xx_number}-{offset}]-----to:[{hex(index_arr)}]-------imm[{hex(index_arr - a_xref.frm)}]---->",
                    a_xref.type, idautils.XrefTypeName(a_xref.type), 'from', hex(a_xref.frm), 'to', hex(a_xref.to))
                """
                開始修正這些指令
                
                計算方式 = 實際目標地址-當前地址 = 立即數
                b 所得立即數
                """
                has_patch = True
                if has_patch:
                    patch_imm = hex(index_arr - a_xref.frm)
                    path_asm = f'b #{patch_imm}'
                    encoding, count = ks.asm(path_asm)
                    patch_byte = bytes([int(i) for i in encoding])
                    print(f"------------------->{path_asm} = [", end='')
                    for i in encoding:
                        print("%02x " % i, end='')
                    print(']')
                    ida_bytes.patch_bytes(a_xref.frm, patch_byte)
                """
                patch 完 這些指令 要把  操作  x0 和 w0 這種指令也 nop掉 向上 搜索
                """
                if has_patch:
                    sub_4 = a_xref.frm - 4
                    sub_8 = a_xref.frm - 8
                    if (idc.print_insn_mnem(sub_4) in ["LDR", "MOV"]) and idc.print_operand(sub_4, 0) in ["X0", "W0"]:
                        ida_bytes.patch_bytes(sub_4, arm64_nop)
                    if (idc.print_insn_mnem(sub_8) in ["LDR", "MOV"]) and idc.print_operand(sub_8, 0) in ["X0", "W0"]:
                        ida_bytes.patch_bytes(sub_8, arm64_nop)
            print('============================================================================')
    print('xref  fix script sucess.......')
   

后續的暢想&吐槽

我只是分享出來我的學習過程,順便吐槽一下開水團 Group的APP很卡,還有就是我在不同意隱私條款的情況下依然在抽樣我的設備信息,具體條款我也看了簡直太流氓了。其次就是有那么點不單純窺探,這些腳本并不能夠完全修正但F5但是可以不致于抓瞎操作,當前這個腳本適用于 安卓和iOS端開水團group的產品,后邊我會把修正字符串加密的腳本也放出,兄弟團大概率會把他們的編譯器升級,把具體跳轉地址也加密了,大家有一起學習iOS&Android逆向的朋友可以一起交流。

連載后續計劃

todo 因為本次教程只是拋磚引玉,并不是完美修復與完美f5,還要修正是樣本中間接二次跳轉還是會干擾反匯編引擎,中間有很多棧相關的操作其實在修正后都是無用的,我還在慢慢手動Patch去二級跳轉帶來的反匯編引擎JUMPOUT,如果要讓IDA完美解析需要把中間二級跳轉與無用棧指令全都干掉。