• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Zydis Static TLS: В поисках LdrpHandleTlsData

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,178
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Приветствую! Я создатель ботнета и рата MonsterV2. Недавно я писал статью, где описывал, почему от использования LoadPE следует по возможности отказаться. Но как быть, если не получается отказаться от LoadPE, например, по религиозным соображениям?

Эта статья призвана помочь крипторам решить проблему статического TLS, но сначала немного матчасти.

Static TLS

Давайте я покажу наглядно, зачем нужен Static TLS:
C++:
#include <iostream>
#include <windows.h>

int main() {
    static HMODULE hNtdll = GetModuleHandleW(L"ntdll");
    std::cout << hNtdll << std::endl;
    return EXIT_SUCCESS;
}
Этот код выводит базовый адрес Ntdll.dll, который перед выводом записывается в статическую переменную. Если мы грузим этот модуль вручную без поддержки статического TLS, то у нас выведется ноль. Всё дело в том, что статические переменные внутри функций на самом деле при компиляции становятся глобальными, а через NtCurrentTeb()->ThreadLocalStoragePointer проверяется, инициализированы ли они. Также программа использует NtCurrentTeb()->ThreadLocalStoragePointer при работе с thread_local (__declspec(thread)) переменными.

Для обработки статического TLS, начиная с Windows Vista, внутри Ntdll лежит функция LdrpHandleTlsData: она парсит IMAGE_DIRECTORY_ENTRY_TLS (9) загружаемого модуля и вызывает для всех потоков NtSetInformationProcess с классом ProcessTlsInformation (Для просмотра ссылки Войди или Зарегистрируйся), который уже в ядре обновляет NtCurrentTeb()->ThreadLocalStoragePointer. Более подробно про работу LdrpHandleTlsData можете прочитать Для просмотра ссылки Войди или Зарегистрируйся. Также я нашёл Для просмотра ссылки Войди или Зарегистрируйся для LdrpHandleTlsData и NtSetInformationProcess с классом ProcessTlsInformation времён Windows Vista. С тех пор код особо не менялся, единственное — соглашение о вызове у функции LdrpHandleTlsData поменялось с __stdcall на __thiscall для Windows 8.1/Windows Server 2012 R2 и выше.

Полный разбор механизма статического TLS тянет на отдельную статью + я оставил достаточно ссылок для изучения его работы, так что повторяться смысла не вижу. Сейчас нам надо разобраться, как отловить TLS для загружаемых вручную модулей.

В поисках LdrpHandleTlsData

LdrpHandleTlsData хэндлит TLS для каждого загружаемого модуля и хранит всю нужную информацию в переменных: LdrpTlsBitmap, LdrpActiveThreadCount и LdrpTlsList, ну и по-хорошему переменную LdrpTlsLock тоже надо бы захватить, чтоб всё было thread-safe.

Чем искать каждую эту переменную и писать ручные костыли, нам проще найти саму функцию LdrpHandleTlsData и вызвать её. Что нам для этого надо? Во-первых, сигнатура:
C++:
// Windows 8.1/Windows Server 2012 R2+
NTSTATUS __thiscall LdrpHandleTlsData(PLDR_DATA_TABLE_ENTRY Module);

// Windows 8 and older
NTSTATUS __stdcall LdrpHandleTlsData(PLDR_DATA_TABLE_ENTRY Module);
О нет, нам что, придётся ещё LDR_DATA_TABLE_ENTRY добавлять в peb->Ldr? Да нет, конечно: для корректной работы у LDR_DATA_TABLE_ENTRY достаточно лишь проиницилизовать поля DllBase и SizeOfImage — к другим эта функция не обращается.

