EternalWindows
LSP / LSPサンプル

今回は、実際にLSPを開発する例を示します。 まず、defファイルの中身について確認します。

EXPORTS   
    WSPStartup
    GetLspGuid

defファイルには、DLLがエクスポートする関数を記述することになります。 ws2_32.dllは、Winsockアプリケーションが初めてsocketを呼び出した場合にLSPのWSPStartupを呼び出すことになっているため、 WSPStartupは必ずエクスポートしておくことになります。 GetLspGuidは、このLSPのGUIDを返す関数であり、 これはLSPのインストーラーが必要とします。 よって、インストーラーが呼び出せるようにエクスポートしておくことになります。

DLLのコードは、次のようになります。

#include <ws2spi.h>
#include <shlobj.h>

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

GUID g_guidProvider = {0x6b7ebbe2, 0x40eb, 0x41ec, {0xbe, 0xdb, 0xc0, 0xcc, 0xe9, 0xba, 0xb1, 0xfb}};

WSPUPCALLTABLE g_upcallTable = {0};
WSPPROC_TABLE  g_procTable = {0};
HMODULE        g_hmod = NULL;

int WSPAPI WSPStartup(WORD wVersionRequested, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable);
SOCKET WSPAPI WSPSocket(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags, LPINT lpErrno);
int WSPAPI WSPConnect(SOCKET s, const struct sockaddr *name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, LPINT lpErrno);
int WSPAPI WSPSend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno);
int WSPAPI WSPRecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno);
int WSPAPI WSPCleanup(LPINT lpErrno);

BOOL FindNextEntry(LPWSAPROTOCOL_INFOW lpOverEntry, LPWSAPROTOCOL_INFOW lpNextEntry);
void WriteLogFile(LPTSTR lpszData);

int WSPAPI WSPStartup(WORD wVersionRequested, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable)
{
	int               nError;
	WCHAR             szDllPath[256];
	WCHAR             szDllPathEnv[256];
	DWORD             dwSize;
	WSPDATA           wspData;
	LPWSPSTARTUP      lpfnWSPStartup;
	WSAPROTOCOL_INFOW nextEntry;
	
	WriteLogFile(TEXT("WSAStartup"));
	
	if (!FindNextEntry(lpProtocolInfo, &nextEntry))
		return WSAEPROVIDERFAILEDINIT;

	dwSize = sizeof(szDllPathEnv) / sizeof(WCHAR);
	WSCGetProviderPath(&nextEntry.ProviderId, szDllPathEnv, (LPINT)&dwSize, &nError);

	dwSize = sizeof(szDllPath) / sizeof(WCHAR);
	ExpandEnvironmentStringsW(szDllPathEnv, szDllPath, dwSize);

	g_hmod = LoadLibraryW(szDllPath);
	if (g_hmod == NULL)
		return WSAEPROVIDERFAILEDINIT;

	lpfnWSPStartup = (LPWSPSTARTUP)GetProcAddress(g_hmod, "WSPStartup");
	if (lpfnWSPStartup == NULL) {
		FreeLibrary(g_hmod);
		return WSAEPROVIDERFAILEDINIT;
	}
	
	if (lpfnWSPStartup(wVersionRequested, &wspData, &nextEntry, UpcallTable, &g_procTable) != 0) {
		FreeLibrary(g_hmod);
		return WSAEPROVIDERFAILEDINIT;
	}

	CopyMemory(lpProcTable, &g_procTable, sizeof(WSPPROC_TABLE));
	lpProcTable->lpWSPCleanup = WSPCleanup;
	lpProcTable->lpWSPConnect = WSPConnect;
	lpProcTable->lpWSPRecv    = WSPRecv;
	lpProcTable->lpWSPSend    = WSPSend;
	lpProcTable->lpWSPSocket  = WSPSocket;

	CopyMemory(lpWSPData, &wspData, sizeof(WSPDATA));
	CopyMemory(&g_upcallTable, &UpcallTable, sizeof(WSPUPCALLTABLE));

	return 0;
}

