Предисловие

Такие языки, как C / C ++, поставляются со всей стороной распределения malloc, calloc, zalloc, realloc и их специализированных версий kmalloc и т. Д. Например, malloc имеет подпись void *malloc(size_t size) Это означает, что можно запросить произвольное количество байтов из кучи, и функция вернет указатель, с которым можно начать работу. Позже память должна быть освобождена с помощью free(). Эти функции по-прежнему представляют интерес для хакеров, которые могут использовать приложения даже в 2019 году - например, недавняя ошибка двойного бесплатного доступа в WhatsApp, о которой я расскажу в следующем посте.

Итак, я недавно побеседовал с Алексеем, который указал мне на свой блог, где он представляет довольно крутой скрипт на основе Ghidra для обнаружения общих malloc ошибок. Я был вдохновлен этим и составил несколько простых запросов с помощью инструмента под названием Ocular, который может помочь в обнаружении таких проблем немного быстрее. Ocular и его двоюродный брат с открытым исходным кодом Joern были разработаны нашей командой в ShiftLeft. Это также будет возможностью узнать, как работают Ocular и Joern, и понять их внутреннюю работу. Если вам не нравится безопасность, вы можете хотя бы изучить Scala с этими инструментами - как и я

malloc () Havoc

Итак, возвращаясь к драме malloc(), вот несколько случаев, когда, казалось бы, правильное использование malloc может пойти не так:

  • Переполнение буфера. Возможно, параметр размера malloc вычисляется другими внешними функциями. Например, как Алексей упомянул в своем сообщении, вот сценарий, в котором размер возвращается из другой функции:
int getNumber() {
  int number = atoi("8");
  number = number + 10;
  return number;
}
void *scenario3() {
  int a = getNumber();
  void *p = malloc(a); 
  return p;
}

