Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    863544248

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

在編寫Android 漏洞利用程序時,突破應用程序沙箱通常是關鍵步驟。有各種各樣的遠程攻擊方法可以讓你以應用程序的權限執行代碼,但仍需要沙盒逃逸才能獲得完整的系統訪問權限。

這篇文章重點介紹可從Android 應用程序沙箱訪問系統底層的一個有趣的攻擊面:圖形處理單元(GPU) 硬件。下面描述了Qualcomm 的Adreno GPU 中的一個漏洞,以及如何使用它在Android 應用程序沙箱中實現內核代碼執行。

這項研究是建立在oldfresher的工作基礎上的,他在2019 年8 月報告了CVE-2019-10567。一年後,也就是2020 年8 月上旬,oldfresher發布了一份披露CVE-2019-10567的漏洞paper,以及一些允許遠程攻擊者破壞整個系統的其他漏洞。

但是在2020 年6 月,我發現CVE-2019-10567 的補丁不完整,並與高通的安全團隊和GPU 工程師合作,從根本上修復了這個問題。此新問題的補丁CVE-2020-11179 已發布給OEM 供應商進行集成。

0x01 Android 攻擊面Android 應用程序沙箱是SELinux、seccomp BPF 過濾器和基於每個應用程序唯一UID 的自主訪問控制的不斷發展的組合。沙箱用於限制應用程序可以訪問的資源,並減少攻擊面。攻擊者能夠使用許多途徑來實現沙箱逃逸,例如:攻擊其他應用程序、攻擊系統服務或攻擊Linux 內核。

在高層次上,Android 生態系統中有幾個不同的攻擊面層。以下是一些重要攻擊面的梳理:

層級:Linux生態

描述:影響Android 生態系統中所有設備的問題。

示例:Linux 內核漏洞,如Dirty COW,或標準系統服務中的漏洞。

層級:芯片組

描述:影響Android 生態系統大部分的問題,具體取決於各種OEM 供應商使用的硬件類型。

示例:Snapdragon SoC 性能計數器漏洞,或Broadcom WiFi 固件堆棧溢出漏洞。

層級:供應商

說明:影響特定Android OEM 供應商的大多數或所有設備的問題

示例:三星內核驅動程序漏洞

層級:設備

說明:影響Android OEM 供應商的特定設備型號的問題

示例:Pixel 4 人臉解鎖“attention aware”漏洞

從攻擊者的角度來看, Android 漏洞利用能力是一個以盡可能最具成本效益的方式覆蓋盡可能廣泛的Android 生態系統的問題。 Linux生態層的漏洞會影響許多設備,但與其他層相比,發現漏洞可能成本高昂且利用效果相對短暫。芯片組層通常會存在相當多的漏洞利用的覆蓋範圍,但不如生態層漏洞影響大。對於某些攻擊面,例如基帶和WiFi 攻擊,芯片組層是主要選擇。供應商和設備層更容易找到漏洞,但需要維護大量單獨的漏洞利用。

對於沙盒逃逸,GPU 從芯片組層提供了一個特別有趣的攻擊面。由於GPU 加速在應用程序中被廣泛使用,Android 沙盒允許完全訪問底層GPU 設備。此外,只有兩種GPU 硬件在Android 設備中特別流行:ARM Mali 和Qualcomm Adreno。

這意味著,如果攻擊者能夠在這兩個GPU 實現中找到一個可很好利用的漏洞,那麼他們就可以有效地保持針對大多數Android 生態系統的沙盒逃逸漏洞利用能力。此外,由於GPU 非常複雜,有大量閉源組件、固件、微代碼,因此很有可能會找到一個危害極高且長期存在的漏洞。

考慮到這一點,在2020 年4 月下旬,我注意到Qualcomm Adreno 內核驅動程序代碼中有以下提交:

From0ceb2be799b30d2aea41c09f3acb0a8945dd8711MonSep1700:00:002001From:JordanCrouseDate:Wed,11Sep201908:32:15-0600Subject:[PATCH]msm:kgsl33 360Makethe'scratch'globalbufferusearandomGPUaddressSelectarandomglobalGPUaddressforthe'scratch'bufferthatisusedbytheringbufferforvarioustasks.當我們想到向地址添加熵時,通常會想到地址空間佈局隨機化(ASLR)。但這裡我們談論的是GPU 虛擬地址,而不是內核虛擬地址,為什麼需要隨機分配GPU 地址?

