Предисловие
Такие языки, как 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 в Твиттере.