Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 8개 있습니다.)
디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
; https://www.sysnet.pe.kr/2/0/13500

디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
; https://www.sysnet.pe.kr/2/0/13836

디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN)
; https://www.sysnet.pe.kr/2/0/13844

디버깅 기술: 207. Windbg로 알아보는 PTE (_MMPTE)
; https://www.sysnet.pe.kr/2/0/13845

디버깅 기술: 208. Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형
; https://www.sysnet.pe.kr/2/0/13846

디버깅 기술: 209. Windbg로 알아보는 Prototype PTE
; https://www.sysnet.pe.kr/2/0/13848

디버깅 기술: 210. Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
; https://www.sysnet.pe.kr/2/0/13849

디버깅 기술: 212. Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터
; https://www.sysnet.pe.kr/2/0/13852




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

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/26/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 61  62  63  64  65  66  67  68  69  70  71  72  [73]  74  75  ...
NoWriterDateCnt.TitleFile(s)
12106정성태1/8/202019579VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
12105정성태1/8/202018073디버깅 기술: 153. C# - PEB를 조작해 로드된 DLL을 숨기는 방법
12104정성태1/7/202019249DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램 [4]
12103정성태1/7/202022346DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제 [1]파일 다운로드2
12102정성태1/6/202018739디버깅 기술: 152. User 권한(Ring 3)의 프로그램에서 _ETHREAD 주소(및 커널 메모리를 읽을 수 있다면 _EPROCESS 주소) 구하는 방법
12101정성태1/5/202018923.NET Framework: 876. C# - PEB(Process Environment Block)를 통해 로드된 모듈 목록 열람
12100정성태1/3/202016403.NET Framework: 875. .NET 3.5 이하에서 IntPtr.Add 사용
12099정성태1/3/202019236디버깅 기술: 151. Windows 10 - Process Explorer로 확인한 Handle 정보를 windbg에서 조회 [1]
12098정성태1/2/202018993.NET Framework: 874. C# - 커널 구조체의 Offset 값을 하드 코딩하지 않고 사용하는 방법 [3]
12097정성태1/2/202017097디버깅 기술: 150. windbg - Wow64, x86, x64에서의 커널 구조체(예: TEB) 구조체 확인
12096정성태12/30/201919830디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작 [1]
12095정성태12/27/201921502VC++: 135. C++ - string_view의 동작 방식
12094정성태12/26/201919214.NET Framework: 873. C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
12093정성태12/26/201918789.NET Framework: 872. C# - 로딩된 Native DLL의 export 함수 목록 출력파일 다운로드1
12092정성태12/25/201917639디버깅 기술: 148. cdb.exe를 이용해 (ntdll.dll 등에 정의된) 커널 구조체 출력하는 방법
12091정성태12/25/201919914디버깅 기술: 147. pdb 파일을 다운로드하기 위한 symchk.exe 실행에 필요한 최소 파일 [1]
12090정성태12/24/201920014.NET Framework: 871. .NET AnyCPU로 빌드된 PE 헤더의 로딩 전/후 차이점 [1]파일 다운로드1
12089정성태12/23/201918906디버깅 기술: 146. gflags와 _CrtIsMemoryBlock을 이용한 Heap 메모리 손상 여부 체크
12088정성태12/23/201917891Linux: 28. Linux - 윈도우의 "Run as different user" 기능을 shell에서 실행하는 방법
12087정성태12/21/201918338디버깅 기술: 145. windbg/sos - Dictionary의 entries 배열 내용을 모두 덤프하는 방법 (do_hashtable.py) [1]
12086정성태12/20/201920783디버깅 기술: 144. windbg - Marshal.FreeHGlobal에서 발생한 덤프 분석 사례
12085정성태12/20/201918772오류 유형: 586. iisreset - The data is invalid. (2147942413, 8007000d) 오류 발생 - 두 번째 이야기 [1]
12084정성태12/19/201919226디버깅 기술: 143. windbg/sos - Hashtable의 buckets 배열 내용을 모두 덤프하는 방법 (do_hashtable.py) [1]
12083정성태12/17/201922206Linux: 27. linux - lldb를 이용한 .NET Core 응용 프로그램의 메모리 덤프 분석 방법 [2]
12082정성태12/17/201920509오류 유형: 585. lsof: WARNING: can't stat() fuse.gvfsd-fuse file system
12081정성태12/16/201922358개발 환경 구성: 465. 로컬 PC에서 개발 중인 ASP.NET Core 웹 응용 프로그램을 다른 PC에서도 접근하는 방법 [5]
... 61  62  63  64  65  66  67  68  69  70  71  72  [73]  74  75  ...