Но как же нам найти адрес самой функции LdrpHandleTlsData, если она не экспортируется? Предлагаете по паттернам искать для каждой версии Винды? или, может, грузить PDB и через него искать? Нет, мы всего лишь продизасмим Ntdll!
Посмотреть вложение 105027
Мы знаем, что внутри LdrpHandleTlsData будет вызываться NtSetInformationProcess с классом ProcessTlsInformation: адрес NtSetInformationProcess мы знаем, так как это экспортируемая функция, значение ProcessTlsInformation равно 35; такой вызов на всю .text секцию Ntdll один — найдём его, найдём и LdrpHandleTlsData.

Для начала получим адрес NtSetInformationProcess
C++:
uintptr_t zydis_tls::getNtSetInformationProcessAddress() {
    return reinterpret_cast<uintptr_t>(&NtSetInformationProcess);
    /* or
    HMODULE hNtdll = GetModuleHandleW(L"ntdll");
    if (!hNtdll) {
        return 0;
    }
    return reinterpret_cast<uintptr_t>(GetProcAddress(hNtdll, "NtSetInformationProcess"));*/
}
Теперь получим .text секцию Ntdll:
C++:
bool zydis_tls::findNtdllTextSection(uintptr_t& addr, size_t& size) {
    HMODULE hNtdll = GetModuleHandleW(L"ntdll");
    if (!hNtdll) {
        return false;
    }
    PIMAGE_NT_HEADERS headers = RtlImageNtHeader(hNtdll);
    if (!headers) {
        return false;
    }

    PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(headers);
    bool ret = false;
    for (WORD i = 0; i < headers->FileHeader.NumberOfSections; ++i) {
        if (!_strnicmp(".text", reinterpret_cast<const char*>(section->Name), IMAGE_SIZEOF_SHORT_NAME)) {
            addr = reinterpret_cast<uintptr_t>(hNtdll) + section->VirtualAddress;
            size = section->Misc.VirtualSize;
            ret = true;
            break;
        }

        ++section;
    }
    return ret;
}
Все нужные адреса у нас есть, приступаем к поиску. Нам нужно пройтись по всей .text секции и записать все адреса, которые являются адресами функций (на которые выполняется call инструкция), а также записать все вызовы NtSetInformationProcess. Для дизасма я буду использовать Zydis, но вы можете это сделать без него, если захотите.
C++:
#include <set>

#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <phnt.h>
#include <Zydis/Zydis.h>

std::set<ZyanU64> zydis_tls::gHrefs{};
std::set<ZyanU64> zydis_tls::gNtSetInformationProcessCalls{};

FARPROC zydis_tls::findLdrpHandleTlsData() {
    uintptr_t sectionStart = 0;
    size_t sectionSize = 0, offset = 0;
    if (!findNtdllTextSection(sectionStart, sectionSize)) {
        return nullptr;
    }
    uintptr_t sectionEnd = sectionStart + sectionSize;
    uintptr_t ntSetInformationProcessAddress = getNtSetInformationProcessAddress();
    if (!ntSetInformationProcessAddress) {
        return nullptr;
    }
    ZydisDecoder decoder;
#ifdef _WIN64
    ZyanStatus status = ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64);
#else
    ZyanStatus status = ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_COMPAT_32, ZYDIS_STACK_WIDTH_32);
