作者:wzt
原文鏈接:https://mp.weixin.qq.com/s/qGQ-_uDD3Umn-7bbRGf7pA

1 地址隨機化與PIE

1.1 pie簡介

gcc 的pie選項可以生成對符號的引用變為與位置無關的代碼。之前對符號的絕對地址引用變為相對于PC指令或相對于二進制某固定位置的偏移引用。當內核被隨機的加載到任意內存地址時,可以簡化對符號重定位的處理。

1.2 pie驗證

我們通過反匯編vmlinux來驗證經過pie編譯后產生的一些代碼是否可以做到位置無關。

1.2.1 塊間全局變量引用 bss段引用

1    104  /root/kernel/linux-4.5/kernel/fork.c <<total_forks>>
unsigned long total_forks;
static int show_stat(struct seq_file *p, void *v)
fs/proc/stat.c

ffffffff81291960 <show_stat>:
ffffffff81291f5a:       4c 8b 05 87 3b d4 00    mov    0xd43b87(%rip),%r8        # ffffffff81fd5ae8 <total_forks>
[root@localhost build-4.5]# readelf -r vmlinux|grep ffffffff81291f5d
ffffffff81291f5d  efc100000002 R_X86_64_PC32     ffffffff81fd5ae8 total_forks - 4

R_X86_64_PC32 &&非percpu變量, 不需要重定位。

1.2.2 模塊內全局變量引用 bss段引用

int max_threads;                /* tunable limit on nr_threads */

ffffffff81087ce0 <set_max_threads>:
ffffffff81087d43:       89 3d 97 dd f4 00       mov    %edi,0xf4dd97(%rip)        # ffffffff81fd5ae0 <max_threads>
  [56] .bss              NOBITS           ffffffff81f2e000  0132e000
       000000000031b000  0000000000000000  WA       0     0     4096
76199: ffffffff81fd5ae0     4 OBJECT  GLOBAL DEFAULT   56 max_threads
[root@localhost build-4.5]# readelf -r vmlinux|grep ffffffff81087d45
ffffffff81087d45  129a700000002 R_X86_64_PC32     ffffffff81fd5ae0 max_threads - 4

R_X86_64_PC32 &&非percpu變量, 不需要重定位。

1.2.3 模塊間函數調用 text段引用

ffffffff81088890 <free_task>:
ffffffff810888c5:       e8 56 87 0c 00          callq  ffffffff81151020 <ftrace_graph_exit_task>
[root@localhost build-4.5]# readelf -r vmlinux|grep ffffffff810888c6
Offset          Info           Type           Sym. Value    Sym. Name + Addend
ffffffff810888c6  11e8900000002 R_X86_64_PC32     ffffffff81151020 ftrace_graph_exit_task - 4

R_X86_64_PC32 &&非percpu變量,不需要重定位。

1.2.4 模塊內函數調用 text段引用

ffffffff810888f0 <__put_task_struct>:
ffffffff8108898a:       e8 01 ff ff ff          callq  ffffffff81088890 <free_task>
[root@localhost build-4.5]# readelf -r vmlinux|grep ffffffff8108898b
ffffffff8108898b  15fc700000002 R_X86_64_PC32     ffffffff81088890 free_task - 4

R_X86_64_PC32 &&非percpu變量, 不需要重定位。

1.2.5 Percpu 變量引用 data段引用

static DEFINE_PER_CPU(struct task_struct *, idle_threads);

struct task_struct *idle_thread_get(unsigned int cpu)
{
        struct task_struct *tsk = per_cpu(idle_threads, cpu);

        if (!tsk)
                return ERR_PTR(-ENOMEM);

        init_idle(tsk, cpu);
        return tsk;
}
ffffffff810ada80 <idle_thread_get>:
ffffffff810ada80:       e8 bb b2 63 00          callq  ffffffff816e8d40 <__fentry__>
ffffffff810ada85:       89 fa                   mov    %edi,%edx
ffffffff810ada87:       55                      push   %rbp
ffffffff810ada88:       48 c7 c0 48 de 00 00    mov    $0xde48,%rax
ffffffff810ada8f:       48 8b 14 d5 40 52 d4    mov    -0x7e2badc0(,%rdx,8),%rdx
ffffffff810ada96:       81
ffffffff810ada97:       48 89 e5                mov    %rsp,%rbp
ffffffff810ada9a:       53                      push   %rbx
ffffffff810ada9b:       48 8b 1c 10             mov    (%rax,%rdx,1),%rbx
[root@localhost build-4.5]# readelf -r vmlinux|grep ffffffff810ada93
ffffffff810ada93  122e20000000b R_X86_64_32S      ffffffff81d45240 __per_cpu_offset + 0

