【问题标题】:Use of gs register on a 32 bit program over a 64 bit linux在 64 位 Linux 上的 32 位程序上使用 gs 寄存器
【发布时间】:2022-01-15 05:01:36
【问题描述】:

在 64 位程序中,用于获取堆栈保护器的选择器:偏移量是 fs:0x28,其中 fs=0。这没有问题,因为在 64 位中我们有 MSR fs_base(设置为指向 TLS)并且 GDT 被完全忽略。

但是对于 32 位程序,堆栈保护器是从 gs:0x14 读取的。在 64 位系统上运行我们有 gs=0x63,在 32 位系统上 gs=0x33。这里没有 MSR,因为它们是在 x86_64 中引入的,所以 GDT 在这里起着重要的作用。

对这两种情况的值进行剖析,我们得到 RPL=3(这是预期的),描述符表选择器指示 GDT(Linux 中不使用 LDT),选择器指向索引为 12 的 64 位和索引条目6 代表 32 位。

使用内核模块,我能够检查 64 位 linux 中的该条目是否为 NULL!所以不明白TLS的地址是怎么解析的。

内核模块的相关部分如下:

void gdtread()
{
    struct desc_ptr gdtr;
    seg_descriptor* gdt_entry = NULL;
    uint16_t tr;
    int i;

    asm("str %0" : "=m"(tr));

    native_store_gdt(&gdtr); // equiv. to asm("sgdt %0" : "=m"(gdtr));
    printk("GDT address: 0x%px, GDT size: %d bytes = %i entries\n",
           (void*)gdtr.address, gdtr.size + 1, (gdtr.size + 1) / 8);

    gdt_entry = (seg_descriptor*)gdtr.address;
    for(i = 0; i < (gdtr.size + 1) / 8; i++)
    {
        if(tr >> 3 == i)
            printk("Entry #%i:\t<--- TSS (RPL = %i)", i, tr & 3);
        else
            printk("Entry #%i:", i);

        if(!((uint64_t*)gdt_entry)[i])
        {
            printk("\tNULL");
            continue;
        }
        
        if(gdt_entry[i].s)
            user_segment_desc(&gdt_entry[i]);
        else
            system_segment_desc((sys_seg_descriptor*)&gdt_entry[i++]);
    }
}

在 64 位系统上输出以下内容:

