학사 나부랭이

Windows - Memory Structure 본문

自習/Windows Operating System

Windows - Memory Structure

태양왕 해킹 (14세) 2021. 7. 2. 02:39

메모리 구조

 Windows의 메모리 구조는, 32bit의 경우, 기본적으로 프로세스 별로 4GB로 구성되어요. 그런데 일반적인 PC의 메모리가 2~8GB인데, 어떻게 프로세스마다 4GB를 할당할까요?

이를 위해 사용되는 게 가상 메모리예요. 다음과 같이 프로세스 별로 유저 영역에 2GB, 커널 영역에 2GB로 총 4GB의 독립된 메모리 공간을 가지며, 실제 커널 영역인 2GB는 모든 프로세스가 공유하죠.

윈도우 프로세스 메모리 구조

이렇게 메모리 가상화를 통해 프로그램은 자신이 모든 메모리를 소유한 것처럼 주소 값에 신경 쓰지 않고 메모리를 사용할 수 있으며, 오류가 발생하더라도 다른 프로세스의 메모리와 격리되어 있으니 안정성을 높일 수 있죠. 각 가상 메모리에 대한 물리 메모리 매핑은 Windows가 맡아서 하죠.

 

 디버거의 메모리 뷰 화면을 통해 유저 모드에 로드된 다양한 PE 파일 이미지와 스택 등을 확인할 수 있어요.

유저 모드의 메모리 구조

그리고 아래로 내려가면 공용으로 사용되는 kernel32.dll이나 user32.dll 등의 DLL들을 확인할 수 있어요.

공용 DLL 영역

프로그램 하나에 연동된 다양한 모듈들이 같이 로드되므로 이로 인해 공격 코드 작성에 필요한 바이트 코드가 공격 대상 파일에 없더라도 함께 로딩되는 다른 모듈 내에서 찾아 쓸 수 있어요. 그러니 넷상에서 아무 DLL 파일을 다운로드하면 안 되겠죠?

 

이렇게 메모리를 살펴본 결과 다음과 같이 크게 스택, 힙, PE, DLL, 공유 영역이 존재하는 것을 확인할 수 있어요. 여기서, 스택 메모리는 높은 주소에서 낮은 주소로, 힙 메모리는 낮은 주소에서 높은 주소로 할당된다는 점을 기억해야 해요.

프로세스 메모리 할당

 

스택&힙

 스택 구조는 메모리 관리를 위해 사용되며, 이를 위해 SP 레지스터와 BP 레지스터가 사용되어요.

int sum(int a, int b) {
    int res = 0;
    res = a + b;
    return res;
}

int minus(int a, int b) {
    int res = 0;
    res = a - b;
    return res;
}

void _tmain(int argc, _TCHAR* argv[]) {
    int x = 9, y = 4;
    printf("sum: %d\n", sum(x, y));
    printf("minus: %d\n", minus(x, y));
}

위의 두 함수 내부에서 계산 값을 임시로 저장하는 res 변수가 있는데, 이는 함수가 종료된 후부터는 필요가 없어지죠.

이런 메모리에 남기면, 후에 메모리의 낭비로 이어지는, 함수 내부의 지역 변수들이 주로 스택에 할당되어요.

 

힙은 힙 관리자와 힙 구조체를 통해 관리되어, 프로그래머가 필요하면 할당과 해제를 할 수 있고, 내부적으로 복잡한 메커니즘으로 관리되어요.

스택보다 큰 메모리가 필요할 때 사용하고 API를 통해 할당 및 반환할 수 있죠.

 

함수 호출&리턴

 함수마다 별도의 스택 공간을 가지며 이를 스택 프레임이라 해요. 스택 프레임을 구분함으로 가변적인 SP와 상관없이 링크의 add 함수에서 BP를 기준으로 변수에 접근할 수 있어요.

해당 링크의 설명과 움짤에 오류가 있는데 또 움짤 만들기 귀찮으니까 윈도우 시스템 해킹 가이드 27페이지부터 보자!

그래도 대충 설명하자면,

  • BP도 레지스터니까 포인터 형식으로 있어야지 왜 stack에 저장되어 있느냐 이말이야!
  • 함수 call 시: 복귀 주소를 스택에 저장(RET) 후 해당 함수 코드로 점프
  • push bp: 이전 스택 프레임의 BP를 SFP(Stack Frame Pointer, RET 위에)에 저장 why? 후에 SF 제거 후 원래 SF으로 복귀하기 위해 이전 SF의 BP가 있어야 하니까 => add의 SFP는 main의 BP가 가리키던 곳을 가리킴
  • pop bp: SP가 가리키는 주소(SFP)에 저장된 이전 BP 주소를 꺼내 BP 레지스터에 저장
  • RET: pop ip와 동일, 스택에 저장된 복귀 주소를 꺼내 IP 레지스터에 저장

이렇게 스택은 정리되고, 함수 종료 후의 스택은 호출 전과 동일해지죠.

 

이런 인자를 전달하고, 스택을 정리하는 과정에서 호출하는 함수와 호출당하는 함수의 혼란을 방지하기 위한 약속이 존재하는데, 이게 함수 호출 규약이에요.

  • _cdecl: 오른쪽 인자부터 전달, 스택 정리는 caller(Add sp, n)
  • _stdcall: 오른쪽 인자부터 전달, 스택 정리는 callee(ret n)
  • _fastcall: 레지스터+스택으로 인자 전달, 속도가 빠르지만 경우에 따라 코드가 길어짐

C 언어에서는 기본적으로 _cdecl 호출 규약을 따르며, WINAPI의 대부분은 _stdcall을 따라요. 그러나 프로그래머가 컴파일할 때, 호출 규칙 옵션을 통해 바꿀 수도 있어요.

_cdecl

_cdecl 규약에 따른 sum(a, b) 호출, bp+x = cx = 9, bp+y = ax = 4를 통해, 오른쪽 인자(=4)부터 push해서 넣은 뒤, sum 함수를 호출, sum 함수가 끝난 뒤, main 함수에서 sp를 add 해줘 필요 없어진 인자의 공간을 없애주는 걸 알 수 있어요.

_stdcall, main 함수

_stdcall 규약에 따른 sum(a,b) 호출인데, 위의 _cdecl 규약과 비슷하지만, sp를 이동해주는 명령이 없네요. 그럼 sum 함수를 살펴볼까요?

_stdcall, sum 함수

함수 복귀 시, 일반적인 retn이 아닌, retn 8 명령이 실행되는데, 이는 sum 함수 수행 전의 push ax, push cx로 인해 늘어난 스택을 복귀와 동시에 줄여주는 거예요.

_fastcall

_fastcall 규약에 따른 sum(a,b) 호출, dx 레지스터와 ax 레지스터에 인자를 넣고, 바로 sum 함수를 호출하죠.

'自習 > Windows Operating System' 카테고리의 다른 글

Windows - Opening  (0) 2021.05.14
Comments