作者:wzt
原文鏈接:https://mp.weixin.qq.com/s/iv55u9VC7R1rZmhbfTMcRA

這篇文章主要是討論ios內核堆的分配特性,辟謠下:zalloc內存分配器并沒有在釋放內存時將object隨機的插入到freelist鏈表里。xnu內核沒有單獨設計這個特性,這個只是zalloc架構特性導致的,無意中讓風水的布局變困難了些。筆者分析過windows、linux、bsd這些os的內存分配器都沒有發現釋放隨機化的安全特性, 因為只有保持內存分配器fifo的特性,才能加快分配速度,任何一個內存分配器的設計值都不會違背這個原則。

下面將仔細探討下xnu的zalloc特性, 在xnu-4903內核中,一個zone有四條隊列:

struct zone {
        struct {
                queue_head_t                    any_free_foreign;       
                queue_head_t                    all_free;
                queue_head_t                    intermediate;
                queue_head_t                    all_used;
        } pages;
}

半滿外圍隊列any_free_foreign,全空隊列all_free,半滿半空隊列intermediate,全滿隊列all_used。我們先討論下zcache關閉的情況下,在try_alloc_from_zone分配一個object內存時有如下調用:

static inline vm_offset_t
try_alloc_from_zone(zone_t zone,
                        vm_tag_t tag __unused,
                    boolean_t* check_poison)
{
        if (zone->allows_foreign && !queue_empty(&zone->pages.any_free_foreign))
                page_meta = (struct zone_page_metadata *)queue_first(&zone->pages.any_free_foreign);
        else if (!queue_empty(&zone->pages.intermediate))
                page_meta = (struct zone_page_metadata *)queue_first(&zone->pages.intermediate);
        else if (!queue_empty(&zone->pages.all_free)) {
                page_meta = (struct zone_page_metadata *)queue_first(&zone->pages.all_free);
                assert(zone->count_all_free_pages >= page_meta->page_count);
                zone->count_all_free_pages -= page_meta->page_count;
        } else {
                return 0;
        }
}

如果zone開啟了allows_foreign并且any_free_foreign隊列不為空,則從此隊列取出隊列頭節點page_meta,從這個meta里分配內存,如果條件不符合,那么依次從intermediate半滿半空隊列里取meta,如果還是不符合,那么在從all_free隊列里選meta。一個zone是否支持allows_foreign,是通過調用zone_change函數設置的,筆者搜索了整個xnu源碼文件,發現只有vm自身的幾個zone才會使用此標志,因此jailbreaker關心的zone,比如ipc_port都沒有使用,這將簡化exploit程序對堆風水布局的處理。當選取完meta后,通過page_metadata_get_freelist獲取了meta上的第一個空閑節點,然后要通過page_metadata_set_freelist重新設置新的空閑節點,滿足下次的分配需求。最后還要重新布局下當前meta所在的隊列。try_alloc_from_zone函數最后有如下代碼:

static inline vm_offset_t
try_alloc_from_zone(zone_t zone,
                        vm_tag_t tag __unused,
                    boolean_t* check_poison)
{
        if (page_meta->free_count == 0) {[1]
                re_queue_tail(&zone->pages.all_used, &(page_meta->pages));
        } else {
                if (!zone->allows_foreign || from_zone_map(element, zone->elem_size)) { 
                        if (get_metadata_alloc_count(page_meta) == page_meta->free_count + 1) {[2]
                                re_queue_tail(&zone->pages.intermediate, &(page_meta->pages));
                        }
                }
        }
}

[1] 當這個meta在分配完一個空閑節點后, 如果free_count為0,說明meta已經沒有空閑的節點了,那么就要將這個meta節點轉移到all_used隊列。 [2]處的判斷meta是否為一個全空的隊列,當它分配一個空閑節點后,就變成半滿的狀態了,因此需要轉移到半滿隊列intermediate。轉移隊列的操作函數為re_queue_tail:

re_queue_tail(queue_t que, queue_entry_t elt)
{
        queue_entry_t   n_elt, p_elt;
        /* remqueue */
        n_elt = elt->next;[1]
        p_elt = elt->prev;
        n_elt->prev = p_elt;
        p_elt->next = n_elt;

        /* enqueue_tail */
        p_elt = que->prev;[2]
        elt->next = que;
        elt->prev = p_elt;
        p_elt->next = elt;
        que->prev = elt;
}

