Android代码入侵原理解析(一)
2017年初,在滴滴安全沙龍上,滴滴出行安全專家——付超紅,針對App的攻與防進(jìn)行了分享。會(huì)后大家對這個(gè)議題反響熱烈,紛紛求詳情求關(guān)注。現(xiàn)在,付超紅詳細(xì)整理了《Android代碼入侵原理解析》,在滴滴安全應(yīng)急響應(yīng)中心的微信公眾號開始連載。技術(shù)干貨滿滿,敬請關(guān)注。
1.代碼入侵原理
代碼入侵,或者叫代碼注入,指的是讓目標(biāo)應(yīng)用/進(jìn)程執(zhí)行指定的代碼。代碼入侵,可以在應(yīng)用進(jìn)行運(yùn)行過程中進(jìn)行動(dòng)態(tài)分析,也是對應(yīng)用進(jìn)行攻擊的一種常見方式。我把代碼入侵分為兩種類型:靜態(tài)和動(dòng)態(tài)。靜態(tài)代碼入侵是直接修改相關(guān)代碼,在應(yīng)用啟動(dòng)和運(yùn)行之前,指定代碼就已經(jīng)和應(yīng)用代碼關(guān)聯(lián)起來。動(dòng)態(tài)代碼入侵是應(yīng)用啟動(dòng)之后,控制應(yīng)用運(yùn)行進(jìn)程,動(dòng)態(tài)加載和運(yùn)行指定代碼。
2.靜態(tài)代碼入侵
靜態(tài)代碼入侵,有直接和間接的手段。
直接手段是修改應(yīng)用本身代碼。修改應(yīng)用本身代碼,在Android和iOS移動(dòng)操作系統(tǒng)上,一般利用重打包的方式來完成。攻擊者需要對應(yīng)用安裝包文件,完成解包、插入指定代碼、重打包的三個(gè)步驟。現(xiàn)在用到的代碼插樁技術(shù)和這個(gè)比較類似,只不過是代碼注入的工作直接在編譯過程中完成了。
間接手段是修改應(yīng)用運(yùn)行環(huán)境。關(guān)于應(yīng)用運(yùn)行環(huán)境,可以是修改和替換關(guān)鍵系統(tǒng)文件,如xposed通過修改應(yīng)用啟動(dòng)的系統(tǒng)文件 /system/bin/app_process 實(shí)現(xiàn)代碼注入。可以造出一套模擬的系統(tǒng)運(yùn)行環(huán)境,如應(yīng)用運(yùn)行沙箱、應(yīng)用雙開器等。對于Android系統(tǒng),可以自行修改系統(tǒng)編譯rom。
3.動(dòng)態(tài)代碼入侵
這里以Android系統(tǒng)為例,說明動(dòng)態(tài)代碼入侵的整個(gè)過程(單指代碼注入,不包括后續(xù)控制邏輯的實(shí)現(xiàn))。動(dòng)態(tài)代碼入侵需要在應(yīng)用進(jìn)程運(yùn)行過程中,控制進(jìn)程加載和運(yùn)行指定代碼。控制應(yīng)用進(jìn)程,我們需要用到ptrace。ptrace是類unix系統(tǒng)中的一個(gè)系統(tǒng)調(diào)用,通過ptrace我們可以查看和修改進(jìn)程的內(nèi)部狀態(tài),能夠修改目標(biāo)進(jìn)程中的寄存器和內(nèi)存,實(shí)現(xiàn)目標(biāo)進(jìn)程的斷點(diǎn)調(diào)試、監(jiān)視和控制。常見的調(diào)試工具如:gdb, strace, ltrace等,這些調(diào)試工具都是依賴ptrace來工作的。
關(guān)于ptrace,可以參考維基百科的說明:https://en.wikipedia.org/wiki/Ptrace
long ptrace(int request, pid_t pid, void *addr, void *data);
pid:?? 目標(biāo)進(jìn)程
addr:? 目標(biāo)地址
data: ?操作數(shù)據(jù)
request:
PTRACE_ATTACH
PTRACE_DETACH
PTRACE_CONT
PTRACE_GETREGS
PTRACE_SETREGS
PTRACE_POKETEXT
PTRACE_PEEKTEXT
ptrace的功能主要是以下:
1)進(jìn)程掛載
2)進(jìn)程脫離
3)進(jìn)程運(yùn)行
4)讀寄存器
5)寫寄存器
6)讀內(nèi)存
7)寫內(nèi)存
進(jìn)程被掛載后處于跟蹤狀態(tài)(traced mode),這種狀態(tài)下運(yùn)行的進(jìn)程收到任何signal信號都會(huì)停止運(yùn)行。利用這個(gè)特性,可以很方便地對進(jìn)程持續(xù)性的操作:查看、修改、確認(rèn)、繼續(xù)修改,直到滿足要求為止。進(jìn)程脫離掛載后,會(huì)繼續(xù)以正常模式(untraced mode)運(yùn)行。
3.1 動(dòng)態(tài)代碼注入的步驟
1)掛載進(jìn)程
2)備份進(jìn)程現(xiàn)場
3)代碼注入
4)恢復(fù)現(xiàn)場
5)脫離掛載
其中,代碼注入的過程相對復(fù)雜,因?yàn)榇a注入過程和cpu架構(gòu)強(qiáng)相關(guān),需要先了解Android系統(tǒng)底層的ARM架構(gòu)。
3.2 ARM架構(gòu)簡介
ARM處理器在用戶模式和系統(tǒng)模式下有16個(gè)公共寄存器:r0~r15。
有特殊用途的通用寄存器(除了做通用寄存器,還有以下功能):
r0~r3: 函數(shù)調(diào)用時(shí)用來傳遞參數(shù),最多4個(gè)參數(shù),多于4個(gè)參數(shù)時(shí)使用堆棧傳遞多余的參數(shù)。其中,r0還用來存儲函數(shù)返回值。
r13:堆棧指針寄存器sp。
r14:鏈接寄存器lr,一般用來表示程序的返回地址。
r15:程序計(jì)數(shù)器pc,當(dāng)前指令地址。
狀態(tài)寄存器cpsr:
N=1:負(fù)數(shù)或小于(negtive)
Z=1:等于零(zero)
C=1:有進(jìn)位或借位擴(kuò)展
V=1:有溢出
I=1:IRQ禁止interrupt
F=1:FIQ禁止fast
T=1/0:Thumb/ARM狀態(tài)位
其中,T位需要注意。程序計(jì)數(shù)器pc(r15)末位為1時(shí)T位置1,否則T位置0。
代碼動(dòng)態(tài)注入過程中,前面的準(zhǔn)備和后面的收尾工作比較簡單,較復(fù)雜的是中間的代碼注入。整體過程的基礎(chǔ)代碼如下:
3.3 代碼注入過程
代碼注入需要完成在目標(biāo)進(jìn)程內(nèi)加載和運(yùn)行指定代碼。指定代碼的一般形式是so文件。動(dòng)態(tài)加載so需要使用到linker提供的相關(guān)方法。關(guān)于linker,請閱讀《程序員的自我修養(yǎng)-鏈接、裝載與庫》。具體來說,代碼注入過程分為三步,也就是三次函數(shù)調(diào)用:
1)dlopen加載so文件
2)dlsym獲取so的入口函數(shù)地址
3)調(diào)用so入口函數(shù)
和正常調(diào)用函數(shù)相比,通過ptrace在目標(biāo)進(jìn)程中調(diào)用函數(shù)是完全不同的,是通過直接修改寄存器和內(nèi)存數(shù)據(jù)來實(shí)現(xiàn)函數(shù)調(diào)用。具體來說,有幾點(diǎn)需要注意:
1)獲取函數(shù)地址
調(diào)用函數(shù)首先要知道函數(shù)地址。因?yàn)锳SLR(地址空間格局隨機(jī)化,Address Space Layout Randomization)的影響,父進(jìn)程孵化子進(jìn)程時(shí),系統(tǒng)動(dòng)態(tài)庫的基地址會(huì)隨機(jī)變化,具體表現(xiàn)為,相同的系統(tǒng)動(dòng)態(tài)庫在不同子進(jìn)程中的內(nèi)存地址是不同的。我們可以利用下面的簡單公式來計(jì)算得到我們需要用到的相關(guān)函數(shù)在目標(biāo)進(jìn)程中的地址:address = base + offset
其中,base是函數(shù)實(shí)現(xiàn)所在動(dòng)態(tài)庫的基地址,offset是函數(shù)在動(dòng)態(tài)庫中的偏移地址。
在Linux系統(tǒng)中,可以通過/proc/<pid>/maps查看進(jìn)程的虛擬地址空間(查看非當(dāng)前進(jìn)程需要root權(quán)限),包括進(jìn)程的所有動(dòng)態(tài)庫的base。通過動(dòng)態(tài)庫文件名查詢虛擬地址空間獲取base。offset值是函數(shù)地址在動(dòng)態(tài)庫中的偏移,可以直接靜態(tài)查看動(dòng)態(tài)庫文件獲得函數(shù)偏移地址,也可以在其他應(yīng)用運(yùn)行時(shí)計(jì)算得出:offset = address - base。具體到代碼注入,需要用到的函數(shù)dlopen和dlsym,其實(shí)現(xiàn)代碼所在文件為 /system/bin/linker(為什么開發(fā)過程中使用dlopen、dlsym, 編譯時(shí)鏈接的是文件libdl.so,運(yùn)行時(shí)鏈接的卻是另外一個(gè)文件 /system/bin/linker,這里不做詳述)。
2)函數(shù)調(diào)用參數(shù)傳遞和返回值獲取
對于ARM體系來說,函數(shù)調(diào)用遵循的是 ATPCS(ARM-Thumb Procedure Call Standard),ATPCS建議函數(shù)的形參不超過4個(gè),如果形參個(gè)數(shù)少于或等于4,則形參由R0、R1、R2、R3四個(gè)寄存器進(jìn)行傳遞;若形參個(gè)數(shù)大于4,大于4的部分必須通過堆棧進(jìn)行傳遞。函數(shù)調(diào)用的返回值通過R0傳遞。
3) 內(nèi)存分配/獲取
像字符串類型這樣的參數(shù)運(yùn)行時(shí)需要占用內(nèi)存。當(dāng)然我們可以通過調(diào)用malloc來動(dòng)態(tài)申請內(nèi)存。但是,正如之前介紹的,通過ptrace進(jìn)行函數(shù)調(diào)用的過程有些復(fù)雜。我們直接使用棧的內(nèi)存空間更加方便。通過棧指針sp,我們將數(shù)據(jù)放到棧暫時(shí)不用的內(nèi)存空間,也能省去釋放內(nèi)存空間的繁瑣。
4)函數(shù)調(diào)用后重獲控制權(quán)
代碼注入需要多次函數(shù)調(diào)用,我們希望調(diào)用第一個(gè)函數(shù)之后,進(jìn)程馬上停下來,等待后續(xù)其他的函數(shù)調(diào)用。這里,我們需要使用到lr寄存器。通過設(shè)置lr為非法地址(一般設(shè)為0),可以使得函數(shù)返回時(shí)出錯(cuò),觸發(fā)非法指令的signal信號,進(jìn)程停止。然后,我們可以重設(shè)進(jìn)程狀態(tài),執(zhí)行后面其他的函數(shù)調(diào)用。
總結(jié)
以上是生活随笔為你收集整理的Android代码入侵原理解析(一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Activity到底是什么时候显示到屏幕
- 下一篇: WatchDog工作原理