ACTIVITY 取組み

取組みトップ

セキュリティプログラミング:カーネルモジュールでファイアウォールの実装

通信のパケットを低レベルで制御できないかと色々と調べていましたら、 Linuxではカーネルモジュールでnetfilterを利用して制御できることが分かり、 最近はLinuxのカーネルモジュールのプログラミングにはまっている白石です。

今回は、簡単なファイアウォールのプログラミングを紹介したいと思います。 今ではほとんどのOSでホスト型のファイアウォールが標準でインストールされ、 ホストへのアクセス制御を簡単に行えるようになっています。 そのため、プログラミングする必要がある状況はほとんどないかと思いますが、 パケットの細かな制御をできるので知っていて損はないかと思います。

※注意
カーネルモジュールの基本的なプログラミングの方法については割愛します。

初めに、ホストが受信したIPv4のICMPパケットをすべてDROPするプログラムのソースコードを見てみましょう。
ソースコードの赤い文字の箇所でパケットを制御します。
ソースコードの緑の文字の箇所で制御するパケットの種類を設定します。

 

/* ソースコード開始 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/version.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/inet.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

MODULE_AUTHOR("Masashi Shiraishi");
MODULE_DESCRIPTION("Filter");
MODULE_LICENSE("GPL");

static struct nf_hook_ops nfho4;

unsigned int hook_ipv4 (unsigned int hooknum,
                        struct sk_buff *skb,
                        const struct net_device *in,
                        const struct net_device *out,
                        int (*okfn)(struct sk_buff *)) {
  struct iphdr      *iph;

  iph = (struct iphdr *)skb_network_header(skb);
  skb->transport_header = skb->network_header + (iph->ihl * 4);

  switch (iph->protocol) {
    case 6:
      return NF_ACCEPT;
      break;
    case 17:
      return NF_ACCEPT;
      break;
    case 1:
      return NF_DROP;
      break;
    default:
      return NF_ACCEPT;
  }
}

int init_module() {
  nfho4.hook     = hook_ipv4;
  nfho4.hooknum  = NF_IP_PRE_ROUTING;
  nfho4.pf       = NFPROTO_IPV4;
  nfho4.priority = NF_IP_PRI_FIRST;

  nf_register_hook(&nfho4);

  return 0;
}

void cleanup_module() {
  nf_unregister_hook(&nfho4);
}
/* ソースコード終了 */

netfilterを利用する場合、パケットを制御できる場所は大きく分けて5箇所となります。
  1. 受信した直後のパケット
  2. 受信したパケットのルーティング処理が行われ、Forward処理により送信される前のパケット
  3. 受信したパケットのルーティング処理が行われ、ホストの別アプリに渡す前のパケット
  4. ルーティング処理前の送信パケット
  5. ルーティング処理後の送信直前のパケット
今回は受信したICMPパケットの処理を行うため1を選択しますが、これは上記ソースコードの NF_IP_PRE_ROUTING となります。 nf_hook_ops 構造体にフックするパケットの情報を格納し、nf_register_hook(&nfho4); 関数を呼び出すことでフックを開始します。 フックしたパケットは hook_ipv4() 関数に渡され、処理されます。

フックしたパケットは hook_ipv4() 関数の引数の struct sk_buff *skb に格納されます。 この構造体からIPパケットの情報を取り出す関数が skb_network_header() となります。 IPパケットを取得できたら、あとはプロトコル情報を iph->protocol で取得し、ICMPパケットの場合だけ破棄します。 パケットは NF_ACCEPT を返すことで受信を許可し、NF_DROP を返すことで破棄します。

プログラムが完成したらコンパイルしてカーネルモジュールを登録します。 カーネルモジュールの登録は insmod を実行し、削除は rmmod を実行します。

以下は、カーネルモジュールの登録及び削除を行ったホスト(192.168.1.103)に対して他のホストからPing(ICMPパケット)を送信した結果です。 カーネルモジュールを登録及び削除することによって、icmp_seq=4 ~ icmp_seq=10のパケットが破棄されていることがわかります。
  icmp_seq=1 ~ icmp_sec=3 がカーネルモジュールの登録前です。
  icmp_seq=11 ~ icmp_sec=13 が登録したカーネルモジュールの削除後です。


[root@VCENTOS5 ~]# ping 192.168.1.103
PING 192.168.1.103 (192.168.1.103) 56(84) bytes of data.
64 bytes from 192.168.1.103: icmp_seq=1 ttl=64 time=0.917 ms
64 bytes from 192.168.1.103: icmp_seq=2 ttl=64 time=0.274 ms
64 bytes from 192.168.1.103: icmp_seq=3 ttl=64 time=0.277 ms
64 bytes from 192.168.1.103: icmp_seq=11 ttl=64 time=0.226 ms
64 bytes from 192.168.1.103: icmp_seq=12 ttl=64 time=0.221 ms
64 bytes from 192.168.1.103: icmp_seq=13 ttl=64 time=0.247 ms

