If a tree falls in a forest and no one is around to hear it, does it make a sound? Similarly, if we patch the memory of Counter-Strike: Global Offensive from kernel space, would that be considered an internal or external hack? Not sure. I know how to do it, though. Do you want to know?

I’ll take that as a yes. I should warn you, my kernel knowledge can be summed up as ‘read a few function comments in the source code once’. I’m no expert, and you won’t be either by the time you’ve finished reading this, but who cares about that, don’t you want to tell your friends you’re a big shot game hacker and kernel hacker at the same time? Damn right you do.

I have no idea what I'm doing. I tracked down the episode and edited the subtitles for this joke. Please clap.

Let’s not get ahead of ourselves just yet, we should at least make sure this thing works in userspace first.

Inspiration for our method comes from the recent glow hack coding contest. The aim was to implement the spectator glow effect in the least amount of characters possible. It only took a few hours before some clever people found out that you can just edit a few bytes for the game to assume you’re always a spectator. That’s the method we’ll be using here.

This isn’t the interesting part so I’ll try to get it out of the way as fast as I can. Open up IDA to client_client.so, search for the string “spec_show_xray”. Jump to the first reference where the ConVar is being initialised, you’ve probably seen this before, rename the unknown variable under the string to spec_show_xray. Creative name, I know.

Cross-references to spec_show_xray ConVar An initialisation function used to create console variables.

Now check the cross-references to that variable. You’ll notice that two of the references are in the current function – the next one is the one we’re interested in. If you jumped there and you see something similar to the picture below then congratulations, you’ve found the GlowEffectSpectator function.

GlowEffectSpectator showing a reference to spec_show_xray Hooray for self-explanatory function names.

According to the Mac binaries from a few months ago, sub_7A4300 is a function called CanSeeSpectatorOnlyTools. As you might expect, it does some checks as to whether you’re a spectator or not – if so, you’re allowed to see the glow effect.

.text:C6E360    E8 CB 5F B3 FF                          call    CanSeeSpectatorOnlyTools
.text:C6E365    84 C0                                   test    al, al
.text:C6E367    0F 84 C3 01 00 00                       jz      loc_C6E530
.text:C6E36D    48 8B 3D C4 AD 91 05                    mov     rdi, cs:qword_6589138
.text:C6E374    48 8D 05 85 AD 91 05                    lea     rax, spec_show_xray

What happens if we get rid of that jump?

#include <dlfcn.h>
#include <string.h>

int __attribute__((constructor)) main(void) {
  // jpeg_UtlBuffer_dest is exported at BE7B20 so add 86847 to get to C6E367
  void* client_client = dlopen("./csgo/bin/linux64/client_client.so", RTLD_NOLOAD | RTLD_NOW);
  void* target_address = dlsym(client_client, "jpeg_UtlBuffer_dest") + 0x86847;

  // overwrite 6 bytes with the NOP instruction
  memset(target_address, 0x90, 6);
}
$ g++ -fPIC -shared glow.cc -o glow.so; sudo csgo_inject glow.so
Thread 1 "csgo_linux64" received signal SIGSEGV, Segmentation fault.
0x00007fd0e8557a05 in __memset_avx2_unaligned_erms () from /usr/lib/libc.so.6

Oh yeah, that part of memory isn’t writable. Let’s try that again.

// jpeg_UtlBuffer_dest is exported at BE7B20 so add 86847 to get to C6E367
void* client_client = dlopen("./csgo/bin/linux64/client_client.so", RTLD_NOLOAD | RTLD_NOW);
void* target_address = dlsym(client_client, "jpeg_UtlBuffer_dest") + 0x86847;

// get the start address of the page containing the memory we want to edit
void* address = (void*)((long)target_address & -sysconf(_SC_PAGESIZE));

// change the page permissions so we can write to it
mprotect(address, sizeof(address), PROT_READ | PROT_WRITE | PROT_EXEC);

// overwrite 6 bytes with the NOP instruction
memset(target_address, 0x90, 6);

Great, now you know how this method works and what to expect. We don’t have the fine grained control we usually have from manipulating the glow object manager but we can at least toggle it by switching spec_show_xray on and off.

Now for the main attraction. Make sure you’ve got the necessary Linux headers installed. You’ll also need this Makefile.

obj-m += kglow.o

all:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
  make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Okay, time to begin. Start by including these header files.

#include <linux/highmem.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/sched/signal.h>

Now, if you want to, include some module information that’ll show up in modinfo.

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Emma N. Skye");
MODULE_DESCRIPTION("A glow hack implemented in kernel space.");

