Control-flow integrity

Перейти к навигацииПерейти к поиску

Control-flow integrity (CFI) — общее название методов в компьютерной безопасности, направленных на то, чтобы ограничить возможные пути исполнения программы в пределах заранее предсказанного графа потока управления для повышения её безопасности[1]. CFI усложняет для злоумышленника захват контроля над исполнением программы, делая невозможными некоторые способы переиспользования уже существующих частей машинного кода. К похожим техникам относятся code-pointer separation (CPS) и code-pointer integrity (CPI)[2][3].

Поддержка CFI присутствует в компиляторах Clang[4] и GCC[5], а также в виде Control Flow Guard[6] и Return Flow Guard[7] от Microsoft и Reuse Attack Protector[8] от PaX Team.

История

Изобретение способов защиты от исполнения произвольного кода, таких как Data Execution Prevention и NX-бит, привело к возникновению новых методов, позволяющих получить контроль над программой (например, возвратно-ориентированное программирование)[8]. В 2003 году PaX Team опубликовала документ с описанием возможных ситуаций, приводящих к взлому программы, и идей по защите от них[8][9]. В 2005 году группа исследователей из Microsoft формализовала эти идеи и ввела термин Control-flow Integrity для обозначения методов защиты от изменения изначального потока управления программы. В дополнение к этому авторы предложили метод инструментации уже скомпилированного машинного кода[1].

Впоследствии исследователи, основываясь на идее CFI, предложили множество различных способов, позволяющих повысить устойчивость программы к атакам. Описанные подходы не получили широкого распространения по причинам, включающим большое замедление программы или необходимость дополнительной информации (например, полученной с помощью профилирования)[10].

В 2014 году команда исследователей из Google опубликовала работу, в которой рассматривалась реализация CFI для промышленных компиляторов GCC и LLVM для инструментации программ на C++. Официальная поддержка CFI была добавлена в 2014 году в GCC 4.9.0[5][11] и в 2015 году в Clang 3.7[12][13]. Компания Microsoft выпустила Control Flow Guard в 2014 году для Windows 8.1, добавив поддержку со стороны операционной системы и в Visual Studio 2015[6].

Описание

На схеме показано, как изменяется управление при вызове подпрограмм и возврате из них. Инструкции call и icall соответствуют прямым переходам, инструкция retn — обратному.
Граф вызовов. Слева показан наивный подход к построению — по указателю может быть вызвана любая функция. Справа — анализ с учётом типов.

При наличии в коде программы косвенных переходов потенциально возникает возможность передать управление на любой адрес, по которому может располагаться команда (например, на x86 это будет любой возможный адрес, так как минимальная длина команды равна одному байту[14]). Если злоумышленник сможет каким-либо образом модифицировать значение, по которому передаётся управление при выполнении инструкции перехода, то он сможет переиспользовать существующий программный код для своих нужд.

В реальных программах нелокальные переходы обычно ведут к началу функций (например, если используется команда вызова процедуры) или же к инструкции, следующей за инструкцией вызова (возврат из процедуры). Первый тип переходов является прямым (англ. forward-edge) переходом, так как на графе потока управления он будет обозначаться прямой дугой. Второй тип называется обратным (англ. back-edge) переходом, по аналогии с первым — дуга, соответствующая переходу, будет обратной[15].

Прямые переходы

Для прямых переходов количество возможных адресов, на которые может быть передано управление, будет соответствовать количеству функций в программе. Также при учёте системы типов и семантики языка программирования, на котором написан исходный код, возможны дополнительные ограничения[16]. Например, в языке C++ в корректной программе указатель на функцию, используемый при косвенном вызове, должен содержать адрес функции с таким же типом, как и у самого указателя[17].

