Microsoft MVP성태의 닷넷 이야기
VC++: 85. Windows Vista부터 바뀐 Credential Provider 예제 분석 (1) [링크 복사], [링크+제목 복사]
조회: 20055
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

Windows Vista부터 바뀐 Credential Provider 예제 분석 (1)

8년 전에 보아둔 것을 이제서야 확인하게 되는군요. ^^

Credential Provider Samples
; https://www.sysnet.pe.kr/2/1/337

Windows XP/2003까지 GINA라고 불리던 인증 모듈이 비스타부터 새로운 체계의 "Credential Provider"로 바뀌었습니다. 관련해서 예제 프로젝트를 다음의 링크에서 다운로드할 수 있습니다.

Windows Vista Credential Provider Samples (이 글의 첨부 파일에 포함)
; http://www.microsoft.com/en-us/download/details.aspx?id=4057

압축 해제 후 함께 첨부되어 있는 "Windows Vista Credential Provider Samples Overview UPDATE.doc" 문서를 보시면 대충 어떤 구조인지 눈에 들어올 텐데요. 어쨌든 나름대로 정리를 해보겠습니다. ^^

가장 기본적인 예제는 역시 SampleCredentialProvider 프로젝트입니다. SampleCredentialProvider 폴더를 열어 SampleCredentialProvider.sln 파일을 Visual Studio에서 로드한 다음 빌드하면 SampleCredentialProvider.dll 파일이 생성됩니다.

SampleCredentialProvider는 크게 다음의 2가지 인터페이스를 구현합니다.

- ICredentialProvider
    가용한 Credential 개체를 열람하는 기능, SampleCredentialProvider 예제에서는 "Administrator", "Guest" 계정만 구현
    쉽게 말해서, 윈도우 시스템은 사용자가 만든 DLL로부터 ICredentialProvider 인터페이스를 구현한 개체를 얻고 
    그것으로부터 등록된 사용자 계정을 열람.

- ICredentialProviderCredential
    인증 절차를 수행하는 동안 각각의 Credential이 갖춰야 할 필수 기능을 노출 
    (즉, 사용자에 대한 인증 기능을 구현)

ICredentialProviderCredential 인터페이스를 구현한 개체 하나는 사용자 계정 하나를 의미합니다. 예를 들어, ICredentialProviderCredential::GetBitmapValue 메서드에서 반환하는 이미지는 로그인 화면에서 해당 사용자 계정을 나타낼 때 보여지는 이미지를 반환하게 됩니다. SampleCredentialProvider 예제에서는 "tileimage.bmp" 파일이 리소스로 포함되어 있는데, 이것으로 "Administrator", "Guest" 계정 2개에 대해 같은 이미지를 반환하도록 작성되어 있기 때문에 다음과 같이 로그인 화면에서 2개 계정 모두 같은 이미지가 나타납니다.

SampleCredentialProvider_1.png

정리해 보면, ICredentialProvider는 컬렉션 컨테이너라고 보시면 되고 그 요소는 ICredentialProviderCredential 인터페이스를 구현하는 정도!




SampleCredentialProvider 예제의 코드는 COM 개체를 노출하지만 regsvr32.exe 등록 과정을 통하지는 않습니다. 그래서 'SampleCredentialProvider.def' 파일에 등록과 관련된 함수는 누락되어 있습니다.

LIBRARY SAMPLECREDENTIALPROVIDER.DLL

EXPORTS
    DllCanUnloadNow                                 PRIVATE
    DllGetClassObject                               PRIVATE

대신 등록을 위한 Register.reg 파일과,

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}]
@="SampleCredentialProvider"

[HKEY_CLASSES_ROOT\CLSID\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}]
@="SampleCredentialProvider"

[HKEY_CLASSES_ROOT\CLSID\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}\InprocServer32]
@="SampleCredentialProvider.dll"
"ThreadingModel"="Apartment"

등록 해제를 위한 Unregister.reg 파일이 제공됩니다.

Windows Registry Editor Version 5.00

[-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}]

보시는 바와 같이 "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers" 쪽에 우리가 만든 사용자 정의 Credential Provider를 등록하고, DLL을 system32 폴더에 복사해서 InprocServer32에서 경로 없이 등록했어도 찾을 수 있도록 합니다.

그런데, 왜 불편하게 regsvr32.exe를 이용해 등록할 수 있도록 DllRegisterServer 함수를 export 안 해주고 이런 식으로 예제를 구성한 것일까요? 아마도, 간결한 예제를 유지하기 위해서인 듯합니다. ATL 라이브러리를 포함하면 DllRegisterServer 함수의 구현이 쉽겠지만 의존성이 생기고, ATL 라이브러리를 포함시키지 않으면 예제에 .rgs 등록 코드까지 추가해야 하는데 이는 엄밀히 Credential Provider 예제와는 무관하기 때문입니다.

