使用VC#制作多线程TCP connect扫描器

来源:互联网 发布:网页编辑软件 编辑:程序博客网 时间:2024/04/20 10:58

使用VC#制作多线程TCP connect扫描器

( 作者:mikespook | 发布日期:2003-8-24 | 浏览次数:250 )

关键字:网络,c#,扫描,多线程 本文已投稿《黑客安全基地》。文章为《黑客安全基地》所有,未经《黑客安全基地》同意不得转载。谢谢合作!

  如果你想知道对方的计算机提供什么服务,什么工具是你最常用的?没错!扫描器。现在各式各样的扫描器少说也后好几百种了。从很早以前的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:
图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.
原创粉丝点击