此提交是CVE-2019-10567 的安全補丁之一,這些補丁在高通的諮詢中有相關鏈接。此CVE 還包含一個相關補丁:

From8051429d4eca902df863a7ebb3c04cbec06b84b3MonSep1700:00:002001From:JordanCrouseDate:Mon,9Sep201910:41:36-0600Subject:[PATCH]msm:kgsl:Execut euserprofilingcommandsinanIBExecuteuserprofilinginanindirectbuffer.Thisensuresthataddressesandvaluesspecifieddirectlyfromtheuserdon'tendupintheringbuffer.所以問題就變成了,為什麼用戶內容不會最終出現在ringbuffer 上,這個補丁真的可以防止這種情況發生嗎?如果我們恢復臨時映射的基地址會發生什麼?至少從表面上看,兩者都是可行的,這個研究項目有了一個良好的開端。

在我們進一步討論之前,讓我們退一步描述一下這裡涉及的一些基本組件:GPU, ringbuffer, scratch mapping等。

0x02 Adreno GPU 簡介GPU 是現代圖形計算的主要組件,大多數應用程序都廣泛使用GPU。從應用程序的角度來看,GPU 硬件的具體實現通常由OpenGL ES 和Vulkan 等庫抽像出來。這些庫實現了一個標準API,用於對常見的GPU 加速操作進行編程,例如texture mapping 和running shaders。然而,在底層,此功能是通過與內核空間中運行的GPU 設備驅動程序交互來實現的。

image-20220324233623933.png image-20220324233623933

特別是對於Qualcomm Adreno,/dev/kgsl-3d0設備文件最終用於實現更高級別的GPU 功能。可在不受信任的應用程序沙箱中直接訪問/dev/kgsl-3d0文件,因為:

1.設備文件在其文件權限中設置了全局讀/寫訪問權限。權限由ueventd設置:

sargo:/#cat/system/vendor/ueventd.rc|grepkgsl-3d0/dev/kgsl-3d00666systemsystem2.設備文件的SELinux 標籤設置為gpu_device,並且untrusted_app SELinux 上下文對此標籤有特定的允許規則:

sargo:/#ls-Zal/dev/kgsl-3d0crw-rw-rw-1systemsystemu:object_r:gpu_device:s0239,02020-07-2115:48/dev/kgsl-3d0hawkes@glaptop:~$adbpull/sys/fs/selinux/policy/sys/fs/selinux/policy33 3601filepulled,0skipped.16.1MB/s.hawkes@glaptop:~$sesearch-A-suntrusted_apppolicy|grepgpu_deviceallowuntrusted_appgpu_device:chr_file{appendgetattrioctllockmapopenreadwrite};這意味著應用程序可以打開設備文件。 Adreno“KGSL”內核設備驅動程序主要通過許多不同的ioctl 調用(例如分配共享內存、創建GPU 上下文、提交GPU 命令等)和mmap(例如將共享內存映射到用戶空間)來調用應用。

0x03 GPU 共享映射在大多數情況下,應用程序使用共享映射將vertices, fragments 和shaders加載到GPU 中並接收計算結果。這意味著某些物理內存頁面會在用戶應用程序和GPU 硬件之間共享。

要設置新的共享映射,應用程序將通過調用IOCTL_KGSL_GPUMEM_ALLOC ioctl 向KGSL 內核驅動程序請求分配。內核驅動程序將準備一個物理內存區域,然後將該內存映射到GPU 的地址空間。最後,應用程序將使用分配ioctl 返回的標識符將共享內存映射到用戶空間地址空間。

此時,物理內存的同一頁上有兩個不同的視圖。第一個視圖來自用戶態應用程序,它使用虛擬地址來訪問映射到其地址空間的內存。 CPU 的內存管理單元(MMU) 將執行地址轉換以找到適當的物理頁面。

