Admin
Администратор
Zydis Static TLS: В поисках LdrpHandleTlsData
Эта статья призвана помочь крипторам решить проблему статического 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;
}
Для обработки статического TLS, начиная с Windows Vista, внутри Ntdll лежит функция LdrpHandleTlsData: она парсит IMAGE_DIRECTORY_ENTRY_TLS (9) загружаемого модуля и вызывает для всех потоков NtSetInformationProcess с классом ProcessTlsInformation (https://ntdoc.m417z.com/processinfoclass), который уже в ядре обновляет 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);
Но как же нам найти адрес самой функции LdrpHandleTlsData, если она не экспортируется? Предлагаете по паттернам искать для каждой версии Винды? или, может, грузить PDB и через него искать? Нет, мы всего лишь продизасмим Ntdll!
Мы знаем, что внутри LdrpHandleTlsData будет вызываться NtSetInformationProcess с классом ProcessTlsInformation: адрес NtSetInformationProcess мы знаем, так как это экспортируемая функция, значение ProcessTlsInformation равно 35; такой вызов на всю .text секцию Ntdll один — найдём его, найдём и LdrpHandleTlsData.
Для начала получим адрес NtSetInformationProcess
C++:
[CODE=cpp]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:
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;
}[/CODE]
Все нужные адреса у нас есть, приступаем к поиску. Нам нужно пройтись по всей .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;
}
Код:
push 23h (ProcessTlsInformation)
push 0FFFFFFFFh (NtCurrentProcess())
Код:
mov edx, 23h (ProcessTlsInformation)
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
}
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 через строковое упоминание в обработчиках исключений: https://github.com/howmp/LdrpHandleTlsData/blob/main/src/main.zig. Я его не тестировал, но, может, вам будет интересно.Свой же я тестировал только на Windows 10 и 11 (32 и 64 бита) — работает как часы.