當前位置:係統粉 >   IT資訊 >   蘋果資訊 >  iOS內核單字節利用技術

iOS內核單字節利用技術

時間:2020-08-03 來源:互聯網 瀏覽量:

iOS內核單字節利用技術(1)

0x01 簡介

在過去的幾年裏,幾乎所有的iOS內核利用都遵循相同的流程:內存破壞和偽造Mach port被用來訪問內核task port,從而為用戶空間提供完美的內核讀/寫原語。最近的iOS內核漏洞緩解措施,比如PAC和zone_require,似乎是為了打破常見的利用流程,但是,這些iOS內核利用從高層次上看是相同的,這引發了一個問題:針對內核task port真的是最好的漏洞利用流程嗎?還是這種策略的趨勢掩蓋了其他可能更有趣的技術?現有的iOS內核緩解措施是否對其他未被發現的開發商是否同樣有效?

在這篇文章中,我將介紹一種新的iOS內核利用技術,它將控製一個字節的堆溢出直接轉換為任意物理地址的讀/寫原語,同時完全避開當前的緩解措施,如KASLR、PAC和zone_require。通過讀取一個特殊的硬件寄存器,可以在物理內存中定位到內核並構建一個內核讀/寫原語,而無需偽造內核task port。最後,我將討論各種iOS緩解措施在阻止這一技術方麵的效果,並對iOS內核利用的最新進展進行總結。您可以在這裏找到Poc代碼。

0x02 前置知識power 結構

在查看XNU的源代碼時,我經常關注一些對象(objects),方便在將來利用它進行操作或破壞。在發現 CVE-2020-3837(oob_timestamp漏洞)後,我偶然發現了vm_map_copy_t的定義:

struct vm_map_copy {
        int                     type;
#define VM_MAP_COPY_ENTRY_LIST          1
#define VM_MAP_COPY_OBJECT              2
#define VM_MAP_COPY_KERNEL_BUFFER       3
        vm_object_offset_t      offset;
        vm_map_size_t           size;
        union {
                struct vm_map_header    hdr;      /* ENTRY_LIST */
                vm_object_t             object;   /* OBJECT */
                uint8_t                 kdata[0]; /* KERNEL_BUFFER */
        } c_u;
};

我覺得這值得關注,有幾個原因:

1.這個結構在頭部有一個type字段,因此越界寫入可能會將其從一種類型更改為另一種類型,從而導致類型混淆。因為iOS是小端(little-endian),因此最低有效字節在內存中排在首位,這意味著即使是一個單字節溢出也足以將類型設置為三個值中的任何一個。2.該類型區分任意可控數據(kdata)和內核指針(hdr和object)之間的交集。因此,破壞type可以讓我們直接偽造指向內核對象的指針,而不需要執行任何重新分配。3.我記得曾經讀過關於vm_map_copy_t在以前的漏洞利用中(在iOS 10之前)被用作原語的內容,但我不記得在哪裏或如何使用它。Ian Beer 也曾使用過vm_map_copy對象:Splitting atoms in XNU。

通過對osfmk/vm/vm_map.c的深入研究,我發現vm_map_copyout_internal()確實以非常有趣的方式使用了copy對象。但首先,讓我們先介紹一下vm_map_copy是什麼以及它如何工作。

vm_map_copy表示進程虛擬地址空間的寫時拷貝,它已經packaged,準備插入到另一個虛擬地址空間中。有三種內部表示形式:作為vm_map_entry對象的列表、作為vm_object或作為直接複製到目標中內聯字節數組。我們將重點討論類型1和3。

基本上 ENTRY_LIST 類型是最強大且最通用的表示形式,而 KERNEL_BUFFER 類型則是一種嚴格的優化。vm_map_entry列表由多個分配和多個間接層組成:每個vm_map_entry 描述了一個虛擬地址範圍[ vme_start ,vme_end ),該範圍由特定的 vm_object 映射,而該列表又包含一個 vm_page 的列表,該列表描述vm_object支持的物理頁麵。

iOS內核單字節利用技術(2)

同時,如果要插入的數據不是共享內存,並且大小大約為兩個pages或更少,則隻需簡單地分配 vm_map_copy 即可將數據內容內聯在同一分配中,而無需進行間接或其他分配。

iOS內核單字節利用技術(3)

通過這種優化,vm_map_copy對象偏移0x20處的8個字節可以是指向vm_map_entry列表頭的指針,也可以是完全由攻擊者控製的數據,所有這些都取決於頭部的type字段。因此,破壞vm_map_copy對象的第一個字節會導致內核將任意可控數據解釋為vm_map_entry指針。

iOS內核單字節利用技術(4)

了解vm_map_copy的內部原理後,讓我們回到vm_map_copyout_internal()。這個函數負責獲取一個vm_map_copy並將其插入目標地址空間(由vm_map_t類型表示)。當進程之間共享內存時,它可以通過Mach消息發送一個外部內存描述符來實現:外部內存以vm_map_copy的形式存儲在內核中,而vm_map_copyout_internal()是將其插入到接收進程中的函數。

事實證明,如果vm_map_copyout_internal()處理一個損壞的vm_map_copy,其中包含一個指向偽造的vm_map_entry的指針,事情會變得相當令人興奮。特別要考慮的是,如果偽造的vm_map_entry 聲稱已連接,這會導致該函數立即嚐試在page中進行錯誤操作:

kern_return_t
vm_map_copyout_internal(
    vm_map_t                dst_map,
    vm_map_address_t        *dst_addr,      /* OUT */
    vm_map_copy_t           copy,
    vm_map_size_t           copy_size,
    boolean_t               consume_on_success,
    vm_prot_t               cur_protection,
    vm_prot_t               max_protection,
    vm_inherit_t            inheritance)
{
...
    if (copy->type == VM_MAP_COPY_OBJECT) {
...
    }
...
    if (copy->type == VM_MAP_COPY_KERNEL_BUFFER) {
...
    }
...
    vm_map_lock(dst_map);
...
    adjustment = start - vm_copy_start;
...
    /*
     *    Adjust the addresses in the copy chain, and
     *    reset the region attributes.
     */
    for (entry = vm_map_copy_first_entry(copy);
        entry != vm_map_copy_to_entry(copy);
        entry = entry->vme_next) {
...
        entry->vme_start += adjustment;
        entry->vme_end += adjustment;
...
        /*
         * If the entry is now wired,
         * map the pages into the destination map.
         */
        if (entry->wired_count != 0) {
...
            object = VME_OBJECT(entry);
            offset = VME_OFFSET(entry);
...
            while (va < entry->vme_end) {
...
                m = vm_page_lookup(object, offset);
...
                vm_fault_enter(m,      // Calls pmap_enter_options()
                    dst_map->pmap,     // to map m->vmp_phys_page.
                    va,
                    prot,
                    prot,
                    VM_PAGE_WIRED(m),
                    FALSE,            /* change_wiring */
                    VM_KERN_MEMORY_NONE,    /* tag - not wiring */
                    &fault_info,
                    NULL,             /* need_retry */
                    &type_of_fault);
...
                offset += PAGE_SIZE_64;
                va += PAGE_SIZE;
           }
       }
   }
...
        vm_map_copy_insert(dst_map, last, copy);
...
    vm_map_unlock(dst_map);
...
}