SOCKET WSPAPI WSPSocket(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags, LPINT lpErrno)
{
	SOCKET soc;
	SOCKET socModify;
	
	WriteLogFile(TEXT("socket"));

	soc = g_procTable.lpWSPSocket(af, type, protocol, lpProtocolInfo, g, dwFlags, lpErrno);
	if (soc != INVALID_SOCKET) {
		socModify = g_upcallTable.lpWPUModifyIFSHandle(lpProtocolInfo->dwCatalogEntryId, soc, lpErrno);
		if (soc != socModify)
			soc = INVALID_SOCKET;
	}

	return soc;
}

int WSPAPI WSPConnect(SOCKET s, const struct sockaddr *name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, LPINT lpErrno)
{
	WriteLogFile(TEXT("connect"));

	return g_procTable.lpWSPConnect(s, name, namelen, lpCallerData, lpCalleeData, lpSQOS, lpGQOS, lpErrno);
}

int WSPAPI WSPSend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno)
{
	WriteLogFile(TEXT("send"));

	return g_procTable.lpWSPSend(s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno);
}

int WSPAPI WSPRecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno)
{
	WriteLogFile(TEXT("recv"));
	
	return g_procTable.lpWSPRecv(s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno);
}

int WSPAPI WSPCleanup(LPINT lpErrno)
{
	int nResult;

	WriteLogFile(TEXT("WSPCleanup"));
	
	nResult = g_procTable.lpWSPCleanup(lpErrno);
	FreeLibrary(g_hmod);

	return nResult;
}

void WSPAPI GetLspGuid(LPGUID lpGuid)
{
	CopyMemory(lpGuid, &g_guidProvider, sizeof(GUID));
}

BOOL FindNextEntry(LPWSAPROTOCOL_INFOW lpOverEntry, LPWSAPROTOCOL_INFOW lpNextEntry)
{
	int                 i;
	int                 nError;
	int                 nTotalEntryCount;
	DWORD               dwSize;
	LPWSAPROTOCOL_INFOW lpEntryList;

	WSCEnumProtocols(NULL, NULL, &dwSize, &nError);
	lpEntryList = (LPWSAPROTOCOL_INFOW)HeapAlloc(GetProcessHeap(), 0, dwSize);
	nTotalEntryCount = WSCEnumProtocols(NULL, lpEntryList, &dwSize, &nError);

	for (i = 0; i < nTotalEntryCount; i++) {
		if (lpEntryList[i].dwCatalogEntryId == lpOverEntry->ProtocolChain.ChainEntries[1]) {
			CopyMemory(lpNextEntry, &lpEntryList[i], sizeof(WSAPROTOCOL_INFOW));
			HeapFree(GetProcessHeap(), 0, lpEntryList);
			return TRUE;
		}
	}
	
	HeapFree(GetProcessHeap(), 0, lpEntryList);

	return FALSE;
}

void WriteLogFile(LPTSTR lpszData)
{
	TCHAR  szFileName[] = TEXT("\\lsplog.txt"); 
	TCHAR  szModulePath[MAX_PATH];
	TCHAR  szDesktopPath[MAX_PATH];
	TCHAR  szBuf[1024];
	HANDLE hFile;
	DWORD  dwResult;	
	
	SHGetSpecialFolderPath(NULL, szDesktopPath, CSIDL_DESKTOPDIRECTORY, FALSE);
	lstrcat(szDesktopPath, szFileName);

	hFile = CreateFile(szDesktopPath, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return;
	
	SetFilePointer(hFile, 0, NULL, FILE_END);
	
	GetModuleFileName(GetModuleHandle(NULL), szModulePath, sizeof(szModulePath) / sizeof(TCHAR));
	wsprintf(szBuf, TEXT("%s : %s\r\n"), szModulePath, lpszData);
	WriteFile(hFile, szBuf, lstrlen(szBuf) * sizeof(TCHAR), &dwResult, NULL);

	CloseHandle(hFile);
}

前節で示したようにWSPStartupでは、lpProcTableのいくつかのメンバに独自の関数を設定しています。 たとえば、lpWSPConnectにはWSPConnectという関数を指定していますから、 Winsockアプリケーションがconnectを呼び出した場合は、 WSPConnectが呼ばれることになります。 WSPConnectとWSPSend、そしてWSPRecvの実装は次のようになっています。

int WSPAPI WSPConnect(SOCKET s, const struct sockaddr *name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, LPINT lpErrno)
{
	WriteLogFile(TEXT("connect"));

	return g_procTable.lpWSPConnect(s, name, namelen, lpCallerData, lpCalleeData, lpSQOS, lpGQOS, lpErrno);
}

int WSPAPI WSPSend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno)
{
	WriteLogFile(TEXT("send"));

	return g_procTable.lpWSPSend(s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno);
}

