Enrique Soriano-Salvador

Logo

sysfatal(blog)

24 June 2026

Running VLC as root and the magic sed

by e__soriano


vlc as root

Recently, a student told me that it wasn’t possible to run VLC as root… it seemed strange to me, so I tried:

# id
uid=0(root) gid=0(root) groups=0(root)
# vlc
VLC is not supposed to be run as root. Sorry.
If you need to use real-time priorities and/or privileged TCP ports
you can use vlc-wrapper (make sure it is Set-UID root and
cannot be run by non-trusted users first).
#

Indeed, it does not allow itself to be run as root due to security reasons. It appears that the application can be recompiled with an option to disable this check.

I would have thought that the quickest approach would be to patch the binary with radare2, for instance, to bypass the check. However, I came across this rather amusing solution: running a simple sed command to replace the string geteuid with the string getppid in the ELF binary, like this:

# pwd
/tmp
# cp $(which vlc) vlc
# sed -i 's/geteuid/getppid/g' vlc
# ./vlc
VLC media player 3.0.20 Vetinari (revision 3.0.20-0-g6f0d0ab126b)
[000064c8dd5c3a80] vlcpulse audio output error: PulseAudio server connection failure: Connection refused
[000064c8dd505550] main libvlc: Running vlc with the default interface. Use 'cvlc' to use vlc without interface.
QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-root'
[000064c8dd59d770] main playlist: playlist is empty

Well, it works! But, how? We are dealing with a typical case that surprises neither a novice (someone who does not know what an executable binary is, who has never heard of ELF or thinks that a binary is like a python program) nor an expert in systems and binary analysis, but rather those who fall somewhere in between these two extremes.

VLC checks the effective UID credential: if it is 0 (the UID of root), it aborts the execution. The trick here is to replace this call. Instead of calling the geteuid function, the program will call the getppid function. Both functions belong to the C standard library. Both have seven-character names. Both take no parameters. Both return an integer value. The key point is that getppid will never return 0. This function returns the PID of the parent process (which will not be 0). Therefore, VLC will assume that the user running the program is not root (0).

Is it calling the getppid function after the modification?

# strace ./vlc 2>&1 | grep getppid
getppid()                               = 99697

Yes, there is a getppid system call. If we try with another function:

# cp $(which vlc) vlc
# sed -i 's/geteuid/getpid/g' vlc
# ./vlc
Segmentation fault (core dumped)
#

Why? The lenght of the name is not 7 chars :)

But how can that sed command achieve this simply by replacing one string with another? Why doesn’t it break the binary? Why it is now calling getppid instead of geteuid?

VLC is a stripped dynamically linked binary with full RELRO:

# cp $(which vlc) vlc
# file vlc
vlc: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=846755832ef8bb6ccc424ad2d6d30be209083a92, for GNU/Linux 3.2.0, stripped
# nm vlc
nm: vlc: no symbols
# checksec --file=./vlc
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols	  Yes	2		3		./vlc
#

This means that, before running, all the relocations for dynamic libraries are resolved. If we dump the dynamic symbols table, we can see the geteuid symbol:

# objdump -T vlc | grep geteuid
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) geteuid
#

But, where is the "geteuid" string? Lets use radare2:

[0x000018e0]> / geteuid
Searching 7 bytes in [0x4010-0x4020]
hits: 0
Searching 7 bytes in [0x3c98-0x4010]
hits: 0
Searching 7 bytes in [0x2000-0x23e8]
hits: 0
Searching 7 bytes in [0x1000-0x1bbd]
hits: 0
Searching 7 bytes in [0x0-0xee8]
hits: 1
Searching 7 bytes in [0x100000-0x1f0000]
hits: 0
0x00000892 hit0_0 ._stack_chk_failgeteuidisattysigempty.
[0x000018e0]> px 128 @ 0x00000892
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00000892  6765 7465 7569 6400 6973 6174 7479 0073  geteuid.isatty.s
0x000008a2  6967 656d 7074 7973 6574 0073 6967 6164  igemptyset.sigad
0x000008b2  6473 6574 0070 7468 7265 6164 5f73 656c  dset.pthread_sel
0x000008c2  6600 7074 6872 6561 645f 7369 676d 6173  f.pthread_sigmas
0x000008d2  6b00 6c69 6276 6c63 5f67 6574 5f63 6861  k.libvlc_get_cha
0x000008e2  6e67 6573 6574 006c 6962 766c 635f 6765  ngeset.libvlc_ge
0x000008f2  745f 7665 7273 696f 6e00 6d65 6d63 7079  t_version.memcpy
0x00000902  006c 6962 766c 635f 6e65 7700 6c69 6276  .libvlc_new.libv
[0x000018e0]> 0x00000892
[0x00000892]> iS.
Current section

nth paddr        size vaddr       vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000798  0x271 0x00000798  0x271 -r-- .dynstr


[0x00000892]>

Here it is, together with other library function names, in the .dynstr section of the ELF file.

What’s this? According to the manual page elf(5):

.dynstr
	 This section holds strings needed for dynamic linking, most
	 commonly the strings that represent the names associated
	 with symbol table entries.  This section is of type
	 SHT_STRTAB.  The attribute type used is SHF_ALLOC.

Those strings are used by the .dynsym section:

.dynsym
       This section holds the dynamic linking symbol table.  This
       section is of type SHT_DYNSYM.  The attribute used is
       SHF_ALLOC.

When a function is imported, the linker:

The relocation specifies the destination through the r_offset field, which points to an entry in the GOT (Global Offset Table). This table, located in .got.plt, will be initialized by the dynamic loader as it resolves the relocations. The loader will map the code of the libraries in the process memory and patch the table with the corresponding addresses.

As said before, with full RELRO, all imported symbols are resolved at startup time. When the program starts, the .got.plt section is completely initialized with the final addresses of the functions and the corresponding memory pages will be marked as read only (it is an exploiting mitigation). Later, when the program calls the function, the trampolines will do the job and the flow will be redirected to the corresponding function code (located in the library’s text pages).

The point is that the loader will use the name of the function (getppid in this case) to resolve the symbol (that is, to search the dynamic libraries used by the binary). Thus, if the string is getppid, the code pointed by the GOT will be the code of the getppid funcion of the libc. This code is in fact the stub code for the getppid system call, which returns the PID of the parent of the process; it will never be 0 (the UID for root). This way, VLC is cheated.

Remember: don’t run VLC as root.

(cc) Enrique Soriano-Salvador Algunos derechos reservados. Este trabajo se entrega bajo la licencia Creative Commons Reconocimiento - NoComercial - SinObraDerivada (by-nc-nd). Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA.

tags: vlc