《黑盒下的逆向分析:利用nm與Hopper定位Incode SDK符號覆蓋問題》

一 背景

在引入第三方Incode SDK的過程中,發(fā)現(xiàn)公司內(nèi)埋點SDK,數(shù)據(jù)上報功能全部失效。
由于Incode不提供給我們閱讀源碼權(quán)限,因此無法直接分析其邏輯。

二 集團業(yè)務(wù)方調(diào)用鏈路

埋點SDK調(diào)用的是SSZipArchive SDK,具體方法是

- (**BOOL**)writeData:(NSData *)data filename:(**nullable** NSString *)filename withPassword:(**nullable** NSString *)password

接著調(diào)用的是zipOpenNewFileInZip5

打斷點發(fā)現(xiàn),沒有執(zhí)行這里的zipOpenNewFileInZip5方法。所以懷疑底層函數(shù)行為已經(jīng)發(fā)生了變化。詢問雙方SSZipArchive SDK版本號,發(fā)現(xiàn)分別是2.2.3和2.5.4,git上可以搜索到這兩個是跨了幾年的大更新。
2.2.3是最后一個版本,后來代碼倉庫都換了。

https://github.com/karmarama/SSZipArchive/tags

https://cocoapods.org/pods/SSZipArchive

三 incode SDK分析步驟

strings libIncdOnboarding.a | grep zipOpenNewFileInZip5
結(jié)果是
zipOpenNewFileInZip5

可以看到有一個結(jié)果,說明該字符串存在于二進制中,可能來源于函數(shù)名、日志或其他常量,但不能證明該函數(shù)被實現(xiàn)或?qū)С觥?/p>

nm -o libIncdOnboarding.a | grep "zipOpenNewFileInZip5"
結(jié)果可以看到

libIncdOnboarding.a:SSZipArchive.o:                  U  zipOpenNewFileInZip5
libIncdOnboarding.a:mz_compat.o: 00000000000003bc T  zipOpenNewFileInZip5

結(jié)果分析:
U 代表這段代碼在這個文件里只是被引用(調(diào)用)。
T 代表這段代碼在 TEXT 段(代碼段)中被真正定義(實現(xiàn))了。

T (Text Section)。 這里的 T 代表該函數(shù)真實的代碼指令就存放在 mz_compat.o 的代碼段(Text Segment)中。00000000000003bc 是它在這個 .o 文件里的相對內(nèi)存地址。

所以SSZipArchive.o 僅僅是調(diào)用方。需要找到實際的定義。

ar -x libIncdOnboarding.a mz_compat.o

可以將mz_compat.o 從libIncdOnboarding.a中抽離出來。

不過其實使用Hopper Disassembler也可以直接拆分出這個。
打開Hopper Disassembler,按下 Option + Enter 生成類似 C 語言的偽代碼(Pseudocode)。

Incode SDK 中的實現(xiàn)位于 mz_compat.o,而埋點 SDK 中未發(fā)現(xiàn)同名 object file,說明兩者內(nèi)部實現(xiàn)來源或封裝方式不同。

四 問題原因

C語言處于扁平命名空間(Flat Namespace),在Mach-O中屬于全局符號。當(dāng)多個靜態(tài)庫包含同名全局 C 函數(shù)時,鏈接器在鏈接階段(Link Time)進行符號解析(Symbol Resolution),可能導(dǎo)致符號被覆蓋(Symbol Collision / Interposition),從而改變實際執(zhí)行邏輯。

五 解決方案

符號前綴隔離方案(Symbol Prefixing)。通過 加前綴重命名所有相關(guān)函數(shù)(聲明、實現(xiàn)、調(diào)用、頭文件暴露)。

六 命令與軟件介紹

  • strings:二進制掃描;查找所有可打印字符串。
  • nm:Mach-O符號表;查看函數(shù)/變量符號。
  • otool:匯編級;查看機器指令。

nm -gU libIncdOnboarding.a | grep -i "zipOpenNewFileInZip5"

提取該函數(shù)的反匯編
otool -tv libIncdOnboarding.a | grep -A 100 "zipOpenNewFileInZip5:"

objdump -t libIncdOnboarding.a | grep zipOpenNewFileInZip5
-t,--syms,查看符號表

otool (macOS/iOS) / objdump: 查看 Mach-O 或 ELF 文件的匯編代碼、依賴庫(otool -L)和頭文件信息。

  • Hopper Disassembler / IDA Pro
    如果不僅僅是看符號,還要看 zipOpenNewFileInZip5 在 2.5.4 版本到底改了什么內(nèi)部邏輯,就可以用這些偽代碼反編譯工具。

  • Hopper Disassembler:50M,價格US99.00。 IDA Pro:770M,官方價格不菲,訂閱制。個人版每年365。

