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

1.1 簡介

XNU的內核內存分配器層次比較多, 因為它是一個混合的內核,bsd、mach層都有自己的內存分配器接口, 但最底層的都是調用zone allocotr分配器。它的內存分配器設計非常簡單,大概是我讀過的眾多主流os內核中無論數據結構還是分配算法都是最簡單的一個。我時常在想XNU內核給MacOS提供了流暢的操作性,但是只從zone的內存分配器來看并不能支撐這個結論,或許慢慢隨著筆者對XNU內核的深入理解,答案也會慢慢水落石出。不過本次我們將探討下zone內存分配器的安全特性以及設計不足,值得肯定的是zone內存分配器在調試和安全特性上的支持已經遠遠甩出了FREEBSD內核,于linux的slab內存分配器也有過之而無不及, 各有春秋。

1.2 zone分配器的基本結構

Zone分配器的最基本管理結構為struct zone_page_metadata, 它相當于linux slab的slab管理結構體。

struct zone_page_metadata {
        queue_chain_t           pages;
        union {
                uint32_t                freelist_offset;
                uint32_t                real_metadata_offset;
        };

        uint16_t                        free_count;
        unsigned                        zindex     : ZINDEX_BITS;   
        unsigned                        page_count : PAGECOUNT_BITS;
};

結構成員中最重要的是freelist_offset, 它保存的是下一個空閑的item地址。 一個item在內存中的結構圖為:

1.3 堆溢出檢測

業界常用的檢測堆溢出的算法為在一個item前后填充若干redzone值,申請或釋放內存時對redzone值進行檢測,以發現是否有溢出行為的發生。XNU內核只有在打開KASAN_ZALLOC內核選項時才會填充redzone。

osfmk/kern/zalloc.c:

static inline vm_offset_t
try_alloc_from_zone(zone_t zone,
                        vm_tag_t tag __unused,
                    boolean_t* check_poison)
{
#if KASAN_ZALLOC
        kasan_poison_range(element, zone->elem_size, ASAN_VALID);
#endif
}

kasan_poison_range->kasan_poison
kasan_poison(vm_offset_t base, vm_size_t size, vm_size_t leftrz, vm_size_t rightrz, uint8_t flags)
{
        uint8_t *shadow = SHADOW_FOR_ADDRESS(base);
        uint8_t partial = size & 0x07;
        vm_size_t total = leftrz + size + rightrz;
        vm_size_t i = 0; 

        if (!kasan_enabled || !kasan_poison_active(flags)) {
                return;
        }

        leftrz /= 8;
        size /= 8;
        total /= 8;

        uint8_t l_flags = flags;
        uint8_t r_flags = flags;

        if (flags == ASAN_STACK_RZ) {
                l_flags = ASAN_STACK_LEFT_RZ;
                r_flags = ASAN_STACK_RIGHT_RZ;
        } else if (flags == ASAN_HEAP_RZ) {
                l_flags = ASAN_HEAP_LEFT_RZ;
                r_flags = ASAN_HEAP_RIGHT_RZ;
        }
        for (; i < leftrz; i++) {
                shadow[i] = l_flags;
        }

        for (; i < leftrz + size; i++) {
                shadow[i] = ASAN_VALID; /* XXX: should not be necessary */
        }

        if (partial && (i < total)) {
                shadow[i] = partial;
                i++;
        }

        for (; i < total; i++) {
                shadow[i] = r_flags;
        }
}

Zone依據內存分配器的不同階段,調用kasan_poison時構造的item前后redzone填充值也不相同。

san/kasan.h

#define ASAN_VALID          0x00
#define ASAN_HEAP_RZ        0xe9
#define ASAN_HEAP_LEFT_RZ   0xfa
#define ASAN_HEAP_RIGHT_RZ  0xfb
#define ASAN_HEAP_FREED     0xfd

Redzone的填充值與其他OS的實現一樣都使用了默認值, exploit程序很容易對其繞過,在筆者給linux slab開發的內核加固補丁AKSP中,堆redzone使用了隨機值,進一步提升了安全性。

1.4 DOUBLE FREE檢測

Zone內存分配器可以做簡單的double free檢測。

osfmk/kern/zalloc.c:

static inline void
free_to_zone(zone_t      zone,
             vm_offset_t element,
             boolean_t   poison)
{
page_meta = get_zone_page_metadata((struct zone_free_element *)element, FALSE); [1]
    old_head = (vm_offset_t)page_metadata_get_freelist(page_meta);              [2]
    if (__improbable(old_head == element))                 [3]
         panic("zfree: double free of %p to zone %s\n",
              (void *) element, zone->zone_name);
}

[1] 處通過參數element獲取struct zone_page_metadata管理體地址,然后獲取其保存的下一個空閑item地址old_head, 在[3]進行比對, 如果相等說明有重復釋放的行為。

這種算法只能檢測簡單的double free操作,也就是連續釋放兩次相同的item地址。

Free(addr1);

Free(addr1);

對于下面這種情況就檢測不到了。

Free(addr1);

Free(addr2);

Free(addr1);

在筆者給linux slab開發的內核加固補丁AKSP中,可以檢測上述或者更復雜的多重釋放問題。

1.5 UAF檢測

業界常用的UFA檢測算法是給item填充固定的poison值,在申請內存時檢測posion是否改變以此來發現UAF的行為。

osfmk/kern/zalloc.c

static void *
zalloc_internal(
        zone_t  zone,
        boolean_t canblock,
        boolean_t nopagewait,
        vm_size_t
#if !VM_MAX_TAG_ZONES
    __unused
#endif
    reqsize,
        vm_tag_t  tag)
{
        zalloc_poison_element(check_poison, zone, addr);
}

