Microsoft MVP성태의 닷넷 이야기
닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제 [링크 복사], [링크+제목 복사],
조회: 2790
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 3개 있습니다.)
VC++: 63. 다른 프로세스에 환경 변수 설정하는 방법
; https://www.sysnet.pe.kr/2/0/1297

.NET Framework: 366. 다른 프로세스에 환경 변수 설정하는 방법 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/1438

닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제
; https://www.sysnet.pe.kr/2/0/13502




C# - 다른 프로세스의 환경변수 읽는 예제

마침 아래의 글이 있군요. ^^

Read Environment Strings of Remote Process
; https://www.codeproject.com/Articles/25647/Read-Environment-Strings-of-Remote-Process

위의 글은 32비트를 대상으로 하는데 64비트도 거의 유사하게 추적할 수 있습니다. 우선, _TEB 주소를 구해야 하는데, 이것은 GS:[0x30]에 위치하고 있습니다.

0:003> dt _TEB
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   ...[생략]...

0:003> dt _NT_TIB
ntdll!_NT_TIB
   +0x000 ExceptionList    : Ptr64 _EXCEPTION_REGISTRATION_RECORD
   +0x008 StackBase        : Ptr64 Void
   +0x010 StackLimit       : Ptr64 Void
   +0x018 SubSystemTib     : Ptr64 Void
   +0x020 FiberData        : Ptr64 Void
   +0x020 Version          : Uint4B
   +0x028 ArbitraryUserPointer : Ptr64 Void
   +0x030 Self             : Ptr64 _NT_TIB

이렇게 구한 TEB의 위치에서 PEB를 구하는데 이것은 0x60에 위치합니다.

0:003> dt _TEB
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x038 EnvironmentPointer : Ptr64 Void
   +0x040 ClientId         : _CLIENT_ID
   +0x050 ActiveRpcHandle  : Ptr64 Void
   +0x058 ThreadLocalStoragePointer : Ptr64 Void
   +0x060 ProcessEnvironmentBlock : Ptr64 _PEB
   ...[생략]...

PEB를 구했으면 그것의 0x20 위치에 ProcessParameters가 위치하고,

0:003> dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   ...[생략]...
   +0x010 ImageBaseAddress : Ptr64 Void
   +0x018 Ldr              : Ptr64 _PEB_LDR_DATA
   +0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS
   ...[생략]...

마지막으로, 0x80 위치에서 환경변수 목록을 구할 수 있습니다.

0:003> dt _RTL_USER_PROCESS_PARAMETERS
ntdll!_RTL_USER_PROCESS_PARAMETERS
   +0x000 MaximumLength    : Uint4B
   ...[생략]...
   +0x050 DllPath          : _UNICODE_STRING
   +0x060 ImagePathName    : _UNICODE_STRING
   +0x070 CommandLine      : _UNICODE_STRING
   +0x080 Environment      : Ptr64 Void
   +0x088 StartingX        : Uint4B

자, 그럼 이 과정을 그대로 C# + P/Invoke 호출을 이용해 구현하면 됩니다. ^^




하지만 실제 소스코드 구현에서는 위의 설명 과정에서 TEB로부터 PEB를 구하는 과정은 생략합니다. 왜냐하면, 처음부터 PEB를 구할 수 있기 때문인데요, "Read Environment Strings of Remote Process" 글의 소스코드에 잘 나와 있습니다. 그리고 아래는 그것을 C#으로 포팅한 예제입니다.

