iOS中的內(nèi)嵌匯編

文章鏈接

寫一篇在iOS上使用匯編的文章的想法在腦袋里面停留了很久了,但是遲遲沒有動手。雖然早前在做啟動耗時優(yōu)化的工作中,也做過通過攔截objc_msgSend并插入?yún)R編指令來統(tǒng)計方法調(diào)用耗時的工作,但也只僅此而已。剛好最近的時間項目在做安全加固,需要寫更多的匯編來提高安全性(文章內(nèi)匯編使用指令集為ARM64),也就有了本文

內(nèi)嵌匯編格式

__asm__ [關(guān)鍵詞]( 
    指令
    : [輸出操作數(shù)列表]
    : [輸入操作數(shù)列表]
    : [被污染的寄存器列表]
);

比如函數(shù)中存在a、b、c三個變量,要實現(xiàn)a = b + c這句代碼,匯編代碼如下:

__asm__ volatile(
    "mov x0, %[b]\n"
    "mov x1, %[c]\n"
    "add x2, x0, x1\n"
    "mov %[a], x2\n"
    : [a]"=r"(a)
    : [b]"r"(b), [c]"r"(c)
);

volatile

volatile關(guān)鍵字表示禁止編譯器對匯編代碼進(jìn)行再優(yōu)化,但基本上有沒有聲明編譯后指令都沒區(qū)別

操作數(shù)

操作數(shù)格式為"[limits]constraint",分為權(quán)限和限定符兩部分。比如"=r"表示參數(shù)是只寫并存放在通用寄存器上

  • limits

    關(guān)鍵字 表意
    = 只寫,通用用于輸出操作數(shù)
    + 讀寫,只能用于輸出操作數(shù)
    & 聲明寄存器只能用于輸出
  • constraint

    關(guān)鍵字 表意
    f 浮點(diǎn)寄存器f0~f7
    G/H 浮點(diǎn)常量立即數(shù)
    I/L/K 數(shù)據(jù)處理用到的立即數(shù)
    J 值為-4095~4095的索引
    l/r 寄存器r0~r15
    M 0~32/2的冪次方的常量
    m 內(nèi)存地址
    w 向量寄存器s0~s31
    X 任何類型的操作數(shù)

指令

由于ARM64的指令過多,可通過文末的擴(kuò)展閱讀查閱指令,這里只講解指令中的一些關(guān)鍵字:

  • %0~%N / %[param]

    在使用C代碼和匯編混編的情況下,%起頭用來關(guān)聯(lián)參數(shù),通過%[param]可以聲明參數(shù)名稱,也可以使用匿名參數(shù)格式%N的方式順序?qū)?yīng)參數(shù)(abc參數(shù)會按照012的順序匹配):

      __asm__ volatile(
          "mov x0, %1\n"
          "mov x1, %2\n"
          "add x2, x0, x1\n"
          "mov %0, x2\n"
          : "=r"(a)
          : "r"(b), "r"(c)
      );
    

    在實操過程中,設(shè)備不一定支持%N的匿名參數(shù)格式,建議使用%[param]使可讀性更強(qiáng)

  • [reg]

    程序運(yùn)行的多數(shù)情況下,寄存器內(nèi)存儲的是存放數(shù)據(jù)的地址,使用[]包裹住寄存器,表示將寄存器的存儲值作為地址訪問數(shù)據(jù)。下面的指令分別是取出地址0x10086存儲的數(shù)據(jù)存放在x1寄存器上,然后存放到地址0x100086的內(nèi)存中:

      "mov x0, #0x10086\n"
      "mov x1, [x0]\n"
      "mov x2, #0x100086\n"
      "str x1, [x2]\n"
    
  • #1 / #0x1

    使用#起頭表示立即數(shù)(常數(shù)),建議使用16進(jìn)制書寫

調(diào)用規(guī)范

ARM64調(diào)用約定采用AAPCS64,參數(shù)從左到右存放到x0~x7寄存器中,參數(shù)超出8個時,多余的從右往左入棧,根據(jù)返回值大小不同存放在x0/x8返回。寄存器規(guī)則如下:

