XNU 進程地址隨機化解讀

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

1.1 進程棧、代碼段地址隨機化

這裏指的是進程的用戶態棧,記住一個進程實際擁有兩個棧, 一個用於跑用戶態的代碼,一個用於請求系統調用時在內核中使用的棧空間。在前面分析BSD進程隨機化時,我們注意到bsd並沒有給進程的用戶態棧加入地址隨機化, XNU雖然繼承了BSD進程模型,但作爲一個商業操作系統沒有棧的隨機化功能顯然是說不過去的, 自然給其進程加入了隨機化功能。

bsd/kern/mach_loader.c

load_return_t
load_machfile(
        struct image_params     *imgp,
        struct mach_header      *header,
        thread_t                thread,
        vm_map_t                *mapp,
        load_result_t           *result
)
{
        if (!(imgp->ip_flags & IMGPF_DISABLE_ASLR)) {
                vm_map_get_max_aslr_slide_section(map, &aslr_section_offset, &aslr_section_size); [1]
                aslr_section_offset = (random() % aslr_section_offset) * aslr_section_size;[2]
                aslr_page_offset = random();[3]
                aslr_page_offset %= vm_map_get_max_aslr_slide_pages(map);
                aslr_page_offset <<= vm_map_page_shift(map);

                dyld_aslr_page_offset = random();[4]
                dyld_aslr_page_offset %= vm_map_get_max_loader_aslr_slide_pages(map);
                dyld_aslr_page_offset <<= vm_map_page_shift(map);

                aslr_page_offset += aslr_section_offset;[5]
        }
}

除了mmap,進程的棧、代碼段以及共享庫的地址隨機化都是在內核從磁盤加載二進制文件時完成的。[1]處vm_map_get_max_aslr_slide_section函數選取了隨機化的範圍,

void

vm_map_get_max_aslr_slide_section(
                vm_map_t                map __unused,
                int64_t                 *max_sections,
                int64_t                 *section_size)
{
#if defined(__arm64__)
        *max_sections = 3;
        *section_size = ARM_TT_TWIG_SIZE;
#else
        *max_sections = 1;
        *section_size = 0;
#endif
}

aslr_section_offset表示的隨機幾個頁面大小,aslr_section_size表示每個頁面的大小,對於不同的CPU體系架構擁有不同的值,在arm64下 aslr_section_offset設定爲3,頁面大小設置爲0x0000000000200000ULL,其他架構下aslr_section_offset設爲1, 頁面大小設爲0。[2]處使用random函數生成了一個臨時隨機範圍aslr_section_offset。[3]處的aslr_page_offset表示棧和代碼段使用的隨機範圍,沒錯xnu的棧和text代碼使用的是同一個隨機範圍, 而linux使用的都是不同的。Xnu這樣做提升了一點性能,但安全性也會降低一些。vm_map_get_max_aslr_slide_pages函數選取了要隨機多少個頁面大小,在arm64下爲1<<12,在其他架構的64位下爲1<<16,32位爲1<<8。

uint64_t

vm_map_get_max_aslr_slide_pages(vm_map_t map)
{
#if defined(__arm64__)
        /* Limit arm64 slide to 16MB to conserve contiguous VA space in the more
         * limited embedded address space; this is also meant to minimize pmap
         * memory usage on 16KB page systems.
         */
        return (1 << (24 - VM_MAP_PAGE_SHIFT(map)));
#else
        return (1 << (vm_map_is_64bit(map) ? 16 : 8));
#endif
}

最後aslr_page_offset在左移12位,那麼在arm64下其隨機化的範圍就爲0-16MB,其實隨機化的範圍並不大,而且都是以頁面對齊的,所以只需暴力才解4096次就能猜到offset。即使加上[5]處的臨時offset,也提高不了多少安全等級。

[4] 處的vm_map_get_max_loader_aslr_slide_pages計算的是共享庫的地址隨機化範圍,與上述類似,最終隨機範圍爲0-4MB。

Win10進程棧在64位下可以做到上TB的隨機化範圍, 筆者也給linux擴展了棧的隨機化範圍,通過打入AKSP補丁,棧的隨機化也可以做到上TB的範圍。如此來看,XNU進程的棧地址隨機化未免有點小家子氣了。

對棧基地址的設置是在load_unixthread裏設置的:

static

load_return_t
load_unixthread(
        struct thread_command   *tcp,
        thread_t                thread,
        int64_t                         slide,
        load_result_t           *result
)
{
        ret = load_threadstack(thread,
                                (uint32_t *)(((vm_offset_t)tcp) +
                                        sizeof(struct thread_command)),
                                tcp->cmdsize - sizeof(struct thread_command),
                                &addr, &customstack, result);
        result->user_stack = addr;
        result->user_stack -= slide;
}

