/dev/null

脳みそのL1キャッシュ

NX bit てなんぞや

はじめに

訳あって NX bit について調べる必要があったので、その際に残したメモをまとめました。

NX bit とは

メモリ領域に付与する実行不可属性です。NX bit が立っているメモリ領域上でコードを実行することはできません。なお、NX(No eXecute) bit は AMD の用語で、Intel では XD (eXecute Disable) bit、ARM では XN(eXecute Never) bit、MIPS では XI(eXecute Inhibit) bit だったりします。今回は NX bit で統一します。

動作確認

Linux 上で NX bit の動作確認をします。以下のようなC言語のコードを用意します。シェルを立ち上げるシェルコードをスタック上において、それを実行するコードになります。

#include <stdio.h>

typedef void (*fn)();

int main(void) {
    char code[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05";
    ((fn)code)();
    return 0;
}

NX bit 有効時

これを NX bit disabled の状態でコンパイルして実行してみましょう。新しいシェルが立ち上がるはずです。なお、-z はリンカにキーワードを渡すオプションです。(ということは、NX bit 関連の設定をするのはリンカ、ということになりますね)

┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ gcc -z execstack -o nx-off nx.c
┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ checksec ./nx-off
[*] '/home/kali/Workspace/misc/nxbit/nx-off'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments
┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ ./nx-off
$ whoami
kali

NX bit 無効時

次に NX bit enabled の状態でコンパイルして実行してみましょう。今度はセグフォになりましたね。

┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ gcc -o nx-on nx.c
┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ ./nx-on
zsh: segmentation fault  ./nx-on

PT_GNU_STACK と p_flags

さて、さきほどから checksec というコマンドを使って NX bit enabled/disabled の判定をしていましたが、具体的に checksec は何を根拠に判定しているのでしょうか。

checksec のソースコードを覗いてみましょう。

    def nx(self):
        """:class:`bool`: Whether the current binary uses NX 
        ...
            case PT_GNU_STACK:
                if (elf_ppnt->p_flags & PF_X)
                    executable_stack = EXSTACK_ENABLE_X;
                else
                    executable_stack = EXSTACK_DISABLE_X;
                break;
        ...
        """
        if not self.executable:
            return True

        for seg in self.iter_segments_by_type('GNU_STACK'):
            return not bool(seg.header.p_flags & P_FLAGS.PF_X)

        # If you NULL out the PT_GNU_STACK section via ELF.disable_nx(),
        # everything is executable.
        return False

ELF 形式の実行ファイルのメタデータを使って判断していますね。

セグメントタイプが PT_GNU_STACK と設定されているプログラムヘッダが存在し、かつ、p_flags で PF_X フラグが立っている場合に NX bit enabled と判断しているようです。

PT_GNU_STACK と p_flags に関しては、下記で述べられている通り、スタックが存在するセグメントの属性を指定しているようです、PF_X の X は eXecute の X なので、このフラグが立っているとスタックは実行可能ということになるのでしょう。

PT_GNU_STACK The p_flags member specifies the permissions on the segment containing the stack and is used to indicate wether the stack should be executable. The absense of this header indicates that the stack will be executable.

引用元: https://refspecs.linuxbase.org/LSB_3.0.0/LSB-PDA/LSB-PDA/progheader.html

カーネル

このプログラムを実行する際に、プログラムのバイナリはカーネルによってメモリ上にロードされますが、その際に、PT_GNU_STACK はどう処理されるのか見てみましょう。訳あって、Linux 2.6.12 という古めのバージョンのコードを読んでいきます。

まずは、ELF 形式のプログラムをロードする処理です。

// https://elixir.bootlin.com/linux/v2.6.12/source/fs/binfmt_elf.c#L511

static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
...
    elf_ppnt = elf_phdata;
    for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
        if (elf_ppnt->p_type == PT_GNU_STACK) {
            if (elf_ppnt->p_flags & PF_X)
                executable_stack = EXSTACK_ENABLE_X;
...
    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                 executable_stack);

案の定 PT_GNU_STACK と p_flags でスタックが実行可能か判断しているっぽいですね。executable_stack 変数に値が設定されたあと、setup_arg_pages 関数に渡ります。

では、次に setup_arg_pages 関数を見てみましょう。スタック領域の設定をしているところです。

// https://elixir.bootlin.com/linux/v2.6.12/source/fs/exec.c#L349

int setup_arg_pages(struct linux_binprm *bprm,
            unsigned long stack_top,
            int executable_stack)
{
...
        /* Adjust stack execute permissions; explicitly enable
        * for EXSTACK_ENABLE_X, disable for EXSTACK_DISABLE_X
        * and leave alone (arch default) otherwise. */
        if (unlikely(executable_stack == EXSTACK_ENABLE_X))
            mpnt->vm_flags = VM_STACK_FLAGS |  VM_EXEC;
        else if (executable_stack == EXSTACK_DISABLE_X)
            mpnt->vm_flags = VM_STACK_FLAGS & ~VM_EXEC;
        else
            mpnt->vm_flags = VM_STACK_FLAGS;
        mpnt->vm_flags |= mm->def_flags;
        mpnt->vm_page_prot = protection_map[mpnt->vm_flags & 0x7];
...
    for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
        struct page *page = bprm->page[i];
        if (page) {
            bprm->page[i] = NULL;
            install_arg_page(mpnt, page, stack_base);
        }
        stack_base += PAGE_SIZE;
    }
    up_write(&mm->mmap_sem);
    
    return 0;
}

