SVC的TraceHook沙箱的實現&無痕Hook實現思路
一
前言
二年前因為工作需要,自己嘗試開發過一套沙盒,普通的Linux IO 函數可以通過Hook Libc 去實現,比如Hook openat 等。大部分文件都可以進行處理(比如VA的IO處理)。但是一直有個心病困擾我半年之久,就是如何處理系統調用/內聯SVC指令的攔截和處理。
大廠基本為了程序的安全,會使用大量內聯SVC去調用系統函數,以此來保護程序的安全。以防被Hook。
如何實現SVC指令的IO重定向,成為最大的問題。嘗試在國內查找這塊資料,發現基本很少,大部分都是介紹基礎而不是去講如何進行Hook和修改,還有的就是通過刷機改源碼的方式,但是大部分大廠都是有自己的真機庫,基本谷歌系列,很容易就被認定為危險設備。
如果通過修改型號等方式去mock成國內的型號也可以,但是這種方式有弊端,如果有的App調用三方服務,比如小米會自帶一些系統服務,這些是沒辦法進行mock的。很容導致app崩潰 。
通過不斷去Google去查閱大量文章,問了很多老外,看代碼后來發現一套比較成熟的方案就是ptrace+seccomp,兩者缺一不可。
二
前奏知識
什么是SVC指令?什么是Syscall?根據我個人的理解,在Linux里面內存主要分為Linux用戶態,內核態。
當用戶自定義運行的函數在用戶態。內核態是當Linux需要處理文件,或者進行中斷IO等操作的時候就會進入內核態。
syscall 就是這個內核態的入口。而syscall函數里面的實現就是一段匯編(具體實現參考如下),匯編里面便是調用了svc的這條指令。
當arm系列cpu發現svc指令的時候,就會陷入中斷,簡稱0x80中斷。開始執行內核態邏輯,這個時候程序會進入暫停狀態。
優先去執行內核的邏輯。以此保證程序的安全性。(當我們自己去設計系統的時候,肯定也不希望在系統執行的時候被程序去干擾,導致系統崩潰)
Linux內核本身提供很多函數,比如常見的文件函數,openat,execve都是Linux內核提供的。這些函數都可以通過svc指令的方式去調用,只是實現的sysnum不一樣。傳入的參數不一樣而已。
通過svc執行的函數無法進行inlinehook Hook ,所以會提升程序的安全度。
總結:
svc是一條arm指令,Syscall函數是libc函數,實現底層使用了svc指令。
syscall 32&64位具體實現如下。
32位:
raw_syscall: MOV R12, SP STMFD SP!, {R4-R7} MOV R7, R0 MOV R0, R1 MOV R1, R2 MOV R2, R3 LDMIA R12, {R3-R6} SVC 0 LDMFD SP!, {R4-R7} mov pc, lr
64位:
raw_syscall: MOV X8, X0 MOV X0, X1 MOV X1, X2 MOV X2, X3 MOV X3, X4 MOV X4, X5 MOV X5, X6 SVC 0 RET
什么是Ptrace&Seccomp ?
Ptrace:
ptrace是linux 提供的調試函數,很多好用的工具,IDA ,LLDB等調試器都是通過ptrace去實現的。
這個函數里面有很多action 每個action都包含一個功能,比如注入進程,暫停,修改寄存器等常用功能。
ptrace當注入當前進程的時候是不需要root。如果注入非自己的進程是需要root才可以。調用注入的時候選擇一個pid即可。
ptrace可以在任何內存地方下斷點,修改對應位置的數據。ptrace的權限非常高。ptrace還可以調試內核態。所以也可以用來處理svc的參數和返回值。
ptrace具體API說明官方文檔如下:
https://man7.org/linux/man-pages/man2/ptrace.2.html
Seccomp:
Seccomp是Linux的一種安全機制,android 8.1以上使用了Seccomp。
主要功能是限制接通過syscall去調用某些系統函數,當開啟了Seccomp的進程在此調用的時候會變走異常的回調。
之前B佬的文章里面便采用了frida+seccomp的方式去做的svc攔截。也是很好的思路,帖子地址如下。
https://bbs.pediy.com/thread-271815.htm
Seccomp的過濾模式有兩種(strict&filter)。
strict
strict模式如果開啟以后,只支持四個函數的系統調用。(read,write,exit,rt_sigreturn)
如果一旦使用了其他的syscall 則會收到SIGKILL信號。
#include #include #include #include #include #include int main(int argc, char **argv){int output = open(“output.txt”, O_WRONLY);const char *val = “test”;//通過prctl函數設置seccomp的模式為strictprintf(“Calling prctl() to set seccomp strict mode…”); prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT); printf(“Writing to an already open file…”);//嘗試寫入write(output, val, strlen(val)+1);printf(“Trying to open file for reading…”); //設置完畢seccomp以后再次嘗試open (因為設置了secomp的模式是strict,所以這行代碼直接sign -9 信號)int input = open(“output.txt”, O_RDONLY);printf(“You will not see this message — the process will be killed first”);}
filter(BPF)
全程Seccomp-bpf,BPF是一種過濾模式,只有在Linux高版本會存在該功能,當某進程調用了svc以后,如果發現當前sysnum是我們進行過濾的sysnum,首先會進入我們自己寫的BPF規則。
通過我們自己的寫的規則,進行判斷該系統調用是否被運行調用,應該怎么進行處理,常用的指令如下:
BPF_LD, BPF_LDX加載指令BPF_ST, BPF_STX存儲指令BPF_ALU, 計算指令BPF_JMP, 跳轉指令BPF_RET, 返回指令 (結束指令)BPF_MISC 其他指令
指令之間可以相加或者相減,來完成一條JUMP操作,這塊挺復雜的。具體就不詳細去說了。
如果對這塊規則感興趣可以看一本書《Linux內核觀測技術BPF》,老外寫的,里面很詳細的介紹了BPF得使用規則。
包括如何配合Seccomp去做系統調用的攔截和trace。微信讀書上面就有不需要去購買紙質版本。三
開發過程
ptrace:
根據之前介紹的思路,第一版本主要通過ptrace去實現svc的參數/返回值的修改。首先先fork出來一條線程。用于跟蹤main進程。
開啟死循環,當使用ptrace的時候需要區分,調試線程(tracer)和被調試線程(tracee),他們是兩條線程。
/* * 用fork出來的進程去attch主進程 */int trace_current_process(int sdkVersion) { ALOGE("start trace_current_process "); prctl(PR_SET_DUMPABLE, 1, 0, 0, 0); mainProcessPid = getpid(); pid_t child = fork(); if (child < 0) { ALOGE("ptrace svc fork() error "); return -errno; } //init first tracer Tracer *first = get_tracer(nullptr, mainProcessPid, true); if (child == 0) { // attch main pid int status = ptrace(PTRACE_ATTACH, mainProcessPid, NULL, NULL); if (status != 0) { //attch失敗 ALOGE(">>>>>>>>> error: attach target process %d ", status); return -errno; } first->wait_sigcont = true; //開始執行死循環代碼,因為處于一直監聽狀態,理論上該進程不會退出 exit(event_loop()); } else { //init seccomp by main process //the seccomp filtering rule is intended only for the current process enable_syscall_filtering(first); } return 0;}
也就是當發現svc指令的一個回調。也就是(SIGTRAP | 0x80),這個時候開始執行調試線程邏輯。被調試線程進入等待狀態。
通過調用ptrace提供的api進行attch,調試進程是一個while true 死循環。這樣就可以一直監聽被調試線程的狀態,調試線程通過linux waitpid函數進行處理和回調,等待被調試線程進入指定回調。
while (true) { int tracee_status; Tracer *tracee; int signal; pid_t pid; free_terminated_tracees(); //-1 all thread pid = waitpid(-1, &tracee_status, 0); if (pid < 0) { ALOGE(">>>>>>>>>> !!!!! error: waitpid() %d %s", pid, strerror(errno)) if (errno != ECHILD) { return EXIT_FAILURE; } break; } tracee = get_tracer(nullptr, pid, true); assert(tracee != nullptr); //handle action signal = handle_tracee_event(tracee, tracee_status); //restart (void) restart_tracee(tracee, signal); } ALOGE("<<<<<<<<<<<< listening was error ,main listener stop !!") return last_exit_status;}
主要包含如下幾種狀態。包括正常退出、異常退出、結束。或者進入系統調用等。代碼如下:
if (WIFEXITED(tracee_status)) { //WEXITSTATUS取得子進程exit()返回的結束代碼 last_exit_status = WEXITSTATUS(tracee_status); //ALOGI("normal exit -> [%d] exit with status: %d ", tracee->pid, tracee_status); //被跟蹤者進程 正常執行結束,釋放當前(被跟蹤者) terminate_tracee(tracee); } else if (WIFSIGNALED(tracee_status)) { int signNum = WTERMSIG(tracee_status); //被跟蹤進程因為信號退出 ALOGE("[%d] process exit with signal: 終止信號 = %d 異常原因 = %s ", tracee->pid, signNum, strsignal(signNum) ) terminate_tracee(tracee); } else if (WIFSTOPPED(tracee_status)) { signal = (tracee_status & 0xfff00) >> 8; switch (signal) { //svc case SIGTRAP | 0x80: //被調試線程調用svc,開始處理參數和返回值 ...
當參數或者返回值處理完畢以后,通過給調試線程,調用ptrace設置被調試線程的啟動PTRACE_SYSCALL事件。
(當被調試進程執行了某些SIGTRAP 事件,程序就會進入暫停,這個時候調試線程開始處理對應的邏輯。
常用的恢復暫停事件有兩個,PTRACE_SYSCALL和PTRACE_CONT ,可以理解成一個是調試的單步執行,一個是繼續執行。
PTRACE_SYSCALL方式重新啟動被調試線程以后,下次遇到SVC的before和after還會繼續暫停。
也就是當svc執行之前(before)和執行之后(after)被調試線程都會暫停。
先把每個版本不同的寄存器進行匹配。用來區分LR,SP,PC等常用寄存器。
#elif defined(ARCH_ARM_EABI)static off_t reg_offset[] = { [SYSARG_NUM] = USER_REGS_OFFSET(uregs[7]), [SYSARG_1] = USER_REGS_OFFSET(uregs[0]), [SYSARG_2] = USER_REGS_OFFSET(uregs[1]), [SYSARG_3] = USER_REGS_OFFSET(uregs[2]), [SYSARG_4] = USER_REGS_OFFSET(uregs[3]), [SYSARG_5] = USER_REGS_OFFSET(uregs[4]), [SYSARG_6] = USER_REGS_OFFSET(uregs[5]), [SYSARG_RESULT] = USER_REGS_OFFSET(uregs[0]), [FRAME_POINTER] = USER_REGS_OFFSET(uregs[12]), [STACK_POINTER] = USER_REGS_OFFSET(uregs[13]), [LINK_REGISTER] = USER_REGS_OFFSET(uregs[14]), [INSTR_POINTER] = USER_REGS_OFFSET(uregs[15]), [USERARG_1] = USER_REGS_OFFSET(uregs[0]), };#elif defined(ARCH_ARM64)#undef USER_REGS_OFFSET#define USER_REGS_OFFSET(reg_name) offsetof(struct user_regs_struct, reg_name) static off_t reg_offset[] = {[SYSARG_NUM] = USER_REGS_OFFSET(regs[8]),[SYSARG_1] = USER_REGS_OFFSET(regs[0]),[SYSARG_2] = USER_REGS_OFFSET(regs[1]),[SYSARG_3] = USER_REGS_OFFSET(regs[2]),[SYSARG_4] = USER_REGS_OFFSET(regs[3]),[SYSARG_5] = USER_REGS_OFFSET(regs[4]),[SYSARG_6] = USER_REGS_OFFSET(regs[5]),[SYSARG_RESULT] = USER_REGS_OFFSET(regs[0]),//https://zhuanlan.zhihu.com/p/42486116//http://blog.chinaunix.net/uid-25564582-id-5852920.html[FRAME_POINTER] = USER_REGS_OFFSET(regs[29]),//64位30是LR寄存器[LINK_REGISTER] = USER_REGS_OFFSET(regs[30]),[STACK_POINTER] = USER_REGS_OFFSET(sp),[INSTR_POINTER] = USER_REGS_OFFSET(pc),[USERARG_1] = USER_REGS_OFFSET(regs[0]),};
這個時候我們可以直接去獲取寄存器內容,判斷路徑,是否是我們需要修改的文件路徑。
/** * Return the *cached* value of the given @Tracers' @reg. * 返回給定@Tracers@reg的緩存值 */word_t peek_reg(const Tracer *Tracer, RegVersion version, Reg reg) { word_t result; assert(version < NB_REG_VERSION); result = REG(Tracer, version, reg); /* Use only the 32 least significant bits (LSB) when running * 32-bit processes on a 64-bit kernel. */ if (is_32on64_mode(Tracer)) result &= 0xFFFFFFFF; return result;} /** * Set the *cached* value of the given @Tracers' @reg. * * 修改寄存器的內容方法,value標識的是指針 */void poke_reg(Tracer *Tracer, Reg reg, word_t value) { //設置之前先判斷是否相等 if (peek_reg(Tracer, CURRENT, reg) == value) //相等直接返回 return; REG(Tracer, CURRENT, reg) = value; //標識他已經被修改 Tracer->_regs_were_changed = true;}
將修改完畢的寄存器內容保存到數組里面,最后通過ptrace PTRACE_SETREGSET action進行寄存器的set。
regs.iov_base = ¤t_sysnum;regs.iov_len = sizeof(current_sysnum); status = ptrace(PTRACE_SETREGSET, Tracer->pid, NT_ARM_SYSTEM_CALL, ?s);if (status < 0) { //note(Tracer, WARNING, SYSTEM, "can't set the syscall number"); return status;}
修改被調試進程的寄存器內容,已達到修改svc參數和返回結果的目的。
這個思路確實是可以實現svc的參數和返回值的修改,但是存在問題。
效率太低,調試線程和被調試線程本身是兩條線程,主要通過線程間交互進行傳遞消息,而且被attch的進行會進行大量的暫停,甚至本身的libc去調用svc的時候也會進行暫停。導致程序卡頓。
當時為了解決這個問題也花了很久查了很多資料。
Seccomp+ptrace:
為了解決這個效率低的問題,看了很多開源框架,比如Strace也在使用ptrace進行svc的跟蹤。
一個打印Syscall調用方法的插件,可以很清楚的打印全部的系統調用,比如文件相關類型函數 網絡相關類型的函數,等...他同樣也可以用在安卓上面, 具體使用方式,國內資料比較多,可以去看一下。
Strace是怎么解決的?用的就是Seccomp+ptrace去做攔截,我們只需要關注我們需要進行攔截的函數即可。
比如常見的IO函數,access,openat,open,fstart等即可。而不需要關注別的系統調用。
seccomp初始化完畢以后,我們只需要在ptrace PTRACE_SETOPTIONS的時候加上PTRACE_O_TRACESECCOMP參數即可。
const unsigned long default_ptrace_options = ( PTRACE_O_TRACESYSGOOD| PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK | PTRACE_O_TRACEVFORKDONE | PTRACE_O_TRACEEXEC | PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXIT); //嘗試開啟ptrace+seccompstatus = ptrace(PTRACE_SETOPTIONS, tracee->pid, NULL,default_ptrace_options | PTRACE_O_TRACESECCOMP);
這樣一來當目標App調用了被我們攔截的系統調用的時候就會走如下case。
case SIGTRAP | PTRACE_EVENT_SECCOMP << 8:
我們直接在這個執行上面的流程設置參數,也是沒問題的。但是這個時候又來一個問題。
Seccomp只能處理svc的before ,也就是當svc執行之前進入到這個case,不能處理after。
因為修改svc的返回結果必須在after里面處理,有人可能會問了為什么要處理返回結果呢?文件重定向只需要處理參數就行了。
參數完全可以在before里面進行處理。為啥還要處理after呢?
答:做指紋mock時候需要在after里面處理,比如socket常見得通訊函數,recv,recvfrom,recvmsg。
他們都是在原始函數調用完畢以后把數據參數放到一個數組里面,如果在before處理這個數組肯定是NULL。
只有在函數執行完畢以后才會將數組的內容進行賦值。比如我之前講的通過netlinker去獲取設備指紋。
https://bbs.pediy.com/thread-271698.htm
這也是大廠的貫通套路,這種指紋想要去mock很難,特別是用內聯svc的方式去獲取。但是用了ptrace想去修改的話就很簡單了。代碼如下,先通過peek_reg寄存器把數據讀到手,然后在把數據處理完畢以后在poke_reg回去。
void NetlinkMacHandler::netlinkHandler_recv(Tracer *tracee) { ssize_t bytes_read = TEMP_FAILURE_RETRY(peek_reg(tracee, CURRENT, SYSARG_RESULT)); if (bytes_read > 0) { word_t buff = peek_reg(tracee, CURRENT, SYSARG_2); //buff長度 auto size = (size_t) peek_reg(tracee, CURRENT, SYSARG_3); char tempBuff[size]; int readStr_ret = read_data(tracee, tempBuff, buff, size); if (readStr_ret != 0) { LOGE("svc netlink handler read_string error %s", strerror(errno)) return; } auto *hdr = reinterpret_cast(tempBuff); //netlink數據包結構體 NetlinkMacHandler::handler_mac_callback_svc(tracee,hdr, bytes_read); //將數據寫入覆蓋掉原來的數據 write_data(tracee, buff, tempBuff, size); }}
為了解決ptrace+seccomp不能處理after的問題想了好久 ,卡了我幾個月之久。后來通過問VA作者發現一個很不錯開源的項目就是proot。
項目地址->https://github.com/proot-me/proot
proot的解決方案也很簡單,只需要一行即可。果然天才需要的是靈感。
poke_reg(tracee, STACK_POINTER, peek_reg(tracee, ORIGINAL, STACK_POINTER));
修改SP寄存器,讓這個方法二次進入,一次修改參數,一次修改返回結果即可。
簡單介紹一下proot這個項目。他就完全符合我們的需求,處理邏輯也類似。
在Linux 里面有一個chroot函數,這個函數可以修改root用戶根目錄的位置,但是這個函數需要root權限才可以用。
比如我想把/data/zhenxi/路徑變成Linux的根目錄。而不是最原始的/,
就可以使用這個proot編譯好以后,直接啟動就可以在免root的環境下進行限制和使用。他的原理也是使用Ptace+seccomp進行限制。
對執行的文件路徑進行替換和處理。我上面發的代碼也都是來自proot。
當然我說的這些也是proot的一小部分功能,更重要的功能是利用ptrace+seccomp實現沙盒文件限制的邏輯。
搞定以后就需要進行注入了,如何把攔截和修改的功能注入到目標App里面,因為Linux特性ptrace只針對當前進程才可以免root進行attch。
四
注入方式
Xposed注入So:
優點非入侵式,不需要修改apk簽名,只在onload里面進行attch當前線程,可實現全部進程的svc修改和mock。
包括文件監聽,過檢測等。
二次打包注入:
入侵式,優點就是可以在免root環境下進行attch,直接把So打進去,然后加載即可,缺點就是修改簽名需要繞過,不過繞過更簡單了。
直接通過SVC的IO重定向把 原始的apk放到任意私有路徑,當對方讀取/data/app/包名/base.apk的時候直接把參數替換成原始的apk路徑即可。這種方式對抗企業殼的重打包檢測依然有效。
五
使用場景
文件讀取監聽&合規檢測:
很多app會去讀取大量別的app私有目錄,比如去遍歷/data/data/xxx/下的文件路徑,獲取讀取SD卡下的其他文件。這些都是不合規或者存在安全隱患問題,用SVC文件文件監聽的方式把對方讀取的路徑打印出來,可以快速的去分析對方app是否合規,是否存在安全隱患。方便進行快速分析和定位。
打印效果如下,讀取哪些文件也是一清二楚:
2022-06-04 15:31:06.910 13927-13960/ I/Zhenxi: io sandbox /vendor/lib64/hw/ -> /vendor/lib64/hw/2022-06-04 15:31:06.910 13951-13951/? I/Zhenxi: io sandbox /vendor/lib64/hw/ -> /vendor/lib64/hw/2022-06-04 15:31:06.910 13927-13960/ I/Zhenxi: io sandbox /vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so -> /vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so2022-06-04 15:31:06.910 13927-13960/ I/Zhenxi: io sandbox /vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so -> /vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so2022-06-04 15:31:06.911 13951-13951/? I/Zhenxi: io sandbox /vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so -> /vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so2022-06-04 15:31:06.911 13951-13951/? I/Zhenxi: io sandbox /proc/self/fd/109 -> /proc/self/fd/1092022-06-04 15:31:06.911 13927-13960/ I/Zhenxi: io sandbox /proc/self/maps -> /proc/self/maps2022-06-04 15:31:06.911 13951-13951/? I/Zhenxi: io sandbox /data/user/0/ /app_virtual_devices/START_UP_0/data/nativeCache/dev_maps_13951_13951 -> /data/user/0/ /app_virtual_devices/START_UP_0/data/nativeCache/dev_maps_13951_139512022-06-04 15:31:06.916 13927-13960/ I/Zhenxi: io sandbox libadreno_utils.so -> libadreno_utils.so2022-06-04 15:31:06.916 13927-13960/ I/Zhenxi: io sandbox /proc/self/maps -> /proc/self/maps2022-06-04 15:31:06.916 13951-13951/? I/Zhenxi: io sandbox /data/user/0/ /app_virtual_devices/START_UP_0/data/nativeCache/dev_maps_13951_13951 -> /data/user/0/ /app_virtual_devices/START_UP_0/data/nativeCache/dev_maps_13951_139512022-06-04 15:31:06.930 13927-13960/ I/Zhenxi: io sandbox /data/user_de/0/ /code_cache/com.android.opengl.shaders_cache -> /data/user/0/ /app_virtual_devices/START_UP_0/user_de/code_cache/com.android.opengl.shaders_cache2022-06-04 15:31:06.930 13951-13951/? I/Zhenxi: io sandbox /data/user/0/ /app_virtual_devices/START_UP_0/user_de/code_cache/com.android.opengl.shaders_cache -> /data/user/0/ /app_virtual_devices/START_UP_0/user_de/code_cache/com.android.opengl.shaders_cache2022-06-04 15:31:06.961 13927-13960/ I/Zhenxi: io sandbox libboost.so -> libboost.so2022-06-04 15:31:06.962 13927-13960/ I/Zhenxi: io sandbox /system/lib64/libboost.so -> /system/lib64/libboost.so2022-06-04 15:31:06.962 13951-13951/? I/Zhenxi: io sandbox /system/lib64/libboost.so -> /system/lib64/libboost.so2022-06-04 15:31:06.962 13951-13951/? I/Zhenxi: io sandbox /proc/self/fd/110 -> /proc/self/fd/1102022-06-04 15:31:06.962 13927-13960/ I/Zhenxi: io sandbox /proc/self/maps -> /proc/self/maps2022-06-04 15:31:06.962 13951-13951/? I/Zhenxi: io sandbox /data/user/0/ /app_virtual_devices/START_UP_0/data/nativeCache/dev_maps_13951_13951 -> /data/user/0/ /app_virtual_devices/START_UP_0/data/nativeCache/dev_maps_13951_139512022-06-04 15:31:06.967 13927-13960/ I/Zhenxi: io sandbox /proc/13927/cmdline -> /proc/13927/cmdline2022-06-04 15:31:06.967 13951-13951/? I/Zhenxi: io sandbox /proc/13927/cmdline -> /proc/13927/cmdline2022-06-04 15:31:06.967 13927-13960/ I/Zhenxi: io sandbox /data/system/migt/migt -> /data/system/migt/migt2022-06-04 15:31:06.967 13951-13951/? I/Zhenxi: io sandbox /data/system/migt/migt -> /data/system/migt/migt ... ...
PASS環境檢測:
Root&magisk
因為一切文件讀取最終底層讀取都是SVC函數去讀取,我們只需要寫個sandbox。
把我們認為的問題關鍵目錄直接全部都進行PASS即可,當目標App去讀到這個路徑以后我們將方法的路徑設置成一個不存在的路徑即可。
代碼如下:
else if (strstr(result, "magisk")) { //直接包含magisk的都給干掉 result = NULL;} else if (strstr(result, "edxposed")) { result = NULL;} else if (strstr(result, "edxp")) { result = NULL;}else if (strstr(result, "lsposed")) { result = NULL;} else if (strstr(result, "libriru") || strstr(result, "/riru")) { result = NULL;} else if (strstr(result, "sandhook")) { result = NULL;} else if (endsWith(result, "/su")) { //su結尾的root文件都直接干掉 result = NULL;}else if (strstr(result, "zygisk")) { result = NULL;}else if (strstr(result, "/data/adb/")) { //這個文件里面包含很多magisk相關的,比如模塊的list /data/adb/modules/ //https://github.com/LSPosed/NativeDetector/blob/master/app/src/main/jni/activity.cpp result = NULL;}
當然這些還是不夠,還有/proc/mounts里面也有一堆magisk文件特征。這個文件可以里面也有一堆特征。
可以在執行之前先生成一份,然后當發現讀取到我們需要IO重定向的路徑直接修改成我們生成的文件即可。
DebugCheck:
還有一些反調試文件,stat status wchan這些也都是在ptrace attch之前進行mock一份,然后IO重定向到新生成的文件路徑即可。
MapsCheck:
maps io重定向的話,不能提前生成,必須在目標app讀取之前進行創建,因為maps 是不斷變化的。以防萬一有人掃描maps去檢測。
在svc的before里面調試線程去創建,然后修改被調試線程的讀取路徑。
AppSign:
很多大廠或者殼子檢測簽名無非幾種,java層檢測和native檢測,這些完全可以Hook 在注入的時候順便把sandhook等框架一起注入。
在配合ptrace+seccomp,Java層的話就是Hook獲取簽名的那幾個方法,然后記得把內存的變量也需要通過反射的方式去set上去。
不能只Hook方法。這塊需要過掉9.0反射限制,可以參考LSP的繞過反射限制代碼項目。
native檢測大部分都是svc openat去讀取文件,然后把apk當成zip進行解壓縮,解析,去計算apk的簽名文件。判斷是否正確。
這種方式很簡單繞過,只需要去在他讀取/data/app/xxx/base.apk的時候,我們把他指向原始包即可繞過。
沙箱的打磨&實踐:
有時候我們經常需要分析一個So, 看看這個So里面讀取了哪些文件,做了哪些事情,調用了哪些Java方法。
我以前挺喜歡用unidbg的,但是發現痛點太多,就是需要補環境,有很多So會調用高版本api,我記得我以前用的時候,只支持23和26版本的SDK,這個時候如果去補環境,真的很累,特別是很多So會去掃描大量的系統文件。這些系統文件都是unidbg里面沒有的,不如我們直接在安卓系統上直接運行這個So。
我們可以先搞個helloword 然后先啟動我們ptrace進行attch。
再配合sandhook和jnitraceforcpp(https://github.com/w296488320/JnitraceForCpp)
這個jnitraceforcpp不是frida的jnitrace,是我以前空閑的時候寫的,代碼沒多少行,但是Hook了全部的jniEnv里面的方法,對方不管調用了什么我們這邊都可以進行打印。hook方法如下:
HOOK_JNI(env, CallObjectMethodV)HOOK_JNI(env, CallBooleanMethodV)HOOK_JNI(env, CallByteMethodV)HOOK_JNI(env, CallCharMethodV)HOOK_JNI(env, CallShortMethodV)HOOK_JNI(env, CallIntMethodV)HOOK_JNI(env, CallLongMethodV)HOOK_JNI(env, CallFloatMethodV)HOOK_JNI(env, CallDoubleMethodV)HOOK_JNI(env, CallVoidMethodV) HOOK_JNI(env, CallStaticObjectMethodV)HOOK_JNI(env, CallStaticBooleanMethodV)HOOK_JNI(env, CallStaticByteMethodV)HOOK_JNI(env, CallStaticCharMethodV)HOOK_JNI(env, CallStaticShortMethodV)HOOK_JNI(env, CallStaticIntMethodV)HOOK_JNI(env, CallStaticLongMethodV)HOOK_JNI(env, CallStaticFloatMethodV)HOOK_JNI(env, CallStaticDoubleMethodV)HOOK_JNI(env, CallStaticVoidMethodV) HOOK_JNI(env, GetObjectField)HOOK_JNI(env, GetBooleanField)HOOK_JNI(env, GetByteField)HOOK_JNI(env, GetCharField)HOOK_JNI(env, GetShortField)HOOK_JNI(env, GetIntField)HOOK_JNI(env, GetLongField)HOOK_JNI(env, GetFloatField)HOOK_JNI(env, GetDoubleField) HOOK_JNI(env, GetStaticObjectField)HOOK_JNI(env, GetStaticBooleanField)HOOK_JNI(env, GetStaticByteField)HOOK_JNI(env, GetStaticCharField)HOOK_JNI(env, GetStaticShortField)HOOK_JNI(env, GetStaticIntField)HOOK_JNI(env, GetStaticLongField)HOOK_JNI(env, GetStaticFloatField)HOOK_JNI(env, GetStaticDoubleField)//常用的字符串操作函數HOOK_JNI(env, NewStringUTF)HOOK_JNI(env, GetStringUTFChars)//HOOK_JNI(env, FindClass)HOOK_JNI(env, ToReflectedMethod)HOOK_JNI(env, FromReflectedMethod)HOOK_JNI(env, GetFieldID)HOOK_JNI(env, GetStaticFieldID)HOOK_JNI(env, NewObjectV)
還有一些常用的字符串操作的函數。
HOOK_SYMBOL_DOBBY(handle, strstr); HOOK_SYMBOL_DOBBY(handle, strcmp); HOOK_SYMBOL_DOBBY(handle, strcpy); HOOK_SYMBOL_DOBBY(handle, strdup); HOOK_SYMBOL_DOBBY(handle, strxfrm);// HOOK_SYMBOL_DOBBY(handle, memcpy); // HOOK_SYMBOL_DOBBY(handle, sprintf);// HOOK_SYMBOL_DOBBY(handle, printf);// HOOK_SYMBOL_DOBBY(handle, snprintf);// HOOK_SYMBOL_DOBBY(handle, vsnprintf);
把這方法全部進行hook以后,再把對方的So加載進來,對方調用了什么方法,做了哪些事情一目了然。
而且最重要的是不需要去補環境。分析效率很高。需要處理什么直接Hook即可。
指紋的對抗:
很多大廠會去獲取設備指紋,Java層那些方法不多說,直接hook就行。
核心都是在native層去處理,system_property_get&read,bootid,UUID,反射內存android id,netlinker獲取網卡。
還有一些就是內聯svc調用讀文件函數也是可以獲取到網卡信息,比如
/sys/class/net/wlan0/address & /sys/devices/virtual/net/wlan0/address
build.prop popen讀取一些設備 如 /sys/devices/soc0/serial_number類似這種。
還有execve去獲取一些設備信息,這些通過svc的IO重定向很容易就可以實現mock和pass。
當讀取的時候我們生產一份新得,指向到新生成的文件即可。
無痕Hook的實現思路:
現在很多native hook思路都是inlinehook ,Got表 ,異常hook(異常信號劫持hook)。
這些思路都是很好的思路,各有各的好處,但是都是有特征,比如inlinehook crc檢測很容易就檢測出來,并且有很多大廠會用shellcode進行繞過,我們能去修改這段指令跳轉到這個某個函數,他當然也可以修改回來。
他只需要把某個方法的指令換成原始的指令,這樣就可以防止被inlinehook Hook(他需要獲取原始指令,可以解析本地的So文件,解析text段,得到最真實的指令信息,保存,然后在函數執行之前在set回內存,都是很好的辦法,還有的干脆直接服務端配置一個服務,直接服務端拉取某個函數的正確指令,都是很好的思路) 當然對抗這也不是沒辦法,我只需要在hook完畢以后再把內存設置成可讀,不可寫,然后Hook mprotect ,不讓他調用mprotect 這樣就可以被shellcode繞過。
說的有點多,重點說一下無痕Hook的實現思路,ptrace有一個很重要的功能就是下斷點,我們只需要在指定內存下斷點,當方法執行到這里以后,直接通過ptrace修改PC寄存器。跳轉到到指定函數即可,把參數也帶過去,因為是執行階段才會修改,而且是直接修改的寄存器,不存在修改指令。所以無需擔心指令的crc檢測不過問題。
測試在64位還是很穩定的,32位的話總有問題,很多32位程序走的是64位的sysnum 。一直報bad syscall num 。原因未知。一直在采坑,還沒填上去。不過還好大部分app都是64位的。
上述的svc攔截核心代碼大部分都能在proot項目里面找到,需要自己移植到android上面 。