在64位linux上的32位程序上使用gs寄存器



在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 entriesn",
(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位系统上尝试过这个模块,但我正在路上。

因此,为了明确这个问题:在64位linux内核上运行的32位程序中,gs段选择器是如何工作的?

在@PeterCordes的评论之后,我在"AMD64体系结构程序员手册,第2卷";,第27页上写着:

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

这意味着管理32位进程的64位内核会像管理64位进程一样使用MSR_*S_BASE寄存器。内核可以在64位长模式下正常设置段基,因此这些MSR是否在长模式的32位兼容子模式下可用,或者在纯32位保护模式(传统模式,32位内核)下可用都无关紧要。64位Linux内核仅对环3(用户空间)使用compat模式,其中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()执行该文件时,上面的代码就会被执行。在编译并运行了一个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位。这可能是一个更好的解决方案,因为它不需要内核模块。(只要你可以假设你运行的CPU支持FSGSBASE扩展,以及一个足够新的内核来启用它。)

类似这样的东西:

#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 = %pn", gs_base);
printf("canary: 0x%08xn", canary);
printf("canary: 0x%08xn", 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的值)。。。

最新更新