在 64 位程序上对 32 位程序使用 gs 寄存器 linux
Use of gs register on a 32 bit program over a 64 bit linux
在 64 位程序中,用于获取堆栈保护器的 selector:offset 是 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(这是预期的),描述符 table 选择器指示 GDT(linux 中未使用 LDT)并且选择器指向64 位索引为 12 的条目和 32 位索引为 6 的条目。
使用内核模块我能够检查 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 位程序 运行 中工作?
在@PeterCordes 的评论之后,我在“AMD64 架构程序员手册,第 2 卷”中进行了搜索,其中第 27 页说:
Compatibility mode ignores the high 32 bits of base address in the FS and GS segment descriptors when calculating an effective address.
这意味着管理 32 位进程的 64 位内核使用 MSR_*S_BASE
寄存器,就像管理 64 位进程一样。内核可以在 64 位长模式下正常设置段基数,因此这些 MSR 在 32 位 compatibility sub-mode of long mode 或纯 32 位保护模式(传统模式)下是否可用并不重要, 32 位内核)。 64 位 Linux 内核仅对 ring 3 (user-space) 使用兼容模式,其中 wrmsr
和 rdmsr
由于特权不可用。与往常一样,基于段的设置在权限级别的更改中持续存在,例如 returning 到 user-space with sysret
or iret
.
让我认为此寄存器未用于兼容模式进程的另一件事是 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然后return到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 [=15=]x33, $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的值)...
在 64 位程序中,用于获取堆栈保护器的 selector:offset 是 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(这是预期的),描述符 table 选择器指示 GDT(linux 中未使用 LDT)并且选择器指向64 位索引为 12 的条目和 32 位索引为 6 的条目。
使用内核模块我能够检查 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 位程序 运行 中工作?
在@PeterCordes 的评论之后,我在“AMD64 架构程序员手册,第 2 卷”中进行了搜索,其中第 27 页说:
Compatibility mode ignores the high 32 bits of base address in the FS and GS segment descriptors when calculating an effective address.
这意味着管理 32 位进程的 64 位内核使用 MSR_*S_BASE
寄存器,就像管理 64 位进程一样。内核可以在 64 位长模式下正常设置段基数,因此这些 MSR 在 32 位 compatibility sub-mode of long mode 或纯 32 位保护模式(传统模式)下是否可用并不重要, 32 位内核)。 64 位 Linux 内核仅对 ring 3 (user-space) 使用兼容模式,其中 wrmsr
和 rdmsr
由于特权不可用。与往常一样,基于段的设置在权限级别的更改中持续存在,例如 returning 到 user-space with sysret
or iret
.
让我认为此寄存器未用于兼容模式进程的另一件事是 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然后return到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 [=15=]x33, $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的值)...