[ 3817.191065] GDT address: 0xfffffe0000001000, GDT size: 128 bytes = 16 entries
[ 3817.191073] Entry #0:
[ 3817.191075]  NULL
[ 3817.191078] Entry #1:
[ 3817.191081]  Raw: 0x00cf9b000000ffff
[ 3817.191084]  Base: 0x00000000
[ 3817.191088]  Limit: 0xfffff
[ 3817.191091]  Flags: 0xc09b
[ 3817.191096]      Type = 0xb (Code, non conforming, readable, accessed)
[ 3817.191100]      S    = 0 (user)
[ 3817.191103]      DPL  = 0
[ 3817.191105]      P    = 1 (present)
[ 3817.191109]      AVL  = 0
[ 3817.191112]      L    = 0 (legacy mode)
[ 3817.191115]      D/B  = 1
[ 3817.191118]      G    = 1 (KiB)
[ 3817.191121] Entry #2:
[ 3817.191124]  Raw: 0x00af9b000000ffff
[ 3817.191127]  Base: 0x00000000
[ 3817.191130]  Limit: 0xfffff
[ 3817.191133]  Flags: 0xa09b
[ 3817.191137]      Type = 0xb (Code, non conforming, readable, accessed)
[ 3817.191141]      S    = 0 (user)
[ 3817.191144]      DPL  = 0
[ 3817.191146]      P    = 1 (present)
[ 3817.191149]      AVL  = 0
[ 3817.191152]      L    = 1 (long mode)
[ 3817.191155]      D/B  = 0
[ 3817.191157]      G    = 1 (KiB)
[ 3817.191160] Entry #3:
[ 3817.191163]  Raw: 0x00cf93000000ffff
[ 3817.191166]  Base: 0x00000000
[ 3817.191169]  Limit: 0xfffff
[ 3817.191171]  Flags: 0xc093
[ 3817.191175]      Type = 0x3 (Data, expand down, writable, accessed)
[ 3817.191178]      S    = 0 (user)
[ 3817.191181]      DPL  = 0
[ 3817.191183]      P    = 1 (present)
[ 3817.191186]      AVL  = 0
[ 3817.191189]      L    = 0
[ 3817.191191]      D/B  = 1
[ 3817.191194]      G    = 1 (KiB)
[ 3817.191197] Entry #4:
[ 3817.191199]  Raw: 0x00cffb000000ffff
[ 3817.191202]  Base: 0x00000000
[ 3817.191205]  Limit: 0xfffff
[ 3817.191207]  Flags: 0xc0fb
[ 3817.191211]      Type = 0xb (Code, non conforming, readable, accessed)
[ 3817.191214]      S    = 0 (user)
[ 3817.191217]      DPL  = 3
[ 3817.191219]      P    = 1 (present)
[ 3817.191222]      AVL  = 0
[ 3817.191224]      L    = 0 (legacy mode)
[ 3817.191227]      D/B  = 1
[ 3817.191230]      G    = 1 (KiB)
[ 3817.191233] Entry #5:
[ 3817.191235]  Raw: 0x00cff3000000ffff
[ 3817.191238]  Base: 0x00000000
[ 3817.191241]  Limit: 0xfffff
[ 3817.191243]  Flags: 0xc0f3
[ 3817.191246]      Type = 0x3 (Data, expand down, writable, accessed)
[ 3817.191250]      S    = 0 (user)
[ 3817.191252]      DPL  = 3
[ 3817.191255]      P    = 1 (present)
[ 3817.191258]      AVL  = 0
[ 3817.191260]      L    = 0
[ 3817.191262]      D/B  = 1
[ 3817.191265]      G    = 1 (KiB)
[ 3817.191268] Entry #6:
[ 3817.191270]  Raw: 0x00affb000000ffff
[ 3817.191273]  Base: 0x00000000
[ 3817.191276]  Limit: 0xfffff
[ 3817.191278]  Flags: 0xa0fb
[ 3817.191281]      Type = 0xb (Code, non conforming, readable, accessed)
[ 3817.191284]      S    = 0 (user)
[ 3817.191287]      DPL  = 3
[ 3817.191289]      P    = 1 (present)
[ 3817.191292]      AVL  = 0
[ 3817.191295]      L    = 1 (long mode)
[ 3817.191298]      D/B  = 0
[ 3817.191300]      G    = 1 (KiB)
[ 3817.191303] Entry #7:
[ 3817.191306]  NULL
[ 3817.191308] Entry #8:    <--- TSS (RPL = 0)
[ 3817.191312]  Raw: 0x00000000fffffe0000008b0030004087
[ 3817.191316]  Base: 0xfffffe0000003000
[ 3817.191321]  Limit: 0x04087
[ 3817.191324]  Flags: 0x008b
[ 3817.191327]      Type = 0xb (Busy 64-bit TSS)
[ 3817.191331]      S    = 1 (system)
[ 3817.191333]      DPL  = 0
[ 3817.191336]      P    = 1 (present)
[ 3817.191339]      AVL  = 0
[ 3817.191341]      L    = 0
[ 3817.191344]      D/B  = 0
[ 3817.191347]      G    = 0 (B)
[ 3817.191349] Entry #10:
[ 3817.191352]  NULL
[ 3817.191355] Entry #11:
[ 3817.191358]  NULL
[ 3817.191360] Entry #12:
[ 3817.191362]  NULL
[ 3817.191365] Entry #13:
[ 3817.191367]  NULL
[ 3817.191369] Entry #14:
[ 3817.191372]  NULL
[ 3817.191374] Entry #15:
[ 3817.191377]  Raw: 0x0040f50000000000
[ 3817.191380]  Base: 0x00000000
[ 3817.191382]  Limit: 0x00000
[ 3817.191385]  Flags: 0x40f5
[ 3817.191389]      Type = 0x5 (Data, expand up, read only, accessed)
[ 3817.191392]      S    = 0 (user)
[ 3817.191395]      DPL  = 3
[ 3817.191397]      P    = 1 (present)
[ 3817.191400]      AVL  = 0
[ 3817.191403]      L    = 0
[ 3817.191405]      D/B  = 1
[ 3817.191408]      G    = 0 (B)

我还没有在 32 位系统上尝试过这个模块,但我正在路上。

那么,把问题说清楚:gs 段选择器如何在 64 位 linux 内核上运行的 32 位程序中工作?