Hopper Disassembler 操作步驟

1 將二進制拖進軟件,需要選擇File。有Entry+ZIP64.o,URL + ZIP.o,F(xiàn)ileManager+ZIP.o,Archive+ZIP64.o。
因為要查SSZip,所以選擇SSZipArchive.o

可以看到Hopper Disassembler主界面的顯示

 zipOpenNewFileInZip5:

0000000000006518         **extern function code**                                   ; CODE XREF=-[IncdZipArchive writeSymlinkFileAtPath:withFileName:compressionLevel:password:AES:]+404, +[IncdZipArchive _zipOpenEntry:name:zipfi:level:password:aes:]+192

CODE XREF=... (Cross Reference,交叉引用)。
雙擊方法名,比如writeSymlinkFileAtPath:,Hopper 會跳轉(zhuǎn)到調(diào)用的那行匯編代碼。
主界面可以看到調(diào)用順序,右側(cè)有簡化的Control Flow Graph。

對incode的分析

Hopper Disassembler可以查看其實現(xiàn)的偽函數(shù)。

int  zipOpenNewFileInZip5(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7) {
    r7 = arg7;
    r6 = arg6;
    r5 = arg5;
    r2 = arg2;
    r1 = arg1;
    r0 = arg0;
    if (r0 != 0x0) {
            r31 = r31 - 0xf0;
            var_60 = r28;
            stack[-88] = r27;
            var_50 = r26;
            stack[-72] = r25;
            var_40 = r24;
            stack[-56] = r23;
            var_30 = r22;
            stack[-40] = r21;
            var_20 = r20;
            stack[-24] = r19;
            var_10 = r29;
            stack[-8] = r30;
            r29 = &var_10;
            r20 = r7;
            r21 = r6;
            r22 = r5;
            r23 = r2;
            r24 = r1;
            r19 = r0;
            r26 = *(int128_t *)(r29 + 0x40);
            r25 = *(int128_t *)(r29 + 0x48);
            r27 = *(int32_t *)(r29 + 0x10);
            if (r2 != 0x0) {
                    r0 = *(int32_t *)r23;
                    if (r0 == 0x0) {
                            r0 =  mz_zip_tm_to_dosdate(r23 + 0x8);
                    }
                     mz_zip_dosdate_to_time_t(r0);
            }
            r28 = *(int32_t *)(r29 +  fill_win32_filefunc64A);
            r23 = *(r29 +  fill_win32_filefunc);
            r8 = "-";
            if (r24 == 0x0) {
                    if (!CPU_FLAGS & E) {
                            r8 = r24;
                    }
                    else {
                            r8 = "-";
                    }
            }
            if (r20 != 0x0) {
                    _strlen(r20);
            }
            r9 = *(int128_t *)(r29 +  fill_fopen64_filefunc);
            r8 = *(int128_t *)(r29 + 0x18);
            if (r28 == 0x0) {
                    asm { cinc       w10, w10, eq };
            }
            r0 = *(r19 + 0x8);
            r0 =  mz_zip_entry_write_open(r0, &var_F0, sign_extend_64(r9), r8 & 0xff, r23);
    }
    else {
            r0 = 0xffffff9a;
    }
    return r0;
}

七 匯編操作含義

1. 提取 C 字符串 (UTF8String)

mov        x0, x21      ; 將某個 Objective-C 對象(可能是 NSString 實例)放入 x0 作為消息接收者
bl         _objc_retainAutorelease ; ARC 機制:保留并自動釋放,確保字符串在執(zhí)行期間不被銷毀
bl         _objc_msgSend$UTF8String ; 調(diào)用 [NSString UTF8String]
  • 解釋: 這一步相當(dāng)于源碼里的 const char *cString = [myString UTF8String];。將 OC 的字符串轉(zhuǎn)換成了 C 語言的字符指針,返回值存放在 x0 寄存器中。這個字符串大概率是解壓密碼或者文件名。

2. 準備 zipOpenNewFileInZip5 的海量參數(shù)

minizip 庫的 zipOpenNewFileInZip5 是一個參數(shù)極其龐大的函數(shù)(足足有 19 個參數(shù))。

在 ARM64 架構(gòu)的調(diào)用約定(Calling Convention)中:前 8 個參數(shù)放在 x0x7 寄存器中,剩下的參數(shù)必須壓入棧(Stack, 即 sp)中傳遞。