void
zalloc_poison_element(boolean_t check_poison, zone_t zone, vm_offset_t addr)
{
        vm_offset_t     inner_size = zone->elem_size;

        if (__improbable(check_poison && addr)) {
                vm_offset_t *element_cursor  = ((vm_offset_t *) addr) + 1;       [1]
                vm_offset_t *backup  = get_backup_ptr(inner_size, (vm_offset_t *) addr);[2]

                for ( ; element_cursor < backup ; element_cursor++)[3]
                        if (__improbable(*element_cursor != ZP_POISON))
                                zone_element_was_modified_panic(zone,
                                                                addr,
                                                                *element_cursor,
                                                                ZP_POISON,
                                                                ((vm_offset_t)element_cursor) - addr);
        }
}

[1] 處首先取得poison的首地址,注意item的第一個地址為加密后的next pointer值,它是與zp_nopoison_cookie異或計算的結果,所以要跳過第一個地址, [2]處取得posion的最后一個地址,在前面的item內存結構視圖中可以看到item的最后一個地址保存的是next_pointer的值,它是與zp_poisoned_cookie或者zp_nopoison_cookie異或計算的結果。

zp_poisoned_cookie與zp_nopoison_cookie是在zone子系統初始化時動態隨機生成的,所有的zone共用同一個值。[3]處與默認填充的poison值進行比對, 如果比對失敗,說明這個item在分配之前已經被改寫過了。

前面分析過item的第一個地址保存的是next pointer的一個混淆值, 在item的最后還保存了一個next pointer的混淆值副本。所以除了填充poison的方法, zone內存分配器還可以使用next pointer和其副本的對比,來發現UAF的行為。

static inline vm_offset_t
try_alloc_from_zone(zone_t zone,
                        vm_tag_t tag __unused,
                    boolean_t* check_poison)
{
        if (__improbable(next_element != (next_element_backup ^ zp_nopoison_cookie))) {
                if (__improbable(next_element != (next_element_backup ^ zp_poisoned_cookie)))
                        /* Neither cookie is valid, corruption has occurred */
                        backup_ptr_mismatch_panic(zone, element, next_element_primary, next_element_backup);
        }
}

由于next pointer的副本根據是否需要填充poison使用不同的xor值,所以分別進行了兩次比對。

在進行完UAF檢查后,zalloc_poison_element還會堆next pointer進行擦除,以后防止地址泄露,并且從泄露的地址推測zp_nopoison_cookie隨機值。

void
zalloc_poison_element(boolean_t check_poison, zone_t zone, vm_offset_t addr)
{
        if (addr) {
                vm_offset_t *primary  = (vm_offset_t *) addr;
                vm_offset_t *backup   = get_backup_ptr(inner_size, primary);

                *primary = ZP_POISON;
                *backup  = ZP_POISON;
        }
}

1.6 item地址隨機化

內核堆的攻擊技術鏈中,item的地址初始化順序十分重要, exploit以此來精確控制要覆蓋的item地址, linux slab使用了洗牌算法將slab里item的初始化順序完全打亂。但是XNU內核使用的item隨機化只有兩種情況, 正向順序或逆向順序,這只能在一定程度上緩解地址隨機化問題,相比linux的洗牌算法會變弱了很多。

random_free_to_zone(
                        zone_t          zone,
                        vm_offset_t     newmem,
                        vm_offset_t     first_element_offset,
                        int             element_count,
                        unsigned int    *entropy_buffer)
{
        assert(element_count && element_count <= ZONE_CHUNK_MAXELEMENTS);

        elem_size = zone->elem_size;
        last_element_offset = first_element_offset + ((element_count * elem_size) - elem_size);
        for (index = 0; index < element_count; index++) {
                assert(first_element_offset <= last_element_offset);
                if (
#if DEBUG || DEVELOPMENT
                leak_scan_debug_flag || __improbable(zone->tags) ||
#endif /* DEBUG || DEVELOPMENT */
                random_bool_gen_bits(&zone_bool_gen, entropy_buffer, MAX_ENTROPY_PER_ZCRAM, 1)) {[1]
                        element_addr = newmem + first_element_offset;
                        first_element_offset += elem_size;
                } else {[2]
                        element_addr = newmem + last_element_offset;
                        last_element_offset -= elem_size;
                }

                if (element_addr != (vm_offset_t)zone) {
                        zone->count++;  /* compensate for free_to_zone */
                        free_to_zone(zone, element_addr, FALSE);
                }

                zone->cur_size += elem_size;
        }

}

random_bool_gen_bits產生一些隨機的0或1,從而選擇是正向順序分配還是逆向順序分配item地址。

1.7 內存拷貝檢查

Zone內存分配器提供了一個啟動參數-no-copyio-zalloc-check, 當發生從用戶空間向內核空間拷貝數據時,會檢測內核空間是否屬于zone的空間,如果屬于那么拷貝的字節數就不能大于zone的item size,這是一個非常棒的安全檢測功能。有點類似linux slab的hardened user copy算法, 只不過它防止的是從內核向用戶空間拷貝敏感的數據,限制了拷貝的范圍。

1.8 雙向安全鏈表

盡管針對內核堆溢出的攻擊中, 很少見到改寫雙向鏈表節點的攻擊手段。但是為了防患于未然,或者說養成良好的安全編程習慣, NT和linux內核都使用了安全雙向鏈表檢查,而XNU未提供此能力。


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