#endif
    if (ZYAN_FAILED(status)) {
        return nullptr;
    }
    ZydisDecodedInstruction instruction{};
    ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]{};
    uintptr_t currentAddress = sectionStart;
    while (currentAddress <= sectionEnd && currentAddress >= sectionStart) {
        status = ZydisDecoderDecodeFull(
            &decoder,
            reinterpret_cast<PVOID>(currentAddress), sectionSize - offset,
            &instruction, operands
        );
        if (ZYAN_FAILED(status)) {
            offset++;
            goto endAddr;
        }
        // Check if it's call mnemonic with immediate address value
        if (
            instruction.mnemonic == ZYDIS_MNEMONIC_CALL &&
            instruction.operand_count_visible == 1 &&
            operands[0].type ==ZYDIS_OPERAND_TYPE_IMMEDIATE
        ) {
            ZyanU64 callAddress = 0;
            // Calculate absolute address
            if (operands[0].imm.is_relative) {
                status = ZydisCalcAbsoluteAddress(&instruction, operands, sectionStart + offset, &callAddress);
                if (!ZYAN_SUCCESS(status)) {
                    goto endOffset;
                }
            }
            else {
                callAddress = operands[0].imm.value.u;
            }
            if (callAddress == ntSetInformationProcessAddress) {
                gNtSetInformationProcessCalls.insert(currentAddress);
            }
            gHrefs.insert(callAddress);
        }
endOffset:
        offset += instruction.length;
endAddr:
        currentAddress = sectionStart + offset;
    }
    for (const ZyanU64 addr : gNtSetInformationProcessCalls) {
        if (isProcessTlsInformationCall(
            addr,
            &decoder, &instruction, operands,
            sectionStart, sectionEnd, sectionSize
        )) {
            FARPROC LdrpHandleTlsData = findFunctionStart(addr, sectionStart, sectionEnd);
            if (LdrpHandleTlsData) {
                return LdrpHandleTlsData;
            }
        }
    }
    return nullptr;
}
Собрав все вызовы NtSetInformationProcess (их на всю .text секцию ntdll будет примерно штук 10–15), смотрим, что он вызывается именно с классом ProcessTlsInformation. Для 32 битов это делается легко: отступаем 4 байта назад и смотрим на значение операнда.
Код:
push 23h (ProcessTlsInformation)
push 0FFFFFFFFh (NtCurrentProcess())
Для 64 битов класс записывается в регистр edx. Я сделал костыльно: отступаю назад, пока не найду следующую инструкцию:
Код:
mov edx, 23h (ProcessTlsInformation)
Чтоб не отступать до конца секции, я установил ограничение в 24 байта — этого должно хватить, чтоб найти нужную инструкцию. Но моя реализация для x64 довольно костыльная и, возможно, потребует доработки напильником.
C++:
bool zydis_tls::isProcessTlsInformationCall(
    uintptr_t address,
    const ZydisDecoder* decoder,
    ZydisDecodedInstruction* instruction,
    ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT],
    uintptr_t sectionStart,
    uintptr_t sectionEnd,
    size_t sectionSize
) {
    if (!decoder || !instruction || !address || !sectionStart || !sectionEnd || !sectionSize || !operands) {
        return false;
    }
#ifndef _WIN64
    if ((address - 4) < sectionStart) {
        return false;
    }
    address -= 2; // push instruction size is 2 bytes
    ZyanStatus status = ZydisDecoderDecodeFull(
        decoder,
        reinterpret_cast<PVOID>(address), 2,
        instruction, operands
    );
    // push 0FFFFFFFFh
    if (ZYAN_FAILED(status) || instruction->mnemonic != ZYDIS_MNEMONIC_PUSH) {
        return false;
    }
    address -= 2;
    status = ZydisDecoderDecodeFull(
        decoder,
        reinterpret_cast<PVOID>(address), 2,
        instruction, operands
    );
    if (ZYAN_FAILED(status) || instruction->mnemonic != ZYDIS_MNEMONIC_PUSH) {
        return false;
    }
    //push 23h (ProcessTlsInformation)
    return instruction->operand_count_visible == 1 &&
        operands[0].type == ZYDIS_OPERAND_TYPE_IMMEDIATE &&
        operands[0].imm.value.s == ProcessTlsInformation;
#else
    uintptr_t limitAddress = std::max(address - 24, sectionStart);
    ZyanStatus status = ZYAN_STATUS_SUCCESS;
    size_t offset = address - sectionStart;
    while (address <= sectionEnd && address >= limitAddress) {
        status = ZydisDecoderDecodeFull(
            decoder,
            reinterpret_cast<PVOID>(address), sectionSize - offset,
            instruction, operands
        );
        if (ZYAN_FAILED(status)) {
            goto end;
        }
   
        // mov edx, 23h (ProcessTlsInformation)
        if (
            instruction->mnemonic == ZYDIS_MNEMONIC_MOV &&
            instruction->operand_count_visible == 2 &&
            operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
            operands[0].reg.value == ZYDIS_REGISTER_EDX &&
            operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE &&
            operands[1].imm.value.s == ProcessTlsInformation
            ) {
            return true;
        }
end:
        offset--;
        address = sectionStart + offset;
    }
    return false;
#endif
}
Ну а теперь дело за малым: мы нашли нужный нам вызов внутри тела искомой функции, так что просто отступаем назад, пока не наткнёмся на адрес, на который где-то в Ntdll выполняется call, благо все call'ы мы запомнили.
C++:
FARPROC zydis_tls::findFunctionStart(
    uintptr_t functionBodyAddr,
    uintptr_t sectionStart,
    uintptr_t sectionEnd
) {
    while (functionBodyAddr <= sectionEnd && functionBodyAddr >= sectionStart) {
        if (gHrefs.contains(functionBodyAddr)) {
            return reinterpret_cast<FARPROC>(functionBodyAddr);
        }
        functionBodyAddr--;
    }
    return nullptr;
}
Ну а теперь просто вызываем нашу функцию для загруженного в память модуля:
C++:
// LdrpHandleTlsData has __thiscall calling convention starting from Windows 8.1/Windows Server 2012 R2
static bool needThisCall() {
    ULONG major = 0, minor = 0, build = 0;
    RtlGetNtVersionNumbers(&major, &minor, &build);
    if (major > 6) {
        return true;
    }
    else if (major < 6) {
        return false;
    }
    else {
        return minor >= 3;
    }
}

