Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 126. windbg - .NET x86 CLR2/CLR4 EXE의 EntryPoint [링크 복사], [링크+제목 복사]
조회: 11704
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 5개 있습니다.)
.NET Framework: 431. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요?
; https://www.sysnet.pe.kr/2/0/1659

.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/1746

VC++: 129. EXE를 LoadLibrary로 로딩해 PE 헤더에 있는 EntryPoint를 직접 호출하는 방법
; https://www.sysnet.pe.kr/2/0/11858

디버깅 기술: 126. windbg - .NET x86 CLR2/CLR4 EXE의 EntryPoint
; https://www.sysnet.pe.kr/2/0/11861

디버깅 기술: 127. windbg - .NET x64 EXE의 EntryPoint
; https://www.sysnet.pe.kr/2/0/11863




windbg - .NET x86 CLR2/CLR4 EXE의 EntryPoint

지난 글에서,

EXE를 LoadLibrary로 로딩해 PE 헤더에 있는 EntryPoint를 직접 호출하는 방법
; https://www.sysnet.pe.kr/2/0/11858

WinDbg로 EXE의 EntryPoint에서 BP 거는 방법
; https://www.sysnet.pe.kr/2/0/11859

실습한 내용을 .NET x86 CLR2/CLR4 EXE에 적용해 보면 어떨까요? 32비트 윈도우 환경에서 x86 windbg로 로딩하면 Native EXE와 동일하게 EntryPoint 단계까지 BP를 걸어 진행할 수 있습니다. 다음은 Windows Server 2008 x86에서 실습한 결과입니다.

0:000> bp $exentry
*** WARNING: Unable to verify checksum for ConsoleApp1.exe

0:000> g
Breakpoint 0 hit
eax=75ead3ff ebx=7ffff000 ecx=00000000 edx=739d4ddb esi=00000000 edi=00000000
eip=739d7cef esp=002cfd28 ebp=002cfd38 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
mscoree!ShellShim__CorExeMain:
739d7cef 8bff            mov     edi,edi

재미있는 점은, EntryPoint가 ConsoleApp1.exe 모듈 내의 코드가 아닌, mscoree!ShellShim__CorExeMain로 되어 있습니다. 심지어 이때의 callstack을 보면,

mscoree!ShellShim__CorExeMain
mscoree!_CorExeMain_Exported+0x8
KERNEL32!BaseThreadInitThunk+0xe
ntdll!__RtlUserThreadStart+0x23
ntdll!_RtlUserThreadStart+0x1b

ShellShim__CorExeMain보다 먼저 _CorExeMain_Exported 단계가 있는데,

mscoree!_CorExeMain_Exported:
739d4ddb 8bff            mov     edi,edi
739d4ddd 56              push    esi
739d4dde e80c2f0000      call    mscoree!ShellShim__CorExeMain (739d7cef)
739d4de3 6a00            push    0

그렇다면 _CorExeMain_Exported의 739d4ddb가 EntryPoint로써 더 적절할 텐데도 windbg는 ShellShim__CorExeMain의 739d7cef 주소를 가리킨다는 것입니다.

여기서 궁금해지는군요. ^^ windbg는 739d7cef 주소를 어떻게 계산한 것일까요? 일단, x86으로 빌드된 .NET EXE는 PE 헤더의 AddressOfEntryPoint 값이 기록되어 있으며 제가 실습한 EXE에는 AddressOfEntryPoint == 0x26ce 값이었습니다. 하지만 모듈의 로딩 주소를 보면,

0:000> lm
start    end        module name
00100000 00108000   ConsoleApp1 C (private pdb symbols)  C:\temp\ConsoleApp1.pdb
739d0000 73a1a000   mscoree    (pdb symbols)          d:\symbols\mscoree.pdb\DBEE0410B7644D97A9BFD1DB523618F02\mscoree.pdb
75e60000 75f3e000   KERNEL32   (pdb symbols)          d:\symbols\kernel32.pdb\6D189265A3F542139F85B845237355A92\kernel32.pdb
774c0000 775e9000   ntdll      (pdb symbols)          d:\symbols\ntdll.pdb\9AD18D20D0F24B41B6F15F4E91A9D47E2\ntdll.pdb