using System.Diagnostics;
using System.Runtime.InteropServices;
using static ConsoleApp1.NativeMethods;

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        int pid = 52436;

        foreach (var item in GetRemoteEnvironmentVariables(pid))
        {
            Console.WriteLine($"{item.Key}={item.Value}");
        }
    }

    public unsafe static Dictionary<string, string> GetRemoteEnvironmentVariables(int pid)
    {
        Dictionary<string, string> envVariables = new Dictionary<string, string>();

        PROCESS_BASIC_INFORMATION processInformation = new PROCESS_BASIC_INFORMATION();
        IntPtr processHandle = Process.GetProcessById(pid).Handle;
        int requireLength = 0;

        int pbiSize = Marshal.SizeOf<PROCESS_BASIC_INFORMATION>();
        int ntStatus = NtQueryInformationProcess(processHandle, PROCESSINFOCLASS.BasicInformation, ref processInformation, pbiSize, ref requireLength);
        if (ntStatus != 0)
        {
            Console.WriteLine("failed to get PEB address");
            return envVariables;
        }

        byte[] buffer = new byte[pbiSize];
        ulong readLength = 0;
        ReadProcessMemory(processHandle, processInformation.PebBaseAddress, buffer, pbiSize, &readLength);
        if (readLength == 0)
        {
            Console.WriteLine("failed to read PEB");
            return envVariables;
        }

        IntPtr pProcessParameters;
        // read int64 from buffer at offset 0x20
        fixed (byte* p = buffer)
        {
            pProcessParameters = Marshal.ReadIntPtr(new IntPtr(p + 0x20));
            if (pProcessParameters == IntPtr.Zero)
            {
                Console.WriteLine("failed to get _RTL_USER_PROCESS_PARAMETERS address");
                return envVariables;
            }
        }

        buffer = new byte[0x80 + 8];
        ReadProcessMemory(processHandle, pProcessParameters, buffer, buffer.Length, &readLength);
        if (readLength == 0)
        {
            Console.WriteLine("failed to read _RTL_USER_PROCESS_PARAMETERS");
            return envVariables;
        }

        IntPtr pEnvironment;
        fixed (byte* p = buffer)
        {
            pEnvironment = Marshal.ReadIntPtr(new IntPtr(p + 0x80));
            if (pEnvironment == IntPtr.Zero)
            {
                Console.WriteLine("failed to get environment address");
                return envVariables;
            }
        }

        int envSize = GetRegionSize(processHandle, pEnvironment);
        buffer = new byte[envSize];
        ReadProcessMemory(processHandle, pEnvironment, buffer, envSize, &readLength);
        if (readLength == 0)
        {
            Console.WriteLine("failed to read environment");
            return envVariables;
        }

        fixed (byte* p = buffer)
        {
            char* pEnv = (char*)p;

            while (true)
            {
                if (*pEnv == '\0') // double null terminated
                {
                    break;
                }

                string text = new string(pEnv);

                pEnv = pEnv + text.Length;
                if (*pEnv != '\0') // must be '\0';
                {
                    break;
                }

                pEnv++;

                string [] nameValue = text.Split('=');
                if (nameValue.Length < 2)
                {
                    continue;
                }

                envVariables.Add(nameValue[0], nameValue[1]);
            }
        }

        return envVariables;
    }

    static int GetRegionSize(IntPtr hProcess, IntPtr pAddress)
    {
        _MEMORY_BASIC_INFORMATION64 mbi = new _MEMORY_BASIC_INFORMATION64();
        uint mbiSize = (uint)Marshal.SizeOf<_MEMORY_BASIC_INFORMATION64>();
        int readBytes = NativeMethods.VirtualQueryEx(hProcess, pAddress, ref mbi, mbiSize);

        if (mbi.Protect == PAGE_NOACCESS ||
            mbi.Protect == PAGE_EXECUTE)
        {
            return 0;
        }

        if (readBytes != mbiSize)
        {
            return 0;
        }

        ulong diff = (ulong)pAddress - (ulong)mbi.BaseAddress;

        return (int)mbi.RegionSize - (int)diff;
    }
}

위에서, 재미있는 부분이 하나 있다면, 환경변수 주소를 가져온 다음 그 영역을 읽어내는 것입니다. 사실, _RTL_USER_PROCESS_PARAMETERS 구조체에는 환경변수 주소만 있고 그 크기에 대한 정보가 없습니다.

