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

1 簡介

今天繼續分析下Freebsd進程的棧、堆、代碼段的地址隨機化實現。

1.1 不可思議的棧隨機化

可能讀者朋友會比較詫異,freebsd內核沒有提供進程棧的地址隨機化功能。 進程棧的地址是execve加載磁盤上的二進制文件時初始化的:

kern/kern_exec.c:
kern_execve()->do_execve()->exec_copyout_strings()
register_t *
exec_copyout_strings(struct image_params *imgp)
{
        arginfo = (struct ps_strings *)p->p_sysent->sv_psstrings;[1]
        destp = (uintptr_t)arginfo;[2]
}

exec_copyout_strings用來拷貝當前進程的二進制信息,這些信息會被動態連接器使用。

[1] 處p->p_sysent->sv_psstrings保存的是當前進程的二進制信息,它位于棧基地址的附近。

看下init進程的sysentvec信息:

struct sysentvec null_sysvec = {
        .sv_usrstack    = USRSTACK,
        .sv_psstrings   = PS_STRINGS,
}
#define PS_STRINGS      (USRSTACK - sizeof(struct ps_strings))

sv_psstrings緊挨著棧的開始地址。

amd64/include/vmparam.h

#define VM_MAXUSER_ADDRESS      UVADDR(NUPML4E, 0, 0, 0)
#define SHAREDPAGE              (VM_MAXUSER_ADDRESS - PAGE_SIZE)
#define USRSTACK                SHAREDPAGE

以amd64架構為例,USRSTACK為進程空間最大的用戶態地址減去一個PAGE_SIZE。 通過代碼路徑的溯源,可以看到freebsd的內核并沒有對棧的地址有隨機化的動作!

1.2 BRK地址隨機化

Libc的brk函數用來控制進程的heap大小,但是從內核源碼來看, freebsd并沒有提供brk的系統調用。

vm/vm_mmap.c:
int
sys_sbrk(struct thread *td, struct sbrk_args *uap)
{
        /* Not yet implemented */
        return (EOPNOTSUPP);
}

事實上,freebsd的進程空間結構與linux的有所不同, 內核對進程空間的管理并沒有明確的brk和mmap概念,linux的mm_struct結構體會有brk和mmap的開始地址標記。

include/linux/mm_types.h:

struct mm_struct {
                unsigned long mmap_base;// mmap區域的基地址
                unsigned long start_brk, brk, start_stack;// brk區域的及地址
}

Linux對brk和mmap區域都有明顯的界限劃分,并且都提供了它們的地址隨機化能力。

在來看下freebsd的定義:

vm/vm_map.h:
struct vmspace {
        caddr_t vm_taddr; // 代碼段基地址     
        caddr_t vm_daddr;// 數據段基地址       
}

Freebsd的進程空間區域只包含代碼段和數據段, 動態生成的heap區域就在data段的后面。雖然libc庫有包裝了brk,但是內核沒有提供此架構與功能。用戶態的內存分配器比如jemalloc,它會優先選擇使用mmap來分配內存。

1.3 mmap地址隨機化

接下來繼續分析mmap的地址隨機化實現, 我們以mmap建立一個匿名映射的路徑來分析:

vm/vm_mmap.c:
sys_mmap()->kern_mmap()->vm_mmap_object():
int
vm_mmap_object(vm_map_t map, vm_offset_t *addr, vm_size_t size, vm_prot_t prot,
    vm_prot_t maxprot, int flags, vm_object_t object, vm_ooffset_t foff,
    boolean_t writecounted, struct thread *td)
{
                if (curmap) {
                        rv = vm_map_find_min(map, object, foff, addr, size,[1]
                            round_page((vm_offset_t)td->td_proc->p_vmspace->
                            vm_daddr + lim_max(td, RLIMIT_DATA)), max_addr,[2]
                            findspace, prot, maxprot, docow);
}

[1] 處用來尋找進程空間是否存在一個合適的地址范圍,注意看[2]處的參數為地址范圍的最小值,它被設置為vm_daddr + lim_max(td, RLIMIT_DATA)), 也就是數據段的最后地址,所以說freebsd的mmap是從緊挨著數據段后面開始的。

