Всем доброго дня! Сегодня я бы хотел раскрыть такую крутую тему как
WinApi функции, благодаря которым мы можем творить с окнами других приложений все что душе угодно.
Самый главный инструмент, который нам понадобится- это
SPY++(прикрепил) , он позволяет отслеживать какие сообщения отправляются окну приложения для совершения с ним того или иного действия, следовательно наша задача сводится к тому чтобы правильно сформировать сообщение и отправить его нужному окну.
Итак, начинаем исследование, в качестве подопытного я выбрал клиент Viber'a для ПК, сделаем с вами авторассыльщик по списку номеров, погнали:
Для работы с WinApi из языка С# я выделил для себя три основных источника:
а) Это конечно же всеми любимый
https://msdn.microsoft.com - там можно получить инструктаж для любой апишной функции, а так же необходимые для них значения констант. Документация для С++
б)
http://www.firststeps.ru - на этом сайте можно посмотреть русскоязычное описание всех основных функций. Документация тоже для С++
в)
http://www.pinvoke.net - на этом сайте через поиск можно найти любую WinApi функцию именно для С#, что нам и нужно
Первое что нам нужно сделать это получить хендл (уникальный идентификатор) окна вайбера, за это отвечает функция FindWindow, идем на pinvoke и находим ее:
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
Куда же ее вставлять? Все просто:
Так же необходимо добавить директиву, как на скрине (именно туда, а не во вкладку Директивы using):
using System.Runtime.InteropServices;
Еще заметьте что саму функцию FindWindow я сделал публичной, чтобы к ней можно было обращаться из экшена "Свой С# код".
Как мы видим данная функция на вход получает имя класс и имя заголовка окна, со вторым все понятно, он всегда написан в верхней шапке окна, а где же взять имя класса? На помощь приходит наш spy++, качаем, запускаем его:
Как вы наверно уже догадались, он нам показывает все текущие открытые окна для того чтобы мы могли посмотреть какие сообщения система отправляет этим окнам для совершения манипуляций с ними. В нем есть прекрасный поиск окон при помощи прицела (его нужно перетащить на окно, которое собираемся атаковать):
После того как наведете прицел на нужное окно и отпустите его, у вас заполнятся поля Handle(уникальный идентификатор), Caption(заголовок окна) и искомое имя Class'a - копируем его и идем обратно в PM, теперь можно уже протестировать как работает наша функция:
Создаем экшен своего С# кода и вставляем туда:
IntPtr hwnd = CommonCode.FindWindow("Qt5QWindowIcon", null);
return hwnd;
Как видите второй параметр я поставил null, т.к. он не обязателен, и нужен только в тех случаях, когда присутствуют окна с таким же именем класса. Так же в некоторых случаях можно не указывать имя класса, но указать заголовок окна.
Отправляем результат в какую нибудь переменную и смотрим:
Работает!! К сожалению, зенка преобразовывает наш IntPtr сразу в тип string и мы не можем посмотреть его value, где это число будет именно в формате IntPtr, тогда бы мы смогли по поиску в spy++ сразу найти это окно и убедиться верно ли мы нашли идентификатор.
Хендл нужного окна получили, дальше нужно отправить ему какое нибудь сообщение, чтобы с ним что то произошло, для этого снова идем в spy++, снова прицелом находим окно viber' a и в этот раз после поиска жмем ОК, щелкаем ПКМ по выделенной строке и выбираем Messages, после этого нас перекидывает в новое пустое окно spy++, НО стоит нам навести мышью на окно вайбера, как тут же мы увидим как сыпятся сообщения:
Теперь все зависит от поставленных перед вами задач. Если мы делаем рассыльщик по вайберу, то в цикле мы должны будет делать следующие действия:
По нашему плану, нам первым делом нужно нажать на иконку с шариками, чтобы была возможность набора номера, смотрим в spy++ что для этого действия отправляется окну вайбера:
Среди всяких сообщений о передвижении мыши и установкой курсора (их названия говорят сами за себя) можно увидеть нажатия кнопки мыши по координатам окна, значит нужно определенно отправить эти два сообщения, а еще по своему опыту я скажу что нужно отправить WM_MOUSEMOVE на эти же координаты, т.к. без него клик может не сработать, давайте разбираться как это делается. Во- первых, сообщения окнам отправляются двумя способами это при помощи функций SendMessage и PostMessage, разница между ними только в том что первая ожидает ответа от приложения и чисто теоретически если приложение вайбера повиснет в момент отправки сообщения, то наше приложение тоже зависнет в ожидании, вторая функция ничего не ожидает, но я все равно буду юзать SendMessage =)
Идем на pinvoke.nеt и ищем эту функцию, я нашел:
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
Пихаем ее в общий код в нашем проекте, так же делаем ее публичной для доступа из экшена.
Смотрим что эта функция принимает на вход, это хендл окна, которому отправляем сообщение (его мы находили выше), само сообщение в типе Uint32 - эту константу найдем на msdn, wParam - что это такое вообще тоже узнаем на msdn , lParam аналогично. Вбиваем в поисковик название нашего сообщения (WM_LBUTTONDOWN), попадаем на msdn и видим:
Это есть наше сообщение в типе UInt32, сразу закидываем его в общий код, снова делая публичным, вот так:
public static UInt32 WM_LBUTTONDOWN = 0x0201;
Дальше видим, что описываются интересующие нас параметры wParam в нашем случае нужен вот такой:
Если вы посмотрите на сообщение в spy++ вы поймете почему =)
Аналогичным способом добавляем ее в общий код
public static UInt32 MK_LBUTTON = 0x0001;
А вот с lParam не все так просто, мсдн нам говорит что он должен содержать low-order - координата Y и high-order - координата X:
Эти low и high ордеры еще называют младшим и старшим словом, т.е. нам нужно из двух этих параметров получить один в типе IntPtr, для этого есть функция MakelParam, на пинвоке ее нету, но я ее нашел в интернетах, держите:
public static IntPtr MakeLParam(int LoWord, int HiWord)
{
return (IntPtr)((HiWord << 16) | (LoWord & 0xffff));
}
Снова пихаем ее в общий код:
Если кому интересно что в ней происходит, то вам сюда
ТЫК
Не забываем про WM_MOUSEMOVE и его тоже цепляем в общий код проекта:
public static UInt32 WM_MOUSEMOVE = 0x0200;
Ну вот, все данные для отправки сообщения мы наскребли, можем формировать вызов функции в нашем экшене:
CommonCode.SendMessage(hwnd /*хендл нужного окна (вайбера)*/, CommonCode.WM_MOUSEMOVE /* сам код сообщения*/, IntPtr.Zero /*wParam, spy++ показал что он заполнен нулями*/, CommonCode.MakeLParam(238, 78) /* сформированный из координатов для клика lParam */ );
CommonCode.SendMessage(hwnd /*хендл нужного окна (вайбера)*/, CommonCode.WM_LBUTTONDOWN /* сам код сообщения*/, (IntPtr)CommonCode.MK_LBUTTON /*wParam, с явным приведением к IntPtr*/, CommonCode.MakeLParam(238, 78) /* сформированный из координатов для клика lParam */ );
По идее уже сейчас если запустить наш проект, то в spy++ мы увидим, в каком виде пришло наше сообщение окну вайбера:
Замечательно, сообщение о передвижении мыши на координаты и нажатии ЛКМ по этим координатам вайбера успешно отправилось, да еще и в правильном виде!
Давайте теперь отправим сообщение о том что ЛКМ отпустили в этих же координатах, т.е. отправим WM_LBUTTONUP.
Снова на msdn находим значение этого сообщения и добавляем в общий код:
И формируем вызов функции:
CommonCode.SendMessage(hwnd /*опять же нужный идентификатор нашего окна */, CommonCode.WM_LBUTTONUP /* само значение сообщения */, IntPtr.Zero /* если внимательно посмотреть в spy++ на это сообщение, то видно что wParam там заполнен нулями, я так и сделал */, CommonCode.MakeLParam(238, 78) /* по старой схеме формируем lParam из координатов*/);
В теории сейчас должна успешно нажаться кнопка для набора номера в вайбере, даже в свернутом состоянии окна, проверяем:
У меня все прекрасно сработало, даже в свернутом вайбере!
Не отступаем от нашего плана и следующим пунктом у нас стоит ввод номера телефона, но надо понимать, что в не активном окне скорее всего при переходе на номеронабератель каретка не будет в нужном месте, давайте ее туда поставим обычным кликом по полю набора номера, точно так же как мы только что выбрали номеронабиратель:
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONDOWN, (IntPtr)CommonCode.MK_LBUTTON, CommonCode.MakeLParam(177, 155));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONUP, IntPtr.Zero, CommonCode.MakeLParam(177, 155));
Тут практика показала, что можно обойтись без WM_MOUSEMOVE.
Отлично, теперь можно набирать текст, лезем в spy++ и смотрим что отправляется окну вайбера при нажатии любой циферки:
Я выделил самое подходящее для нас сообщение о нажатии клавиши, однако, в некоторых случаях его может и не быть, тогда вам нужно будет смотреть на сообщения WM_KEYDOWN и WM_KEYUP.
Так вот, это сообщение говорит окну о том что пользователь нажал какой либо символ, значит мы должны сделать так же, в путь:
Сгугливаем значение WM_CHAR и кидаем в общий код:
public static UInt32 WM_CHAR = 0x0102;
Читаем на мсдн, что wParam - это есть char код символа, а lParam расписан вот так:
Это означает что придется немного поиграться с побитовыми операциями, юхууу!
Из всего этого списка нам нужно только лишь сформировать верный scan code клавиши и выполнить сдвиг на 16 бит влево и запихать туда единичку, т.к. в spy++ сказано что cRepeat: 1 - это мы и сделаем, но для получения scan code воспользуемся функцией:
[DllImport("user32.dll")]
public static extern uint MapVirtualKey(uint uCode, uint uMapType);
Которую тоже нужно положить в общий код. Если вы загуглите что это за функция, то поймете что по сути она возвращает scan code клавиши, получая на входе ее virtual code и тип преобразования.
Формируем сообщение для нажатия клавиши 5 например:
CommonCode.SendMessage(hwnd, CommonCode.WM_CHAR, (IntPtr)'5' /*явное преобразование char в IntPtr*/, (IntPtr)(CommonCode.MapVirtualKey('5', 0)<<16|1) /*это и есть получение scan code клавиши , выполнение сдвига влево на 16 бит и запихивание туда 1*/ );
Но нам же нужно набрать целый номер, поэтому будем печатать посимвольно из всей строки по циклу:
string number = "500092511";
for(int i=0; i< number.Length ;i++)
CommonCode.SendMessage(hwnd, CommonCode.WM_CHAR, (IntPtr)number[i], (IntPtr)(CommonCode.MapVirtualKey('6', 0)<<16|1));
Правильней конечно было бы написать функцию, которая на вход получает любой текст и набирает его в нашем окне вайбера, ну да ладно, так нагляднее, проверяем:
Как видно, все работает, сообщения шлются, текст набирается, прекрасно! Осталось пару штрихов =)
По уже знакомой нам методике жмем кнопку, при помощи сообщений о нажатии ЛКМ, меняем только координаты:
CommonCode.SendMessage(hwnd, CommonCode.WM_MOUSEMOVE, IntPtr.Zero, CommonCode.MakeLParam(190,503));
CommonCode.SendMessage(hwnd,
CommonCode.WM_LBUTTONDOWN, (IntPtr)CommonCode.MK_LBUTTON, CommonCode.MakeLParam(190,503));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONUP, IntPtr.Zero, CommonCode.MakeLParam(190,503));
Пробуем иии, не работает
Но мы же не отчаиваемся и смотрим в spy++, скорее всего что то пропустили:
Попалась! Не хватает WM_MOUSEACTIVATE, давайте попробуем его добавить:
public static UInt32 WM_MOUSEACTIVATE = 0x0021;
Это в общий код. Так же на мсдн находим значение переменное HTCLIENT и она равна 1, поэтому я ее добавлять не буду, а запишу прямо в формировании сообщения:
CommonCode.SendMessage(hwnd, CommonCode.WM_MOUSEACTIVATE, hwnd /*по документации msdn сказано что wParam это родительское окно, в нашем случае же это главное окно, поэтому сюда снова передаем hwnd*/, CommonCode.MakeLParam(1 /*это наша HTCLIENT*/, (int)CommonCode.WM_LBUTTONDOWN )/*так формируется lParam согласно инструкциям*/);
Важно понимать порядок передачи сообщений, т.е. сначала мы наводим курсор (WM_MOUSEMOVE), дальше активируется элемент (WM_MOUSEACTIVATE) потом идет нажатие мышкой (WM_LBUTTONDOWN), в коде это выглядит так:
CommonCode.SendMessage(hwnd, CommonCode.WM_MOUSEMOVE, IntPtr.Zero, CommonCode.MakeLParam(190,503));
CommonCode.SendMessage(hwnd, CommonCode.WM_MOUSEACTIVATE, hwnd, CommonCode.MakeLParam(1, (int)CommonCode.WM_LBUTTONDOWN));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONDOWN, (IntPtr)CommonCode.MK_LBUTTON, CommonCode.MakeLParam(190,503));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONUP, IntPtr.Zero, CommonCode.MakeLParam(190,503));
Следующий пункт - это ввод текста сообщения для рассылки, опять же не забываем поставить каретку в нужное место, но как? Если вы еще не заметили, то до этого момента, пока мы работали с левой панелькой вайбера, мы могли спокойно забирать координаты из spy++ и пользоваться ими, т.к. элементы на этой панельке не меняют свои координаты относительно всего окна, в отличии от поля, в которое нужно вводить текст сообщения, если мы возьмем для него координаты, а потом небрежный пользователь изменит размеры окна вайбера, то все сломается, нужно это как то учесть...
Решение очевидно, исходя из расчета что относительно нижней границы окна поле для ввода сообщения никогда не поменяет свое положение по оси Y, ну а по оси X значение можно подобрать таким, чтобы клик всегда попадал по этому полю. Значит нам нужно получать текущие размеры окна и из его высоты отнимать определенное число, которое нужно подобрать, поехали:
В общий код кидаем вот эти функции:
[DllImport("user32.dll")]
static extern bool GetWindowRect(IntPtr hWnd, out Rectangle lpRect);
[StructLayout(LayoutKind.Sequential)]
public struct Rectangle
{
public readonly int Left;
public readonly int Top;
public readonly int Right;
public readonly int Bottom;
public readonly int Height;
public readonly int Width;
public Rectangle(int width, int height) : this()
{
this.Height = height;
this.Width = width;
}
}
public static Rectangle GetWindowRectEx(IntPtr hWnd)
{
if (IntPtr.Zero == hWnd) return default(Rectangle);
Rectangle lprect;
GetWindowRect(hWnd, out lprect);
return new Rectangle(lprect.Right - lprect.Left, lprect.Bottom - lprect.Top);
}
В этой статье я уже не буду мучать вас понятиями структур и как они работаю, так что если интересно узнать как это работает, то гуглите или пишите.
Теперь мы можем получить в своем коде размеры окна, вызвав функцию GetWindowRectEx:
var rect = CommonCode.GetWindowRectEx(hwnd);
Из этой переменной нас интересует только высота, она достигает максимального значения на нижней границе окна вайбера, значит мы должны из нее отнимать примерно 100, чтобы всегда попадать кликом на поле ввода текста, координата X будет примерно 570, именно при таком размере щелчек всегда будет попадать, вот что получается:
var rect = CommonCode.GetWindowRectEx(hwnd);
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONDOWN, (IntPtr)CommonCode.MK_LBUTTON, CommonCode.MakeLParam(570,rect.Height-100));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONUP, IntPtr.Zero, CommonCode.MakeLParam(570,rect.Height-100));
И теперь это работает с любым размером окна, нештяк! =)
Вводим наш текст по старой схеме:
string text = "Это добрый спам по вайберу! Никакой рекламы, только добро! Всем добра!";
for(int i=0; i< text.Length ;i++)
CommonCode.SendMessage(hwnd, CommonCode.WM_CHAR, (IntPtr)text[i], (IntPtr)(CommonCode.MapVirtualKey(text[i], 0)<<16|1));
Все работает на УРА! Остался последний шаг - отправить сообщение, но так просто кликнуть на стрелочку мы опять не можем потому что у него каждый раз могут быть разные координаты относительно всего окна, по оси Y можно опять таким же макаром высчитать, но вот по оси X не получится, благо есть отправка по нажатию кнопки ENTER, действуем:
Сразу скажу что сообщение WM_CHAR с кодом кнопки enter вайбер видимо не обрабатывает, поэтому будем отправлять сообщение WM_KEYDOWN
Опять кидаем его значение в общий код:
public static UInt32 WM_KEYDOWN = 0x0100;
Гуглим эту функцию и видим, что wParam у нас виртуальный код клавиши, забираем нужный нам в исходный код:
public static UInt32 VK_RETURN = 0x0D;
lParam опять стряпаем из битов, не проблема, сделано:
CommonCode.SendMessage(hwnd, CommonCode.WM_KEYDOWN, (IntPtr)CommonCode.VK_RETURN, (IntPtr)(CommonCode.MapVirtualKey(CommonCode.VK_RETURN, 0)<<16|1));
Все по аналогии с примером на WM_CHAR.
Хотел уже сказать что все готово, но не тут то было)) Оказывается поле для ввода номера не отчищается автоматически, поэтому придется после каждого нажатия на номеронабиратель отчищать это поле, последний рывок:
В виду того что вайбер не хочет принимать сообщения нажатий Ctrl+A (хотя я уверен что это можно сделать, просто я уже туплю) я принял брутальное решение, просто 100 раз отправлю нажатие Backspace
:
for(int i=0; i<100; i++)
CommonCode.SendMessage(hwnd, CommonCode.WM_KEYDOWN, (IntPtr)CommonCode.VK_BACK, (IntPtr)(CommonCode.MapVirtualKey(CommonCode.VK_BACK, 0)<<16|1));
И чисто и быстро =)
Остается только все скомпоновать и можно запускать нашу адскую машинку, полный листинг сниппета:
IntPtr hwnd = CommonCode.FindWindow("Qt5QWindowIcon", null); // получаем хендл окна вайбера
var all_number = project.Lists["Номера для рассылки"];// лист, в котором лежат номера для рассылки
foreach(string a in all_number) // идем по циклу по всем номерам
{
//клик мышкой по номеронаберателю
CommonCode.SendMessage(hwnd, CommonCode.WM_MOUSEMOVE, IntPtr.Zero, CommonCode.MakeLParam(243,76));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONDOWN, (IntPtr)CommonCode.MK_LBUTTON, CommonCode.MakeLParam(238, 78));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONUP, IntPtr.Zero, CommonCode.MakeLParam(238, 78));
//
//клик по полю ввода номера (установка каретки)
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONDOWN, (IntPtr)CommonCode.MK_LBUTTON, CommonCode.MakeLParam(177, 155));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONUP, IntPtr.Zero, CommonCode.MakeLParam(177, 155));
//
//супер кастыль для отчищения поля от остатков предыдущего номера
for(int i=0; i<100; i++)
CommonCode.SendMessage(hwnd, CommonCode.WM_KEYDOWN, (IntPtr)CommonCode.VK_BACK, (IntPtr)(CommonCode.MapVirtualKey(CommonCode.VK_BACK, 0)<<16|1));
//
// текущую итерацию цикла складываем в переменную number и посимвольно отправляем соответствующие нажатия в окно вайбера
string number = a;
for(int i=0; i< number.Length ;i++)
CommonCode.SendMessage(hwnd, CommonCode.WM_CHAR, (IntPtr)number[i], (IntPtr)(CommonCode.MapVirtualKey(number[i], 0)<<16|1));
//
// Клик по кнопке перехода в чат с номером
CommonCode.SendMessage(hwnd, CommonCode.WM_MOUSEMOVE, IntPtr.Zero, CommonCode.MakeLParam(190,503));
CommonCode.SendMessage(hwnd, CommonCode.WM_MOUSEACTIVATE, hwnd, CommonCode.MakeLParam(1, (int)CommonCode.WM_LBUTTONDOWN));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONDOWN, (IntPtr)CommonCode.MK_LBUTTON, CommonCode.MakeLParam(190,503));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONUP, IntPtr.Zero, CommonCode.MakeLParam(190,503));
//
// Пауза для того чтобы вайбер успел прорисовать элементы после перехода в чат
Thread.Sleep(1000);
//получаем текущий размер окна
var rect = CommonCode.GetWindowRectEx(hwnd);
//кликаем по полю ввода сообщения с учетом размеров окна
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONDOWN, (IntPtr)CommonCode.MK_LBUTTON, CommonCode.MakeLParam(570,rect.Height-100));
CommonCode.SendMessage(hwnd, CommonCode.WM_LBUTTONUP, IntPtr.Zero, CommonCode.MakeLParam(570,rect.Height-100));
//
//текст, который будет рассылаться
string text = "Это добрый спам по вайберу! Никакой рекламы, только добро! Всем добра!";
//по циклу печатаем каждый символ текста в окно вайбера
for(int i=0; i< text.Length ;i++)
CommonCode.SendMessage(hwnd, CommonCode.WM_CHAR, (IntPtr)text[i], (IntPtr)(CommonCode.MapVirtualKey(text[i], 0)<<16|1));
CommonCode.SendMessage(hwnd, CommonCode.WM_KEYDOWN, (IntPtr)CommonCode.VK_RETURN, (IntPtr)(CommonCode.MapVirtualKey(CommonCode.VK_RETURN, 0)<<16|1));
}
return hwnd;
Демонстрация этого чуда
Вообще область применения таких функций довольно таки обширная, не забывайте про эмуляторы Android, которыми так же без проблем можно управлять из PM...
Если у кого то возникнут вопросы, то пишите на почту
[email protected]
Всем спасибо за внимание, надеюсь вы все разберетесь в этом нелегком направлении! =)