참고로, InprocServer32에 경로를 명시해도 됩니다.

[HKEY_CLASSES_ROOT\CLSID\{b84ca702-35a8-4e67-8d2a-6c2807b297d3}\InprocServer32]
@="c:\\temp\\SampleCredentialProvider.dll"
"ThreadingModel"="Apartment"

실제 제품 배포 시에는 system32 (또는 Program Files)에다 하는 것이 보안상 좋겠지만 개발 시에는 이렇게 해서 테스트/디버깅하는 것이 더 편합니다.

Dll.cpp 파일과 Dll.h 파일은 DllMain을 구현하고, DllCanUnloadNow, DllGetClassObject 함수 구현을 포함합니다.

운영체제는 레지스트리를 통해서 SampleCredentialProvider.dll 파일을 로드할 것이고, export 함수 중에 DllGetClassObject를 호출합니다.

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
    return CClassFactory_CreateInstance(rclsid, riid, ppv);
}

HRESULT CClassFactory_CreateInstance(REFCLSID rclsid, REFIID riid, void** ppv)
{
    HRESULT hr;
    if (CLSID_CSampleProvider == rclsid)
    {
        CClassFactory* pcf = new CClassFactory;
        if (pcf)
        {
            hr = pcf->QueryInterface(riid, ppv);
            pcf->Release();
        }
        else
        {
            hr = E_OUTOFMEMORY;
        }
    }
    else
    {
        hr = CLASS_E_CLASSNOTAVAILABLE;
    }
    return hr;
}

보시는 바와 같이 우리가 만든 Credential Provider COM 개체를 만들어 주는 CClassFactory라는 COM Class Factory 개체를 생성해서 반환합니다.

class CClassFactory : public IClassFactory
{
  public:
    // IUnknown
    STDMETHOD_(ULONG, AddRef)() { ... }
    STDMETHOD_(ULONG, Release)() { ... }
    STDMETHOD (QueryInterface)(REFIID riid, void** ppv) { ... }

    // IClassFactory
    STDMETHOD (CreateInstance)(IUnknown* pUnkOuter, REFIID riid, void** ppv)
    {
        HRESULT hr;
        if (!pUnkOuter)
        {
            hr = CSampleProvider_CreateInstance(riid, ppv);
        }
        else
        {
            hr = CLASS_E_NOAGGREGATION;
        }
        return hr;
    }

    STDMETHOD (LockServer)(BOOL bLock) { ... }

  private:
     CClassFactory() : _cRef(1) {}
    ~CClassFactory(){}

  private:
    LONG _cRef;

    friend HRESULT CClassFactory_CreateInstance(REFCLSID rclsid, REFIID riid, void** ppv);
};

전형적인 Class Factory 코드를 따르고 있으며 CreateInstance에서 CSampleProvider.cpp에 구현된 CSampleProvider_CreateInstance 함수를 호출하고 있습니다.

만약 여러분만의 Credential Provider 모듈을 만든다면 위의 Dll.h, Dll.cpp 파일은 거의 그대로 사용해도 됩니다. 단지 CLSID_CSampleProvider와 그에 대한 GUID 값만 살짝 바꿔 주시고 나머지 2개의 파일(CSampleCredential.cpp, CSampleProvider.cpp)만 자신의 시나리오에 맞게 작성하시면 됩니다.




이제, 시스템에 등록된 "계정"들을 열람하는 CSampleProvider 코드를 살펴보겠습니다. Credential Provider 개체는 반드시 ICredentialProvider 인터페이스를 구현해야 합니다.

ICredentialProvider : public IUnknown
{
public:
    virtual HRESULT STDMETHODCALLTYPE SetUsageScenario( 
        /* [in] */ CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
        /* [in] */ DWORD dwFlags) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE SetSerialization( 
        /* [in] */ const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcs) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE Advise( 
        /* [in] */ ICredentialProviderEvents *pcpe,
        /* [in] */ UINT_PTR upAdviseContext) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE UnAdvise( void) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE GetFieldDescriptorCount( 
        /* [out] */ DWORD *pdwCount) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE GetFieldDescriptorAt( 
        /* [in] */ DWORD dwIndex,
        /* [out] */ CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR **ppcpfd) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE GetCredentialCount( 
        /* [out] */ DWORD *pdwCount,
        /* [out] */ DWORD *pdwDefault,
        /* [out] */ BOOL *pbAutoLogonWithDefault) = 0;
        
    virtual HRESULT STDMETHODCALLTYPE GetCredentialAt( 
        /* [in] */ DWORD dwIndex,
        /* [out] */ ICredentialProviderCredential **ppcpc) = 0;
        
};