"Read Environment Strings of Remote Process" 글에서는 이 부분을 VirtualQueryEx를 통해 해결하고 있는데요,

BOOL CProcessEnvReader::HasReadAccess( HANDLE hProcess,
                                        void* pAddress, int& nSize )
{
    MEMORY_BASIC_INFORMATION memInfo;
    __try
    {
        VirtualQueryEx( hProcess, pAddress,&memInfo,sizeof(memInfo));
        if( PAGE_NOACCESS == memInfo.Protect ||
        PAGE_EXECUTE == memInfo.Protect )
        {
            nSize = 0;
            return FALSE;
        }
        nSize = memInfo.RegionSize;
        return TRUE;
    }
    __except( SHOW_ERR_DLG( _T("Failed to query memory access")))
    {
    }
    return FALSE;
} 

전에 저도 이 함수를 이용해 스택 메모리의 상태 출력을 한 적이 있습니다.

C# - VMMap처럼 스택 메모리의 reserve/guard/commit 상태 출력
; https://www.sysnet.pe.kr/2/0/13174

그런데, 위의 소스 코드에는 문제가 하나 있습니다. 환경변수를 가리키는 pAddress로 전달한 주소가 해당 Region의 시작 주소와 일치하지 않기 때문에 전체 RegionSize를 반환해 버리면 저 영역을 넘어선 부분까지 읽기 시도를 할 수가 있습니다.

아마도 저 소스코드는 32비트에서 테스트해서 운이 좋게 다음 Region도 commit 영역이었을 확률이 높지만 x64의 광활한 메모리 공간에서는 오류 확률이 매우 높게 됩니다.

따라서, BaseAddress로부터 pAddress가 떨어진 만큼의 크기를 RegionSize에서 빼서 반환하는 보정을 거쳐야 합니다.

static int GetRegionSize(IntPtr hProcess, IntPtr pAddress)
{
    _MEMORY_BASIC_INFORMATION64 mbi = new _MEMORY_BASIC_INFORMATION64();
    uint mbiSize = (uint)Marshal.SizeOf<_MEMORY_BASIC_INFORMATION64>();
    int readBytes = NativeMethods.VirtualQueryEx(hProcess, pAddress, ref mbi, mbiSize);

    if (mbi.Protect == PAGE_NOACCESS ||
        mbi.Protect == PAGE_EXECUTE)
    {
        return 0;
    }

    if (readBytes != mbiSize)
    {
        return 0;
    }

    ulong diff = (ulong)pAddress - (ulong)mbi.BaseAddress;8
    return (int)mbi.RegionSize - (int)diff;
}

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

참고로, PEB, _RTL_USER_PROCESS_PARAMETERS의 offset 위치가 운영체제/패치마다 다를 수도 있습니다. 아마도 환경변수 영역은 거의 변하지 않을 거라 예상은 하지만 그래도 안전하게 하고 싶다면 "KernelStructOffset" 등의 도움을 받으면 됩니다.




보통, TEB를 가져오기 위한 용도로 (x64의 경우) GS 세그먼트를 사용한 연산을 합니다. 일례로, Visual C++의 __readgsqword 함수를 사용하면 이렇게 TEB를 가져올 수 있는데요,

unsigned __int64 fsSelf = __readgsqword(0x30);

기계어로는 다음과 같이 번역됩니다.

mov         rax,qword ptr gs:[30h]  
mov         qword ptr [fsReg],rax 

그런데 재미있는 건, 0x30h의 값이 바로 TEB 자신의 주솟값이라는 것입니다.

lkd> dt _TEB
nt!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x038 EnvironmentPointer : Ptr64 Void
   +0x040 ClientId         : _CLIENT_ID
   +0x050 ActiveRpcHandle  : Ptr64 Void
...[생략]...

