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
.
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.