다음은 CSampleProvider.h 파일의 개략적인 코드입니다.

#include <credentialprovider.h>
#include <windows.h>
#include <strsafe.h>

#include "CSampleCredential.h"
#include "helpers.h"

#define MAX_CREDENTIALS 3
#define MAX_DWORD   0xffffffff        // maximum DWORD

class CSampleProvider : public ICredentialProvider
{
  public:
    // 전형적인 IUnknown 구현 코드
    STDMETHOD_(ULONG, AddRef)() { return _cRef++; }
    
    STDMETHOD_(ULONG, Release)() { ... }
    
    STDMETHOD (QueryInterface)(REFIID riid, void** ppv)
    {
        HRESULT hr;
        if (IID_IUnknown == riid || 
            IID_ICredentialProvider == riid)
        {
            *ppv = this;
            reinterpret_cast<IUnknown*>(*ppv)->AddRef();
            hr = S_OK;
        }
        else
        {
            *ppv = NULL;
            hr = E_NOINTERFACE;
        }
        return hr;
    }

  public:
    // ICredentialProvider 인터페이스의 메서드
    IFACEMETHODIMP SetUsageScenario(CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus, DWORD dwFlags);
    IFACEMETHODIMP SetSerialization(const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs);

    IFACEMETHODIMP Advise(__in ICredentialProviderEvents* pcpe, UINT_PTR upAdviseContext);
    IFACEMETHODIMP UnAdvise();

    IFACEMETHODIMP GetFieldDescriptorCount(__out DWORD* pdwCount);
    IFACEMETHODIMP GetFieldDescriptorAt(DWORD dwIndex,  __deref_out CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR** ppcpfd);

    IFACEMETHODIMP GetCredentialCount(__out DWORD* pdwCount,
                                      __out DWORD* pdwDefault,
                                      __out BOOL* pbAutoLogonWithDefault);
    IFACEMETHODIMP GetCredentialAt(DWORD dwIndex, 
                                   __out ICredentialProviderCredential** ppcpc);

    // CClassFactory의 CreateInstance 메서드에서 Credential Provider 개체를 생성할 때 사용한 메서드
    friend HRESULT CSampleProvider_CreateInstance(REFIID riid, __deref_out void** ppv);

  protected:
    CSampleProvider();
    __override ~CSampleProvider();
    
  private:
    // ...[생략]...
};

가능한 실행 순서에 따라 한번 따라가 보겠습니다. 우선, Class Factory에서 CSampleProvider_CreateInstance 메서드를 불러서 우리가 제공하는 Credential Provider 개체를 생성해서 윈도우 시스템에 반환합니다.

HRESULT CSampleProvider_CreateInstance(REFIID riid, void** ppv)
{
    HRESULT hr;

    CSampleProvider* pProvider = new CSampleProvider();

    if (pProvider)
    {
        hr = pProvider->QueryInterface(riid, ppv);
        pProvider->Release();
    }
    else
    {
        hr = E_OUTOFMEMORY;
    }
    
    return hr;
}

위의 구현 코드 역시 전형적인 재사용 예제 코드이므로 그냥 사용하시면 됩니다. CSampleProvider 개체까지 생성되었으니, 이제 ICredentialProvider 인터페이스의 구현에 윈도우 시스템과 상호작용하는 부분만 남았습니다.

이에 대한 구체적인 실행 순서는 이후의 글에서 별도로 다루겠습니다.




Credential Provider의 특성상 디버깅 시에 DLL의 "잠김"에 유의해야 합니다. 다행히 윈도우 시스템이 항상 로드해서 잠그진 않고 필요할 때만 DLL을 로드하는데, 예를 들어 "로그인 화면"으로 빠진 경우에만 DLL이 잠기므로 그 외에는 빌드하고 곧바로 배포할 수 있으므로 개발상 불편함이 GINA에 비해 많이 줄었습니다. 또한, CredUIPromptForWindowsCredentials Win32 API를 이용하면 F5 디버깅까지 가능합니다. 이에 대해서는 다음번에 알아보겠습니다.

마치기 전에, 한 가지 주의해야 할 사항이 있는데요.
SampleCredentialProvider 예제 프로젝트는 MSVCR???.dll에 의존성을 가지고 있습니다. 가령 Visual Studio 2013으로 빌드하면 MSVCR120.dll로 의존성을 갖는데, 문제는 대상 운영체제가 msvcr120.dll을 가지고 있지 않는 경우가 있다는 점입니다. 따라서, 만약 SampleCredentialProvider 프로젝트 기반으로 바이너리를 만들었으면 반드시 설치 프로그램에서 msvcr120.dll을 함께 배포하든가, 아니면 이에 대한 의존성을 제거해야 합니다.