lkd> dt _NT_TIB
nt!_NT_TIB
   +0x000 ExceptionList    : Ptr64 _EXCEPTION_REGISTRATION_RECORD
   +0x008 StackBase        : Ptr64 Void
   +0x010 StackLimit       : Ptr64 Void
   +0x018 SubSystemTib     : Ptr64 Void
   +0x020 FiberData        : Ptr64 Void
   +0x020 Version          : Uint4B
   +0x028 ArbitraryUserPointer : Ptr64 Void
   +0x030 Self             : Ptr64 _NT_TIB

재미있지 않나요? ^^ 그냥 gs 레지스터가 가리키는 값을 가져오면 될 텐데, 왜 굳이 gs:[0x30]으로 가는 방식으로 정해서 구조체에까지 영향을 주는 식으로 정의를 했을까요?

사실 GS는 일반적인 레지스터가 아닌 세그먼트 레지스터라서 그것 자체의 값은 주소가 아닙니다.

0:007> r gs
gs=002b

그리고 저 Selector에 해당하는 정보를 구하면,

0:007> dg gs
                                                    P Si Gr Pr Lo
Sel        Base              Limit          Type    l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
002B 00000000`00000000 00000000`ffffffff Data RW Ac 3 Bg Pg P  Nl 00000cf3

의외로 Base 주솟값이 00000000`00000000으로 나옵니다. 아니, 그렇다면 gs:[0x30]은 결국 0x30 가상 주소를 가리키는 것과 같을 텐데요, 당연히 이 영역은 null 주소에 가까운 페이지라서 커밋이 안 돼 있는 영역입니다.

0:007> dd 0x30 L4
00000000`00000030  ???????? ???????? ???????? ????????

이게 가능한 이유는,

windbg - Why does the GS register resolve to offset 0x0?
; https://reverseengineering.stackexchange.com/questions/21033/windbg-why-does-the-gs-register-resolve-to-offset-0x0

특별히, gs의 경우에는 MSR(Model Specific Registers)에 보관된 값을 base address로 사용하기 때문이라고 합니다.

SWAPGS
; https://wiki.osdev.org/SWAPGS

위의 문서에 보면,

(IA32_FS_BASE) FSBase is MSR 0xC0000100, 
(IA32_GS_BASE) GSBase is 0xC0000101, 
(IA32_KERNEL_GS_BASE) and KernelGSBase is 0xC0000102.

// 그 외, IA32_DS_AREA, IA32_L-STAR, IA32_SYSENTER_EIP, IA32_SYSENTER_ESP

MSR에서 GS가 참조하는 주솟값이 나옵니다.




마지막으로, 다음과 같이 코딩을 하면,

; gs_register.asm
; MASM - "ml64.exe /c /nologo /Zi /Fo"x64\Debug\gs_register.obj" /W3 /errorReport:prompt  /Tags_register.asm"

    .code

get_gs_addr proc
    mov rax, ds:[20]    
    ret

get_gs_addr endp

end

"error A2202: illegal use of segment register" 오류가 발생합니다. 검색해 보면, 64비트 모드에서 CS/DS/ES/SS를 오버라이드할 수는 있지만 실제로는 무시되기 때문에 MASM의 경우에는 아예 지정하지 못하게 막았고, 다른 어셈블러들은 허용했다고 합니다. 그래도 이게 아주 쓸모없지는 않은 게, CS/DS/ES/SS로 지정함으로써 명령어 길이를 늘려 메모리 정렬을 맞추기 위한 패딩 용도로 쓴다고 합니다.

반면, 원래 이 글에서 우리가 원했던 표현은,

mov rax, gs:0 // error A2027: operand must be a memory expression

GS가 명시된 경우 오퍼랜드로 가능한 것은 반드시 메모리 (주솟값 자체가 아닌) 참조만 가능하다고 합니다. 만약 저 표현이 가능했다면 "mov rax, gs:[0x30]" 코드는 필요 없었을 것입니다.




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







[최초 등록일: ]
[최종 수정일: 12/28/2023]

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

비밀번호

댓글 작성자
 




