Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터
지난 글에서,
Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
; https://www.sysnet.pe.kr/2/0/13849
선형 주소로의 변환 시 관여하는 세그먼트 레지스터를 살펴봤는데요, 이번에는 남은 2개의 세그먼트인 FS, GS 레지스터를 마저 살펴보겠습니다.
FS, GS 레지스터의 최초 역할은 아래의 글에 설명이 나옵니다.
What is the "FS"/"GS" register intended for?
; https://stackoverflow.com/questions/10810203/what-is-the-fs-gs-register-intended-for
즉, 16비트 CPU 시절에 64KB 단위의 메모리 구역을 DS, ES로 나눠 지정할 수 있었던 것을 동시에 4개까지 서로 다른 64KB 영역을 지정할 수 있도록 추가로 제공한 것이 FS, GS였다고 합니다.
So, by using FS and GS, you could effectively address two more 64KB memory segments from your program without the need to change DS or ES registers whenever you need to address other segments than were loaded in DS or ES.
하지만, 32비트 시절로 오면서는 주소(Address) 선이 32비트로 넓어지면서 4개의 데이터 세그먼트 레지스터가 (그다지 절실하게) 필요하지는 않게 되었고, 따라서 운영체제 측에서 임의의 목적으로 사용할 수 있게 되었습니다.
실제로, 운영체제마다 FS, GS를 각기 다른 목적으로 사용할 수 있는데요, x64 윈도우 운영체제의 경우에는,
What is the GS register used for on Windows?
; https://stackoverflow.com/questions/39137043/what-is-the-gs-register-used-for-on-windows
FS 레지스터를 32비트 프로세스의 Thread Environment Block(TEB)를 가리키도록 하고 GS 레지스터는 64비트 프로세스의 TEB를 가리키는 용도로 사용합니다. (x86 윈도우의 경우에는 FS 레지스터만 사용하고 GS 레지스터는 사용하지 않습니다.)
실습으로 FS, GS가 어떤 값을 가지고 있는지 WinDbg를 통해 직접 확인해 볼 텐데요, 이를 위해 notepad.exe를 실행하고 WinDbg를 Attach시켜 rM 명령어를 사용하면,
// Windows 10 x64 환경의 notepad.exe를 디버그 연결
0:049> rM 8
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!DbgBreakPoint:
00007ffe`07814090 cc int 3
저렇게 fs, gs 세그먼트의 값이 나옵니다. x64 윈도우에서 x64 프로세스(notepad.exe)를 실행했으니, 일단 GS 레지스터가 사용될 텐데요 그것의 GDT offset 값은 5로 계산되고,
2b == 00000000 00101011 (offset 0n5)
GDT로부터 offset 5에 해당하는 selector를 찾아보면,
// 세그먼트 1개 데이터 크기 8 바이트 * offset 5 == 40 (0x28)
4: kd> dg 28
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0028 00000000`00000000 00000000`ffffffff Data RW Ac 3 Bg Pg P Nl 00000cf3
예상했던 것과는 달리 Base가 0으로 나옵니다. 말이 안 되죠? 가령, 흔히 하는 gs 레지스터 연산에서 gs:[0x30]을 접근하게 되는데, 그렇다면 Base 주소가 0이니 선형 주소로 00000000`00000030을 접근할 것이고, 당연히 해당 주소 영역은 윈도우에서 사용하지 않기 때문에 Access Violation 예외가 발생하게 됩니다.
0:049> db 00000000`00000030 L4
00000000`00000030 ?? ?? ?? ?? ????
물론, 이게 다가 아니겠죠? ^^
x64 CPU는 FS, GS에 대해 특별한 대우를 하는데요, 그 2개의 세그먼트에 대해서는 주소 계산을 GDT를 이용하지 않고 별도의 MSR(Model Specific Register) 값을 기반으로 합니다.
// https://sites.uclouvain.be/SystInfo/usr/include/asm/msr-index.h.html
#define MSR_FS_BASE 0xc0000100 /* 64bit FS base */
#define MSR_GS_BASE 0xc0000101 /* 64bit GS base */
가령, GS Base 주소는 MSR_GS_BASE(0xc0000101)를 통해 얻을 수 있는데요, 아쉽게도 "사용자 모드 (Ring 3)" 디버깅 상태로는 WinDbg에서 이 값을 확인할 수 없습니다.
// rdmsr은 Ring 0의 특권에서만 실행 가능한 명령어
// https://modoocode.com/en/inst/rdmsr
0:049> rdmsr 0xc0000101
^ Bad register error in 'rdmsr 0xc0000101'
대신, 우회적으로 !teb 명령을 실행하면 GS base의 주솟값과 그와 관련된 데이터 내용을 확인할 수 있습니다.
// 사용자 모드 응용 프로그램의 디버깅 중에 실행 (Windows 10 x64 환경)
0:049> !teb
TEB at 000000061ed12000
ExceptionList: 0000000000000000
StackBase: 0000000620740000
StackLimit: 000000062073c000
SubSystemTib: 0000000000000000
FiberData: 0000000000001e00
ArbitraryUserPointer: 0000000000000000
Self: 000000061ed12000
EnvironmentPointer: 0000000000000000
ClientId: 000000000000cd9c . 0000000000011290
RpcHandle: 0000000000000000
Tls Storage: 0000000000000000
PEB Address: 000000061ecab000
LastErrorValue: 0
LastStatusValue: 0
Count Owned Locks: 0
HardErrorMode: 0
// !teb를 통해 알아낸 gs base 주소로부터 64바이트 값을 덤프
// 즉, gs:[0] ~ gs:[0x3f] 범위의 값을 8바이트 단위로 덤프
0:004> dq 000000061ed12000 L8
00000006`1ed12000 00000000`00000000 00000006`20740000
00000006`1ed12010 00000006`2073c000 00000000`00000000
00000006`1ed12020 00000000`00001e00 00000000`00000000
00000006`1ed12030 00000006`1ed12000 00000000`00000000
혹은, !teb가 출력한 주솟값을 대상으로
_TEB 구조체를 씌워 직접 모든 필드의 값을 확인하는 것도 가능합니다.
0:049> dt ntdll!_TEB 000000061ed12000 .
+0x000 NtTib :
+0x000 ExceptionList : (null)
+0x008 StackBase : 0x00000006`20740000 Void
+0x010 StackLimit : 0x00000006`2073c000 Void
+0x018 SubSystemTib : (null)
+0x020 FiberData : 0x00000000`00001e00 Void
+0x020 Version : 0x1e00
+0x028 ArbitraryUserPointer : (null)
+0x030 Self : 0x00000006`1ed12000 _NT_TIB // GS base 주소
+0x038 EnvironmentPointer :
+0x040 ClientId :
+0x000 UniqueProcess : 0x00000000`0000cd9c Void // Process ID
+0x008 UniqueThread : 0x00000000`00011290 Void // 스레드 ID
...[생략]...
위의 구조체에서 재미있는 점은 _TEB.Self 필드, 즉 gs:[0x30] 위치에 있는 값이 GS 세그먼트의 base 주소를 가리킨다는 점입니다. 이름이 의미하는 대로 자기 자신을 가리키는 포인터인데요, 실제로 !teb로 얻은 주솟값에서 0x30 위치의 값이 동일한 것을 확인할 수 있습니다.
0:049> dq 000000061ed12000 L8
00000006`1ed12000 00000000`00000000 00000006`20740000
00000006`1ed12010 00000006`2073c000 00000000`00000000
00000006`1ed12020 00000000`00001e00 00000000`00000000
00000006`1ed12030 00000006`1ed12000 00000000`00000000
다시 말해, 만약 rdmsr 명령어가 수행 가능했다면 MSR_GS_BASE(0xc0000101)를 통해 얻은 값이 0x000000061ed12000이었을 것으로 예측할 수 있습니다.
// 만약 WinDbg 사용자 모드 디버깅에서 아래의 명령어가 수행 가능했다면!
0:049> rdmsr 0xc0000101
msr[c0000101] = 00000006`1ed12000
그러고 보니, 예전 글에서 TEB를 구하기 위해 GS 레지스터를 사용한 적이 있었는데요,
x64 Visual C++에서 TEB 주소 구하는 방법
; https://www.sysnet.pe.kr/2/0/1387
따라서, C/C++에서 GS base 주소(MSR_GS_BASE)를 gs:[0x30]으로부터 구할 수 있고 결국 그 값이 gs:0의 위치를 의미한다는 것을 다음과 같이 테스트할 수 있습니다.
unsigned __int64* gsBaseAddress = (unsigned __int64*)__readgsqword(0x30);
assert(gsBaseAddress[6] == __readgsqword(0x30));
printf("GS base address (MSR_GS_BASE, rdmsr 0xc0000101): 0x%p\n", gsBaseAddress);
아래는 이것을 좀 더 확장한 예제 코드입니다.
#include <stdio.h>
#include <windows.h>
extern "C"
{
void FuncInThread1()
{
Sleep(1000);
}
void FuncInThread2()
{
Sleep(1000);
}
void PrintThreadInfo()
{
DWORD tid = GetCurrentThreadId();
printf("\nThread ID: %d (0x%x)\n", tid, tid);
for (int i = 0; i < 8 * 8; i += (8 * 2))
{
unsigned __int64 dqValue1 = __readgsqword(i);
unsigned __int64 dqValue2 = __readgsqword(i + 8);
printf("gs:[%04x] %016I64x %016I64x\n", i, dqValue1, dqValue2);
}
printf("\n");
unsigned __int64 *gsBaseAddress = (unsigned __int64* )__readgsqword(0x30);
{
for (int i = 0; i < 8; i += 2)
{
unsigned __int64 dqValue1 = gsBaseAddress[i];
unsigned __int64 dqValue2 = gsBaseAddress[i + 1];
printf("gs:[%04x] %016I64x %016I64x\n", i * 8, dqValue1, dqValue2);
}
}
{
unsigned __int64* gsBaseAddress = (unsigned __int64*)__readgsqword(0x30);
assert(gsBaseAddress[6] == __readgsqword(0x30));
printf("GS base address (MSR_GS_BASE, rdmsr 0xc0000101): 0x%p\n", gsBaseAddress);
}
}
void ThreadProc1()
{
PrintThreadInfo();
while (true)
{
FuncInThread1();
}
}
void ThreadProc2()
{
PrintThreadInfo();
while (true)
{
FuncInThread2();
}
}
int main()
{
DWORD pid = ::GetProcessId(::GetCurrentProcess());
printf("PID: %d (0x%x)\n", pid, pid);
::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, NULL);
Sleep(1000);
::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, NULL);
Sleep(-1);
}
}
실행해 보면, 이런 식의 결과가 나옵니다.
Thread ID: 55672 (0xd978)
gs:[0000] 0000000000000000 0000005937f00000
gs:[0010] 0000005937efd000 0000000000000000
gs:[0020] 0000000000001e00 0000000000000000
gs:[0030] 0000005937998000 0000000000000000
gs:[0000] 0000000000000000 0000005937f00000
gs:[0010] 0000005937efd000 0000000000000000
gs:[0020] 0000000000001e00 0000000000000000
gs:[0030] 0000005937998000 0000000000000000
GS base address (MSR_GS_BASE, rdmsr 0xc0000101): 0x0000005937998000
Thread ID: 49868 (0xc2cc)
gs:[0000] 0000000000000000 0000005938000000
gs:[0010] 0000005937ffd000 0000000000000000
gs:[0020] 0000000000001e00 0000000000000000
gs:[0030] 000000593799a000 0000000000000000
gs:[0000] 0000000000000000 0000005938000000
gs:[0010] 0000005937ffd000 0000000000000000
gs:[0020] 0000000000001e00 0000000000000000
gs:[0030] 000000593799a000 0000000000000000
GS base address (MSR_GS_BASE, rdmsr 0xc0000101): 0x000000593799A000
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]