Microsoft MVP성태의 닷넷 이야기
VC++: 86. Windows Vista부터 바뀐 Credential Provider 예제 분석 (2) [링크 복사], [링크+제목 복사],
조회: 23920
글쓴 사람
정성태 (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)
13536정성태1/23/20242595닷넷: 2209. .NET 8 - NonGC Heap / FOH (Frozen Object Heap) [1]
13535정성태1/22/20242454닷넷: 2208. C# - GCHandle 구조체의 메모리 분석
13534정성태1/21/20242255닷넷: 2207. C# - SQL Server DB를 bacpac으로 Export/Import파일 다운로드1
13533정성태1/18/20242470닷넷: 2206. C# - TCP KeepAlive의 서버 측 구현파일 다운로드1
13532정성태1/17/20242360닷넷: 2205. C# - SuperSimpleTcp 사용 시 주의할 점파일 다운로드1
13531정성태1/16/20242254닷넷: 2204. C# - TCP KeepAlive에 새로 추가된 Retry 옵션파일 다운로드1
13530정성태1/15/20242204닷넷: 2203. C# - Python과의 AES 암호화 연동파일 다운로드1
13529정성태1/15/20242066닷넷: 2202. C# - PublishAot의 glibc에 대한 정적 링킹하는 방법
13528정성태1/14/20242189Linux: 68. busybox 컨테이너에서 실행 가능한 C++, Go 프로그램 빌드
13527정성태1/14/20242134오류 유형: 892. Visual Studio - Failed to launch debug adapter. Additional information may be available in the output window.
13526정성태1/14/20242221닷넷: 2201. C# - Facebook 연동 / 사용자 탈퇴 처리 방법
13525정성태1/13/20242171오류 유형: 891. Visual Studio - Web Application을 실행하지 못하는 IISExpress
13524정성태1/12/20242238오류 유형: 890. 한국투자증권 KIS Developers OpenAPI - GW라우팅 중 오류가 발생했습니다.
13523정성태1/12/20242044오류 유형: 889. Visual Studio - error : A project with that name is already opened in the solution.
13522정성태1/11/20242211닷넷: 2200. C# - HttpClient.PostAsJsonAsync 호출 시 "Transfer-Encoding: chunked" 대신 "Content-Length" 헤더 처리
13521정성태1/11/20242274닷넷: 2199. C# - 한국투자증권 KIS Developers OpenAPI의 WebSocket Ping, Pong 처리
13520정성태1/10/20242019오류 유형: 888. C# - Unable to resolve service for type 'Microsoft.Extensions.ObjectPool.ObjectPool`....'
13519정성태1/10/20242109닷넷: 2198. C# - Reflection을 이용한 ClientWebSocket의 Ping 호출파일 다운로드1
13518정성태1/9/20242374닷넷: 2197. C# - ClientWebSocket의 Ping, Pong 처리
13517정성태1/8/20242210스크립트: 63. Python - 공개 패키지를 이용한 위성 이미지 생성 (pystac_client, odc.stac)
13516정성태1/7/20242320닷넷: 2196. IIS - AppPool의 "Disable Overlapped Recycle" 옵션의 부작용
13515정성태1/6/20242600닷넷: 2195. async 메서드 내에서 C# 7의 discard 구문 활용 사례 [1]
13514정성태1/5/20242276개발 환경 구성: 702. IIS - AppPool의 "Disable Overlapped Recycle" 옵션
13513정성태1/5/20242203닷넷: 2194. C# - WebActivatorEx / System.Web의 PreApplicationStartMethod 특성
13512정성태1/4/20242160개발 환경 구성: 701. IIS - w3wp.exe 프로세스의 ASP.NET 런타임을 항상 Warmup 모드로 유지하는 preload Enabled 설정
13511정성태1/4/20242180닷넷: 2193. C# - ASP.NET Web Application + OpenAPI(Swashbuckle) 스펙 제공
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...