另一個是從GPU 硬件本身來看,它使用GPU 虛擬地址。 GPU 虛擬地址由KGSL 內核驅動程序選擇,它使用僅用於GPU 的頁表結構配置設備的IOMMU(在ARM 上稱為SMMU)。當GPU 嘗試讀取或寫入共享內存映射時,IOMMU 會將GPU 虛擬地址轉換為內存中的物理頁面。這類似於在CPU 上執行的地址轉換,但地址空間完全不同,即應用程序中使用的指針值將不同於GPU 中使用的指針值。

image-20220324234002661 image-20220324234002661.png

每個用戶態進程都有自己的GPU 上下文,這意味著當某個應用程序在GPU 上運行操作時,GPU 將只能訪問它與該進程共享的映射。這是必需的,這樣一個應用程序就不能要求GPU 從另一個應用程序讀取共享映射。在實踐中,這種分離是通過在GPU 上下文切換發生時更改將哪一組頁表加載到IOMMU 來實現的。每當安排GPU 運行來自不同進程的命令時,就會發生GPU 上下文切換。

然而,某些映射被所有GPU 上下文使用,因此可以出現在每組頁表中。它們被稱為全局共享映射,用於GPU 和KGSL 內核驅動程序之間的各種系統和調試功能。雖然它們從未直接映射到用戶級應用程序,例如惡意應用程序無法直接讀取或修改全局映射的內容,但它們會同時映射到GPU 和內核地址空間。

在被root的Android 設備上,我們可以使用以下命令dump全局映射及其GPU 虛擬地址:

sargo:/#cat/sys/kernel/debug/kgsl/globals0x00000000fc000000-0x00000000fc000fff4096setstate0x00000000fc001000-0x00000000fc040fff262144gpu-qdss0x00000000fc041000-0x00000000fc048fff32768memstore0x00000000fce7a000-0x00000000fce7aff f4096scratch0x00000000fc049000-0x00000000fc049fff4096pagetable_desc0x00000000fc04a000-0x00000000fc04afff4096profile_desc0x00000000fc04b000-0x00000000fc052fff32768ringbuffer0x00000000fc053000-0x00000000fc053fff4096pagetable_desc0x00 000000fc054000-0x00000000fc054fff4096profile_desc0x00000000fc055000-0x00000000fc05cfff32768ringbuffer0x00000000fc05d000-0x00000000fc05dfff4096pagetable_desc0x00000000fc05e000-0x00000000fc05efff4096profile_desc0x00000000fc05f000-0x0 0000000fc066fff32768ringbuffer0x00000000fc067000-0x00000000fc067fff4096pagetable_desc0x00000000fc068000-0x00000000fc068fff4096profile_desc0x00000000fc069000-0x00000000fc070fff32768ringbuffer0x00000000fc071000-0x00000000fc0a0fff1966 08profile0x00000000fc0a1000-0x00000000fc0a8fff32768ucode0x00000000fc0a9000-0x00000000fc0abfff12288capturescript0x00000000fc0ac000-0x00000000fc116fff438272capturescript_regs0x00000000fc117000-0x00000000fc117fff4096powerup_register_l ist0x00000000fc118000-0x00000000fc118fff4096alwayson0x00000000fc119000-0x00000000fc119fff4096preemption_counters0x00000000fc11a000-0x00000000fc329fff2162688preemption_desc0x00000000fc32a000-0x00000000fc32afff4096perfcounter_save_re store_desc0x00000000fc32b000-0x00000000fc53afff2162688preemption_desc0x00000000fc53b000-0x00000000fc53bfff4096perfcounter_save_restore_desc0x00000000fc53c000-0x00000000fc74bfff2162688preemption_desc0x00000000fc74c000-0x00000000fc74 cfff4096perfcounter_save_restore_desc0x00000000fc74d000-0x00000000fc95cfff2162688preemption_desc0x00000000fc95d000-0x00000000fc95dfff4096perfcounter_save_restore_desc0x00000000fc95e000-0x00000000fc95efff4096smmu_info從左到右,我們看到每個全局映射的GPU 虛擬地址,然後是大小,然後是分配的名稱。通過多次重啟設備並檢查佈局,可以看到暫存緩衝區確實是隨機的:

