问题始于一个简单的场景:“canary绕过”,一下子唤起我多年的问题,FS寄存器究竟是什么,在哪里?

如下是段寄存器的结构示意图:

可以注意到,一个64位的段寄存器分为两个部分,Hidden Part部分包括了我们一般会用到的Base Address。常说的“用户无法访问FS寄存器”应该改为"用户无法直接访问FS寄存器"便不会引起误会了。

来看看官方手册怎么说:

Intel手册:

In order to set up compatibility mode for an application, segment-load instructions (MOV to Sreg, POP Sreg) work
normally in 64-bit mode. An entry is read from the system descriptor table (GDT or LDT) and is loaded in the hidden
portion of the segment register. The descriptor-register base, limit, and attribute fields are all loaded. However, the
contents of the data and stack segment selector and the descriptor registers are ignored.

When FS and GS segment overrides are used in 64-bit mode, their respective base addresses are used in the linear
address calculation: (FS or GS).base + index + displacement. FS.base and GS.base are then expanded to the full
linear-address size supported by the implementation. The resulting effective address calculation can wrap across
positive and negative addresses; the resulting linear address must be canonical.

In 64-bit mode, memory accesses using FS-segment and GS-segment overrides are not checked for a runtime limit
nor subjected to attribute-checking. Normal segment loads (MOV to Sreg and POP Sreg) into FS and GS load a
standard 32-bit base value in the hidden portion of the segment register. The base address bits above the standard
32 bits are cleared to 0 to allow consistency for implementations that use less than 64 bits.

AMD手册:

FS and GS Registers in 64-Bit Mode. Unlike the CS, DS, ES, and SS segments, the FS and GS
segment overrides can be used in 64-bit mode. When FS and GS segment overrides are used in 64-bit
mode, their respective base addresses are used in the effective-address (EA) calculation. The complete
EA calculation then becomes (FS or GS).base + base + (scale ∗ index) + displacement. The FS.base
and GS.base values are also expanded to the full 64-bit virtual-address size, as shown in Figure 4-5.
Any overflow in the 64-bit linear address calculation is ignored and the resulting address instead wraps
around to the other end of the address space.

In 64-bit mode, FS-segment and GS-segment overrides are not checked for limit or attributes. Instead,
the processor checks that all virtual-address references are in canonical form.

Segment register-load instructions (MOV to Sreg and POP Sreg) load only a 32-bit base-address value
into the hidden portion of the FS and GS segment registers. The base-address bits above the low 32 bits
are cleared to 0 as a result of a segment-register load. When a null selector is loaded into FS or GS, the
contents of the corresponding hidden descriptor register are not altered.

There are two methods to update the contents of the FS.base and GS.base hidden descriptor fields. The
first is available exclusively to privileged software (CPL = 0). The FS.base and GS.base hidden
descriptor-register fields are mapped to MSRs. Privileged software can load a 64-bit base address in
canonical form into FS.base or GS.base using a single WRMSR instruction. The FS.base MSR address
is C000_0100h while the GS.base MSR address is C000_0101h.

The second method of updating the FS and GS base fields is available to software running at any
privilege level (when supported by the implementation and enabled by setting CR4[FSGSBASE]).
The WRFSBASE and WRGSBASE instructions copy the contents of a GPR to the FS.base and
GS.base fields respectively. When the operand size is 32 bits, the upper doubleword of the base is
cleared. WRFSBASE and WRGSBASE are only supported in 64-bit mode

二者均提到的WRFSBASE才是真正对FS进行操作的方式。内核代码如下:

/*
		 * Set the selector to 0 for the same reason
		 * as %gs above.
		 */
		if (task == current) {
			loadseg(FS, 0);
			x86_fsbase_write_cpu(arg2);
			/*
			 * On non-FSGSBASE systems, save_base_legacy() expects
			 * that we also fill in thread.fsbase.
			 */
			task->thread.fsbase = arg2;
		} else {
			task->thread.fsindex = 0;
			x86_fsbase_write_task(task, arg2);
		}

首先会把GDT或LDT的0号选择子加载到FS里。但根据AMD手册可知:

“When a null selector is loaded into FS or GS, the contents of the corresponding hidden descriptor register are not altered.”

FS的低位并不会做出改变,更加重要的是第二个函数x86_fsbase_write_cpu,实现如下:

static inline void x86_fsbase_write_cpu(unsigned long fsbase)
{
	if (static_cpu_has(X86_FEATURE_FSGSBASE))
		wrfsbase(fsbase);
	else
		wrmsrl(MSR_FS_BASE, fsbase);
}

其调用wrfsbase来真正向FS中写入Base等数据。至于wrfsbase是什么,它不过只是一条指令罢了。此前的MSR还在使用FSGSBASE指令来写FS寄存器,但它不如wrfsbase来得效率,因此目前的新版本更多愿意选择wrfsbase。这些指令不同于mov、pop等外部修改指令,它们能够直接操作寄存器内部的值,不会把寄存器的内容外泄出来。

另外,gdb是怎么拿到fsbase的?具体是方式是什么?来看pwndbg的源代码:

PTRACE_ARCH_PRCTL = 30
ARCH_GET_FS = 0x1003
ARCH_GET_GS = 0x1004
    @property
    @pwndbg.memoize.reset_on_stop
    def fsbase(self):
        return self._fs_gs_helper(ARCH_GET_FS)

所以结论是,内核向用户提供了接口,用户是能够间接访问FS寄存器的,通过arch_prctl即可。

综上所述,最后来回答一下开始的几个问题吧。

问题一:

  • FS寄存器里放些什么?
  • 答:放的是一个指针,它会指向一个TLS结构体(对于单线程,或许用TCB更加准确)

问题二:

  • FS究竟在哪?
  • 答:这是我最开始学习时产生的误解,我误以为FS并不实际存在,而是虚拟出的一个寄存器。但现在我们知道,FS是真真正正在硬件上存在的寄存器。

问题三:

  • 究竟如何获取FS的内容?
  • 答:一般的,gdb里直接用fsbase指令就能获取了,或者手动使用call调用arch_prctl也不是不行,内核已经提供了获取fsbase的接口了。

问题四:

  • FS寄存器的结构是什么?
  • 上文的图片给出了。

最后,我觉得有点意外也有些特殊的是,在64位模式下,CS、DS、ES、SS寄存器将被直接弃用。这显得有些怪异,毕竟一直以来我都觉得计算机设计得可谓是将冗余降到最低。现在突然多出了几个完全不被使用的寄存器,有点意外。

另外再记录几项资料:

https://stackoverflow.com/questions/28209582/why-are-the-data-segment-registers-always-null-in-gdb

https://stackoverflow.com/questions/11497563/detail-about-msr-gs-base-in-linux-x86-64

https://stackoverflow.com/questions/23095665/using-gdb-to-read-msrs/59125003#59125003

https://dere.press/2020/10/18/glibc-tls/

https://github.com/pwndbg/pwndbg/blob/89b2df582a323b98c04c5d35e3323ad291514f63/pwndbg/regs.py#L268

A possible end to the FSGSBASE saga [LWN.net]

插画ID:93763504


"The unexamined life is not worth living."