[1] 處先從當前隊列刪除自身,然后[2]處將其掛載到新隊列的末尾。注意這個操作是掛接到隊尾,那么當出現一些極端狀況發生隊列轉移時,如果新的隊列節點不為空,就要等待前面所有的節點都被用完或發生隊列遷移時,這個節點才會被調用到,這就發生了不是fifo的情況,對于jailbreaker的風水布局就會產生影響,這個狀態被一些人誤以為是ios提供了新的安全特性,在釋放時”隨機”的將object插入到freelist鏈表中。

然后有個很有意思的事情發生了, 在最新的xnu-7195.81.3內核中,筆者發現meta隊列轉移的操作不是掛接到隊列末尾,而是隊列頭部,將隊列變成fifo的鏈表了!

__header_always_inline void
zone_meta_queue_push(zone_t z, zone_pva_t *headp,
    struct zone_page_metadata *meta, zone_addr_kind_t kind)
{
        zone_pva_t head = *headp;
        zone_pva_t queue_pva = zone_queue_encode(headp);             [1]
        struct zone_page_metadata *tmp;

        meta->zm_page_next = head; [2]
        if (!zone_pva_is_null(head)) {
                tmp = zone_pva_to_meta(head, kind);
                if (!zone_pva_is_equal(tmp->zm_page_prev, queue_pva)) { [3]
                        zone_page_metadata_list_corruption(z, meta);
                }
                tmp->zm_page_prev = zone_pva_from_meta(meta, kind);  [4]
        }
        meta->zm_page_prev = queue_pva;                           [5]
        *headp = zone_pva_from_meta(meta, kind);                    [6]
}

新內核的struct zone_page_metadata結構多了兩個成員:

struct zone_page_metadata {
        zone_pva_t      zm_page_next;
        zone_pva_t      zm_page_prev;
}

在zone中的隊列頭也使用zone_pva_t重新定義:

typedef struct zone_packed_virtual_address {
        uint32_t packed_address;
} zone_pva_t

struct zone {
        zone_pva_t          pages_any_free_foreign;     
        zone_pva_t          pages_all_used_foreign;
        zone_pva_t          pages_all_free;
        zone_pva_t          pages_intermediate;
        zone_pva_t          pages_all_used;
        zone_pva_t          pages_sequester;
}

從四條隊列變為了六條隊列,每個隊列頭的地址是被編碼起來保存的,通過zone_pva_from_meta函數將一個meta進行轉化編碼為隊列地址,其實就是meta地址在meta base中的偏移。每個meta結構通過雙向鏈鏈表起來,如果meta是隊列中的第一個節點,則它的zm_page_prev指向的是一個”特殊地址”, 這個地址通過zone_queue_encode進行編碼,主要原理是把當前隊列的地址轉為為在zone_array中的索引進行保存。當以后隊列節點不為空時,通過[3]處的比較可以確定當前隊列的頭節點是否被破壞了。如果meta不是隊列中的第一個節點,則指向前一個帶有正確編碼的隊列地址。[6]處的操作將meta節點掛載到了最隊列的頭部,所以這是一個fifo的隊列。

在隊列取節點時,也是從隊列頭取出,而不是隊列末尾。

__header_always_inline struct zone_page_metadata *
zone_meta_queue_pop(zone_t z, zone_pva_t *headp, zone_addr_kind_t kind,
    vm_offset_t *page_addrp)
{
        zone_pva_t head = *headp;
        struct zone_page_metadata *meta = zone_pva_to_meta(head, kind);
        vm_offset_t page_addr = zone_pva_to_addr(head);
        struct zone_page_metadata *tmp;

        if (kind == ZONE_ADDR_NATIVE && !from_native_meta_map(meta)) {
                zone_page_metadata_native_queue_corruption(z, headp);
        }

        if (kind == ZONE_ADDR_FOREIGN && from_zone_map(meta, sizeof(*meta))) {
                zone_page_metadata_foreign_queue_corruption(z, headp);
        }

        if (!zone_pva_is_null(meta->zm_page_next)) {
                tmp = zone_pva_to_meta(meta->zm_page_next, kind);
                if (!zone_pva_is_equal(tmp->zm_page_prev, head)) {
                        zone_page_metadata_list_corruption(z, meta);
                }
                tmp->zm_page_prev = meta->zm_page_prev;
        }
        *headp = meta->zm_page_next;
        *page_addrp = page_addr;
        return meta;
}

那么在新內核里,對于布局堆的風水其實是更加便利了!


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1487/