성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - 다른 프로세스의 환경변수 읽는 예제</h1> <p> 마침 아래의 글이 있군요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Read Environment Strings of Remote Process ; <a target='tab' href='https://www.codeproject.com/Articles/25647/Read-Environment-Strings-of-Remote-Process'>https://www.codeproject.com/Articles/25647/Read-Environment-Strings-of-Remote-Process</a> </pre> <br /> 위의 글은 32비트를 대상으로 하는데 64비트도 거의 유사하게 추적할 수 있습니다. 우선, _TEB 주소를 구해야 하는데, 이것은 GS:[0x30]에 위치하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:003> <span style='color: blue; font-weight: bold'>dt _TEB</span> ntdll!_TEB +0x000 NtTib : _NT_TIB ...[생략]... 0:003> <span style='color: blue; font-weight: bold'>dt _NT_TIB</span> 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 <span style='color: blue; font-weight: bold'>+0x030 Self : Ptr64 _NT_TIB</span> </pre> <br /> 이렇게 구한 TEB의 위치에서 PEB를 구하는데 이것은 0x60에 위치합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 <span style='color: blue; font-weight: bold'>+0x060 ProcessEnvironmentBlock : Ptr64 _PEB</span> ...[생략]... </pre> <br /> PEB를 구했으면 그것의 0x20 위치에 ProcessParameters가 위치하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:003> <span style='color: blue; font-weight: bold'>dt _PEB</span> ntdll!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar ...[생략]... +0x010 ImageBaseAddress : Ptr64 Void +0x018 Ldr : Ptr64 _PEB_LDR_DATA <span style='color: blue; font-weight: bold'>+0x020 ProcessParameters : Ptr64 _RTL_USER_PROCESS_PARAMETERS</span> ...[생략]... </pre> <br /> 마지막으로, 0x80 위치에서 환경변수 목록을 구할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:003&gt; 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 <span style='color: blue; font-weight: bold'>+0x080 Environment : Ptr64 Void</span> +0x088 StartingX : Uint4B </pre> <br /> 자, 그럼 이 과정을 그대로 C# + P/Invoke 호출을 이용해 구현하면 됩니다. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 하지만 실제 소스코드 구현에서는 위의 설명 과정에서 TEB로부터 PEB를 구하는 과정은 생략합니다. 왜냐하면, 처음부터 PEB를 구할 수 있기 때문인데요, "<a target='tab' href='https://www.codeproject.com/Articles/25647/Read-Environment-Strings-of-Remote-Process'>Read Environment Strings of Remote Process</a>" 글의 소스코드에 잘 나와 있습니다. 그리고 아래는 그것을 C#으로 포팅한 예제입니다.<br /> <br /> <pre style='height: 400px; margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; } } </pre> <br /> 위에서, 재미있는 부분이 하나 있다면, 환경변수 주소를 가져온 다음 그 영역을 읽어내는 것입니다. 사실, _RTL_USER_PROCESS_PARAMETERS 구조체에는 환경변수 주소만 있고 그 크기에 대한 정보가 없습니다.<br /> <br /> "<a target='tab' href='https://www.codeproject.com/Articles/25647/Read-Environment-Strings-of-Remote-Process'>Read Environment Strings of Remote Process</a>" 글에서는 이 부분을 VirtualQueryEx를 통해 해결하고 있는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; } </pre> <br /> 전에 저도 이 함수를 이용해 스택 메모리의 상태 출력을 한 적이 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - VMMap처럼 스택 메모리의 reserve/guard/commit 상태 출력 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13174'>https://www.sysnet.pe.kr/2/0/13174</a> </pre> <br /> 그런데, 위의 소스 코드에는 문제가 하나 있습니다. 환경변수를 가리키는 pAddress로 전달한 주소가 해당 Region의 시작 주소와 일치하지 않기 때문에 전체 RegionSize를 반환해 버리면 저 영역을 넘어선 부분까지 읽기 시도를 할 수가 있습니다.<br /> <br /> 아마도 저 소스코드는 32비트에서 테스트해서 운이 좋게 다음 Region도 commit 영역이었을 확률이 높지만 x64의 광활한 메모리 공간에서는 오류 확률이 매우 높게 됩니다.<br /> <br /> 따라서, BaseAddress로부터 pAddress가 떨어진 만큼의 크기를 RegionSize에서 빼서 반환하는 보정을 거쳐야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; } <span style='color: blue; font-weight: bold'>ulong diff = (ulong)pAddress - (ulong)mbi.BaseAddress;</span>8 <span style='color: blue; font-weight: bold'>return (int)mbi.RegionSize - (int)diff;</span> } </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=2126&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> 참고로, PEB, _RTL_USER_PROCESS_PARAMETERS의 offset 위치가 운영체제/패치마다 다를 수도 있습니다. 아마도 환경변수 영역은 거의 변하지 않을 거라 예상은 하지만 그래도 안전하게 하고 싶다면 "<a target='tab' href='https://www.sysnet.pe.kr/2/0/12098'>KernelStructOffset</a>" 등의 도움을 받으면 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 보통, TEB를 가져오기 위한 용도로 (x64의 경우) GS 세그먼트를 사용한 연산을 합니다. 일례로, Visual C++의 <a target='tab' href='https://www.sysnet.pe.kr/2/0/1387'>__readgsqword 함수를 사용하면 이렇게 TEB</a>를 가져올 수 있는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > unsigned __int64 fsSelf = __readgsqword(0x30); </pre> <br /> 기계어로는 다음과 같이 번역됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > mov rax,qword ptr gs:[30h] mov qword ptr [fsReg],rax </pre> <br /> 그런데 재미있는 건, 0x30h의 값이 바로 TEB 자신의 주솟값이라는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > lkd> <span style='color: blue; font-weight: bold'>dt _TEB</span> nt!_TEB +0x000 NtTib : _NT_TIB +0x038 EnvironmentPointer : Ptr64 Void +0x040 ClientId : _CLIENT_ID +0x050 ActiveRpcHandle : Ptr64 Void ...[생략]... lkd> <span style='color: blue; font-weight: bold'>dt _NT_TIB</span> 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 <span style='color: blue; font-weight: bold'>+0x030 Self : Ptr64 _NT_TIB</span> </pre> <br /> 재미있지 않나요? ^^ 그냥 gs 레지스터가 가리키는 값을 가져오면 될 텐데, 왜 굳이 gs:[0x30]으로 가는 방식으로 정해서 구조체에까지 영향을 주는 식으로 정의를 했을까요?<br /> <br /> 사실 GS는 일반적인 레지스터가 아닌 세그먼트 레지스터라서 그것 자체의 값은 주소가 아닙니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:007> <span style='color: blue; font-weight: bold'>r gs</span> gs=002b </pre> <br /> 그리고 저 Selector에 해당하는 정보를 구하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:007> <span style='color: blue; font-weight: bold'>dg gs</span> 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 </pre> <br /> 의외로 Base 주솟값이 00000000`00000000으로 나옵니다. 아니, 그렇다면 gs:[0x30]은 결국 0x30 가상 주소를 가리키는 것과 같을 텐데요, 당연히 이 영역은 null 주소에 가까운 페이지라서 커밋이 안 돼 있는 영역입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:007> <span style='color: blue; font-weight: bold'>dd 0x30 L4</span> 00000000`00000030 ???????? ???????? ???????? ???????? </pre> <br /> 이게 가능한 이유는,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > windbg - Why does the GS register resolve to offset 0x0? ; <a target='tab' href='https://reverseengineering.stackexchange.com/questions/21033/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</a> </pre> <br /> 특별히, gs의 경우에는 MSR(Model Specific Registers)에 보관된 값을 base address로 사용하기 때문이라고 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > SWAPGS ; <a target='tab' href='https://wiki.osdev.org/SWAPGS'>https://wiki.osdev.org/SWAPGS</a> </pre> <br /> 위의 문서에 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > (IA32_FS_BASE) FSBase is MSR 0xC0000100, (IA32_GS_BASE) GSBase is 0xC0000101, (IA32_KERNEL_GS_BASE) and KernelGSBase is 0xC0000102. // <a target='tab' href='https://www.felixcloutier.com/x86/wrmsr'>그 외, IA32_DS_AREA, IA32_L-STAR, IA32_SYSENTER_EIP, IA32_SYSENTER_ESP</a> </pre> <br /> MSR에서 GS가 참조하는 주솟값이 나옵니다. <br /> <br /> <hr style='width: 50%' /><br /> <br /> 마지막으로, 다음과 같이 코딩을 하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ; 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 <span style='color: blue; font-weight: bold'>mov rax, ds:[20]</span> ret get_gs_addr endp end </pre> <br /> "error A2202: illegal use of segment register" 오류가 발생합니다. <a target='tab' href='https://stackoverflow.com/questions/50400274/why-is-the-use-of-the-ds-segment-override-illegal-in-64-bit-mode'>검색해 보면</a>, 64비트 모드에서 CS/DS/ES/SS를 오버라이드할 수는 있지만 실제로는 무시되기 때문에 MASM의 경우에는 아예 지정하지 못하게 막았고, 다른 어셈블러들은 허용했다고 합니다. 그래도 이게 아주 쓸모없지는 않은 게, CS/DS/ES/SS로 지정함으로써 명령어 길이를 늘려 메모리 정렬을 맞추기 위한 패딩 용도로 쓴다고 합니다.<br /> <br /> 반면, 원래 이 글에서 우리가 원했던 표현은,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > mov rax, gs:0 // error A2027: operand must be a memory expression </pre> <br /> GS가 명시된 경우 오퍼랜드로 가능한 것은 반드시 메모리 (주솟값 자체가 아닌) 참조만 가능하다고 합니다. 만약 저 표현이 가능했다면 "mov rax, gs:[0x30]" 코드는 필요 없었을 것입니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1813
(왼쪽의 숫자를 입력해야 합니다.)