內網穿透工具的原理與開發實戰

前言

在我國,由於網民眾多,運營商無法保證為每一個寬頻用戶提供全球唯一的公網IPv4地址。因此很多用戶會發現通過路由器端查看到的WAN端IP與百度「IP」關鍵詞所得到的IP不一致,並且前者的IP為一個私有IP。

而還有一些情況下,公網IP比較昂貴,企業雖然本身也持有少量的獨立的公網IP,但是由於成本限制無法為企業內每一台主機都提供一個公網IP,或者內網並不是所有服務都需要暴露到公網中進行訪問,那麼企業有可能就會使用NAT技術將大量的內網IP通過一定規則映射到公網IP上。而最常見的其中一種技術就是NAPT,也叫「網路埠地址轉換」。因為一般一個服務都是通過一個埠來提供,因此通過這種方式可以將特定的服務通過特定的規則開放到少量的公網IP上。

問題

但是有的時候我們個人寬頻用戶也想將自己的服務發布到公網IP上。比如說我們做了一個很漂亮的網站想發布到互聯網上供大家參觀,在沒有公網IP的情況下該怎麼實現呢?

還有的時候我們在對企業做滲透測試的時候,發現企業某台公網伺服器只對公網開放了常見的80埠,而我們提權時需要用到的3389等埠沒有對公網開放,這個時候又該怎麼辦呢?

解決這些問題的方式就是內網穿透了。

NAPT原理

簡單來說,在NAT網關上會有一張映射表,表上記錄了內網向公網哪個IP和埠發起了請求,然後如果內網有主機向公網設備發起了請求,內網主機的請求數據包傳輸到了NAT網關上,那麼NAT網關會修改該數據包的源IP地址和源埠為NAT網關自身的IP地址和任意一個不衝突的自身未使用的埠,並且把這個修改記錄到那張映射表上。最後把修改之後的數據包發送到請求的目標主機,等目標主機發回了響應包之後,再根據響應包裡面的目的IP地址和目的埠去映射表裡面找到該轉發給哪個內網主機。這樣就實現了內網主機在沒有公網IP的情況下,通過NAPT技術藉助路由器唯一的一個公網IP來訪問公網設備。

具體原理我到網上找了一張圖片

從這裡我們可以看到,NAPT只解決了內網主機在沒有公網IP的情況下如何訪問公網主機的問題,但是並不能解決公網主機如何主動向內網主機發起請求的問題。

私有地址

在較早以前的 RFC 1918 文檔中對私有地址有相關的說明。

網際網路域名分配組織IANA組織(Internet Assigned Numbers Authority)保留了以下三個IP地址塊用於私有網路。

10.0.0.0 - 10.255.255.255 (10/8比特前綴)

172.16.0.0 - 172.31.255.255 (172.16/12比特前綴)

192.168.0.0 - 192.168.255.255 (192.168/16比特前綴)

我們可以看到其中有1個A類地址塊,32個B類地址塊和256個C類地址塊。主流的家用路由器使用C類私有地址作為路由器LAN端的IP地址較多,所以我們可以看到路由器設置頁面的IP一般都為192.168開頭。

原因

先說說家庭寬頻的情況吧。家庭寬頻如果沒有公網IP,那麼意味著你在本機上監聽的任何埠,都只能在本機網卡所在的網路中訪問,這個網路一般是路由器LAN端所在的網路。如果沒有做特定的映射規則,那麼路由器WAN端所連接到的網路將無法正常訪問該主機提供的服務。

如果這種情況下想要讓WAN端(如果運營商為你分配了公網IP,那麼WAN端所連接到的網路通常就是公網),那麼需要在路由器上做埠映射。比如說路由器的LAN IP為192.168.1.1,WAN IP為23.23.23.23,我想讓內網192.168.1.2主機的80埠提供的HTTP伺服器直接能夠在公網中通過23.23.23.23訪問,那麼就要將192.168.1.2:80映射到23.23.23.23:80上。

但是通常情況下,運營商是不會給普通用戶公網IP的。那麼用這種方法映射,在公網仍然是無法訪問的,因為你的路由器WAN端連接的又是運營商更上一級的路由器LAN端,嚴重一點,甚至是層層連接最後才到公網,這種行為稱作流量穿透。國內某電,某動的寬頻就有大量這種行為。通過流量穿透的方式來提供的寬頻服務,看似便宜,實則影響很大,由於大家公用一個IP,可能會導致很多網站的反SPAM策略傷及無辜,或者內部為了節省帶寬,使用緩存,導致一些不該緩存的敏感安全頁面被緩存起來,甚至導致部分網站緩存失效完全打不開。