В этом случае, хотя источником аргумента размера malloc является просто atoi(), это не всегда так. Что, если значение целого числа (number + 10) выйдет за пределы и станет намного меньше, чем требовалось впоследствии (например, memcpy)? Это может привести к переполнению буфера при доступе к нему или записи в него.

  • Нулевое распределение: как указано в замечательной работе Жюльена (https://openwall.info/wiki/_media/people/jvanegue/files/woot10.pdf), обеспечивающей нулевое поскольку параметр size, хотя и действителен, может вызвать ошибки за пределами допустимого диапазона. Это вполне возможно в тех случаях, когда размер определяется после некоторой арифметической операции (например, операции умножения).
void *scenario2(int y) {
  int z = 10;
  void *p = malloc(y * z);
  return p;
}

Что, если предположительно управляемый извне y оценивается как ноль? В этом случае malloc может возвращать указатель NULL, но пользователь должен убедиться, что есть NULL проверки перед использованием выделенной памяти.

  • Переполнение кучи внутри фрагмента. Один из моих любимых эксплойтов, который я видел несколько раз в дикой природе и сам был жертвой самого себя, - это случай, когда данный фрагмент выделенной памяти, вы случайно перезаписываете один раздел во время работы с другим, не связанным с ним. Пример из Блог Криса Эванса хорошо это объясняет:
struct goaty { char name[8]; int should_run_calc; };
int main(int argc, const char* argv[]) {
   struct goaty* g = malloc(sizeof(struct goaty));
   g->should_run_calc = 0;
   strcpy(g->name, "projectzero");
   if (g->should_run_calc) execl("/bin/gnome-calculator", 0);
}
  • UAF, утечки памяти: они также довольно распространены - если забыть free() выделенную память в конструкциях цикла, это может привести к утечкам, которые в некоторых случаях могут быть использованы для бокового возникновения злонамеренных сбоев. или общее снижение производительности. Другой случай - не забывать освобождать память, но пытается использовать ее позже, что вызывает ошибки использования после освобождения. Несмотря на отсутствие высокой воспроизводимости эксплойтов, это все же может быть вызвано, когда free близко к malloc и мы пытаемся перераспределить (что обычно возвращает тот же или ближайший адрес), что позволяет нам получить доступ к ранее освобожденной памяти.

В этом блоге мы попытаемся охватить первые два случая, когда мы используем Ocular для очистки аргумента malloc's size и посмотрим, могут ли они в конечном итоге привести к переполнению буфера или ошибкам с нулевым распределением.

Окуляр

Ocular позволяет нам сначала представить код (C / C ++ / Java / Scala / C # и т. Д.) В виде графа, называемого График свойств кода - CPG (это похоже на смесь графов AST, потока управления и потоков данных). Я называю это полукомпилятором. Мы берем исходный код (C / C ++ / C #) или байт-код (Java) и компилируем его до IR. Этот IR - это, по сути, график, который у нас есть (CPG). Вместо того, чтобы компилировать его дальше, мы загружаем его в память и позволяем задавать вопросы этому IR для оценки утечки данных между функциями, анализа потока данных, обеспечения правильного использования переменных в критических секциях, обнаружения переполнения буфера, UAF и т. Д.

А поскольку это график, ну, запросы довольно интересны и на 100% написаны на Scala и, как GDB или Radare, могут быть написаны на специальной оболочке Ocular Shell. Например, вы могли бы сказать:

«Привет, Ocular, перечислите все функции в исходном коде, у которых есть« alloc »в своем имени, и дайте мне имя его параметра»

В Ocular Shell это будет переведено как:

ocular> cpg.method.name(".*alloc.*").parameter.name.l
res1: List[String] = List("a")

Вы действительно можете сойти с ума - например, вот я создаю график кода и перечисляю все методы в коде менее чем за минуту:

Обнаружение ошибок распределения с помощью Ocular / Joern

Давайте немного повысим уровень и попробуем сделать несколько простых запросов, специфичных для malloc(). Рассмотрим следующий фрагмент кода. Вы можете сохранить его и поиграть с ним в Ocular или его брате с открытым исходным кодом Joern.

#include <stdio.h>
#include <stdlib.h>
int getNumber() {
  int number = atoi("8");
  number = number + 10;
  return number;
}
void *scenario1(int x) {
  void *p = malloc(x);
  return p;
}
void *scenario2(int y) {
  int z = 10;
  void *p = malloc(y * z);
  return p;
}
void *scenario3() {
  int a = getNumber();
  void *p = malloc(a); 
  return p;
}

В приведенном выше коде давайте определим сайты вызовов malloc, перечислив имя файла и номера строк. Мы можем сформулировать это в следующем запросе к оболочке Ocular:

Глазной запрос:

ocular> cpg.method.callOut.name("malloc").map(x => (x.location.filename, x.lineNumber.get)).l

Результат:

List[(String, Integer)] = List(
  ("../../Projects/tarpitc/src/alloc/allocation.c", 23),
  ("../../Projects/tarpitc/src/alloc/allocation.c", 17),
  ("../../Projects/tarpitc/src/alloc/allocation.c", 11)
)

В примере кода явным индикатором нулевого выделения, которое может произойти, является сценарий 2 или сценарий 3, в котором арифметические операции выполняются в потоке данных, ведущем к параметру malloc call-site. Итак, давайте попробуем сформулировать запрос, который перечисляет потоки данных с источником в качестве параметров из методов «сценария» и приемниками в качестве всех узлов вызова malloc. Затем мы находим все потоки и фильтруем те, которые производят арифметические операции с данными в потоке. Это явный индикатор возможности нулевого или неправильного распределения.

Глазной запрос:

ocular> val sink = cpg.method.callOut.name("malloc").argument
ocular> var source = cpg.method.name(".*scenario.*").parameter
ocular> sink.reachableBy(source).flows.passes(".*multiplication.*").p

Результат:

В приведенном выше запросе мы создали локальные переменные в оболочке Ocular с именами source и sink. Язык - scala, но, как вы можете видеть, он довольно многословен, поэтому мне не нужно много объяснять, но все же, для полноты, вот как мы можем объяснить первое утверждение в запросе Ocular на английском языке:

Чтобы идентифицировать приемник, найдите все сайты вызовов ( callOut ) для всех методов на графике ( cpg ) с именем malloc и отметьте свои аргументы как приемные.

В приведенном выше коде это будут x, (y * z) и a. Вы поняли

Довольно круто, но можно сказать, что это банально. Поскольку мы явно отмечаем исходный метод как сценарий. Давайте немного повысим уровень. Что, если мы не хотим перебирать все методы, а затем выяснять, уязвимы ли они? Что, если бы мы могли перейти с любого произвольного сайта вызова в качестве источника на сайт вызова malloc в качестве приемника, пытаясь найти поток данных, в котором выполняются арифметические операции? Мы можем сформулировать запрос на английском языке, в котором мы определяем источник, сначала просматривая все call-сайты всех методов, отфильтровывая те, которые имеют malloc (интересующий приемник) и любую операцию (неинтересную), и затем сделайте источник как возврат (methodReturn) фактических методов вызывающих сайтов. В его случае это «атой» и «getnumber». Затем найдите потоки данных из этих источников в аргумент malloc CallSite в качестве приемника, который выполняет арифметические операции с данными в потоке. Звучит запутанно, но, возможно, Ocular Query поможет объяснить это более программно:

Глазной запрос:

ocular> val sink = cpg.method.callOut.name("malloc").argument
ocular> var source = cpg.method.callOut.nameNot(".*(<operator>|malloc).*").calledMethod.methodReturn 
ocular> sink.reachableBy(source).flows.passes(".*(multiplication|addition).*").p

Результат:

Если это не круто, тогда я ухожу отсюда. Я обычно не одобряю технологии решительно, потому что однажды мы все умрем, и все это будет чьей-то проблемой, но если безопасность должна быть обеспечена должным образом, это то, как вы должны это делать. Нельзя очистить затопленный подвал, откачивая воду ведрами, пока вода капает с потолка. И принятие взлома, на мой взгляд, является худшим способом отговорки.

Вы должны копать глубоко и заменить протекающие трубы, чтобы остановить наводнение.

В следующем блоге я покажу, как с помощью Ocular / Joern создать детектор двойного освобождения всего в трех строчках Scala.

Первоначально опубликовано нашим штатным ученым Сучакрой Шарма на https://suchakra.wordpress.com/2019/10/07/zero-day-snafus-hunting-memory-allocation-bugs/. Вы можете следить за ним @tuxology в Твиттере.