※ 개인적인 생각이 추가되어 정확하지 않은 정보일 수 있다. 자꾸 까먹어서 기억을 위해 기록
공식 문서 : https://learn.microsoft.com/ko-kr/cpp/build/reference/stack-stack-allocations?view=msvc-170
공식 문서에서 정의한 페이지 상태
| 페이지 상태 | 의미 |
|---|---|
| MEM_COMMIT (0x1000) | 메모리 또는 디스크의 페이징 파일에서 실제 스토리지가 할당된 커밋된 페이지를 나타냅니다. |
| MEM_FREE (0x10000) | 호출 프로세스에 액세스할 수 없고 할당할 수 있는 사용 가능한 페이지를 나타냅니다. 무료 페이지의 경우 AllocationBase, AllocationProtect, Protect 및 Type 멤버의 정보는 정의되지 않습니다. |
| MEM_RESERVE (0x2000) | 실제 스토리지가 할당되지 않고 프로세스의 가상 주소 공간 범위가 예약된 예약된 페이지를 나타냅니다. 예약된 페이지의 경우 보호 멤버의 정보가 정의되지 않습니다. |
VirtualQuery를 통해 얻은 _MEMORY_BASIC_INFORMATION 구조체의 Protect 멤버는 페이지 상태가 MEM_FREE / MEM_RESERVE 상태에서는 정의되지 않는다.
https://learn.microsoft.com/ko-kr/windows/win32/memory/memory-protection-constants




Guard 부분은 32비트 기준 2Page, 64비트 기준 3Page로 동작하는 듯.
공식 문서는 초기 커밋 값이 4KB라고 나와있는데 내가 측정하면 4KB이상의 값들이 COMMIT되어있어서 어떻게하면 4KB COMMIT을 확인할 수 있을까? 싶어서 스레드를 생성만 하고 그 상태를 측정해보았다.
새로 생성한 스레드 ID : 0x6f58
main 스레드 ID : 0xa1fc
스레드를 생성하고 실행 전의 메모리 주소를 Windbg를 통해 확인

확인 결과

unKnown으로 표시된 부분에 새로 생성되는 스레드에 관한 정보가 들어올 것 같은 느낌이 든다.
주황색으로 칠된 부분은 완전히 내가 측정한 스택의 구조와 비슷하게 가고 있지 않은가?? 그래서


TEB에 대한 포인터는 노랑 + 연보라로 칠한 부분,
스택에 관한 부분은 주황색 부분으로 확인할 수 있었고
스택 구조를 담아놓은 unknown 상태에서 Commit된 메모리가 0x1000 (4KB) 인 것을 확인했으며, 스레드 초기화 과정에서 스택을 4KB 이상 사용해 GUARD를 넘어서서 자동으로 확장되는 것이 아닐까?? 라는 생각을 해본다.
PAGE_GUARD 속성의 페이지에 접근을 시도하면 시스템은 그 사실을 알게 되고, 이때 시스템은 가드 페이지 이하에 추가적으로 페이지를 커밋하고 현재 가드 페이지의 PAGE_GUARD 속성을 해제하고 커밋된 페이지에 대해서 PAGE_GUARD 속성을 추가한다.
스레드가 가장 마지막의 가드 페이지에 접근하게 되면 시스템은 EXCEPTION_STACK_OVERFLOW 예외를 발생시킨다.
확인을 위해 스택이 Reserve된 페이지를 끝까지 늘려보도록 하였다.

마지막 1개의 Reserve가 남은 상태에서 Guard Page에 접근하게되면 StackOveflow가 발생하는 것을 확인하였다.
Test 1 : Guard가 2개 있는데
[낮은 주소]
[Guard1]
[Guard2]
[Commit]
[높은 주소]
만약 Guard2를 건드리지 않고 Guard1을 건드리면 Guard2도 커밋이 될까? 또 다음 Guard는 몇 개가 올라갈까? 테스트해봤지만.. 아직 잘 모르겠다 더 공부해야할듯
불확실한.. 생각...100%
코드에서 임의로 Guard1만 건드리도록 설정


결과


