1. 程式人生 > >使用VC#製作多執行緒TCP connect掃描器

使用VC#製作多執行緒TCP connect掃描器

本文已投稿《黑客安全基地》。文章為《黑客安全基地》所有,未經《黑客安全基地》同意不得轉載。謝謝合作!

  如果你想知道對方的計算機提供什麼服務,什麼工具是你最常用的?沒錯!掃描器。現在各式各樣的掃描器少說也後好幾百種了。從很早以前的HakTek(這並不是最早的掃描器,但是筆者見到的第一個掃描器。)到現在的X-Scan。中國的外國的,數不勝數。
  今天我就是要教大家用VC#製作自己的多執行緒掃描器。首先,就以X-Scan為例簡單介紹一下掃描器的原理。
  我想X-Scan大家都用過吧?沒用過總見過吧?(哦,你一直用的是自己做的掃描器?得,那您翻下一頁,這頁甭看了。)X-Scan在設定的時候,主要設定主機地址和埠範圍。還有一個就是TCP或者SYN掃描方式。主機地址和埠範圍沒什麼好解釋的。關鍵就是在這個TCP/SYN的掃描方式上。TCP掃描或者說TCP connect掃描是一種最基本的掃描方式,利用完全的TCP協議進行連線嘗試。當同主機連線成功時就切斷連線,轉到下一個埠嘗試是否連線成功。如此反覆嘗試直到所有埠掃描完畢。TCP connect掃描器製作起來也非常簡單,並且掃描速度快,不需要特殊許可權(這裡指UNIX主機的許可權問題,本文不涉及這方面的內容。)。但是TCP connect 掃描有一個最大的缺點。它很容易被檢測到,而且防備較好的主機還有可能對其進行過濾。系統日誌會對其進行記錄。SYN掃描也叫半掃描,實際上就是將TCP的三次握手不完成最後一次。進行連線時發出連線請求。當收到主機反饋的SYN訊號,也就是允許連線訊號時就轉向下一個埠,而並不傳送連線確認。這樣實際上並沒有和主機完成連線,而不被日誌記錄。彌補了TCP connect的不足。不過這裡要補充說明一下的是,現在大多數伺服器都會記錄這種連線,因為SYN掃描實在太普及了。
  這裡我想解釋一下為什麼我要用VC#來做開發工具。C#也許在很多高手眼中根本不屑。但是C#是一種開發效率和執行效率相對均衡的語言。而VC#的開發環境更讓它的優勢發揮得淋漓盡致。加上.NET類庫的強大支援,很適合初學者快速入門使用。
  好了廢話少說,下面我們就開始製作自己的TCP connect掃描器。
  在VC#中新建一個Windows應用程式的工程,起名叫ScanTest。在下面的說明中括號裡的內容是控制元件的屬性設定。在窗體(Name:frmMain Text:ScanTest)上新增一個按鈕(Name:btnScan Text:掃描)、一個文字框(Name:txtIP)、兩個NumericUpDown控制元件(Name1:numPortMin Value1:1 Maximum1:65535 Minimum1:1,Name2:numPortMax Valude2:1024 Maximum1:65535 Minimum1:1)、和一個顯示掃描結果用的ListBox(Name:lbResult)。如圖1:
2003-8-24vcsscan.jpg
圖1
  設計好了程式介面,下邊就開始編碼。
  既然要TCP connect掃描器,那麼就少不了TCP連線庫的支援。這裡我們主要用的兩個類System.Net.IPAddress和System.Net.Sockets.TcpClient。你要在你的窗體.cs檔案中包含這兩個類所在的名字空間。如下:
  using System.Net;
  using System.Net.Sockets;
  這兩個類的功能非常強大,但是我們只用其中的一點就可以了。大家如果有興趣,可以查MSDN瞭解更多的內容。
  在窗體的類中新增一個方法isPortOpen。如下:
public bool isPortOpen(string ip, int port)
{
try
{
TcpClient client = new TcpClient();//建立一個TcpClient例項
IPAddress address = IPAddress.Parse(ip);//轉化string型別的IP地址到IPAddress
client.Connect(address, port);//連線伺服器address的port埠
client.Close();//連線成功立即斷開
return true;//返回true,連線成功
}
catch(Exception e)//連線失敗TcpClient類丟擲異常,這裡捕獲
{
return false;//返回false,連線失敗
}
}
這個方法用起來很簡單,比如在btnScan的Click事件中加入如下程式碼:
private void btnScan_Click(object sender, System.EventArgs e)
{
if(this.isPortOpen("127.0.0.1", 80))
lbResult.Items.Add((object)"本地計算機80埠連線成功");
else
lbResult.Items.Add((object)"本地計算機80埠連線失敗");
}
  點選窗體上的btnScan這個按鈕就可以檢查本地計算機的80埠是否開放。
  現在,你已經向你的目標開始接近了。
  這只是檢查一個埠是否開放, 而且不能控制檢查哪個計算機。恩,對上面的程式碼做這樣的修改:
