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

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

지난 글에서는,

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

개략적인 설명만 했을 뿐 정작 중요한 2개의 인터페이스는 거의 설명을 하지 않았는데요. 왜냐하면, ICredentialProvider, ICredentialProviderCredential 인터페이스를 Visual Studio의 디버깅을 이용해 직접/쉽게 체험해 볼 수 있기 때문입니다. 이번 글에서 다루는 내용이 바로 SampleCredentialProvider 예제를 F5 디버깅이 가능하도록 만드는 것인데, "Windows Vista Credential Provider Samples Overview UPDATE.doc" 문서에도 설명이 있습니다.

지난 글에서 SampleCredentialProvider DLL이 잠기는 순간이 로그인 화면에서라고 했는데요. 기존의 Windows XP/2003 이전의 GINA 인증 모듈이 항상 winlogon.exe 프로세스에 로드되어 있어 디버깅이 불편했던 것과는 달리, 새로운 "Credential Provider" 모델은 필요할 때만 인증 프로세스에 의해 잠기는 구조로 인해 디버깅이 훨씬 쉬워졌다는 장점이 있습니다.

자... 그럼 해볼까요? ^^

우선, 지난번 SampleCredentialProvider 프로젝트와 함께 새롭게 Win32 Console 유형의 EXE 프로젝트를 하나 더 추가하고 CredUIPromptForWindowsCredentials API를 사용해서,

CredUIPromptForWindowsCredentials Win32 API 사용법 정리
; https://www.sysnet.pe.kr/2/0/1827

다음과 같은 코드를 작성해 줍니다.

#include "stdafx.h"
#include <windows.h>
#include <WinCred.h>

#pragma comment(lib, "Credui.lib")

int _tmain(int argc, _TCHAR* argv[])
{
    BOOL save = false;
    DWORD authPackage = 0;
    LPVOID authBuffer;
    ULONG authBufferSize = 0;
    CREDUI_INFO credUiInfo;

    credUiInfo.pszCaptionText = TEXT("My caption");
    credUiInfo.pszMessageText = TEXT("My message");
    credUiInfo.cbSize = sizeof(credUiInfo);
    credUiInfo.hbmBanner = NULL;
    credUiInfo.hwndParent = NULL;

    CredUIPromptForWindowsCredentials(&(credUiInfo), 0, &(authPackage), 
        NULL, 0, &authBuffer, &authBufferSize, &(save), 0);

    return 0;
}

그렇습니다. 위의 CredUIPromptForWindowsCredentials Win32 API로 인해 우리가 만들어 등록한 SampleCredentialProvider DLL이 로드되고 관련해서 ICredentialProvider, ICredentialProviderCredential 인터페이스들의 상호작용이 있을 것이므로 편안하게 BreakPoint를 함수에 걸어 디버깅할 수 있습니다.

EXE 프로세스가 마련되었으니, 기존 SampleCredentialProvider 프로젝트의 F5 디버깅을 위한 몇 가지 준비를 해보겠습니다.

우선, SampleCredentialProvider 프로젝트에 포함된 Register.reg 파일의 DLL 경로를 바꿔줍니다.

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]
@="C:\\temp\\scp\\SampleCredentialProvider.dll"
"ThreadingModel"="Apartment"

왜냐하면, C:\Windows\System32 폴더에 배포하기 위해서는 관리자 권한이 필요하므로 Visual Studio를 그 권한으로 실행시키지 않고 테스트하기 위해 c:\temp\scp 폴더로 지정한 것입니다.

그다음, Post-Build Event에 다음과 같이 등록해 주고,

copy /y *.reg $(OutputPath)

robocopy $(OutputPath) c:\temp\scp /XF *.ilk  *.exp *.lib 
if %ERRORLEVEL% GTR 4 goto BuildError

exit 0

:BuildError
exit 1

빌드해 줍니다. 그럼 c:\temp\scp 폴더에 빌드 결과물들이 들어갑니다. 거기에 있는 register.reg 파일을 두번 클릭해서 시스템에 등록합니다. 마지막으로 SampleCredentialProvider 프로젝트 속성 창의 "Debugging" / "Command"에 "$(TargetDir)\ConsoleApplication1.exe" 값을 넣어주면 됩니다.

