D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Приветствую! Я создатель ботнета и рата MonsterV2. Недавно я писал статью, где описывал, почему от использования LoadPE следует по возможности отказаться. Но как быть, если не получается отказаться от LoadPE, например, по религиозным соображениям?
Эта статья призвана помочь крипторам решить проблему статического TLS, но сначала немного матчасти.
Давайте я покажу наглядно, зачем нужен Static TLS:
C++: Скопировать в буфер обмена
Этот код выводит базовый адрес Ntdll.dll, который перед выводом записывается в статическую переменную. Если мы грузим этот модуль вручную без поддержки статического TLS, то у нас выведется ноль. Всё дело в том, что статические переменные внутри функций на самом деле при компиляции становятся глобальными, а через
Для обработки статического TLS, начиная с Windows Vista, внутри Ntdll лежит функция
Полный разбор механизма статического TLS тянет на отдельную статью + я оставил достаточно ссылок для изучения его работы, так что повторяться смысла не вижу. Сейчас нам надо разобраться, как отловить TLS для загружаемых вручную модулей.
Чем искать каждую эту переменную и писать ручные костыли, нам проще найти саму функцию
C++: Скопировать в буфер обмена
О нет, нам что, придётся ещё
Но как же нам найти адрес самой функции
Мы знаем, что внутри
Для начала получим адрес
C++: Скопировать в буфер обмена
Теперь получим .text секцию Ntdll:
C++: Скопировать в буфер обмена
Все нужные адреса у нас есть, приступаем к поиску. Нам нужно пройтись по всей .text секции и записать все адреса, которые являются адресами функций (на которые выполняется call инструкция), а также записать все вызовы
C++: Скопировать в буфер обмена
Собрав все вызовы
Код: Скопировать в буфер обмена
Для 64 битов класс записывается в регистр edx. Я сделал костыльно: отступаю назад, пока не найду следующую инструкцию:
Код: Скопировать в буфер обмена
Чтоб не отступать до конца секции, я установил ограничение в 24 байта — этого должно хватить, чтоб найти нужную инструкцию. Но моя реализация для x64 довольно костыльная и, возможно, потребует доработки напильником.
C++: Скопировать в буфер обмена
Ну а теперь дело за малым: мы нашли нужный нам вызов внутри тела искомой функции, так что просто отступаем назад, пока не наткнёмся на адрес, на который где-то в Ntdll выполняется call, благо все call'ы мы запомнили.
C++: Скопировать в буфер обмена
Ну а теперь просто вызываем нашу функцию для загруженного в память модуля:
C++: Скопировать в буфер обмена
Спасибо за прочтение! Я не могу гарантировать стабильную работу моего кода для всех версий Windows, начиная с Висты, для 32 и 64 бита, но мой метод будет лучше, чем поиск по паттернам с вычитанием оффсетов или подгрузка PDB, так что при желании вы уже сможете доработать его напильником, как вам будет удобно. У меня не получилось в паблике найти кода, который искал бы функцию LdrpHandleTlsData таким же образом (но это не значит, что его нет, так что если вы видели, то дайте знать), хотя, как по мне, мой способ гораздо более очевиднее, чем поиск по паттернам. Но я находил китайский код, который ищет LdrpHandleTlsData через строковое упоминание в обработчиках исключений: https://github.com/howmp/LdrpHandleTlsData/blob/main/src/main.zig. Я его не тестировал, но, может, вам будет интересно.
Свой же я тестировал только на 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
(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);
LDR_DATA_TABLE_ENTRY
добавлять в peb->Ldr
? Да нет, конечно: для корректной работы у LDR_DATA_TABLE_ENTRY
достаточно лишь проиницилизовать поля DllBase
и SizeOfImage
— к другим эта функция не обращается.Но как же нам найти адрес самой функции
LdrpHandleTlsData
, если она не экспортируется? Предлагаете по паттернам искать для каждой версии Винды? или, может, грузить PDB и через него искать? Нет, мы всего лишь продизасмим Ntdll!Мы знаем, что внутри
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)
Чтоб не отступать до конца секции, я установил ограничение в 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
}
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 бита) — работает как часы.
Контакты / Contacts
- Tox: 78FCE948A377D5BA27AEE0E47EC27BEB537AF607C4F2DE8BF8B5C6018E27690FB691E72B1238
- Jabber: monsterv2@exploit.im
- Session: 0521bb4bb6a0cac3007e4da53e5cb1f5f0baefa2eeb439466bd6eb2a6e45b1c661
- Telegram: https://t.me/mosterv2
- Telegram channel: https://t.me/monster_update_news
Форумы