수동으로 구성해 본 VC++ 프로젝트 설정: ReleaseMinDependency
; https://www.sysnet.pe.kr/2/0/800

참고로, MSDN Magazine에 보면 스마트 카드 인증 모듈을 구현한 예제 코드를 소개하고 있으니 이것도 참조해 보시면 이해가 좀 더 잘 될 것입니다.

Windows Vista용 자격 증명 공급자로 사용자 지정 로그인 환경 만들기
; https://learn.microsoft.com/ko-kr/archive/msdn-magazine/2007/january/custom-login-experiences-credential-providers-in-windows-vista





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

[연관 글]






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

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

비밀번호

댓글 작성자
 



2018-09-05 01시59분
Winlogon GINA/Credential Provider 커스터마이징
; http://thermidor.tistory.com/1414

Credential Provider 자동 로그인(AutoLogon) 구현
; https://lynnbaek.github.io/2018/12/17/credential-provider-auto-logon/
정성태
2018-10-18 02시30분
[정수] 혹시 샘플코드 빌드해서 경로에 넣고 레지스트리에 추가도 했는데 로그인 화면에 변화가 없다 하시는분들!
빌드 하실 때 운영체제 환경에 맞게 빌드 하셔야 되요 (32bit / 64bit)
참고 : https://igotit.tistory.com/147

이것때문에 한참 해맸음..ㅠ
[guest]
2021-06-08 10시59분
정성태
2022-07-26 08시55분
The hidden side of Seclogon part 2: Abusing leaked handles to dump LSASS memory
; https://splintercod3.blogspot.com/p/the-hidden-side-of-seclogon-part-2.html

The hidden side of Seclogon part 3: Racing for LSASS dumps
; https://splintercod3.blogspot.com/p/the-hidden-side-of-seclogon-part-3.html
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13606정성태4/24/202487닷넷: 2247. C# - tensorflow 연동 (MNIST 예제)파일 다운로드1
13605정성태4/23/2024323닷넷: 2246. C# - Python.NET을 이용한 파이썬 소스코드 연동파일 다운로드1
13604정성태4/22/2024339오류 유형: 901. Visual Studio - Unable to set the next statement. Set next statement cannot be used in '[Exception]' call stack frames.
13603정성태4/21/2024572닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동파일 다운로드1
13602정성태4/20/2024796닷넷: 2244. C# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)파일 다운로드1
13601정성태4/19/2024838닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/2024846닷넷: 2242. C# - 관리 스레드와 비관리 스레드
13599정성태4/17/2024862닷넷: 2241. C# - WAV 파일의 PCM 사운드 재생(Windows Multimedia)파일 다운로드1
13598정성태4/16/2024884닷넷: 2240. C# - WAV 파일 포맷 + LIST 헤더파일 다운로드2
13597정성태4/15/2024866닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/20241049닷넷: 2238. C# - WAV 기본 파일 포맷파일 다운로드1
13595정성태4/13/20241050닷넷: 2237. C# - Audio 장치 열기 (Windows Multimedia, NAudio)파일 다운로드1
13594정성태4/12/20241068닷넷: 2236. C# - Audio 장치 열람 (Windows Multimedia, NAudio)파일 다운로드1
13593정성태4/8/20241079닷넷: 2235. MSBuild - AccelerateBuildsInVisualStudio 옵션
13592정성태4/2/20241218C/C++: 165. CLion으로 만든 Rust Win32 DLL을 C#과 연동
13591정성태4/2/20241194닷넷: 2234. C# - WPF 응용 프로그램에 Blazor App 통합파일 다운로드1
13590정성태3/31/20241078Linux: 70. Python - uwsgi 응용 프로그램이 k8s 환경에서 OOM 발생하는 문제
13589정성태3/29/20241150닷넷: 2233. C# - 프로세스 CPU 사용량을 나타내는 성능 카운터와 Win32 API파일 다운로드1
13588정성태3/28/20241263닷넷: 2232. C# - Unity + 닷넷 App(WinForms/WPF) 간의 Named Pipe 통신 [2]파일 다운로드1
13587정성태3/27/20241168오류 유형: 900. Windows Update 오류 - 8024402C, 80070643
13586정성태3/27/20241329Windows: 263. Windows - 복구 파티션(Recovery Partition) 용량을 늘리는 방법
13585정성태3/26/20241112Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241062개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241195Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241454Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...