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

1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13693정성태7/24/20247233개발 환경 구성: 717. Visual Studio - C# 프로젝트에서 레지스트리에 등록하지 않은 COM 개체 참조 및 사용 방법파일 다운로드1
13692정성태7/24/20248012디버깅 기술: 199. Windbg - 리눅스에서 뜬 닷넷 응용 프로그램 덤프 파일에 포함된 DLL의 Export Directory 탐색
13691정성태7/23/20247370디버깅 기술: 198. Windbg - 스레드의 Win32 Message Queue 정보 조회
13690정성태7/23/20247005오류 유형: 919. Visual C++ 리눅스 프로젝트 - error : ‘u8’ was not declared in this scope
13689정성태7/22/20248509디버깅 기술: 197. Windbg - PE 포맷의 Export Directory 탐색
13688정성태7/21/20247615닷넷: 2281. C# - Lock / Wait 상태에서도 일부 Win32 메시지 처리파일 다운로드1
13687정성태7/19/20248045닷넷: 2280. C# - PostThreadMessage로 보낸 메시지를 Windows Forms에서 수신하는 방법파일 다운로드1
13686정성태7/19/20247842오류 유형: 918. Visual Studio - ATL Simple Object 추가 시 error C2065: 'IDR_...': undeclared identifier
13685정성태7/19/20247990스크립트: 66. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법 - 두 번째 이야기
13684정성태7/19/20248164닷넷: 2279. C# - 문자열 보간식 사례 (예: 조건 연산자 사용)
13683정성태7/18/20247637오류 유형: 917. ClrMD - Linux 환경의 .NET 5 덤프 분석 시 hang 현상
13682정성태7/18/20247854닷넷: 2278. WPF - 스레드에 종속되는 DependencyObject파일 다운로드1
13681정성태7/17/20247459닷넷: 2277. C# 13 - (2) 메서드 그룹의 자연 타입 개선 (메서드 추론 개선)파일 다운로드1
13680정성태7/16/20247838닷넷: 2276. C# - Method Group, Natural Type, function_type파일 다운로드1
13679정성태7/16/20246934Linux: 76. Linux - C++ (getaddrinfo 등을 담고 있는) libnss 정적 링크
13678정성태7/15/20247051VS.NET IDE: 191. Visual Studio 2022 - .NET 5 프로젝트를 Docker Support로 실행했을 때 오류
13677정성태7/15/20247132오류 유형: 916. MSBuild - CheckEolTargetFramework (warning NETSDK1138)
13676정성태7/14/20247315Linux: 75. gdb에서 glibc의 함수에 Breakpoint 걸기
13675정성태7/13/20249102C/C++: 166. C/C++ - DLL에서 template 함수를 export하는 방법 [1]파일 다운로드1
13674정성태7/13/20247986오류 유형: 915. Unhandled Exception: Microsoft.Diagnostics.NETCore.Client.ServerNotAvailableException: Unable to connect to Process
13673정성태7/11/20248427닷넷: 2275. C# 13 - (1) 신규 이스케이프 시퀀스 '\e'파일 다운로드1
13672정성태7/10/20247142닷넷: 2274. IIS - (프로세스 종료 없는) AppDomain Recycle
13671정성태7/10/20247258오류 유형: 914. Package ca-certificates is not installed.
13669정성태7/9/20247366오류 유형: 913. C# - AOT StaticExecutable 정적 링킹 시 빌드 오류
13668정성태7/8/20247393개발 환경 구성: 716. Hyper-V - Ubuntu 22.04 Generation 2 유형의 VM 설치
13667정성태7/7/20246608닷넷: 2273. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK)파일 다운로드1
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...