0x00000000fc0df000-0x00000000fc0dffff4096scratch.0x00000000fcfc0000-0x00000000fcfc0fff4096scratch.0x00000000fc9ff000-0x00000000fc9fffff4096scratch.0x00000000fcb4d000-0x00000000fcb4dfff4096scratch同樣的測試表明,暫存緩衝區是唯一隨機化的全局映射,所有其他全局映射在[0xFC000000,0xFD400000]範圍內都有一個固定的GPU 地址。這是有道理的,因為CVE-2019-10567 的補丁只為暫存緩衝區分配引入了KGSL_MEMDESC_RANDOM 標誌。

所以我們現在知道暫存緩衝區至少在某種程度上是正確隨機的,並且它是存在於每個GPU 上下文中的全局共享映射,但是暫存緩衝區到底是做什麼用的呢?

0x04 Scratch 緩衝區深入驅動程序代碼,我們可以清楚地看到在驅動程序的探測例程中分配了暫存緩衝區,這意味著暫存緩衝區將在設備首次初始化時分配:

intadreno_ringbuffer_probe(structadreno_device*adreno_dev,boolnopreempt){.status=kgsl_allocate_global(device,device-scratch,PAGE_SIZE,0,KGSL_MEMDESC_RANDOM,'scratch');我們還發現了下面的註釋:

/*SCRATCHMEMORY:Thescratchmemoryisonepageworthofdatathat*ismappedintotheGPU.Thisallowsforsome'shared'databetween*theGPUandCPU.Forexample,itwillbeusedbytheGPUtowrite*eachupdatedRPTRforeachRB.通過在內核驅動程序中交叉引用生成的內存描述符(device-scratch )的所有用法,我們可以找到暫存緩衝區的兩個主要用法:

1.搶占恢復緩衝區的GPU 地址被dump到暫存內存中,如果較高優先級的GPU 命令中斷較低優先級的命令,則會使用該暫存內存。

2.環形緩衝區(RB) 的讀指針(RPTR) 從臨時內存中讀取,並在計算環形緩衝區中的可用空間量時使用。

可以開始串聯思路。首先,我們知道CVE-2019-10567 的補丁包括對暫存緩衝區和環形緩衝區處理代碼的更改——這表明我們應該關注上面的第二個用例。

如果GPU 正在將RPTR 值寫入共享映射(如註釋所示),並且如果內核驅動程序正在從暫存緩衝區讀取RPTR 值並將其用於分配大小計算,那麼如果我們可以讓GPU 寫入一個RPTR 值無效或不正確。

0x05 環形緩衝區要了解無效RPTR 值對環緩衝區分配可能意味著什麼,我們首先需要描述環緩衝區本身。當用戶態應用程序提交GPU 命令( IOCTL_KGSL_GPU_COMMAND ) 時,驅動程序代碼通過使用生產者-消費者模式的環形緩衝區將命令分派給GPU。內核驅動程序會將命令寫入環形緩衝區,GPU 將從環形緩衝區讀取命令。

這以與經典循環緩衝區類似的方式發生。在底層,ringbuffer 是一個固定大小為32768 字節的全局共享映射。維護兩個索引來跟踪CPU 寫入的位置(WPTR) 和GPU 讀取的位置(RPTR)。為了在ringbuffer 上分配空間,CPU 必須計算當前WPTR 和當前RPTR 之間是否有足夠的空間。這發生在adreno_ringbuffer_allocspace 中:

unsignedint*adreno_ringbuffer_allocspace(structadreno_ringbuffer*rb,unsignedintdwords){structadreno_device*adreno_dev=ADRENO_RB_DEVICE(rb);unsignedintrptr=adreno_get_rptr(rb);[1]unsignedintret;if(rptr_wptr){[2]unsign edint*cmds;if(rb-_wptr+dwords_wptr;rb-_wptr=(rb-_wptr+dwords)%KGSL_RB_DWORDS;returnRB_HOSTPTR(rb,ret);}/**Thereisn'tenoughspacetowardtheendofringbuffer.So*lookforspacefromthebeginningofringbufferuptothe*readpointer.*/