stihl не предоставил(а) никакой дополнительной информации.
Приветствую! Я создатель ботнета и рата MonsterV2. Недавно я писал статью, где описывал, почему от использования LoadPE следует по возможности отказаться. Но как быть, если не получается отказаться от LoadPE, например, по религиозным соображениям?
Эта статья призвана помочь крипторам решить проблему статического TLS, но сначала немного матчасти.
Давайте я покажу наглядно, зачем нужен Static TLS:
Этот код выводит базовый адрес Ntdll.dll, который перед выводом записывается в статическую переменную. Если мы грузим этот модуль вручную без поддержки статического TLS, то у нас выведется ноль. Всё дело в том, что статические переменные внутри функций на самом деле при компиляции становятся глобальными, а через
Для обработки статического TLS, начиная с Windows Vista, внутри Ntdll лежит функцияили Зарегистрируйся), который уже в ядре обновляет или Зарегистрируйся. Также я нашёл Для просмотра ссылки Войди или Зарегистрируйся для
Полный разбор механизма статического TLS тянет на отдельную статью + я оставил достаточно ссылок для изучения его работы, так что повторяться смысла не вижу. Сейчас нам надо разобраться, как отловить TLS для загружаемых вручную модулей.
Чем искать каждую эту переменную и писать ручные костыли, нам проще найти саму функцию
О нет, нам что, придётся ещё
Но как же нам найти адрес самой функции
Посмотреть вложение 105027
Мы знаем, что внутри
Для начала получим адрес
Теперь получим .text секцию Ntdll:
Все нужные адреса у нас есть, приступаем к поиску. Нам нужно пройтись по всей .text секции и записать все адреса, которые являются адресами функций (на которые выполняется call инструкция), а также записать все вызовы
Собрав все вызовы
Для 64 битов класс записывается в регистр edx. Я сделал костыльно: отступаю назад, пока не найду следующую инструкцию:
Чтоб не отступать до конца секции, я установил ограничение в 24 байта — этого должно хватить, чтоб найти нужную инструкцию. Но моя реализация для x64 довольно костыльная и, возможно, потребует доработки напильником.
Ну а теперь дело за малым: мы нашли нужный нам вызов внутри тела искомой функции, так что просто отступаем назад, пока не наткнёмся на адрес, на который где-то в Ntdll выполняется call, благо все call'ы мы запомнили.
Ну а теперь просто вызываем нашу функцию для загруженного в память модуля:
Спасибо за прочтение! Я не могу гарантировать стабильную работу моего кода для всех версий Windows, начиная с Висты, для 32 и 64 бита, но мой метод будет лучше, чем поиск по паттернам с вычитанием оффсетов или подгрузка PDB, так что при желании вы уже сможете доработать его напильником, как вам будет удобно. У меня не получилось в паблике найти кода, который искал бы функцию LdrpHandleTlsData таким же образом (но это не значит, что его нет, так что если вы видели, то дайте знать), хотя, как по мне, мой способ гораздо более очевиднее, чем поиск по паттернам. Но я находил китайский код, который ищет LdrpHandleTlsData через строковое упоминание в обработчиках исключений: Для просмотра ссылки Войди или Зарегистрируйся. Я его не тестировал, но, может, вам будет интересно.
Свой же я тестировал только на Windows 10 и 11 (32 и 64 бита) — работает как часы.
Эта статья призвана помочь крипторам решить проблему статического 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;
}
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"));*/
}
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;
}
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())
Код:
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 через строковое упоминание в обработчиках исключений: Для просмотра ссылки Войди Свой же я тестировал только на Windows 10 и 11 (32 и 64 бита) — работает как часы.
Контакты / Contacts
Скрытое содержимое могут видеть только пользователи групп(ы): Администратор, Модератор