有的人發現,即使自己有公網IP,但是仍然無法通過常規方法架設伺服器,這是怎麼回事呢?這是運營商為了防止個人隨意開設各種非法服務,也防止黑客通過掃描器進行抓雞和批量掃描,將一些常用埠進行了封禁,比如說我們這的中國電信就將80,8080等埠封禁了。這樣封禁,雖然一定程度上保證了我們的網路安全,比如說前段時間的勒索病毒正因為國內大部分用戶沒有獨立的公網IP,並且操作系統最容易爆發漏洞的一些,135,139等埠被運營商封禁了,使得國內個人家庭電腦中招的概率小了很多;但是導致即使有公網IP,也無法使用常用埠向外網提供服務,只能更換到其他埠。這樣有什麼不好呢?比較典型的問題就是WEB網站默認使用80埠,那麼在輸入網址的時候可以不用帶上埠號,顯得比較美觀。

解決方案

如果遇到了上述情況,我們該如何解決呢?

如果我們沒有一台公網伺服器,我們可以使用國內大名鼎鼎的「花生殼」,「nat123」等服務來實現,但是他們背後的原理是什麼呢?

我們如果在自己擁有一台具有公網IP伺服器的情況下,我們可以藉助這台公網IP伺服器提供服務。而具體又該怎麼操作呢?

解決方案的實現

先假設我們自己有一台公網伺服器,他的IP為45.45.45.45。我們又有一台內網伺服器IP為23.23.23.23.我們現在想把23.23.23.23:80,即內網伺服器上的HTTP服務開放到45.45.45.45上。

最簡單粗暴的方式就是,我們可以直接將整個內網伺服器環境在公網伺服器上重新搭建一遍。

但是這樣做很麻煩,我們有的時候並不想這樣做,我們只是想簡單的藉助公網伺服器的網路來發布一個內網服務。

前面我們通過NAPT原理得知:NAPT實現了內網主機在沒有公網IP的情況下訪問公網主機。那麼我們可以這樣做:假設公網IP為23.23.23.23,內網IP為192.168.1.2。公網主機先監聽80埠,監聽這個埠是用於向外部提供一個HTTP服務,80是WEB伺服器的默認埠。同時其他任意一個埠(這裡我們假設為7777),監聽這個埠是用於讓內網伺服器主動連接進來打通一個隧道。接著內網再主動向公網主機的7777發起一個請求,這樣內網就成功與公網主機建立了一個連接通道。然後當有任何客戶端主動連接公網的80埠時,公網接收到連接請求之後馬上把這連接請求通過先前建立好的隧道轉發到內網主機,內網主機接收到來自隧道的數據包後再主動連接內網主機自身的80埠,連接成功之後將數據包原封不動地轉發數據包給80埠,待HTTP伺服器程序處理完這個數據包,生成了響應報文之後再原路轉發回去,最終到達公網的80埠,然後返回給最開始請求公網伺服器80埠的客戶端。

看起來是不是比較繞呢?事實上大名鼎鼎的花生殼內網版以及nat123等內網穿透工具的原理基本就是如此,但是並不完全是這樣。因為一個運輸層埠只能同時提供一種服務,但是我們會發現花生殼這種內網穿透服務是藉助一個公網IP同時給很多用戶提供了服務,這是因為花生殼在流量轉發這一層上並不是像我之前所說的原封不動的將報文進行轉發,而是在轉發之前加了一些控制協議的內容,用於指明該轉發到哪個花生殼客戶端所在的內網主機上。前者這種原封不動的轉發方式通常叫做透明傳輸或者透明代理。

穿透防火牆

為了安全起見,通常會在網路中加入防火牆,防火牆有入站規則和出站規則。如果不是非常嚴格的安全管控,通常是不會設置出站規則的,但是入站規則一般都會設置的,比如說外部可以通過80埠傳入內網的WEB伺服器訪問網頁,但是不能通過3389埠登陸內網的遠程桌面。

而在內網滲透的過程中碰到這種情況,我們也可以藉助上面內網傳統的方式實現穿透防火牆的入站規則。因為防火牆通常只攔截了入站,沒有攔截出站,那麼我們可以讓內網伺服器主動出站(主動連接到黑客的伺服器),與黑客自己的伺服器打通隧道,最終繞過防火牆連上3389遠程桌面。

還有一種情況就是我們已經拿下了內網其中一台並沒有做任何防火牆規則的白名單伺服器,但是我們想連上內網另一台做了入站規則的目標伺服器,那麼我們可以讓這台白名單伺服器作為一個跳板,讓他先監聽自身任意一個埠,然後在有任何用戶連上這個埠之後,白名單伺服器就主動連上內網的目標伺服器,然後藉助這台白名單伺服器打通黑客和目標伺服器的連接隧道。

而在黑客工具中大名鼎鼎的lcx原理也就是如此,前者的實現是lcx的listen和slave命令,後者的實現是lcx的tran命令。

代碼實現

知道了原理之後,具體該怎麼實現呢?

我這裡選擇了使用Go語言編程實現了這樣一個內網穿透工具。

Golang本身提供了非常多的網路庫,並且Golang本身內置的Goroutine能夠很方便的處理網路編程中的非同步IO,而且最重要的是,Golang開發的程序是可以跨平台運行的,意味著寫了一份代碼,我們可以在任何一個操作系統上編譯並使用。