어떻게 windbg가 (00100000 + 0x26ce = ) 001026ce 값이 아닌 739d7cef를 구한 것인지 감이 안 옵니다. 어쩌면, windbg가 EntryPoint를 .NET EXE에 한해 mscoree의 CorExeMain을 보여주는 것이 아닐까요? 확인을 해보면, AddressOfEntryPoint로 계산한 001026ce 주소에는 0xff, 0x25로 시작하는 JMP DWORD PTR 코드가 있는데,

0:000> u 001026ce
ConsoleApp1!COM+_Entry_Point <PERF> (ConsoleApp1+0x26ce):
001026ce ff2500201000    jmp     dword ptr [ConsoleApp1!COM+_Entry_Point <PERF> (ConsoleApp1+0x2000) (00102000)]

0:000> db 001026ce
001026ce  ff 25 00 20 10 00 00 00-00 00 00 00 00 00 00 00  .%. ............
001026de  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

이때의 00102000 주소에 담긴 값이,

0:000> dd 00102000 L4
00102000  739d4ddb 00000000 00000048 00050002

_CorExeMain_Exported에서 봤던 그 주소 값입니다. 그러니까, 결국 AddressOfEntryPoint로 계산한 그 JMP 문에서 시작하는 것이 맞습니다. 그런데, 여기서 재미있는 점이 있습니다. AddressOfEntryPoint로 계산한 주솟값을 bp로 설정해도,

0:000> bp 001026ce

0:000> bl
 0 e 001026ce     0001 (0001)  0:**** ConsoleApp1!COM+_Entry_Point <PERF> (ConsoleApp1+0x26ce)

막상 실행해 보면, windbg는 해당 주소에서 (BP로 인한) 정지를 하지 않습니다. 물론 bp 739d4ddb 명령어로 하면 정지하는데 windbg의 이런 동작을 이해할 수가 없군요. ^^;




참고로, 코드가 별로 없는 EXE라면 windbg에서 진입점 코드를 0xff, 0x25 (JMP DWORD PTR) 바이트를 검색하는 식으로 찾을 수도 있습니다. 예를 들어 이번 글의 예제 같은 경우에는 EXE가 00100000 ~ 00108000 주소에 로드되어 있으므로 s(earch) 명령을 이용해 찾을 수 있습니다.

0:000> s 00100000 00108000 ff 25
001026ce  ff 25 00 20 10 00 00 00-00 00 00 00 00 00 00 00  .%. ............




재미 삼아서 PE 헤더에 나온 AddressOfEntryPoint 값의 파일 위치를 한번 계산해 볼까요? ^^ 제가 만든 .NET EXE의 경우 AddressOfEntryPoint == 0x26ce였고, .text 섹션의 정보는 다음과 같았습니다.

.text
    Virtual Address == 0x2000
    Raw Address == 0x200

예전에도 한번 설명했던 방법에 따라,

닷넷의 어셈블리 서명 데이터 확인 방법
; https://www.sysnet.pe.kr/2/0/10816

0x26ce - VA + RawAddr
= 0x26ce - 0x2000 + 0x200 = 000008ce

이렇게 구한 000008ce 값의 EXE (로드된 위치가 아닌) 파일 위치를 hexa 덤프로 보면,

 Offset    0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F   Ascii

000008A0  00 AD 26 00 00 00 00 00 00 00 00 00 00 00 00 5F  .?............_
000008B0  43 6F 72 45 78 65 4D 61 69 6E 00 6D 73 63 6F 72  CorExeMain.mscor
000008C0  65 65 2E 64 6C 6C 00 00 00 00 00 00 00 00 FF 25  ee.dll.........%
000008D0  00 20 40 00 00 00 00 00 00 00 00 00 00 00 00 00  ..@.............
000008E0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

결국 "JMP DWORD PTR [00402000]" 코드가 됩니다. 이에 대해서는 아래의 글에도 나오지만,

What is the Entry Point RVA in a .Net PE file?
; https://stackoverflow.com/questions/6101388/what-is-the-entry-point-rva-in-a-net-pe-file

마이크로소프트의 ECMA 제출 문서에 보면,

