文章連接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
關鍵字表示禁止編譯器對彙編代碼進行再優化,但基本上有沒有聲明編譯後指令都沒區別數組
操做數格式爲"[limits]constraint"
,分爲權限和限定符兩部分。好比"=r"
表示參數是隻寫並存放在通用寄存器上安全
limits
markdown
關鍵字 | 表意 |
---|---|
= | 只寫,通用用於輸出操做數 |
+ | 讀寫,只能用於輸出操做數 |
& | 聲明寄存器只能用於輸出 |
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
,所以須要只須要在name
和proc
中插入一個無用的臨時變量,就能讓參數對應起來:
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
個問題:
__attribute__((naked))
來處理r19~r28
)首先先判斷須要多長的棧空間,根據函數sysctl(name, 4, &proc, &size, NULL, 0)
判斷
name
總共佔用 4 * int
空間,記爲0x10
proc
在arm64
下,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"
......
);
複製代碼
肯定proc
的內存以後,須要將:
size_t size = sizeof(struct kinfo_proc);
struct kinfo_proc proc;
memset(&proc, 0, size);
複製代碼
轉換成對應的彙編,其中proc
存儲在x20
,x22
存儲了size
,memset
一共須要三個參數,分別入參:
__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
是int
數組,在明確其存儲位置的狀況下,須要分別將4
個4字節
的參數存儲到對應的內存位置,其位置分佈以下:
-------------
| 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
,根據參數和寄存器對應關係入參調用便可:
__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"
......
);
複製代碼
最終須要返回p_flag
和P_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_un
的size
爲0x10
,以及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
......
);
複製代碼