【问题讨论】:

  • 我很确定 64 位内核可以在 64 位模式下使用 MSR(或 wrgsbase),然后再返回用户空间(或第一次进入)。所以你只需要在 32 位内核中处理 GDT。
  • @PeterCordes,当我在调试 32 位程序时在 gdb 中尝试 i r $gs_base 时,我得到“无效寄存器”,所以我认为它们在传统模式下无法访问。但是您的评论让我在文档中进行搜索,特别是“AMD64 Architecture Programmer's Manual, vol. 2”,并在第 27 页中说 Compatibility mode 在计算时忽略 FS 和 GS 段描述符中基地址的高 32 位一个有效地址。 这意味着实际上 GS 和 FS 的基地址寄存器也用于传统模式。
  • 在 64 位模式下,不使用段选择器。相反,一个特殊的 MSR 用于确定 FS 和 GS 的段基础。
  • @PeterCordes,确实如此!我修改内核模块以读取 32 位程序的 gs_base,是的,它指向 TLS,其内容为 0x00000000f7f84040
  • 您是否看到 GDB 使用 ptrace(PTRACE_POKETEXT)rdgsbase 插入目标进程并单步执行?这与在来宾线程的上下文中进行系统调用一样不可能。 (虽然strace -f 无法证明这一点;一个过程一次只能被一件事追踪)。内核更有可能通过ptrace 使基于段的寄存器可用,作为GDB 可以通过ptrace(PTRACE_PEEKUSER) 或它实际使用的任何东西读取的线程架构状态的一部分。如果 32 位 ABI 不包含这些结构字段,可能仅适用于 64 位进程?

标签: linux assembly x86 memory-segmentation gdt


【解决方案1】:

在@PeterCordes 的评论之后,我在“AMD64 Architecture Programmer's Manual, vol. 2”中进行了搜索,在第 27 页中说:

在计算有效地址时,兼容模式会忽略 FS 和 GS 段描述符中基地址的高 32 位。

这意味着管理 32 位进程的 64 位内核使用 MSR_*S_BASE 寄存器,就像它用于 64 位进程一样。内核可以在 64 位长模式下正常设置段基数,因此这些 MSR 是否在 32 位 compatibility sub-mode of long mode 或纯 32 位保护模式(传统模式,32位内核)。 64 位 Linux 内核仅对 ring 3(用户空间)使用兼容模式,其中 wrmsrrdmsr 由于特权不可用。与往常一样,基于段的设置在权限级别的更改中保持不变,例如使用sysretiret 返回用户空间。

另一件让我觉得这个寄存器不用于兼容模式进程的事情是 GDB。这是在调试 32 位程序时尝试打印此寄存器时发生的情况。:

(gdb) i r $gs_base
Invalid register `gs_base'

调试一个 64 位程序它工作正常。

(gdb) i r $fs_base
fs_base        0x7ffff7d00c00      0x7ffff7d00c00

由于指令rdgsbase 是 64 位指令(尝试在 32 位程序中执行该操作码会产生 SIGILL 信号),因此在 32 位内获取此寄存器的值有点棘手程序。

我想到的第一个解决方案是从内核模块中读取它:

unsigned long gs_base = 0xdeadbeefc0ffee13;
asm("swapgs;"
    "rdgsbase %0;"    // maybe unsafe if an interrupt happens here
                      // be careful if using this for anything more than toy experiments.
    "swapgs;"
    : "=r"(gs_base));
printk("gs_base: 0x%016lx", gs_base);

所以我在/dev 中为设备创建了一个驱动程序,所以当一个程序open()s 执行上面的代码时。编译并运行打开此文件的 32 位程序后,我得到了这个

[10793.682033] gs_base: 0x00000000f7f9f040

并且使用 gdb 检查 0xf7f9f040+0x14 我看到了金丝雀,这意味着它是 TLS。

(gdb) x/wx 0xf7f9f040+0x14
0xf7f9f054: 0x21f03c00
(gdb) x/wx $ebp-0xc
0xbffff60c: 0x21f03c00

我能想到的另一种方法是执行远调用以从 32 位更改为 64 位,执行 rdgsbase 然后返回到 64 位。可能这是一个更好的解决方案,因为它不需要内核模块。 (只要您可以假设您在支持 FSGSBASE 扩展的 CPU 上运行,并且有足够新的内核来启用它。)

类似这样的:

#include <stdio.h>

__attribute__((naked))   // or define the function in an asm statement at global scope
extern void rdgsbase()
{
    asm("rdgsbase %eax; retf");
}

int main()
{
    unsigned int* gs_base = NULL;
    unsigned int canary;
    // would be unsafe in a leaf function: clobbers the red zone
    asm("lcall $0x33, $rdgsbase; mov %%eax, %0" : "=m"(gs_base) : : "eax");
    asm("mov %%gs:0x14, %%eax ; mov %%eax, %0" : "=m"(canary) : : "eax");
    printf("gs_base = %p\n", gs_base);
    printf("canary: 0x%08x\n", canary);
    printf("canary: 0x%08x\n", gs_base[5]);
}

我知道它非常肮脏和丑陋,但它确实有效。

$ gcc gs_base.c -o gs_base -m32
/usr/bin/ld: /tmp/ccAPoxwj.o: warning: relocation against `rdgsbase' in read-only section `.text'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE

$ ./gs_base 
gs_base = 0xf7f80040
canary: 0x59511d00
canary: 0x59511d00

在 32 位系统中,gs 段选择器的值为 0x33,它指向 GDT 中的第 7 个条目(索引 6)。那么让我们看看里面有什么。

使用我在 OP 中显示的相同模块(仅稍作修改),我打印了在执行特定进程期间使用的 GDT。这是索引为 6 的条目:

[ 3579.535005] Entry #6:
[ 3579.535007]  Raw: 0xd100ffff
[ 3579.535009]  Base: 0xb7fcd100
[ 3579.535011]  Limit: 0xfffff
[ 3579.535013]  Flags: 0xd0f3
[ 3579.535018]      Type = 0x3 (Data, expand down, writable, accessed)
[ 3579.535019]      S    = 0 (user)
[ 3579.535021]      DPL  = 3
[ 3579.535023]      P    = 1 (present)
[ 3579.535025]      AVL  = 1
[ 3579.535027]      L    = 0
[ 3579.535028]      D/B  = 1
[ 3579.535030]      G    = 1 (KiB)

在 gdb 中我们可以验证它是否与所述进程的 TLS 一致:

(gdb) x/wx $ebp-0xc
0xbffff60c: 0xa6e29800
(gdb) x/wx 0xb7fcd100+0x14
0xb7fcd114: 0xa6e29800

使用strace我们可以看到32位glibc如何在64位系统上设置gs:

set_thread_area({entry_number=-1, base_addr=0xf7ebb040, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}) = 0 (entry_number=12)

此系统调用在内核中使用参数base_addr 中指定的值执行MSR_GS_BASE 的设置。内核还将值 0x63 放在 gs 寄存器中,它指向索引为 12 的条目,即 NULL 条目。

在 32 位系统上,系统调用完全相同

set_thread_area({entry_number=-1, base_addr=0xb7f66100, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}) = 0 (entry_number=6)

但是在这里,在 32 位内核(它不知道任何关于 MSR_GS_BASE)上,gs 寄存器的值是 0x33,指向 GDT 中的索引 6。由于现在没有 MSR_GS_BASE,因此设置了 GDT 条目,其基地址和限制字段(以及其余字段)等于参数中指定的字段。

另一方面,64 位 glibc 使用系统调用 arch_prctl(ARCH_SET_FS, 0x...) 来设置 MSR_FS_BASE 的值。此系统调用仅适用于 64 位程序。

唯一我还不太明白的是为什么设置 gs=0x63 而不是 0 或 0x2b(ss、ds 和 es 的值)...

【讨论】:

  • 但它们也存在于传统模式(32 位)中。 - 你刚才说 MSR 存在于保护模式中。 32 位保护模式是传统模式的子模式。 32 位兼容模式是长模式的子模式。 en.wikipedia.org/wiki/X86-64#Operating_modes。这句话的意思是 64 位内核可以在切换到兼容模式用户空间之前使用 MSR。用户空间不使用 MSR,仅使用通过任何方法设置的实际段基,包括 MSR 或从 GDT 加载。
  • 除非您使用-mno-red-zone 编译,否则您的内联汇编是不安全的。编译器可能会将本地变量保持在 RSP 之下。 Inline assembly that clobbers the red zone (虽然在这种情况下它不会因为它可以看到 printf 调用,所以它知道该函数是非叶子的,这对于当前的 GCC 意味着它恰好选择不使用红色区域。)
  • @PeterCordes 修复了兼容模式和传统模式之间的混淆。谢谢。
  • @PeterCordes 说 asm 代码只是一个肮脏的实验,没什么大不了的。
  • 另外,如果中断处理程序假定当前 GS 是 kernelGS,swapgs; rdgsbase; swapgs 在可抢占内核中可能是不安全的。 cli / sti 在它周围可能会更好,或者如果它可能在已禁用中断的情况下运行(因此您不应该盲目地重新启用),请使用 Linux 现有的帮助函数之一来禁用/恢复中断状态。仅在保存的用户空间上下文中查找此任务也可能有效,尽管如果内核的每个条目都没有更新,那么如果 64 位用户空间使用了wrgsbase,它将是陈旧的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-10-26
  • 2010-10-18
  • 2012-06-25
相关资源
最近更新 更多