TLB 快取延遲重新整理漏洞 CVE-2018-18281 解析
最近, 業內發現了一批記憶體管理系統的漏洞, project 0 的Jann Horn 放出了其中一個漏洞CVE-2018-18281 的writeup , CVE-2018-18281 是一個 linux kernel 的通用漏洞, 這個漏洞的模式比較罕見, 不同於常規的記憶體溢位類漏洞, 也不是常見的 UAF 漏洞, 它是由記憶體管理系統的底層邏輯錯誤導致的, 根本原因是 TLB 快取沒有及時重新整理造成虛擬地址複用, 可以實現較為穩定的提權利用.
TLB
linux 核心通過多級頁表 實現虛擬記憶體機制, 為了提高訪問速度, 一些對映資訊會被快取在TLB 裡, cpu 在訪問一個虛擬地址的時候, 會先查詢 TLB , 如果沒有命中, 才去遍歷主存裡的多級頁表, 並將查詢到的對映關係填入 TLB
反過來, 如果某個對映關係要解除, 除了在主存裡的相關表項要刪除, 還需要對多個cpu core 同步執行 TLB 重新整理, 使得在所有 TLB 快取裡該對映關係消除, 否則就會出現不一致.
上述關於 TLB 和記憶體對映的說明只是簡化版本, 用於簡單理解這個漏洞的原因, 真正的實現不同作業系統, 不同體系架構, 都不一樣. 可以查閱晶片手冊, 如TLBs, Paging-Structure Caches, and Their Invalidation 和一些分析, 如Reverse Engineering Hardware Page Table Caches
漏洞
先看兩個系統呼叫
這兩個系統呼叫表面上看八竿子打不著, 但在 linux 核心的實現裡, 他們的呼叫鏈條會出現一個競態條件異常
1) sys_mremap() -> mremap_to()->move_vma()->move_page_tables(). move_page_tables() first calls move_ptes() in a loop, then performs a TLB flush with flush_tlb_range(). 2) sys_ftruncate()->do_sys_ftruncate()->do_truncate()->notify_change() ->shmem_setattr()->unmap_mapping_range()->unmap_mapping_range_tree() ->unmap_mapping_range_vma() ->zap_page_range_single()->unmap_single_vma() ->unmap_page_range()->zap_pud_range()->zap_pmd_range()->zap_pte_range() can concurrently access the page tables of a process that is in move_page_tables(), between the move_ptes() loop and the TLB flush.
mremap 底層實現主要是 move_ptes 函式
89 static void move_ptes(struct vm_area_struct *vma, pmd_t *old_pmd, 90unsigned long old_addr, unsigned long old_end, 91struct vm_area_struct *new_vma, pmd_t *new_pmd, 92unsigned long new_addr, bool need_rmap_locks) 93 { 94struct address_space *mapping = NULL; 95struct anon_vma *anon_vma = NULL; 96struct mm_struct *mm = vma->vm_mm; 97pte_t *old_pte, *new_pte, pte; 98spinlock_t *old_ptl, *new_ptl; ======================== skip ====================== 133old_pte = pte_offset_map_lock(mm, old_pmd, old_addr, &old_ptl); 134new_pte = pte_offset_map(new_pmd, new_addr); 135new_ptl = pte_lockptr(mm, new_pmd); 136if (new_ptl != old_ptl) 137spin_lock_nested(new_ptl, SINGLE_DEPTH_NESTING); 138arch_enter_lazy_mmu_mode(); 139 140for (; old_addr < old_end; old_pte++, old_addr += PAGE_SIZE, 141new_pte++, new_addr += PAGE_SIZE) { 142if (pte_none(*old_pte)) 143continue; 144pte = ptep_get_and_clear(mm, old_addr, old_pte); 145pte = move_pte(pte, new_vma->vm_page_prot, old_addr, new_addr); 146pte = move_soft_dirty_pte(pte); 147set_pte_at(mm, new_addr, new_pte, pte); 148} 149 150arch_leave_lazy_mmu_mode(); 151if (new_ptl != old_ptl) 152spin_unlock(new_ptl); 153pte_unmap(new_pte - 1); 154pte_unmap_unlock(old_pte - 1, old_ptl); 155if (anon_vma) 156anon_vma_unlock_write(anon_vma); 157if (mapping) 158i_mmap_unlock_write(mapping); 159 }
結合上面程式碼, 有兩點需要注意
- 鎖, 133 ~ 137 這幾行目的是獲取 pmd (pmd 指標指向一個存滿了 pte 結構的頁面) 的鎖 (包括舊的和新的), 151 ~ 154 這幾行是釋放 pmd 鎖
- ptes 拷貝, 對一個 pmd 裡的所有 pte 執行拷貝操作, 144 這一行呼叫 ptep_get_and_clear 將 old_pte 的值賦值給臨時變數 pte 並清空舊的頁表項, 147 這一行呼叫 set_pte_at 將剛剛的 pte 賦值給 new_pte 指標
簡單而言, move_ptes 將舊的 pmd 頁的值 ( ptes ) 拷貝到了新的 pmd 頁, 這就是 mremap 函式在底層的實現, 它並不需要刪除舊地址對應的 pages, 只需要將舊地址關聯到的 ptes 拷貝到新地址關聯的頁表, 這種拷貝是按照 pmd 為單位進行的, 每處理完一個 pmd, 對應的 pmd lock 就會釋放.
ftruncate 函式將檔案大小變為指定的大小, 如果新的值比舊的值小, 則需要將檔案在記憶體的虛存空間變小, 這需要呼叫到 zap_pte_range 函式
1107 static unsigned long zap_pte_range(struct mmu_gather *tlb, 1108struct vm_area_struct *vma, pmd_t *pmd, 1109unsigned long addr, unsigned long end, 1110struct zap_details *details) 1111 { 1112struct mm_struct *mm = tlb->mm; 1113int force_flush = 0; 1114int rss[NR_MM_COUNTERS]; 1115spinlock_t *ptl; 1116pte_t *start_pte; 1117pte_t *pte; 1118swp_entry_t entry; 1119 1120 again: 1121init_rss_vec(rss); 1122start_pte = pte_offset_map_lock(mm, pmd, addr, &ptl); 1123pte = start_pte; 1124flush_tlb_batched_pending(mm); 1125arch_enter_lazy_mmu_mode(); 1126do { 1127pte_t ptent = *pte; ========================== skip ========================== 1146ptent = ptep_get_and_clear_full(mm, addr, pte, 1147tlb->fullmm); 1148tlb_remove_tlb_entry(tlb, pte, addr); ========================== skip ========================== 1176entry = pte_to_swp_entry(ptent); ========================== skip ========================== 1185if (unlikely(!free_swap_and_cache(entry))) 1186print_bad_pte(vma, addr, ptent, NULL); 1187pte_clear_not_present_full(mm, addr, pte, tlb->fullmm); 1188} while (pte++, addr += PAGE_SIZE, addr != end); 1189 1190add_mm_rss_vec(mm, rss); 1191arch_leave_lazy_mmu_mode(); 1192 1193/* Do the actual TLB flush before dropping ptl */ 1194if (force_flush) 1195tlb_flush_mmu_tlbonly(tlb); 1196pte_unmap_unlock(start_pte, ptl); ========================== skip ========================== 1212return addr; 1213 }
結合上面程式碼, 有三點需要注意,
- 鎖, 1122 行獲取了 pmd 的鎖, 1196 行釋放了 pmd 的鎖, 這裡的 pmd 鎖跟 move_ptes 函式裡的是同一個東西
- pte, 1146 行清空了頁表項
- page, 1185 行呼叫函式 free_swap_and_cache 釋放了 pte 對應的 page cache, 將物理頁面釋放, 這是與 move_ptes 不同的地方
將上述兩個函式的流程放到一起分析, 假設下面這種情況:
假設一個程序有 A,B,C 三個執行緒:
- 1) A 對映一個檔案 a 到地址 X, 對映條件為: PROT_READ , MAP_SHARED
- 2) C 迴圈讀取 X 的內容
-
3) A 呼叫 mremap 重新對映 X 到 Y, 這個呼叫會執行下面兩個函式:
-
3.1) move_ptes , 該函式做如下操作:
- 3.1.1) 獲取 X 頁表和 Y 頁表的鎖
- 3.1.2) 遍歷 X 對應頁表的 pte , 釋放之, 並在 Y 頁表重建這些 pte
- 3.1.3) 釋放 Y 頁表的鎖
- 3.1.4) 釋放 X 頁表的鎖
- 3.2) flush_tlb_range : 重新整理 X 對應的 TLB 快取
-
3.1) move_ptes , 該函式做如下操作:
-
4) B 呼叫 ftruncate 將檔案 a 的檔案大小改為 0, 這個呼叫會執行下面操作:
- 4.1) 獲取 Y 頁表的鎖
- 4.2) 刪除 Y 對應的頁表
- 4.3) 釋放 Y 對應的 pages
- 4.4) 重新整理 Y 對應的 TLB 快取
說明: 實際上 X 和 Y 是兩塊記憶體區域, 也就是說可能比一個 pmd 所容納的地址範圍大, 不管是 mremap 還是 ftruncate, 底層實現會將 X 和 Y 按照 pmd 為單位迴圈執行上表的操作, 即上表所說的 X 頁表實際指的是 X 記憶體區域裡的某個 pmd, 這裡是為了表達方便簡化處理, 下面的描述也是一樣.
這裡存在的競態條件是當 4.3 已經執行完畢 (3.1.3 釋放 Y 鎖 4.1 就可以執行), 地址 Y 的記憶體已經釋放, 物理頁面已經返回給夥伴系統 , 並再一次分配給新的虛擬記憶體, 而此時 3.2 還沒有執行, 這種情況下, 雖然 X 的對映關係在頁表裡已經被清空, 但在 TLB 快取裡沒有被清空, 執行緒 C 依然可以訪問 X 的記憶體, 造成地址複用
注意: 除了可以用 ftruncate 函式來跟 mremap 競爭, 還有一個 linux 系統特有的 系統函式 fallocate 也可以起到同樣的效果, 原因很簡單, fallocate 和 ftruncate 的底層呼叫鏈是一樣的 sys_fallocate()->shmem_fallocate()->shmem_truncate_range() ->shmem_undo_range()->truncate_inode_page()->unmap_mapping_range
v4.9 之前的核心都是上述列表顯示的程式碼邏輯
v4.9 之後的核心, move_ptes 的邏輯與上述有些許不同
注意: 在 versions > 4.9 的 linux 核心, Dirty 標記的頁面會在 move_ptes 函式內部重新整理 TLB , 而不是等到 3.2 由 flush_tlb_range 函式去重新整理, 因此, race 發生之後, 執行緒 C 能通過 X 訪問到的記憶體都是之前 non-Dirty 的頁面, 即被寫過的頁面都無法複用. 這點改變會對 poc 和 exploit 造成什麼影響? 留給大家思考.
簡單版的 poc
根據上述分析, 一個簡單的 poc 思路就出來了, 通過不斷檢測執行緒 C 從地址 X 讀取的內容是不是初始內容就可以判斷 race 是否被觸發, 正常情況下, C 讀取 X 只會有兩種結果, 一種是 mremap 徹底完成, 即 3.2 執行完畢, 此時地址 X 為無效地址, C 的讀操作引發程序奔潰退出, 第二種是 mremap 還未完成, C 讀取的地址返回的是 X 的初始內容, 只有這兩種情況才符合 mremap 函式的定義. 但是由於漏洞的存在, 實際執行會存在第三種情況, 即 C 讀取 X 不會奔潰(3.2 還沒執行, 地址對映還有效), 但內容變了( 4.3 執行完畢, 物理頁面已經被其他地方複用)
這份 poc 可以清晰看出 race 是怎麼發生的, 需要注意, 這份 poc 必須配合核心補丁才能穩定觸發 race , 否則命中率非常低, 補丁通過在 move_page_tables 函式呼叫 flush_tlb_range 之前(即 3.2 之前)增加一個大迴圈來增大 race 條件的時間視窗以提高命中率
上述 poc 的執行結果是, 大部分情況下 poc 奔潰退出, 少數情況下讀取 X 會返回一個被其他地方複用的頁面
這離穩定提權還有很遠的距離, 為了得到穩定利用, 至少有兩個問題需要解決:
- 如何提高 race 的命中率
- 怎麼實現提權
如何提高 race 的命中率
要提高本漏洞 race 的命中率, 就是要增大 move_ptes 函式和 flush_tlb_range 函式之間的時間間隔
怎麼才能增加這倆函式執行的時間間隔呢?
這裡要引入linux核心的程序搶佔 概念, 如果目標核心是可搶佔的 (CONFIG_PREEMPT=y) , 則如果能讓程序在執行 flush_tlb_range 函式之前被搶佔, 那麼 race 的時間視窗就夠大了, 使用者空間的普通程式能不能影響某個程序的排程策略呢? 答案是肯定的.
有兩個系統函式可以影響程序的排程
- sched_setaffinity 函式用來繫結程序到某個 cpu core
- sched_setscheduler 函式用來設定程序的排程策略和排程引數
使用這兩個函式將 poc 修改為下面的方案,
新建 A,B,C,D 四個執行緒:
- 1) A 對映一個檔案 a 到地址 X, A 繫結到核 c1, A 排程策略設定為 SCHED_IDLE
- 2) C 繫結到核 c1, C 阻塞在某個 pipe, pipe 返回則呼叫 ftruncate 將檔案 a 的檔案大小改為 0
-
3) A 呼叫 mremap 重新對映 X 到 Y, 這將執行下面兩個函式:
- 3.1) move_ptes
- 3.2) flush_tlb_range
- 4) D 繫結到核 c2, 監控程序的記憶體對映情況,如果發生變化則通過寫 pipe 喚醒 C
- 5) B 繫結到核 c3, 迴圈讀取 X 的內容, 並判斷是否還是初始值
注意: mremap 執行 move_ptes 函式會引發記憶體狀態變化, 這種變化可以通過 使用者態檔案 /proc/pid/status 檔案獲取, 這就是執行緒 D 的作用
此時, 通過監控執行緒 D 喚醒 C, 由於A 和 C 繫結在同一個核心 c1, 且 A 的排程策略被設定
為最低優先順序 SCHED_IDLE, C 的喚醒將搶佔 A 的執行, 如此一來, 3.2 的執行就可能被延遲.
C 被喚醒後立即執行 ftruncate 釋放 Y 的記憶體觸發漏洞.
通過上述方案可以理論上讓執行緒 A 在執行 3.1 後, 執行 3.2 前被掛起,
從而擴大 3.1 和 3.2 的時間間隔
這個 poc 是根據上述思路寫的
改進版的 poc
實測發現上述 poc 觸發率還是低, 借鑑 Jann Horn 的思路, 繼續如下修改 poc
改進版方案: 新建 A,B,C,D,E 五個執行緒:
- 1) A 對映一個檔案 a 到地址 X, A 繫結到核 c1, A 排程策略設定為 SCHED_IDLE
- 2) C 繫結到核 c1, C 阻塞在某個 pipe, pipe 返回則立即將 A 重新繫結到核 c4, 並呼叫 ftruncate 將檔案 a 的檔案大小改為 0
-
3) A 呼叫 mremap 重新對映 X 到 Y
- 3.1) move_ptes
- 3.2) flush_tlb_range
- 4) D 繫結到核 c2, 監控程序的記憶體對映情況,如果發生變化則通過寫 pipe 喚醒 C
- 5) B 繫結到核 c3, 迴圈讀取 X 的內容, 並判斷是否還是初始值
- 6) E 繫結到核 c4, 執行一個死迴圈.
改進的地方有兩點, 1 是增加一個 E 執行緒繫結到核 c4 並執行死迴圈, 2 是執行緒 C 被喚醒後立刻重繫結執行緒 A 到核 c4, 即讓 A 和 E 在同一個核上
這個改變會提高 race 觸發的命中率, 個人判斷原因是由於當 C 的管道返回後手動執行重繫結操作會比執行其他操作更容易導致 A 立即被掛起
改進版 poc 程式碼 是根據上述思路寫的
利用這個 poc, 我們可以將這個漏洞的 race 命中率提升到可以接受的程度.
物理頁面管理
現在我們可以在比較短的時間內穩定觸發漏洞, 得到一片已經被釋放的物理頁面的使用權,
而且可讀可寫, 怎麼利用這一點來提權?
這裡需要了解實體記憶體的分配和釋放細節, 實體記憶體管理屬於夥伴系統 , 參考記憶體管理
物理頁面的管理是分層的:
- node: NUMA 體系架構有 node 的概念, 不同 node 的實體記憶體是分開管理的
-
zone: 根據實體記憶體的區域分若干種 zone, 不同場景會優先向不同的 zone 分配 , 比如使用者空間申請記憶體, 會優先從 ZONE_NORMAL 這個 zone 分配, 如果不夠再從其他 zone 分配
- ZONE_DMA
- ZONE_NORMAL
- ZONE_HIGHMEM
- 其他
-
migration-type: 核心根據可遷移性對頁面進行分組管理, 用於 anti-fragmentation, 可以參考核心頁面遷移與反碎片機制
- MIGRATE_UNMOVABLE
- MIGRATE_RECLAIMABLE
- MIGRATE_MOVABLE
__alloc_pages_nodemask 函式是 zoned buddy allocator 的分配入口, 它有快慢兩條路徑:
-
get_page_from_freelist , 快路徑
-
1) if order == 0, 從 per-cpu 的指定 zone 指定 migratetype 的 cache list 裡獲取 page
- pcp = &this_cpu_ptr(zone->pageset)->pcp
- list = &pcp->lists[migratetype]
- page = list_entry(list->next, struct page, lru);
-
2) __rmqueue_smallest : 在指定遷移型別下自底向上進行各階遍歷查詢所需的空閒頁面
- area = &zone->free_area[current_order]
- list = &area->free_list[migratetype]
- page = list_entry(list->next, struct page, lru);
- 3) __rmqueue_cma,連續記憶體分配器 用於DMA對映框架下提升連續大塊記憶體的申請
- 4) __rmqueue_fallback, 如果在指定遷移型別下分配失敗,且型別不為MIGRATE_RESERVE時, 就在 fallbacks 數組裡找到下一個 migratetype, 由此獲得的階號和遷移型別查詢zone->free_area[]->free_list[]空閒頁面管理連結串列
-
1) if order == 0, 從 per-cpu 的指定 zone 指定 migratetype 的 cache list 裡獲取 page
-
__alloc_pages_slowpath, 慢路徑
- 略
從漏洞利用的角度, 我們希望將漏洞釋放的物理頁面儘可能快的被重新分配回來, 所以, 用來觸發漏洞釋放物理頁面的場景和重新申請物理頁面用來利用的場景, 這兩種場景的 zone, migratetype 最好一致, 而且這兩個場景的觸發最好在同一個 cpu core 上.
比如, 觸發漏洞時, 通過使用者空間 mmap 一片地址, 然後訪問這片地址觸發實體記憶體分配, 這種分配大概率是從 ZONE_NORMAL 而來, 而且頁面大概率是 MIGRATE_MOVABLE 的, 然後用 ftruncate 釋放, 這些頁面很可能會掛在當前 cpu 的 freelist 上.所以, 漏洞利用的時候如果是在其他 cpu core 觸發申請物理頁面, 則可能申請不到目標頁面, 或者, 觸發申請物理頁面的場景如果是某種 dma 裝置, 那麼也大概率命中不到目標頁面.
怎麼實現提權
根據上述實體記憶體管理的分析, 選擇使用檔案的 page cache 用於重新申請目標物理頁面, 在此基礎上, 想辦法實現提權
linux 上硬碟檔案的內容在核心用 page cache 來維護, 如果漏洞觸發後釋放的頁面被用於某個檔案的 page cache, 則我們擁有了讀寫該檔案的能力, 如果這個檔案恰好是使用者態的重要動態庫檔案, 正常情況下普通程序無法改寫這種檔案, 但通過漏洞普通程序可以改寫它, 這樣就可以通過修改動態庫檔案的程式碼段來提權.
上述利用思路的關鍵有3點:
- 選擇目標動態庫檔案
- 選擇目標檔案要改寫的位置
- 提高目標位置所在頁面的命中率
這個動態庫必須是能被高許可權程序所使用
目標位置最好是頁面對齊的, 這樣目標位置可以以頁面為單位載入進記憶體, 或者以頁面為單位置換到硬碟
目標位置被呼叫的時機不能太頻繁, 要不然修改操作會影響系統穩定性, 而且呼叫時機必須可以由普通程序觸發
下面是一個符合上述條件的動態庫和函式:
- libandroid_runtime.so 動態庫
-
com_android_internal_os_Zygote_nativeForkAndSpecialize 函式
- 這個函式被 zygote 呼叫, zygote 程序是一個特權程序
- 這個函式在 libandroid_runtime.so (pixel2 PQ1A.181105.017.A1) 檔案的偏移是 0x157000, 這個偏移是頁面對齊的
- 這個函式一般情況下不會被呼叫, 只有啟動新的 app 時會被 zygote 呼叫, 可以由普通 app 觸發 zygote 去執行
利用思路
漏洞觸發 race 後, 讓釋放的物理頁面剛好被用於目標頁面( libandroid_runtime.so 檔案的 offset = 0x157000 這個頁面), 再可以通過 UAF 地址注入 shellcode 到目標位置, 從而改寫 com_android_internal_os_Zygote_nativeForkAndSpecialize 函式的程式碼邏輯, 最後發訊息觸發 zygote 去執行 shellcode
如何提高檔案 page cache 命中率
這節解決的問題是, 怎麼控制 race 釋放的頁面剛好能被目標頁面使用
這篇論文 的 section VIII-B 介紹了一種演算法用於精確控制一個 file page cache 的載入
- 1) 開啟一個大檔案 a, mmap 到記憶體
- 2) 開啟目標檔案 b, mmap 到記憶體
-
3) 在一個迴圈內, 執行:
-
3.1) 按照 pagesize 逐頁面讀取 a 的內容
這會導致核心申請大量 page cache 來裝載檔案 a,
從而迫使其他檔案的 page cache 被置換到硬碟 - 3.2) 判斷目標頁面 X 是否在記憶體裡, 如果不是, 跳轉到 4.1
-
3.1) 按照 pagesize 逐頁面讀取 a 的內容
-
4) 在一個迴圈內, 執行:
-
4.1) 按照 pagesize 逐頁面讀取 b 的內容, 但遇到目標頁面 X 則跳過
這會導致目標檔案除目標頁面 X 之外其他頁面被重新裝載回記憶體
- 4.2) 判斷目標頁面 X 是否在記憶體裡, 如果是, 跳轉到 3.1
-
4.1) 按照 pagesize 逐頁面讀取 b 的內容, 但遇到目標頁面 X 則跳過
- 5) 如果讀取完全部 b 的內容, 目標頁面 X 仍然沒有在記憶體裡, 結束.
通過上述演算法, 可以讓一個目標檔案的目標頁面 X 被置換到硬碟, 而該檔案其他頁面保留在記憶體裡, 這樣在漏洞觸發之後, 再來訪問目標頁面, 則很大機會會分配剛剛釋放的物理頁面給目標頁面
注意: mincore 函式可以用來判斷一個區域內的記憶體是在實體記憶體中或被交換出磁碟 上述演算法在 linux 的實現依賴於 mincore
exploit code
我改了一份exploit 程式碼在這裡 , 主要包含下面幾個檔案:
- compile.sh
- shellcode.s
- exp.c
- watchdog.c
compile.sh
這是編譯指令碼
1) aarch64-linux-gnu-as arm_shellcode.s -o arm_shellcode.o 2) aarch64-linux-gnu-ld arm_shellcode.o -o arm_shellcode 3) aarch64-linux-gnu-objcopy --dump-section .text=arm_shellcode.bin arm_shellcode 4) xxd -i arm_shellcode.bin > arm_shellcode.h 5) make
1~3 是將彙編檔案 arm_shellcode.s 編譯成二進位制並將可執行檔案的程式碼段 (.text) 提取到檔案 arm_shellcode.bin
4 使用 linux 的 xxd 工具將 arm_shellcode.bin 放進一個 c 語言分格的陣列,後續在 c 程式碼裡以陣列變數的形式操作它
5 根據 Android.mk 編譯可執行檔案
shellcode.s
下面簡單看一下 shellcode.s 彙編,不感興趣可以略過
-
shellcode.s 本身很簡單: 讀取檔案 “/proc/self/attr/current” ,然後將讀取的內容作為引數呼叫 sethostname 函式,從而更改系統的 hostname
-
因為普通 app 沒有許可權呼叫系統函式 ‘sethostname’, 本 exploit 通過注入 shellcode.s 到 libandroid_runtime.so, 然後觸發 zygote 程序執行 shellcode.s 達到越權執行的目的
// open file _start: mov x0, #-100 adrp x1, _start // NOTE: We are changing the page-relative alignment of the shellcode, so normal // aarch64 RIP-relative addressing doesn't work. add x1, x1, attr_path-file_start mov x2, #0 mov x8, #0x38 svc #0 attr_path: .ascii "/proc/self/attr/current\0"
第一段彙編作用是 open 檔案 “/proc/self/attr/current”, #0x38 是系統呼叫號,對應系統呼叫 __NR_openat (系統呼叫號定義: include/uapi/asm-generic/unistd.h), 將 0x38 放入 x8 暫存器,svc #0 指令觸發軟中斷,進入核心系統呼叫, 根據 openat 函式的定義, x1 暫存器存放要開啟的檔案路徑的地址, x0 和 x2 這裡忽略.
這段彙編執行後,x0暫存器存放返回值,即開啟檔案的 fd
// read from file sub sp, sp, #128 mov x1, sp mov x2, #128 mov x8, #0x3f svc #0
第二段彙編執行 read 系統呼叫,讀取 128 位元組放入棧, #0x3f 對應系統呼叫 read, x0 存放要讀取檔案的 fd, x1 是棧頂指標 sp, 在此之前,sp 被移動了#128 位元組,相當於一個 128 位元組的棧陣列作為 buf傳給 read 函式第二個引數, x2 是要讀取的長度, 這裡是 128
這段彙編執行後, sp 指向的位置存放檔案 ‘/proc/self/attr/current’ 的內容
// shove file contents into hostname mov x1, x0 mov x0, sp mov x8, #0xa1 svc #0
第三段彙編執行 sethostname 系統呼叫, #0xa1 對應系統呼叫 sethostname, x0 即要更新的域名字串, 這裡放入 sp 指標, 即將上一步 read 函式讀取的 buf 值作為 sethostname 的引數 name, x1 是長度, 這裡值是上一步read 的返回值
這段彙編執行後, hostname 將被更新為檔案 ‘/proc/self/attr/current’ 的內容
watchdog.c
這個檔案的作用是不斷呼叫 exp 可執行檔案並監控 exploit 是否成功, 之所以需要這個主調程式是由於這個漏洞在觸發的時候, 大部分情況會引發程式奔潰, 這時候需要一個看門狗程式不斷重啟它
exp.c
這個檔案實現了 exploit 的主體功能
- kickout_victim_page 函式
- idle_worker 執行緒
- spinner 執行緒
- nicer_spinner 執行緒
- read_worker 執行緒
- segv_handler 函式
kickout_victim_page 函式實現瞭如何提高檔案 page cache 命中率的演算法, 最開始執行
idle_worker 執行緒用於觸發 mremap 呼叫, 先繫結到 c1, spinner 喚醒後重繫結 idle_worker 到 c3, 排程策略為 SCHED_IDLE , 其他執行緒都是普通排程策略
spinner 執行緒用於觸發 fallocate (跟 ftruncate 效果類似) 呼叫, 繫結到 c2
nicer_spinner 執行緒繫結到 c3, 用於搶佔 idle_worker的 cpu 使用權
read_worker 執行緒繫結到 c4, 用於監控目標記憶體, 一旦發現 race 成功觸發, 則注入 shellcode 到目標記憶體
segv_handler 函式是段錯誤處理函式, 這裡會再一次檢測 shellcode 是否已經成功注入到目標檔案, 如果是, 則通知 watchdog 停止重啟 exp
執行 exploit 之前, libandroid_runtime.so 如下
adb pull /system/lib64/libandroid_runtime.so root@jiayy:CVE-2018-18281# xxd -s 0x157000 -l 100 libandroid_runtime.so 00157000: 0871 0091 5f00 08eb c000 0054 e087 41a9.q.._......T..A. 00157010: e303 1f32 0800 40f9 0801 43f9 0001 [email protected]...?. 00157020: 2817 40f9 a983 5af8 1f01 09eb e110 0054([email protected] 00157030: ff03 1191 fd7b 45a9 f44f 44a9 f657 43a9.....{E..OD..WC. 00157040: f85f 42a9 fa67 41a9 fc6f c6a8 c003 5fd6._B..gA..o...._. 00157050: f801 00b0 d901 00b0 ba01 00f0 7b02 00f0............{... 00157060: 9c01 0090
執行 exploit 之後, libandroid_runtime.so 如下
adb pull /system/lib64/libandroid_runtime.so root@jiayy:CVE-2018-18281# xxd -s 0x157000 -l 100 libandroid_runtime.so 00157000: 0000 20d4 0000 20d4 600c 8092 0100 0090.. ... .`....... 00157010: 2120 0191 0200 80d2 0807 80d2 0100 00d4! .............. 00157020: ff03 02d1 e103 0091 0210 80d2 e807 80d2................ 00157030: 0100 00d4 e103 00aa e003 0091 2814 80d2............(... 00157040: 0100 00d4 0000 0014 2f70 726f 632f 7365......../proc/se 00157050: 6c66 2f61 7474 722f 6375 7272 656e 7400lf/attr/current. 00157060: eaff ff17....