SampleCredentialProvider_f5debug_1.png

끝이군요. ^^ 이제 편안하게 F5 디버깅 모드로 진입하면 CSampleProvider.cpp, CSampleCredential.cpp 어느 곳이든 BreakPoint를 잡은 곳에 실행이 멈추고 분석할 수 있습니다. 이렇게!

SampleCredentialProvider_f5debug_2.png




F5 디버깅을 통해 몇번 실행하다 보면 ICredentialProviderCredential, ICredentialProvider 인터페이스가 제공하는 메서드들의 역할을 어렵지 않게 알 수 있습니다. 그렇게 한 후, 아래의 글을 보면,

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

윈도우의 로그온 시에 사용자가 등록한 Credential Provider가 어떻게 동작하는지 OutputDebugString으로 출력한 내용을 보면 이해가 더 잘 됩니다. 아래에서, "Provider::"로 시작하는 출력은 ICredentialProvider 인터페이스의 메서드가 실행됨을 의미하고, "Credential::"은 ICredentialProviderCredential의 메서드를 의미합니다.

1. [The system boots]
2. [LogonUI.exe process is created]
3. [Credential provider DLLs are loaded]
4. Provider::CreateInstance 

5. [User presses Ctrl+Alt+Del]

6. Provider::SetUsageScenario (CPUS_LOGON)
    // 우리가 구현한 Credential Provider가 어떤 인증 시나리오에서 실행되는지 윈도우 시스템이 알려줍니다.
    // 참고로 SampleCredentialProvider 예제의 CredUIPromptForWindowsCredentials Win32 API로 불릴 때는 CPUS_CREDUI 값이 넘어옴
    //       SampleCredentialProvider 예제는 CPUS_CREDUI 시나리오는 처리하지 않음
    //       SampleCredentialProvider 예제는 CPUS_LOGON, CPUS_UNLOCK_WORKSTATION 시나리오에 대해서만 반응

    // 따라서, CredUIPromptForWindowsCredentials Win32 API로 디버깅을 하고 싶다면
    //          SampleCredentialProvider 예제의 SetUsageScenario 내부의 switch 구문에 CPUS_CREDUI 경우도 처리하도록 코드 변경을 해야 함!

7. Credential::Initialize
    // ICredentialProviderCredential를 구현한 개체를 초기화 할 수 있는 기회

8. Provider::Advise 

9. Provider::GetCredentialCount
    // Credential Provider에 등록된 사용자 계정의 수를 윈도우에 반환

10. Provider::GetCredentialAt (dwIndex = 0)
    // dwIndex 번째의 사용자 계정 정보를 담은 ICredentialProviderCredential 개체를 반환

11. Provider::GetFieldDescriptorCount
    // Credential Provider가 요구하는 인증 정보를 표현할 UI의 요소 수를 반환
    // 예를 들어, Active Directory에 참여한 PC라면 Id, Password, Domain Name의 3가지 필드가 필요하므로 3을 반환
    // 예제에서는 5를 반환함.

12. Provider::GetFieldDescriptorAt (dwIndex = 0)
    // dwIndex 번째의 필드 정보를 담은 CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR 정보 반환
13. Provider::GetFieldDescriptorAt (dwIndex = 1)
14. Provider::GetFieldDescriptorAt (dwIndex = 2)
15. Provider::GetFieldDescriptorAt (dwIndex = 3)
16. Provider::GetFieldDescriptorAt (dwIndex = 4)

17. Credential::GetBitmapValue (dwFieldID = 0; tile image)
    // GetFieldDescriptorCount로 반환한 0번째 필드의 값은 
    // BITMAP 유형이라고 GetFieldDescriptorAt에서 반환했으므로 GetBitmapValue 메서드가 호출됨
    // 여기서 사용자 계정을 나타내는 이미지를 반환

18. Credential::GetStringValue (dwFieldID = 1; user name field)
19. Credential::GetFieldState (dwFieldID = 1; user name field)
20. Credential::GetStringValue (dwFieldID = 2; password field)
21. Credential::GetFieldState (dwFieldID = 2; password field)
22. Credential::GetSubmitButtonValue (dwFieldID = 3; submit button)
23. Credential::GetFieldState (dwFieldID = 3; submit button)
24. Credential::GetStringValue (dwFieldID = 4; domain name field)
25. Credential::GetFieldState (dwFieldID = 4; domain name field)