vm_map_find_min()->vm_map_find():

int
vm_map_find(vm_map_t map, vm_object_t object, vm_ooffset_t offset,
            vm_offset_t *addr,  /* IN/OUT */
            vm_size_t length, vm_offset_t max_addr, int find_space,
            vm_prot_t prot, vm_prot_t max, int cow)
{
                if (try == 1 && en_aslr && !cluster) {
                        pidx = MAXPAGESIZES > 1 && pagesizes[1] != 0 &&[1]
                            (find_space == VMFS_SUPER_SPACE || find_space ==
                            VMFS_OPTIMAL_SPACE) ? 1 : 0;
                        gap = vm_map_max(map) > MAP_32BIT_MAX_ADDR &&[2]
                            (max_addr == 0 || max_addr > MAP_32BIT_MAX_ADDR) ?
                            aslr_pages_rnd_64[pidx] : aslr_pages_rnd_32[pidx];
                        if (vm_map_findspace(map, curr_min_addr, length +
                            gap * pagesizes[pidx], addr) ||
                            (max_addr != 0 && *addr + length > max_addr))
                                goto again;

                        /* And randomize the start address. */
                        *addr += (arc4random() % gap) * pagesizes[pidx];[3]
}

[1]處Pidx用來選擇page size的小大, [2]處gap用來選擇隨機化的page數目。[3]處通過arc4random()生成一個隨機數, 并計算最后的隨機地址。以amd64架構為例:

static const int aslr_pages_rnd_64[2] = {0x1000, 0x10};
u_long pagesizes[MAXPAGESIZES] = { PAGE_SIZE };

可以看到mmap的最大地址隨機范圍不超過:0x1000*4096=0x1000000, 也就10M的地址范圍,隨機范圍并不大。

1.4 代碼段地址隨機化

代碼段指的是text代碼段,包含共享庫,它們的基地址,freebsd都做了隨機化的能力。

kern/imgact_elf.c:

static int
__CONCAT(exec_, __elfN(imgact))(struct image_params *imgp)
{
        if (hdr->e_type == ET_DYN) {
                if (baddr == 0) {
                        if ((sv->sv_flags & SV_ASLR) == 0 ||
                            (fctl0 & NT_FREEBSD_FCTL_ASLR_DISABLE) != 0)
                                et_dyn_addr = ET_DYN_LOAD_ADDR;
                        else if ((__elfN(pie_aslr_enabled) &&
                            (imgp->proc->p_flag2 & P2_ASLR_DISABLE) == 0) ||
                            (imgp->proc->p_flag2 & P2_ASLR_ENABLE) != 0)
                                et_dyn_addr = ET_DYN_ADDR_RAND;
                        else
                                et_dyn_addr = ET_DYN_LOAD_ADDR;
                }
}

對于使用pie編譯為地址無關代碼的程序,或者共享庫文件,elf文件頭都設置為ET_DYN, elf的第一個load段內存地址都設置為0。如果沒有開啟地址隨機化,那么et_dyn_addr被設置為0x01021000(64位)。如果設置隨機化,則執行如下路徑:

maxv = vm_map_max(map) - lim_max(td, RLIMIT_STACK);

if (et_dyn_addr == ET_DYN_ADDR_RAND) {
                et_dyn_addr = __CONCAT(rnd_, __elfN(base))(map,
                    vm_map_min(map) + mapsz + lim_max(td, RLIMIT_DATA),
                    /* reserve half of the address space to interpreter */
                    maxv / 2, 1UL << flsl(maxalign));
        }

rnd函數從某個地址范圍隨機選取一個滿足條件的地址區域。它的最小地址設置為數據段的開始地址,最大地址為棧的下限地址。

static u_long
__CONCAT(rnd_, __elfN(base))(vm_map_t map __unused, u_long minv, u_long maxv,
    u_int align)
{
        u_long rbase, res;
        arc4rand(&rbase, sizeof(rbase), 0);
        res = roundup(minv, (u_long)align) + rbase % (maxv - minv);
        res &= ~((u_long)align - 1);
        if (res >= maxv)
                res -= align;
}

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