int WSPAPI WSPRecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, LPWSATHREADID lpThreadId, LPINT lpErrno)
{
	WriteLogFile(TEXT("recv"));
	
	return g_procTable.lpWSPRecv(s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine, lpThreadId, lpErrno);
}

これらの関数が行っていることは、WriteLogFileという自作関数を通じて関数名をファイルに保存しているだけです。 これにより、ユーザーはそのファイルを開くことで、 どのようなWinsock関数が呼ばれたのかを確認することができます。 各関数で行われなければならない実際の処理は、下のエントリの関数に任せるようにするため、 全ての引数をg_procTableの適切な関数に指定しています。

WSPSocketの実装は、次のようになっています。

SOCKET WSPAPI WSPSocket(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags, LPINT lpErrno)
{
	SOCKET soc;
	SOCKET socModify;
	
	WriteLogFile(TEXT("socket"));

	soc = g_procTable.lpWSPSocket(af, type, protocol, lpProtocolInfo, g, dwFlags, lpErrno);
	if (soc != INVALID_SOCKET) {
		socModify = g_upcallTable.lpWPUModifyIFSHandle(lpProtocolInfo->dwCatalogEntryId, soc, lpErrno);
		if (soc != socModify)
			soc = INVALID_SOCKET;
	}

	return soc;
}

LSPは、Winsockアプリケーションのsocket呼び出しを検出するつもりがない場合でも、 この関数を実装して適切な処理を行う必要があります。 理由は、このLSPから返されるソケットが、 単に下に存在するエントリが作成したソケットに過ぎないからです。 このように、下のエントリのソケットを使用するLSPはIFS LSPと呼ばれ、 WSPUPCALLTABLE構造体のlpWPUModifyIFSHandleを呼び出さなければならないことになっています。 具体的には、g_procTable.lpWSPSocketから下のエントリのWSPSocketを呼び出し、 ここで取得した下のエントリのソケットをlpWPUModifyIFSHandleに指定します。 ここで返されたソケットが、下のエントリのソケットと同一である場合、 LSPは下のエントリのソケットを返してよいことになります。 ちなみに、WSPUPCALLTABLE構造体に格納されている関数は、ws2_32.dllによって実装されています。 LSPはws2_32.dllの機能を利用したい場合に、WSPUPCALLTABLE構造体の関数を呼び出します。

WSPCleanupの実装は、次のようになっています。

int WSPAPI WSPCleanup(LPINT lpErrno)
{
	int nResult;

	WriteLogFile(TEXT("WSPCleanup"));
	
	nResult = g_procTable.lpWSPCleanup(lpErrno);
	FreeLibrary(g_hmod);

	return nResult;
}

この関数を独自に実装している理由は、FreeLibraryによって下のエントリのDLLをアンロードするためです。 このようにしなければ、WinsockアプリケーションがWSACleanupを呼び出した後でも、 下のエントリのDLLがプロセスのアドレス空間に残ってしまい、 あまり好ましいこととはいえません。 WSPStartupのlpfnWSPStartupを呼び出した後にFreeLibraryを呼び出せばよいようにも思えますが、 これをしてしまうとDLLのアンロードによって、 lpProcTableやg_procTableに格納されているアドレスが無効になってしまいます。 よって、FreeLibraryの呼び出しは、ws2_32.dllからの最後の呼び出しなるWSPCleanupで行うべきといえます。