load_threadstack選取了棧基地址,然後減去slide。Slide爲上述的aslr_page_offset,但是它的使用還有個前提條件:

static

load_return_t
parse_machfile(
        struct vnode            *vp,
        vm_map_t                map,
        thread_t                thread,
        struct mach_header      *header,
        off_t                   file_offset,
        off_t                   macho_size,
        int                     depth,
        int64_t                 aslr_offset,
        int64_t                 dyld_aslr_offset,
        load_result_t           *result,
        load_result_t           *binresult,
        struct image_params     *imgp
)
{
        int64_t                 slide = 0;

        if ((header->flags & MH_PIE) || is_dyld) {
                slide = aslr_offset;
        }
}

Slide初始化爲0,只有當二進制爲PIE編譯或者爲動態連接器纔會被設置爲aslr_offset,這樣對於普通的二進制程序棧並沒有地址隨機化能力!

1.2 mmap地址隨機化

XNU提供了posix標準的mmap函數,對於匿名映射的內存地址隨機化是在mach層的vm_map_enter函數來設置的。

osfmk/vm/vm_map.c

kern_return_t
vm_map_enter(
        vm_map_t                map,
        vm_map_offset_t         *address,       /* IN/OUT */
        vm_map_size_t           size,
        vm_map_offset_t         mask,
        int                     flags,
        vm_map_kernel_flags_t   vmk_flags,
        vm_tag_t                alias,
        vm_object_t             object,
        vm_object_offset_t      offset,
        boolean_t               needs_copy,
        vm_prot_t               cur_protection,
        vm_prot_t               max_protection,
        vm_inherit_t            inheritance)
{
        boolean_t               random_address = ((flags & VM_FLAGS_RANDOM_ADDR) != 0);[1]

        if (anywhere) {
                vm_map_lock(map);
                map_locked = TRUE; 

                if (entry_for_jit) {[2]
#if CONFIG_EMBEDDED
                        if (map->jit_entry_exists) {
                                result = KERN_INVALID_ARGUMENT;
                                goto BailOut;
                        }

                        random_address = TRUE;
#endif
                }

                if (random_address) {
                        result = vm_map_random_address_for_size(map, address, size); [3]
                        if (result != KERN_SUCCESS) {
                                goto BailOut;
                        }
                        start = *address;
                }
}

對於主動提供了VM_FLAGS_RANDOM_ADDR標誌或者在CONFIG_EMBEDDED下開啓了jit code條件下都會使用[3]處的vm_map_random_address_for_size函數選取一塊包含了隨機化範圍的起始地址。

#define MAX_TRIES_TO_GET_RANDOM_ADDRESS 1000

kern_return_t
vm_map_random_address_for_size(
        vm_map_t        map,
        vm_map_offset_t *address,
        vm_map_size_t   size)
{
addr_space_size = vm_map_max(map) - vm_map_min(map);[1]

        while (tries < MAX_TRIES_TO_GET_RANDOM_ADDRESS) {
                random_addr = ((vm_map_offset_t)random()) << PAGE_SHIFT; [2]
                random_addr = vm_map_trunc_page(
                        vm_map_min(map) +(random_addr % addr_space_size),
                        VM_MAP_PAGE_MASK(map));

                if (vm_map_lookup_entry(map, random_addr, &prev_entry) == FALSE) {
                        if (prev_entry == vm_map_to_entry(map)) {
                                next_entry = vm_map_first_entry(map);
                        } else {
                                next_entry = prev_entry->vme_next;
                        }
                        if (next_entry == vm_map_to_entry(map)) {
                                hole_end = vm_map_max(map);
                        } else {
                                hole_end = next_entry->vme_start;
                        }

                        vm_hole_size = hole_end - random_addr;
                        if (vm_hole_size >= size) {
                                *address = random_addr;
                                break;
                       }
                }
                tries++;
        } 

        if (tries == MAX_TRIES_TO_GET_RANDOM_ADDRESS) {
                kr = KERN_NO_SPACE;
        }

        return kr;
}

這個函數比較奇葩, 嘗試循環1000次找到帶有隨機化範圍的vm_map_entry,[1]處首先計算當前進程還剩的虛擬內存空間大小, [2]處使用random函數產生了一個頁面對齊的隨機數,然後與addr_space_size取模,在64位下,addr_space_size的取值可能非常大, 所以xnu嘗試最多1000次循環來找到一個合適的地址空間。使用這樣的算法,offset的可控性很差, 還有可能因爲隨機數的問題導致整個mmap動作失敗,我覺得後續xnu的內核工程師應該會改進這個算法。


Paper 本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1475/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章