private void btnScan_Click(object sender, System.EventArgs e)
{
for(int i = (int)numPortMin.Value;i <= (int)numPortMax.Value; i++ )
if(this.isPortOpen(txtIP.Text, i))
lbResult.Items.Add((object)(txtIP.Text+":"+i.ToString()+"埠連線成功"));
else
lbResult.Items.Add((object)(txtIP.Text+":"+i.ToString()+"埠連線失敗"));//一般來說掃描程式不記錄連線失敗的埠,這裡僅僅是為了演示
}
  編譯執行工程,在文字框txtIP填入你的目標地址,比如“127.0.0.1”或“192.168.1.14”什麼的。然後設定numPortMin和numPortMax的值比如50和100,表示掃描從50到100的埠。我建議這兩個值只差不要太大,要不你要很有耐心才行。後面我會解釋為什麼。然後點選按鈕btnScan。程式沒響應了!!!!只要你沒有把掃描埠的範圍設得太大,稍微等幾秒種程式就會顯示出掃描結果。
  恩,是不是很興奮?你可以掃描某臺計算機的任意範圍埠了。
  等等,先彆著急做實驗。我們還有一個嚴重的問題沒有解決:程式中斷響應!
  為什麼會這樣呢?其實很好解釋:在這裡我們使用了一個迴圈。當點選btnScan除法Click事件的時候,程式進入這個迴圈中執行。直到迴圈執行結束程式才繼續響應。恩,知道了為什麼,下面我們看看怎麼解決這個問題。
  先說一下你要做的準備工作。首先讓你的窗體.cs檔案包含System.Threading這個名字空間。這個名字空間下的內容都是控制執行緒用的。然後在窗體類中新增這麼兩個欄位:
ThreadStart threadstart;
Thread thread;
  在窗體的建構函式中新增執行緒構造的內容,如下:
public frmMain()
{
InitializeComponent();
threadstart = new ThreadStart(ScanThread);//你要新增的就是這兩句
thread = new Thread(threadstart);
}
ScanThread是你要新增的一個方法。這個方法必須是沒有引數,返回值為void。將剛才按鈕btnScan的Click事件中的程式碼放到這個ScanThread方法中來:
public void ScanThread()
{
for(int i = (int)numPortMin.Value;i <= (int)numPortMax.Value; i++ )
{
if(this.isPortOpen(txtIP.Text, i))
lbResult.Items.Add((object)(txtIP.Text+":"+i.ToString()+"埠連線成功"));
/*else
lbResult.Items.Add((object)(txtIP.Text+":"+i.ToString()+"埠連線失敗"));
我們只記錄連線成功的埠,所以我遮蔽掉這些程式碼。但是為了讓大家看得更清楚,我保留這些註釋。*/
this.Text = "ScanTest 正在掃描埠:"+i.ToString();//這裡是原來程式碼所沒有的,僅僅是為了讓你看得更清楚而已。
}
}
修改按鈕btnScan的Click事件的程式碼:
private void btnScan_Click(object sender, System.EventArgs e)
{
if(thread.ThreadState.Equals(ThreadState.Running))//判斷執行緒是否已經執行
{
thread.Abort();//中斷執行緒執行
btnScan.Text = "掃描";
}
else
{
thread.Start();//開始執行緒執行
btnScan.Text = "停止";
}
}
  現在再編譯執行一下工程,看看效果。我們可以瞭解掃描的進度,而且程式也不再停止響應了。哈,真棒!
  完工了?No!!!這並不是多執行緒,你現在只建立了一個執行緒對埠進行著很慢的逐一掃描。那麼繼續我們的工作。“Take it easy! You will see New York.”
  在我們建立多執行緒掃描之前,有個問題需要解決:因為我們無法用引數傳遞的方式呼叫執行緒的函式,所以讓執行緒知道掃描到哪個埠就是個很關鍵的問題。對了,用公共變數。下面我們就對程式進行一次比較大的改造。
  首先,修改窗體類中的執行緒宣告如下:
ThreadStart threadstart;
Thread [] thread;
  並且新增兩個整形欄位到窗體類中:
int port;//當前正在掃描的埠
int portmax;//掃描埠最大值
  在窗體建構函式中的程式碼修改如下:
public frmMain()
{
InitializeComponent();
threadstart = new ThreadStart(ScanThread);
thread = new Thread[10];//這裡設定使用10個執行緒
for(int i=0;i<10;i++)//利用迴圈初始化10個執行緒
{
Thread t = new Thread(threadstart);
thread[i] = t;
}
}
  既然是多執行緒,那麼執行執行緒和停止執行緒也當然與單執行緒不太一樣:
