Microsoft MVP성태의 닷넷 이야기
VC++: 86. Windows Vista부터 바뀐 Credential Provider 예제 분석 (2) [링크 복사], [링크+제목 복사],
조회: 24052
글쓴 사람
정성태 (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 마이크로소프트 측의 링크가 더 이상 제공되지 않아 이 글에 첨부해 두었습니다.
정성태

... 16  17  18  19  20  21  22  23  24  25  26  27  28  29  [30]  ...
NoWriterDateCnt.TitleFile(s)
12895정성태12/31/20219570.NET Framework: 1126. C# - snagit처럼 화면 캡처를 연속으로 수행해 동영상 제작 [1]파일 다운로드1
12894정성태12/30/20217533.NET Framework: 1125. C# - DefaultObjectPool<T>의 IDisposable 개체에 대한 풀링 문제 [3]파일 다운로드1
12893정성태12/27/20219161.NET Framework: 1124. C# - .NET Platform Extension의 ObjectPool<T> 사용법 소개파일 다운로드1
12892정성태12/26/20217132기타: 83. unsigned 형의 이전 값이 최댓값을 넘어 0을 지난 경우, 값의 차이를 계산하는 방법
12891정성태12/23/20217037스크립트: 38. 파이썬 - uwsgi의 --master 옵션
12890정성태12/23/20217178VC++: 152. Golang - (문자가 아닌) 바이트 위치를 반환하는 strings.IndexRune 함수
12889정성태12/22/20219633.NET Framework: 1123. C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법파일 다운로드1
12888정성태12/21/20217704.NET Framework: 1122. C# - ImageCodecInfo 사용 시 System.Drawing.Image와 System.Drawing.Bitmap에 따른 Save 성능 차이파일 다운로드1
12887정성태12/21/20219856오류 유형: 777. OpenCVSharp4를 사용한 프로그램 실행 시 "The type initializer for 'OpenCvSharp.Internal.NativeMethods' threw an exception." 예외 발생
12886정성태12/20/20217653스크립트: 37. 파이썬 - uwsgi의 --enable-threads 옵션 [2]
12885정성태12/20/20217908오류 유형: 776. uwsgi-plugin-python3 환경에서 MySQLdb 사용 환경
12884정성태12/20/20216943개발 환경 구성: 620. Windows 10+에서 WMI root/Microsoft/Windows/WindowsUpdate 네임스페이스 제거
12883정성태12/19/20217864오류 유형: 775. uwsgi-plugin-python3 환경에서 "ModuleNotFoundError: No module named 'django'" 오류 발생
12882정성태12/18/20216963개발 환경 구성: 619. Windows Server에서 WSL을 위한 리눅스 배포본을 설치하는 방법
12881정성태12/17/20217419개발 환경 구성: 618. WSL Ubuntu 20.04에서 파이썬을 위한 uwsgi 설치 방법 (2)
12880정성태12/16/20217290VS.NET IDE: 170. Visual Studio에서 .NET Core/5+ 역어셈블 소스코드 확인하는 방법
12879정성태12/16/202113565오류 유형: 774. Windows Server 2022 + docker desktop 설치 시 WSL 2로 선택한 경우 "Failed to deploy distro docker-desktop to ..." 오류 발생
12878정성태12/15/20218585개발 환경 구성: 617. 윈도우 WSL 환경에서 같은 종류의 리눅스를 다중으로 설치하는 방법
12877정성태12/15/20217254스크립트: 36. 파이썬 - pymysql 기본 예제 코드
12876정성태12/14/20217101개발 환경 구성: 616. Custom Sources를 이용한 Azure Monitor Metric 만들기
12875정성태12/13/20216742스크립트: 35. python - time.sleep(...) 호출 시 hang이 걸리는 듯한 문제
12874정성태12/13/20216753오류 유형: 773. shell script 실행 시 "$'\r': command not found" 오류
12873정성태12/12/20217897오류 유형: 772. 리눅스 - PATH에 등록했는데도 "command not found"가 나온다면?
12872정성태12/12/20217734개발 환경 구성: 615. GoLang과 Python 빌드가 모두 가능한 docker 이미지 만들기
12871정성태12/12/20217774오류 유형: 771. docker: Error response from daemon: OCI runtime create failed
12870정성태12/9/20216344개발 환경 구성: 614. 파이썬 - PyPI 패키지 만들기 (4) package_data 옵션
... 16  17  18  19  20  21  22  23  24  25  26  27  28  29  [30]  ...