이번 글에서는 지난시간에 알아본 세그먼테이션 과정이 윈도우에서 실제로 어떻게 진행되는지 알아보자. 프로그램이 사용하는 논리 주소가 선형 주소로 어떻게 변경되는지를 확인함으로써 윈도우에서 세그먼테이션 과정이 어떻게 진행 되는지 확인 할 수 있다. 지난번 포스트에 관해서는 아래 링크를 참조하자.

Memory Management : Segmentation 1 

Memory Management : Segmentation 2






우선 시작하기 전에, 유저 레벨에서, 그리고 커널 레벨에서 세그먼트 레지스터가 어떻게 보이는지 확인 해 보자.

[Windows 구조와 원리, 281p ~ 289p 참고] 





1. User Level


아래 스크린샷은 윈도우 Xp Sp3 - 32 Bit 에서 메모장 프로그램을 WinDBG 에 연결해서 본 레지스터 상황이다.



코드 세그먼트 셀렉터 값 : 0x1b 

데이터 세그먼트 셀렉터 값 : 0x23

이제 WinDBG에서 현재 메모장 프로그램이 사용하고 있는 셀렉터들을 dg 명령어를 이용해 확인 해 보자.



어? 0x1B 값과, 0x23 값을 가진 셀렉터는 없다. 왜 그럴까? 이유는 WinDBG 에서 dg 명령어를 통해서 보여지는 셀렉터 값들은 DPL(Descriptor Privilege Level)Bit가 모두 0인 값을 기준으로 하기 때문이다.

아래 그림을 보자. 0x1B 값에서 DPL 비트를 모두 0으로 하면 0x18값이 나오고 0x23값에서 DPL비트를 0으로 하면 0x20 값이 나온다. 빨간색으로 표시 한 부분이 레지스터 창에 보이는 값이고, 파란색으로 표시한 부분이 DPL 비트가 0이 되어 dg 명령어를 사용했을때 보이는 값이다. 0x38의 경우에는 이미 DPL 비트가 0이기 때문에 dg 명령어를 이용해도 0x38로 보인다.


FS 가 사용하는 셀렉터의 Base Address 와 Table Limit 에 대해서는 나중에 알아보기로 하고, 0x23과 0x1B가 가지는 Table Limit에 주목 해 보자. 

우리가 이미 알고 있듯이, 프로그램에서 사용할 수 있는 가상 메모리는 0x00000000 - 0xFFFFFFFF 까지다. 이중에서 0x7FFFFFFF 까지의 2GB는 유저 레벨로 사용되고 (Ring 3), 0x80000000 - 0xFFFFFFFF 까지의 2GB 는 커널레벨로 사용된다 (Ring 0)

그런데 WinDBG에서 셀렉터들을 확인하면 DPL 값이 0인 셀렉터건(Ring 0), 3인 셀렉터건(Ring 3) 모두 Base Address 가 0x00000000 이고Table Limit 이 0xFFFFFFFF 까지다.

Ring 3 사용하는 DS 셀렉터를 이용해서도 커널 메모리에 접근할 수 있다는 뜻인가? 물론 아니다. 실제로 어플리케이션에서 포인터를 이용해 커널 메모리에 접근하려 하면 메모리 접근 에러가 난다.

이는, 윈도우가 세그먼테이션을 이용해서 메모리 접근 보호를 하고 있지 않기 때문이다. 윈도우에서는 페이징을 통해서 어플리케이션에서 커널 메모리로 접근하는 것을 제한 하고 있다.




이제 FS 에 대한 의문을 풀어보자. 왜 FS 는 Base Address 가 0x7ffdc000 이며 Table Limit 이 0xFFF ( 즉, 세그먼트당 0x1000 = 4KB ) 인걸까?

FS 는 32Bit 프로세서인 80386 부터 제공되었으며 프로그램에서 보조적인 용도로 사용할 수 있도록 CS, DS, ES, SS 외에 추가적으로 제공 해 준 레지스터다. 윈도우즈에서는 FS 레지스터를 TEB(Thread Environment Block) 을 지정하는데 사용하고 있다.
TEB 는 유저 모드에서 사용하는 스레드 정보 저장 구조체다.

( TEB 에 대해서 알고 싶다면 다음을 링크를 참고하라. PEB, TEB, EPROCESS, KPROCESS, ETHREAD, KTHREAD )

그렇다면, FS 가 TEB 를 가리키므로 현재 0x7ffdc000에 TEB 가 있다는 말이 된다. 한번 확인 해 보자.


종합해보면 다음과 같다.

"User Level에서 FS는 현재 프로그램이 사용하고 있는 스레드의 정보를 저장하고 

있는 TEB 를 가리킨다." 



그렇다면 스레드가 여러개 일때 TEB 도 여러개가 되어야 하는데, 다른 TEB 는 어느곳에 저장되는 것일까? WinDBG를 통해 확인 해 보자. WinDBG 는 현재 메모장 프로그램에 붙어있다.



