EternalWindows
SSPI / ハンドシェイク(通信有り)
対応するサーバーはこちら

今回のクライアントプログラムは、実際にサーバーと通信して認証を行います。 事前に対応するサーバープログラムを起動しておいてください。

#define  SECURITY_WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <security.h>

#pragma comment (lib, "secur32.lib")
#pragma comment (lib, "ws2_32.lib")

SOCKET g_soc = INVALID_SOCKET;

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort);
BOOL ClientHandshake(PCredHandle phCredential, PCtxtHandle phContext);
void SendData(LPVOID lpData, ULONG uSize);
LPVOID ReceiveData(ULONG *lpuSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	CredHandle hCredential;
	CtxtHandle hContext;
	TimeStamp  ts;

	if (AcquireCredentialsHandle(NULL, TEXT("NTLM"), SECPKG_CRED_OUTBOUND, NULL, NULL, NULL, NULL, &hCredential, &ts) != SEC_E_OK) {
		MessageBox(NULL, TEXT("クレデンシャルハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	g_soc = InitializeWinsock("localhost", "3000");
	if (g_soc == INVALID_SOCKET) {
		FreeCredentialsHandle(&hCredential);
		WSACleanup();
		return 0;
	}
	
	if (!ClientHandshake(&hCredential, &hContext)) {
		FreeCredentialsHandle(&hCredential);
		closesocket(g_soc);
		WSACleanup();
		MessageBox(NULL, TEXT("認証に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	MessageBox(NULL, TEXT("認証に成功しました。"), TEXT("クライアント"), MB_OK);

	DeleteSecurityContext(&hContext);
	FreeCredentialsHandle(&hCredential);
	closesocket(g_soc);
	WSACleanup();

	return 0;
}

BOOL ClientHandshake(PCredHandle phCredential, PCtxtHandle phContext)
{
	BOOL            bFirst = TRUE;
	ULONG           uSize;
	ULONG           uAttributes = ISC_REQ_STREAM;
	SecBuffer       sbOut[1];
	SecBuffer       sbIn[1];
	SecBufferDesc   sbdOut;
	SecBufferDesc   sbdIn;
	SECURITY_STATUS ss = SEC_I_CONTINUE_NEEDED;
	
	while (ss == SEC_I_CONTINUE_NEEDED) {
		if (!bFirst) {
			sbIn[0].pvBuffer   = ReceiveData(&uSize);
			sbIn[0].cbBuffer   = uSize;
			sbIn[0].BufferType = SECBUFFER_TOKEN;

			sbdIn.ulVersion = SECBUFFER_VERSION;
			sbdIn.cBuffers  = 1;
			sbdIn.pBuffers  = sbIn;
		}
		
		sbOut[0].cbBuffer   = 0;
		sbOut[0].BufferType = SECBUFFER_TOKEN;
		sbOut[0].pvBuffer   = NULL;

		sbdOut.ulVersion = SECBUFFER_VERSION;
		sbdOut.cBuffers  = 1;
		sbdOut.pBuffers  = sbOut;
		
		ss = InitializeSecurityContext(phCredential, bFirst ? NULL : phContext, NULL, uAttributes | ISC_REQ_ALLOCATE_MEMORY,
			0, SECURITY_NETWORK_DREP, bFirst ? NULL : &sbdIn, 0, phContext, &sbdOut, &uAttributes, NULL);
		
		if (sbOut[0].cbBuffer != 0) {
			SendData(sbOut[0].pvBuffer, sbOut[0].cbBuffer);
			FreeContextBuffer(sbOut[0].pvBuffer);
		}
		
		if (!bFirst)
			HeapFree(GetProcessHeap(), 0, sbIn[0].pvBuffer);

		bFirst = FALSE;
	}

	return ss == SEC_E_OK;
}

void SendData(LPVOID lpData, ULONG uSize)
{
	send(g_soc, (char *)&uSize, sizeof(ULONG), 0);
	send(g_soc, (char *)lpData, uSize, 0);
}

LPVOID ReceiveData(ULONG *lpuSize)
{
	ULONG  uSize;
	LPVOID lpData;

	recv(g_soc, (char *)&uSize, sizeof(ULONG), 0);
	lpData = HeapAlloc(GetProcessHeap(), 0, uSize);
	recv(g_soc, (char *)lpData, uSize, 0);
	
	*lpuSize = uSize;

	return lpData;
}

SOCKET InitializeWinsock(LPSTR lpszServerName, LPSTR lpszPort)
{
	WSADATA    wsaData;
	ADDRINFO   addrHints;
	LPADDRINFO lpAddrList;
	SOCKET     soc;
	
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	ZeroMemory(&addrHints, sizeof(addrinfo));
	addrHints.ai_family   = AF_INET;
	addrHints.ai_socktype = SOCK_STREAM;
	addrHints.ai_protocol = IPPROTO_TCP;

	if (getaddrinfo(lpszServerName, lpszPort, &addrHints, &lpAddrList) != 0) {
		MessageBox(NULL, TEXT("ホスト情報からアドレスの取得に失敗しました。"), NULL, MB_ICONWARNING);
		WSACleanup();
		return INVALID_SOCKET;
	}

	soc = socket(lpAddrList->ai_family, lpAddrList->ai_socktype, lpAddrList->ai_protocol);

	if (connect(soc, lpAddrList->ai_addr, (int)lpAddrList->ai_addrlen) == SOCKET_ERROR) {
		int nError = WSAGetLastError();
		if (nError == WSAECONNREFUSED)
			MessageBox(NULL, TEXT("サーバーがリッスンしていません。"), NULL, MB_ICONWARNING);
		else if (nError == WSAEHOSTUNREACH)
			MessageBox(NULL, TEXT("サーバーが存在しません。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("サーバーへの接続が失敗しました。"), NULL, MB_ICONWARNING);
		freeaddrinfo(lpAddrList);
		closesocket(soc);
		WSACleanup();
		return INVALID_SOCKET;
	}
	
	freeaddrinfo(lpAddrList);

	return soc;
}

このプログラムでは、通信に使用するネットワークAPIとしてWinsockを採用しています。 自作関数のInitializeWinsockは、Winsockと初期化とサーバーへの接続を行い、 戻り値はサーバーと接続されたソケットの記述子になります。 第1引数は接続するサーバーの名前であり、第2引数はサーバーのポート番号です。 接続に成功したらClientHandshakeを呼び出し、 サーバーと実際に通信することになります。 データを送信する際に呼び出すSendDataは、次のようになっています。

void SendData(LPVOID lpData, ULONG uSize)
{
	send(g_soc, (char *)&uSize, sizeof(ULONG), 0);
	send(g_soc, (char *)lpData, uSize, 0);
}

データはlpDataに格納されていますが、 これを送信する前にデータのサイズを送信しておきます。 これにより、受信側はデータのサイズを前もって知ることができます。 g_socはサーバーと接続されたソケットであり、 グローバル変数として定義されています。 データを受信するReceiveDataは、次のようになっています。

LPVOID ReceiveData(ULONG *lpuSize)
{
	ULONG  uSize;
	LPVOID lpData;

	recv(g_soc, (char *)&uSize, sizeof(ULONG), 0);
	lpData = HeapAlloc(GetProcessHeap(), 0, uSize);
	recv(g_soc, (char *)lpData, uSize, 0);
	
	*lpuSize = uSize;

	return lpData;
}

通信相手は、データのサイズから送信しているため、 1回目のrecvの呼び出しでは、データのサイズを取得することになります。 ここで、そのサイズだけメモリを確保し、 2回目のrecvの呼び出しによって実際にデータを取得することになります。 取得したデータのサイズをReceiveDataの呼び出し側が特定できるために、 lpuSizeにサイズを代入しておきます。 関数の戻り値は、取得したデータへのアドレスになります。

SSPIとローカルログオン

クライアントがAcquireCredentialsHandleの第2引数にNTLMを指定する場合、 第5引数にはSEC_WINNT_AUTH_IDENTITY構造体のアドレスを指定することができます。 この構造体には、ログオンするユーザー名やパスワードを表すメンバが含まれているため、 これを初期化することで、任意のユーザーとしてサーバーにログオンすることができるようになります。 次に、例を示します。

authIdentity.User           = (USHORT *)szUserName;
authIdentity.UserLength     = lstrlen(szUserName);
authIdentity.Domain         = (USHORT *)szDomainName;
authIdentity.DomainLength   = lstrlen(szDomainName);
authIdentity.Password       = (USHORT *)szPassword;
authIdentity.PasswordLength = lstrlen(szPassword);
authIdentity.Flags          = SEC_WINNT_AUTH_IDENTITY_UNICODE;

各種メンバの意味は、メンバ名が示す通りとなっています。 Flagsは、アプリケーションがUNICODEとして実行されている場合は、SEC_WINNT_AUTH_IDENTITY_UNICODEを指定します。 これらの認証情報を基にサーバーで正しく認証された場合は、 クライアントがサーバーにログオンしたことになります。 つまり、サーバー上にログオンタイプがNetworkであるログオンセッションが作成されます。 第5引数にSEC_WINNT_AUTH_IDENTITY構造体ではなくNULLを指定した場合は、 呼び出し側スレッドのアカウントで認証が行われますが、 認証が成功してもログオンセッションが作成されることはありません。

SEC_WINNT_AUTH_IDENTITY構造体を使用すれば、サーバーへログオンすることができるわけですが、 それではクライアントサーバーを同じコンピュータ上で実行した場合はどうなるのでしょうか。 答えは、ローカルコンピュータへのローカルログオンです。 つまり、下記のようにLogonUserを呼び出すのとほぼ同じ意味を持ちます。

LogonUser(szUserName, NULL, szPassword, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, &hToken);

SSPIによるログオンが複雑な手順を踏むのに対して、 LogonUserの呼び出しは上記のように単純であるため、 SSPIによるローカルログオンを実行する必要はないように思えます。 ただし、LogonUserはWindows 95と98では実装されておらず、 Windows NTと2000では実装こそされているものの、 SE_TCB_NAME特権が別途必要となり、環境によっては呼び出せない場合があります。 よって、これらの環境を想定する場合は、SSPIによるローカルログオンの方法を知っておくべきといえます。

ローカルログオンの方法といっても、それは単純に1つのアプリケーション内で、 クライアントとサーバーのコードを実行するだけです。 つまり、ClientHandshakeとServerHandshakeを交互に呼び出します。 次に、例を示します。

#define  SECURITY_WIN32
#include <windows.h>
#include <security.h>

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

BOOL ClientHandshake(PCredHandle phCredential, PCtxtHandle phContext, PVOID *ppData, PULONG puSize, BOOL bFirst);
BOOL ServerHandshake(PCredHandle phCredential, PCtxtHandle phContext, PVOID *ppData, PULONG puSize, BOOL bFirst);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	CredHandle              hCredentialClient, hCredentialServer;
	CtxtHandle              hContextClient, hContextServer;
	TimeStamp               ts;
	PVOID                   pData;
	ULONG                   uSize;
	BOOL                    bResult;
	TCHAR                   szUserName[] = TEXT("");
	TCHAR                   szPassword[] = TEXT("");
	TCHAR                   szDomainName[] = TEXT("");
	SEC_WINNT_AUTH_IDENTITY authIdentity;

#if UNICODE
	authIdentity.User           = (USHORT *)szUserName;
	authIdentity.UserLength     = lstrlen(szUserName);
	authIdentity.Domain         = (USHORT *)szDomainName;
	authIdentity.DomainLength   = lstrlen(szDomainName);
	authIdentity.Password       = (USHORT *)szPassword;
	authIdentity.PasswordLength = lstrlen(szPassword);
	authIdentity.Flags          = SEC_WINNT_AUTH_IDENTITY_UNICODE;
#else
	authIdentity.User           = (USHORT *)szUserName;
	authIdentity.UserLength     = lstrlen(szUserName);
	authIdentity.Domain         = (USHORT *)szDomainName;
	authIdentity.DomainLength   = lstrlen(szDomainName);
	authIdentity.Password       = (USHORT *)szPassword;
	authIdentity.PasswordLength = lstrlen(szPassword);
	authIdentity.Flags          = SEC_WINNT_AUTH_IDENTITY_ANSI;
#endif
	
	if (AcquireCredentialsHandle(NULL, TEXT("NTLM"), SECPKG_CRED_OUTBOUND, NULL, &authIdentity, NULL, NULL, &hCredentialClient, &ts) != SEC_E_OK) {
		MessageBox(NULL, TEXT("クレデンシャルハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	if (AcquireCredentialsHandle(NULL, TEXT("NTLM"), SECPKG_CRED_INBOUND, NULL, NULL, NULL, NULL, &hCredentialServer, &ts) != SEC_E_OK) {
		FreeCredentialsHandle(&hCredentialClient);
		MessageBox(NULL, TEXT("クレデンシャルハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	ClientHandshake(&hCredentialClient, &hContextClient, &pData, &uSize, TRUE);
	ServerHandshake(&hCredentialServer, &hContextServer, &pData, &uSize, TRUE);
	ClientHandshake(&hCredentialClient, &hContextClient, &pData, &uSize, FALSE);
	bResult = ServerHandshake(&hCredentialServer, &hContextServer, &pData, &uSize, FALSE);

	if (bResult)
		MessageBox(NULL, TEXT("認証に成功しました。"), NULL, MB_OK);
	else
		MessageBox(NULL, TEXT("認証に失敗しました。"), NULL, MB_ICONWARNING);

	DeleteSecurityContext(&hContextClient);
	DeleteSecurityContext(&hContextServer);
	FreeCredentialsHandle(&hCredentialClient);
	FreeCredentialsHandle(&hCredentialServer);

	return 0;
}

BOOL ClientHandshake(PCredHandle phCredential, PCtxtHandle phContext, PVOID *ppData, PULONG puSize, BOOL bFirst)
{
	ULONG           uAttributes = 0;
	SecBuffer       sbOut[1];
	SecBuffer       sbIn[1];
	SecBufferDesc   sbdOut;
	SecBufferDesc   sbdIn;
	SECURITY_STATUS ss;
	
	if (!bFirst) {
		sbIn[0].pvBuffer   = *ppData;
		sbIn[0].cbBuffer   = *puSize;
		sbIn[0].BufferType = SECBUFFER_TOKEN;

		sbdIn.ulVersion = SECBUFFER_VERSION;
		sbdIn.cBuffers  = 1;
		sbdIn.pBuffers  = sbIn;
	}
	
	sbOut[0].cbBuffer   = 0;
	sbOut[0].BufferType = SECBUFFER_TOKEN;
	sbOut[0].pvBuffer   = NULL;

	sbdOut.ulVersion = SECBUFFER_VERSION;
	sbdOut.cBuffers  = 1;
	sbdOut.pBuffers  = sbOut;
	
	ss = InitializeSecurityContext(phCredential, bFirst ? NULL : phContext, NULL, uAttributes | ISC_REQ_ALLOCATE_MEMORY,
		0, SECURITY_NETWORK_DREP, bFirst ? NULL : &sbdIn, 0, phContext, &sbdOut, &uAttributes, NULL);
	
	if (*ppData != NULL)
		FreeContextBuffer(*ppData);
	
	*ppData = sbOut[0].pvBuffer;
	*puSize = sbOut[0].cbBuffer;

	return ss == SEC_E_OK;
}

BOOL ServerHandshake(PCredHandle phCredential, PCtxtHandle phContext, PVOID *ppData, PULONG puSize, BOOL bFirst)
{
	ULONG           uAttributes = 0;
	SecBuffer       sbOut[1];
	SecBuffer       sbIn[1];
	SecBufferDesc   sbdOut;
	SecBufferDesc   sbdIn;
	SECURITY_STATUS ss;
	
	sbIn[0].pvBuffer   = *ppData;
	sbIn[0].cbBuffer   = *puSize;
	sbIn[0].BufferType = SECBUFFER_TOKEN;

	sbdIn.ulVersion = SECBUFFER_VERSION;
	sbdIn.cBuffers  = 1;
	sbdIn.pBuffers  = sbIn;
	
	sbOut[0].cbBuffer   = 0;
	sbOut[0].pvBuffer   = NULL;
	sbOut[0].BufferType = SECBUFFER_TOKEN;

	sbdOut.ulVersion = SECBUFFER_VERSION;
	sbdOut.cBuffers  = 1;
	sbdOut.pBuffers  = sbOut;
	
	ss = AcceptSecurityContext(phCredential, bFirst ? NULL : phContext, &sbdIn, uAttributes | ASC_REQ_ALLOCATE_MEMORY,
		SECURITY_NETWORK_DREP, phContext, &sbdOut, &uAttributes, NULL);
	
	if (*ppData != NULL)
		FreeContextBuffer(*ppData);
	
	*ppData = sbOut[0].pvBuffer;
	*puSize = sbOut[0].cbBuffer;

	return ss == SEC_E_OK;
}

WinMainでは、AcquireCredentialsHandleを2回呼び出しています。 1つは、クライアント用の認証ハンドルの取得で、もう1つはサーバー用の認証ハンドルの取得です。 クライアントの呼び出しでは、SEC_WINNT_AUTH_IDENTITY構造体を指定し、 特定のユーザーがログオンできるようにします。 認証ハンドルを取得すれば、ClientHandshakeとServerHandshakeを交互に呼び出します。 NTLMの場合は、これを2回実行すれば認証は終了することになります。

実を言うと、LogonUserにLOGON32_LOGON_NETWORKを指定したログオンと、 SSPIによるログオンには若干の違いがあります。 それは、次に示すレジストリキーのforceguestエントリが1である場合の動作です。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa

forceguestが1である場合、SSPIによるログオンは必ずGuestアカウントとして行われます。 つまり、ユーザー名とパスワードが正しい場合でもGuestアカウントとしてログオンされますし、 ユーザー名やパスワードが正しくない場合でもGuestアカウントとしてログオンされます。 一方、LogonUserにLOGON32_LOGON_NETWORKを指定したログオンでは、 ユーザー名とパスワードが正しい場合はそのユーザーでログオンされますが、 ユーザー名やパスワードが正しくない場合はGuestアカウントとしてログオンされます。 ただし、どちらのログオンについても、 Guestアカウントにネットワークログオンが許可されていない場合は、失敗します。 forceguestが0である場合は、どちらのログオンについても、 ユーザー名やパスワードが正しくない場合はログオン失敗となります。



戻る