26. Credential::Advise 
27. Credential::GetSerialization 
28. Credential::UnAdvise 
29. Provider::UnAdvise 

30. [The WinLogon process calls LogonUser]

31. Credential::Advise 
32. Credential::ReportResult (ntsStatus = 0)
33. Credential::UnAdvise




주의할 것은, "CredUIPromptForWindowsCredentials" Win32 API로 호출하는 것과 윈도우 로그온 시에 호출되는 상황은 다르다는 점을 알아야 합니다. 이 때문에 Win32 API로 테스트를 잘 했다고 해도 로그온 시의 동작에 문제가 생길 수 있습니다. 이런 경우 디버깅을 해보는 것이 도움이 되는 데요. "Windows Vista Credential Provider Samples Overview UPDATE.doc" 문서에 보면 컴퓨터 대 컴퓨터로 커널 디버거를 이용해서 logonui.exe를 디버깅 하는 방법을 설명하고 있는데... 사실, kd.exe를 이용해 디버깅하는 것은 너무 번거롭고 그냥 쉽게 Visual Studio의 원격 디버깅을 이용해도 상관없습니다. 방법은 다음과 같습니다.

[A 컴퓨터는 Credential Provider를 설치하고, B 컴퓨터는 Visual Studio 2013 (Update 4) 설치 상태]

  1. A 컴퓨터에 Remote Tools for Microsoft Visual Studio 2013 Update 4 설치, 실행 후 "Tools" / "Options" 메뉴에서 "NoAuthentication"을 선택하고, "Allow any user to debug" 옵션 활성화
  2. B 컴퓨터에서 "DEBUG" / "Attach to Process..." 메뉴를 선택하고 "A 컴퓨터"에 연결 후 "LogonUI.exe" 프로세스를 "Attach"
  3. B 컴퓨터에서 mstsc.exe를 실행시켜 A 컴퓨터로 접속, 로그인을 시도하면, 등록된 "Credential Provider" DLL들이 로드되고 따라서 Visual Studio 디버깅 환경 내에서 BreakPoint 활성화

이렇게 하면 디버깅하기가 매우 쉽습니다. ^^

그런데 만약 개발한 Credential Provider의 버그로 인해 로그인 자체가 불가능하게 되었다면 어떻게 해야 할까요? 이런 경우에는, 안전 모드로 부팅해서 해당 Credential Provider DLL 등록을 해제하면 됩니다. 물론, 이건 너무 번거로운 방법이고 원격 레지스트리 연결을 통해 Credential Provider의 레지스트리 등록을 해제해 버리는 것이 더 빠릅니다. 또는, 등록 해제까지는 하지 말고, 아래의 글에 설명한 것처럼 "Disabled" (REG_DWORD) 값을 1로 등록해 주면 비활성화시키는 것도 가능합니다.

Testing a Credential Provider
; http://blogs.technet.com/b/ad/archive/2009/07/10/testing-a-credential-provider.aspx

참고로, 비활성화하는 것은 gpedit.msc에서도 제공합니다.




그 외에 이 글을 읽고 몇 가지 궁금한 사항들이 있으실 텐데요.

예를 들어, 지난번 글에 보면 SampleCredentialProvider를 설치 후 로그인 화면에 Administrator과 Guest 계정 2개를 선택할 수 있는 것이 추가되었는데요. 이건 예제이기 때문에 그런 것이고, 현실적으로 봤을 때는 해당 컴퓨터에 등록된 모든 계정 수 만큼 중복되어 보여지는 것이 맞습니다. 가령, "지문 인식"을 위한 Credential Provider를 만든다면 지문을 등록한 컴퓨터 계정은 모두 로그온 화면에 나와야 하는 것입니다.