메모장 프로그램의 Process ID = 514 이며, 스레드는 현재 2개를 사용하고 있다. 

하나는 Thread ID = 0x848 = 2120d, TEB at 0x7ffdd000

다른 하나는 Thread ID = 0x258 = 600d, TEB at 0x7ffdc000 (현재 Thread)

아래는 ProcExp 를 통해 확인 해 본 Thread Creation Time 이다.




재밌는 점은 

0x848 = 2120d 번 스레드가 먼저 생성 되었음에도 TEB 가 존재하는 주소 번지가 (0x7ffdd000)

0x258 = 600d 번 스레드 보다도 하위에 있다는 점이다. (0x7ffdc000)

더 늦게 생성 된 스레드의 TEB 가 더 하위 메모리에 존재 한다는 의미다. 이는 아마도 윈도우즈 운영체제에서 스레드의 개수를 최대 2048개 만큼 제한 해 놓았기 때문일 것이다.

스레드는 기본적으로 1MB의 스택을 사용하므로 가상 메모리상에서 스레드는 최대 2048개(2GB)가 될 수 있다. 스레드가 2048개라면, TEB도 2048개 여야 하고 따라서 0x1000 (TEB SIZE) x 0x800 (NUMBER OF THREAD) = 0x800000 만큼의 메모리를 차지하는데 최초의 TEB는 0x7ffdd000 에서 시작하므로 나중에 생성된 TEB 가 더 상위 메모리에 존재한다면 마지막 2048번째 TEB는 0x807dd000 에 존재하게 된다. 그러나 이 곳은 커널 영역이므로 TEB 가 존재할 수 없다. (TEB는 유저 레벨에서 사용되는 구조체다. 커널레벨에서는 ETHREAD 구조체가 사용된다.) 이런 이유에서 더 나중에 생긴 TEB 가 더 하위 메모리에 존재 한다. (복군의 추측.^^)


이처럼 각각의 스레드가 같은 FS (0x38) 를 사용하면서 세그먼트의 Base Address( = TEB의 주소) 를 다르게 한다는 의미는 스레드 스위칭이 발생할 때마다 Wiondows 에서 그 내용을 바꾸어 주고 있다는 것을 뜻한다.







2. Kernel Level


이번엔 커널 레벨에서 사용하는 셀렉터에 대해서 알아보자.



유저 레벨과 다른점은 

1. CS 가 0x8 셀렉터를 사용한다는 점.
2. DS가 유저 레벨과 같이 0x23 셀렉터를 사용 한다는 점.
2. SS가 유저 레벨에선 DS와 같은 0x23 셀렉터를 사용했지만 커널 레벨에서는 0x10 셀렉터를 사용한다는 점.
3. FS가 0x38 셀렉터가 아니라 0x30 셀렉터를 사용한다는 점.

이렇게 3가지이다.

아래는 dg 명령어를 이용해 커널에서 사용하는 셀렉터를 나열한 것이다.


궁금한 점을 하나씩 풀어보자. CS의 경우는 딱히 의문점이 없다. Ring 0이므로 당연히 DPL 값이 0인 세그먼트 셀렉터를 사용해야 한다.

2. DS가 유저 레벨과 같이 0x23 셀렉터를 사용 한다는 점.

왜 유저 레벨과 똑같이 DPL이 3인 세그먼트 셀렉터를 커널 레벨에서 사용할까? 이유는 다음과 같다. 



" 낮은 권한의 코드 세그먼트로부터 높은 권한의 코드 세그먼트로의 실행 전환은 

해당 세그먼트가 Conforming 이거나 Gate를 통해서 가능하다. 그러나 

Conforming 세그먼트이건 Non - Conforming 세그먼트이건 높은 권한을 가진 

코드 세그먼트에서 낮은 권한을 가진 세그먼트로의 실행 전환은 불가능하다. 



데이터 세그먼트의 경우에는 Non - Conforming이다. 즉, 낮은 권한을 가진 

프로그램이나 프로시저는 높은 권한을 가진 데이터 세그먼트로의 접근이 

불가능하다. 그러나 반대로, 높은 권한을 가지고 있는 프로그램이나 

프로시저로부터 낮은 권한의 데이터 세그먼트로의 접근은 가능하다. "



이렇게 인텔 프로세서에서 커널의 DPL (Descriptor Privilege Level) 값이 0인 코드 세그먼트로부터 DPL (Descriptor Privilege Level) 값이 3인 데이터 세그먼트로의 접근이 가능하기 때문에 커널을 위한 DPL (Descriptor Privilege Level) 값이 0인 데이터 세그먼트 셀렉터를 사용하지 않는 것이다.




3. SS가 유저 레벨에선 DS와 같은 0x23 셀렉터를 사용했지만 커널 레벨에서는