EXSTACK_ENABLE_X なら vm_flags に VM_STACK_FLAGS と VM_EXEC を設定し、それが protection_map 配列によって別の値に変換され、vm_page_prot に設定されているのがわかります。つまり、スタックの実行可否の情報は vm_page_prot に設定されたということですね。

この vm_page_prot には一体どんな情報が設定されるのでしょうか。protection_map 配列を見てみると

// https://elixir.bootlin.com/linux/v2.6.12/source/mm/mmap.c#L57

pgprot_t protection_map[16] = {
    __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
    __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
};

と定義されています。では、この配列の中身は何でしょうか。これらの値の定義は CPU ごとに異なります。今回は x86_64 の実装を見ます。

// https://elixir.bootlin.com/linux/v2.6.12/source/include/asm-x86_64/pgtable.h#L191

...

#define _PAGE_BIT_NX           63

...

#define _PAGE_NX        (1UL<<_PAGE_BIT_NX)

...

#define PAGE_NONE  __pgprot(_PAGE_PROTNONE | _PAGE_ACCESSED)
#define PAGE_SHARED    __pgprot(_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | _PAGE_ACCESSED | _PAGE_NX)
#define PAGE_SHARED_EXEC __pgprot(_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | _PAGE_ACCESSED)
#define PAGE_COPY_NOEXEC __pgprot(_PAGE_PRESENT | _PAGE_USER | _PAGE_ACCESSED | _PAGE_NX)
#define PAGE_COPY PAGE_COPY_NOEXEC
#define PAGE_COPY_EXEC __pgprot(_PAGE_PRESENT | _PAGE_USER | _PAGE_ACCESSED)
#define PAGE_READONLY  __pgprot(_PAGE_PRESENT | _PAGE_USER | _PAGE_ACCESSED | _PAGE_NX)
#define PAGE_READONLY_EXEC __pgprot(_PAGE_PRESENT | _PAGE_USER | _PAGE_ACCESSED)

...

#define __P000 PAGE_NONE
#define __P001 PAGE_READONLY
#define __P010 PAGE_COPY
#define __P011 PAGE_COPY
#define __P100 PAGE_READONLY_EXEC
#define __P101 PAGE_READONLY_EXEC
#define __P110 PAGE_COPY_EXEC
#define __P111 PAGE_COPY_EXEC

#define __S000 PAGE_NONE
#define __S001 PAGE_READONLY
#define __S010 PAGE_SHARED
#define __S011 PAGE_SHARED
#define __S100 PAGE_READONLY_EXEC
#define __S101 PAGE_READONLY_EXEC
#define __S110 PAGE_SHARED_EXEC
#define __S111 PAGE_SHARED_EXEC

NX の文字が見えましたね。_PAGE_NX の値が 1 << 63 と設定されていますが、これは一体なんでしょうか。Intel SDM を見てみましょう。

出典: https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf

(0 based indexing で) 63 bit 目に XD と書いてありますね。つまり、1 << 63 は NX bit を指してるわけです。

おわりに

gcc -z execstack が指定されるとどうなるのか、カーネルレベルまで追ってみました。頭ではたぶんこうなっているなと思っていましたが、実際にコードを追うと、自分の考えに自信を持てるようになりますね。