Я добавляю защиту PAGE_GUARD
к страницам стека текущего потока через VirtualProtect
. Однако при доступе к стеку установленный мной обработчик исключений никогда не выполняется.
Я предполагаю, что ОС просто молча проглатывает исключения Guard-Page-Violation, потому что она использует защитные страницы для управления ростом стека и переполнением стека.
Можно ли в этом случае вообще отловить эти исключения?
Вот мой C-код
#include <stdio.h>
#include <Windows.h>
#include <assert.h>
LONG WINAPI BlanketExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo) {
printf("exception code: %lx\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
return EXCEPTION_CONTINUE_SEARCH;
}
int main(void) {
// Install exception handler
SetUnhandledExceptionFilter(BlanketExceptionHandler);
ULONG_PTR stackBot = 0, stackTop = 0;
GetCurrentThreadStackLimits(&stackBot, &stackTop);
assert(stackBot < stackTop);
// turn all pages in the stack into guard-pages
DWORD savedProtect = 0;
if (!VirtualProtect((LPVOID) stackBot, stackTop - stackBot, PAGE_READWRITE | PAGE_GUARD, &savedProtect)){
fprintf(stderr, "[Error]: Could not add guard pages: %ld\n", GetLastError());
return 1;
}
// access some stack memory page. This should trigger the registered exception handler!
*(DWORD*) (stackTop - 0x1500) = 0xdeadbeef;
return 0;
}
Код должен быть скомпилирован с
cl /nologo main.c /link /STACK:0x100000,0x100000
Запуск кода не дает результата из BlanketExceptionHandler
. Но, используя VirtualQuery
, я заметил, что страницы стека имеют правильную защиту.
Я также пытался установить обработчик исключений через AddVectoredExceptionHandler
, но это тоже не сработало.
Спасибо за ответ, но, как я уже сказал в своем первоначальном посте, AddVectoredExceptionHandler(1, BlanketExceptionHandler);
тоже не работает.
не будет притворяться, что знает ответ; но если вы охраняете весь свой стек, как вы представляете, как будет вызываться обработчик исключений? вам нужен стек, чтобы сделать вызов и выполнить его код.
@LouisKronberg, «как я уже сказал в своем первоначальном посте, AddVectoredExceptionHandler(1, BlanketExceptionHandler);
тоже не работает» - извините, я не видел эту часть, когда читал ваш вопрос.
@YakovGalka Это действительно интересная идея. Хотя, если у обработчика исключений действительно есть проблемы из-за защиты стека на других страницах, я ожидал, что программа рухнет или, по крайней мере, каким-то образом пожалуется под отладчиком. Я посмотрю на это больше. Отказ от ответственности: у меня очень поверхностное понимание обработки исключений.
@RemyLebeau - фактически созданная ОС, а не какой-либо произвольной защитной страницей, созданной пользователем - страница, не содержащая информации, установившей флаг page_guard. например _resetstkoflw установить PAGE_GUARD
.. исключение автоматической обработки ядра, если оно находится в диапазоне текущего пользовательского стека. он также регулирует StackLimit
в NT_TIB
во время этого
ОС просто молча проглатывает исключения Guard-Page-Violation - правда. если он находится в диапазоне стека пользовательского режима текущего потока. в вашем коде, в обычном случае - VirtualProtect
уже должно произойти сбой, потому что память еще не выделена в stackBot
, но поскольку вы установили stackcommit==stacklimit - эта работа. а затем сброс защиты системой уже при выходе VirtualProtect API, потому что вы устанавливаете это для всего стека
Можно ли в этом случае вообще отловить эти исключения? - нет. а для чего вы вообще хотите это сделать?
@RbMm «страница, не содержащая информации о том, кто установил флаг page_guard» - верно, но ОС знает, ГДЕ в стеке находится ее защитная страница. Таким образом, он знает, происходит ли ошибка защитной страницы на защитной странице, зарезервированной для управления стеком. «ядро автоматически обрабатывает исключение защиты, если оно находится в диапазоне текущего пользовательского стека» - я был бы очень обеспокоен, если ядро манипулирует стеком при любой ошибке защитной страницы, которая возникает где-либо в стеке, а не только на зарезервированной защитной странице.
@RemyLebeau - действительно, я сейчас проверяю - ОС обрабатывает исключение защиты, если оно возникает в любом месте текущего стека потоков. даже если он выше текущего esp/rsp. так что проверки не очень строгие
@RemyLebeau - я был бы очень обеспокоен, если бы ядро манипулировало стеком при любой ошибке защитной страницы, которая возникает где-либо в стеке, а не только на зарезервированной защитной странице, - но вы можете проверить это ... может быть не очень логично, но как есть
@RbMm "но ты можешь проверить это .." - может быть, ВЫ можете, но я не могу. Я не настолько талантлив, чтобы копаться во внутренностях ОС, как вы.
@RemyLebeau - я проверяю .. это. это реальная работа. os даже устанавливает StackLimit
неправильное значение (выше текущего rsp), если исключение защиты находится в области выше rsp.
и здесь вообще не нужно копаться в ОС, я имею в виду просто написать тестовый код.. сейчас я просто не могу написать хороший пример, потому что не на рабочем компе, но могу проверить отладчиком
я сделаю следующий тест - первый вызов void* p = alloca(0x10000)
для выделения 64 КБ в стеке. чем установить PAGE_GUARD
на BYTE* q = (PBYTE)p+0x8000
адрес. скажем 2 страницы. затем получите доступ к адресу q
(скажите *q=0
). посмотрите карту памяти до и после этого. и ищите StackLimit
до и после *q = 0;
Можно ли в этом случае вообще отловить эти исключения?
нет. если вы обращаетесь к странице защиты, исключение (как и любое исключение) сначала будет обрабатываться кодом ядра. если исключение происходит в текущем диапазоне стека потока - обработчик ядра удаляет PAGE_GUARD
со страницы, где было исключение, убедитесь, что ниже этой страницы - несколько PAGE_GUARD
существуют и настройте StackLimit := PAGE_ALIGN(MemmoryAccessAddress)
в NT_TIB
(вы можете это проверить). и не вызывать обработчики исключений пользовательского режима (если это не последняя страница в стеке)
пример кода:
PCSTR GetStateSz(ULONG State)
{
switch (State)
{
case MEM_FREE: return "FREE";
case MEM_COMMIT: return "COMMIT";
case MEM_RESERVE: return "RESERVE";
}
return "?";
}
PCSTR GetTypeSz(ULONG Type)
{
switch (Type)
{
case MEM_IMAGE: return "IMAGE";
case MEM_MAPPED: return "MAPPED";
case MEM_PRIVATE: return "PRIVATE";
}
return "?";
}
void FormatProtect(ULONG Protect, PSTR psz, ULONG cch)
{
static const ULONG pp[] = {
PAGE_NOACCESS,
PAGE_READONLY,
PAGE_READWRITE,
PAGE_WRITECOPY,
PAGE_EXECUTE,
PAGE_EXECUTE_READ,
PAGE_EXECUTE_READWRITE,
PAGE_EXECUTE_WRITECOPY,
PAGE_GUARD,
PAGE_NOCACHE,
PAGE_WRITECOMBINE,
};
static const PCSTR ss[] = {
"NOACCESS",
"READONLY",
"READWRITE",
"WRITECOPY",
"EXECUTE",
"EXECUTE_READ",
"EXECUTE_READWRITE",
"EXECUTE_WRITECOPY",
"GUARD",
"NOCACHE",
"WRITECOMBINE",
};
ULONG n = _countof(pp);
do
{
if (Protect & pp[--n])
{
int len = sprintf_s(psz, cch, " | %s", ss[n]);
if (0 >= len)
{
break;
}
psz += len, cch -= len;
}
} while (n);
}
ULONG WINAPI PrintProtect_I(PVOID BaseAddress)
{
PVOID AllocationBase = BaseAddress;
::MEMORY_BASIC_INFORMATION mbi;
while (VirtualQuery(BaseAddress, &mbi, sizeof(mbi)) &&
mbi.AllocationBase == AllocationBase)
{
CHAR szProtect[0x100];
FormatProtect(mbi.Protect, szProtect, _countof(szProtect));
DbgPrint("[%p, %p) [%X] %s %s { %s }\n", mbi.BaseAddress,
BaseAddress = (PBYTE)mbi.BaseAddress + mbi.RegionSize,
mbi.RegionSize >> PAGE_SHIFT, GetStateSz(mbi.State), GetTypeSz(mbi.Type), szProtect);
}
return 0;
}
void PrintProtect(PVOID BaseAddress)
{
if (HANDLE hThread = CreateThread(0, 0, PrintProtect_I, BaseAddress, 0, 0))
{
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
PNT_TIB tib = reinterpret_cast<PNT_TIB>(NtCurrentTeb());
DbgPrint("[%p, %p) << Current Stack\n\n", tib->StackLimit, tib->StackBase);
}
ULONG WINAPI SetGuard(PVOID pv)
{
ULONG op;
return VirtualProtect(pv, PAGE_SIZE, PAGE_EXECUTE_READWRITE|PAGE_GUARD, &op);
}
void GDemo()
{
PVOID stack = alloca(0x10000);
PBYTE pb = (PBYTE)PAGE_ALIGN((PBYTE)stack + 0x8000);
ULONG_PTR a, b;
GetCurrentThreadStackLimits(&a, &b);
DbgPrint("[%p, %p) << Stack Region\n\n", a, b);
PrintProtect((PVOID)a);
if (HANDLE hThread = CreateThread(0, 0, SetGuard, pb, 0, 0))
{
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
PrintProtect((PVOID)a);
*pb = 0;
PrintProtect((PVOID)a);
}
}
и вывод:
[000000EAAE4C0000, 000000EAAE5C0000) << Stack Region
[000000EAAE4C0000, 000000EAAE5AB000) [EB] RESERVE PRIVATE { }
[000000EAAE5AB000, 000000EAAE5AE000) [3] COMMIT PRIVATE { | GUARD | READWRITE }
[000000EAAE5AE000, 000000EAAE5C0000) [12] COMMIT PRIVATE { | READWRITE }
[000000EAAE5AE000, 000000EAAE5C0000) << Current Stack
[000000EAAE4C0000, 000000EAAE5AB000) [EB] RESERVE PRIVATE { }
[000000EAAE5AB000, 000000EAAE5AE000) [3] COMMIT PRIVATE { | GUARD | READWRITE }
[000000EAAE5AE000, 000000EAAE5B7000) [9] COMMIT PRIVATE { | READWRITE }
[000000EAAE5B7000, 000000EAAE5B8000) [1] COMMIT PRIVATE { | GUARD | EXECUTE_READWRITE }
[000000EAAE5B8000, 000000EAAE5C0000) [8] COMMIT PRIVATE { | READWRITE }
[000000EAAE5AE000, 000000EAAE5C0000) << Current Stack
[000000EAAE4C0000, 000000EAAE5AB000) [EB] RESERVE PRIVATE { }
[000000EAAE5AB000, 000000EAAE5AE000) [3] COMMIT PRIVATE { | GUARD | READWRITE }
[000000EAAE5AE000, 000000EAAE5B4000) [6] COMMIT PRIVATE { | READWRITE }
[000000EAAE5B4000, 000000EAAE5B7000) [3] COMMIT PRIVATE { | GUARD | READWRITE }
[000000EAAE5B7000, 000000EAAE5B8000) [1] COMMIT PRIVATE { | EXECUTE_READWRITE }
[000000EAAE5B8000, 000000EAAE5C0000) [8] COMMIT PRIVATE { | READWRITE }
[000000EAAE5B7000, 000000EAAE5C0000) << Current Stack
Большое спасибо за ответ на мой вопрос! Есть ли источник, где я могу прочитать об этом, или нужно перепроектировать этот материал?
@LouisKronberg - я не уверен, где это можно прочитать, но это можно перевернуть и проверить. вы можете расширить собственные тесты под отладчиком. посмотрите по коду или карте памяти некоторых инструментов текущего процесса (текущая область стека). посмотрите до и после доступа к памяти. как оно изменилось. тестировать StackLimit
значение после каждого доступа. просто проверьте больше, чтобы лучше понять
«ОС ... использует защитные страницы для управления ростом стека и переполнением стека» - только тогда, когда исключение возникает на защитной странице, которая фактически создается ОС, а не на какой-либо произвольной защитной странице, созданной пользовательским кодом. См. Более пристальный взгляд на страницу защиты стека, то есть: «Обработчик исключений по умолчанию обрабатывает исключение, просматривая, находится ли адрес в области страницы защиты текущего стека. Если да, то он обновляет следующую зарезервированную страницу. на защитную страницу, а затем возобновляет выполнение».