... 31  32  33  [34]  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12801정성태8/23/20218725.NET Framework: 1101. C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용파일 다운로드1
12800정성태8/22/20218958개발 환경 구성: 599. PyCharm - (반대로) 원격 프로세스가 PyCharm에 디버그 연결하는 방법
12799정성태8/22/20218972.NET Framework: 1100. C# 10 - (5) 속성 패턴의 개선파일 다운로드1
12798정성태8/21/202110436개발 환경 구성: 598. PyCharm - 원격 프로세스를 디버그하는 방법
12797정성태8/21/20218048Windows: 197. TCP의 MSS(Maximum Segment Size) 크기는 고정된 것일까요?
12796정성태8/21/20218670.NET Framework: 1099. C# 10 - (4) 상수 문자열에 포맷 식 사용 가능파일 다운로드1
12795정성태8/20/20219343.NET Framework: 1098. .NET 6에 포함된 신규 BCL API - 스레드 관련
12794정성태8/20/20218807스크립트: 23. 파이썬 - WSGI를 만족하는 최소한의 구현 코드 및 PyCharm에서의 디버깅 방법 [1]
12793정성태8/20/20219484.NET Framework: 1097. C# 10 - (3) 개선된 변수 초기화 판정파일 다운로드1
12792정성태8/19/20219928.NET Framework: 1096. C# 10 - (2) 전역 네임스페이스 선언파일 다운로드1
12791정성태8/19/20218281.NET Framework: 1095. C# COM 개체를 C++에서 사용하는 예제 [3]파일 다운로드1
12790정성태8/18/202110564.NET Framework: 1094. C# 10 - (1) 구조체를 생성하는 record struct파일 다운로드1
12789정성태8/18/20219654개발 환경 구성: 597. PyCharm - 윈도우 환경에서 WSL을 이용해 파이썬 앱 개발/디버깅하는 방법
12788정성태8/17/20218115.NET Framework: 1093. C# - 인터페이스의 메서드가 다형성을 제공할까요? (virtual일까요?)파일 다운로드1
12787정성태8/17/20218338.NET Framework: 1092. (책 내용 수정) "4.5.1.4 인터페이스"의 "인터페이스와 다형성"
12786정성태8/16/20219908.NET Framework: 1091. C# - Python range 함수 구현 (2) INumber<T>를 이용한 개선 [1]파일 다운로드1
12785정성태8/16/20218164.NET Framework: 1090. .NET 6 Preview 7에 추가된 숫자 형식에 대한 제네릭 연산 지원 [1]파일 다운로드1
12784정성태8/15/20217602오류 유형: 757. 구글 메일 - 아웃룩에서 메일 전송 시 Sending' reported error (0x800CCC0F, 0x800CCC92)
12783정성태8/15/20217085.NET Framework: 1089. C# - Indexer에 Range 및 람다 식을 이용한 필터 구현 [1]파일 다운로드1
12782정성태8/14/20216888오류 유형: 756. 파이썬 - 윈도우 환경에서 pytagcloud의 한글 출력 방법
12781정성태8/14/20219119오류 유형: 755. 파이썬 - konlpy 사용 시 JVM과 jpype1 관련 오류
12780정성태8/13/20217472.NET Framework: 1088. C# - 버스 노선 및 위치 정보 조회 API 사용을 위한 기초 라이브러리 [2]
12779정성태8/13/20219340개발 환경 구성: 596. 공공 데이터 포털에서 버스 노선 및 위치 정보 조회 API 사용법
12778정성태8/12/20216544오류 유형: 755. PyCharm - "Manage Repositories"의 목록이 나오지 않는 문제
12777정성태8/12/20218122오류 유형: 754. Visual Studio - Input or output cannot be redirected because the specified file is invalid.
12776정성태8/12/20217442오류 유형: 753. gunicorn과 uwsgi 함께 사용 시 ERR_CONNECTION_REFUSED
... 31  32  33  [34]  35  36  37  38  39  40  41  42  43  44  45  ...