セキュリティが声高に叫ばれている現在、アンチウイルスソフトやFirewallは、もはや無くてはならない存在になってしまいました。というわけで、今回はパケットフィルタリングについてやってみようと思います。一般的な商用のFirewallなどは、デバイスドライバ(フィルタドライバ)を使い、パケットフィルタリングを行っていますが、このテキストでは、アプリケーションレベルでそれをやってみることにします。
私が使用する環境は「WindowsXP + VC++.NET」ですが、Windows2000以降ならば、問題ないでしょう。
パケットフィルタリングとは、ルータやファイヤーウォールが持っている機能の一つで、送られてきたパケットを検査して通過させるかどうか判断する機能です。パケットのヘッダにはプロトコルや送信元アドレス、送信先アドレスやポート番号などの情報が含まれており、これを参照して通過するかどうかを決定します。そして、通過できなかったパケットは送信元に通知されたり、破棄されたりします。どのような方針に基づいて判断するかは、そのネットワークの管理者が任意に設定することになり、現在では、最も一般的かつ簡便なセキュリティ技術として知られています。最近のルータは大半が持っている機能ですが、よく知られているだけに破る手段も多く、他の技術と併用することが肝要です。(IT用語辞書e-wordより)
まずはMSDNのパケットフィルタリングリファレンスを読破してください。
あっ、やっぱり読破はしないでください。読破してしまったら、このテキスト読まなくても理解できてしまうので(笑)。なので、流し読みしてください。それで、どうやらパケットフィルタリング関連のAPIを利用すると、パケット通過の可否をアプリケーション側で決められるようです。というわけで、早速利用したプログラムを作成します。
----- ex1.cpp
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <fltdefs.h>
#pragma comment(lib, "iphlpapi.lib")
int main(void)
{
BYTE localAddr[4] = { 192,168,4,2 };
INTERFACE_HANDLE hFilterIf;
DWORD dwRet = PfCreateInterface(
NULL, PF_ACTION_DROP, PF_ACTION_DROP, FALSE, TRUE, &hFilterIf);
if(dwRet != NO_ERROR){
fprintf(stderr, "PfCreateInterface failed");
return -1;
}
printf("Start Filtering");
dwRet = PfBindInterfaceToIPAddress(hFilterIf, PF_IPV4, localAddr);
if(dwRet != NO_ERROR){
fprintf(stderr, "PfBindInterfaceToIPAddress failed");
return -1;
}
_getch();
PfUnBindInterface(hFilterIf);
PfDeleteInterface(hFilterIf);
return 0;
}
-----
最初に定義されているlocalAddr配列には自分マシンのIPアドレスを指定してください。私の環境ではIPアドレスが「192.168.4.2」でした。最初にPfCreateInterface関数を使って新しいフィルタインターフェースを生成します。PfCreateInterfaceは以下のように定義されています。
----- PfCreateInterface関数 DWORD PfCreateInterface( DWORD dwName, PFFORWARD_ACTION inAction, PFFORWARD_ACTION outAction, BOOL bUseLog, BOOL bMustBeUnique, INTERFACE_HANDLE* ppInterface ); -----
dwNameには、インターフェースの識別子を指定しますが、ここをNULL(0)にすると、適当な識別子を決定してくれますので、NULLで問題ありません。inActionとoutActionはそれぞれ受信するパケットと送信するパケットの扱いを決めます。
inAction PF_ACTION_DROP → 受信するパケットを破棄する
PF_ACTION_FORWARD → 受信するパケットを破棄しない
outAction PF_ACTION_DROP → 送信するパケットを破棄する
PF_ACTION_FORWARD → 送信するパケットを破棄しない
bUseLogはログを使用するかどうかでしょうか? 実は私も良く分かりません(^^;。bMustBeUniqueは、インターフェースを共有するか否かを決めます。TRUEならば共有しません。そして最後のppInterfaceにインターフェースハンドルを指定します。この関数が成功すると、以後ppInterfaceに渡したものがインターフェースハンドルとして使用されます。PfCreateInterfaceを使ってインターフェースを生成した場合は、PfDeleteInterfaceを呼び出してインターフェースを削除しなければなりません。ex1.cppでは、一番最後に呼び出しています。
次のPfBindInterfaceToIPAddress関数は以下のように定義されています。
----- PfBindInterfaceToIPAddress関数 DWORD PfBindInterfaceToIPAddress( INTERFACE_HANDLE pInterface, PFADDRESSTYPE pfatType, PBYTE IPAddress ); -----
pInterfaceはPfCreateInterface関数に渡したインターフェースハンドルです。pfatTypeはIPのバージョンを指定します。
PF_IPV4 → IP version 4(現在のデフォルト) PF_IPV6 → IP version 6(10年後のデフォルト?)
IPAddressは自分のマシンのIPアドレスを指定します。
PfBindInterfaceToIPAddressを使用したら、その後、PfUnBindInterfaceを使ってバインドを解除しなければなりません。ex1.cppでは、最後にそれを行っています。ではex1.cppをコンパイルしてください。そして、実行する前に、まず他のサーバへpingを打ってください。
----- コマンドプロンプト
C:\kenji>ping yahoo.co.jp
Pinging yahoo.co.jp [203.141.35.113] with 32 bytes of data:
Reply from 203.141.35.113: bytes=32 time=19ms TTL=242
Reply from 203.141.35.113: bytes=32 time=19ms TTL=242
Reply from 203.141.35.113: bytes=32 time=19ms TTL=242
Reply from 203.141.35.113: bytes=32 time=19ms TTL=242
Ping statistics for 203.141.35.113:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 19ms, Maximum = 19ms, Average = 19ms
C:\kenji>
-----
ちゃんと、pingが通っていることが確認できます。では、次にex1.cppをコンパイルしてできたex1.exeの実行中にpingを打ってください。ex1.exeは_getchのところでキー入力を求めて処理が止まるので、そのままの状態でpingを打ちます。
----- ex1.exe実行中 Start Filtering -----
----- コマンドプロンプト
C:\kenji>ping yahoo.co.jp
Pinging yahoo.co.jp [203.141.35.113] with 32 bytes of data:
Request timed out.
Request timed out.
Request timed out.
Request timed out.
Ping statistics for 203.141.35.113:
Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),
C:\kenji>
-----
今度はpingがyahoo.co.jpへ届きませんでした。pingの他にも、ブラウザでお好みのサイトへアクセスするなどなど、いろいろと試してみてください。おそらくすべて失敗するはずです。すべてのネットワークパケットを遮断しているため、ex1.exe実行中は、ネットワークを経由したどんなもアクセスも禁止されます。「yahoo.co.jp」をIPアドレスに変換するためにDNSサーバに問い合わせるかもしれませんが、その時点でパケット送信が失敗する可能性もあります。とにかくpingは成功しないことでしょう。
ex1.exeがすべてのパケットを遮断したのは、PfCreateInterface関数の引数inActionとoutActionにPF_ACTION_DROPを指定していたからです。つまり、逆にPF_ACTION_FORWARDを指定していたら、すべてのパケットを通過させていました。まずはそれを頭に置いておいてください。
----- PfAddFiltersToInterface関数
DWORD PfAddFiltersToInterface(
INTERFACE_HANDLE ih, // インターフェースハンドル
DWORD cInFilters, // 受信フィルタ数
PPF_FILTER_DESCRIPTOR pfiltIn, // 受信フィルタ
DWORD cOutFilters, // 送信フィルタ数
PPF_FILTER_DESCRIPTOR pfiltOut, // 送信フィルタ
PFILTER_HANDLE pfHandle // フィルタハンドルの配列を格納する領域
// 通常はNULLで構わない
);
-----
PfAddFiltersToInterface関数は、指定されたインタフェースに指定されたフィルタを加えます。この時にフィルタがどう働くのか、それはPfCreateInterface関数に指定したinActionとoutActionによって変わります。
inActionとoutActionにパケットの破棄(PF_ACTION_DROP)を指定していたらなら、PfAddFiltersToInterface関数を使って追加されたフィルタは、パケット通過の条件となります。逆に、パケットの通過(PF_ACTION_FORWARD)を指定していたらなら、PfAddFiltersToInterface関数を使って追加されたフィルタは、パケット破棄の条件となります。つまり、ex1.exeにPfAddFiltersToInterface関数を使って新たにフィルタを加えた場合、そのフィルタの条件にあったパケットは通過します。それを示したプログラムex2.cppを見てください。
----- ex2.cpp
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <fltdefs.h>
#define DIR_INPUT 1
#define DIR_OUTPUT 2
#pragma comment(lib, "iphlpapi.lib")
int parseAddress(char *addrStr,
BYTE *addr,
BYTE *mask)
{
int ip[4], mk[4];
int num = sscanf(addrStr, "%d.%d.%d.%d/%d.%d.%d.%d",
&ip[0], &ip[1], &ip[2], &ip[3],
&mk[0], &mk[1], &mk[2], &mk[3]);
if(num != 8)
return -1;
for(int i=0; i < 4; i++){
if(ip[i] < 0 || ip[i] > 255 || mk[i] < 0 || mk[i] > 255)
return -1;
addr[i] = ip[i], mask[i] = mk[i];
}
return 0;
}
int addFilter(INTERFACE_HANDLE hIf,
int flag,
DWORD dwProtocol,
char *localAddrStr,
char *remoteAddrStr)
{
BYTE localAddrBin[4], remoteAddrBin[4];
BYTE localMaskBin[4], remoteMaskBin[4];
parseAddress(localAddrStr, localAddrBin, localMaskBin);
parseAddress(remoteAddrStr, remoteAddrBin, remoteMaskBin);
PF_FILTER_DESCRIPTOR filter;
memset(&filter, 0, sizeof(filter));
filter.dwFilterFlags = FD_FLAGS_NOSYN;
filter.dwRule = 0;
filter.pfatType = PF_IPV4;
filter.dwProtocol = dwProtocol;
filter.fLateBound = 0;
filter.wSrcPort = ((dwProtocol == FILTER_PROTO_ICMP)
? FILTER_ICMP_TYPE_ANY : FILTER_TCPUDP_PORT_ANY);
filter.wDstPort = ((dwProtocol == FILTER_PROTO_ICMP)
? FILTER_ICMP_TYPE_ANY : FILTER_TCPUDP_PORT_ANY);
filter.wSrcPortHighRange = filter.wSrcPort;
filter.wDstPortHighRange = filter.wDstPort;
DWORD dwRet;
if(flag == DIR_INPUT){
filter.SrcAddr = remoteAddrBin, filter.SrcMask = remoteMaskBin;
filter.DstAddr = localAddrBin, filter.DstMask = localMaskBin;
dwRet = PfAddFiltersToInterface(hIf, 1, &filter, 0, NULL, NULL);
}else{
filter.SrcAddr = localAddrBin, filter.SrcMask = localMaskBin;
filter.DstAddr = remoteAddrBin, filter.DstMask = remoteMaskBin;
dwRet = PfAddFiltersToInterface(hIf, 0, NULL, 1, &filter, NULL);
}
if(dwRet != NO_ERROR){
printf("addFilter: PfAddFiltersToInterface failed");
return -1;
}
return 0;
}
int main(void)
{
BYTE localAddr[4] = { 192,168,4,2 };
INTERFACE_HANDLE hFilterIf;
DWORD dwRet = PfCreateInterface(
NULL, PF_ACTION_DROP, PF_ACTION_DROP, FALSE, TRUE, &hFilterIf);
if(dwRet != NO_ERROR){
fprintf(stderr, "PfCreateInterface failed");
return -1;
}
addFilter(hFilterIf, DIR_OUTPUT, FILTER_PROTO_ICMP,
"192.168.4.2/255.255.255.255", "210.198.12.224/255.255.255.255");
addFilter(hFilterIf, DIR_INPUT, FILTER_PROTO_ICMP,
"192.168.4.2/255.255.255.255", "210.198.12.224/255.255.255.255");
printf("Start Filtering");
dwRet = PfBindInterfaceToIPAddress(hFilterIf, PF_IPV4, localAddr);
if(dwRet != NO_ERROR){
fprintf(stderr, "PfBindInterfaceToIPAddress failed");
return -1;
}
_getch();
PfUnBindInterface(hFilterIf);
PfDeleteInterface(hFilterIf);
return 0;
}
-----
ex2.cppで追加しているフィルタは、ICMPで「192.168.4.2」から「210.198.12.224」へ送信されるパケットと「210.198.12.224」から「192.168.4.2」へ送信されるパケットの2つです。これらの条件に当てはまるパケットは破棄されずに、正常にネットワークを通過することになります。ちなみに「210.198.12.224」は「ruffnex.oc.to」のIPアドレスです。では、このプログラムを実行してください。そしてコマンドプロンプトから「ping 210.198.12.224」と入力してください。「ping ruffnex.oc.to」とすると、名前解決のためにDNSサーバに問い合わせる可能性があるので、必ずIPアドレスを指定してください。
----- コマンドプロンプト
C:\kenji>ping 210.198.12.224
Pinging 210.198.12.224 with 32 bytes of data:
Reply from 210.198.12.224: bytes=32 time=20ms TTL=242
Reply from 210.198.12.224: bytes=32 time=21ms TTL=242
Reply from 210.198.12.224: bytes=32 time=20ms TTL=242
Reply from 210.198.12.224: bytes=32 time=20ms TTL=242
Ping statistics for 210.198.12.224:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 20ms, Maximum = 21ms, Average = 20ms
C:\kenji>
-----
正常にパケットは通過します。しかし、その他のパケットはすべて破棄されます。デフォルトの設定がパケット破棄(PF_ACTION_DROP)であり、その状態でPfAddFiltersToInterfaceにてフィルタの設定をしたので、フィルタの条件と一致したパケットは通過することになるわけです。そして、逆に、デフォルトの設定をパケット通過(PF_ACTION_FORWARD)にしておいた場合、フィルタの条件と一致したパケットは破棄されることになります。
このパケットフィルタリングのテクニックを利用すると、例えば自分のコンピュータにインターネット利用の制限をかけることが可能になります。以下のプログラムをみてください。
----- ex3.cpp
#define WIN32_LEAN_AND_MEAN
#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <fltdefs.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#define DIR_INPUT 1
#define DIR_OUTPUT 2
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
int getIpAddress(char *list)
{
WSADATA WinsockData;
if(WSAStartup(MAKEWORD(2, 2), &WinsockData) != 0)
return -1;
SOCKET sock = WSASocket(AF_INET, SOCK_DGRAM, 0, 0, 0, 0);
if(sock == SOCKET_ERROR)
return -1;
unsigned long nBytesReturned;
INTERFACE_INFO InterfaceList[32];
if(WSAIoctl(sock, SIO_GET_INTERFACE_LIST, 0, 0, &InterfaceList,
sizeof(InterfaceList), &nBytesReturned, 0, 0) == SOCKET_ERROR)
return -1;
int nNumInterfaces = nBytesReturned / sizeof(INTERFACE_INFO);
if(0 < nNumInterfaces){
sockaddr_in *pAddress = (sockaddr_in *) & (InterfaceList[0].iiAddress);
lstrcpy(list, inet_ntoa(pAddress->sin_addr));
}
WSACleanup();
return 0;
}
int parseAddress(char *addrStr,
BYTE *addr,
BYTE *mask)
{
int ip[4], mk[4];
int num = sscanf(addrStr, "%d.%d.%d.%d/%d.%d.%d.%d",
&ip[0], &ip[1], &ip[2], &ip[3],
&mk[0], &mk[1], &mk[2], &mk[3]);
if(num != 8)
return -1;
for(int i=0; i < 4; i++){
if(ip[i] < 0 || ip[i] > 255 || mk[i] < 0 || mk[i] > 255)
return -1;
addr[i] = ip[i], mask[i] = mk[i];
}
return 0;
}
int addFilter(INTERFACE_HANDLE hIf,
int flag,
DWORD dwProtocol,
char *localAddrStr,
char *remoteAddrStr,
WORD srcPort,
WORD dstPort,
WORD srcHighPort,
WORD dstHighPort)
{
BYTE localAddrBin[4], remoteAddrBin[4];
BYTE localMaskBin[4], remoteMaskBin[4];
parseAddress(localAddrStr, localAddrBin, localMaskBin);
parseAddress(remoteAddrStr, remoteAddrBin, remoteMaskBin);
PF_FILTER_DESCRIPTOR filter;
memset(&filter, 0, sizeof(filter));
filter.dwFilterFlags = FD_FLAGS_NOSYN;
filter.dwRule = 0;
filter.pfatType = PF_IPV4;
filter.dwProtocol = dwProtocol;
filter.fLateBound = 0;
DWORD dwRet;
if(flag == DIR_INPUT){
filter.SrcAddr = remoteAddrBin, filter.SrcMask = remoteMaskBin;
filter.DstAddr = localAddrBin, filter.DstMask = localMaskBin;
filter.wSrcPort = dstPort, filter.wDstPort = srcPort;
filter.wSrcPortHighRange = dstHighPort;
filter.wDstPortHighRange = srcHighPort;
dwRet = PfAddFiltersToInterface(hIf, 1, &filter, 0, NULL, NULL);
}else{
filter.SrcAddr = localAddrBin, filter.SrcMask = localMaskBin;
filter.DstAddr = remoteAddrBin, filter.DstMask = remoteMaskBin;
filter.wSrcPort = srcPort, filter.wDstPort = dstPort;
filter.wSrcPortHighRange = srcHighPort;
filter.wDstPortHighRange = dstHighPort;
dwRet = PfAddFiltersToInterface(hIf, 0, NULL, 1, &filter, NULL);
}
if(dwRet != NO_ERROR){
printf("addFilter: PfAddFiltersToInterface failed");
return -1;
}
return 0;
}
int main(void)
{
char useip[32] = "192.168.4.2";
char iplist[32];
if(!getIpAddress(iplist))
lstrcpy(useip, iplist);
INTERFACE_HANDLE hFilterIf;
DWORD dwRet = PfCreateInterface(
NULL, PF_ACTION_FORWARD, PF_ACTION_FORWARD, FALSE, TRUE, &hFilterIf);
if(dwRet != NO_ERROR){
fprintf(stderr, "PfCreateInterface failed\r\n");
return -1;
}
lstrcat(useip, "/255.255.255.255");
addFilter(hFilterIf, DIR_OUTPUT, FILTER_PROTO_TCP,
useip, "0.0.0.0/0.0.0.0", FILTER_TCPUDP_PORT_ANY, 20,
FILTER_TCPUDP_PORT_ANY, 25);
addFilter(hFilterIf, DIR_INPUT, FILTER_PROTO_TCP,
useip, "0.0.0.0/0.0.0.0", FILTER_TCPUDP_PORT_ANY, 20,
FILTER_TCPUDP_PORT_ANY, 25);
addFilter(hFilterIf, DIR_OUTPUT, FILTER_PROTO_TCP,
useip, "0.0.0.0/0.0.0.0", FILTER_TCPUDP_PORT_ANY, 81,
FILTER_TCPUDP_PORT_ANY, 65535);
addFilter(hFilterIf, DIR_INPUT, FILTER_PROTO_TCP,
useip, "0.0.0.0/0.0.0.0", FILTER_TCPUDP_PORT_ANY, 81,
FILTER_TCPUDP_PORT_ANY, 65535);
int ip[4], mk[4];
int num = sscanf(useip, "%d.%d.%d.%d/%d.%d.%d.%d",
&ip[0], &ip[1], &ip[2], &ip[3],
&mk[0], &mk[1], &mk[2], &mk[3]);
if(num != 8)
return -1;
BYTE localAddr[4];
for(int i=0; i < 4; i++)
localAddr[i] = ip[i];
printf("Start Filtering");
dwRet = PfBindInterfaceToIPAddress(hFilterIf, PF_IPV4, (PBYTE)localAddr);
if(dwRet != NO_ERROR){
fprintf(stderr, "PfBindInterfaceToIPAddress failed");
_getch();
return -1;
}
_getch();
PfUnBindInterface(hFilterIf);
PfDeleteInterface(hFilterIf);
return 0;
}
-----
このプログラムはex2.cppに多少の改良を加えたものです。このプログラムは、コンピュータが行う、ポート「20番から25番」と「81番から65535番」へのアクセスをすべて禁止します。つまり、FTPやSMTPやPOP3といったプロトコルが使用できなくなります。よく高校や大学などの教育機関に置いてあるパソコンには、学生が変なことをしないように制限がかけてある場合がありますが、このようなツールを作成することによって容易にそれが実現できます。また、フィルタを強化すれば、簡易的なFirewallといったものも作成することができると思います。
さて、いかがだったでしょうか。アプリケーションレベルでもパケットフィルタリングが可能なんて少しビックリですが、パケットの内容を解析できないので、結局、本格的にパケットフィルタリングを行うためには、ドライバを組む必要がありそうです。実はこの内容は、ネット上でドライバでのパケットフィルタリングを調べている最中に分かったものでして、本当はドライバでのパケットフィルタリングを書こうとしていたのですが、どうせなので、こっちも書くことにしました。来月は「パケットフィルタリング 〜デバイスドライバ篇〜」と題してお送りする予定ですが、予定は未定なので期待しないで待っててください(^^;。さて、最後になりましたが、ここまで読んでくれて本当にありがとうございます。
では、また会う日まで...