R_X86_64_32S && 非percpu變量,需要重定位。

[root@localhost build-4.5]# readelf -s  vmlinux|grep   __per_cpu_offset
74466: ffffffff81d45240 65536 OBJECT  GLOBAL DEFAULT   30 __per_cpu_offset
  [30] .data             PROGBITS         ffffffff81c00000  00e00000
       0000000000165d80  0000000000000000  WA       0     0     4096
  [33] .data..percpu     PROGBITS         0000000000000000  01000000
       0000000000018098  0000000000000000  WA       0     0     4096
[root@localhost build-4.5]# readelf -s vmlinux|grep percpu|grep 33
9758: ffffffff810e63b0   337 FUNC    LOCAL  DEFAULT    1 __free_percpu_irq
11209: 000000000000f1c0   728 OBJECT  LOCAL  DEFAULT   33 tick_percpu_dev

1.2.6 引用rodata變量

[root@localhost build-4.5]# readelf -S vmlinux|grep text

[ 1] .text             PROGBITS         ffffffff81000000  00200000
[root@localhost build-4.5]# readelf -S vmlinux|grep rodata
  [ 7] .rodata           PROGBITS         ffffffff81800000  00a00000
ffffffff81d80c98 <start_kernel>:
ffffffff81d80d76:       48 c7 c6 e0 00 80 81    mov    $0xffffffff818000e0,%rsi

[root@localhost build-4.5]# readelf -r vmlinux|grep ffffffff81d80d79

ffffffff81d80d79  10f420000000b R_X86_64_32S      ffffffff818000e0 linux_banner + 0

R_X86_64_32S && 非percpu變量,需要重定位。

rodata:FFFFFFFF818000E0                 public linux_banner
.rodata:FFFFFFFF818000E0 linux_banner    db 'Linux version 4.5.0 (root@localhost.localdomain) (gcc version 4.8'
.rodata:FFFFFFFF818000E0                                         ; DATA XREF: start_kernel+DE↓o
.rodata:FFFFFFFF818000E0                 db '.5 20150623 (Red Hat 4.8.5-28) (GCC) ) #1 SMP Tue Aug 28 12:48:38'
.rodata:FFFFFFFF818000E0                 db ' CST 2018',0Ah,0

2. Linux kaslr 實現原理

2.1 vmlinux 是如何生成的

由于內核是要把內核符號表一同鏈接進vmlinux里, 因此需要分三步進行鏈接: Scripts/ link-vmlinux.sh

第一步:

       ld               nm                   
   *.o---->.tmp_vmlinux1---->.tmp_kallsyms1.o

第二步:

                              ld                nm
.tmp_vmlinux1 + .tmp_kallsyms1.o ---->.tmp_vmlinux2-----à.tmp_kallsyms2.o

第三步:

                                  ld
    .tmp_vmlinux2 + .tmp_kallsyms2.o ---->vmlinux

由各種.o文件鏈接成臨時內核 .tmp_vmlinux1,然后利用nm提取出內核符號導入到 .tmp_kallsyms1.o 文件,在把 .tmp_vmlinux1.tmp_kallsyms1.o 一起鏈接為臨時內核 .tmp_vmlinux2,此時新增了一個kallsyms section,里面保存的就是nm導出的內核符號值, 注意對于kallsyms section自身產生的符號并沒有提取出來,需要在重復上一步的鏈接處理,此時得到的 tmp_kallsyms2.o 已經包含了完整的符號,將其與 .tmp_vmlinux2 鏈接,產生最終的vmlinux。如果內核配置文件開啟了 KALLSYMS_EXTRA_PASS,為了避免產生align對齊的一些bug,還需在重復上述步驟一次,產生 .tmp_vmlinux3

2.2 vmlinuz 是如何產生的