寄存器 特殊名稱 規(guī)則
r31 SP 存放棧頂?shù)刂?/td>
r30 LR 存放函數(shù)返回地址
r29 FP 存放函數(shù)使用棧幀地址
r19~r28 被調(diào)用方需要保護(hù)的寄存器
r18 平臺寄存器,不建議當(dāng)做臨時寄存器使用
r17 IP1 進(jìn)程內(nèi)使用寄存器,不建議當(dāng)做臨時寄存器使用
r16 IP0 同r17,同時作為軟中斷svc中的系統(tǒng)調(diào)用參數(shù)
r9~r15 臨時寄存器(匯編指令中嵌入函數(shù)地址參數(shù)時,會用于保存函數(shù)地址)
r8 返回值寄存器(其他時候同r9~r15)
r0~r7 傳遞存儲調(diào)用參數(shù),r0可作為返回值寄存器
NZCV 狀態(tài)寄存器

實戰(zhàn)

調(diào)試檢測

iOS應(yīng)用安全加固中,通過sysctl + kinfo_proc的方案可以檢測應(yīng)用是否被調(diào)試:

__attribute__((__always_inline)) bool checkTracing() {
    size_t size = sizeof(struct kinfo_proc);
    struct kinfo_proc proc;
    memset(&proc, 0, size);
    
    int name[4];
    name[0] = CTL_KERN;
    name[1] = KERN_PROC;
    name[2] = KERN_PROC_PID;
    name[3] = getpid();
    
    sysctl(name, 4, &proc, &size, NULL, 0);
    return proc.kp_proc.p_flag & P_TRACED;
}

但由于fishhook這種直接修改懶符號地址的方案存在,直接使用sysctl是不安全的,因此多數(shù)開發(fā)者會將這一調(diào)用替換成內(nèi)嵌匯編的方案執(zhí)行:

size_t size = sizeof(struct kinfo_proc);
struct kinfo_proc proc;
memset(&proc, 0, size);

int name[4];
name[0] = CTL_KERN;
name[1] = KERN_PROC;
name[2] = KERN_PROC_PID;
name[3] = getpid();

__asm__(
    "mov x0, %[name_ptr]\n"
    "mov x1, #4\n"
    "mov x2, %[proc_ptr]\n"
    "mov x3, %[size_ptr]\n"
    "mov x4, #0x0\n"
    "mov x5, #0x0\n"
    "mov w16, #202\n"
    "svc #0x80\n"
    :
    :[name_ptr]"r"(&name), [proc_ptr]"r"(&proc), [size_ptr]"r"(&size)
);

return proc.kp_proc.p_flag & P_TRACED;

踩坑

使用C代碼內(nèi)嵌匯編開發(fā)的時候,有個致命的問題是函數(shù)入口會將臨時變量入棧,并且將這些變量存放到寄存器中。上面的混編代碼實際運(yùn)行時,會出現(xiàn)下面的情況:

// 函數(shù)入口生成的臨時變量代碼
add x0, sp, #0x24       // x0存放name
add x1, sp, #0x34       // x1存放proc
add x2, sp, #020        // x2存放size

......

// 內(nèi)嵌匯編
mov x0, x0              // name正常賦值
mov x1, #4              // proc數(shù)據(jù)被破壞
mov x2, x1              // size數(shù)據(jù)被破壞
mov x3, x2
mov x4, #0x0
mov x5, #0x0
mov x12, #0xca
svc #0x80

編譯后的代碼由于臨時變量順序問題,導(dǎo)致了svc中斷調(diào)用sysctl無法傳入正確參數(shù),最終卡死應(yīng)用

修復(fù)

插入臨時變量

通過編譯后的指令得到一張對應(yīng)表:

變量 寄存器 入?yún)⒓拇嫫?/th>
name x0 x0
proc x1 x2
size x2 X3

如果能夠讓存儲臨時變量的寄存器和svc中斷時的入?yún)⒓拇嫫鞅3忠恢?,就不會遭到破?/p>

ARM64調(diào)用約定,參數(shù)從右往左入棧

因為檢測函數(shù)無入?yún)?,所以臨時參數(shù)入?yún)⒑笠来未娣诺搅?code>x0~x2寄存器中,順序為name、proc、size,因此需要只需要在nameproc中插入一個無用的臨時變量,就能讓參數(shù)對應(yīng)起來:

size_t size = sizeof(struct kinfo_proc);
struct kinfo_proc proc;
memset(&proc, 0, size);

int placeholder;
int name[4];
name[0] = CTL_KERN;
name[1] = KERN_PROC;
name[2] = KERN_PROC_PID;
name[3] = getpid();

編譯后指令變?yōu)椋?/p>

