iOS中的內嵌彙編

文章連接html

寫一篇在iOS上使用匯編的文章的想法在腦殼裏面停留了好久了,可是遲遲沒有動手。雖然早前在作啓動耗時優化的工做中,也作過經過攔截objc_msgSend並插入彙編指令來統計方法調用耗時的工做,但也只僅此而已。恰好最近的時間項目在作安全加固,須要寫更多的彙編來提升安全性(文章內彙編使用指令集爲ARM64),也就有了本文node

內嵌彙編格式

__asm__ [關鍵詞]( 
    指令
    : [輸出操做數列表]
    : [輸入操做數列表]
    : [被污染的寄存器列表]
);
複製代碼

好比函數中存在a、b、c三個變量,要實現a = b + c這句代碼,彙編代碼以下:ios

__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關鍵字表示禁止編譯器對彙編代碼進行再優化,但基本上有沒有聲明編譯後指令都沒區別數組

操做數

操做數格式爲"[limits]constraint",分爲權限和限定符兩部分。好比"=r"表示參數是隻寫並存放在通用寄存器上安全

  • limitsmarkdown

    關鍵字 表意
    = 只寫,通用用於輸出操做數
    + 讀寫,只能用於輸出操做數
    & 聲明寄存器只能用於輸出
  • constraint架構

    關鍵字 表意
    f 浮點寄存器f0~f7
    G/H 浮點常量當即數
    I/L/K 數據處理用到的當即數
    J 值爲-4095~4095的索引
    l/r 寄存器r0~r15
    M 0~32/2的冪次方的常量
    m 內存地址
    w 向量寄存器s0~s31
    X 任何類型的操做數

指令

因爲ARM64的指令過多,可經過文末的擴展閱讀查閱指令,這裏只講解指令中的一些關鍵字:app

  • %0~%N / %[param]iphone

    在使用C代碼和彙編混編的狀況下,%起頭用來關聯參數,經過%[param]能夠聲明參數名稱,也可使用匿名參數格式%N的方式順序對應參數(abc參數會按照012的順序匹配):tcp

    __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)
      );
    複製代碼

    在實操過程當中,設備不必定支持%N的匿名參數格式,建議使用%[param]使可讀性更強

  • [reg]

    程序運行的多數狀況下,寄存器內存儲的是存放數據的地址,使用[]包裹住寄存器,表示將寄存器的存儲值做爲地址訪問數據。下面的指令分別是取出地址0x10086存儲的數據存放在x1寄存器上,而後存放到地址0x100086的內存中:

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

    使用#起頭表示當即數(常數),建議使用16進制書寫

調用規範

ARM64調用約定採用AAPCS64,參數從左到右存放到x0~x7寄存器中,參數超出8個時,多餘的從右往左入棧,根據返回值大小不一樣存放在x0/x8返回。寄存器規則以下:

寄存器 特殊名稱 規則
r31 SP 存放棧頂地址
r30 LR 存放函數返回地址
r29 FP 存放函數使用棧幀地址
r19~r28 被調用方須要保護的寄存器
r18 平臺寄存器,不建議當作臨時寄存器使用
r17 IP1 進程內使用寄存器,不建議當作臨時寄存器使用
r16 IP0 同r17,同時做爲軟中斷svc中的系統調用參數
r9~r15 臨時寄存器(彙編指令中嵌入函數地址參數時,會用於保存函數地址)
r8 返回值寄存器(其餘時候同r9~r15)
r0~r7 傳遞存儲調用參數,r0可做爲返回值寄存器
NZCV 狀態寄存器

實戰

調試檢測

iOS應用安全加固中,經過sysctl + kinfo_proc的方案能夠檢測應用是否被調試:

__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是不安全的,所以多數開發者會將這一調用替換成內嵌彙編的方案執行:

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代碼內嵌彙編開發的時候,有個致命的問題是函數入口會將臨時變量入棧,而且將這些變量存放到寄存器中。上面的混編代碼實際運行時,會出現下面的狀況:

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

......

// 內嵌彙編
mov x0, x0              // name正常賦值
mov x1, #4              // proc數據被破壞
mov x2, x1              // size數據被破壞
mov x3, x2
mov x4, #0x0
mov x5, #0x0
mov x12, #0xca
svc #0x80
複製代碼