讓我們一步步完成這個步驟。首先,處理其他vm_map_copy類型:

    if (copy->type == VM_MAP_COPY_OBJECT) {
...
    }
...
    if (copy->type == VM_MAP_COPY_KERNEL_BUFFER) {
...
    }

這個 vm_map 是加鎖的:

   vm_map_lock(dst_map);

我們在vm_map_entry(fake)對象鏈表上輸入一個for循環:

    for (entry = vm_map_copy_first_entry(copy);
        entry != vm_map_copy_to_entry(copy);
        entry = entry->vme_next) {

我們處理的情況下,vm_map_entry是wired的,因此應該立即出現故障:

        if (entry->wired_count != 0) {

設置後,我們將遍曆wired條目中的每個虛擬地址。由於我們控製了偽造的vm_map_entry內容,所以我們可以控製讀取的對象指針(類型為vm_objec)和偏移量:

            object = VME_OBJECT(entry);
            offset = VME_OFFSET(entry);
...
            while (va < entry->vme_end) {

我們為需要wired的內存的每個物理頁查找vm_page 結構。由於我們控製了偽造的vm_object和偏移量,我們可以使vm_page_lookup()返回一個指針,指向一個偽造的vm_page結構體,我們控製它的內容:

                m = vm_page_lookup(object, offset);

最後,我們調用vm_fault_enter()在頁麵中進行故障處理:

                vm_fault_enter(m,      // Calls pmap_enter_options()
                    dst_map->pmap,     // to map m->vmp_phys_page.
                    va,
                    prot,
                    prot,
                    VM_PAGE_WIRED(m),
                    FALSE,            /* change_wiring */
                    VM_KERN_MEMORY_NONE,    /* tag - not wiring */
                    &fault_info,
                    NULL,             /* need_retry */
                    &type_of_fault);

vm_fault_enter()的調用比較複雜,所以我不把代碼放在這裏。可以這樣說,通過適當地設置偽造objects中的字段,就可以使用偽造的vm_page對象確定vm_fault_enter(),從而使用任意的物理頁碼調用pmap_enter_options():

kern_return_t
pmap_enter_options(
        pmap_t pmap,
        vm_map_address_t v,
        ppnum_t pn,
        vm_prot_t prot,
        vm_prot_t fault_type,
        unsigned int flags,
        boolean_t wired,
        unsigned int options,
        __unused void   *arg)

pmap_enter_options()負責修改目標的頁表,以插入轉換表條目,該轉換表條目將建立從虛擬地址到物理地址的映射。關於 vm_map 如何管理地址空間的虛擬映射的狀態,pmap結構管理地址空間物理映射(即頁表)的狀態。並根據osfmk/arm/pmap.c中的源代碼。在添加轉換表條目之前,不會對提供的物理頁碼進行進一步驗證。

因此,我們破壞的vm_map_copy對象實際上為我們提供了一個非常強大的原語:將任意物理內存直接映射到用戶空間中的進程中!

iOS內核單字節利用技術(5)

我決定在iOS 13.3的oob_timestamp exploit所提供的內核讀/寫原語之上為vm_map_copy物理內存映射技術構建POC,有兩個主要原因。

首先,我們有一個較好的漏洞可以用來開發一個完整的exploit。盡管我最初是在試圖利用oob_timestamp漏洞時偶然發現這個想法的,但很快就發現這個漏洞並不適合這種技術。

其次,我希望獨立實現該技術所使用的漏洞來評估該技術。似乎很有可能使該技術具有確定性(也就是說,沒有故障案例);在一個不可靠的的漏洞上實施它會使得很難單獨進行評估。

這種技術適合於kalloc.80 至kalloc.32768的任何分配器區域中的可控一字節堆溢出(即65到32768字節的通用分配),為了便於在後文中參考,我將其稱為“單字節利用技術”。

我們已經列出了上述技術的基本要素:創建一個KERNEL_BUFFER類型的vm_map_copy,其中包含一個指向偽造的vm_map_entry列表的指針,將該類型破壞為ENTRY_LIST,使用vm_map_copyout_internal()接收它,並將任意物理內存映射到我們的地址空間中。但是,成功的利用要複雜得多:

1.我們還沒有處理偽造的vm_map_entry/vm_object/vm_page將被構造在哪裏。2.我們需要確保在映射物理頁麵之後,調用vm_map_copyout_internal()的內核線程不會崩潰,死機或死鎖。3.映射一個物理頁麵是非常好的,但是它本身可能還不足以實現任意內核讀/寫。這是因為:kernelcache在物理內存中的加載地址是未知的,因此我們不能直接映射它的任何特定頁麵,而應該首先定位它。一些硬件設備可能公開一個MMIO接口,該接口本身足夠強大,可以構建某種讀/寫原語;但是,我不知道有任何這樣的組件。因此,我們將需要映射多個物理地址,並且很可能需要使用從一個映射讀取的數據來查找另一個映射的物理地址。也就是說,我們的映射原語不能是一次性的。4.在for循環嚐試將vm_map_copy_insert()複製到vm_map_copy_zone後,調用vm_map_copy_insert。因為KERNEL_BUFFER對象最初是使用kalloc()分配的,所以如果vm_map_copy的初始類型是KERNEL_BUFFER,這將導致死機。PAN 捷徑

單字節技術的一個重要前提條件是在已知地址上創建一個fake vm_map_entry對象。因為我們已經在oob_timestamp上構建了這個POC,因此我決定使用在利用那個bug時學到的一個技巧。在實際情況中,除了單字節溢出之外,可能還需要另一個漏洞來泄漏內核地址。

在為oob_timestamp開發POC時,我了解到AGXAccelerator內核擴展提供了一個非常好的原語:OAccelSharedUserClient2和IOAccelCommandQueue2同時允許在用戶空間和內核之間共享大範圍的分頁內存。訪問用戶/內核共享內存在開發exploits時非常有用,因為您可以在那裏放置偽造的內核數據結構,並在內核訪問它們時對它們進行操作。當然,這個AGXAccelerator原語並不是獲得內核/用戶共享內存的唯一方法;例如physmap還將大部分DRAM映射到虛擬內存中,因此它也可以用於將用戶空間內存內容映射到內核中。但是,AGXAccelerator原語在實踐中要方便得多:首先,它在一個受約束的地址範圍內提供了一個非常大的連續內存共享區域;其次,它更容易泄漏相鄰對象的地址來定位它。

在iPhone 7之前,iOS設備不支持PAN(Privileged Access Never),也就是說所有的用戶空間都是與內核共享內存的,您可以重寫內核中的指針指向用戶空間中偽造的數據結構。

但是,當前iOS設備啟用了PAN,因此內核直接訪問用戶空間內存將會直接出錯。這就是AGXAccelerator共享內存原語非常好用的原因:如果您可以構建一個較大的共享內存區域並知道其在內核中的地址,基本上等同於關閉了PAN。

當然,這句話的一個關鍵部分是“在內核中學習它的地址”;這樣做通常需要一個漏洞。相反,由於我們已經依賴於oob_timestamp,我們將對共享內存地址進行簡單的硬編碼,動態地查找地址留給讀者作為練習。

POC panicking

有了內核讀/寫和用戶/內核共享內存緩衝區後,我們就可以編寫POC了。

我們首先在內核中創建共享內存區域,我們在共享內存中初始化一個偽造的vm_map_entry列表。列表包含3個條目:一個”ready”,一個”mapping”和一個”done”,這些條目表示每個映射操作的當前狀態。

iOS內核單字節利用技術(6)

我們在Mach消息中包含一個偽造的vm_map_header的內存描述符發送到保留端口,外部內存作為vm_map_copy對象存儲在內核中,該對象的類型為KERNEL_BUFFER(值為3)。

iOS內核單字節利用技術(7)

我們模擬了一個單字節的堆溢出,該溢出會破壞 vm_map_copy 的 type 字段,並將其更改為ENTRY_LIST(值為1)。

iOS內核單字節利用技術(8)

我們啟動一個線程,它接收在保留端口上的Mach消息。這會在破壞的vm_map_copy上觸發對vm_map_copyout_internal()的調用。

由於vm_map_entry列表初始化的配置,vm_map_copyout線程將在“done”條目上無限循環,為我們操作它做好準備。

iOS內核單字節利用技術(9)

此時,我們有一個內核線程正在準備映射所需的任何物理頁麵。

要映射一個頁麵,我們首先將“ready”條目設置為鏈接到它本身,然後將“done”條目設置為鏈接到“ready”條目。這將導致vm_map_copyout線程轉換為”ready”。

iOS內核單字節利用技術(10)

在轉換“ready”時,我們將“mapping”條目標記為與單個物理頁麵連接,並將其鏈接到“done”條目,我們將該條目鏈接到自身。我們還要填充偽造的vm_object和vm_page,以映射所需的物理頁碼。

iOS內核單字節利用技術(11)

然後,我們可以通過將“ready”條目鏈接到“mapping”條目來執行映射。vm_map_copyout_internal()將映射到頁麵中,然後在“done”條目上轉換,表示完成。

iOS內核單字節利用技術(12)

這為我們提供了一個可重用的原語,該原語將任意物理地址映射到進程中。作為初步的Poc,我映射了不存在的物理地址0x414140000並試圖從中讀取,從而觸發EL0de LLC總線錯誤:

iOS內核單字節利用技術(13)

至此,我們已經證明了映射原語是正確的,但我們仍然不知道如何處理它。

我首先想到的是,最簡單的方法是在內存中查找kernelcache image。請注意,在目前的iphone上,即使使用直接的物理讀/寫原語,KTRR也會阻止我們修改內核image鎖定的部分,因此我們不能僅僅修補內核的可執行代碼。但是,kernelcache image的某些段在運行時仍然是可寫的,包括含有sysctls的部分數據段。因為sysctls以前就被用於構建讀/寫原語,所以感覺這是一個穩定的方向。

接下來的挑戰是使用映射原語在物理內存中定位kernelcache,這樣sysctl結構就可以映射到用戶空間並進行修改。但首先,在我們弄清楚如何定位kernelcache之前,先來了解一下iPhone 11 Pro的物理內存知識。

iPhone 11 Pro在物理地址0x80000000有4GB的DRAM,因此物理DRAM地址的範圍從0x800000000 到 0x900000000。其中,0x801b80000 到0x8ec9b4000 保留給應用處理器(AP),它是運行XNU內核和手機應用程序的主處理器。這個區域之外的內存為協處理器保留,例如:AOP(Always On Processor)、ANE(Apple Neural Engine)、SIO (possibly Apple SmartIO)、 AVE、ISP、 IOP等。這些和其他區域的地址可以通過解析devicetree或在DRAM開始轉儲iboot-handoff區域來找到。

iOS內核單字節利用技術(14)

在boot時,kernelcache被連續加載到物理內存中,也就是說找到一個kernelcache頁麵就足以定位整個image了。另外,雖然KASLR可能會在虛擬內存中大量地減少kernelcache,但物理內存中的加載地址是相當有限的:在我的測試中,內核header總是加載在0x805000000和0x807000000之間的地址,範圍隻有32MB。

事實證明,此範圍比內核緩存本身小,為 0x23d4000 字節,即35.8MB。因此,我們可以確定在運行時地址0x807000000包含一個kernelcache頁麵。

但是,在嚐試映射kernelcache時,我很快陷入了panic:

panic(cpu 4 caller 0xfffffff0156f0c98): "pmap_enter_options_internal: page belongs to PPL, " "pmap=0xfffffff031a581d0, v=0x3bb844000, pn=2103160, prot=0x3, fault_type=0x3, flags=0x0, wired=1, options=0x1"

該panic字符串似乎來自於pmap_enter_options_internal()函數,它位於XNU(osfmk/arm/pmap.c)的源代碼中,但是源代碼中沒有出現panic。因此,我逆向了kernelcache中的pmap_enter_options_internal(),以了解發生了什麼。

我了解到,問題在於我試圖映射的page是Apple PPL(Protection Layer )中的一部分,該page是XNU內核的一部分,用於管理頁麵表,它被認為比內核的其他部分擁有更大的權限。PPL的目標是防止攻擊者修改受保護的page。

為了使受保護的pages不能被修改,PPL必須保護頁表和頁表元數據。因此,當我試圖將一個受ppl保護的page映射到用戶空間時,引發了panic。

if (pa_test_bits(pa, 0x4000 /* PP_ATTR_PPL? */)) {
    panic("%s: page belongs to PPL, " ...);
}

if (pvh_get_flags(pai_to_pvh(pai)) & PVH_FLAG_LOCKDOWN) {
    panic("%s: page locked down, " ...);
}

PPL的存在使物理映射原語的使用更在複雜,因為嚐試映射受PPL保護的頁麵將引起panic。而且kernelcache本身包含許多受ppl保護的page,將連續的35 MB二進製文件拆分為較小的 PPL-free 塊,這些塊不再橋接kernelcache的物理結構。因此,我們不再可以映射一個kernelcache頁麵的物理地址。

而其他DRAM區域也是一個同樣危險的雷區。PPL會抓取物理頁麵以供使用,並根據需要返回到內核,因此在運行時,PPL頁麵就像地雷一樣分散在物理內存中。因此,在任何地方都沒有確保不會被破壞的靜態地址。

iOS內核單字節利用技術(15)

在A13上的AP DRAM的每一頁上顯示保護標誌的map。黃色為PPL+LOCKDOWN,紅色為PPL,綠色為LOCKDOWN,藍色為unguarded(可映射)。

0x03 兩種技術DRAM 保護之路

然而,這並不完全正確。應用程序處理器的DRAM區域可能是一個雷區,但在它之外的任何區域都不是。這包括協處理器使用的DRAM,以及係統任何其他的尋址組件,比如通過memory-mapped I/O (MMIO)訪問係統組件的硬件寄存器。

有了這麼強大的原語,我希望可以使用多種技術來構建讀/寫原語。我希望通過直接訪問特殊的硬件寄存器和協同處理器,可以完成許多事情。不幸的是,這不是我非常熟悉的領域,因此在這裏我將介紹一次繞過PPL的嚐試(失敗)。

我的想法是控製一些協處理器,同時使用協處理器和AP上的執行來攻擊內核。首先,我們使用物理映射原語來修改DRAM中協處理器存儲數據的部分,以便在該協處理器上執行代碼。接下來,回到主處理器上,我們再次使用映射原語來映射和禁用協處理器的設備地址解析表或DART(基本上是IOMMU)。在協處理器上執行代碼並禁用了相應的DART的情況下,我們可以從協處理器直接無保護地訪問物理內存,從而使我們能夠完全避開對PPL的保護(PPL隻在AP上執行)。

然而,每當我試圖修改協處理器使用的DRAM的某些區域時,我就會遇到內核panic,特別是區域0x800000000 – 0x801564000看起來是隻讀的:

panic(cpu 5 caller 0xfffffff0189fc598): "LLC Bus error from cpu1: FAR=0x16f507f10 LLC_ERR_STS/ADR/INF=0x11000ffc00000080/0x214000800000000/0x1 addr=0x800000000 cmd=0x14(acc_cifl2c_cmd_ncwr)"

panic(cpu 5 caller 0xfffffff020ca4598): "LLC Bus error from cpu1: FAR=0x15f03c000 LLC_ERR_STS/ADR/INF=0x11000ffc00000080/0x214030800104000/0x1 addr=0x800104000 cmd=0x14(acc_cifl2c_cmd_ncwr)"

panic(cpu 5 caller 0xfffffff02997c598): "LLC Bus error from cpu1: FAR=0x10a024000 LLC_ERR_STS/ADR/INF=0x11000ffc00000082/0x21400080154c000/0x1 addr=0x80154c000 cmd=0x14(acc_cifl2c_cmd_ncwr)"

這是非常奇怪的:這些地址在KTRR鎖定區域之外,所以沒有任何內容能夠阻止對DRAM這部分的寫操作。因此,必須在此物理範圍上強製執行其他一些未記錄的鎖定。

另一方麵,0x801564000-0x801b80000仍然是可寫的,對這個區域的不同區域寫入會產生奇怪的係統行為,這支持了協處理器使用的數據被破壞的理論。例如,在某些區域寫入會導致相機和手電筒失去響應,而在其他區域寫入會導致手機在開啟靜音滑塊時出現panic。

為了方便了解可能發生的情況,我通過檢查devicetree並dump內存來確定這個範圍內的區域。最後,我發現在0x800000000 – 0x801b80000範圍內的協處理器固件段布局如下:

iOS內核單字節利用技術(16)

因此,被鎖定的區域都是協處理器固件的所有 TEXT 段,這表明,蘋果已經增加了一個新的緩解措施,使協處理器 TEXT 段在物理內存中隻讀,類似於AMCC(可能是蘋果的內存控製器)上的KTRR,但不隻是針對AP內核,而是針對協處理器固件。這可能是之前發布的xnu-6153.41.3源代碼中引用的未記錄的CTRR緩解措施,它似乎是A12及更高版本上KTRR的增強版;Ian Beer建議CTRR可以表示協處理器Text隻讀區域。

然而,在這些協處理器上執行代碼應該仍然是可行的:就像KTRR不能阻止AP上的利用一樣,協處理器 Text 鎖定緩解也不能阻止對協處理器的攻擊。因此,即使這種緩解會使事情變得更困難,但此時我們禁用DART並使用協處理器上的代碼執行來寫入受PPL保護的物理地址的方案,應該仍然可行。

PPL的影響

但是,PPL在應用程序處理器上強製執行的DART/IOMMU鎖定確實是一個障礙。在boot時,XNU解析devicetree中的“pmap-io-ranges”屬性來填充io_attr_table數組,該數組存儲特定物理I/O地址的頁麵屬性。然後,當嚐試映射物理地址時,pmap_enter_options_internal()檢查屬性,看看是否應該不允許某些映射:

wimg_bits = pmap_cache_attributes(pn); // checks io_attr_table
if ( flags )
    wimg_bits = wimg_bits & 0xFFFFFF00 | (u8)flags;
pte |= wimg_to_pte(wimg_bits);
if ( wimg_bits & 0x4000 )
{
    xprr_perm = (pte >> 4) & 0xC | (pte >> 53) & 1 | (pte >> 53) & 2;
    if ( xprr_perm == 0xB )
        pte_perm_bits = 0x20000000000080LL;
    else if ( xprr_perm == 3 )
        pte_perm_bits = 0x20000000000000LL;
    else
        panic("Unsupported xPRR perm ...");
    pte = pte_perm_bits | pte & ~0x600000000000C0uLL;
}
pmap_enter_pte(pmap, pte_p, pte, vaddr);

因此,隻有清除了wimg字段中的0x4000位,我們才能將DART的I/O地址映射到我們的進程中。不幸的是,快速查看一下devicetree中的“pmap-io-ranges”屬性,可以確認為每個DART設置了0x4000位:

    addr         len        wimg     signature
0x620000000, 0x40000000,       0x27, 'PCIe'
0x2412C0000,     0x4000,     0x4007, 'DART' ; dart-sep
0x235004000,     0x4000,     0x4007, 'DART' ; dart-sio
0x24AC00000,     0x4000,     0x4007, 'DART' ; dart-aop
0x23B300000,     0x4000,     0x4007, 'DART' ; dart-pmp
0x239024000,     0x4000,     0x4007, 'DART' ; dart-usb
0x239028000,     0x4000,     0x4007, 'DART' ; dart-usb
0x267030000,     0x4000,     0x4007, 'DART' ; dart-ave

因此,我們無法將DART映射到用戶空間以禁用它。

即使PPL阻止我們映射頁表和DART I/O地址,其他硬件組件的物理I/O地址仍然是可映射的。因此,仍然可以映射和讀取某些係統組件的硬件寄存器來嚐試定位內核。

我最初的嚐試是從IORVBAR讀取,通過MMIO可訪問ResetVector基地址寄存器。ResetVector是複位後在CPU上執行的第一段代碼。因此,讀取IORVBAR 將為我們提供XNU ResetVector的物理地址,該地址將精確定位物理內存中的內核緩存(kernelcache)。

IORVBAR映射在devicetree中每個CPU的“reg-private”地址後的偏移0x40000處;例如,在A13 CPU 0上,它位於物理地址0x210050000。它包含同一組CoreSight和DBGWRAP 的寄存器集的一部分,以前是用來繞過KTRR的,但是,我發現IORVBAR在A13上是不可訪問的:嚐試從中讀取將導致panic。

我花了一些時間在A13的SecureROM上搜索有意思的物理地址,後來Jann Horn建議我把KTRR鎖定寄存器映射到蘋果的內存控製器AMCC上。這些寄存器存儲KTRR區域的物理內存邊界,強製KTRR隻讀,以防止來自協處理器的攻擊。

iOS內核單字節利用技術(17)

在物理地址0x200000680處映射和讀取AMCC的RORGNBASEADDR寄存器,可以輕鬆地生成物理內存中包含kernelcache鎖定區域的起始地址。使用安全緩解機製來破壞其他安全緩解機製是非常有意思的。

在找到使用AMCC的正確方法之後,我研究了最後一種可能性,然後放棄繞過PPL。

iOS配置了40位物理地址和16K pages (14位)。同時,傳遞給pmap_enter_options_internal()的任意物理頁碼是32位,當插入到級別3的轉換表條目(L3 TTE)時,它被移動14位,並用0xFFFF_FFFF_C000屏蔽。也就是說我們可以控製TTE的第45-14 位,即使根據編程中TCR_EL1.IPS的物理地址大小,45-40位應該始終為零。

如果硬件忽略了超出支持的最大物理地址大小的位,那麼我們可以通過提供一個與DART I/O地址或頁表完全匹配的物理頁碼來繞過PPL,但設置了一個高位。設置高位將導致映射地址無法匹配“pmap io ranges”中的任何地址,即使TTE將映射相同的物理地址。這樣做很巧妙,因為它可以讓我們繞過PPL作為內核讀/寫/執行的前提。

不幸的是,實踐證明,硬件實際上會檢查超出支持的物理地址大小的TTE位是否為零。因此,我繼續使用AMCC技巧來定位kernelcache。

控製 sysctl

此時,我們有了一個用於非ppl物理地址的物理讀/寫原語,並且知道了物理內存中的kernelcache的地址。下一步是構建一個虛擬的讀/寫原語。

對於這一部分,我決定堅持使用已知的技術:利用sysctl() syscall使用的sysctl_oid樹存儲在kernelcache的可寫內存中這一事實來操作它,並將app 沙盒允許的sysctl轉換為內核讀/寫原語。

XNU從FreeBSD繼承了sysctls;它們提供對用戶空間的某些內核變量的訪問。例如,”hw.l1dcachesize” 隻讀sysctl允許進程確定L1數據高速緩存線的大小。而“ kern.securelevel”讀/寫sysctl控製“係統安全級別”,用於BSD內核部分的操作。

sysctl被組織成樹層次結構,樹中的每個節點由sysctl_oid結構體表示。構建內核讀取原語非常簡單,隻需為app沙盒中可讀的sysctl映射sysctl_oid結構,並將目標變量指針(oid_arg1)更改為指向我們想要讀取的虛擬地址。然後調用sysctl讀取該地址。

iOS內核單字節利用技術(18)

使用sysctls構建寫原語有點複雜,因為在容器沙箱配置文件中沒有列出可寫的sysctls。iOS 10.3.1的ziVA exploit通過將sysctl的oid_handler字段更改為調用copyin()來解決這個問題。但是,在像A13這樣啟用PAC的設備上,oid_handler是由PAC保護的,也就是說我們無法更改它的值。

但是,在逆向hook_system_check_sysctlbyname()函數時,我注意到一個沒有文檔記錄的行為:

// Sandbox check sysctl-read
ret = sb_evaluate(sandbox, 116u, &context);
if ( !ret )
{
    // Sandbox check sysctl-write
    if ( newlen | newptr && (namelen != 2 || name[0] != 0 || name[1] != 3) )
        ret = sb_evaluate(sandbox, 117u, &context);
    else
        ret = 0;
}

出於某些原因,如果在沙箱中認為sysctl節點是可讀的,那麼就不會在特定的sysctl節點{0,3}上執行寫檢查!也就是說{0,3}在每個可讀的沙箱中都是可寫的,而不管沙箱配置文件是否允許對sysctl進行寫操作。

結果是,sysctl{0,3}的名稱是“sysctl.name2mib”,這是一個可寫的sysctl,用於將sysctl的字符串名轉換為數字形式,這樣查找起來更快。它用於實現sysctlnametomib()。因此,這個sysctl通常應該是可寫的。

0x04 回到主題pmap fields 之戰

我們已經研究了很久,但是還沒有結束:我們必須打破僵局。就目前情況而言,vm_map_copyout_internal()在“完成”的vm_map_entry 上進行無限循環,它的vme_next指針指向自己。我們必須引導這一功能的安全返回,以保持係統的穩定。

iOS內核單字節利用技術(19)

有兩個問題阻礙了這一點。首先,因為我們在pmap層將條目插入到頁表中,而沒有在vm_map層創建相應的虛擬條目,所以當前地址空間的pmap和vm_map視圖之間存在衝突。如果沒有解決這一問題,將導致進程退出時出現panic。其次,一旦循環中斷,vm_map_copyout_internal()將調用vm_map_copy_insert(),這會在將破壞的vm_map_copy釋放到錯誤區域時產生panic。

我們將首先處理pmap/vm_map衝突。假設我們能夠跳出for循環並允許vm_map_copyout_internal()返回。對vm_map_copy_insert()的調用發生在for循環遍曆vm_map_copy中的所有條目之後,將它們從vm_map_copy的條目列表中unlinks,並將它們鏈接到vm_map的條目列表中。

static void
vm_map_copy_insert(
    vm_map_t        map,
    vm_map_entry_t  after_where,
    vm_map_copy_t   copy)
{
    vm_map_entry_t  entry;

    while (vm_map_copy_first_entry(copy) !=
               vm_map_copy_to_entry(copy)) {
        entry = vm_map_copy_first_entry(copy);
        vm_map_copy_entry_unlink(copy, entry);
        vm_map_store_entry_link(map, after_where, entry,
            VM_MAP_KERNEL_FLAGS_NONE);
        after_where = entry;
    }
    zfree(vm_map_copy_zone, copy);
}

由於vm_map_copy的vm_map_entrys都是駐留在共享內存中的偽造對象,因此我們確實不希望將它們鏈接到 vm_map 的條目列表中,在進程退出時將其釋放。因此,最簡單的解決方案是更新破壞的vm_map_copy 的條目列表,使其看起來為空。

強製vm_map_copy 的條目列表顯示為空無疑使我們可以安全地從vm_map_copyout_internal()返回,但是一旦進程退出,我們仍然會造成panic:

panic(cpu 3 caller 0xfffffff01f4b1c50):“ pmap_tte_deallocate():pmap = 0xfffffff06cd8fd10 ttep = 0xfffffff0a90d0408 ptd = 0xfffffff132fc3ca0 refcnt = 0x2 \ n”

問題在於,在利用過程中,我們的映射原語強製pmap_enter_options()將3級轉換表條目(L3 TTEs)插入到流程的頁表中,但在vm_map上層的相應計算從未產生。pmap和vm_map視圖之間的區別很重要,因為pmap層要求在銷毀pmap之前顯式地刪除所有物理映射,如果 vm_map_entry 沒有描述相應的虛擬映射,則vm_map層將不知道刪除物理映射。

由於PPL的原因,我們不能直接更新pmap,因此最簡單的解決方法是獲取一個指向帶有錯誤頁麵的合法vm_map_entry的指針,並將其覆蓋在pmap_enter_options()建立物理映射的虛擬地址範圍之上。因此,我們將更新破壞的vm_map_copy條目列表,使其指向這個單一的“overlay”條目。

最後,是時候將vm_map_copyout_internal()從for循環中斷開了。

    for (entry = vm_map_copy_first_entry(copy);
        entry != vm_map_copy_to_entry(copy);
        entry = entry->vme_next) {

vm_map_copy_to_entry(copy) 宏擴展為:

    (struct vm_map_entry *)(©->c_u.hdr.links)

因此,為了跳出循環,我們需要處理一個vm_map_entry,其中vme_next 指向最初傳遞給此函數的已損壞的vm_map_copy中c_u.hdr.links 字段。

該函數目前在“done” vm_map_entry上轉換,我們需要鏈接到一個最終的“overlay” vm_map_entry中,以解決pmap/vm_map衝突問題。因此,打破循環的最簡單的方法是修改“overlay”條目的vme_next,使其指向&copy->c_u.hdr.links。然後更新“done”條目的vme_next,以指向覆蓋條目。

iOS內核單字節利用技術(20)

問題是前麵提到的對vm_map_copy_insert()的調用,它釋放vm_map_copy,好像它是ENTRY_LIST類型:

zfree(vm_map_copy_zone, copy);

但是,傳遞給zfree()的對象是已破壞的vm_map_copy,它是由kalloc()分配的;嚐試將其釋放到vm_map_copy_zone將導致panic。因此,我們需要確保將一個不同的、合法的vm_map_copy對象傳遞給zfree()。

如果您查看了vm_map_copyout_internal()的反彙編,vm_map_copy指針會在for循環期間溢出到堆棧中!

FFFFFFF007C599A4     STR     X28, [SP,#0xF0+copy]
FFFFFFF007C599A8     LDR     X25, [X28,#vm_map_copy.links.next]
FFFFFFF007C599AC     CMP     X25, X27
FFFFFFF007C599B0     B.EQ    loc_FFFFFFF007C59B98
...                             ; The for loop
FFFFFFF007C59B98     LDP     X9, X19, [SP,#0xF0+dst_addr]
FFFFFFF007C59B9C     LDR     X8, [X19,#vm_map_copy.offset]

這樣就可以很容易地確保傳遞給zfree()的指針是從vm_map_copy_zone分配的一個合法的vm_map_copy:隻要在vm_map_copyout_internal()線程轉換時掃描它的內核堆棧,並將指向破壞的vm_map_copy的指針與合法的指針交換。

iOS內核單字節利用技術(21)

最後,我們已經完全修複了狀態,允許vm_map_copyout_internal()中斷循環並安全返回。

在虛擬內核讀/寫原語和vm_map_copyout_internal()線程安全返回後,我們實現了我們的目標:通過將一個字節控製的堆溢出直接轉換為任意物理地址映射原語。

準確地說,一個幾乎任意的物理地址映射原語。正如我們所看到的,PPL保護的地址無法使用此技術進行映射。

單字節技術最初似乎與主流的攻擊流相對應。在閱讀了vm_map.c和pmap.c中的代碼之後,我希望能夠簡單地將所有DRAM映射到我的地址空間,然後通過使用這些映射執行手動頁表遍曆來實現內核讀/寫。但實踐證明,PPL通過阻止某些頁麵被映射來阻止這種技術在目前iOS上應用。

有意思是,類似的研究在幾年前也曾被提過,那時這樣的研究本是可行的。為這篇文章做背景調查的時候,我遇到了一個由Aximum公司提出的名為“iOS 6 Kernel Security:A Hacker’s Guide”:介紹了至少四個獨立的原語,這些原語可以通過破壞vm_map_u copy_t的各個字段來構建:相鄰內存泄漏、任意內存泄漏、擴展堆溢出,以及地址泄漏和已公開地址的堆溢出的組合。

iOS內核單字節利用技術(22)

在演示時,KERNEL_BUFFER 類型有一個稍微不同的結構,因此c_u.hdr.links.next 與存儲vm_map_copy 的kalloc()分配大小的字段重疊。在某些平台上,仍然可以將一個字節的溢出轉換為物理內存映射原語,但這將更加困難,因為這將需要映射空頁和共享地址空間。但是,上述四種技術中使用的更大的溢出肯定會改變type和c_u.hdr.links.next字段。

在OS X 10.11和ios9中,修改vm_map_copy結構,刪除KERNEL_BUFFER實例中冗餘的分配大小和內聯數據指針字段。這樣做可能是為了減輕這種結構在利用中被頻繁濫用的情況,盡管很難判斷,因為這些字段是冗餘的,可以簡單地刪除來清理代碼。無論如何,刪除這些字段將vm_map_copy更改為當前形式,將執行該技術所需的前提條件演變為單字節溢出。

緩解機製

那麼,各種iOS內核漏洞攻擊緩解措施在阻止單字節技術方麵效果如何,如果進一步加強,效果會有多大?

我考慮的緩解措施包括KASLR、PAN、PAC、PPL和zone_Required。還有許多其他的緩解措施,但它們要麼不適用於堆溢出類漏洞,要麼它們不是緩解這種特定技術的最佳方案。

首先,內核地址隨機化(KASLR)。在虛擬內存中移動kernelcache image,以及kernel_map和submaps(zone_map、kalloc_map等)的隨機化,它們統稱為“kernel heap”。內核堆隨機化表明確實需要某種方法來確定我們在其中偽造的VM對象的內核/用戶共享內存緩衝區的地址。。但是,一旦有了共享緩衝區的地址,兩種形式的隨機化都不會對該技術產生太大影響,原因有兩個:首先,存在通用的iOS內核堆源於,vm_map_copy 之前分配的區域,可用於將所有分配放入目標kalloc中。所以隨機化不會阻止初始內存破壞。其次,在破壞發生後,授權的原語是任意物理讀/寫的,這與虛擬地址隨機化無關。

唯一影響exploit的地址隨機化是物理內存中的kernelcache加載地址的隨機化。當iOS啟動時,iBoot以隨機地址將內核緩存加載到物理DRAM中。正如第一部分所討論的,這種物理隨機化隻有32 MB,但是,改進的隨機化不會有幫助,因為AMCC硬件寄存器可以被映射到物理內存中的內核緩存,而不管它位於哪裏。

其次考慮PAN,這是一種ARMv8.1安全緩解措施,可防止內核直接訪問用戶空間虛擬內存,從而防止覆蓋指向內核對象的指針,以使它們指向用戶空間中的偽造的對象的常見技術。繞過PAN是此技術的前提條件:我們需要在一個已知地址上建立vm_map_entry、vm_object和vm_page對象的結構。雖然對於這個POC來說,對共享緩衝區地址進行硬編碼已經足夠好了,但要真正利用它,還需要更好的技術。

PAC,即指針身份驗證代碼,是蘋果A12 SOC中引入的ARMv8.3安全特性。iOS內核使用PAC有兩個目的:首先是針對常見漏洞的利用緩解措施,其次是作為內核控製流完整性的一種形式,以防止使用內核讀/寫的攻擊者獲得任意代碼執行。在這個設置中,我們隻對PAC作為一種漏洞利用緩解措施感興趣。

Apple的網站上有一個表格顯示了各種類型的指針是如何被PAC保護的,這些指針大部分都是被編譯器自動PAC保護的,到目前為止PAC對c++對象的影響最大,尤其是IOKit。同時,單字節利用技術隻涉及vm_map_copy、vm_map_entry、vm_object和vm_page對象,這些都是內核Mach部分中的普通C結構,因此不受PAC的影響。

但是,在BlackHat 2019上,蘋果公司的Ivan Krstić 將很快用於保護某些“重要的數據結構成員”,包括“進程、任務、代碼簽名、虛擬內存子係統,IPC結構”。截至2020年5月,增強的PAC保護尚未發布,但如果實施,它可能會有效地阻止單字節技術。

下一個緩解是PPL,它代表頁麵保護層。PPL在管理頁表的代碼和XNU內核之間創建了一個安全邊界。這是除PAN之外唯一影響該利用技術的緩解措施。

在實踐中,PPL在允許將哪些物理地址映射到用戶空間進程方麵要嚴格得多。例如,用戶空間進程訪問內核緩存頁麵沒有合法的用例,因此在kernelcache頁麵上設置PVH_FLAG_LOCKDOWN這樣的標誌可能是一個微弱但明智的步驟。一般而言,對於大多數進程而言,可能會使應用處理器的DRAM區域之外的地址(包括用於硬件組件的物理I/O地址)無法映射,可能會有一個特殊情況下逃逸權限。

最後一個緩解是zone_require,這是在iOS 13中引入的一個軟件緩解機製,它在使用一些內核指針之前檢查它們是否從預期的zalloc區域分配。我認為XNU的區域分配器最初並不是為了降低安全性,在漏洞利用期間經常成為目標,許多對象(特別是ipc_ports、tasks和threads)都是從專屬zone分配的。

理論上,zone_require可以用來保護從專屬區域分配的幾乎所有對象;但事實上,kernelcache中的絕大多數zone_require()檢查都是在ipc_port對象上進行的。因為單字節技術避免了使用偽造的Mach ports,所以現有的zone_require()檢查都不適用。

但是,如果擴大了zone_require的使用範圍,則有可能緩解該技術。特別是,一旦vm_map_copyout_internal()確定了vm_map_copy的類型為ENTRY_LIST,那麼在vm_map_copy中插入一個zone_require()調用,將確保vm_map_copy不會是具有已破壞的KERNEL_BUFFER對象。當然,與所有緩解措施一樣,這也不是100%可靠的:在exploit中使用該技術仍然可以,但是與單字節溢出相比,它可能需要更好的原語。

0x05 “附錄A”:漏洞利用史

在我看來,在這篇文章中介紹的單字節利用技術與至少自iOS 10以來采用的傳統方法是有區別的。自從iOS10以來,我發現的24個原始公開漏洞中,有19個使用偽造Mach ports 作為exploitation原語。在iOS10.3 以來公布的20個exploits中,有18個是通過構建一個偽造的內核task port,這使得Mach ports成為目前iOS內核利用的特性。

在經曆了使用單字節技術在模擬堆溢出的基礎上構建內核讀/寫原語的過程之後,,我可以看到使用內核task port的邏輯。自從iOS10之後,我看到的大多數exploits都有一個相對模塊化的設計和流程:獲得初始原語,操作狀態,采用漏洞利用技術來構建更強大的原語,再次對狀態進行操作,然後應用另一種技術,等等,直到最後你有足夠的能力來構建一個偽造的內核task port。整個過程都有所檢查:初始破壞、掛起的Mach port、4字節讀原語等等。每一種情況下的具體步驟順序是不同的,但大體上來講,不同exploits的設計是一致的。由於這種趨勢,最後一個exploits的步驟幾乎可以與其他任何exploits的步驟互換。

這種模塊化並不適用於這種單字節技術。一旦啟動了vm_map_copyout_internal()循環,就會一直執行這個過程,直到獲得內核讀/寫原語為止。並且由於vm_map_copyout_internal()在循環期間持有 vm_map 鎖,因此無法執行任何虛擬內存操作(比如分配虛擬內存),而這些操作通常是常規漏洞利用中不可缺少的步驟。因此,編寫這個exploit感覺有所不同,更麻煩。

話雖如此,單字節方法給我直接的感覺是“技術上更精巧”:它將較弱的前提條件直接轉化為非常強大的原語,同時避開了大多數緩解措施,避免了通用iOS漏洞利用中出現的大多數不穩定因素。在我所分析的24個iOS漏洞中,有22個依賴於重新分配一個最近被釋放的對象的slot。除了SockPuppet這個例外,這是一個危險操作,因為另一個線程可能會重新分配該slot。此外,自從iOS 11以來的19個exploits中有11個依賴於強製垃圾回收區域,這是一個更危險的步驟,通常需要幾秒鍾才能完成。

同時,單字節技術沒有內在的不穩定因素或大量的時間成本。看起來更像是我認為熟練的攻擊者會感興趣的技術類型。即使在漏洞利用過程中出了點問題,並且內核中取消了錯誤地址的引用,持有 vm_map 鎖的也證明該錯誤會導致死鎖而不是內核崩潰,exploit失敗看起來像冷凍過程而不是係統崩潰。你甚至可以在應用切換UI中“kill”死鎖應用,然後繼續使用該設備。

0x06 “附錄B”:結論

最後,我將回到本文最開始的三個問題:

1.將內核task port作為目標真的是最好的利用方法嗎?2.還是這種趨勢掩蓋了其他也許更好的技術?3.現有的iOS內核緩解措施是否同樣有效地防止了其他以前未被發現的漏洞利用方法?

這些問題都太“模糊”,無法給出具體的答案,但是無論如何我都會嚐試回答。

對於第一個問題,我認為答案是否定的,內核task port不是唯一的最佳利用方法。單字節技術在大多數情況下都一樣好用,而且在我個人看來,我希望還有其他尚未公布的技術也一樣好用。

對於第二個問題,關於內核task port的趨勢是否已經掩蓋了其他技術:我認為沒有足夠公開的iOS研究來給出結論,但我的直覺是肯定的。根據我自己的經驗,了解我要尋找的漏洞類型影響了我所發現的漏洞,並且回顧過去的exploits指導了我在漏洞利用過程中的選擇。

最後,現有的iOS內核漏洞利用緩解措施是否有效地防止了未被發現的漏洞利用方法?在我為單字節技術開發了POC之後,我認為答案是否定的;但是在這次分析的最後,我不太確定。我不認為PPL是專門設計來防止這種技術的,PAC並沒有做任何事情來阻止這種技術,但是將來PAC保護的指針可能會阻止這種技術。雖然zone_require根本沒有影響該exploit,但是添加一行代碼將是從單字節溢出增強為跨越區域邊界的溢出所需的前提條件。因此,即使以目前的形式,Apple的內核漏洞利用緩解措施對於這種未公開的技術也無效。

最後一個想法。在2018年公開的Deja-XNU中,Ian Beer考慮了四年前iOS內核利用的”state-of-the-art” :

一段時間以來,我一直想嚐試的一個想法是,利用我在iOS學習到的知識,重新研究過去的漏洞,然後嚐試再次利用它們,我希望這篇文章能讓我們了解到幾年前最新的iOS漏洞利用是什麼樣子的,如果我們能進一步思考目前最新的iOS漏洞利用是什麼樣子的話,這篇文章可能會對我們有所幫助。

這是一個需要考慮的問題,因為作為防守方,我們從來沒有看到過最前沿攻擊者的能力。如果攻擊者在私下使用的技術和防守方所知道的技術之間出現了差距,則防守方可能會浪費資源來緩解這種技術。

我不認為這種技術代表了目前最新的技術。就像Deja-XNU一樣,它可能代表了幾年前的水平。值得考慮的是,在此期間,最新的技術可能會走向什麼方向。

本文翻譯自 googleprojectzero.blogspot.com, 原文鏈接 。如若轉載請注明出處。

我是安仔,一名剛入職網絡安全圈的網安萌新,歡迎關注我,跟我一起成長; 歡迎大家私信回複【入群】,加入安界網大咖交流群,跟我一起交流討論。

安界網周年慶,cisp考證報名送價值6000元電腦;nisp一級報名送100元現金劵;nisp二級報名送1000元現金劵。更多優惠聯係我們。

小白入行網絡安全、混跡安全行業找大咖,以及更多成長幹貨資料,歡迎關注#安界網人才培養計劃#、#網絡安全在我身邊#、@安界人才培養計劃

我要分享:

最新熱門遊戲

版權信息

Copyright @ 2011 係統粉 版權聲明 最新發布內容 網站導航