Windbg로도 계속 수정해보고 확인해봤는데 지금 위의 1,2,3번처럼 생각하면 계속 수정했을 때 내가 생각하는 것처럼 동작하지 않는다... 이런 행동은 결과를 100% 보장할 수 없는 것 같다.... 흠... 그래서 이런 상황일 때의 동작은 UB로 처리되는걸까 궁금하다..
Windbg도 명령어들 계속 찾아보면서 하고 있는데 빨리 활용할 수 있도록 하자.. 너무 어렵다.
코드
#include <iostream>
#include <Windows.h>
#include <unordered_map>
#include <string>
#include <process.h>
#include <conio.h>
#define PAGE_SIZE (0x00001000)
#define STACK_SIZE (0x00100000)
#define PAGE_SIZE_MASK (0xfffff000)
#define GRANULARITY_BOUNDARY_MASK (0xffff0000)
INT_PTR stackBase;
std::unordered_map<void*, MEMORY_BASIC_INFORMATION> p;
void TEST1();
std::string GetPageStateStr(DWORD state)
{
switch (state)
{
case 0x1000:return " MEM_COMMIT";
case 0x10000:return " MEM_FREE";
case 0x2000:return " MEM_RESERVE";
default:return " INVALID_STATE";
}
}
std::string GetPageProtectionConstantsStr(DWORD protect)
{
// 메모리 보호 상수
https://learn.microsoft.com/ko-kr/windows/win32/memory/memory-protection-constants
DWORD protectionConstant = (protect & 0xff);
std::string ret{""};
switch (protectionConstant)
{
// PAGE_READONLY
// PAGE_READWRITE
// PAGE_WRITECOPY
// 의 DEP (Data Execution Prevention) : https://learn.microsoft.com/ko-kr/windows/win32/memory/data-execution-prevention
// Windows에서 기본 옵션은 [DEP 켜져있음]
case 0x00:ret += "NONE"; break;
case 0x01: ret += "NO_ACCESS"; break; // 모든 접근 => 액세스 위반
case 0x02: ret += "PAGE_READONLY"; break;// 쓰기, ●DEP 사용 시 : 실행 => 액세스 위반 [읽기 가능 속성]
case 0x04: ret += "PAGE_READWRITE"; break;// ●DEP 사용 시 : 실행 => 액세스 위반 [읽기, 쓰기 가능 속성]
case 0x08: ret += "PAGE_WRITECOPY"; break;// ●DEP 사용 시 : 실행 => 액세스 위반 [읽기, 복사 가능 속성]
case 0x10: ret += "PAGE_EXECUTE"; break;// 쓰기 => 액세스 위반 [실행 가능 속성]
case 0x20:ret += "PAGE_EXECUTE_READ"; break;// 쓰기 => 액세스 위반 [실행 / 읽기 가능 속성]
case 0x40: ret += "PAGE_EXECUTE_READWRITE"; break; // [실행 / 읽기 / 쓰기 가능 속성]
case 0x80: ret += "PAGE_EXECUTE_WRITECOPY"; break; // [실행 / 읽기 / 복사 가능 속성]
// PAGE_TARGETS_INVALID와 PAGE_TARGETS_NO_UPDATE에 대한 부분은 잘 모르겠다.
default: ret += "INVALID_PROTECTION_C"; break;
}
int s = 22 - ret.length();
for (int i = 0; i < s; i++)
{
ret += " ";
}
DWORD protectionQualifier = (protect & 0x0f00);
switch (protectionQualifier)
{
case 0x100: ret += " | PAGE_GUARD"; break; // GUARD 속성 => 접근 시 STATUS_GUARD_PAGE_VIOLATION 예외를 발생 + 해당 가드 속성을 제거
case 0x200: ret += " | PAGE_NOCACHE"; break;
case 0x400: ret += " | PAGE_WRITECOMBINE"; break;
default:break;
}
return ret;
}
std::string GetKilloBytes(size_t byteSize)
{
size_t kb = byteSize / 1024;
size_t b = byteSize % 1024;
std::string ret = "[ " + std::to_string(kb) + "KB " + std::to_string(b) + "BYTE ]";
return ret;
}
unsigned WINAPI TestThread(LPVOID lparam)
{
return 0;
}
int main()
{
HANDLE h = (HANDLE)_beginthreadex(nullptr, 0, TestThread, nullptr, CREATE_SUSPENDED, nullptr);
if (nullptr == h)
return 0;
printf("Created Thread ID : 0x%x\n", GetThreadId(h));
printf("ID : 0x%x\n", GetCurrentThreadId());
ResumeThread(h);
// TEB 정보를 얻어옴.
PNT_TIB tib = (PNT_TIB)NtCurrentTeb();
// https://ko.wikipedia.org/wiki/Win32_%EC%8A%A4%EB%A0%88%EB%93%9C_%EC%A0%95%EB%B3%B4_%EB%B8%94%EB%A1%9D
// TIB(TEB) 정보, Windbg로도 확인할 수 있었음.
//
// +--------------------+
// | Page속성(Guard) |
// | |
// |--------------------|
// | StackLimit |
// | |
// | |
// | StackBase |
// +--------------------+
//
// 스택 베이스 / 스택의 바닥 (높은 주소)
stackBase = (INT_PTR)tib->StackBase;
// 스택 한계 / 스택의 천장 (낮은 주소)
//printf("StackLimit : 0x%p\n", (void*)stackLimit);
//std::cout << "초기 커밋 사이즈: "
// << ((INT_PTR)stackBase - (INT_PTR)stackLimit) << " bytes\n";
// 기본 스택(1MB)의 구조 파악을 위해 스택의 낮은 주소 -> 높은 주소로의 페이지 속성을 확인한다.
while (1)
{
if (_kbhit())
{
char c = _getch();
if ('q' == c || 'Q' == c)
break;
}
INT_PTR stackStartPos = stackBase - (STACK_SIZE);
INT_PTR stackLimit = (INT_PTR)tib->StackLimit;
int Cnt = 0;
printf("StackStartPos : 0x%p\n", (void*)stackStartPos);
printf("StackBase : 0x%p\n", (void*)stackBase);
printf("StackLimit : 0x%p\n", (void*)stackLimit);
int remainingSize = 1024 * 1024;
// 자동 페이지 확장을 위한 변수
int* pAuto = (int*)0xffffffff;
while (stackStartPos < stackBase)
{
MEMORY_BASIC_INFORMATION mbi{};
VirtualQuery((void*)stackStartPos, &mbi, sizeof(mbi));
// 얻은 페이지의 속성이 얼마나 연속적인가를 나타낸다. (해당 크기만큼의 페이지는 동일한 속성을 가진다.)
remainingSize -= mbi.RegionSize;
if (remainingSize < 0)
DebugBreak();
printf("-----------------------------------------------\n");
printf("Count : %d\n", Cnt++);
printf("Address : 0x%p\n", (void*)stackStartPos);
printf("Page State : %s\n", GetPageStateStr(mbi.State).c_str());
printf("Page Protection : %s\n", GetPageProtectionConstantsStr(mbi.Protect).c_str());
printf("Region Size : %s\n", GetKilloBytes(mbi.RegionSize).c_str());
printf("Remaining Size : %s\n", GetKilloBytes(remainingSize).c_str());
printf("\n\n");
pAuto = (int*)stackStartPos;
// breakPoint 걸어서 pA조작
{
int testA = 3;
int* pA = &testA;
// PAGE_PROTECTION이 GUARD인 경우에 바꾸면 다시 처음부터 스택 정보 출력.
*pA = 43;
if (pA != &testA)
{
TEST1();
break;
}
}
stackStartPos += mbi.RegionSize;
}
bool bAutoEx = false;
// 자동으로 확장을 원하는 경우 여기도 breakPoint 걸어서 조작
if(bAutoEx)
*(--pAuto) = 4;
Sleep(100);
system("cls");
}
CloseHandle(h);
return 0;
}
void TEST1()
{
// TEST를 위해서 STACK 시작지점부터 다시 메모리 체크.
INT_PTR stackStartPos = stackBase - (STACK_SIZE);
int Cnt = 0;
printf("StackStartPos : 0x%p\n", (void*)stackStartPos);
printf("StackBase : 0x%p\n", (void*)stackBase);
int remainingSize = 1024 * 1024;
while (stackStartPos < stackBase)
{
MEMORY_BASIC_INFORMATION mbi{};
VirtualQuery((void*)stackStartPos, &mbi, sizeof(mbi));
// 얻은 페이지의 속성이 얼마나 연속적인가를 나타낸다. (해당 크기만큼의 페이지는 동일한 속성을 가진다.)
remainingSize -= mbi.RegionSize;
if (remainingSize < 0)
DebugBreak();
printf("------------------------------------------------------------------------------\n");
printf(" [TEST1 Count : %d\n", Cnt++);
printf(" [TEST1 Address : 0x%p\n", (void*)stackStartPos);
printf(" [TEST1 Page State : %s\n", GetPageStateStr(mbi.State).c_str());
printf(" [TEST1 Page Protection : %s\n", GetPageProtectionConstantsStr(mbi.Protect).c_str());
printf(" [TEST1 Region Size : %s\n", GetKilloBytes(mbi.RegionSize).c_str());
printf(" [TEST1 Remaining Size : %s\n", GetKilloBytes(remainingSize).c_str());
printf("\n\n");
stackStartPos += mbi.RegionSize;
}
}
// 스택 오버플로 체킹을 위한 코드.
// 32비트 기준
// [스택의 마지막 RESERVE 부분]
// [스택 GUARD]
// [스택 GUARD] <------┐
// │
// 상태에서 ------┘ 부분을 건드리면 마지막 RESERVE 부분이 건드려지면서 STACK OVERFLOW가 동작.