문제는, 로그온 아이콘들이 이런 식으로 중복되면 사용자에게 혼란을 가져올 수 있다는 점인데, 이런 문제를 해결하려면 기존 Credential Provider를 비활성화 시켜야 합니다. 하지만, Credential Provider를 제품으로 만들었다면 이런 식으로 처리하는 것은 그다지 우아한 방법은 아닙니다. 대신 마이크로소프트는 이런 상황에서 사용할 수 있도록 필터링할 수 있는 ICredentialProviderFilter 인터페이스를 정의하고 있으므로 이를 통해서 하는 것도 좋습니다. 이에 대해서는 다음의 글에서 간략하게 설명하고 있습니다.

How to hide credential providers from Login Screen for Windows 7 
; http://www.pagepinner.com/2013/12/how-to-hide-credential-providers-from.html

그런데, 위의 방법 또한 그리 매끄러운 해결 방법은 아닙니다. 이 때문에 마이크로소프트는 윈도우 8에서 Picture Password를 구현하는 것과 함께 2세대 Credential Provider 체계를 만들어 공개했습니다. 즉, Credential Provider들이 '하나의 계정'으로 묶이면서 로그인 방법을 선택할 수 있도록 바뀐 것입니다. (이것 때문에 "Picture password" Credential Provider가 윈도우 8에 추가되었음에도 불구하고 사용자 계정 로그온 아이콘이 2배로 늘어나지 않게 되었습니다.)

1세대, 2세대 Credential Provider의 차이점은 다음의 문서에서 자세하게 설명하고 있으니 참조하시면 되겠습니다.

Credential Provider Framework Changes in Windows 8
; http://go.microsoft.com/fwlink/p/?linkid=253508

(* 마이크로소프트 측의 링크가 더 이상 제공되지 않아 이 글에 첨부해 두었습니다.)

비스타/7 시절에 개발된 Credential Provider들은 모두 윈도우 8에서 기본적으로 1세대 Credential Provider로 취급받으며 동작 방식은 비스타/7과 동일합니다. 단지, 그것들을 2세대 Credential Provider를 만족하는 인터페이스를 구현해 주는 경우 Picture Password처럼 좀 더 자연스럽게 윈도우 8에서 통합할 수 있으니 고려할 만한 가치가 있습니다.




마지막으로 한 가지만 더 이야기하자면.

기존 마이크로소프트가 구현한 Credential Provider도 쉽게 래핑할 수 있습니다. 이에 대해서는 Windows Vista Credential Provider Samples 예제에 "SampleWrapExistingCredentialProvider" 프로젝트를 참고하시면 됩니다. 이 예제는, 마이크로소프트의 PasswordCredentialProvider를 래핑하는 방법을 보여주고 있습니다. 이렇게 구현한 Credential Provider를 배포하는 경우 사용자 컴퓨터에 기존의 PasswordCredentialProvider가 활성화 될 필요가 없을 텐데요, 이 부분은 위에서 설명했던 ICredentialProviderFilter 인터페이스를 구현한 개체로 처리해주면 됩니다.




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







[최초 등록일: ]
[최종 수정일: 5/11/2023]

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

비밀번호

댓글 작성자
 



2017-02-26 03시07분
[guest]
2018-05-24 02시37분
@P 마이크로소프트 측의 링크가 더 이상 제공되지 않아 이 글에 첨부해 두었습니다.
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13606정성태4/24/202498닷넷: 2247. C# - tensorflow 연동 (MNIST 예제)파일 다운로드1
13605정성태4/23/2024328닷넷: 2246. C# - Python.NET을 이용한 파이썬 소스코드 연동파일 다운로드1
13604정성태4/22/2024349오류 유형: 901. Visual Studio - Unable to set the next statement. Set next statement cannot be used in '[Exception]' call stack frames.
13603정성태4/21/2024600닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동파일 다운로드1
13602정성태4/20/2024797닷넷: 2244. C# - PCM 오디오 데이터를 연속(Streaming) 재생 (Windows Multimedia)파일 다운로드1
13601정성태4/19/2024838닷넷: 2243. C# - PCM 사운드 재생(NAudio)파일 다운로드1
13600정성태4/18/2024848닷넷: 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/2024867닷넷: 2239. C# - WAV 파일의 PCM 데이터 생성 및 출력파일 다운로드1
13596정성태4/14/20241052닷넷: 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/20241196닷넷: 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/20241063개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241302Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241559Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...