Control-flow integrity
Control-flow integrity (CFI) — совокупность методов в компьютерной безопасности, предназначенных для ограничения возможных путей исполнения программы исключительно заранее определённым графом потока управления с целью повышения защищённости[1]. Механизмы control-flow integrity затрудняют злоумышленнику захват управления выполнением программы, предотвращая целый ряд атак повторного использования кода. К близким технологиям относятся «разделение указателей на код» и «целостность указателей на код»[2][3].
Поддержка control-flow integrity реализована в компиляторах Clang[4] и GCC[5], а также в виде Guard потока управления (англ. Control Flow Guard)[6] и Guard возврата управления (англ. Return Flow Guard)[7] от Microsoft, а также Reuse Attack Protector (RAP)[8] от PaX Team.
История
Появление методов предотвращения исполнения произвольного кода, таких как Data Execution Prevention и NX-бит, привело к развитию новых атак, позволяющих перехватить управление программой (например, возвратно-ориентированное программирование)[8]. В 2003 году команда PaX опубликовала описание типовых сценариев эксплуатации уязвимостей и предложила собственные идеи по их предотвращению[8][9]. В 2005 году исследователи из Microsoft формализовали эти подходы и ввели термин control-flow integrity для обозначения технологий защиты первичного потока управления программой, а также предложили методику инструментирования уже скомпилированного машинного кода[1].
В дальнейшем основываясь на принципах control-flow integrity, были предложены многочисленные методы повышения устойчивости ПО к атакам. Однако широкого распространения такие подходы не получили из-за значительного замедления исполнения либо необходимости привлечения дополнительной информации (например, профилирования)[10].
В 2014 году команда исследователей из Google представила реализацию control-flow integrity для промышленных компиляторов GCC и LLVM, способных инструментировать программы на C++. Официальная поддержка CFI была включена в GCC 4.9.0 в 2014 году[5][11] и в Clang 3.7 в 2015 году[12][13]. Microsoft выпустила Guard потока управления (Control Flow Guard) для Windows 8.1 и реализовала поддержку CFI в Visual Studio 2015[6].
Описание
Наличие косвенных переходов в программном коде создаёт риск передачи управления на любой адрес, где начинается команда (например, в архитектуре x86 — любой байт как начало команды[14]). Если злоумышленник сможет модифицировать адрес, используемый командой перехода, он сможет использовать существующий код для своих целей.
В типичных программах такие нелокальные переходы ведут к началу функции (при вызове процедуры) или к инструкции после вызова (возврат из процедуры). Первый вариант — прямой переход, отражённый прямой дугой на графе потока управления; второй — обратный переход по аналогии отмечается обратной дугой[15].
Прямые переходы
Для прямых переходов допустимые адреса ограничены набором функций программы. С учётом системы типов и семантики языка программирования это множество может быть дополнительно сокращено[16]. Например, в C++ по стандарту корректный указатель на функцию, используемый при косвенном вызове, обязан содержать адрес функции с совместимым типом[17].
Методы реализации control-flow integrity для прямых переходов основаны на выделении множества легальных адресов для каждой косвенной инструкции[1]. Для этого используется статический анализ кода на различных уровнях: исходный, внутреннее представление или машинный[1][10]. На основании анализа, к инструкциям косвенного перехода добавляется проверочный код — он сравнивает вычисленный во время исполнения адрес с предвычисленным набором. При расхождении выполнение или аварийно прерывается, или обрабатывается по заданной политике[18][19]. В результате граф потока управления ограничивается только разрешёнными рёбрами и вершинами[1][16][20], поэтому попытка подмены указателя в целях атаки становится безуспешной.
Такой подход эффективно блокирует техники возвратно-ориентированного программирования на прямых переходах: jump-oriented programming[21] и call-oriented programming[22].
Обратные переходы
Для обратных переходов применяются различные методики[8].
Один из подходов аналогичен методу для прямых переходов — статически вычисляются допустимые адреса возврата[23].
Во втором подходе к адресу возврата применяется особая обработка: кроме сохранения в стек, он дополнительно помещается (в возможно изменённом виде) в специальную область памяти или, например, в регистр процессора; перед возвратом производится сравнение оригинала и копии[8].
Третий способ требует аппаратной поддержки и использует так называемый «теневой стек» — особую, непривилегированную область памяти для сохранения адресов возврата при вызовах[24].
Методы control-flow integrity для обратных переходов позволяют блокировать атаки возврата в библиотеку и возвратно-ориентированное программирование путём контроля адреса возврата на стеке[23].
Примеры
Ниже приведены примеры реализации control-flow integrity.
Clang Indirect Function Call Checking
Indirect Function Call Checking (IFCC) обеспечивает контроль косвенных переходов в программе (за исключением некоторых особых, например, вызова виртуальных функций). При построении допустимых адресов учитывается тип функции, что препятствует не только выполнению перехода за пределы функций, но и некорректному приведению типов в коде. Для включения проверки в Clang используется опция -fsanitize=cfi-icall[4].
// clang-ifcc.c
#include <stdio.h>
int sum(int x, int y) {
return x + y;
}
int dbl(int x) {
return x + x;
}
void call_fn(int (*fn)(int)) {
printf("Result value: %d\n", (*fn)(42));
}
void erase_type(void *fn) {
// Поведение не определено, если динамический тип fn не совпадает с int (*)(int).
call_fn(fn);
}
int main() {
// При вызове erase_type теряется статическая информация от типе.
erase_type(sum);
return 0;
}
Компиляция без проверок позволяет программе работать без ошибок, но результат неопределён и может различаться между запусками:
$ clang -Wall -Wextra clang-ifcc.c
$ ./a.out
Result value: 1388327490С применённой опцией, контроль приводит к аварийному завершению исполнения при вызове call_fn:
$ clang -flto -fvisibility=hidden -fsanitize=cfi -fno-sanitize-trap=all clang-ifcc.c
$ ./a.out
clang-ifcc.c:12:32: runtime error: control flow integrity check for type 'int (int)' failed during indirect function call
(./a.out+0x427a20): note: (unknown) defined hereClang Forward-Edge CFI для виртуальных вызовов
Этот метод контролирует интегритет виртуальных вызовов в C++. Для каждой иерархии классов с виртуальными функциями строятся битовые маски, отражающие допустимые функции по каждому статическому типу. Модификация таблицы виртуальных функций (например, при неправильном приведении типа либо ошибке памяти) приводит к невозможности совпадения динамического типа с предсказанными значениями[10][25].
// virtual-calls.cpp
#include <cstdio>
struct B {
virtual void foo() = 0;
virtual ~B() {}
};
struct D : public B {
void foo() override {
printf("Right function\n");
}
};
struct Bad : public B {
void foo() override {
printf("Wrong function\n");
}
};
int main() {
Bad bad; // Стандарт C++ позволяет делать приведение типов по следующей схеме:
B &b = static_cast<B&>(bad); // Derived1 -> Base -> Derived2.
D &normal = static_cast<D&>(b); // В результате динамический тип объекта normal
normal.foo(); // будет bad и вызовется неправильная функция.
return 0;
}
Компиляция без проверок:
$ clang++ -std=c++11 virtual-calls.cpp
$ ./a.out
Wrong functionВ данном случае вместо метода foo класса D вызывается foo из класса Bad. Активированный контроль с помощью -fsanitize=cfi-vcall предотвращает ошибку:
$ clang++ -std=c++11 -Wall -flto -fvisibility=hidden -fsanitize=cfi-vcall -fno-sanitize-trap=all virtual-calls.cpp
$ ./a.out
virtual-calls.cpp:24:3: runtime error: control flow integrity check for type 'D' failed during virtual call (vtable address 0x000000431ce0)
0x000000431ce0: note: vtable is of type 'Bad'
00 00 00 00 30 a2 42 00 00 00 00 00 e0 a1 42 00 00 00 00 00 60 a2 42 00 00 00 00 00 00 00 00 00
^Примечания
Литература
- Muchnick, Steven S. Advanced Compiler Design and Implementation : [англ.]. — Morgan Kaufmann Publishers, 1997. — ISBN 1-55860-320-4.
- Ахо, Альфред В.; Сети, Рави; Ульман, Джеффри Д. Компиляторы — принципы, технологии, инструменты, 2-е изд. : [рус.]. — Вильямс, 2008. — ISBN 978-5-8459-1349-4.
- Martín Abadi, Mihai Budiu, Úlfar Erlingsson, Jay Ligatti (2005). “Control-flow Integrity”. Proceedings of the 12th ACM Conference on Computer and Communications Security [англ.]. ACM: 340—353. DOI:10.1145/1102120.1102165. ISBN 1595932267. Дата обращения 2017-12-22.
- Caroline Tice, Tom Roeder, Peter Collingbourne, Stephen Checkoway, Úlfar Erlingsson (2014). “Enforcing Forward-edge Control-flow Integrity in GCC & LLVM”. Proceedings of the 23rd USENIX Conference on Security Symposium [англ.]. USENIX Association: 941—955. ISBN 9781931971157. Дата обращения 2017-12-22.