0x10 셀렉터를 사용한다는 점.
 



스택 세그먼트도 데이터 세그먼트의 일종이다. 하지만 SS 레지스터를 사용해 세그먼트를 선택하는 스택의 경우에는 DS 를 사용하는 경우와는 조금 다르다. 아래의 인텔 문서에 다음과 같이 나와 있다.

"프로세서는 SS 레지스터로 스택 세그먼트 셀렉터를 로드할때 권한을 검사한다. 

로드할 스택 세그먼트와 관련된 모든 권한들이 CPL (Current Privilege Level)과 

매치 되어야 한다. 즉, 현재 가지고 있는 권한인 CPL 과 로드할 세그먼트 셀렉터의 

RPL (Requested Privilege Level)과 해당 세그먼트를 설명하는 GDT 내의 디스크

립터가 가지고 있는 DPL (Descriptor Privilege Level)이 모두 같아야 한다. 만약, 

RPL과 DPL이 CPL와 같지 않다면, #GP가 발생한다."




4. FS가 0x38 셀렉터가 아니라 0x30 셀렉터를 사용한다는 점.


User Level (Ring 0) 에서는 FS가 0x38 셀렉터를 사용하며 TEB (Thread Environment Block) 를 가리킨다. 하지만, Kernel Level (Ring 3) 에서는 FS가 0x30 셀렉터를 사용하여 KPCR (Kernel's Processor Control Region) 를 가리킨다. 

이 말은 프로그래머가 인터럽트를 핸들링 함으로써 유저 레벨로부터 커널 레벨로 제어 이행이 수행될 경우 반드시 FS 레지스터를 커널 레벨의 세그먼트 값 0x30으로 바꿔 주어야 한다는 뜻이다. 만약 그렇게 하지 않을 경우, 커널 레벨에서 실행되는 함수들은 KPCR 혹은 KPRCB를 참조 하는 것이 아니라TEB를 참조하는 엉뚱한 상황이 발생 할 것이다.

KPCR 은 커널에서 프로세서의 정보를 관리하기 위해서 사용하는 구조체다. 따라서 프로세서가 만약 2개라면 2개 존재하고, 1개라면 1개 존재한다. WinDBG 에서 확인 해 보자. !pcr (Processor Control Region) 명령어를 이용해 확인할 수 있다.


IDT, GDT, Thread, IRQL 등에 관한 정보를 담고 있다. 아래는 KPCR 의 구조체를 WinDBG 를 통해 확인한 내용이다.

마지막 0x120에서 볼 수 있는 구조체가 중요한데, 이름이 KPRCB (Kernel's Processor Contorl Block) 이다. WinDBG를 이용해서 어떤 정보들을 담고 있는지 확인 해 보자.




KPRCB 는 우리가 흔히 커널이라 부르는 NtosKrnl.exe 에 의해서 사용되는 비 공개 구조체다. 이름에서 볼 수 있듯이 프로세서를 제어하기 위해 사용하는 구조체다. WinDBG를 통해서 확인 해 보자.


FS:[0]이 KPCR을 가리키므로, KPCR 내의 0x120에 위치하는 KPRCB 은 fs:[120]이다. KPRCB 는 Native API에 의해서 주로 쓰이는데 대표적인 예가 PsGetCurrentThread 와 IoGetCurrentProcess 다. 한번 확인 해 보자. 아래는 PsGetCurrentThread 를 어셈블리어 형태로 변환한 결과다.

FS:[124] 값을 eax로 로드 해 리턴한다. FS:[124]는 KTHREAD 구조체다.


이번엔 IoGetCurrentProcess 를 분석해 보자.

FS:[124]를 통해서 KTHREAD 를 EAX 로 옮기고, KTHREAD + 0x44 로부터 값을 얻어와 EAX로 옮기고 리턴한다.

KTHREAD 0x44는 뭘까?, WinDBG 에서 확인해보면  0x34는 _KAPC_STATE고, 여기서 0x10을 더하면 _KPROCESS가 된다.

결국 IoGetCurrentProcess는 _KPROCESS 를 리턴한다.

EPROCESS 의 첫번째 인자가 KPROCESS 이므로 결국 IoGetCurrentProcess 는 KPROCESS 또는 EPROCESS 구조체를 리턴한다고 말할 수 있다.

ETHREAD의 경우도 마찬가지다. ETHREAD 의 첫번째 인자가 KTHREAD 이므로 PsGetCurrentThread 는 KTHREAD 또는 ETHREAD를 리턴한다고 말할 수 있다.





여기까지 유저 레벨과 커널 레벨에서 사용하는 세그먼트 셀렉터에 대해서 알아보았다. 다음 시간에는 논리 주소를 선형 주소로 변환 해 봄으로써 윈도우에서 세그먼테이션이 어떻게 일어나는지 확인 해 보겠다.

Posted by cyj4369
,