Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거

예전에 PDB 파일을 이용해 소스 코드 라인 정보를 다루는 방법을 설명한 적이 있는데요,

PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법
; https://www.sysnet.pe.kr/2/0/1763

이번에는 PDB 파일로부터 Type 정보 및 Symbol을 열거해 보겠습니다.




windbg를 사용하다 보면 dt 명령어로 구조체 정의를 확인하는 경우가 종종 있습니다.

0:007> dt _PEB
ntdll!_PEB
   +0x000 InheritedAddressSpace : UChar
   +0x001 ReadImageFileExecOptions : UChar
   +0x002 BeingDebugged    : UChar
   +0x003 BitField         : UChar
   +0x003 ImageUsesLargePages : Pos 0, 1 Bit
   ...[생략]...

위의 출력 결과로부터, _PEB 타입은 ntdll 모듈에 정의되어 있는 것인데 이렇게 windbg가 출력해 줄 수 있는 것은 타입 정보가 PDB 파일에 보관되어 있기 때문입니다. 예전에 소개한 DbgOffset 타입도,

C# - 커널 구조체의 Offset 값을 하드 코딩하지 않고 사용하는 방법
; https://www.sysnet.pe.kr/2/0/12098

타입의 구조를 모두 보여줄 수 있지만 한 가지 아쉬운 점이 있습니다. 바로 타입의 "전체 크기"를 알 수 없다는 것인데, 실제로 일부 타입은 다음과 같은 식으로 출력이 되기 때문에,

0:007> dt _KPROCESS
ntdll!_KPROCESS
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 ProfileListHead  : _LIST_ENTRY
   +0x028 DirectoryTableBase : Uint8B
   +0x030 ThreadListHead   : _LIST_ENTRY
   ...[생략]...
   +0x288 AddressPolicy    : UChar
   +0x289 Spare2           : [71] UChar
   +0x2d0 InstrumentationCallback : Ptr64 Void
   +0x2d8 SecureState      : <anonymous-tag>

마지막 필드의 크기를 가늠할 수 없습니다. 이런 경우에는 어쩔 수 없이 PDB 파일로부터 타입을 열거해 크기 정보를 받아와야 합니다.




Type 정보에 관해 알아봤으니 이번에는 Symbol 정보에 대해 설명해 보겠습니다. 역시 windbg를 사용하다 보면 "dps" 명령어를 통해 특정 모듈이 제공하는 Symbol의 값을 확인하는 경우가 있습니다. 자주 사용하는 사례로는 stack의 값들 중에서 연관된 symbol을 보여주고 싶은 경우일 텐데요,

0:007> dps @rsp
0000003d`74affa48  00007ffb`649ad4db ntdll!DbgUiRemoteBreakin+0x4b
0000003d`74affa50  00000000`00000000
0000003d`74affa58  00000000`00000000
0000003d`74affa60  00000000`00000000
0000003d`74affa68  00000000`00000000
0000003d`74affa70  00000000`00000000
0000003d`74affa78  00007ffb`63977bd4 KERNEL32!BaseThreadInitThunk+0x14
0000003d`74affa80  00000000`00000000
0000003d`74affa88  00000000`00000000
0000003d`74affa90  00000000`00000000
0000003d`74affa98  00000000`00000000
0000003d`74affaa0  00000000`00000000
0000003d`74affaa8  00007ffb`6494ced1 ntdll!RtlUserThreadStart+0x21
0000003d`74affab0  00000000`00000000
0000003d`74affab8  00000000`00000000
0000003d`74affac0  00000000`00000000

위의 출력에서는 dll이 export하고 있는 함수에 대해서만 출력이 되었지만, 명시적인 export 이외에 내부 전역 변수나 함수 등의 symbol 정보도 출력하는 것이 가능합니다. 가령, _PEB에서 소유하고 있는 KernelCallbackTable 필드의 주소를,

0:007> dt _PEB @$peb
ntdll!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   ...[생략]...
   +0x050 ProcessImagesHotPatched : 0y0
   +0x050 ReservedBits0    : 0y000000000000000000000000 (0)
   +0x054 Padding1         : [4]  ""
   +0x058 KernelCallbackTable : 0x00007ffb`63537330 Void
   ...[생략]...

dps로 덤프해 보면 user32.dll에 포함된 symbol의 주소와 연결된 것을 볼 수 있습니다.

0:007> dps 0x00007ffb`63537330 L5
00007ffb`63537330  00007ffb`634b5160 USER32!_fnCOPYDATA
00007ffb`63537338  00007ffb`6352ec60 USER32!_fnCOPYGLOBALDATA
00007ffb`63537340  00007ffb`634d2720 USER32!_fnDWORD
00007ffb`63537348  00007ffb`634d61d0 USER32!_fnNCDESTROY
00007ffb`63537350  00007ffb`634dc830 USER32!_fnDWORDOPTINLPMSG

즉, 우리가 원하는 것은 바로 저 symbol 목록을 얻고 싶은 것입니다.




