Microsoft MVP성태의 닷넷 이야기
VC++: 85. Windows Vista부터 바뀐 Credential Provider 예제 분석 (1) [링크 복사], [링크+제목 복사],
조회: 23418
글쓴 사람
정성태 (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
정성태

... 31  32  33  34  [35]  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12954정성태2/4/20229765VS.NET IDE: 173. 비주얼 스튜디오 - Output 창에 색상이 지정된 출력 결과가 "[39m[22m" 식의 문자로 나오는 문제
12953정성태2/2/20229702Linux: 48. Windows 11 + WSL 우분투 GUI 환경에서 한글 출력
12952정성태2/2/202210539.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)파일 다운로드1
12951정성태2/2/202210403.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)파일 다운로드1
12950정성태2/1/20229837.NET Framework: 1146. .NET 6에 추가되지 않은 Generic Math (예: INumber<T>)
12949정성태2/1/20229508.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비파일 다운로드1
12948정성태1/30/20229745.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력파일 다운로드1
12947정성태1/30/202211374개발 환경 구성: 634. ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
12946정성태1/28/20229683오류 유형: 792. .NET Core - 로컬 개발 중에 docker 호스팅으로 바꾸는 경우 SQL 서버 접근 방법
12945정성태1/28/20229915오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
12944정성태1/28/202212858.NET Framework: 1143. C# - Entity Framework Core 6 개요
12943정성태1/27/202212306.NET Framework: 1142. .NET 5+로 포팅 시 플랫폼 호환성 경고 메시지(SYSLIB0006, SYSLIB0011, CA1416) [1]파일 다운로드1
12942정성태1/27/202211963.NET Framework: 1141. XmlSerializer와 Dictionary 타입파일 다운로드1
12941정성태1/26/202213403오류 유형: 790. AKS/k8s - pod 상태가 Pending으로 지속되는 경우
12940정성태1/26/202210200오류 유형: 789. AKS에서 hpa에 따른 autoscale 기능이 동작하지 않는다면?
12939정성태1/25/202211323.NET Framework: 1140. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP3 오디오 파일 인코딩/디코딩하는 예제파일 다운로드1
12938정성태1/24/202213763개발 환경 구성: 633. Docker Desktop + k8s 환경에서 local 이미지를 사용하는 방법
12937정성태1/24/202211362.NET Framework: 1139. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 오디오(mp2) 인코딩하는 예제(encode_audio.c) [2]파일 다운로드1
12936정성태1/22/202211524.NET Framework: 1138. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 멀티미디어 파일의 메타데이터를 보여주는 예제(metadata.c)파일 다운로드1
12935정성태1/22/202211747.NET Framework: 1137. ffmpeg의 파일 해시 예제(ffhash.c)를 C#으로 포팅파일 다운로드1
12934정성태1/22/202211147오류 유형: 788. Warning C6262 Function uses '65564' bytes of stack: exceeds /analyze:stacksize '16384'. Consider moving some data to heap. [2]
12933정성태1/21/202211718.NET Framework: 1136. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)파일 다운로드1
12932정성태1/20/202212388.NET Framework: 1135. C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c) [2]파일 다운로드1
12931정성태1/20/20229768개발 환경 구성: 632. ASP.NET Core 프로젝트를 AKS/k8s에 올리는 과정
12930정성태1/19/202210941개발 환경 구성: 631. AKS/k8s의 Volume에 파일 복사하는 방법
12929정성태1/19/202210756개발 환경 구성: 630. AKS/k8s의 Pod에 Volume 연결하는 방법
... 31  32  33  34  [35]  36  37  38  39  40  41  42  43  44  45  ...