private void btnScan_Click(object sender, System.EventArgs e)
{
if(btnScan.Text.Equals("掃描"))//這裡我偷懶了,好的程式不應該這麼寫
{
portmax = (int)numPortMax.Value;  //這裡初始化執行緒執行所需要用的變數
port =  = (int)numPortMin.Value;
for(int i=0;i<10;i++) //這裡依然是利用迴圈執行執行緒
thread[i].Start();
btnScan.Text = "停止";
}
else
{
for(int i=0;i<10;i++) //這裡依然是利用迴圈停止執行緒
thread[i].Abort();
btnScan.Text = "掃描";
}
}
  好了,現在就需要進行最關鍵的函式ScanThread的改造,請大家仔細體會我的註釋的講解:
public void ScanThread()
{
lock(this)//這裡為了  避免重複掃描同一個埠,設定下面的執行為臨界區,也就是說同一時刻只有一個執行緒能執行臨界區的程式碼。
{
while(port <= portmax)//這裡不使用for語句,是因為port對於單一執行緒來說不是逐一遞增了。
{
if(this.isPortOpen(txtIP.Text, port))
lbResult.Items.Add((object)(txtIP.Text+":"+port.ToString()+"埠連線成功"));
/*else
lbResult.Items.Add((object)(txtIP.Text+":"+i.ToString()+"埠連線失敗"));
我們只記錄連線成功的埠,所以我遮蔽掉這些程式碼。但是為了讓大家看得更清楚,我保留這些註釋。*/
this.Text = "ScanTest 正在掃描埠:"+port.ToString();
port++;
}
}
}
  有臨界區、能多執行緒執行、不會衝突。很不錯的一段程式碼。執行一下看看?
  (……漫長的等待……)
  “你確定這是多執行緒?”也許會有讀者終於忍不住要問。是的,這的確是多執行緒。但是我們的程式碼有問題!!!造成掃描的速度非常慢。
  大家注意到,我的臨界區是整個掃描函式。那麼同一時刻只有一個執行緒能執行掃描。雖然我們設定了多執行緒,但是執行速度不會比單執行緒快多少。甚至更慢(如果算上執行緒切換的時間的話)。怎麼辦?首先,我們看看為什麼要設定這個臨界區再來解決這個多執行緒執行的問題。
  如果沒有臨界區,可能一個執行緒在執行到該執行if(this.isPortOpen(txtIP.Text, port))這一步時,另一個執行緒執行了port++這一語句。你的一個埠被跳過了,沒有掃描。我們必須把有關變數port的操作都放到臨界區裡面去。保證一個執行緒使用變數port或者對其操作的時候,別的執行緒必須等待。上面的ScanThread函式是很好的滿足這個要求的。但是,因為把埠檢測的程式碼也放入了臨界區,造成埠檢測的時候依然只能有一個執行緒在操作。
  知道了問題的所在,那就開動腦筋解決問題吧。現在要將對變數port的操作放入臨界區加以保護。同時還要將isPortOpen的操作放在臨界區之外,以便多執行緒併發的掃描。
  那麼新的ScanThread函式就產生了:
public void ScanThread()
{
while(true)//我們讓迴圈一直進行
{
int nowport;//真正要掃描的埠,這是區域性變數
lock(this)//進入臨界區
{
if(port > portmax)
return;//當掃描到最大埠就終止函式執行
else
{
nowport = port;//否則設定真正掃描的埠
port++;//跳轉到下一埠
}
}//出臨界區
if(this.isPortOpen(txtIP.Text, nowport))//請注意我這裡使用的nowprot變數。這時候其他執行緒對變數port的操作已經不影響埠的檢測,而nowport是一個區域性變數,不受其他執行緒影響
lbResult.Items.Add((object)(txtIP.Text+":"+nowport.ToString()+"埠連線成功"));
/*else
lbResult.Items.Add((object)(txtIP.Text+":"+i.ToString()+"埠連線失敗"));
我們只記錄連線成功的埠,所以我遮蔽掉這些程式碼。但是為了讓大家看得更清楚,我保留這些註釋。*/
this.Text = "ScanTest 正在掃描埠:"+nowport.ToString();

}
}
  現在你再編譯執行試試看?是不是速度快了很多呢?
  至此,一個標準的多執行緒TCP connect掃描器就做好了。這裡我要補充幾點簡單說明(順便再多騙點稿費,嘿嘿~)。
1. C#本身的執行速度不是很快,所以這個掃描器的速度肯定比不上C/C++製作的掃描器。
2. 這裡我沒有像X-Scan那樣讓使用者自行設定執行緒的執行個數。不過我想這已經不是什麼難題了。(提示:你只要把建構函式裡關於執行緒初始化那部分程式碼找個合適的地方放下,添點控制元件,重新調整調整程式碼就可以控制執行緒個數了。)
3. 雖然這裡只是掃描一臺計算機的埠,但是如何掃描多臺主機我想聰明的讀者心裡已經有了答案。(一邊換主機地址,一邊換埠對你來說不難吧?)
  其實一個好的掃描器還應該有很多功能,比如漏洞檢測什麼的。但是知道了基本原理這些功能應該不是問題。對掃描到的有可能有漏洞的開放埠,傳送特定的資料進行檢查既可。
祝大家玩得開心。^@^ The end.