Manual Control Flow Guard in C

Тема в разделе "WASM.ARTICLES", создана пользователем psh3nka, 25 янв 2017.

  1. psh3nka

    psh3nka Active Member

    Публикаций:
    0
    Регистрация:
    21 янв 2017
    Сообщения:
    104
    Последние версии Windows имеют новую функцию противодействия эксплоитам под названием Control Flow Guard (CFG) (хабра). Перед косвенным вызовом функции ― то есть указатели на функции и виртуальные методы ― целевой адрес проверяется по таблице допустимых адресов вызовов. Если адрес не является точкой входа известной функции, то программа прерывается.

    Если в приложении есть уязвимость переполнения буфера (buffer overflow), злоумышленник может использовать ее для перезаписи указателя на функцию и, с помощью вызова через этот указатель, контролировать ход выполнения программы. Это один из способов атаки возвратно-ориентированного программирования (ROP) ― метод эксплуатации уязвимостей, используя который атакующий может выполнить необходимый ему код при наличии в системе защитных технологий, например, технологии, запрещающей исполнение кода с определённых страниц памяти. Метод заключается в том, что атакующий может получить контроль над стеком вызовов, найти в коде последовательности инструкций, выполняющие нужные действия и называемые «гаджетами», выполнить «гаджеты» в нужной последовательности. «Гаджет», обычно, заканчивается инструкцией возврата и располагается в оперативной памяти в существующем коде (в коде программы или в коде разделяемой библиотеки). Атакующий добивается последовательного выполнения гаджетов с помощью инструкций возврата, составляет последовательность гаджетов так, чтобы выполнить желаемые операции, все это достигается без подачи атакующим какого-либо вспомогательного кода.

    Двумя наиболее распространенными техниками предотвращения ROP-атак сегодня являются: рандомизация адресного пространства (ASLR) и stack protectors. Первая рандомизирует базовый адрес исполняемых образов (программы, совместно используемые библиотеки) так, что разметка памяти процесса непредсказуема для атакующего. Адреса в цепочке ROP-атаки зависят от раскладки памяти во время исполнения, поэтому злоумышленник должен также найти и проэксплотировать утечку информации, чтобы обойти ASLR.

    Для стек протекторов компилятор выделяет "осведомителя" (canary) на стеке поверх других распределений стека и устанавливает его в случайное значение для каждого потока. Если в результате переполнения буфера будет переписан указатель на адрес возврата функции, то и "осведомитель" будет тоже переписан. Перед тем как функция вернет управление через адрес возврата, она проверяет "осведомителя". Если он не соответствует известному значению, то программа прерывается.
    [​IMG]
    CFG работает по похожему принципу ― выполняется проверка перед передачей управления по адресу в указателе - за исключением того, что вместо проверки "осведомителя", проверяется сам целевой адрес. Это намного сложнее, и, в отличие от фичи с "осведомителем", по существу, требует координации со стороны платформы. Для верификации мы должны быть в курсе всех действительных целей вызовов, будь то вызов из основной программы или из общих библиотек.

    В то время как (пока?) не широко применяется, но достоен упоминания Clang's SafeStack. Каждый поток получает 2 стека: "безопасный стек" для указателей на адрес возврата и других важных значений, и "небезопасный стек" для буферов и так далее. Переполнения буфера повредят другие буферы, но не перепишет указатели на адрес возврата, тем самым ограничивая ущерб.

    Пример эксплоита

    Рассмотрим этот тривиальный пример на Си ,demo.c:
    Код (C):
    1. int
    2. main(void)
    3. {
    4.   char name[8];
    5.   gets(name);
    6.   printf("Hello, %s.\n", name);
    7.   return 0;
    8. }
    Он считывает имя в буфер и выводит его обратно с приветствием. Будучи тривиальным этот пример не так уж и безобиден. Этот наивный вызов gets() не проверяет границы буфера, предоставляя возможность для его переполнения. Это настолько очевидно, что компилятор и линковщик сообщат нам об этом.

    Для простоты предположим, что программа также содержит опасную функцию.
    Код (C):
    1. void
    2. self_destruct(void)
    3. {
    4.   puts("**** GO BOOM! ****");
    5. }
    Злоумышленник может использовать переполнение буфера, чтобы вызвать эту опасную функцию.

    Чтобы упростить атаку для этой статьи, предполагается, что программа не использует ASLR (т.е. без -fpie/-pie, или с -fno-pie/-no-pie). Для этого конкретного примера я также специально отключил защиту от переполнения буфера. (_FORTIFY_SOURCE and stack protectors).
    Код (Bash):
    1. $ gcc -Os -fno-pie -D_FORTIFY_SOURCE=0 -fno-stack-protector \
    2. -o demo demo.c
    Для начала найдем адрес self_destruct().
    Код (Bash):
    1. $ readelf -a demo | grep self_destruct
    2. 46: 00000000004005c5  10 FUNC  GLOBAL DEFAULT 13 self_destruct
    Пример выполнен на x86-64, так что это 64-bit адрес. Размер буфера "name" 8 байт, и, если посмотреть на дизассм., то можно заметить дополнительные 8 байт сверху стека после буфера (адрес возврата), поэтому мы должны заполнить 16 байт, а потом поместить еще 8 байт, чтобы переписать указатель с адресом возврата self_destruct.
    Код (Bash):
    1. $ echo -ne 'xxxxxxxxyyyyyyyy\xc5\x05\x40\x00\x00\x00\x00\x00' > boom
    2. $ ./demo < boom
    3. Hello, xxxxxxxxyyyyyyyy?@.
    4. **** GO BOOM! ****
    5. Segmentation fault
    С помощью этого ввода я успешно использовал переполнение буфера, чтобы передать управление self_destruct(). Когда main() пытается вернуться в libc, вместо этого она прыгает в опасную функцию, после чего падает, когда функция пытается вернуть значение ― хотя, видимо, система уже самоликвидировалась. Включение стек протектора останавливает этот эксплоит.
    Код (Bash):
    1. $ gcc -Os -fno-pie -D_FORTIFY_SOURCE=0 -fstack-protector \
    2. -o demo demo.c
    3. $ ./demo < boom
    4. Hello, xxxxxxxxaaaaaaaa?@.
    5. *** stack smashing detected ***: ./demo terminated
    6. ======= Backtrace: =========
    7. ... lots of backtrace stuff ...
    Стек протектор успешно блокирует эксплоит. Чтобы обойти эту проблему, я должен бы либо угадать значение "осведомителя" или обнаружить утечку информации, которая раскрывает его.

    Стек протектор преобразовал программу в нечто следующее:
    Код (C):
    1. int
    2. main(void)
    3. {
    4.   long __canary = __get_thread_canary();
    5.   char name[8];
    6.   gets(name);
    7.   printf("Hello, %s.\n", name);
    8.   if (__canary != __get_thread_canary())
    9.   abort();
    10.   return 0;
    11. }
    Тем не менее, на самом деле реализовать протектор стека в C не возможно. Переполнения буфера ― это неопределенное поведение, и "осведомитель" влияет только на переполнение буфера, что позволяет компилятору оптимизировать его.

    Указатели на функции и виртуальные методы

    После того, как атакующему удалось успешно ликвидировать предыдущий пк, высшее руководство поручило выполнять проверку пароля перед всеми процедурами самоуничтожения. Вот как это выглядит теперь:
    Код (C):
    1. void
    2. self_destruct(char *password)
    3. {
    4.   if (strcmp(password, "12345") == 0)
    5.   puts("**** GO BOOM! ****");
    6. }
    Пароль "захардкожен", и это та вещь, которую может сделать только идиот, но мы предполагаем, что это неизвестно злоумышленнику. Тем более, как я покажу позднее, это не будет иметь никакого значения. Высшее руководство также утвердило стек протекторы, поэтому предполагается, что начиная отсюда они будут включены.

    Кроме того, программа немного эволюционировала и теперь использует указатель на функцию для полиморфизма.
    Код (C):
    1. struct greeter {
    2.   char name[8];
    3.   void (*greet)(struct greeter *);
    4. };
    5.  
    6. void
    7. greet_hello(struct greeter *g)
    8. {
    9.   printf("Hello, %s.\n", g->name);
    10. }
    11.  
    12. void
    13. greet_aloha(struct greeter *g)
    14. {
    15.   printf("Aloha, %s.\n", g->name);
    16. }
    Теперь у нас есть объект greeter и указатель на функцию делающий его поведение полиморфным. Считайте, что это хэндмейд виртуальный метод для С. Вот обновленная main():
    Код (C):
    1. int
    2. main(void)
    3. {
    4.   struct greeter greeter = {.greet = greet_hello};
    5.   gets(greeter.name);
    6.   greeter.greet(&greeter);
    7.   return 0;
    8. }
    (В реальной программе что-то еще влияет на greeter и выбирает его собственный указатель на функцию для greet)

    Вместо того, чтобы перезаписывать указатель на адрес возврата, злоумышленник имеет возможность перезаписать указатель на функцию на структуру. Давайте разбирать эксплоит, как мы делали до этого.
    Код (Bash):
    1. $ readelf -a demo | grep self_destruct
    2. 54: 00000000004006a5  10 FUNC  GLOBAL DEFAULT  13 self_destruct
    Мы не знаем пароль, но мы знаем (из дизассемблера), что проверка пароля составляет 16 байт. Атака должна прыгнуть на 16 байт в функцию, пропуская проверки (0x4006a5 + 16 = 0x4006b5).
    Код (Bash):
    1. $ echo -ne 'xxxxxxxx\xb5\x06\x40\x00\x00\x00\x00\x00' > boom
    2. $ ./demo < boom
    3. **** GO BOOM! ****
    Ни стек протектор, ни пароль не спасли нас от эксплоита. Стек протектор защищает только указатель адреса возврата, но не указатель на функцию структуры.

    Вот тут в игру вступает Control Flow Guard. При включенном CFG, компилятор вставляет проверку перед вызовом указателя на функцию greet(). Она должна указывать на начало известной функции, в противном случае она будет прервана, так же как и при стек протекторе. Поскольку середина self_destruct() не является началом функции, то при попытке использовать этот эксплоит программа будет прервана.

    Тем не менее, я сижу на Linux и под него нет CFG (пока?). Так что я буду осуществлять это сам, с помощью ручных проверок.
     
    comrade, _edge и rococo795 нравится это.
  2. psh3nka

    psh3nka Active Member

    Публикаций:
    0
    Регистрация:
    21 янв 2017
    Сообщения:
    104
    Битовая карта адреса функции

    Как описано в PDF прикрепленной в верхней части этой статьи, CFG в Windows, реализована с использованием битовой карты. Каждый бит в битовой карте представляет собой 8 байт памяти. Если эти 8 байт содержат начало функции,то бит будет установлен в единицу. Проверка указателя означает проверку ему соответствующего бита на битовой карте.

    Для моего CFG, я решил сохранить такое же разрешение 8-Byte: нижние три бита целевого адреса будут отброшены. Следующие 24 бита будут использоваться для индексирования в битовой карте. Все остальные биты в указателе будут игнорироваться. 24-битный индекс означает, что размер битовой карты будет всего 2 Mb.

    Этих 24-х бит вполне достаточно для 32-битных систем, но это означает, что на 64-разрядных системах могут быть ложные срабатывания: некоторые адреса не будут представлять собой начало функции, но их бит будет установлен в 1. Это допустимо, особенно потому, что только функции, известные как цели непрямых вызовов будут регистрироваться в таблице, уменьшая процент ложных срабатываний.

    Примечание: полагаясь на то,что преобразование битов указателя в целое число не определено и не является переносимым, но эта реализация будет прекрасно работать везде, где я бы захотел ее использовать.

    Вот параметры CFG. Я сделал их макросами, так что они легко могут быть настроены во время компиляции. cfg_bits является целочисленным типом представляющий массив битовой карты. CFG_RESOLUTION это количество отброшенных битов, так что "3" является зерном 8 байт.
    Код (C):
    1. typedef unsigned long cfg_bits;
    2. #define CFG_RESOLUTION  3
    3. #define CFG_BITS  24
    Дан указатель на функцию f, этот макрос извлекает индекс битовой карты.
    Код (C):
    1. #define CFG_INDEX(f) \
    2.   (((uintptr_t)f >> CFG_RESOLUTION) & ((1UL << CFG_BITS) - 1))
    CFG ― просто массив целых чисел. Обнулим его для инициализации.
    Код (C):
    1. struct cfg {
    2.   cfg_bits bitmap[(1UL << CFG_BITS) / (sizeof(cfg_bits) * CHAR_BIT)];
    3. };
    Функции вручную регистрируются в битовой карте с помощью cfg_register().
    Код (C):
    1. void
    2. cfg_register(struct cfg *cfg, void *f)
    3. {
    4.   unsigned long i = CFG_INDEX(f);
    5.   size_t z = sizeof(cfg_bits) * CHAR_BIT;
    6.   cfg->bitmap[i / z] |= 1UL << (i % z);
    7. }
    Поскольку функции регистрируются во время выполнения, все это полностью совместимо с ASLR. Если ASLR включена, битовая карта будет немного отличаться при каждом запуске. На той же ноте, возможно стоит рандомно поXORить каждый элемент битовой карты, рантайм значение ― это аналог уже известного нам "осведомителя" в стеке. Чтобы усложнить манипуляции битовой картой для атакующего, мы должны дать ему возможность перезаписать его с помощью уязвимости. С другой стороны, битовая карта может быть переключена в режим только для чтения (то есть mprotect()), как только все будет зарегистрировано.

    И, наконец, функция проверки, используемая непосредственно перед непрямыми вызовами. Она убеждается в том, что f ранее был передан cfg_register() (за исключением ложных срабатываний, как уже обсуждалось). Поскольку она будет вызываться часто, то должна быть быстрой и простой.
    Код (C):
    1. void
    2. cfg_check(struct cfg *cfg, void *f)
    3. {
    4.   unsigned long i = CFG_INDEX(f);
    5.   size_t z = sizeof(cfg_bits) * CHAR_BIT;
    6.   if (!((cfg->bitmap[i / z] >> (i % z)) & 1))
    7.   abort();
    8. }
    Вот и все! Теперь допилим main, чтобы можно было использовать все это:
    Код (C):
    1. struct cfg cfg;
    2.  
    3. int
    4. main(void)
    5. {
    6.   cfg_register(&cfg, self_destruct);  // to prove this works
    7.   cfg_register(&cfg, greet_hello);
    8.   cfg_register(&cfg, greet_aloha);
    9.  
    10.   struct greeter greeter = {.greet = greet_hello};
    11.   gets(greeter.name);
    12.   cfg_check(&cfg, greeter.greet);
    13.   greeter.greet(&greeter);
    14.   return 0;
    15. }
    А теперь пробуем эксплоит:
    Код (Bash):
    1. $ ./demo < boom
    2. Aborted
    Обычно self_destruct() не будет зарегистрирована, так как она не является законной целью непрямого вызова, но эксплоит все равно не работает, потому что он вызывается в середине self_destruct(), что не является действительным адресом в битовой карте. Проверка прервет программу, прежде чем она может быть успешно атакована.

    В реальном приложении я бы сделал глобальную CFG битовую карту для всей программы, и определил cfg_check() в заголовке в качестве inline функции.

    Несмотря на возможность реализации на чистом C без помощи дополнительных инструментов, позволить компилятору и платформе управлять Control Flow Guard было бы равнозначно уменьшению сложности и количеству возможных ошибок. Это более подходящее место для его реализации.

    Источник: http://nullprogram.com/blog/2017/01/21/
    Сильно не пинайте. Мой первый перевод. Если нашли ошибки, то прошу под кат или в лс.
     
    comrade и rococo795 нравится это.
  3. yashechka

    yashechka Ростовский фанат Нарвахи

    Публикаций:
    90
    Регистрация:
    2 янв 2012
    Сообщения:
    1.449
    Адрес:
    Россия
  4. _edge

    _edge Well-Known Member

    Публикаций:
    1
    Регистрация:
    29 окт 2004
    Сообщения:
    631
    Адрес:
    Russia
    Если нашли ошибки, то прошу под кат

    Яша, стетей много ) По теме здесь отписываются )
     
  5. psh3nka

    psh3nka Active Member

    Публикаций:
    0
    Регистрация:
    21 янв 2017
    Сообщения:
    104
    Спасибо :)
    Я пока поищу еще что-нибудь интересное по эксплоитам. В твиттере очень удобно следить за актуальными темами, если подписаться на кучу vxеров и ибэшников. Эта статья как раз там подсмотрена)
     
    al79 нравится это.
  6. UbIvItS

    UbIvItS Well-Known Member

    Публикаций:
    0
    Регистрация:
    5 янв 2007
    Сообщения:
    6.074
    что-то это крайне странное высказывание: неопределённое поведение начинается с места, где произошла перезапись буфера/стека, а до этой точки код работает вполне штатно; если же компилятор вносит свои изменения в ключевой код - никто не мешает запретить ему (компилю) так своевольничать.