EXTENDED FLOW GUARD ПОД МИКРОСКОПОМ
18 мая 2021 г.
Microsoft, похоже, постоянно расширяет и развивает свой набор средств защиты, разработанный и реализованный для Windows 10. В этой статье мы рассмотрим новую функцию безопасности под названием eXtended Flow Guard (XFG).
XFG еще не выпущен и не будет частью грядущей версии Windows 10 21H1. Тем не менее, он присутствует в Dev Channel предварительного просмотра для инсайдеров [1]. На данный момент единственное публичное упоминание XFG от Microsoft было в 2019 году на Bluehat Shanghai [2].
Хотя XFG еще не выпущен, тем не менее, можно скомпилировать приложения с XFG с помощью Visual Studio 2019 Preview, ориентируясь на предварительную версию Windows 10. Было выпущено несколько сообщений в блогах о том, как работает XFG [3], но они в основном рассматривают, как XFG могут быть скомпилированы в пользовательское приложение вместо того, чтобы подробно исследовать распространенные сценарии эксплуатации.
Цель этой статьи - разобраться, действительно ли XFG является более безопасной и усиленной версией Control Flow Guard (CFG). Мы начнем с краткого обзора того, как работают CFG и XFG.
SETTING THE BASELINE
CFG был представлен в Windows 10 в 2015 году и претерпел несколько модификаций для снижения уязвимостей в его реализации. По сути, CFG - это решение для обеспечения целостности потока управления (CFI) общего назначения, которое поддерживает bitmap, соответствующую каждой функции, и при вызове определяет, является ли рассматриваемая функция допустимой целью вызова.
Microsoft публично признала, что одним из недостатков CFG является перезапись обратного адреса. Эта проблема будет решена с помощью Intel CET и Shadow Stack. XFG имеет дизайн, очень похожий на CFG, и поэтому должен также полагаться на Shadow Stack для защиты от перезаписи адреса возврата.
Поскольку мы фокусируемся на XFG, а не на отдельных средствах защиты, таких как Intel CET и Shadow Stack, мы рассмотрим перезапись vtable допустимыми целями вызова.
При создании эксплойта браузера распространенным методом получения управления указателем инструкции является перезапись записи в vtable объектов и вызов соответствующего метода. CFG был фактически введен, чтобы смягчить именно этот тип сценария эксплуатации.
Поскольку CFG представляет собой крупномасштабное решение CFI, запись vtable может быть заменена другим указателем на функцию, если это допустимая цель вызова. Это означает, что CFG не рассматривает место вызова, а только цель вызова.
Чтобы понять это немного лучше, давайте рассмотрим API NtCreateFile в ntdll.dll и то, как это проверяется CFG. Проверка CFG выполняется функцией LdrpDispatchUserCallTargetESS, которая ожидает найти адрес NtCreateFile в регистре RAX.
Чтобы смоделировать это, мы можем изменить RAX и RIP в WinDBG и выполнить первую часть LdrpDispatchUserCallTargetESS:
Listing – CFG locating bitmap value
LdrpDispatchUserCallTargetESS использует адрес функции в RAX в качестве индекса в битовой карте CFG и возвращает 64-битное значение.
Затем выполняется битовый тест, чтобы проверить, является ли предоставленный адрес функции допустимой целью вызова. Это делается путем повторного использования адреса функции в качестве индекса:
Listing – Validating the call target
В этом случае мы обнаружили, что NtCreateFile является допустимой целью вызова, а LdrpDispatchUserCallTargetES отправляет ему выполнение с помощью инструкции JMP.
Иногда мы можем использовать это при разработке эксплойтов, перезаписав указатель vtable адресом функции, которая является допустимой целью вызова. Чтобы это работало правильно, нам также необходимо иметь возможность управлять аргументами функции, что выходит за рамки статьи.
Таким образом, CFG называется «крупнозернистым», потому что он учитывает только цель вызова, а не место вызова. Это делает его более уязвимым для обходов.
ПОНИМАНИЕ ХЭШ
Согласно докладу в Bluehat Shanghai, с помощью XFG Microsoft пытается разработать и внедрить более детализированное решение CFI. XFG учитывает как цель вызова, так и место вызова.
Общая концепция заключается в том, что перед каждым использованием XFG компилятор генерирует 55-битный хэш на основе имени функции, количества аргументов, типа аргументов и типа возвращаемого значения. Этот хэш будет встроен в код непосредственно перед вызовом XFG.
Ниже приведен фрагмент кода, взятого из Chakra.dll, который использует XFG в предварительном просмотре для инсайдеров.
Listing – Hash is placed into R10
Как и в случае с CFG, адрес функции, которую вызывают, помещается в RAX.
Функция XFG называется LdrpDispatchUserCallTargetXFG, и мы можем аналогичным образом продемонстрировать, как она работает, вручную установив для RIP значение LdrpDispatchUserCallTargetXFG и RAX в значение NtCreateFile.
Listing – Call site is verified
В последней инструкции, показанной выше, хэш в R10 сравнивается со значением за 8 байтов до цели вызова. Компилятор вставляет сгенерированный хэш непосредственно перед каждой функцией.
Поскольку цель вызова перед вызовом LdrpDispatchUserCallTargetXFG переместит хеш-значение в R10, эти два значения должны совпадать, чтобы разрешить выполнение. Если сравнение прошло успешно, выполнение отправляется с помощью инструкции JMP.
Такое использование хешей времени компиляции намного труднее обойти, чем грубый подход CFG. Перезапись vtable другим указателем на функцию кажется почти невозможной, так как хеш-коллизия очень маловероятна, учитывая использование 55-битного хэша.
FALLING BACK
На данный момент кажется, что XFG успешно сумел смягчить любые попытки перезаписи vtable и намного безопаснее, чем CFG. Однако нам все еще нужно исследовать, что происходит, когда сравнение хешей не удается.
Если выполнение не отправлено в цель вызова, выполняется сегмент кода, показанный ниже:
Listing – Fetching a bitmap value
Мы замечаем, что адрес целевой функции вызова перемещается в R11 и используется в качестве индекса в битовой карте CFG. То же самое 64-битное значение битовой карты CFG перемещается в R11 в конце сегмента кода.
Сначала это может сбивать с толку, но если мы продолжим выполнение, мы также найдем код, показанный ниже:
Listing – Dispatching execution based on the bitmap
Целевой адрес вызова снова используется в качестве индекса для проверки значения битовой карты с помощью битовой проверки. На этом этапе мы должны заметить, что код почти идентичен коду CFG.
После битового теста мы снова обнаруживаем, что NtCreateFile является допустимой целью вызова, и ему отправляется выполнение. Это происходит даже в том случае, если мы не предоставили никакого хеша, и первоначальное сравнение хешей не удалось.
Фактически, XFG возвращается к использованию CFG, если не указан правильный хэш. Из примера, показанного с NtCreateFile, ясно, что XFG не блокирует нас от перезаписи vtable с допустимой для CFG целью вызова.
ВЫВОДЫ
На первый взгляд, XFG кажется гораздо более детализированным решением CFI, чем CFG, и должен смягчить большинство методов эксплуатации, которые пытаются перезаписать vtable.
Однако в реализации XFG Microsoft по существу встроила функцию понижения уровня безопасности для случаев, когда сравнение на основе хешей не удается. Этот переход на более раннюю версию означает, что XFG не более безопасен, чем CFG, и будет подвержен тем же атакам.
Следует отметить, что XFG доступен только в предварительной версии для инсайдеров и, следовательно, может претерпевать изменения перед выпуском. На момент написания этой статьи текущая реализация существовала более шести месяцев.
REFERENCES
(Microsoft, 2021): https://insider.windows.com/en-us/ [1]
(David Weston, 2019): https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RE37dMC [2]
(Connor McGarr, 2020): https://connormcgarr.github.io/examining-xfg/ [3]
(Quarkslab, 2020): https://blog.quarkslab.com/how-the-msvc-compiler-generates-xfg-function-prototype-hashes.html [3]
Перевод by sploitem.
ОРИГИНАЛ
18 мая 2021 г.
Microsoft, похоже, постоянно расширяет и развивает свой набор средств защиты, разработанный и реализованный для Windows 10. В этой статье мы рассмотрим новую функцию безопасности под названием eXtended Flow Guard (XFG).
XFG еще не выпущен и не будет частью грядущей версии Windows 10 21H1. Тем не менее, он присутствует в Dev Channel предварительного просмотра для инсайдеров [1]. На данный момент единственное публичное упоминание XFG от Microsoft было в 2019 году на Bluehat Shanghai [2].
Хотя XFG еще не выпущен, тем не менее, можно скомпилировать приложения с XFG с помощью Visual Studio 2019 Preview, ориентируясь на предварительную версию Windows 10. Было выпущено несколько сообщений в блогах о том, как работает XFG [3], но они в основном рассматривают, как XFG могут быть скомпилированы в пользовательское приложение вместо того, чтобы подробно исследовать распространенные сценарии эксплуатации.
Цель этой статьи - разобраться, действительно ли XFG является более безопасной и усиленной версией Control Flow Guard (CFG). Мы начнем с краткого обзора того, как работают CFG и XFG.
SETTING THE BASELINE
CFG был представлен в Windows 10 в 2015 году и претерпел несколько модификаций для снижения уязвимостей в его реализации. По сути, CFG - это решение для обеспечения целостности потока управления (CFI) общего назначения, которое поддерживает bitmap, соответствующую каждой функции, и при вызове определяет, является ли рассматриваемая функция допустимой целью вызова.
Microsoft публично признала, что одним из недостатков CFG является перезапись обратного адреса. Эта проблема будет решена с помощью Intel CET и Shadow Stack. XFG имеет дизайн, очень похожий на CFG, и поэтому должен также полагаться на Shadow Stack для защиты от перезаписи адреса возврата.
Поскольку мы фокусируемся на XFG, а не на отдельных средствах защиты, таких как Intel CET и Shadow Stack, мы рассмотрим перезапись vtable допустимыми целями вызова.
При создании эксплойта браузера распространенным методом получения управления указателем инструкции является перезапись записи в vtable объектов и вызов соответствующего метода. CFG был фактически введен, чтобы смягчить именно этот тип сценария эксплуатации.
Поскольку CFG представляет собой крупномасштабное решение CFI, запись vtable может быть заменена другим указателем на функцию, если это допустимая цель вызова. Это означает, что CFG не рассматривает место вызова, а только цель вызова.
Чтобы понять это немного лучше, давайте рассмотрим API NtCreateFile в ntdll.dll и то, как это проверяется CFG. Проверка CFG выполняется функцией LdrpDispatchUserCallTargetESS, которая ожидает найти адрес NtCreateFile в регистре RAX.
Чтобы смоделировать это, мы можем изменить RAX и RIP в WinDBG и выполнить первую часть LdrpDispatchUserCallTargetESS:
Код:
0:019> r rip = ntdll!LdrpDispatchUserCallTargetES
0:019> r rax = ntdll!NtCreateFile
ntdll!LdrpDispatchUserCallTargetES:
00007ffb`27dd11d0 4c8b1dd1910f00 mov r11,qword ptr [ntdll!LdrSystemDllInitBlock+0xb8 (00007ffb`27eca3a8)] ds:00007ffb`27eca3a8=00007df5f77d0000
0:019> p
ntdll!LdrpDispatchUserCallTargetES+0x7:
00007ffb`27dd11d7 4c8bd0 mov r10,rax
0:019>
ntdll!LdrpDispatchUserCallTargetES+0xa:
00007ffb`27dd11da 49c1ea09 shr r10,9
0:019>
ntdll!LdrpDispatchUserCallTargetES+0xe:
00007ffb`27dd11de 4f8b1cd3 mov r11,qword ptr [r11+r10*8] ds:00007ff5`e41c7868=1111144444444444
LdrpDispatchUserCallTargetESS использует адрес функции в RAX в качестве индекса в битовой карте CFG и возвращает 64-битное значение.
Затем выполняется битовый тест, чтобы проверить, является ли предоставленный адрес функции допустимой целью вызова. Это делается путем повторного использования адреса функции в качестве индекса:
Код:
ntdll!LdrpDispatchUserCallTargetES+0x12:
00007ffb`27dd11e2 4c8bd0 mov r10,rax
0:019>
ntdll!LdrpDispatchUserCallTargetES+0x15:
00007ffb`27dd11e5 49c1ea03 shr r10,3
0:019>
ntdll!LdrpDispatchUserCallTargetES+0x19:
00007ffb`27dd11e9 a80f test al,0Fh
0:019>
ntdll!LdrpDispatchUserCallTargetES+0x1b:
00007ffb`27dd11eb 7509 jne ntdll!LdrpDispatchUserCallTargetES+0x26 (00007ffb`27dd11f6) [br=0]
0:019>
ntdll!LdrpDispatchUserCallTargetES+0x1d:
00007ffb`27dd11ed 4d0fa3d3 bt r11,r10
0:019>
ntdll!LdrpDispatchUserCallTargetES+0x21:
00007ffb`27dd11f1 731b jae ntdll!LdrpDispatchUserCallTargetES+0x3e (00007ffb`27dd120e) [br=0]
0:019>
ntdll!LdrpDispatchUserCallTargetES+0x23:
00007ffb`27dd11f3 48ffe0 jmp rax {ntdll!NtCreateFile (00007ffb`27de1ab0)}
В этом случае мы обнаружили, что NtCreateFile является допустимой целью вызова, а LdrpDispatchUserCallTargetES отправляет ему выполнение с помощью инструкции JMP.
Иногда мы можем использовать это при разработке эксплойтов, перезаписав указатель vtable адресом функции, которая является допустимой целью вызова. Чтобы это работало правильно, нам также необходимо иметь возможность управлять аргументами функции, что выходит за рамки статьи.
Таким образом, CFG называется «крупнозернистым», потому что он учитывает только цель вызова, а не место вызова. Это делает его более уязвимым для обходов.
ПОНИМАНИЕ ХЭШ
Согласно докладу в Bluehat Shanghai, с помощью XFG Microsoft пытается разработать и внедрить более детализированное решение CFI. XFG учитывает как цель вызова, так и место вызова.
Общая концепция заключается в том, что перед каждым использованием XFG компилятор генерирует 55-битный хэш на основе имени функции, количества аргументов, типа аргументов и типа возвращаемого значения. Этот хэш будет встроен в код непосредственно перед вызовом XFG.
Ниже приведен фрагмент кода, взятого из Chakra.dll, который использует XFG в предварительном просмотре для инсайдеров.
Код:
.text:000000000002E8CC mov rcx, [rbx+18h]
.text:000000000002E8D0 and [rsp+48h+var_18], 0
.text:000000000002E8D5 mov rax, [rcx]
.text:000000000002E8D8 mov r10, 0F8D8BEB272D33870h
.text:000000000002E8E2 mov rdx, [rsp+48h+arg_8]
.text:000000000002E8E7 lea r9, [rsp+48h+var_18]
.text:000000000002E8EC mov rax, [rax+18h]
.text:000000000002E8F0 mov r8d, 4
.text:000000000002E8F6 call cs:__guard_xfg_dispatch_ical
Как и в случае с CFG, адрес функции, которую вызывают, помещается в RAX.
Функция XFG называется LdrpDispatchUserCallTargetXFG, и мы можем аналогичным образом продемонстрировать, как она работает, вручную установив для RIP значение LdrpDispatchUserCallTargetXFG и RAX в значение NtCreateFile.
Код:
0:019> r rip = ntdll!LdrpDispatchUserCallTargetXFG
0:019> r rax = ntdll!NtCreateFile
0:019> p
ntdll!LdrpDispatchUserCallTargetXFG+0x4:
00007ffb`27dd1234 a80f test al,0Fh
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x6:
00007ffb`27dd1236 750f jne ntdll!LdrpDispatchUserCallTargetXFG+0x17 (00007ffb`27dd1247) [br=0]
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x8:
00007ffb`27dd1238 66a9ff0f test ax,0FFFh
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0xc:
00007ffb`27dd123c 7409 je ntdll!LdrpDispatchUserCallTargetXFG+0x17 (00007ffb`27dd1247) [br=0]
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0xe:
00007ffb`27dd123e 4c3b50f8 cmp r10,qword ptr [rax-8] ds:00007ffb`27de1aa8=0000000000841f0f
В последней инструкции, показанной выше, хэш в R10 сравнивается со значением за 8 байтов до цели вызова. Компилятор вставляет сгенерированный хэш непосредственно перед каждой функцией.
Поскольку цель вызова перед вызовом LdrpDispatchUserCallTargetXFG переместит хеш-значение в R10, эти два значения должны совпадать, чтобы разрешить выполнение. Если сравнение прошло успешно, выполнение отправляется с помощью инструкции JMP.
Такое использование хешей времени компиляции намного труднее обойти, чем грубый подход CFG. Перезапись vtable другим указателем на функцию кажется почти невозможной, так как хеш-коллизия очень маловероятна, учитывая использование 55-битного хэша.
FALLING BACK
На данный момент кажется, что XFG успешно сумел смягчить любые попытки перезаписи vtable и намного безопаснее, чем CFG. Однако нам все еще нужно исследовать, что происходит, когда сравнение хешей не удается.
Если выполнение не отправлено в цель вызова, выполняется сегмент кода, показанный ниже:
Код:
ntdll!LdrpDispatchUserCallTargetXFG+0x17:
00007ffb`27dd1247 4c8bd8 mov r11,rax
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x1a:
00007ffb`27dd124a 48c1e008 shl rax,8
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x1e:
00007ffb`27dd124e 418ac2 mov al,r10b
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x21:
00007ffb`27dd1251 48c1c808 ror rax,8
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x25:
00007ffb`27dd1255 49c1eb09 shr r11,9
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x29:
00007ffb`27dd1259 49c1e303 shl r11,3
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x2d:
00007ffb`27dd125d 4c031d44910f00 add r11,qword ptr [ntdll!LdrSystemDllInitBlock+0xb8 (00007ffb`27eca3a8)] ds:00007ffb`27eca3a8=00007df5f77d0000
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x34:
00007ffb`27dd1264 4d8b1b mov r11,qword ptr [r11] ds:00007ff5`e41c7868=1111144444444444
Мы замечаем, что адрес целевой функции вызова перемещается в R11 и используется в качестве индекса в битовой карте CFG. То же самое 64-битное значение битовой карты CFG перемещается в R11 в конце сегмента кода.
Сначала это может сбивать с толку, но если мы продолжим выполнение, мы также найдем код, показанный ниже:
Код:
ntdll!LdrpDispatchUserCallTargetXFG+0x37:
00007ffb`27dd1267 48c1c803 ror rax,3
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x3b:
00007ffb`27dd126b 448ad0 mov r10b,al
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x3e:
00007ffb`27dd126e 48c1c003 rol rax,3
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x42:
00007ffb`27dd1272 a80f test al,0Fh
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x44:
00007ffb`27dd1274 7511 jne ntdll!LdrpDispatchUserCallTargetXFG+0x57 (00007ffb`27dd1287) [br=0]
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x46:
00007ffb`27dd1276 4d0fa3d3 bt r11,r10
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x4a:
00007ffb`27dd127a 732b jae ntdll!LdrpDispatchUserCallTargetXFG+0x77 (00007ffb`27dd12a7) [br=0]
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x4c:
00007ffb`27dd127c 48c1e008 shl rax,8
0:019> p
ntdll!LdrpDispatchUserCallTargetXFG+0x50:
00007ffb`27dd1280 48c1e808 shr rax,8
0:019>
ntdll!LdrpDispatchUserCallTargetXFG+0x54:
00007ffb`27dd1284 48ffe0 jmp rax {ntdll!NtCreateFile (00007ffb`27de1ab0)}
Целевой адрес вызова снова используется в качестве индекса для проверки значения битовой карты с помощью битовой проверки. На этом этапе мы должны заметить, что код почти идентичен коду CFG.
После битового теста мы снова обнаруживаем, что NtCreateFile является допустимой целью вызова, и ему отправляется выполнение. Это происходит даже в том случае, если мы не предоставили никакого хеша, и первоначальное сравнение хешей не удалось.
Фактически, XFG возвращается к использованию CFG, если не указан правильный хэш. Из примера, показанного с NtCreateFile, ясно, что XFG не блокирует нас от перезаписи vtable с допустимой для CFG целью вызова.
ВЫВОДЫ
На первый взгляд, XFG кажется гораздо более детализированным решением CFI, чем CFG, и должен смягчить большинство методов эксплуатации, которые пытаются перезаписать vtable.
Однако в реализации XFG Microsoft по существу встроила функцию понижения уровня безопасности для случаев, когда сравнение на основе хешей не удается. Этот переход на более раннюю версию означает, что XFG не более безопасен, чем CFG, и будет подвержен тем же атакам.
Следует отметить, что XFG доступен только в предварительной версии для инсайдеров и, следовательно, может претерпевать изменения перед выпуском. На момент написания этой статьи текущая реализация существовала более шести месяцев.
REFERENCES
(Microsoft, 2021): https://insider.windows.com/en-us/ [1]
(David Weston, 2019): https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RE37dMC [2]
(Connor McGarr, 2020): https://connormcgarr.github.io/examining-xfg/ [3]
(Quarkslab, 2020): https://blog.quarkslab.com/how-the-msvc-compiler-generates-xfg-function-prototype-hashes.html [3]
Перевод by sploitem.
ОРИГИНАЛ