--- 192.168.1.103 ping statistics ---
13 packets transmitted, 6 received, 53% packet loss, time 12001ms
rtt min/avg/max/mdev = 0.221/0.360/0.917/0.250 ms
[root@VCENTOS5 ~]#

 

では次にTCPのパケットのアクセス制御を行ってみます。 ホストが受信した80/tcp宛のパケットをすべてDROPするプログラムのソースコードを見てみましょう。

 

/* ソースコード開始 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/version.h>
#include <linux/skbuff.h>
#include <linux/ip.h>
#include <linux/inet.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/icmp.h>
#include <linux/netdevice.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

MODULE_AUTHOR("Masashi Shiraishi");
MODULE_DESCRIPTION("Filter");
MODULE_LICENSE("GPL");

static struct nf_hook_ops nfho4;

unsigned int hook_ipv4 (unsigned int hooknum,
                        struct sk_buff *skb,
                        const struct net_device *in,
                        const struct net_device *out,
                        int (*okfn)(struct sk_buff *)) {
  struct iphdr      *iph;
  struct tcphdr     *tcph;
  struct udphdr     *udph;
  struct icmphdr    *icmph;

  iph = (struct iphdr  *)skb_network_header(skb);
  skb->transport_header = skb->network_header + (iph->ihl * 4);

  switch (iph->protocol) {
    case 6:
      tcph = (struct tcphdr *)skb_transport_header(skb);
      if (tcph->dest == htons(80)) {
        return NF_DROP;
      }
      return NF_ACCEPT;
      break;
    case 17:
      udph  = (struct udphdr *)skb_transport_header(skb);
      return NF_ACCEPT;
      break;
    case 1:
      icmph = (struct icmphdr *)skb_transport_header(skb);
      return NF_DROP;
      break;
    default:
      return NF_ACCEPT;
  }
}

int init_module() {
  nfho4.hook     = hook_ipv4;
  nfho4.hooknum  = NF_IP_PRE_ROUTING;
  nfho4.pf       = NFPROTO_IPV4;
  nfho4.priority = NF_IP_PRI_FIRST;

  nf_register_hook(&nfho4);

  return 0;
}

void cleanup_module() {
  nf_unregister_hook(&nfho4);
}
/* ソースコード終了 */

 

ICMPのソースコードが理解できていれば、今回は応用ですので簡単に理解できるかと思います。 受信したパケットのトランスポート層のヘッダの位置を skb->transport_header = ~ で設定します。 その後、TCPのヘッダ情報を tcph = (struct tcphdr *)skb_transport_header(skb); で取得します。 TCPのヘッダ情報を取得できたら、後は80番ポートへのアクセスのみ破棄してあげればプログラムは完成です。

以下は、80/tcpでWebサービスを稼働させ、カーネルモジュールを登録する前と登録した後の状況です。 カーネルモジュールを登録することによって80/tcpへのアクセスを不可能とすることができました。

 

■登録前
[root@VCENTOS5 ~]# telnet 192.168.1.103 80
Trying 192.168.1.103...
Connected to 192.168.1.103 (192.168.1.103).
Escape character is '^]'.
GET / HTTP/1.0

HTTP/1.1 200 OK
Date: Fri, 01 Jul 2011 15:16:24 GMT
Server: Apache/2.2.15 (Unix) DAV/2
Last-Modified: Sat, 20 Nov 2004 20:16:24 GMT
ETag: "4021e-2c-3e9564c23b600"
Accept-Ranges: bytes
Content-Length: 44
Connection: close
Content-Type: text/html

<html><body><h1>It works!</h1></body></html>Connection closed by foreign host.
[root@VCENTOS5 ~]#
■登録後
[root@VCENTOS5 ~]# telnet 192.168.1.103 80
Trying 192.168.1.103...

[root@VCENTOS5 ~]#

 

今回は簡単なアクセス制御を行うプログラムを作成しましたが、受信したパケットはLayer-2(MAC)レベルで取得できますのでLayer-2からLayer-7での様々なアクセス制御をすることが可能です。 また、受信したパケットを一部改編することや転送することも可能なため、iptables等の標準のファイアウォール機能では出来ない制御もできるようになります。
(例:IPv4アドレスをIPv6アドレスに、IPv6アドレスをIPv4アドレスに変換して送受信することが可能)

 

タイガーチームメンバー 白石 雅