사실, 이에 관해 검색해 보면 C/C++ 소스 코드로 다음과 같이 친절하게 ^^ 구현되어 있습니다.

mridgers/pdbdump.c - Small tool to list and query symbols in PDB files.
; https://gist.github.com/mridgers/2968595

소스 코드가 상당히 간결하기 때문에 C#으로의 마이그레이션도 어렵지 않은데요, 위의 코드에 따라 SymInitialize까지 호출하는 코드를 다음과 같이 작성하고,

uint options = NativeMethods.SymGetOptions();
Console.WriteLine($"SymGetOptions: {options}");

options &= ~(uint)SymOpt.SYMOPT_DEFERRED_LOADS;
options |= (uint)SymOpt.SYMOPT_LOAD_LINES;
options |= (uint)SymOpt.SYMOPT_IGNORE_NT_SYMPATH;
#if ENABLE_DEBUG_OUTPUT
options |= (uint)SymOpt.SYMOPT_DEBUG;
#endif
options |= (uint)SymOpt.SYMOPT_UNDNAME;

NativeMethods.SymSetOptions(options);

int pid = Process.GetCurrentProcess().Id;
IntPtr processHandle = NativeMethods.OpenProcess(ProcessAccessRights.PROCESS_QUERY_INFORMATION | ProcessAccessRights.PROCESS_VM_READ, false, pid);

if (NativeMethods.SymInitialize(processHandle, null, false) == false)
{
    return;
}

SymLoadModuleEx를 호출하면 되는데, 이때 PDB 파일의 경로가 필요합니다.

IntPtr baseAddress = new IntPtr((long)NativeMethods.SymLoadModuleEx(processHandle,
    IntPtr.Zero, pdbFilePath, null, baseAddress.ToInt64(), moduleSize, null, 0));

해보진 않았지만 pdbFilePath 인자에 dll 경로를 넣는 경우 symchk.exe가 가졌던 DLL들이 필요할 것이므로 그런 의존성을 제거하고 싶다면 직접 PDB 파일을 다운로드하는 방법을 쓰면 됩니다. 그러고 보니 ^^ 저번에 이런 기능을 하는 코드를 작성해 두었었죠.

C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
; https://www.sysnet.pe.kr/2/0/12094

따라서 이 코드를 곁들이면 다음과 같이 SymLoadModuleEx 처리가 완료됩니다.

PEImage pe = null;
string moduleName = "ntdll.dll";
string pdbFilePath = null;

{
    string rootPathToSave = Path.Combine(Environment.CurrentDirectory, "sym");

    pe = PEImage.FromLoadedModule(moduleName);
    pdbFilePath = pe.DownloadPdb(pe.ModulePath, rootPathToSave); // ntdll.dll에 대한 pdb 파일을 다운로드 하고,
}

IntPtr baseAddress = new IntPtr((long)NativeMethods.SymLoadModuleEx(processHandle,
    IntPtr.Zero, pdbFilePath, null, pe.BaseAddress.ToInt64(), pe.MemorySize, null, 0));

이쯤에서 "mridgers/pdbdump.c" 코드를 보면 재미있는 것이 하나 있는데, 위에서 사용한 processHandle, pe.BaseAddress, pe.MemorySize들이 꼭 정확할 필요는 없다는 것입니다. 실제로 원 저작자의 코드에서는 관련 값들을 다음과 같이 하드 코딩했고,

HANDLE g_handle = (HANDLE)0x493;
uintptr_t base_addr = 0x400000;

base_addr = (size_t)SymLoadModuleEx(g_handle, NULL, buffer, NULL,
        base_addr, 0x7fffffff, NULL, 0)

그래도 잘 동작하는 것을 확인할 수 있습니다. 하지만, 제 경우에는 그냥 PEImage 정보가 있기 때문에 그걸 이용해 정확한 값을 설정했습니다.




일단, 여기까지 마무리되었으면 이제 남은 작업은 심벌과 타입 정보를 열거하는 것입니다. 이 작업은 각각에 해당하는 함수를 호출하는 것으로 간단하게 해결됩니다.

// Symbol 열거
NativeMethods.SymEnumSymbols(processHandle, (ulong)_baseAddress.ToInt64(), "*", enum_proc, IntPtr.Zero);

// 타입 열거
NativeMethods.SymEnumTypes(processHandle, (ulong)baseAddress.ToInt64(), enum_proc, IntPtr.Zero);

private static unsafe bool enum_proc(IntPtr pinfo, uint size, IntPtr pUserContext)
{
    SYMBOL_INFO info = SYMBOL_INFO.Create(pinfo);

    // info.Name == symbol 이름
    // info.Size == 구조체 열거인 경우 크기 정보
    // info.Address == Symbol 열거인 경우 메모리 상의 위치

    return true;
}

끝입니다. 위와 같이 실행하면 Symbol 또는 Type 하나마다 enum_proc 메서드가 callback 방식으로 호출되고 첫 번째 인자로 들어오는 pinfo 정보로부터 SYMBOL_INFO 구조체의 값을 구하면 됩니다.




위의 내용들을 종합해 github에 PdbDump.cs 파일을 추가했으니,