; ----- 這里是準備壓入棧(sp)的參數(shù) (第9個到第19個參數(shù)) -----
str        wzr, [sp, #0x4f0 + var_4B0]  ; 往棧里存 0 (wzr 是 zero register)
mov        w8, #0x300                   ; 準備一個常量 0x300
stp        x8, xzr, [sp, #0x30]         ; 把 0x300 和 0 壓入棧
strb       w24, [sp, #0x4f0 + var_4C8]  ; 存入一個布爾值或字節(jié)
stp        x0, xzr, [sp, #0x18]         ; 注意這里!把剛才 UTF8String 的結(jié)果 (x0) 壓入棧了,說明密碼或注釋作為后面的參數(shù)傳遞了
mov        w8, #0x8
mov        x9, #0xfff100000000
movk       x9, #0xffff, lsl #48
stp        x9, x8, [sp, #0x8]           ; 壓入一系列標(biāo)志位 (flags / method 等參數(shù))
stp        w8, w23, [sp]

; ----- 這里是準備寄存器的參數(shù) (前8個參數(shù)) -----
add        x2, sp, #0x50                ; 參數(shù)2 (x2):將棧上的一個地址傳給 x2。這通常是指向 zip_fileinfo 結(jié)構(gòu)體的指針
mov        x0, x25                      ; 參數(shù)0 (x0):zipFile file (Zip 文件的句柄)
mov        x1, x26                      ; 參數(shù)1 (x1):const char* filename (要在 Zip 里創(chuàng)建的文件名)
mov        x3, #0x0                     ; 參數(shù)3 (x3):extrafield_local = NULL
mov        w4, #0x0                     ; 參數(shù)4 (x4):size_extrafield_local = 0
mov        x5, #0x0                     ; 參數(shù)5 (x5):extrafield_global = NULL
mov        w6, #0x0                     ; 參數(shù)6 (x6):size_extrafield_global = 0
mov        x7, #0x0                     ; 參數(shù)7 (x7):comment = NULL
  • 解釋: 這段代碼看似凌亂,其實就是在老老實實地湊齊 zipOpenNewFileInZip5 所需要的 19 個參數(shù)。大部分額外字段都被寫死了 NULL (0x0)。

3. 執(zhí)行核心調(diào)用

bl          zipOpenNewFileInZip5 ; 調(diào)用方法!
mov        x23, x0                    ; 將返回值 (通常是 int) 備份到 x23 寄存器里。0 一般代表 ZIP_OK。

4. 寫入數(shù)據(jù) (zipWriteInFileInZip)

打開了 Zip 里的空文件后,接下來要往里面寫東西。

ldr        x24, [x22, #0x10]          ; 從 x22 對象的偏移 0x10 處取出 zipFile 句柄,暫存到 x24

add        x0, sp, #0x98              ; x0 指向棧上的一個字符串緩沖區(qū)
bl         _strlen                    ; 調(diào)用 strlen(x0) 獲取字符串長度
mov        x2, x0                     ; 將長度 (返回值) 放入 x2,作為 write 函數(shù)的 len 參數(shù)

add        x1, sp, #0x98              ; 將剛才的字符串緩沖區(qū)地址放入 x1,作為 write 的 buf 參數(shù)
mov        x0, x24                    ; 將 zipFile 句柄放入 x0
bl          zipWriteInFileInZip  ; 調(diào)用 zipWriteInFileInZip(file, buf, len)
  • 解釋: 相當(dāng)于 C 源碼:zipWriteInFileInZip(file, buffer, strlen(buffer));。它在把一段字符串(可能是一個符號鏈接的內(nèi)容、或者簡單的文本配置)寫入到剛才創(chuàng)建的 Zip 文件條目中。

5. 關(guān)閉文件并判斷結(jié)果

ldr        x0, [x22, #0x10]           ; 再次取出 zipFile 句柄放入 x0
bl          zipCloseFileInZip    ; 調(diào)用 zipCloseFileInZip(file) 關(guān)閉當(dāng)前文件條目

cmp        w23, #0x0                  ; w23 保存的是第3步 zipOpenNewFileInZip5 的返回值。這里將它和 0 (ZIP_OK) 進行比較
cset       w22, eq                    ; Condition Set: 如果相等 (eq),就把 w22 寄存器設(shè)置為 1 (YES/true),否則設(shè)置為 0 (NO/false)
  • 解釋: 對應(yīng)源碼:BOOL success = (openResult == ZIP_OK);。記錄剛才打開文件的操作是否成功,供后續(xù)邏輯使用。

技術(shù)點總結(jié)

在無三方源碼的限制下,使用 strings 命令進行全盤文本嗅探。
使用 nm 工具分析 Mach-O 符號表,精準判別符號的定義,鎖定沖突源文件。
使用 ar 命令對龐大的靜態(tài)庫進行 .o 目標(biāo)文件的抽離與精準降噪。
借助逆向工具Hopper Disassembler,提取其偽代碼。
通過符號前綴隔離方案(Symbol Prefixing)解決命名沖突而導(dǎo)致符號覆蓋的問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容