When we load our module later we’ll provide two arguments: one for the patch address – this will be relative to the base address of client_client.so in /proc/pid/maps, the other argument will be how many bytes to replace with NOP instructions

static uint size;
static ulong offset;

module_param_named(patch_size, size, uint, 0644);
MODULE_PARM_DESC(patch_size, "Amount of bytes to replace with NOP instruction.");

module_param_named(patch_offset, offset, ulong, 0644);
MODULE_PARM_DESC(patch_offset, "Target patching address relative to 'client_client.so'.");

There are a few different ways for userspace to communicate with the kernel. In the few minutes I spent searching, I found that making an entry in /proc was the easiest. Here’s everything we’ll need for that.

static int kglow_proc_show(struct seq_file* m, void* v) {
  // our code goes here!
  return 0;
}

static int kglow_proc_open(struct inode* i, struct file* f) {
  return single_open(f, kglow_proc_show, 0);
}

static const struct file_operations kglow_proc_fops = {
  .owner = THIS_MODULE,
  .open = kglow_proc_open,
  .read = seq_read,
  .llseek = seq_lseek,
  .release = single_release,
};

static int __init kglow_proc_init(void) {
  proc_create("kglow", 0, 0, &kglow_proc_fops);
  return 0;
}

static void __exit kglow_proc_exit(void) {
  remove_proc_entry("kglow", NULL);
}

module_init(kglow_proc_init);
module_exit(kglow_proc_exit);

Our code will be triggered by reading from /proc/kglow – pretty lame, but it works.

With that out of the way, we’re finally ready to write some real code! We’ll start by finding the csgo_linux64 process.

static int kglow_proc_show(struct seq_file* f, void* v) {
  struct task_struct* task;

  for_each_process(task) {
    if (strcmp(task->comm, "csgo_linux64") == 0) {
    }
  }

  return 0;
}

Fairly straight-forward so far. We use the for_each_process macro to enumerate the list of processes. In this case, processes are represented as a task_struct which contains a variable called comm – the base executable name.

Next on the list, finding the ‘base’ address of client_client.so. For this, I looked at the source of show_map_vma which is used for generating the /proc/pid/maps file. Everything we need is in the vm_area_struct, including the next one. Simply walk through the list, skipping those that aren’t backed by files.

if (strcmp(task->comm, "csgo_linux64") == 0) {
  struct vm_area_struct* current_mmap;
  
  for (current_mmap = task->mm->mmap; (current_mmap = current_mmap->vm_next) != NULL;) {
    if (current_mmap->vm_file == NULL)
      continue;

    if (strcmp(current_mmap->vm_file->f_path.dentry->d_iname, "client_client.so") != 0)
      continue;

    break;
  }
}

We’ve got everything we need to perform the actual memory manipulation now.

struct page* game_page;
int locked = 1;

down_write(&task->mm->mmap_sem);

if (get_user_pages_remote(task, task->mm, current_mmap->vm_start + offset, 1,
                          FOLL_FORCE, &game_page, NULL, &locked) == 1) {
  void* target = kmap(game_page);
  memset(target + (offset % PAGE_SIZE), 0x90, size);
  kunmap(game_page);
}

up_write(&task->mm->mmap_sem);

Firstly, lock the semaphore in the vm_area_struct for writing, this is a requirement for calling the next function, get_user_pages_remote. This function is used to pin the target page into memory – check out this blog post for a much better explanation on pages and general memory management than you’re ever going to find here.

Once we have the page, we pass it to kmap. This function creates a special mapping in kernel address space for our page, allowing us to manipulate it directly. It’s pretty simple after that, we use a modulo operation on the user-provided offset to get the distance from the page – for example 0xC6E367 would become 0x367 on a system with a page size of 0x1000.

Since we have write access from passing FOLL_FORCE in the gup_flags parameter, we can simply call memset, write our NOP instructions and then unmap the page.

All done with that now, it’s time to test this out! Make sure you’ve noted down the patch address from earlier, you’ll need to convert it to decimal (in this case, C6E367 = 13034343) when you pass the patch_offset parameter to the module loader.

$ make clean all
$ sudo insmod kglow.ko patch_offset=13034343 patch_size=6

Once you’ve got the game running, it’s just one more step to trigger our code.

$ cat /proc/kglow
This whole post in video form, along with a quick test in-game. (mirror)

I’m still very new to the kernel but I plan on trying more ambitious stuff in the future. It looks like there’s some hysteria after some users of a certain internal multihack were recently banned, maybe I’ll write about some alternative injection methods or start looking into some “safer” external stuff.

Big thanks to aixxe for hosting & editing this mess into something readable. ♥

Last updated Friday, 17 January 2020 at 07:09 PM.

Emma N. Skye