430 likes | 652 Views
ECN KG 発表. カーネルをハックしてみよう. ネットワークスタックをいぢる. FreeBSD のカーネルコードに printf をいれて処理を追ってみる 今日のターゲットは ICMP です!. まずは QEMU を手に入れよう. 既に QEMU をダウンロードしてありますよね? http://www.ht.sfc.keio.ac.jp/~sada/etc/qemu-0.7.zip 下記媒体でも配布します。 CD-R SD カード コンパクトフラッシュ QEMU を手に入れたら、ローカルの好きな場所においてください. FreeBSD@QEMU の起動.
E N D
ECN KG 発表 カーネルをハックしてみよう
ネットワークスタックをいぢる • FreeBSDのカーネルコードにprintfをいれて処理を追ってみる • 今日のターゲットはICMPです!
まずはQEMUを手に入れよう • 既にQEMUをダウンロードしてありますよね? • http://www.ht.sfc.keio.ac.jp/~sada/etc/qemu-0.7.zip • 下記媒体でも配布します。 • CD-R • SDカード • コンパクトフラッシュ • QEMUを手に入れたら、ローカルの好きな場所においてください
FreeBSD@QEMU の起動 • qemu-freebsd.batをダブルクリック! • ユーザはroot、パスワードは・・・ • 下記のようにタイプするとIPアドレスがとれます • dhclient ed0 • 外部とはport 22しか繋がりません。 • qemu-freebsd.batを編集すると、好きなportで通信できます QEMU 仮想ルータ10.0.2.2 FreeBSD10.0.2.16
カーネルのソースコード構成 • どんなファイルがあるか、ざっくりチェックしてみてください。 i386 X86特有 kern 一般的なカーネル net 一般的なネットワーク netinet TCP/IP / usr src sys sys カーネルヘッダ ufs Unixファイルシステム vm カーネルヘッダ
ICMP Echo Request, Reply application application ip_output() rip_input() icmp_input() ether_output() ip_input() arpresolve() ether_input()
ICMP ECHO Requestの送信 • ICMP Echo Request を送信する • ping プログラムで実際にパケットの流れをみてみる • ip_output() • IPヘッダの初期化 • 経路選択、アドレス選択 • フラグメンテーション • ether_output() • フレーム構築 • インタフェースへのキューイング • arpresolve() • arp 解決
実際にコードを見ながらprintfを入れよう • printfの基本的な使い方は大丈夫ですよね??だめ? • #define DBG(fmt, arg...) printf("%s: "fmt "\n", __FUNCTION__, ## arg) • ↑みたく書くと、後で色々楽です。 • と、いうわけで、取りあえず送信からいきましょ • ip_output関数はsys/netinet/ip_output.cにあります
ip_output()(IPヘッダができるまで) ICMP ip_output(){ …… ip = mtod(m, struct ip *); if ((flags & (IP_FORWARDING|IP_RAWOUTPUT)) == 0) { ip->ip_v = IPVERSION; ip->ip_hl = hlen >> 2; ip->ip_id = ip_newid(); ipstat.ips_localout++; } else { hlen = ip->ip_hl << 2; } (RAW output なので、 ここでは、初期化されない) IP ICMP
ip_output()(経路選択、アドレス選択) ip_output(){ …… 経路キャッシュや経路テーブルをみて宛て先決定 dst = (struct sockaddr_in *)&ro->ro_dst; again: …… 送信元の決定 if (ip->ip_src.s_addr == INADDR_ANY) { /* Interface may have no addresses. */ if (ia != NULL) { ip->ip_src = IA_SIN(ia)->sin_addr; } }
ip_output()(経路選択、アドレス選択) ip_output(){ …… フラグメントの必要がなければ送信 error = (*ifp->if_output)(ifp, m, (struct sockaddr *)dst, ro->ro_rt); goto done; } } • 送信前にIPヘッダのメンバを表示してみよう
IP header 全体の長さ ip_len バージョン ip_v ヘッダ長 ip_hl tos ip_tos 識別子 ip_id フラグ、フラグメントオフセット ip_off プロトコル ip_p ヘッダチェックサム ip_sum TTL ip_ttl 発信元IPアドレス ip_src 宛て先IPアドレス ip_dst データ
32bitアドレスの表示方法 #define IP_ADDR_FORMAT(addr) \ ((addr) & 0xff), \ (((addr) >> 8) & 0xff), \ (((addr) >> 16) & 0xff), \ (((addr) >> 24) & 0xff) DBG(“ip_src %d.%d.%d.%d ip_dst %d.%d.%d.%d", IP_ADDR_FORMAT(ip->ip_src.s_addr), IP_ADDR_FORMAT(ip->ip_dst.s_addr));
ここでカーネルをコンパイルしてみよう • コンパイル方法 • cd /usr/src/sys/i386/compile/ECN • make kernel • make kernel-install • ぜーーったい、make clean をしないでください! • 一からmakeしようとすると、それだけでこの授業時間が終わります。。。 • 再起動 • shutdown -r now
Pingしてみよう • タイプしてみてください • dhclient ed0 • ping 10.0.2.2 • dmesg すると、出力はどうなりましたか?
spl…関数 • ip_outputでsplnet(), splx()を見ませんでしたか? • FreeBSDでは、割り込み処理に優先度が割り当てています • ネットワーク周りだとこの2つが使われます。splnet(), splimp() • splimpはsplnetより優先度が高く、ネットワークデバイスからの割り込みを禁止する。 • インタフェースキューを操作する際は、splimpが使用される
spl…関数一覧 低 高 別
ether_output() • sys/net/if_ethersubr.c内の関数 • 処理は • フレーム構築 • インタフェースキューイング
ether_output(ARP解決) int ether_output(){ …… case AF_INET: error = arpresolve(ifp, rt0, m, dst, edst); if (error) return (error == EWOULDBLOCK ? 0 : error); type = htons(ETHERTYPE_IP); break; もし、ARPがまだ解決されてなければ、返る (arpresolvがARP解決後、再びether_outputを呼び出す)。 解決されていれば、フレームの構築を行なう。
ether_output(フレーム構築) int ether_output(){ …… M_PREPEND(m, ETHER_HDR_LEN, M_DONTWAIT); if (m == NULL) senderr(ENOBUFS); eh = mtod(m, struct ether_header *); (void)memcpy(&eh->ether_type, &type, sizeof(eh->ether_type)); (void)memcpy(eh->ether_dhost, edst, sizeof (edst)); M_PREPENDでIPヘッダの前に領域を確保後、 ETHERヘッダの中身を埋めていく IP ICMP eth IP ICMP
ether_output_frame(出力へのキューイング) int ether_output_frame(){ …… IFQ_HANDOFF(ifp, m, error); return (error); } 送信キューにデータをキューイングする。 というわけで、イーサヘッダもprintfしよう。
Ether Header 発信元アドレス 送信元アドレス タイプ struct ether_header{ u_char ether_dhost[6]; u_char ether_shost[6]; u_short ether_type; } IPv4の場合typeは0x0800となる
MACアドレスの表示方法は? 以下のような感じで表示させてみよう マクロにしたり、define で書いてもいいかも。 void mac_print(u_char *macaddr) { int n; printf("mac = ["); for (n=0; n<6; n++) printf("%02x ",(u_int)macaddr[n]); printf("]\n"); }
arpresolve • ether_output()でARP解決がまだされていないときに、ARP解決がされます • ARPは大丈夫ですよね。。。。? • 主に2つの関数が使われる • arpresolve • ARPエントリを検索 • なければ、arprequestを呼び出す • arprequest • ARPパケットを作成 • インタフェースへ出力する
arpresolve(ARPエントリの検索) • sys/netinet/if_ether.cにあります int arpresolve(){ …… rt = arplookup(SIN(dst)->sin_addr.s_addr, 1, 0); ……. if ((rt->rt_expire == 0 || rt->rt_expire > time_second) && sdl->sdl_family == AF_LINK && sdl->sdl_alen != 0) { • arplookupでARPエントリを検索後、エントリが妥当かどうか調べる • もし、妥当であればそのARP解決がされている
arpresolve(ARP要求) int arpresolve(){ …… if (rt->rt_expire) { rt->rt_flags &= ~RTF_REJECT; if (la->la_asked == 0 || rt->rt_expire != time_second) { rt->rt_expire = time_second; if (la->la_asked++ < arp_maxtries) { struct in_addr sin = SIN(rt->rt_ifa->ifa_addr)->sin_addr; RT_UNLOCK(rt); arprequest(ifp, &sin, &SIN(dst)->sin_addr, IF_LLADDR(ifp)); • ARPタイマ、ARP回数のチェック設定後、ARP要求送信を行うarprequest()を呼び出す
arprequest(ARP要求パケット構築、送信) static void arprequest(){ ……… if ((m = m_gethdr(M_DONTWAIT, MT_DATA)) == NULL) return; • ARPパケット用mbufを確保 …… ah->ar_pro = htons(ETHERTYPE_IP); ah->ar_hln = ifp->if_addrlen; ah->ar_pln = sizeof(struct in_addr); ……… • パケットの中身を埋める (*ifp->if_output)(ifp, m, &sa, (struct rtentry *)0); • インタフェースへ出力
ARPヘッダ(メンバを色々出力させてみよう) ■if_ether.h で定義されています struct arphdr { u_short ar_hrd; HWアドレスのフォーマット u_char ar_hln; ハードウェアアドレスの長さ u_char ar_pln;プロトコルアドレスの長さ u_short ar_op; ARPパケットの種類 }; struct ether_arp { struct arphdr ea_hdr; ARPの固定サイズヘッダ u_char arp_sha[ETHER_ADDR_LEN]; 送信元MACアドレス u_char arp_spa[4]; 送信元のプロトコルアドレス u_char arp_tha[ETHER_ADDR_LEN]; ターゲットのMACアドレス u_char arp_tpa[4]; ターゲットのプロトコルアドレス }; ether_arp arphdr
ここまで分かると色々できそう? • IPアドレスを詐称 • MACアドレスを詐称 • ARPフラッディング発生 みたいなカーネルが簡単にできそうだね!! でも、よいこのみんなはやっちゃだめだよ!!
ここでまたカーネルをコンパイルしてみよう • コンパイル方法 • cd /usr/src/sys/i386/compile/ECN • make kernel • make kernel-install • ぜーーったい、make clean をしないでください! • 一からmakeしようとすると、それだけでこの授業時間が終わります。。。 • 再起動 • shutdown -r now
カーネルの出力は? • dmesg で確認してみよう
ICMP ECHO Replyの受信 • ICMP Echo Reply を受信する • ping プログラムで実際にパケットの流れをみてみる • ether_input() • etherヘッダの妥当性チェック • 受信キューに入れる • ip_input() • IPパケットの妥当性チェック • 再組み立てやオプション、転送処理 • 振り分け • icmp_input() • メッセージの妥当性チェック • ICMPメッセージに応じた処理
ether_input(フレームのチェック) • sys/net/if_ethersubr.c 内にあります static void ether_input(){ ……… eh = mtod(m, struct ether_header *); etype = ntohs(eh->ether_type); ……… ether_demux(ifp, m); } • フレームの妥当性をチェックする。 • ether_demuxを呼び出しデマルチプレクス。 • ここでも、etherヘッダのメンバを色々表示してみよう
ether_demux(デマルチプレクス) voidether_demux(){ …… m_adj(m, ETHER_HDR_LEN);etherヘッダ削除 switch (ether_type) { case ETHERTYPE_IP: if (ip_fastforward(m)) return; isr = NETISR_IP; break; ……… netisr_dispatch(isr, m); return; • ether_typeにより上位層への割り込みのスケジューリングがされ、適切なキューへキューイング
ip_input(IPパケットの妥当性チェック) • schednetisrにより呼び出される • sys/netinet/ip_input.cにあります。 voidip_input(){ ………… ip = mtod(m, struct ip *); if (ip->ip_v != IPVERSION) { ipstat.ips_badvers++; goto bad; } • 妥当性のチェックを行う • 受信したIPヘッダの中身を表示させよう
ip_input(オプション処理&転送) • voidip_input(){ ………… if (hlen > sizeof (struct ip) && ip_dooptions(m, 0)) return; • IPヘッダのサイズが20より大きい場合、オプション処理を行う LIST_FOREACH(ia, INADDR_HASH(ip->ip_dst.s_addr), ia_hash) { if (IA_SIN(ia)->sin_addr.s_addr == ip->ip_dst.s_addr && (!checkif || ia->ia_ifp == m->m_pkthdr.rcvif)) • アドレスリストをチェックし、自分宛か、転送するべきか判断する。
ip_input(リアセンブリ&振り分け) • voidip_input(){ ………… if (ip->ip_off & (IP_MF | IP_OFFMASK)) { m = ip_reass(m); • オフセットやフラグがあったならば、フラグメントパケット。リアセンブリ処理を行う (*inetsw[ip_protox[ip->ip_p]].pr_input)(m, hlen); return; • プロトコルスイッチにより、適切なトランスポート層へデータを渡す
icmp_input(ICMPの妥当性チェック) • sys/netinet/ip_icmp.c内にあります voidicmp_input(){ ip = mtod(m, struct ip *); m->m_len -= hlen; m->m_data += hlen; icp = mtod(m, struct icmp *); if (in_cksum(m, icmplen)) { • mbufのデータポインタを調整後、icmpヘッダへのポインタを取り出し、妥当性チェック • 受信したICMPヘッダのメンバを色々表示してみよう IP ICMP
icmp_input(メッセージの処理&raw input) voidicmp_input(){ ……… code = icp->icmp_code; switch (icp->icmp_type) { ………. case ICMP_ECHOREPLY: default: break; } raw: rip_input(m, off); return;
ここでまたまたカーネルをコンパイルしてみようここでまたまたカーネルをコンパイルしてみよう • コンパイル方法 • cd /usr/src/sys/i386/compile/ECN • make kernel • make kernel-install • ぜーーったい、make clean をしないでください! • 一からmakeしようとすると、それだけでこの授業時間が終わります。。。 • 再起動 • shutdown -r now
pingの送受信の流れは分かりましたか? • 余力があれば以下の処理を見つけて表示させてみよう • ICMP ECHO Request を受信後、replyを返す所 • arp request受信後、replyを返す所 • フラグメントを発生させて、フラグメント&組み立て処理 • さらに余力があれば、ネットワークスタックとは関係ないですが、、、、 • カーネル起動時のデバイス初期化メッセージを色々変えてみたら楽しいかも (e.g.そんな餌におれは釣られないクマ――等の巨大AAが出てくるなど。個人的に見てみたい人はどうぞ)