// 函數(shù)入口生成的臨時變量代碼
add x0, sp, #0x24       // x0存放name
add x1, sp, #0x34       // x1存放placeholder
add x2, sp, 0x38        // x2存放proc
add x3, sp, #020        // x3存放size

......

// 內(nèi)嵌匯編
mov x0, x0           
mov x1, #4           
mov x2, x2             
mov x3, x3
mov x4, #0x0
mov x5, #0x0
mov x12, #0xca
svc #0x80

修改指令順序

設(shè)置入?yún)⒌闹噶顣茐募拇嫫魃弦延械闹担敲幢WC設(shè)置入?yún)⒅?,寄存器沒被破壞就可以了:

__asm__(
    "mov x0, %[name_ptr]\n"
    "mov x3, %[size_ptr]\n"
    "mov x2, %[proc_ptr]\n"
    "mov x1, #4\n"
    "mov x4, #0x0\n"
    "mov x5, #0x0\n"
    "mov w16, #202\n"
    "svc #0x80\n"
    :
    :[name_ptr]"r"(&name), [proc_ptr]"r"(&proc), [size_ptr]"r"(&size)
);

編譯后指令如下:

// 內(nèi)嵌匯編
mov x0, x0              // x0保存name
mov x3, x2              // x3保存size
mov x2, x1              // x2保存proc
mov x1, #4
mov x4, #0x0
mov x5, #0x0
mov x12, #0xca
svc #0x80

全匯編實現(xiàn)

在和C代碼混編的情況下,無法保證哪些寄存器會被破壞,那么直接使用匯編實現(xiàn)整個邏輯是一個不錯的選擇,需要注意2個問題:

  1. 保證函數(shù)調(diào)用前后不會生成出入口指令,使用__attribute__((naked))來處理
  2. 所有變量存儲在棧上,需要把控制好棧的使用
  3. 使用安全的寄存器(r19~r28

首先先判斷需要多長的??臻g,根據(jù)函數(shù)sysctl(name, 4, &proc, &size, NULL, 0)判斷

  • 參數(shù)name總共占用 4 * int空間,記為0x10
  • 參數(shù)procarm64下,sizof()計算長度為0x288
  • 參數(shù)&size指針長度為0x8
  • 共計0x2a0

函數(shù)入口時,需要對FP/LR寄存器進(jìn)行入棧,保證函數(shù)能正確退出。另外r19~r28共計10個寄存器需要進(jìn)行入棧保護(hù),最終得出函數(shù)運(yùn)行時的棧空間圖:

---------- 
|   FP   |
----------  sp + 0x2f8
|   LR   |
----------  sp + 0x2f0
|   r20  |
----------  sp + 0x2e8
|   r19  |
----------  sp + 0x2e0
|   r22  |
----------  sp + 0x2d8
|   r21  |
----------  sp + 0x2d0
|   r24  |
----------  sp + 0x2c8
|   r23  |
----------  sp + 0x2c0
|   r26  |
----------  sp + 0x2b8
|   r25  |
----------  sp + 0x2b0
|   r28  |
----------  sp + 0x2a8
|   r27  |
----------  sp + 0x2a0
| p_size |
----------  sp + 0x298
|  proc  |
----------  sp + 0x10
|  name  |  
----------  sp

在保存r19~r28寄存器入棧后,使用其中五個寄存器來保存一些參數(shù):

------------------
|   參數(shù)  | 寄存器 |
------------------  
|  name  |  r19  |
------------------   
|  proc  |  r20  |
------------------  
| p_size |  r21  |
------------------  
|  size  |  r22  |
------------------  
|   sp   |  r23  |
------------------  
|  temp  |  r24  |
------------------ 

確認(rèn)好棧上空間的使用后,可以開始分步驟實現(xiàn):

函數(shù)出入口

在函數(shù)的出入口負(fù)責(zé)兩件事情:FP/LR的出入棧、r19~r28的出入棧

__asm__ volatile(
    "stp x29, x30, [sp, #-0x10]!\n"
    "stp x19, x20, [sp, #-0x10]!\n"
    "stp x21, x22, [sp, #-0x10]!\n"
    "stp x23, x24, [sp, #-0x10]!\n"
    "stp x25, x26, [sp, #-0x10]!\n"
    "stp x27, x28, [sp, #-0x10]!\n"
    
    ......
    
    "ldp x19, x20, [sp], #0x10\n"
    "ldp x21, x22, [sp], #0x10\n"
    "ldp x23, x24, [sp], #0x10\n"
    "ldp x25, x26, [sp], #0x10\n"
    "ldp x27, x28, [sp], #0x10\n"
    "ldp x29, x30, [sp], #0x10\n"
);

棧開辟空間

臨時變量總共用到0x2a0的空間,并且需要使用5個寄存器保存變量

__asm__ volatile(
    ......
    "sub sp, sp, #0x2a0\n"
    
    // 開辟??臻g,寄存器保存變量
    "mov x19, sp\n"             // x19 = name
    "add, x20, sp, #0x10\n"     // x20 = proc
    "add, x21, sp, #0x298\n"    // x21 = p_size
    "mov x22, #0x288\n"         // x22 = size
    "mov x23, sp\n"             // x23 = sp
    "str x22, [x21]\n"          // p_size = &size
    
    "add sp, sp, #0x2a0\n"
    ......
);

kinfo_proc

確定proc的內(nèi)存之后,需要將:

size_t size = sizeof(struct kinfo_proc);
struct kinfo_proc proc;
memset(&proc, 0, size);

轉(zhuǎn)換成對應(yīng)的匯編,其中proc存儲在x20,x22存儲了sizememset一共需要三個參數(shù),分別入?yún)ⅲ?/p>

