clinuxx86-64cpu-registersmemory-segmentation

Reading a segment register (%gs) which contains the pointer to a list


I have stumbled upon this kernel page: https://www.kernel.org/doc/html/v5.9/x86/x86_64/fsgs.html and wanted to play around with LD_PRELOAD to see whether I can allocate a value at a load-time and read the segment register at runtime to load the value.

For example, I have two simple codes that can be easily compiled and executed to demonstrate (I had to modify a bit from example code provided in the kernel site):

load.c compiled with gcc -Wall -fPIC -shared -o load.so load.c -ldl -mfsgsbase

#include <sys/auxv.h>
#include <elf.h>
#include <immintrin.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
/* Will be eventually in asm/hwcap.h */
#ifndef HWCAP2_FSGSBASE
#define HWCAP2_FSGSBASE        (1 << 1)
#endif
#define _GNU_SOURCE
void __attribute__((constructor)) load()
{
    int a = 4;
    unsigned val = getauxval(AT_HWCAP2);
    if (val & HWCAP2_FSGSBASE)
        printf("FSGSBASE enabled\n");

    int *addr_a = mmap(NULL, getpagesize(), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);     
    if (addr_a == MAP_FAILED) {     
        fprintf(stderr, "mmap() failed\n");     
        exit(EXIT_FAILURE);
    } 
    *addr_a = a;
    _writegsbase_u64(addr_a);
}

and

main.c compiled with gcc -S main.c -o main.s && sed -i 's/-4(%rbp)/%gs:0x0/g' main.s && gcc main.s -o main

#include <stdio.h>

int main() {
    int a;  
    printf("From main: %d\n", a);
    printf("Hello World!\n");
    return 0;
}

Upon executing the command: > LD_PRELOAD=$PWD/load.so ./main

FSGSBASE enabled
From main: 4
Hello World!

So now I can successfully load the value at runtime (sed modifies the assembly code to read %gs register as instructed in the kernel page)


Next, I wanted to do something more interesting: store the base address of a list in the segment register %gs and access this address at runtime.

For instance, I modified the above load code like this (I just added two lines, but for the sake of completeness):

load_updated.c compiled with gcc -Wall -fPIC -shared -o load_updated.so load_updated.c -ldl -mfsgsbase

void __attribute__((constructor)) load()
{
    int a = 4;
    unsigned val = getauxval(AT_HWCAP2);
    if (val & HWCAP2_FSGSBASE)
        printf("FSGSBASE enabled\n");

    int *addr_a = mmap(NULL, getpagesize(), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);     
    if (addr_a == MAP_FAILED) {     
        fprintf(stderr, "mmap() failed\n");     
        exit(EXIT_FAILURE);
    } 
    *addr_a = a;
    int *table[] = {addr_a};
    printf("Table base address: %p\n", &table);
    _writegsbase_u64(&table);
}

However, loading the pointer address to a list to the segment register is not the same as loading the value (my first example). Therefore, I wanted to know how I should approach solving this problem of trying to read a segment register that contains the pointer to a list.

I have attempted to debug this by executing it with gdb like the following:

gdb ./main
pwndbg> set environment LD_PRELOAD ./load_updated.so
pwndbg> b main
Breakpoint 1 at 0x1171
pwndbg> start
FSGSBASE enabled
Table base address: 0x7fffffffd880 0x7ffff7fb5000
pwndbg> i r $gs_base
gs_base        0x7fffffffd880      140737488345216

So I can check that I did indeed load the correct pointer address to the %gs register. Then when I step into

   0x555555555171 <main+8>     sub    rsp, 0x10
 ► 0x555555555175 <main+12>    mov    eax, dword ptr gs:[0]
   0x55555555517d <main+20>    mov    esi, eax

The instruction where I am trying to dereference the value of %gs into the %eax register, %eax register is loaded with nothing:

pwndbg> i r eax
rax            0x0                 0

I have also tried to simply mov the value from the %gs register into the %eax register (no dereferencing) by manually modifying the assembly code of movl %gs:0, %eax -> movl %gs, %eax

But to no avail, and now I'm wondering whether there might be some other assembly codes I may need to add and trying to get some clue on how to go from this.

I thought this might be interesting because if I can obtain the base address of a list at runtime, I could most likely use pointer arithmetic to access different elements of the list allocated at load time.

FWIW here is my OS information and architecture:

> uname -a
Linux pop-os 6.2.6-76060206-generic #202303130630~1685473338~22.04~995127e SMP PREEMPT_DYNAMIC Tue M x86_64 x86_64 x86_64 GNU/Linux

I appreciate any insights, and please let me know if I'm unclear in any parts of my question.


Edit: Thanks to @Peter Cordes; I figured out that you can instead add the assembly instruction of:

    rdgsbase %rax
    movq    0x0(%rax), %rax

To answer the question.

The warning is that although you can read the %gs register this way, for unknown reasons, the address stored in the register doesn't contain the array (so that's another thing to figure out).

Just for completeness, what I mean is that if I were to run the gdb similar to the above, it would look like this:

pwndbg> set environment LD_PRELOAD ./load.so
pwndbg> b main
Breakpoint 1 at 0x1171
pwndbg> start
Temporary breakpoint 2 at 0x1171
Table addr: (base address) 0x7fffffffd870 (*table[0]) 0x7ffff7ffa000

it looks like this in GDB:

*RAX  0x7fffffffd870 ◂— 0x86d11c8e53b3e43
──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────
   0x555555555171 <main+8>     sub    rsp, 0x10
   0x555555555175 <main+12>    rdgsbase rax
 ► 0x55555555517a <main+17>    mov    rax, qword ptr [rax + 0x40]

As it can be observed, 0x7fffffffd870 doesn't contain 0x7ffff7ffa000 as expected, but some garbage value of 0x86d11c8e53b3e43.


Solution

  • movl %gs, %eax reads the 16-bit selector value (index into the GDT), zero-extended to 32-bit. (https://felixcloutier.com/x86/mov).

    You're not in real mode so that's unrelated to the segment base, and a 64-bit kernel will just use a null selector (0) for ds/es/fs/gs, only setting up segment descriptors for CS and SS which x86-64 still requires to keep the machine happy (and tell it that it's in 64-bit long mode).

    You need rdgsbase or a system call (since reading an MSR is a privileged operation, not available directly in user-space.) In 64-bit mode, the mechanism for setting a 64-bit segment base for FS or GS is by writing MSRs, rather than creating a GDT or LDT entry and doing mov %eax, %gs. (In 64-bit mode, other segment bases are fixed at 0.)

    My answer on How to access segment register without linking libc.so? shows how to write FS with a Linux arch_prctl system call, which was the only option on CPUs without the FSGSBASE extension and a kernel that enabled it for use by user-space. (The latter being much more recently than the first silicon to support wrfsbase / rdfsbase etc.) The same syscall with different args could read a segment base.