bool zydis_tls::setupStaticTlsForModule(
    PVOID moduleBase,
    size_t moduleSize
) {
    if (!moduleBase || !moduleSize) {
        return false;
    }
    FARPROC LdrpHandleTlsDataAddress = findLdrpHandleTlsData();
    if (!LdrpHandleTlsDataAddress) {
        return false;
    }
    using STDCALL = NTSTATUS(__stdcall*)(PLDR_DATA_TABLE_ENTRY);
    using THISCALL = NTSTATUS(__thiscall*)(PLDR_DATA_TABLE_ENTRY);
    union {
        STDCALL stdcall;
        THISCALL thiscall;

        FARPROC ptr;
    } LdrpHandleTlsData = {0};
    bool thiscall = needThisCall();
    LdrpHandleTlsData.ptr = LdrpHandleTlsDataAddress;
    LDR_DATA_TABLE_ENTRY entry{};
    entry.DllBase = moduleBase;
    entry.SizeOfImage = moduleSize;
    __try {
        NTSTATUS status = thiscall ? LdrpHandleTlsData.thiscall(&entry) : LdrpHandleTlsData.stdcall(&entry);
        return NT_SUCCESS(status);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        return false;
    }
}

Вместо заключения

Спасибо за прочтение! Я не могу гарантировать стабильную работу моего кода для всех версий Windows, начиная с Висты, для 32 и 64 бита, но мой метод будет лучше, чем поиск по паттернам с вычитанием оффсетов или подгрузка PDB, так что при желании вы уже сможете доработать его напильником, как вам будет удобно. У меня не получилось в паблике найти кода, который искал бы функцию LdrpHandleTlsData таким же образом (но это не значит, что его нет, так что если вы видели, то дайте знать), хотя, как по мне, мой способ гораздо более очевиднее, чем поиск по паттернам. Но я находил китайский код, который ищет LdrpHandleTlsData через строковое упоминание в обработчиках исключений: Для просмотра ссылки Войди или Зарегистрируйся. Я его не тестировал, но, может, вам будет интересно.

Свой же я тестировал только на Windows 10 и 11 (32 и 64 бита) — работает как часы.

Контакты / Contacts
Скрытое содержимое могут видеть только пользователи групп(ы): Администратор, Модератор
 
Activity
So far there's no one here
Сверху Снизу