寫一篇在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,因此需要只需要在name和proc中插入一個無用的臨時變量,就能讓參數(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個問題:
- 保證函數(shù)調(diào)用前后不會生成出入口指令,使用
__attribute__((naked))來處理 - 所有變量存儲在棧上,需要把控制好棧的使用
- 使用安全的寄存器(
r19~r28)
首先先判斷需要多長的??臻g,根據(jù)函數(shù)sysctl(name, 4, &proc, &size, NULL, 0)判斷
- 參數(shù)
name總共占用4 * int空間,記為0x10 - 參數(shù)
proc在arm64下,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存儲了size,memset一共需要三個參數(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
由于name是int數(shù)組,在明確其存儲位置的情況下,需要分別將4個4字節(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_flag和P_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_un的size為0x10,以及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
......
);