성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
[정성태] 저렇게 조각 코드 말고, 실제로 재현이 되는 예제 프로젝트를 압...
[정성태] Modules 창(Ctrl+Shift+U)을 띄워서, 해당 Op...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <div style='font-family: 맑은 고딕, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>라이선스까지도 뛰어넘는 .NET Profiler</div> <br /> .NET Profiler에 대해 공부해 본 분이라면, 아래의 글에 대해서 읽어보셨을 텐데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > No Code Can Hide from the Profiling API in the .NET Framework 2.0 ; <a target='_tab' href='https://learn.microsoft.com/en-us/archive/msdn-magazine/2005/january/no-code-can-hide-from-the-profiling-api-in-the-net-framework-2-0'>https://learn.microsoft.com/en-us/archive/msdn-magazine/2005/january/no-code-can-hide-from-the-profiling-api-in-the-net-framework-2-0</a> </pre> <br /> 혹시나, 위의 글을 못 보셨다고 해도... "No Code"의 범위가 어디까지인지를 실감할 수 있도록 이번 글에서 한번 다뤄볼까 하는데요. 이를 위해 제가 선택한 사례는 바로 (어찌 보면 가장 민감할 수 있는) '라이선스'입니다.<br /> <br /> 지난번에, .NET에서 기본 제공되는 라이선스 체계에 대한 설명을 했었지요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > 닷넷 System.ComponentModel.LicenseManager를 이용한 라이선스 적용 ; <a target='_tab' href='http://www.sysnet.pe.kr/2/0/1045'>http://www.sysnet.pe.kr/2/0/1045</a> </pre> <br /> 사실, 위의 글은 원래 이번 글을 위해 미리 라이선스 지식을 전달하기 위해 씌여진 것입니다. 이제 본격적으로, 위의 지식을 바탕으로 라이선스를 우회할 수 있는 .NET Profiler를 제작해 보겠습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 복습 겸 해서, 닷넷의 System.ComponentModel.LicenseManager 기능에 대해 정리를 해볼까요? 단적인 예로, System.ComponentModel.LicenseManager를 사용하는 대부분의 '상용 컴포넌트'들은 그들이 만든 클래스의 생성자에 보통 다음과 같은 구문을 넣어둡니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > private MyLicensedComponent() { LicenseManager.Validate(typeof(MyLicensedComponent), this); // 이후 구성 요소 초기화 작업 코드 실행 } </pre> <br /> LicenseManager.Validate 메서드는 전달된 Type에 지정된 LicenseProviderAttribute 특성을 읽어들여 LicenseProvider를 상속받은 타입을 생성하고 System.ComponentModel.LicenseProvider.GetLicense 메서드를 불러서 License 개체를 반환받게 되어 있습니다. 만약, 이 과정에서 정상적으로 License 개체를 반환받지 못하면 보통 System.ComponentModel.LicenseException 예외가 발생하게 되고, Validate 메서드를 호출한 생성자에서는 예외 발생으로 인해 이후의 초기화 코드가 실행될 수 없어 개체 자체가 정상적인 역할을 하지 못하게 되는 것입니다.<br /> <br /> 라이선스 우회를 하기 위해 .NET Profiler에서 해야 할 일은 간단합니다. 바로 LicenseManager.Validate의 코드를 아무것도 하지 않도록 바꿔주면 됩니다.<br /> <br /> .NET Reflector를 통해서 원래의 LicenseManager.Validate 정의를 보면 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > public static License Validate(Type type, object instance) { License license; if (!ValidateInternal(type, instance, true, out license)) { throw new LicenseException(type, instance); } return license; } </pre> <br /> 그렇다면, 위의 메서드 정의를 이렇게 바꿔버리면 되는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > public static License Validate(Type type, object instance) { return null; } </pre> <br /> 이번 글에서는, 실제로 위와 같은 동작을 하는 .NET Profiler를 ATL COM 프로젝트로 만들어 보겠습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 우선, ATL Simple 유형의 프로젝트를 생성하고, COM 개체를 하나 추가합니다. (이 예제에서는 COM 개체 이름을 CIgnore로 정했습니다.) 그런 후, .NET Profiler가 구현해야할 인터페이스인 ICorProfilerCallback, ICorProfilerCallback2, ICorProfilerCallback3의 뼈대를 담은 Impl 헤더를 마련해줍니다.<br /> <br /> <ul> <li>ICorProfilerCallbackImpl.h</li> <li>ICorProfilerCallbackImpl2.h</li> <li>ICorProfilerCallbackImpl3.h</li> </ul> <br /> 위의 파일들은 대략 아래와 같은 내용의 헤더 파일입니다. (선언해 주어야 할 메서드 목록은 MSDN 도움말에서 ICorProfilerCallback, ICorProfilerCallback2, ICorProfilerCallback3 인터페이스를 찾아보시면 됩니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > template<class T> class ATL_NO_VTABLE ICorProfilerCallbackImpl : public <b style='COLOR: blue'>ICorProfilerCallback</b> { public: ICorProfilerCallbackImpl() {}; virtual ~ICorProfilerCallbackImpl() {}; STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0; _ATL_DEBUG_ADDREF_RELEASE_IMPL(ICorProfilerCallbackImpl) <b style='COLOR: blue'> STDMETHOD(Initialize)(IUnknown * pICorProfilerInfoUnk) { return E_NOTIMPL; }</b> STDMETHOD(Shutdown)() { return E_NOTIMPL; } ...[생략]... }; </pre> <br /> 이제 Ignore.h 헤더 파일에서 ICorProfilerCallback3Impl을 상속시켜 주고, COM_INTERFACE_ENTRY에도 추가해줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > #include "ICorProfilerCallback3Impl.h" class ATL_NO_VTABLE CIgnore : public CComObjectRootEx<CComMultiThreadModel>, public CComCoClass<CIgnore, &CLSID_Ignore>, public IDispatchImpl<IIgnore, &IID_IIgnore, &LIBID_OverrideLicenseLib, /*wMajor =*/ 1, /*wMinor =*/ 0>, <b style='COLOR: blue'>public ICorProfilerCallback3Impl<CIgnore></b> { ...[생략]... BEGIN_COM_MAP(CIgnore) COM_INTERFACE_ENTRY(IIgnore) COM_INTERFACE_ENTRY(IDispatch) <b style='COLOR: blue'> COM_INTERFACE_ENTRY(ICorProfilerCallback) COM_INTERFACE_ENTRY(ICorProfilerCallback2) COM_INTERFACE_ENTRY(ICorProfilerCallback3)</b> END_COM_MAP() ...[생략]... </pre> <br /> <div style='font-size: 12pt; font-family: 맑은 고딕, Consolas; color: #2211AA; text-align: left; font-weight: bold'>ICorProfilerCallback::Initialize 구현</div><br /> <br /> .NET Profiler가 CLR에 의해 로드되면 최초로 ICorProfilerCallback::Initialize 함수를 호출해 주는데, 이 때 임의의 초기화 코드를 넣어줄 수 있습니다. 따라서, CIgnore 클래스의 .h/.cpp 파일에 각각 Initialize 선언과 정의 추가를 추가해 줍니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > ==== Ignore.h ==== STDMETHOD(Initialize)(IUnknown * pICorProfilerInfoUnk); CComQIPtr<ICorProfilerInfo> m_pICorProfilerInfo; // .NET 1.0 때부터 지원되는 프로파일러 인터페이스 CComQIPtr<ICorProfilerInfo2> m_pICorProfilerInfo2; // .NET 2.0 때부터 지원되는 프로파일러 인터페이스 CComQIPtr<ICorProfilerInfo3> m_pICorProfilerInfo3; // .NET 4.0 때부터 지원되는 프로파일러 인터페이스 </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > ==== Ignore.cpp ==== HRESULT CIgnore::Initialize(IUnknown * pICorProfilerInfoUnk) { if (pICorProfilerInfoUnk == NULL) { return S_OK; } m_pICorProfilerInfo = pICorProfilerInfoUnk; if (m_pICorProfilerInfo == NULL) { return S_OK; } m_pICorProfilerInfo2 = pICorProfilerInfoUnk; m_pICorProfilerInfo3 = pICorProfilerInfoUnk; <b style='COLOR: blue'>DWORD dwEventMask = COR_PRF_MONITOR_JIT_COMPILATION | COR_PRF_USE_PROFILE_IMAGES;</b> <b style='COLOR: blue'>m_pICorProfilerInfo->SetEventMask(dwEventMask);</b> ::OutputDebugString(L"Profiler Initialize - End!\r\n"); return S_OK; } </pre> <br /> 위의 코드에서 중요한 것은 ICorProfilerInfo::SetEventMask 메서드에 전달되는 COR_PRF_MONITOR_JIT_COMPILATION, COR_PRF_USE_PROFILE_IMAGES 값입니다.<br /> <br /> COR_PRF_MONITOR_JIT_COMPILATION 값을 전달해 주면 CLR 엔진은 메서드에 대한 JIT 컴파일 관련 콜백을 호출해 줍니다. 사실 사용자 코드를 프로파일링 하는 정도라면 이 옵션만으로도 충분한데 애석하게도 우리가 다루려는 LicenseManager.Validate 메서드는 SYSTEM.dll에 정의되어 있고 이 파일은 NGen에 의해서 미리 기계어로 컴파일 된 것을 사용하기 때문에 JIT 컴파일이 발생하지 않게 됩니다.<br /> <br /> 이 때문에, 기존에 NGen되어 있는 DLL들도 JIT 컴파일이 발생하도록 하기 위해 COR_PRF_USE_PROFILE_IMAGES 값을 함께 OR 연산 처리해서 SetEventMask에 넘겨주는 것입니다.<br /> <br /> <div style='font-size: 12pt; font-family: 맑은 고딕, Consolas; color: #2211AA; text-align: left; font-weight: bold'>ICorProfilerCallback::JITCompilationStarted 구현</div><br /> <br /> 이제, 우리의 Profiler 개체(CIgnore)는 CLR 엔진이 JIT 컴파일을 하기 바로 직전에 JITCompilationStarted 콜백 함수를 불러주게 됩니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > STDMETHOD(JITCompilationStarted)(FunctionID functionId, BOOL fIsSafeToBlock); </pre> <br /> 바로 이 단계에서 해당 메서드의 IL 코드를 변경해 줄 수가 있는데요. 우리가 원하는 것은 System.ComponentModel.LicenseManager라는 클래스의 Validate라는 이름의 메서드가 JIT 컴파일 될 때 IL 코드를 변경하는 것이기 때문에 JITCompilationStarted 콜백 함수 호출 시에 넘어오는 functionId 인자를 통해서 현재 JIT 컴파일 되는 메서드의 정보를 알아내서 Validate 메서드인지를 판단해 주어야 합니다.<br /> <br /> 그래서, 다음과 같이 해주면 클래스 및 메서드에 대한 이름을 구할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > ClassID classId; ModuleID moduleId = 0; mdToken methodToken = 0; HRESULT hr = m_pICorProfilerInfo-><b style='COLOR: blue'>GetFunctionInfo(functionId</b>, &classId, &moduleId, &methodToken); // ========= 1. 필요한 인터페이스를 구하고, IMetaDataImport* pMetaDataImport = NULL; IMetaDataAssemblyImport* pMetaDataAssemblyImport = NULL; hr = m_pICorProfilerInfo->GetModuleMetaData(moduleId, ( ofRead | ofWrite ), IID_IMetaDataAssemblyImport, (LPUNKNOWN *)&pMetaDataAssemblyImport ); pMetaDataAssemblyImport->QueryInterface(IID_IMetaDataImport, (LPVOID *)&pMetaDataImport); ULONG cchFunction; mdToken typeToken; wchar_t szFunction[2048]; wchar_t szClass[2048]; // ========= 2. 메서드 이름을 구하고, hr = pMetaDataImport-><b style='COLOR: blue'>GetMethodProps</b>(methodToken, &typeToken, <b style='COLOR: blue'>szFunction</b>, 2048, &cchFunction, NULL, NULL, NULL, NULL, NULL); // ========= 3. 타입명을 구하고, ULONG cchClass; hr = pMetaDataImport-><b style='COLOR: blue'>GetTypeDefProps</b> (typeToken, <b style='COLOR: blue'>szClass</b><b style='COLOR: blue'></b>, 2048, &cchClass, 0, 0); </pre> <br /> 타입과 메서드 이름을 구했으니 "System.ComponentModel.LicenseManager.Validate" 문자열을 비교해서 같을 때에만 IL 코드를 교체하는 다음 단계로 넘어가게 해주면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > wchar_t buf[2048]; _snwprintf_s(buf, 2048, 2048, L"%s.%s", szClass, szFunction); if (wcscmp(buf, L"System.ComponentModel.LicenseManager.Validate") != 0) { break; } // 이하, rewrite IL code </pre> <br /> 이제, IL 코드를 교체하기 위해 IMetaDataEmit, IMethodMalloc 인터페이스를 각각 구하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > hr = pMetaDataAssemblyImport->QueryInterface(IID_IMetaDataEmit, (LPVOID *)&pMetaDataEmit ); if (S_OK != hr) { break; } hr = m_pICorProfilerInfo-><b style='COLOR: blue'>GetILFunctionBodyAllocator</b>(moduleId, &pMalloc); if (S_OK != hr) { break; } </pre> <br /> 교체해 줄 IL 코드를 바이트 배열로 보관해 둡니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > vector<BYTE> ilCodes; ilCodes.push_back(CEE_LDNULL); ilCodes.push_back(CEE_RET); </pre> <br /> 위의 코드에서 사용된 CEE_LDNULL이나 CEE_RET와 같은 상수 정의를 위해 다음과 같은 #include 문을 cpp 파일 상단에 추가해 주어야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > #define OPDEF( id, s, pop, push, args, type, l, OpCode1, OpCode2, ctrl ) id, typedef enum enumOpcode { #include "opcode.def" CEE_COUNT, /* number of instructions and macros pre-defined */ } OPCODE; #undef OPDEF </pre> <br /> 그다음, 우리는 단순히 "return null;" 코드만을 포함한 메서드를 만드는 것이기 때문에 .NET Method 헤더를 Tiny 유형으로 정의해 줄 수 있습니다. 그렇게, Tiny 헤더 및 IL 코드를 포함하여 BYTE 배열에 담아주는 작업을 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > <b style='COLOR: blue'>IMAGE_COR_ILMETHOD ilHeader</b>; ilHeader.Tiny.Flags_CodeSize = (BYTE)((ilCodes.size() << 2) | CorILMethod_TinyFormat); size_t allocationSize = ilCodes.size() + sizeof(ilHeader.Tiny); BYTE *pAllocated = (BYTE *)pMalloc->Alloc((ULONG)allocationSize); BYTE *pBytes = pAllocated; <b style='COLOR: blue'>memcpy(pBytes, &ilHeader.Tiny, sizeof(ilHeader.Tiny));</b> pBytes += sizeof(ilHeader.Tiny); <b style='COLOR: blue'>memcpy(pBytes, ilCodes.data(), ilCodes.size());</b> pBytes += ilCodes.size(); </pre> <br /> 마지막 단계로, 기존 메서드를 무시하는 새로운 IL 코드로 된 메서드 정의로 교체해 주기만 하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > m_pICorProfilerInfo-><b style='COLOR: blue'>SetILFunctionBody</b>(moduleId, methodToken, (LPCBYTE)pAllocated); </pre> <br /> 정상적으로 빌드해 주면 끝!<br /> <br /> <div style='font-size: 12pt; font-family: 맑은 고딕, Consolas; color: #2211AA; text-align: left; font-weight: bold'>프로파일러를 이용한 라이선스 우회 테스트</div><br /> <br /> 라이선스를 우회하는 .NET Profiler를 테스트 하기 위해 <a target='_tab' href='http://www.sysnet.pe.kr/2/0/1045'>지난번 글</a>에서 작성한 <a target='_tab' href='ConsoleApplication1.zip'>ConsoleApplication1.zip</a>을 다운로드 받습니다.<br /> <br /> 압축을 풀고, *.lic, *.licx 파일을 제거한 후 다시 빌드 합니다. 그런 후 ConsoleApplication1.exe 파일을 실행해 주면 다음과 같이 '정상적으로' 라이선스 예외가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > D:\...[생략]...\bin\Debug><b style='COLOR: blue'>ConsoleApplication1.exe</b> <b style='COLOR: blue'>Unhandled Exception: System.ComponentModel.LicenseException</b>: An instance of type 'CommercialLibrary.LicFileLicensedClass1' was being created, and a valid license could not be granted for the type 'CommercialLibrary.LicFileLicensedClass1'. Please, contact the manufacturer of the component for more information. at System.ComponentModel.LicenseManager.Validate(Type type, Object instance) at CommercialLibrary.LicFileLicensedClass1..ctor() in D:\...[생략]...\CommercialLibrary\Class1.cs:line 14 at ConsoleApplication1.Program.Main(String[] args) in D:\...[생략]...\ConsoleApplication1\Program.cs:line 12 </pre> <br /> 이제 명령행 창을 열고, 다음과 같이 .NET Profiler를 CLR 엔진에 알리는 환경 변수를 설정해 줍니다. (물론, 위에서 빌드한 CIgnore 개체는 regsvr32.exe에 의해서 레지스트리에 등록되어 있어야 합니다. x86/x64 용 DLL 구분도 주의하시고!)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; width: 800px; background-color: #fbedbb; overflow-x: scroll; font-family: Consolas, Verdana;' > SET COR_ENABLE_PROFILING=1 SET COR_PROFILER={F54F8382-DB3F-4847-A2ED-84DCA14B7433} // CIgnore COM 개체의 CLSID 값 </pre> <br /> 다시 ConsoleApplication1.exe를 실행시키면, Validate 메서드에서 예외가 발생하지 않고 통과하는 것을 확인할 수 있습니다.<br /> <br /> 이 정도면... .NET 세상에서 가히 무소불위의 권력이라고 할 수 있겠지요. (여러분들이 알고 있는 Visual Studio의 IntelliTrace 기능이 .NET Profiler의 실 사용예입니다.)<br /> <br /><br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
2001
(왼쪽의 숫자를 입력해야 합니다.)