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

(시리즈 글이 5개 있습니다.)
VC++: 84. CredUIPromptForWindowsCredentials Win32 API 사용법 정리
; https://www.sysnet.pe.kr/2/0/1827

VC++: 85. Windows Vista부터 바뀐 Credential Provider 예제 분석 (1)
; https://www.sysnet.pe.kr/2/0/1828

VC++: 86. Windows Vista부터 바뀐 Credential Provider 예제 분석 (2)
; https://www.sysnet.pe.kr/2/0/1829

.NET Framework: 927. C# - 윈도우 프로그램에서 Credential Manager를 이용한 보안 정보 저장
; https://www.sysnet.pe.kr/2/0/12267

닷넷: 2299. C# - Windows Hello 사용자 인증 다이얼로그 표시하기
; https://www.sysnet.pe.kr/2/0/13744




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
정성태

... 106  107  108  109  110  111  112  113  114  115  116  117  118  [119]  120  ...
NoWriterDateCnt.TitleFile(s)
10949정성태4/28/201619936.NET Framework: 575. SharedDomain과 JIT 컴파일파일 다운로드1
10948정성태4/28/201623900.NET Framework: 574. .NET - 눈으로 확인하는 SharedDomain의 동작 방식 [3]파일 다운로드1
10947정성태4/27/201621780.NET Framework: 573. .NET CLR4 보안 모델 - 4. CLR4 보안 모델에서의 조건부 APTCA 역할파일 다운로드1
10946정성태4/26/201624566VS.NET IDE: 106. Visual Studio 2015 확장 - INI 파일을 위한 사용자 정의 포맷 기능 (Syntax Highlighting)파일 다운로드1
10945정성태4/26/201618340오류 유형: 327. VSIX 프로젝트 빌드 시 The "VsTemplatePaths" task could not be loaded from the assembly 오류 발생
10944정성태4/22/201619559디버깅 기술: 80. windbg - 풀 덤프 파일로부터 텍스트 파일의 내용을 찾는 방법
10943정성태4/22/201624416디버깅 기술: 79. windbg - 풀 덤프 파일로부터 .NET DLL을 추출/저장하는 방법 [1]
10942정성태4/19/201619726디버깅 기술: 78. windbg 사례 - .NET 예외가 발생한 시점의 오류 분석 [1]
10941정성태4/19/201619636오류 유형: 326. Error MSB8020 - The build tools for v120_xp (Platform Toolset = 'v120_xp') cannot be found.
10940정성태4/18/201622931Windows: 116. 프로세스 풀 덤프 시간을 줄여 주는 Process Reflection [3]
10939정성태4/18/201623936.NET Framework: 572. .NET APM 비동기 호출의 Begin...과 End... 조합 [3]파일 다운로드1
10938정성태4/13/201623501오류 유형: 325. 파일 삭제 시 오류 - Error 0x80070091: The directory is not empty.
10937정성태4/13/201631693Windows: 115. UEFI 모드로 윈도우 10 설치 가능한 USB 디스크 만드는 방법
10936정성태4/8/201642443Windows: 114. 삼성 센스 크로노스 7 노트북의 운영체제를 USB 디스크로 새로 설치하는 방법 [3]
10935정성태4/7/201626712웹: 32. Edge에서 Google Docs 문서 편집 시 한영 전환키가 동작 안하는 문제
10934정성태4/5/201625404디버깅 기술: 77. windbg의 콜스택 함수 인자를 쉽게 확인하는 방법 [1]
10933정성태4/5/201631038.NET Framework: 571. C# - 스레드 선호도(Thread Affinity) 지정하는 방법 [8]파일 다운로드1
10932정성태4/4/201623315VC++: 96. C/C++ 식 평가 - printf("%d %d %d\n", a, a++, a);
10931정성태3/31/201623606개발 환경 구성: 283. Hyper-V 내에 구성한 Active Directory 환경의 시간 구성 방법 [3]
10930정성태3/30/201621573.NET Framework: 570. .NET 4.5부터 추가된 CLR Profiler의 실행 시 Rejit 기능
10929정성태3/29/201631674.NET Framework: 569. ServicePointManager.DefaultConnectionLimit의 역할파일 다운로드1
10928정성태3/28/201637376.NET Framework: 568. ODP.NET의 완전한 닷넷 버전 Oracle ODP.NET, Managed Driver [2]파일 다운로드1
10927정성태3/25/201626576.NET Framework: 567. System.Net.ServicePointManager의 DefaultConnectionLimit 속성 설명
10926정성태3/24/201626135.NET Framework: 566. openssl의 PKCS#1 PEM 개인키 파일을 .NET RSACryptoServiceProvider에서 사용하는 방법 [10]파일 다운로드1
10925정성태3/24/201620423.NET Framework: 565. C# - Rabin-Miller 소수 생성 방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자 - 두 번째 이야기파일 다운로드1
10924정성태3/22/201621116오류 유형: 324. Visual Studio에서 Azure 클라우드 서비스 생성 시 Failed to initialize the PowerShell host 에러 발생
... 106  107  108  109  110  111  112  113  114  115  116  117  118  [119]  120  ...