github.com/cw1997/NATBy

初始運行時根據情況輸出歡迎信息已經語法提示(這裡要重點注意printWelcome函數末尾調用了time.Sleep阻塞一秒,這是因為fmt包輸出是非線程安全的,而log包下的輸出都是線程安全的,因此為了防止後面執行流中打的日誌會穿插到提示信息中而使用該函數休息一秒鐘)

首先通過判斷傳入參數決定當前使用何種轉發策略

然後再判斷傳入參數是否正確,通過正則表達式等方式驗證IP的合法性以及埠範圍

通過port2port函數實現了兩個埠同時監聽雙向並且轉發數據。

在port2host操作中實現了跳板中轉。

在host2host中實現了主動連接打通隧道的功能。

看代碼便可以知道,在Golang中進行socket操作的net包要比C語言中的socket.h操作簡單很多。

轉發功能的核心就在於forward函數部分。

先輸出一個日誌說明是對哪兩個連接進行雙向轉發,然後通過sync包下的WaitGroup實現一個條件阻塞功能,防止在Goroutine還未執行完,主線程就已經退出了。

然後發射兩個Goroutine,分別處理連接1到連接2的IO數據包拷貝以及連接2到連接1的IO數據包拷貝。因為要保證兩個埠間的通信是全雙工的,也就是兩邊同時都要能夠互相交換數據,所以要用Goroutine來實現這兩個操作的並發。

而IO數據包的拷貝核心代碼在connCopy函數中,根據是否要記錄流量日誌判斷是否要使用io.MultiWriter這個多路寫數據流的函數。

如果打開日誌文件的文件流成功,則通過io.MultiWriter函數生成一個多路寫入流,這裡這個多路寫入流的變數名為w,任何寫入到w這個寫入流的數據都會同時寫入先前參與執行多路寫入流創建函數io.MultiWriter的參數中,在這裡參數為conn1和logFile,即為埠1和日誌文件流。

接著調用io.Copy將第二參數的讀取流中讀取到的數據源源不斷地拷貝到第一個參數的寫入流中。

這裡要注意io.Copy函數是同步阻塞的,意味著只要連接沒有斷開,那麼程序執行流將一直卡在這個函數。如果拷貝出錯,那麼io.Copy函數就會返回,也就是執行他下面的代碼。拷貝出錯意味著可能連接已斷開,那麼先把寫入流的連接斷掉。

這裡要重點注意,為什麼是斷開寫入流而不是讀寫流全部斷開呢。因為我們前面發射了兩個Goroutine,如果盲目全部斷開,將會導致另一個Goroutine中可能還有未寫完的數據丟失。具體可以根據TCP四次揮手來分析。

實際執行

如果沒有Golang環境的朋友可以直接下載編譯好的可執行文件,下載地址:github.com/cw1997/NATBy

我們先來看看代碼編譯之後實際運行,上圖左邊為虛擬機跑的內網伺服器,他已經設置了入站規則,通過直接連接192.168.2.112:3389是無法連上遠程桌面的。

此時此刻右圖開始通過listen命令監聽7777和9999埠。

接著內網伺服器再通過slave命令開始雙向連接黑客的主機(右圖的192.168.2.101:7777)以及本地的127.0.0.1:3389。

然後黑客的主機上連接本地監聽的另一個埠127.0.0.1:9999即可連接上內網伺服器的遠程桌面。

大家可以根據netstat -an的結果以及控制台日誌輸出來綜合理解這個過程。

寫在最後

這個工具現在實現的仍然只是簡單的透明傳輸,並且存在諸多問題。比如說本地主動連接內網服務是一開始就預連接好的,這樣會導致一些服務如果在連上之後長期沒有數據傳輸,會主動斷掉連接,導致公網端偶爾出現無法連接上,要重新斷開重連後才能連上的小BUG,具體在HTTP伺服器要多刷新幾次頁面,遠程桌面則可能要連接上然後又取消,然後再連才能連上。而且在並發連接上處理還有一些細節沒有做好。並且目前還僅僅支持TCP連接的轉發等等,當然要實現UDP的轉發也不是很困難稍加改進即可。大家也可以點個star,提個pull request一起來改進這些問題。目前相關的開源項目也有做的比較成熟的,比如說Golang寫的ngork,大家也可以參考參考。

本文章由 @昌維 原創,在知乎專欄-代碼之美 https://zhuanlan.zhihu.com/codes 首發,轉載請註明出處。大家喜歡和支持我的文章可以點開我的頭像以及專欄名稱進行關注,或是點擊下方的打賞按鈕進行支持,謝謝!

推薦閱讀:

移動支付時代,我們的賬戶資金安全誰來保障?
由淺入深寫代理(5)-socks5-代理
socket內存佔用疑問?
圖解SDN:軟體定義網路導論篇
酷站推薦 - yunshan.net - 雲杉網路 | Pure Play SDN | 軟體定義網路

TAG:计算机网络 | 编程 | 黑客Hacker |