由于vmlinux即使沒有采用pie,所產生的二進制文件仍然很大,所以采用了將vmlinux進行壓縮的方案,在bootloader加載內核時,在對其進行解壓,有點類似于加殼程序的執行流程。 Linux在鏈接階段會產生如下文件: vmlinux.bin 經過strip后的二進制,去掉了debug和comment信息。 vmlinux.bin.allvmlinux.binvmlinux.relocs組成,vmlinux.relocs保存的是需要重定位的地址數組。 vmlinux.bin.(gz|bz2|lzma|...)vmlinux.bin.all + u32 size, size是一個四字節的數值,保存的是vmlinux.bin.all的文件大小,最后由gzip等壓縮工具壓縮。 舉例vmlinux.bin.gz是由gzip壓縮后的二進制文件。 而vmlinuz則由以下幾個部分組成:

  |--------------------piggy.s---------------|
----------------------------------------------------------
| uncompress code| asm globals |         vmlinux.bin.gz        |
----------------------------------------------------------
| vmlinux.bin | vmlinux.relocs | size |
------------------------------

2.3 vmlinux.relocs 是什么

Linux沒有選擇在bootloader階段對內核進行復雜的重定位工作, 由于內核是pie編譯產生的,我們從最前面的反匯編信息來看,大部分符號的重定位工作只需加上內核被隨機化產生的偏移值即可完成重定位,而不需要解析x86定義的各種重定位類型。因此只需要在重定位時提供給bootloader需要被重定位的地址即可,這些地址保存在vmlinux.relocs里。 在vmlinux生成后,通過arch/x86/tools/relocs來提取vmlinux rela保存的信息,它的結構如下:

--------------------------------------------------------------------------
   | 0 | 64bit relocation address …| 0 | 32 bit inverse relocation …| 0 | 32 bit relocation …|
--------------------------------------------------------------------------

在解析vmlinux rela重定位表的時候需要做過濾,當需要被重定位的符號是絕對地址時,如果不在白名單內就要報錯,提醒內核開發者需要將絕對地址的引用代碼進行修改。