DotNetSamples/WinConsole/PEFormat/WindowsPE/PdbDump.cs
; https://github.com/stjeong/DotNetSamples/blob/master/WinConsole/PEFormat/WindowsPE/PdbDump.cs

다음과 같이 간단하게 사용하시면 됩니다.

using KernelStructOffset;
using System;
using System.IO;
using WindowsPE;

namespace ConsoleApp1
{
    // mridgers/pdbdump.c
    // https://gist.github.com/mridgers/2968595

    // Install-Package WindowsPE
    class Program
    {
        static void Main(string[] args)
        {
            PEImage pe = null;
            string moduleName = "ntdll.dll";
            string pdbFilePath = null;

            {
                string rootPathToSave = Path.Combine(Environment.CurrentDirectory, "sym");
                pe = PEImage.FromLoadedModule(moduleName);
                pdbFilePath = pe.DownloadPdb(pe.ModulePath, rootPathToSave);
            }

            {
                PdbStore symbolStore = PdbDump.CreateSymbolStore(pdbFilePath, pe.BaseAddress, pe.MemorySize);

                foreach (SYMBOL_INFO si in symbolStore.Enumerate())
                {
                    Console.WriteLine(si.Name + " at " + si.Address.ToString("x"));
                }
            }

            {
                PdbStore typeStore = PdbDump.CreateTypeStore(pdbFilePath, pe.BaseAddress, pe.MemorySize);

                foreach (SYMBOL_INFO si in typeStore.Enumerate())
                {
                    Console.WriteLine(si.Name + ", sizeof() == " + si.Size);
                }
            }
        }
    }
}

첨부 파일은 위의 샘플 코드ntdll.dll의 Symbol 목록Type 목록이 출력된 결과입니다.




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





[최초 등록일: ]
[최종 수정일: 1/13/2020 ]

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

비밀번호

댓글 쓴 사람
 



2020-02-09 11시57분
PEImage.FromLoadedModule 호출 시점에 없는 DLL의 경우, 그냥 사전에 LoadLibrary를 호출해 주면 됩니다.

        [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
        static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);

        IntPtr baseAddress = LoadLibrary(moduleName);
정성태

1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12111정성태3/23/20201692디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [12]
12110정성태6/23/20201143디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례 [5]파일 다운로드1
12109정성태1/10/2020864오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/2020691오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/2020778.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/2020878VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
12105정성태1/8/2020810디버깅 기술: 153. C# - PEB를 조작해 로드된 DLL을 숨기는 방법
12104정성태1/9/20201262DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램 [4]
12103정성태4/23/20202153DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제 [1]파일 다운로드2
12102정성태1/6/20201353디버깅 기술: 152. User 권한(Ring 3)의 프로그램에서 _ETHREAD 주소(및 커널 메모리를 읽을 수 있다면 _EPROCESS 주소) 구하는 방법
12101정성태1/8/2020886.NET Framework: 876. C# - PEB(Process Environment Block)를 통해 로드된 모듈 목록 열람
12100정성태1/3/2020624.NET Framework: 875. .NET 3.5 이하에서 IntPtr.Add 사용
12099정성태1/3/2020910디버깅 기술: 151. Windows 10 - Process Explorer로 확인한 Handle 정보를 windbg에서 조회
12098정성태9/10/2020907.NET Framework: 874. C# - 커널 구조체의 Offset 값을 하드 코딩하지 않고 사용하는 방법 [3]
12097정성태1/2/2020666디버깅 기술: 150. windbg - Wow64, x86, x64에서의 커널 구조체(예: TEB) 구조체 확인
12096정성태1/2/2020897디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작
12095정성태12/27/20191052VC++: 135. C++ - string_view의 동작 방식
12094정성태12/26/2019920.NET Framework: 873. C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
12093정성태12/26/20191108.NET Framework: 872. C# - 로딩된 Native DLL의 export 함수 목록 출력파일 다운로드1
12092정성태12/25/2019971디버깅 기술: 148. cdb.exe를 이용해 (ntdll.dll 등에 정의된) 커널 구조체 출력하는 방법
12091정성태12/25/20191412디버깅 기술: 147. pdb 파일을 다운로드하기 위한 symchk.exe 실행에 필요한 최소 파일 [1]
12090정성태12/24/2019901.NET Framework: 871. .NET AnyCPU로 빌드된 PE 헤더의 로딩 전/후 차이점
12089정성태12/23/2019793디버깅 기술: 146. gflags와 _CrtIsMemoryBlock을 이용한 Heap 메모리 손상 여부 체크
12088정성태12/23/2019649Linux: 28. Linux - 윈도우의 "Run as different user" 기능을 shell에서 실행하는 방법
12087정성태12/21/2019756디버깅 기술: 145. windbg/sos - Dictionary의 entries 배열 내용을 모두 덤프하는 방법 (do_hashtable.py)
12086정성태12/20/2019992디버깅 기술: 144. windbg - Marshal.FreeHGlobal에서 발생한 덤프 분석 사례
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...