DzonnyDZ/Tools - Partition II Metadata.doc
; https://github.com/DzonnyDZ/Tools/blob/master/Used%20doc/CLI/Partition%20II%20Metadata.doc

Entry Point RVA	
	RVA of entry point , needs to point to bytes 0xFF 0x25 followed by the RVA in a section marked execute/read for EXEs or 0 for DLLs

무조건 이를 따르는 것으로 나옵니다. 따라서 windbg가 진입점을 AddressOfEntryPoint가 아닌, 0xff, 0x25 이후의 코드로 계산한다는 것이 크게 무리는 없어 보입니다.

그런데, 어떻게 이 코드가 실행 시에는 "JMP DWORD PTR [00102000]"으로 바뀌는 걸까요? 왜냐하면, EXE를 빌드할 때는 PE 헤더의 Optional Header에 있는 ImageBase를 기준으로 호출하는 코드를 컴파일러가 작성하기 때문입니다. 즉, PE의 ImageBase 값은 0x400000이고, 따라서 "JMP DWORD PTR [00402000]" 코드는 현재 이미지가 0x400000 메모리에 로딩되었다는 것을 가정으로 0x2000 위치에 있는 주소를 참조하는 것입니다.

이 때문에 Image의 로딩 주소가 바뀌면, 즉 windbg에서 lm 명령으로 확인한 로딩 주소인 0x100000으로 바뀌면 저 주소는 "JMP DWORD PTR [00102000]"이 되는 것입니다.




x86 .NET EXE를 64비트 윈도우에서 x86 windbg로 로드하면 다음과 같은 식의 오류가 발생합니다.

Could not create process 'c:\temp\ConsoleApplication1\Debug\ConsoleApp1.exe', Win32 error 0n50

The request is not supported.

반면 x86 .NET EXE임에도 x64 windbg로 로딩을 시도하면 정상적으로 ntdll Loader 단계까지의 진입이 됩니다. 하지만 이후 과정에서 정상적인 디버깅이 되지 않습니다.

0:000>  bp $exentry
*** WARNING: Unable to verify checksum for ConsoleApp1.exe

0:000> bl
     0 e Disable Clear  x86 0000022c`a532275e     0001 (0001)  0:**** ConsoleApp1!COM+_Entry_Point  (ConsoleApp1+0x275e)

0:000> g
Unable to insert breakpoint 0 at 0000022c`a532275e, Win32 error 0n998
    "Invalid access to memory location."
The breakpoint was set with BP.  If you want breakpoints
to track module load/unload state you must use BU.
bp0 at 0000022c`a532275e failed
WaitForEvent failed
ntdll!LdrpDoDebuggerBreak+0x31:
00007ff8`970e2cbd eb00            jmp     ntdll!LdrpDoDebuggerBreak+0x33 (00007ff8`970e2cbf)

아마도 windbg의 버그로 보이는데, 따라서 x86 .NET EXE 모듈은 64비트 환경에서 (현재는) 가능하지 않습니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/6/2019]

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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13446정성태11/16/20232416닷넷: 2161. .NET Conf 2023 - Day 1 Blazor 개요 정리
13445정성태11/15/20232679Linux: 62. 리눅스/WSL에서 CA 인증서를 저장하는 방법
13444정성태11/15/20232440닷넷: 2160. C# 12 - Experimental 특성 지원
13443정성태11/14/20232489개발 환경 구성: 687. OpenSSL로 생성한 사용자 인증서를 ASP.NET Core 웹 사이트에 적용하는 방법
13442정성태11/13/20232305개발 환경 구성: 686. 비주얼 스튜디오로 실행한 ASP.NET Core 사이트를 WSL 2 인스턴스에서 https로 접속하는 방법
13441정성태11/12/20232644닷넷: 2159. C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법파일 다운로드1
13440정성태11/11/20232349Windows: 253. 소켓 Listen 시 방화벽의 Public/Private 제어 기능이 비활성화된 경우
13439정성태11/10/20232837닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/20232457닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/20232653닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/20232874닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/20232795닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/20232606스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20232340스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/20232371오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/20232697스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20232596닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20232849닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/20232896닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/20233079닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/20233260스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/20233084닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/20233059스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/20233199닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
13421정성태10/4/20233237닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/20235253스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...