編譯後的代碼因爲臨時變量順序問題,致使了svc中斷調用sysctl沒法傳入正確參數,最終卡死應用

修復

插入臨時變量

經過編譯後的指令獲得一張對應表:

變量 寄存器 入參寄存器
name x0 x0
proc x1 x2
size x2 X3

若是可以讓存儲臨時變量的寄存器和svc中斷時的入參寄存器保持一致,就不會遭到破壞

ARM64調用約定,參數從右往左入棧

由於檢測函數無入參,因此臨時參數入參後依次存放到了x0~x2寄存器中,順序爲name、proc、size,所以須要只須要在nameproc中插入一個無用的臨時變量,就能讓參數對應起來:

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();
複製代碼

編譯後指令變爲:

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

......

// 內嵌彙編
mov x0, x0           
mov x1, #4           
mov x2, x2             
mov x3, x3
mov x4, #0x0
mov x5, #0x0
mov x12, #0xca
svc #0x80
複製代碼

修改指令順序

設置入參的指令會破壞寄存器上已有的值,那麼保證設置入參以前,寄存器沒被破壞就能夠了:

__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)
);
複製代碼

編譯後指令以下:

// 內嵌彙編
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
複製代碼

全彙編實現

在和C代碼混編的狀況下,沒法保證哪些寄存器會被破壞,那麼直接使用匯編實現整個邏輯是一個不錯的選擇,須要注意2個問題:

  1. 保證函數調用先後不會生成出入口指令,使用__attribute__((naked))來處理
  2. 全部變量存儲在棧上,須要把控制好棧的使用
  3. 使用安全的寄存器(r19~r28

首先先判斷須要多長的棧空間,根據函數sysctl(name, 4, &proc, &size, NULL, 0)判斷

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

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

---------- 
|   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寄存器入棧後,使用其中五個寄存器來保存一些參數:

------------------
|   參數  | 寄存器 |
------------------  
|  name  |  r19  |
------------------   
|  proc  |  r20  |
------------------  
| p_size |  r21  |
------------------  
|  size  |  r22  |
------------------  
|   sp   |  r23  |
------------------  
|  temp  |  r24  |
------------------ 
複製代碼

確認好棧上空間的使用後,能夠開始分步驟實現:

函數出入口

在函數的出入口負責兩件事情: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"
    
    // 開闢棧空間,寄存器保存變量
    "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的內存以後,須要將:

size_t size = sizeof(struct kinfo_proc);
struct kinfo_proc proc;
memset(&proc, 0, size);
複製代碼

轉換成對應的彙編,其中proc存儲在x20x22存儲了sizememset一共須要三個參數,分別入參:

__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數組,在明確其存儲位置的狀況下,須要分別將44字節的參數存儲到對應的內存位置,其位置分佈以下:

-------------
|  name[3]  |  
-------------  sp + 0xc
|  name[2]  |  
-------------  sp + 0x8
|  name[1]  |  
-------------  sp + 0x4
|  name[0]  |  
-------------  sp
複製代碼

另外name須要使用到getpid()來配置參數,經過svc的中斷能夠獲取這一參數(svc系統調用參數能夠參考擴展閱讀中的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()

    // 設置參數並存儲
    "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

最後是調用sysctl,根據參數和寄存器對應關係入參調用便可:

__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在結構體中的偏移來訪問數據,struct extern_proc的結構以下:

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,能夠確認結構體的內存佔用圖:

-------------------
|      p_flag     |  
-------------------  kinfo_proc + 0x20
|     p_sigacts   |  
-------------------  kinfo_proc + 0x18
|     p_vmspace   |  
-------------------  kinfo_proc + 0x10
|    union p_un   |  
-------------------  kinfo_proc
複製代碼

比對標記而且將檢測結果存放到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
    
    ......
);
複製代碼

擴展閱讀

Kernel_Syscalls

ARM64 架構之入棧/出棧操做

深刻iOS系統底層之CPU寄存器

Procedure Call Standard for the ARM 64-bit Architecture

相關文章
相關標籤/搜索