__asm__ volatile(
    ......
    
    "mov x24, %[memset_ptr]\n"
    "mov x0, x20\n"
    "mov x1, #0x0\n"
    "mov x2, x12\n"
    "blr x24\n"
    
    ......
    :
    :[memset_ptr]"r"(memset)
);

name

由于nameint數(shù)組,在明確其存儲位置的情況下,需要分別將44字節(jié)的參數(shù)存儲到對應(yīng)的內(nèi)存位置,其位置分布如下:

-------------
|  name[3]  |  
-------------  sp + 0xc
|  name[2]  |  
-------------  sp + 0x8
|  name[1]  |  
-------------  sp + 0x4
|  name[0]  |  
-------------  sp

另外name需要使用到getpid()來配置參數(shù),通過svc的中斷可以獲取這一參數(shù)(svc系統(tǒng)調(diào)用參數(shù)可以參考擴(kuò)展閱讀中的Kernel Syscalls

#define CTL_KERN        1
#define KERN_PROC       14
#define KERN_PROC_PID   1

__asm__ volatile(
    ......
    
    // getpid
    "mov x0, #0\n"
    "mov w16, #20\n"
    "mov x3, x0\n"          // name[3]=getpid()

    // 設(shè)置參數(shù)并存儲
    "mov x0, #0x1\n"
    "mov x1, #0xe\n"
    "mov x2, #0x1\n"
    "str w0, [x23, 0x0]\n"
    "str w1, [x23, 0x4]\n"
    "str w2, [x23, 0x8]\n"
    "str w3, [x23, 0xc]\n"
    
    ......
);

sysctl

最后是調(diào)用sysctl,根據(jù)參數(shù)和寄存器對應(yīng)關(guān)系入?yún)⒄{(diào)用即可:

__asm__ volatile(
    ......

    "mov x0, x19\n"
    "mov x1, #0x4\n"
    "mov x2, x20\n"
    "mov x3, x21\n"
    "mov x4, #0x0\n"
    "mov x5, #0x0\n"
    "mov w16, #202\n"
    "svc #0x80\n"
            
    ......
);

flag檢測

最終需要返回p_flagP_TRACED的與比較檢測,這里需要通過獲取p_flag在結(jié)構(gòu)體中的偏移來訪問數(shù)據(jù),struct extern_proc的結(jié)構(gòu)如下:

struct extern_proc {
    union {
        struct {
            struct  proc *__p_forw; /* Doubly-linked run/sleep queue. */
            struct  proc *__p_back;
        } p_st1;
        struct timeval __p_starttime;   /* process start time */
    } p_un;
    
    #define p_forw p_un.p_st1.__p_forw
    #define p_back p_un.p_st1.__p_back
    #define p_starttime p_un.__p_starttime
    
    struct  vmspace *p_vmspace;     /* Address space. */
    struct  sigacts *p_sigacts;     /* Signal actions, state (PROC ONLY). */
    int     p_flag;                 /* P_* flags. */
    char    p_stat;                 /* S* process status. */
    pid_t   p_pid;                  /* Process identifier. */
    pid_t   p_oppid;         /* Save parent pid during ptrace. XXX */
    int     p_dupfd;         /* Sideways return value from fdopen. XXX */
    /* Mach related  */
    caddr_t user_stack;     /* where user stack was allocated */
    void    *exit_thread;   /* XXX Which thread is exiting? */
    int             p_debugger;             /* allow to debug */
    boolean_t       sigwait;        /* indication to suspend */
    /* scheduling */
    u_int   p_estcpu;        /* Time averaged value of p_cpticks. */
    int     p_cpticks;       /* Ticks of cpu time. */
    fixpt_t p_pctcpu;        /* %cpu for this process during p_swtime */
    void    *p_wchan;        /* Sleep address. */
    char    *p_wmesg;        /* Reason for sleep. */
    u_int   p_swtime;        /* Time swapped in or out. */
    u_int   p_slptime;       /* Time since last blocked. */
    struct  itimerval p_realtimer;  /* Alarm timer. */
    struct  timeval p_rtime;        /* Real time. */
    u_quad_t p_uticks;              /* Statclock hits in user mode. */
    u_quad_t p_sticks;              /* Statclock hits in system mode. */
    u_quad_t p_iticks;              /* Statclock hits processing intr. */
    int     p_traceflag;            /* Kernel trace points. */
    struct  vnode *p_tracep;        /* Trace to vnode. */
    int     p_siglist;              /* DEPRECATED. */
    struct  vnode *p_textvp;        /* Vnode of executable. */
    int     p_holdcnt;              /* If non-zero, don't swap. */
    sigset_t p_sigmask;     /* DEPRECATED. */
    sigset_t p_sigignore;   /* Signals being ignored. */
    sigset_t p_sigcatch;    /* Signals being caught by user. */
    u_char  p_priority;     /* Process priority. */
    u_char  p_usrpri;       /* User-priority based on p_cpu and p_nice. */
    char    p_nice;         /* Process "nice" value. */
    char    p_comm[MAXCOMLEN + 1];
    struct  pgrp *p_pgrp;   /* Pointer to process group. */
    struct  user *p_addr;   /* Kernel virtual addr of u-area (PROC ONLY). */
    u_short p_xstat;        /* Exit status for wait; also stop signal. */
    u_short p_acflag;       /* Accounting flags. */
    struct  rusage *p_ru;   /* Exit information. XXX */
};

其中union p_unsize0x10,以及p_flag前面的兩個指針分別占用0x8,可以確認(rèn)結(jié)構(gòu)體的內(nèi)存占用圖:

-------------------
|      p_flag     |  
-------------------  kinfo_proc + 0x20
|     p_sigacts   |  
-------------------  kinfo_proc + 0x18
|     p_vmspace   |  
-------------------  kinfo_proc + 0x10
|    union p_un   |  
-------------------  kinfo_proc

比對標(biāo)記并且將檢測結(jié)果存放到x0中返回:

#define P_TRACED        0x00000800

__asm__ volatile(
    ......
    
    "ldr, x24, [x20, #0x20]\n"      // x24 = proc.kp_proc.p_flag
    "mov x25, #0x800\n"             // x25 = P_TRACED
    "blc x0, x24, x25\n"            // x0 = x24 & x25
    
    ......
);

擴(kuò)展閱讀

Kernel_Syscalls

ARM64 架構(gòu)之入棧/出棧操作

深入iOS系統(tǒng)底層之CPU寄存器

Procedure Call Standard for the ARM 64-bit Architecture

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

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

  • 寄存器 內(nèi)部部件之間由總線連接 對程序員來說,CPU中最主要部件是寄存器,可以通過改變寄存器的內(nèi)容來實現(xiàn)對CPU的...
    成績是汗閱讀 2,174評論 0 3
  • 以arm64為例 xcode調(diào)試匯編 1. xcode 查看運(yùn)行時的匯編代碼 debug -> debug wor...
    meryin閱讀 2,664評論 1 6
  • 在定位某些crash問題的時候,有時候遇到一些問題很詭異。有時候掛在了系統(tǒng)庫里面。這個時候定位crash問題往往是...
    kakukeme閱讀 6,699評論 0 58
  • 初識匯編 我們是逆向iOS系統(tǒng)上面的APP.那么我們知道,一個APP安裝在手機(jī)上面的可執(zhí)行文件本質(zhì)上是二進(jìn)制文件....
    looha閱讀 656評論 0 2
  • 1.地址總線,數(shù)據(jù)總線,控制總線在哪里,它們有什么作用?答:它們都是cpu連接外部組件的線路。地址總線:地址總線A...
    MagicalGuy閱讀 1,671評論 0 1

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