Один из способов реализации control-flow integrity для прямых переходов заключается в том, что можно проанализировать программу и определить множество легальных адресов для различных инструкций перехода[1]. Для построения такого множества обычно применяется статический анализ кода на каком-либо уровне абстракции (на уровне исходного кода, внутреннего представления анализатора или машинного кода[1][10]). Затем с помощью полученной информации рядом с инструкциями косвенного перехода вставляется код для проверки, соответствует ли адрес, полученный во время исполнения, вычисленному статически. При расхождении программа, обычно, аварийно завершается, хотя реализации позволяют настроить поведение в случае нарушения предсказанного потока управления[18][19]. Таким образом, граф потока управления ограничивается только теми рёбрами (вызовами функций) и вершинами (точками входа в функции)[1][16][20], которые вычисляются во время статического анализа, поэтому при попытке модифицировать указатель, использующийся для косвенного перехода, злоумышленник потерпит неудачу.

Данный способ позволяет предотвратить jump-oriented programming[21] и call-oriented programming[22], так как последние активно используют прямые косвенные переходы.

Обратные переходы

Для обратных переходов возможно несколько подходов к реализации CFI[8].

Первый подход основывается на тех же предположениях, что и CFI для прямых переходов, то есть на возможности вычислить адреса возврата из функции[23].

Второй подход заключается в особом обращении с адресом возврата. Помимо того, чтобы просто сохранять его на стек, он сохраняется, возможно с некоторыми модификациями, ещё и в специально выделенное для него место (например, в один из регистров процессора). Также перед инструкцией возврата добавляется код, восстанавливающий адрес возврата и сверяющий его с тем, который лежит на стеке[8].

Третий подход требует дополнительную поддержку от аппаратной части. Совместно с CFI используется теневой стек (англ. shadow stack) — специальная недоступная злоумышленнику область памяти, в которую сохраняются адреса возврата при вызове функций[24].

При реализации схем CFI для обратных переходов возможно предотвратить атаку возврата в библиотеку и возвратно-ориентированное программирование (англ. return-oriented programming), основанные на изменении адреса возврата на стеке[23].

Примеры

В данном разделе будут рассмотрены примеры реализаций control-flow integrity.

Clang Indirect Function Call Checking

Indirect Function Call Checking (IFCC) включает в себя проверки косвенных переходов в программе за исключением некоторых «особенных» переходов, таких как вызовы виртуальных функций. При построении множества адресов, по которым может произойти переход, учитывается тип функции. Благодаря этому возможно предотвратить не только использование неправильных значений, указывающих не на начало функции, но и неверное приведение типов в исходном коде. Для включения проверок в компиляторе есть опция -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 here

Clang Forward-Edge CFI for Virtual Calls

Этот метод направлен на контроль целостности виртуальных вызовов в языке 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
              ^

