解決Android加殼過程中mprotect調(diào)用失敗的原因分析
問題原由
函數(shù)抽取殼是當(dāng)前最為流行的DEX加殼方式之一,這種加殼方式的主要流程包含兩個(gè)步驟:一、將DEX中需要保護(hù)的函數(shù)指令置空(即抽取函數(shù)體);二、在應(yīng)用啟動(dòng)的過程中,HOOK 類的加載過程,比如ClassLinker::LoadMethod函數(shù),然后及時(shí)回填指令。
筆者在實(shí)現(xiàn)抽取殼的過程中遇到了一個(gè)問題,即在步驟二回填指令之前,需要先調(diào)用mprotect將目標(biāo)內(nèi)存設(shè)置為“可寫”,但在初次嘗試過程中一直調(diào)用失敗,于是有了今天這篇文章。
本文探討的主要內(nèi)容是mprotect調(diào)用失敗的根本原因,以及在加殼實(shí)現(xiàn)中的解決方案,通過本文的闡述,一方面能夠幫助遇到同類問題的小伙伴解決心中的疑惑,另一方面能夠給大家提供可落地的實(shí)現(xiàn)方案。
調(diào)用mprotect修改內(nèi)存失敗的現(xiàn)象
以下代碼塊截取自自定義LoadMethod函數(shù),其目標(biāo)是將目標(biāo)函數(shù)指令所在內(nèi)存頁的屬性修改為可寫——通過mprotect函數(shù)的參數(shù)“PROT_WRITE”指定,實(shí)際結(jié)果是mprotect調(diào)用失敗了,返回”-1“,errno為”13“
int pagesize = sysconf(_SC_PAGESIZE); int protectsize = pagesize; byte *code_item_start = static_cast<byte *>(code_item_addr) + 16; void *protectaddr = (void*) ((int) code_item_start - ((int) code_item_start % pagesize) - pagesize); LOGD("process:%d,enter loadmethod:protectaddr:%p,protectsize:%d", getpid(), protectaddr, protectsize); int result = mprotect(protectaddr, protectsize, PROT_WRITE); LOGD("mprotect return 0: %d, errno: %d", result, errno);
”13“號(hào)errno的符號(hào)為EACCES,查看linux手冊可知是權(quán)限問題。手冊中給出一個(gè)可能的場景,即如果使用mmap映射一個(gè)以”只讀“模式打開的文件,然后使用mprotect嘗試修改內(nèi)存屬性為可寫,就會(huì)返回EACCES錯(cuò)誤。
EACCES The memory cannot be given the specified access. This can
happen, for example, if you mmap(2) a file to which you
have read-only access, then ask mprotect() to mark it
PROT_WRITE.
接下來我們將沿著這個(gè)可能的場景,首先驗(yàn)證DEX文件是否以只讀模式打開,然后再進(jìn)行下一步分析。
mprotect調(diào)用失敗的原因分析
使用strace跟蹤應(yīng)用的系統(tǒng)調(diào)用,驗(yàn)證了DEX文件的打開模式為只讀模式——"O_RDONLY",然后通過mmap2將DEX文件映射進(jìn)內(nèi)存,內(nèi)存屬性為只讀的私有映射。
[pid 13190] openat(AT_FDCWD, "/storage/emulated/0/payload.dex", O_RDONLY|O_LARGEFILE) = 49
mmap2(NULL, 2121728, PROT_READ, MAP_PRIVATE, 49, 0) = 0xcef7a000
為了進(jìn)一步證實(shí)并徹底理清背后的邏輯,我研究了下mprotect的設(shè)計(jì)文檔[1]。mprotect是用戶空間PAX的一部分,它的核心目標(biāo)是緩解可利用內(nèi)存漏洞被利用的情況,所以我理解mprotect實(shí)際上就是“memory protect”,它的主要目的是從安全的角度保護(hù)內(nèi)存:
The goal of MPROTECT is to help prevent the introduction of new executable
code into the task's address space. This is accomplished by restricting the
mmap() and mprotect() interfaces.
mprotect通過內(nèi)存屬性控制內(nèi)存的訪問權(quán)限,其中安全狀態(tài)良好的屬性組合包括如下幾種:
VM_WRITE
VM_MAYWRITE
VM_WRITE | VM_MAYWRITE
VM_EXEC
VM_MAYEXEC
VM_EXEC | VM_MAYEXEC
即內(nèi)存要么是“可寫”的,要么是“可執(zhí)行”的,“可寫”與“可執(zhí)行”必須互斥,這樣才能阻斷“寫入并執(zhí)行”的內(nèi)存攻擊。
理解了mprotect的設(shè)計(jì)理念之后,我們再回到本文所遇到的問題本身:為什么以只讀方式打開的DEX文件映射到內(nèi)存之后,無法使用mprotect修改為“可寫”內(nèi)存?
根據(jù)mprotect設(shè)計(jì)文檔的闡述,mprotect主要通過VM_MAYWRITE控制內(nèi)存是否可被設(shè)置為“可寫”,該屬性的設(shè)置時(shí)機(jī)在mmap調(diào)用之時(shí):
VM_WRITE | VM_MAYWRITE or VM_MAYWRITE if PROT_WRITE was requested at
mmap() time
mmap首先將所有可能的屬性標(biāo)致置位,然后再進(jìn)行合法性檢查:
kernel/msm/+/refs/heads/android-msm-vega-4.4-oreo-daydream/mm/mmap.c
/* Do simple checking here so the lower-level routines won't have * to. we assume access permissions have been handled by the open * of the memory object, so we don't do any here. */ vm_flags |= calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) | mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
如果文件打開時(shí)未設(shè)置“可寫”屬性,則清除“VM_MAYWRITE”屬性。
kernel/msm/+/refs/heads/android-msm-vega-4.4-oreo-daydream/mm/mmap.c
if (!(file->f_mode & FMODE_WRITE)) vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
最后mprotect會(huì)對(duì)相關(guān)屬性進(jìn)行檢查,如果VM_MAYWRITE沒有被設(shè)置,則不可通過mprotect設(shè)置內(nèi)存的寫屬性,返回EACCES錯(cuò)誤標(biāo)識(shí):
kernel/msm/+/refs/heads/android-msm-vega-4.4-oreo-daydream/mm/mprotect.c
/* newflags >> 4 shift VM_MAY% in place of VM_% */ if ((newflags & ~(newflags >> 4)) & (VM_READ | VM_WRITE | VM_EXEC)) { error = -EACCES; goto out; }
通過strace日志可以證實(shí)mmap DEX文件到內(nèi)存的過程中并沒有設(shè)置VM_MAYWRITE和VM_WRITE,所以直接使用mprotect設(shè)置內(nèi)存為“可寫”的行為會(huì)被拒絕。
mmap2(NULL, 2121728, PROT_READ, MAP_PRIVATE, 49, 0) = 0xcef7a000
綜上,mprotect修改內(nèi)存為可寫的整個(gè)邏輯如下:
系統(tǒng)以只讀模式打開DEX文件,所以mmap在映射文件時(shí)清除了VM_MAYWRITE標(biāo)志,導(dǎo)致接下來在調(diào)用mprotect修改內(nèi)存為可寫的過程中,mprotect檢測目標(biāo)內(nèi)存未設(shè)置VM_MAYWRITE標(biāo)志,返回EACCES錯(cuò)誤代碼。
兩種可行的解決方案
在研究清楚原因之后,我們再來聊聊可能的解決方案。我這里給出兩種經(jīng)過驗(yàn)證的思路:
1)hook openat函數(shù),設(shè)置文件打開時(shí)的屬性為可讀寫——O_RDWR;
if(strstr(pathname,"payload")){ LOGD("[myopenat] path: %s, flags: %d", (char*)pathname, flags); flags &= (~O_RDONLY); flags |= O_RDWR; }
2)hook mmap函數(shù),或者在mmap之前修改傳入mmap的標(biāo)簽,直接將內(nèi)存屬性修改為“可寫”。這里我們以后面一種思路為例,HOOK MemMap::MapFileAtAddress函數(shù),在調(diào)用mmap映射文件之前修改prot參數(shù):
art/runtime/mem_map.cc
void* myMapFileAtAddr(int expected_ptr, int byte_count, int prot, int flags, int fd, int start, int low_4gb, int reuse, char *filename, int error_msg){ if(strstr(filename, "payload")) { LOGD("[myMapFileAtAddr] file name contains 'payload': %s, prot: %d, flags: %d, fd: %d", filename, prot, flags, fd); prot |= PROT_WRITE; } void* res = oriMapFileAtAddr(expected_ptr, byte_count, prot, flags, fd, start, low_4gb, reuse, filename, error_msg); return res; }
小結(jié)
網(wǎng)絡(luò)上很多關(guān)于抽取殼實(shí)現(xiàn)的教程都沒有提過mprotect的問題,默認(rèn)mprotect修改內(nèi)存是成功的,這可能是因?yàn)榇蠖鄶?shù)人都是通過模擬器進(jìn)行實(shí)驗(yàn)。然而,如果我們要做線上的加殼產(chǎn)品,面向生產(chǎn)環(huán)境進(jìn)行開發(fā)的話,mprotect調(diào)用失敗的問題大概率會(huì)遇到,希望本文能有所幫助。
參考:
[1].mprotect設(shè)計(jì)文檔:https[:][/][/]pax[.]grsecurity[.]net[/]docs[/]mprotect[.]txt
到此這篇關(guān)于Android加殼過程中mprotect調(diào)用失敗的原因及解決方案的文章就介紹到這了,更多相關(guān)Android加殼mprotect調(diào)用失敗內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用Android實(shí)現(xiàn)比較炫酷的自定義View
自定義View、多線程、網(wǎng)絡(luò),被認(rèn)為是Android開發(fā)者必須牢固掌握的最基礎(chǔ)的三大基本功,這篇文章主要給大家介紹了關(guān)于如何利用Android實(shí)現(xiàn)比較炫酷的自定義View的相關(guān)資料,需要的朋友可以參考下2021-07-07Android中使用itemdecoration實(shí)現(xiàn)時(shí)間線效果
這篇文章主要介紹了Android中使用itemdecoration實(shí)現(xiàn)時(shí)間線效果,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02Android TextView實(shí)現(xiàn)圖文混合編排的方法
這篇文章主要為大家詳細(xì)介紹了Android TextView實(shí)現(xiàn)圖文混合編排的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07Android 開發(fā)中fragment預(yù)加載問題
這篇文章主要介紹了Android 開發(fā)中fragment預(yù)加載問題的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-01-01Android中斷并重啟一個(gè)Thread線程的簡單方法
下面小編就為大家?guī)硪黄狝ndroid中斷并重啟一個(gè)Thread線程的簡單方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02