static const char * const sym_regex_kernel[S_NSYMTYPES] = {
        [S_ABS] =
        "^(xen_irq_disable_direct_reloc$|"
        "xen_save_fl_direct_reloc$|"
        "VDSO|"
        "__crc_)",

白名單中的這些值都是經過內核開發者人工review過的,確認即使內核加載在不同的地址,這些符號地址仍然是不變的, 因此連接器生成的重定位表中包含這些符號時,可過濾掉,不進行重定位處理。 還有一些符號雖然被連接器標記為絕對地址,但是內核開發者人工review過也是相對地址引用的, 所以這些符號是需要被重定位的。

 [S_REL] =
        "^(__init_(begin|end)|"
        "__x86_cpu_dev_(start|end)|"
        "(__parainstructions|__alt_instructions)(|_end)|"
        "(__iommu_table|__apicdrivers|__smp_locks)(|_end)|"
        "__(start|end)_pci_.*|"
        "__(start|end)_builtin_fw|"
        "__(start|stop)___ksymtab(|_gpl|_unused|_unused_gpl|_gpl_future)|"
        "__(start|stop)___kcrctab(|_gpl|_unused|_unused_gpl|_gpl_future)|"
        "__(start|stop)___param|"
        "__(start|stop)___modver|"
        "__(start|stop)___bug_table|"
        "__tracedata_(start|end)|"
        "__(start|stop)_notes|"
        "__end_rodata|"
        "__initramfs_start|"
        "(jiffies|jiffies_64)|"
#if ELF_BITS == 64
        "__per_cpu_load|"
        "init_per_cpu__.*|"
        "__end_rodata_hpage_align|"
#endif
        "__vvar_page|"
        "_end)$"

還有一個需要特別處理的是內核.data..percpu這個section,當在x86_64 SMP下,連接器給這個section生成的虛擬地址是0,因此在解析重定位表時,如果碰到對.data..percpu的引用,需要首先修正引用的值,.data..percpu 可以通過定義在text段的__per_cpu_load變量進行修正,它的符號值在鏈接時是確定的。

static void percpu_init(void)
{
        int i;
        for (i = 0; i < ehdr.e_shnum; i++) {
                ElfW(Sym) *sym;
                if (strcmp(sec_name(i), ".data..percpu"))
                        continue;

                if (secs[i].shdr.sh_addr != 0)  /* non SMP kernel */
                        return;

                sym = sym_lookup("__per_cpu_load");
                if (!sym)
                        die("can't find __per_cpu_load\n");

                per_cpu_shndx = i;
                per_cpu_load_addr = sym->st_value;
                return;
        }
}

static int do_reloc64(struct section *sec, Elf_Rel *rel, ElfW(Sym) *sym,
                      const char *symname)
{
        if (sec->shdr.sh_info == per_cpu_shndx)
                offset += per_cpu_load_addr;
}

2.4 bootloader 的重定位流程

2.4.1 隨機偏移值的選取

內核被加載的物理地址起始值為PHYSICAL_START 0x1000000,隨機化的意思是基于這個起始地址在向后偏移一段隨機地址。而這個隨機值不能為任意值,因為: X86處理器內存分頁機制有幾種模式,每個模式定義的物理內存頁的大小也不一樣。

如果一段內存由于沒有基于物理頁對齊的話,它會產生于兩個物理頁之間,而這兩個物理頁可能具有不同的權限,不如一個只讀,一個可寫,這樣原本只想可讀的那段內存就有一部分具有了可寫的權限。Linux為了保持兼容性,64位下選擇了2mb對齊,32位選擇了8k對齊。

2.4.2 重定位處理邏輯

Bootloader在選取合適的偏移值后,會將內核二進制中的text和data段拷貝到PHYSICAL_START+offset的物理地址上,然后執行重定位處理,前面講過vmlinuz中保存著vmlinux.relocs文件,它里面包含的就是需要重定位的地址,因此bootloader從個文件中提取出要重定位的地址。

arch/x86/boot/compressed/misc.c

static void handle_relocations(void *output, unsigned long output_len)
{
        int *reloc;
        unsigned long delta, map, ptr;
        unsigned long min_addr = (unsigned long)output;
        unsigned long max_addr = min_addr + output_len;

        delta = min_addr - LOAD_PHYSICAL_ADDR;
        if (!delta) {
                debug_putstr("No relocation needed... ");
                return;
        }
        debug_putstr("Performing relocations... ");
        map = delta - __START_KERNEL_map;
        for (reloc = output + output_len - sizeof(*reloc); *reloc; reloc--) {
                int extended = *reloc;
                extended += map;

                ptr = (unsigned long)extended;
                if (ptr < min_addr || ptr > max_addr)
                        error("32-bit relocation outside of kernel!\n");

                *(uint32_t *)ptr += delta;
        }
#ifdef CONFIG_X86_64
        while (*--reloc) {
                long extended = *reloc;
                extended += map;

                ptr = (unsigned long)extended;
                if (ptr < min_addr || ptr > max_addr)
                        error("inverse 32-bit relocation outside of kernel!\n");

                *(int32_t *)ptr -= delta;
        }
        for (reloc--; *reloc; reloc--) {
                long extended = *reloc;
                extended += map;

                ptr = (unsigned long)extended;
                if (ptr < min_addr || ptr > max_addr)
                        error("64-bit relocation outside of kernel!\n");

                *(uint64_t *)ptr += delta;
        }

前面提到vmlinux.relocs的文件結構有三段,所以上面有三個循環來分別解析處理,我們已64位reloc信息的處理為例:

for (reloc--; *reloc; reloc--) {
                long extended = *reloc;
                extended += map;

                ptr = (unsigned long)extended;
                *(uint64_t *)ptr += delta;
        }

筆者認為這段代碼寫的比較隱晦難懂, reloc保存的是vmlinux鏈接后的虛擬地址,本來內核是鏈接在PHYSICAL_START這個物理地址,虛擬地址和物理地址的映射關系是: 物理地址 = 虛擬地址 - START_KERNEL_map START_KERNEL_map是內核起始的虛擬地址,在重定位階段,內核還沒啟用分頁機制,所以對地址的引用都是物理地址,而reloc保存的是鏈接后的虛擬地址,因此要利用上面的公式進行轉換,同時也要把隨機偏移值加上。 ptr為最終要修正的物理地址:reloc - START_KERNEL_map + delta, 這個物理地址保存的值為**reloc+delta, delta為隨機偏移值。


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