Примечания

  1. 1 2 3 4 5 Martín Abadi, Mihai Budiu, Úlfar Erlingsson, Jay Ligatti. Control-flow Integrity // Proceedings of the 12th ACM Conference on Computer and Communications Security. — New York, NY, USA: ACM, 2005. — С. 340—353. — ISBN 1595932267. — doi:10.1145/1102120.1102165.
  2. Volodymyr Kuznetsov, László Szekeres, Mathias Payer, George Candea, R. Sekar. Code-pointer Integrity // Proceedings of the 11th USENIX Conference on Operating Systems Design and Implementation. — Berkeley, CA, USA: USENIX Association, 2014. — С. 147—163. — ISBN 9781931971164.
  3. On differences between the CFI, CPS, and CPI properties (англ.). nebelwelt.net. Дата обращения: 22 декабря 2017. Архивировано 22 декабря 2017 года.
  4. 1 2 Control Flow Integrity — Clang 5 documentation. releases.llvm.org. Дата обращения: 22 декабря 2017. Архивировано 23 декабря 2017 года.
  5. 1 2 vtv - GCC Wiki. gcc.gnu.org. Дата обращения: 22 декабря 2017. Архивировано 11 июля 2017 года.
  6. 1 2 Control Flow Guard (Windows) (англ.). msdn.microsoft.com. Дата обращения: 22 декабря 2017. Архивировано 22 декабря 2017 года.
  7. Return Flow Guard – Tencent's Xuanwu Lab (англ.). xlab.tencent.com. Дата обращения: 22 декабря 2017. Архивировано 23 декабря 2017 года.
  8. 1 2 3 4 5 grsecurity (англ.). www.grsecurity.net. Дата обращения: 22 декабря 2017. Архивировано 17 февраля 2018 года.
  9. [1] Архивная копия от 5 августа 2017 на Wayback Machine PaX future
  10. 1 2 3 Caroline Tice, Tom Roeder, Peter Collingbourne, Stephen Checkoway, Úlfar Erlingsson. Enforcing Forward-edge Control-flow Integrity in GCC & LLVM // Proceedings of the 23rd USENIX Conference on Security Symposium. — Berkeley, CA, USA: USENIX Association, 2014. — С. 941—955. — ISBN 9781931971157.
  11. GCC 4.9 Release Series - GNU Project - Free Software Foundation (FSF) (англ.). gcc.gnu.org. Дата обращения: 22 декабря 2017. Архивировано 15 января 2018 года.
  12. Clang 3.7 Release Notes — Clang 3.7 documentation. releases.llvm.org. Дата обращения: 22 декабря 2017. Архивировано 26 ноября 2017 года.
  13. LLVM releases. releases.llvm.org. Дата обращения: 22 декабря 2017. Архивировано 15 декабря 2017 года.
  14. Intel® 64 and IA-32 Architectures Software Developer Manuals | Intel® Software (англ.). software.intel.com. Дата обращения: 22 декабря 2017. Архивировано 25 декабря 2017 года.
  15. Security - WebAssembly. webassembly.org. Дата обращения: 22 декабря 2017. Архивировано 23 декабря 2017 года.
  16. 1 2 Ахо, Альфред В.; Сети, Рави; Ульман, Джеффри Д. Компиляторы — принципы, технологии, инструменты, 2-е изд. — Вильямс. — 2008. — С. 1062—1066. — ISBN 978-5-8459-1349-4.
  17. ISO/IEC 14882:2014 — Information technology — Programming languages — C++. — ISO. — 2014. — С. 105. Архивировано 29 апреля 2016 года.
  18. Vtable Verification - User's Guide. docs.google.com. Дата обращения: 22 декабря 2017. Архивировано 12 июня 2019 года.
  19. Control Flow Integrity — Clang 5 documentation. releases.llvm.org. Дата обращения: 22 декабря 2017. Архивировано 23 декабря 2017 года.
  20. Muchnick, Steven S. Advanced Compiler Design and Implementation. — Morgan Kaufmann Publishers, 1997. — С. 609—618. — ISBN 1-55860-320-4.
  21. Tyler Bletsch, Xuxian Jiang, Vince W. Freeh, Zhenkai Liang. Jump-oriented Programming: A New Class of Code-reuse Attack // Proceedings of the 6th ACM Symposium on Information, Computer and Communications Security. — New York, NY, USA: ACM, 2011. — С. 30—40. — ISBN 9781450305648. — doi:10.1145/1966913.1966919.
  22. AliAkbar Sadeghi, Salman Niksefat, Maryam Rostamipour. Pure-Call Oriented Programming (PCOP): chaining the gadgets using call instructions (англ.) // Journal of Computer Virology and Hacking Techniques. — 2017-05-15. — P. 1—18. — ISSN 2263-8733. — doi:10.1007/s11416-017-0299-1. Архивировано 22 декабря 2017 года.
  23. 1 2 RAP: RIP ROP — Reuse Attack Protector. PaX Team. Дата обращения: 22 декабря 2017. Архивировано из оригинала 20 мая 2020 года.
  24. Control-flow Enforcement Technology Preview. Intel Developer Zone. Дата обращения: 22 декабря 2017. Архивировано 14 августа 2017 года.
  25. Control Flow Integrity Design Documentation — Clang 5 documentation. releases.llvm.org. Дата обращения: 22 декабря 2017. Архивировано 23 декабря 2017 года.

Литература

Книги
Статьи

Ссылки