Patch for PunkBuster screenshot vulnerability
Download
- pbss_rce_fix_complete.so (disable screenshots altogether, recommended)
- pbss_rce_fix_malicious.so (only allow writes to
pb/svss/
)
Description
In 2020, a severe vulnerability has been discovered in PunkBuster's screenshot mechanism. It allows an attacker to upload an arbitrary file to an arbitrary location on the machine running the affected PunkBuster server version. As EvenBalance has discontinued support for ET, every server running PunkBuster is vulnerable.
As far as I'm aware, this hasn't been exploited in the wild, but the attack is far from being theoretical.
Usage
Use LD_PRELOAD
environment variable to load the library into etded
, like so:
LD_PRELOAD=pbss_rce_fix.so etded +set dedicated ...
Or, if you already use preloading (order doesn't matter):
LD_PRELOAD="pbss_rce_fix.so libetwolf_server_demo.so" etded +set dedicated ...
Patch
This is simple fopen(3)
hook that checks for relative return address within pbsv.so
. In other words, the hook intercepts and alters the function's behavior (returns NULL
) depending on where from and with what parameters it has been called.
Set DISABLE_ALTOGETHER
to turn off the feature completely, otherwise only possibly malicious screenshots will be dropped. The PB_SV_SsPath
cvar is not being taken into account, because who cares, given that the screenshots are usually void anyway since Windows 7 or so.
#define _GNU_SOURCE
#include <link.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#define LIB_NAME "pbss_rce_fix"
#define PBSV_LIB "pbsv.so"
#define PBSS_PNG_RETURN_ADDR (void *) 0x11e696
#define PBSS_HTM1_RETURN_ADDR (void *) 0x0db819
#define PBSS_HTM2_RETURN_ADDR (void *) 0x11e7a8
#define PBSS_HTM3_RETURN_ADDR (void *) 0x0ba03a
#define MAX_OSPATH 256
#define CVAR_INIT 16
#define VER_MEM_CHECK 0x8d1cec83
#define FUNC_COM_PRINTF 0x806C450
#define FUNC_CVAR_GET 0x8071E40
#define DISABLE_ALTOGETHER 1
#if !DISABLE_ALTOGETHER
typedef struct cvar_s {
char *name;
char *string;
char *resetString;
char *latchedString;
int flags;
int modified;
int modificationCount;
float value;
int integer;
struct cvar_s *next;
struct cvar_s *hashNext;
} cvar_t;
cvar_t* (*Cvar_Get)(const char *var_name, const char *var_value, int flags);
#endif
FILE* (*orig_fopen)(const char *filename, const char* mode);
void (*Com_Printf)(const char *msg, ...);
static int dl_iterate_phdr_callback(struct dl_phdr_info *info, size_t size, void *data) {
for (int i = 0; i < info->dlpi_phnum; i++) {
if (
(unsigned int) data > (info->dlpi_addr + info->dlpi_phdr[i].p_vaddr)
&& (unsigned int) data < (info->dlpi_addr + info->dlpi_phdr[i].p_vaddr + info->dlpi_phdr[i].p_memsz)
&& !strcmp(PBSV_LIB, basename(info->dlpi_name))
&& (data - info->dlpi_addr + info->dlpi_phdr[i].p_vaddr == PBSS_PNG_RETURN_ADDR ||
data - info->dlpi_addr + info->dlpi_phdr[i].p_vaddr == PBSS_HTM1_RETURN_ADDR ||
data - info->dlpi_addr + info->dlpi_phdr[i].p_vaddr == PBSS_HTM2_RETURN_ADDR ||
data - info->dlpi_addr + info->dlpi_phdr[i].p_vaddr == PBSS_HTM3_RETURN_ADDR)) {
return 1;
}
}
return 0;
}
static int check_ss_name(const char *filename) {
#if DISABLE_ALTOGETHER
return 0;
#else
unsigned int len = strlen(filename);
if (len < 4 || len > MAX_OSPATH) {
return 0;
}
if ( strcasecmp(filename + len - 4, ".png") != 0
&& strcasecmp(filename + len - 4, ".htm") != 0) {
return 0;
}
cvar_t* fs_homepath = Cvar_Get("fs_homepath", "", CVAR_INIT);
unsigned int hpLen = strlen(fs_homepath->string);
for (int i = 0; i < len; i++) {
if (i < hpLen) {
if (filename[i] != fs_homepath->string[i]) {
return 0;
}
continue;
}
if (i == hpLen) {
if (strncmp("/pb/svss/pb", filename + i, 11) != 0) {
return 0;
}
i += 10;
continue;
}
if (i == len - 4) {
break;
}
if (filename[i] < '0' || filename[i] > '9') {
return 0;
}
}
return 1;
#endif
}
FILE* fopen(const char* filename, const char* mode) {
if (dl_iterate_phdr(dl_iterate_phdr_callback, __builtin_return_address(0)) && !check_ss_name(filename)) {
#if !DISABLE_ALTOGETHER
Com_Printf("%s: intercepted possibly malicious fopen(%s)\n", LIB_NAME, filename);
#endif
errno = EACCES;
return NULL;
}
return orig_fopen(filename, mode);
}
void __attribute__ ((constructor)) init(void) {
if (*(int *) FUNC_COM_PRINTF != VER_MEM_CHECK) {
fprintf(stderr, "%s: memory check failed - incompatible etded binary\n", LIB_NAME);
exit(1);
}
void *libc_handle = dlopen("libc.so.6", RTLD_LAZY);
orig_fopen = dlsym(libc_handle,"fopen");
Com_Printf = (void *) FUNC_COM_PRINTF;
#if !DISABLE_ALTOGETHER
Cvar_Get = (void *) FUNC_CVAR_GET;
#endif
}