GetLspGuidの実装は、次のようになっています。

void WSPAPI GetLspGuid(LPGUID lpGuid)
{
	CopyMemory(lpGuid, &g_guidProvider, sizeof(GUID));
}

この関数は、グローバルに定義しておいたLSPのGUIDを引数にコピーするだけです。 これによって、インストーラーはLSPのGUIDを取得できるようになります。 g_guidProviderの初期値については、guidgen.exeやCoCreateGuildで得られたGUID値を指定しておきます。

WriteLogFileの実装は、次のようになっています。

void WriteLogFile(LPTSTR lpszData)
{
	TCHAR  szFileName[] = TEXT("\\lsplog.txt"); 
	TCHAR  szModulePath[MAX_PATH];
	TCHAR  szDesktopPath[MAX_PATH];
	TCHAR  szBuf[1024];
	HANDLE hFile;
	DWORD  dwResult;	
	
	SHGetSpecialFolderPath(NULL, szDesktopPath, CSIDL_DESKTOPDIRECTORY, FALSE);
	lstrcat(szDesktopPath, szFileName);

	hFile = CreateFile(szDesktopPath, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return;
	
	SetFilePointer(hFile, 0, NULL, FILE_END);
	
	GetModuleFileName(GetModuleHandle(NULL), szModulePath, sizeof(szModulePath) / sizeof(TCHAR));
	wsprintf(szBuf, TEXT("%s : %s\r\n"), szModulePath, lpszData);
	WriteFile(hFile, szBuf, lstrlen(szBuf) * sizeof(TCHAR), &dwResult, NULL);

	CloseHandle(hFile);
}

LSP内におけるセキュリティ操作は、このLSPがロードされたプロセスのセキュリティコンテキストに大きく左右されます。 たとえば、ログファイルをCドライブの直下に作成する設計にした場合、 制限されたプロセスにLSPがロードされた場合は、ファイルの書き込みに失敗することになります。 この点を踏まえて、書き込み対象とするフォルダはデスクトップにしています。 ただし、Window Vista以降のInternet Explorerでは、整合性レベルの関係でデスクトップへの書き込みは失敗します。 関数の処理としては、SHGetSpecialFolderPathでデスクトップのパスを取得し、 これにファイル名を連結させてCreateFileを呼び出します。 その後、ファイルに追加書き込みができるようにファイルポインタを終端に移動させ、 WriteFileでバッファの内容をファイルに書き込みます。 バッファに含まれるのは、このLSPをロードしているプロセス、 つまりWinsock関数を呼び出したプロセスのパスと、呼び出したWinsock関数の名前です。

WSPConnectの実装例

今回のLSPでは、単純に各関数内でWriteLogFileを呼び出しているだけですが、 当然ながら実際の開発では関数の引数を参照することになります。 たとえば、特定のIPアドレスに対する接続を無効にしたい場合、 WSPConnectの実装は次のようになるでしょう。 LSPは、TCP/IPv4の上にインストールされているものと仮定します。

int WSPAPI WSPConnect(SOCKET s, const struct sockaddr *name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, LPINT lpErrno)
{
	LPSOCKADDR_IN lpSockAddrIn = (LPSOCKADDR_IN)name;
	DWORD         dwIpAddress = inet_addr("127.0.0.1");

	if (lpSockAddrIn-> == dwIpAddress) {
		*lpErrno = WSAEADDRNOTAVAIL;
		return SOCKET_ERROR;
	}

	return g_procTable.lpWSPConnect(s, name, namelen, lpCallerData, lpCalleeData, lpSQOS, lpGQOS, lpErrno);
}

第2引数のsockaddr構造体は、ポート番号やIPアドレスを格納しており、 これをSOCKADDR_INで表すことによって、各値を参照できるようになります。 通信先のIPアドレスはsin_addr.s_addrに格納されており、 これが予め定義しておいたIPアドレスと一致した場合は、 エラーを返すようにしています。 WSPSendやWSPRecvなどの関数では、WSABUF構造体からデータとサイズを参照することになります。



戻る