«Сейчас вы напишите самую сложную в своей жизни программу, которая будет просто складывать два числа»
Рыжова Ирина Михайловна


Интеллектуально-азартные онлайн-игры – лакомый кусок для ботоводов. Даже если разработчики игрового софта тратят большие деньги на отлов ботов, – как в онлайн покер-румах, например – всё равно велика вероятность наткнуться на «умного бота», игра с которым будет в одни ворота. Особенно, если бот абсолютное неуязвим… Ибо никакие деньги не защитят систему, через которую проходят большие деньги.


Рис. 1. Ботоводы наступают


Скрытая угроза


Интеллектуально-азартные онлайн-игры – небезопасны. В особенности небезопасны те из них, где вращаются живые деньги. Их небезопасность проявляется прежде всего в том, что невозможно знать наверняка, с кем играешь: с живым человеком, или же с – ботом. Во втором случае игра будет в одни ворота. В играх «на интерес», – шахматы и шашки на маил.ру, к примеру, – владельцы онлайн-игр смотрят на ботовождение сквозь пальцы. В многопользовательских онлайн-играх, где в какой-то степени фигурируют живые деньги, отлову ботов уделяется несколько большее внимание. Однако даже если подходить к пресечению ботоводства в высшей степени серьёзно – как это например сделано в онлайн покер-румах – всё равно велика вероятность наткнуться на бота.

Разработчики софта для игры в покер тратят большие деньги на разоблачение ботов, и тем не менее, ботоводство в онлайн-покере процветает. Ибо никакие деньги не смогут защитить систему, через которую проходят большие деньги. Ботоводы и ловцы ботов, – анализируя уловки оппонентов и принимая соответствующие контрмеры, – попеременно одерживают победу. Кто-то может думать, что битве между ними никогда не будет конца. Однако, существует схема безопасного ботовождения, перед которой ловцы ботов – пасуют. Она интересна тем, что даже если «борцы за справедливость» будут иметь на руках подробно прокомментированные исходные коды бота, они не смогут установить факт его использования. Реализация данной схемы – мероприятие дорогостоящее, однако поскольку потенциальная выгода велика, схема вполне актуальна.

# Автобиографические заметки

Мой двоюродный дядя – рыбак, он и меня к рыбалке приобщил. Он – виртуозный рыбак со стажем, окончивший ТУСУР. Дядя начинал рыбачить на военном приборостроительном заводе РОТОР (Алтайский край, Барнаул), участвуя в разработке подводных ракет спирального наведения на «объекты-невидимки» (не помню, как это по-умному называется). А желание пойти в ТУСУР у него возникло, когда, дядя в очередной раз отправляясь копать червей, в специально приготовленную для этого консервную банку, наткнулся на своего гения-знакомого, который не долго думая сделал из этой консервной банки радио. Это так впечатлило дядю, что он захотел стать электронщиком. После рыбалки на РОТОРе, он переквалифицировался на производство электроудочек, которые даже патентовать не надо было, поскольку дядя использовал в них мало кому доступные запчасти-обломки ЭВМ (тех самых многокомнатных ЭВМ прошлого тысячелетия); у него был доступ к этой технике, т.к. он после РОТОРа что-то околоЭВМное инженерил. У меня до сих пор на веранде ещё какие-то запчасти этих динозавров остались, хотя я и прореживаю регулярно их завалы. В прошлом году наконец-то выкинул ведро давно протухшего винегрета из микросхем (ЛА3, Триггеры, мультиплексоры, таймеры, буфера и т.д.). После электроудочек мой дядя переквалифицировался в обычного рыбака. Просто стал ходить на рыбалку. Вот на этих рыбалках он меня и начал приобщать к полезным делам за компьютером. Одна из самых запоминающихся рыбалок для меня – это разработка программно-аппаратного комплекса для невидимой рыбалки в онлайн-покер-румах (бот для покера, в простонародии). А в покер-румах, как известно, самый чуткий рыбнадзор, поэтому просто программная эмуляция, даже на уровне драйверов, может быть отслежена – со всеми вытекающими. Поэтому невидимый покер-бот – это довольно-таки дорогая игрушка, но с помощью неё в покер-румах очень много рыбы наловить можно (подробнее см. «Рыбацкие байки автобиографического характера»).



Три условия неуязвимого ботовождения


Любой бот по сути своей делает всего три вещи: фотографирует, анализирует и реагирует. Во-первых, бот фотографирует текущее состояние игры – либо скриншотом экрана, либо перехватом сетевого трафика. Во-вторых, бот принимает решение, как поступать в сложившихся обстоятельствах, – иногда прибегая к помощи стороннего софта (например, в случае с шахматами, он может взаимодействовать с каким-нибудь шахматным движком). Наконец в-третьих, бот эмулирует взаимодействие пользователя с клавиатурой и мышью. Далее этот троичный цикл, – который ловцы ботов стремятся отследить и сломать, – повторяется. Соответственно битва ботоводов и «борцов за справедливость» разворачивается на трёх фронтах. У этой битвы продолжительная и захватывающая история, однако мы остановимся лишь на заключительном её поединке, в котором «борцы за справедливость», перед лицом абсолютно неуязвимого бота, – потерпели безоговорочное фиаско.

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

# Таргетированный руткит

Это одна из передовых методик разоблачения покерных ботов. Она, скажем так не совсем законная, поэтому когда разработчиков покерного софта спрашивают, какими механизмами для обнаружения ботов те пользуются, они отвечают что-то вроде: «Мы не разглашаем эту информацию, чтобы не давать ботоводам пищу для размышлений о создании более продвинутых ботов». Именно из-за своих «не совсем законных действий» покерный софт иногда натыкается на «ложные срабатывания» антивирусов. Однако антивирусы, принимающие покерный софт за трояна, бэкдор или руткит – не всегда так уж и далеки от истины.

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

Для обхода внешнего периметра защиты, клиентский софт, подгружающий таргетированный руткит, пользуется элементами стеганографии: «распыляет» код либо по графическим файлам, либо по невостребованным частям протокола передачи данных. Именно поэтому ботоводы, – перехватывающие трафик клиентского софта, – обнаруживают, что там всё происходит «как-то не очень стандартизовано».

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

Также стоит отметить, что таргетированный руткит, это шеллкод одноразового пользования. Поэтому два гипотетических человека, заподозренные в ботоводстве, получат два абсолютно непохожих шеллкода. Кроме того, инфицирование таргетинговым руткитом происходит не в автоматическом, а полуавтоматическом режиме, а логика его работы программируется в зависимости от того, какая информация требуется покер-руму о человеке, заподозренном в ботоводстве. Отсюда и название – «таргетированный».


Современный покерный софт имеет в себе элементы руткита, и т.о. внедряется в систему настолько глубоко, что может отслеживать любые попытки эмуляции игры. Решение заключается в том, чтобы запускать бота не на рабочей станции, с которой ведётся игра, а на отдельном компьютере – эмуляторе. Тогда, при соблюдении трёх условий параноидального характера, у покерного софта не будет шансов распознать факт эмуляции: 1) рабочая станция и эмулятор не должны иметь возможности связываться по сети; 2) для фотографирования текущего состояния игры следует использовать аналоговый выход видеокарты, – соединённый с «компьютером-скриннером» посредством платы видеозахвата; 3) для эмуляции клавиатуры и мыши следует использовать программно-аппаратную примочку, вход которой подключен к компьютеру-эмулятору, а выход – к двум PS/2 рабочей станции, с которой ведётся игра. Именно аналоговый видеовыход и именно PS/2 – чтобы покерный софт не знал, что к компьютеру подключено какое-то дополнительное оборудование.


Биометрия и технометрия


Реализуя эмуляцию клавиатуры и мыши, следует учесть соответствующие биометрические и технометрические обстоятельства, – которые также могут отслеживаться покерным софтом. Что касается биометрии, то при эмуляции клавиатуры и мыши следует позаботиться, чтобы эмулируемые телодвижения были похожи на правду. При этом следует учесть, что, во-первых, живой человек не может играть на стабильно высоком темпе в течение долгого времени сразу за 16-ю столами одновременно – в особенности на восьмидюймовом мониторе. Во-вторых, живой человек не может играть 28 часов в сутки, 6 дней в неделю. Наконец в-третьих, телодвижения живого человека могут меняться – в зависимости оттого, сколько времени он провёл за компьютером. Для эмуляции правдоподобных биометрических обстоятельств потребуется: 1) «PS/2-сниффер» – программно-аппаратная примочка, которая будет слушать поток данных, посылаемых клавиатурой и мышью на порт PS/2, и сохранять всё услышанное в отдельный файл, который в последующем будет использоваться для эмуляции правдоподобных биометрических обстоятельств; 2) живой человек, который реально поиграет в онлайн-покер столько времени, на сколько планируется включать бота – именно его действия и будет фиксировать «PS/2-сниффер».

Что касается технометрии, то у каждой клавиатуры и мыши есть свой уникальный «почерк». Его можно отследить, анализируя на низком уровне сырой поток сигналов, которые поступают от них на компьютер. Поэтому если в ботовождении участвуют сразу несколько рабочих станций, то при подготовке к биометрической эмуляции, для каждой из них следует: 1) организовать индивидуальную прослушку, – с разными клавиатурами и разными мышами, 2) посадить за каждую рабочую станцию разных людей, чьи действия будут фиксироваться «PS/2-сниффером». Необходимость второго пункта обусловлена тем фактом, что каждый человек пользуется мышью и клавиатурой индивидуальным образом. При достаточно обширном статистическом материале, можно с достаточной степенью достоверности определить, кто сидит за компьютером, – даже если пользователь не аутентифицирован.

# Технометрия по-дедовски

В далёком советском прошлом, когда в ходу были печатные машинки и в стране была жёсткая цензура, спецслужбы ставили некоторые из печатных машинок «на учёт»: фиксировали физические особенности литерных рычагов, – образно говоря, «снимали у печатных машинок отпечатки пальцев». В результате, когда появлялся какой-то «неугодный текст», КГБ могло отследить, на какой из машинок он был напечатан. Более того, уже тогда спецслужбы могли восстанавливать текст буквально из пепла. В числе прочих с их этой способностью столкнулись последователи Харе Кришна, чьи книги – в особенности «Бхагавад-гита» и «Шримад-Бхагаватам» – в советские времена были под строжайшим запретом. Поэтому в целях конспирации писатели-подпольщики, после того как рукопись переводилась в печатный текст, – не просто разрывали её на мелкие кусочки и сжигали, а разбрасывали пепел по каплям в разных местах: частично спускали в туалет, частично высыпали в одну помойку, частично в другую, третью, десятую. Трудно себе представить, какими возможностями обладают спецслужбы сейчас, когда научно-технический прогресс, по сравнению с советскими временами, ушёл далеко вперёд.


Итак, подготовка к эмуляции с правдоподобными биометрическими и технометрическими параметрами произведена. Теперь осталось запомнить, какая клавиатура с мышью, какому пользователю соответствуют – и никогда не нарушать этого соответствия. Игнорирование биометрии и технометрии или неправдоподобное их эмулирование, может привести к блокировке аккаунта без объяснения причин. При соблюдении же всех мер предосторожности, – какими бы параноидальными они не казались, – можно рассчитывать на неуязвимое ботовождение.


Элементы неуязвимого бота



Рис. 2. Неуязвимый бот

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

1. Рабочая станция (с выделенным IP или качественным PROXY), где одновременно запущено 9 покерных комнат. На ней должна быть видеокарта с аналоговым видеовыходом высокой чёткости и большой монитор, – поскольку много комнат на маленьком мониторе выглядят подозрительно. Экономически более рентабельно использовать не одну рабочую станцию – а сразу порядка десяти. В таком случае бот можно будет запустить одновременно сразу в 90 комнат.

2. Скриннер – мощный компьютер с платой видеозахвата, которая соединена через видео-сплиттер со всеми рабочими станциями. Задача скриннера – распознавать принятые сигналы и переводить их в структурный вид. Эти данные в последующем будут использованы для 1) записи в базу данных, 2) аналитики и 3) принятия решений к действию.

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

4. Шпион – отдельные аккаунты, с которых игра не ведётся, а ведётся просто последовательный сбор всей доступной информации со всех доступных комнат. Шпионов должно быть несколько, потому как если параллельно с нашей игрой один и тот же аккаунт логинится, а с её концом – разлогинивается, это может кидать планомерную монету в копилку подозрений, и когда копилка эта копилка переполнится, нам просто без объяснения причин закроют аккаунт. «Шпион» должен работать по тому же принципу, что и «рабочая станция». Может показаться, что здесь достаточно программного скрина и программной эмуляции мыши с клавиатурой. Однако, поскольку покерный софт потенциально может к железу привязаться и последующие аккаунты баннить, для шпиона следует сделать аппаратный скрин и эмуляцию – также как и для рабочей станции.

5. Эмулятор – компьютер, мощность которого не принципиальна. Он соединён с несколькими сплиттерами для мыши и клавиатуры. От этих сплиттеров идут провода на входы PS/2 для мыши и клавиатуры на каждую из рабочих станций и шпионов. Этот компьютер получает от аналитика команды и передаёт их рабочим станциям. Учитывая при этом аппаратные особенности мыши и клавиатуры, и физиологические особенности поведения человека, работающего за ними (биометрию и технометрию).

6. Два живых наблюдателя. Это уже не компьютеры, а именно живые люди. Они нужны, потому что разработчики покерного софта не дремлют, и в любой момент могут запустить какой-нибудь уникальный тест, – чтобы выявить ботоводов. Одного человека недостаточно, – поскольку он может уйти по нужде или попудрить носик, а в это время какая-то нештатная ситуация возникнет. Бывают ситуации, где всё решают секунды. Поэтому важно, чтобы рядом с машинами гарантированно кто-то всегда находился. Чтобы когда «загорится красная кнопка» (когда аналитик встретит незнакомое поведение), человек мог подсказать ему, что сделать (в чате что-то написать или окошко какое-то закрыть). Причём ни в коем случае нельзя подходить непосредственно к рабочей станции, – чтобы не сломать соответствующую ей поведенческую модель клавиатуры и мыши. Действия необходимо совершать только через «командный пункт».

7. Командный пункт – компьютер, мощность которого также не принципиальна. Он должен быть соединён только с эмулятором. Это именно та машина, у которой сидят два живых наблюдателя и через которую они при необходимости вносят необходимые корректировки в поведение бота.


Последние штрихи


Итак, это 7 элементов неуязвимого бота. При их подключении следует учесть, что скриннер, аналитик и эмулятор обмениваются данными по сети. Взаимодействие же с рабочими станциями и шпионами происходит строго через аналоговый выход видеокарты и PS/2. Причём, эти компьютеры должны быть оторваны физически ото всех остальных. Не может быть и речи связи между ними по сети. Ввиду сложности такого программно-аппаратного комплекса, важно также позаботиться о том, чтобы минимизировать ошибки связанные с человеческим фактором. Для этого эмулятор помимо всего прочего заботится о запуске всего необходимого софта на рабочих станциях и на шпионе. Скриннер, аналитик и эмулятор должны быть в боевой готовности сразу после включения – соответствующий софт просто в автозагрузке должен быть.


Новая надежда



Рис. 3. Новая надежда

Описанная схема ботовождения, даже при поверхностном ознакомлении с ней – не для слабонервных. А ведь выше приведено, хоть и подробное, но всё-таки – поверхностное её описание. Для её реализации нужно обладать не дюжей технической квалификацией, как в программировании, так и в электронике. При программировании разработчику потребуются знания из таких областей, как дискретная математика, сплайновая аппроксимация, вейвлет-преобразования, нейронные сети, конечные автоматы, нечёткая логика, многопоточное программирование, цифровая обработка сигналов. При реализации аппаратной части бота, разработчику потребуются знания из таких областей, как микропроцессорные системы, цифровая и микропроцессорная техника, работа с микроконтроллерами и ПЛИС, основы электротехники, низкоуровневое программирование драйверов, архитектура ОС, архитектура процессора.

# Кто кого перехучит

Общеизвестное вялотекущее противостояние ботоводов и их ловцов – это своеобразная игра в прятки, развивающаяся по принципу «кто кого перехучит». Боты средней продвинутости, – перехватывая соответствующие функции, которыми пользуется покерный софт, – пытаются скрыть факт фотографирования экрана, эмуляции мыши и использования запрещённых программ. А те в свою очередь, стараются обнаружить факт подобного перехвата. В процессе этой увлекательной игры, – начинающейся с функций вроде SetWindowsHookEx, CreateToolHelpSnapshot32, EnumProcessModules, – ботоводы и их ловцы закапываются всё глубже и глубже в ядро операционной системы. Такое погружение в ОС происходит ни без юмора. Например, несколько лет назад случился казус: спустя пару недель после публикации исходников одного покерного бота, PokerStars ввёл санкции для игроков, на чьих компьютерах была обнаружена среда Visual Studio.


Итак, это схема безопасного ботовождения. Как было сказано вначале, она интересна тем, что даже если «борцы за справедливость» будут иметь на руках подробно прокомментированные исходные коды бота, они не смогут установить факт его использования. Тогда как обычного бота, – без функции невидимости, – «борцы за справедливость» могут отследить, даже не имея его исходников. Современные технологии, в частности руткиты, – на которые разработчики защиты покерного софта возлагают большие надежды, – позволяют глубоко окопаться в операционной системе, и т.о. действовать по принципу «высоко сижу, далеко гляжу». С развитием руткитов и других защитных технологий, у «борцов за справедливость» появилась новая надежда на безоговорочную победу. Однако вышеприведённая схема безопасного ботовождения, сводит все эти надежды на нет. Реализация данной схемы – мероприятие дорогостоящее, однако поскольку потенциальная выгода велика, схема вполне актуальна. Так что имеет смысл дерзнуть.


Месть ситхов


Сказано – сделано. Дерзнём! Вот только стоит ли для этого тратить время на остаток статьи? Первоклассному IT-инженеру, чья принадлежность к высшей лиге X-сцены подтверждена чем-то более весомым, нежели просто красивая печать в дипломе, – определённо не стоит. В его X-арсенале уже достаточно мистических приёмов, чтобы без чьей либо посторонней помощи «отомстить» за старых ботов, которым «светлая сторона силы» некогда перекрыла кислород.

Продвинутым девелоперам, чья сопричастность с X-сценой обусловлена лишь их виртуозным владением C++ и ассемблером, время тратить также не стоит, но уже по иной причине – не потянут. Степень подробности излагаемого материала для них недостаточна, – поскольку в этой статье намеренно оставлены без внимания не только многие тривиальные задачи программирования (с ними продвинутый девелопер справляется на раз-два-три), но также и некоторые принципиальные инженерные концепции. Что же касается скрипткиддис, которые даже разговорным C++ и ассемблером не владеют – то о пользе этой статьи для них, вообще речи нет.

Однако инженер средней руки, – уже вышедший из подросткового девелоперства (развивший мало-мальски инженерную смекалку), но ещё не вступивший в пору взрослого инжиниринга, – найдёт здесь интересные идеи, которые помогут ему реализовать неуязвимого бота. Постольку поскольку он уже способен находить простые решения для кажущихся сложными задач, используя доступные ресурсы. А это именно та мистическая сила, которая ценится на X-сцене, – как на светлой, так и на тёмной её стороне. Умение владеть ею, как раз и отличает инженера от девелопера. Итак, зададим «борцам за справедливость» жару, показав, на что способна тёмная сторона силы.


Империя наносит ответный удар



Рис. 4. Империя наносит ответный удар

Хорошая новость: самое сложное уже позади. Стратегический план действий, – по совместительству являющийся неформальной постановкой задачи, – готов. Теперь его осталось только слегка конкретизировать и детализировать – описать в тактических действиях. На этапе детализации самое важное – хладнокровно пройтись «от общего к частному», и т.о. сформировать каркас будущего проекта. При этом, не вдаваясь в детали реализации конкретных функциональных узлов, а просто фиксируя их. Об их детализации позаботимся позже, – когда каркас уже будет полностью готов.

# Самая сложная программа

Хорошая новость в том, что разработка неуязвимого бота – это не самый сложный инженерный проект. Потому что самая сложная железка и самая сложная программа, которые только доводилось создавать инженеру – это его самая первая железка и самая первая программа. В связи с этим вспоминается далёкий 99-й год. Когда в 10-м классе, на самом первом уроке информатики, Рыжова Ирина Михайловна, моя первая учительница программирования, сказала: «Сейчас вы напишите самую сложную в своей жизни программу, которая будет просто складывать два числа». Это действительно была самая сложная программа, потому что на тот момент о языках программирования и об их IDE мы не имели ровным счётом никакого представления.


Для большей наглядности этапа детализации, воспользуемся синтаксисом языка Форт (не путать с Фортраном). Это уникальный язык, который естественным образом располагает к рациональному движению инженерной мысли. Прелесть такого «Форт-заимствованного» подхода в том, что все концептуальные недочёты проекта, выявляются ещё на берегу, а не в середине его реализации, – что с самого начала способствует качественной разработке. Без необходимости последующего рефакторинга. Итак, каркас «от общего к частному»:
Каркас «от общего к частному»
: КОМАНДНЫЙ-ПУНКТ
  НОВЫЙ-ТРЕД КОНСОЛЬ-УПРАВЛЕНИЯ
  НОВЫЙ-ТРЕД МОНИТОР-СОСТОЯНИЯ
  НОВЫЙ-ТРЕД ОРУДИЯ-К-БОЮ
  ЖДАТЬ-ЗАВЕРШЕНИЯ
;

: ОРУДИЯ-К-БОЮ
  НОВЫЙ-ТРЕД !!!СКРИННЕР!!!
  НОВЫЙ-ТРЕД !!!АНАЛИТИК!!!
  НОВЫЙ-ТРЕД !!!ЭМУЛЯТОР!!!
  НОВЫЙ-ТРЕД ШПИОНЫ
  НОВЫЙ-ТРЕД РАБОЧИЕ-СТАНЦИИ
;

: МОНИТОР-СОСТОЯНИЯ
  ЛОВИТЬ ФОРСМАЖОР-ВСЕХ-ТРЕДОВ
  ПЕРЕДАТЬ-НА КОНСОЛЬ-УПРАВЛЕНИЯ
  ПОВТОРЯТЬ-ДО-ЗАВЕРШЕНИЯ
;

: КОНСОЛЬ-УПРАВЛЕНИЯ
  СООБЩИТЬ-О ФОРСМАЖОР-ВСЕХ-ТРЕДОВ
  РЕАГИРОВАТЬ-НА КОМАНДЫ-ОПЕРАТОРА
  ПОВТОРЯТЬ-ДО-ЗАВЕРШЕНИЯ
;

: !!!СКРИННЕР!!!
  ДЛЯ-КАЖДОЙ-РАБОЧЕЙ-СТАНЦИИ ТЕКУЩИЙ-СКРИН
  ДЛЯ-КАЖДОГО-ШПИОНА ТЕКУЩИЙ-СКРИН
  ПОВТОРЯТЬ-ДО-ЗАВЕРШЕНИЯ
;

: ТЕКУЩИЙ-СКРИН
  ПЕРЕКЛЮЧИТЬСЯ-НА-НУЖНЫЙ-КАНАЛ
  СНЯТЬ-СКРИН-С-ПЛАТЫ-ВИДЕО-ЗАХВАТА
  РАСПОЗНАТЬ-ИГРОВОЕ-ПОЛЕ
;

: РАСПОЗНАТЬ-ИГРОВОЕ-ПОЛЕ
  ДЛЯ-КАЖДОГО-ИГРОКА РАСПОЗНАТЬ-ИГРОКА
  РАСПОЗНАТЬ-НЕЙРОСЕТЬЮ ОТКРЫТЫЕ-КАРТЫ
  РАСПОЗНАТЬ-НЕЙРОСЕТЬЮ НЕИЗМЕННОСТЬ-СТАТИКИ
  РАСПОЗНАТЬ-НЕЙРОСЕТЬЮ СООБЩЕНИЯ-ЧАТА
  РАСПОЗНАТЬ-НЕЙРОСЕТЬЮ ВСЁ-ОСТАЛЬНОЕ
  СОХРАНИТЬ-ВСЁ-В-СТРУКТУРУ
  ПЕРЕДАТЬ-НА ОРАКУЛ
;

: !!!АНАЛИТИК!!!
  ВЗЯТЬ-ИЗ-БД ТЕКУЩЕЕ-СОСТОЯНИЕ-ИГРЫ
  ВЗЯТЬ-ИЗ-БД ИСТОРИЮ-ИГРЫ
  ВЗЯТЬ-ИЗ-БД ПОХОЖИЕ-СИТУАЦИИ
  ВЗЯТЬ-ИЗ-БД КАК-РАЗВИВАЛИСЬ-СОБЫТИЯ
  ОЦЕНИТЬ-ВЫГОДУ
  ОПРЕДЕЛИТЬСЯ-С-ДЕЙСТВИЕМ
  СООБЩИТЬ-О-РЕШЕНИИ-ЭМУЛЯТОРУ
  ПОВТОРЯТЬ-ДО-ЗАВЕРШЕНИЯ
;

: !!!ЭМУЛЯТОР!!!
  ПЕРЕКЛЮЧИТЬСЯ-НА-НУЖНЫЙ-КАНАЛ
  ПРИНЯТЬ-КОМАНДУ-АНАЛИТИКА
  ЭМУЛИРОВАТЬ-МЫШЬ
  ЭМУЛИРОВАТЬ-КЛАВИАТУРУ
  ПОВТОРЯТЬ-ДО-ЗАВЕРШЕНИЯ
;

: !!!ШПИОН!!!
  ПОДСТРОИТЬ-РАБОЧЕЕ-МЕСТО
  СООБЩИТЬ-О-ГОТОВНОСТИ-СКРИННЕРУ
;

: !!!РАБОЧАЯ-СТАНЦИЯ!!!
  ПОДСТРОИТЬ-РАБОЧЕЕ-МЕСТО
  СООБЩИТЬ-О-ГОТОВНОСТИ-СКРИННЕРУ
;

: ПОДСТРОИТЬ-РАБОЧЕЕ-МЕСТО
  ЗАПУСТИТЬ-ПОКЕРНЫЙ-КЛИЕНТ
  ЗАЙТИ-В-ДЕВЯТЬ-КОМНАТ
  СМЕНИТЬ-КОМНАТУ-ПРИ-НЕОБХОДИМОСТИ
;


Ещё одно преимущество использования синтаксиса языка Форт при составлении общего каркаса проекта – это возможность сразу же скомпилировать написанный текст. Потому что этот текст уже сам по себе является программой. В принципе, если реализовать все недостающие функциональные узлы средствами языка Форт и добавить их к вышеприведённому каркасу, то получится тот самый неуязвимый бот, над которым мы трудимся. Получится весьма лаконично. Тем не менее, приведённые ниже фрагменты кода, будут написаны на более традиционных языках – C++ и ассемблере. «Форт-заимствованное» представление в данном случае интересно лишь для наглядности общего каркаса проекта. Итак, «хладнокровно от общего к частному» прошлись, теперь рассмотрим подробнее детали реализации некоторых функциональных узлов бота.


Невидимое фотографирование


Здесь у нас три принципиальные задачи: 1) найти приемлемое аппаратное решение, 2) разработать структуру данных, которые будут считываться с фотографий, 3) реализовать функцию распознавания. Результатом этапа фотографирования должна стать полная и неизбыточная структура данных, опираясь на которую автомат логики сможет однозначно идентифицировать текущее состояние игры – за как можно меньшее число подряд идущих снимков.

1. Что касается аппаратного решения, то идеальный вариант (с экономической точки зрения и с точки зрения простоты последующего программирования) – это чтобы все видеосигналы сходились на один видео-сплиттер, который можно было бы переключать с компьютера. В таком случае задачу «скриннера» сможет выполнять один компьютер с одной платой видеозахвата. Для каждого шпиона и рабочей станции в этом случае надо будет завести по одной папке, – туда будут сохраняться соответствующие скрины. Если этот вариант реализовать не получается (в силу недоступности соответствующего оборудования и/или в силу недостаточной технической квалификации для адаптации существующего оборудования под собственные нужды), то можно использовать несколько компьютеров, объединённых в локальную сеть, в каждом из которых будет по несколько плат видео-захвата – по одной на каждого «шпиона» и «рабочую станцию». В таком случае нужно будет либо в общую сетевую папку все отскриненные битмапы складывать, либо на сокетах клиент-серверное приложение написать, которое будет все битмапы в одном месте собирать. Для загрузки битмапов, с целью их последующего распознавания, можно использовать следующий код:
Загрузка битмапов
void LoadRamaFromFile(LPCSTR lpcszFileName, HDC *lphdcRama)
{
  HANDLE hBitmap = LoadImage(0, lpcszFileName, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
  *lphdcRama = CreateCompatibleDC(NULL); // Подготовить контекст для снимка
  SelectObject(*lphdcRama, hBitmap);   // Поместить снимок в контекст
  DeleteObject(hBitmap);         // Освободить память
}


2. Что касается структуры данных, то она может быть такой:
Структура данных
// Раунды
typedef enum ROUND
{
  RES_WAITINGBLINDS, // ожидание блайндов (служебный раунд)
  RES_FLOPHIDDEN,  // начался флоп, но карты ещё не вскрыты ()
  PREFLOP,      // префлоп
  FLOP,       // флоп
  TURN,       // терн
  RIVER,       // ривер
  UNKNOWN,      // неизвестный
  ZSTATE
} *LPROUND;

// Возможные действия игроков                      //
typedef enum ACTION
{
  AC_POSTSB,   // оплатил малый блайнд
  AC_POSTBB,   // оплатил большой блайнд
  AC_POSTSBANDBB,
  AC_CHECK,    // передал право первого слова
  AC_BET,     // сделал первую ставку
  AC_RAISE,    // повысил предыдущую ставку
  AC_CALL,    // принял текущую ставку
  AC_FOLD,    // сбросил карты
  AC_ALLIN,    // поставил всё
  AC_SITOUT,   // не заплатил блайнд и пропустил руку или не сделал ход во время
  AC_MUCK,    // скрыл свои карты
  AC_SHOWCARDS,  // показал свои карты
  AC_NONE,    // пустой кругляк, но игрок в игре (карты при нём)
  AC_TNB,     // терм недоставляющий беспокойства (место свободно)
  AC_ZSTATE
} *LPACTION;

// Информация по одному игроку
typedef struct tagSITA_UVACHA
{
  char  szNickname[STR_SIZE_NICKNAME];  // Псевдоним
  char  szStack[STR_SIZE_STACK];     // Размер стека
  char  szHoleCards[STR_SIZE_HOLECARDS]; // Какие карты на руках (если известны)
  ACTION  action;
} SITA_UVACHA, *LPSITA_UVACHA;

// Информация по всей игре
typedef struct tagRAMA_UVACHA
{
  SITA_UVACHA  sita[MAX_COUNT_SITA];   // Информация по всем игрокам
  DWORD  dwCountSita;           // Количество игроков за столом
  DWORD  dwBUPos;             // Позиция баттона
  char  szPOT[STR_SIZE_POT];       // размер банка
  char  szBoardCards[STR_SIZE_HOLECARDS]; // карты, лежащие на игровом столе
  ROUND  round;              // текущий раунд
} RAMA_UVACHA, *LPRAMA_UVACHA;

////////////////////////////

typedef struct tagKRISHNA_UVACHA
{
  RAMA_UVACHA  rama;
  DWORD    dwFirstSaid;    // первый новый говорящий на текущем снимке
  DWORD    dwLastSaid;     // последний новый говорящий на текущем снимке
  ACTION    last_action;    // последнее действие на текущем снимке
} KRISHNA_UVACHA, *LPKRISHNA_UVACHA;


3. Один из возможных вариантов функции распознавания, – настроить для этого нейронную сеть. Данная тема заслуживает отдельного разговора, поэтому о нейросетевом подходе в этой статье мы говорить не будем. Более простой вариант – привязаться к соответствующим битмапам. Вот как может выглядеть соответствующая структура данных:

Структура данных для распознавания
typedef struct tagSURJA_CRAPH_DATA
{
  BYTE  PATTERN_LETTER[COUNT_LETTERS][SIZE_PATTERN_LETTER];       // рисунки символов
  BYTE  LETTER_CODE[COUNT_LETTERS];                   // коды символов
  BYTE  LETTER_SIZE[COUNT_LETTERS];                   // ширина символов

  BYTE  PATTERN_INSCRIPT[COUNT_INSCRIPTIONS][SIZE_PATTERN_INSCRIPTION]; // рисунки действий игроков
  char  INSCRIPTION_TEXT[COUNT_INSCRIPTIONS][SIZE_INSCRIPTION_TEXT];  // действия игроков
  BYTE  PATTERN_CARD[COUNT_CARDS][SIZE_PATTERN_CARD];          // рисунки карт
  char  CARD_TEXT[COUNT_CARDS][SIZE_CARD_TEXT];             // перечень карт
  BYTE  PATTERN_HOLEHIDDEN[COUNT_HOLEHIDDEN][SIZE_PATTERN_HOLEHIDDEN]; // рисунки скрытых ручных карт
  char  HOLEHIDDEN_TEXT[COUNT_HOLEHIDDEN][SIZE_HOLEHIDDEN_TEXT];    // текстовые обозначения скрытых ручных карт
} SURJA_CRAPH_DATA, *LPSURJA_GRAPH_DATA;


Самый простой способ её заполнения – ручной. Просто делаем снимок экрана, открываем его в фотошопе, активизируем инструмент «пипетка» и переписываем цифры с интересующих нас областей. Лаконичным это решение не назовёшь даже с большой натяжкой. Кроме того, оно имеет существенное ограничение, – даже самые незначительные изменения в графике покерного софта приведут к тому, что бот перестанет работать. Однако если техническая квалификация не позволяет придумать что-то более стоящее, то вот как может выглядеть подготовка к «ручному распознаванию» (здесь приведена только небольшая выборка из кода почти в 2000 строк):
Ручное распознавание
BOOL LoadGraphData()
{
//===================== Сначала обнуляем все шаблоны ================
  memset(m_BramaGraphData.ptRamaCoords,  0,  sizeof(POINT)*COUNT_VARRIOUS_RAMA*COUNT_RAMA_VALUES);
  memset(m_BramaGraphData.ptSitaCoords,  0,  sizeof(POINT)*COUNT_VARRIOUS_RAMA*MAX_COUNT_SITA*COUNT_SITA_VALUES);
  memset(m_BramaGraphData.ptRecognizeSize,0,  sizeof(POINT)*COUNT_VAL_SIZE);
//===================== Параметры 9-стульчатого стола ===============
  m_BramaGraphData.ptRamaCoords[INDEX_RAMA_AT_9_SITA][INDEX_VAL_RAMA_POT].x  = 210;
  m_BramaGraphData.ptRamaCoords[INDEX_RAMA_AT_9_SITA][INDEX_VAL_RAMA_POT].y  = 34;
  m_BramaGraphData.ptSitaCoords[INDEX_RAMA_AT_9_SITA][0][INDEX_VAL_SITA_NICKNAME].x    = 340;
  m_BramaGraphData.ptSitaCoords[INDEX_RAMA_AT_9_SITA][0][INDEX_VAL_SITA_NICKNAME].y    = 44;
  m_BramaGraphData.ptSitaCoords[INDEX_RAMA_AT_9_SITA][1][INDEX_VAL_SITA_NICKNAME].x    = 423;
  m_BramaGraphData.ptSitaCoords[INDEX_RAMA_AT_9_SITA][1][INDEX_VAL_SITA_NICKNAME].y    = 77;
  m_BramaGraphData.ptSitaCoords[INDEX_RAMA_AT_9_SITA][2][INDEX_VAL_SITA_INSCRIPTION].x  = 438;
  m_BramaGraphData.ptSitaCoords[INDEX_RAMA_AT_9_SITA][2][INDEX_VAL_SITA_INSCRIPTION].y  = 165;
//===================== Параметры 10-стульчатого стола ==============
//===================== Шаблоны доступных символов ==================
  m_BramaGraphData.PATTERN_LETTER[PAT_0][0] = b01111000;
  m_BramaGraphData.PATTERN_LETTER[PAT_0][1] = b10000100;
  m_BramaGraphData.PATTERN_LETTER[PAT_8][2] = b10100100;
  m_BramaGraphData.PATTERN_LETTER[PAT_8][3] = b01011000;
  m_BramaGraphData.LETTER_CODE[PAT_0] = '0';
  m_BramaGraphData.LETTER_CODE[PAT_1] = '1';
  m_BramaGraphData.LETTER_CODE[PAT_2] = '2';
//===================== Шаблоны надписей в кругляках ================
  m_BramaGraphData.PATTERN_INSCRIPT[PAT_INSCRIPTION_SEATOPEN][0]  = 55;
  m_BramaGraphData.PATTERN_INSCRIPT[PAT_INSCRIPTION_SEATOPEN][1]  = 56;
  m_BramaGraphData.PATTERN_INSCRIPT[PAT_INSCRIPTION_SEATOPEN][2]  = 124;
  m_BramaGraphData.PATTERN_INSCRIPT[PAT_INSCRIPTION_SEATOPEN][3]  = 215;
//===================== Текст надписей в кругляках ==================
  lstrcpy(m_BramaGraphData.INSCRIPTION_TEXT[PAT_INSCRIPTION_SEATOPEN],  "SEATOPEN");
  lstrcpy(m_BramaGraphData.INSCRIPTION_TEXT[PAT_INSCRIPTION_EMPTYSEAT],  "EMPTYSEAT");
  lstrcpy(m_BramaGraphData.INSCRIPTION_TEXT[PAT_INSCRIPTION_CALL],    "CALL");
  lstrcpy(m_BramaGraphData.INSCRIPTION_TEXT[PAT_INSCRIPTION_BET],      "BET");
//===================== Шаблоны открытых карт =======================
  m_BramaGraphData.PATTERN_CARD[PAT_CARD_EMPTY][0] = 30;
  m_BramaGraphData.PATTERN_CARD[PAT_CARD_EMPTY][1] = 30;
  m_BramaGraphData.PATTERN_CARD[PAT_CARD_EMPTY][2] = 30;
  m_BramaGraphData.PATTERN_CARD[PAT_CARD_HIDE][6] = 188;
  m_BramaGraphData.PATTERN_CARD[PAT_CARD_HIDE][7] = 151;
//===================== Текстовые обозначения карт ==================
  lstrcpy(m_BramaGraphData.CARD_TEXT[PAT_CARD_EMPTY],  "");
  lstrcpy(m_BramaGraphData.CARD_TEXT[PAT_CARD_HIDE],  "hid");
  lstrcpy(m_BramaGraphData.CARD_TEXT[PAT_CARD_2C],  "2C");
  lstrcpy(m_BramaGraphData.CARD_TEXT[PAT_CARD_4H],  "4H");
//===================== Шаблоны скрытых ручных карт =================
//===================== Текстовые обозначения скрытых ручных карт ===
  lstrcpy(m_BramaGraphData.HOLEHIDDEN_TEXT[PAT_HOLEHIDDEN_EMPTY],  "");
  lstrcpy(m_BramaGraphData.HOLEHIDDEN_TEXT[PAT_HOLEHIDDEN_ONE],  "one-hidden");
  lstrcpy(m_BramaGraphData.HOLEHIDDEN_TEXT[PAT_HOLEHIDDEN_TWO],  "hidden");
//===================== Единичные шаблоны ===========================
  m_BramaGraphData.PATTERN_MISC[PAT_MISC_BUTTON][0] = 216;
  m_BramaGraphData.PATTERN_MISC[PAT_MISC_BUTTON][1] = 72;
  m_BramaGraphData.PATTERN_MISC[PAT_MISC_BUTTON][2] = 8;
  m_BramaGraphData.PATTERN_MISC[PAT_MISC_BUTTON][3] = 151;
  m_BramaGraphData.PATTERN_MISC[PAT_MISC_BUTTON][4] = 221;
  m_BramaGraphData.PATTERN_MISC[PAT_MISC_BUTTON][5] = 194;
  m_BramaGraphData.PATTERN_MISC[PAT_MISC_BUTTON][6] = 231;

  return TRUE;
}


А вот пример кода, который использует такие битмапы для распознавания:
Пример распознающего кода
typedef struct tagSURJA_CRAPH_DATA
{
  BYTE  PATTERN_LETTER[COUNT_LETTERS][SIZE_PATTERN_LETTER];       // рисунки символов
  BYTE  LETTER_CODE[COUNT_LETTERS];                   // коды символов
  BYTE  LETTER_SIZE[COUNT_LETTERS];                   // ширина символов

  BYTE  PATTERN_INSCRIPT[COUNT_INSCRIPTIONS][SIZE_PATTERN_INSCRIPTION]; // рисунки действий игроков
  char  INSCRIPTION_TEXT[COUNT_INSCRIPTIONS][SIZE_INSCRIPTION_TEXT];  // действия игроков
  BYTE  PATTERN_CARD[COUNT_CARDS][SIZE_PATTERN_CARD];          // рисунки карт
  char  CARD_TEXT[COUNT_CARDS][SIZE_CARD_TEXT];             // перечень карт
  BYTE  PATTERN_HOLEHIDDEN[COUNT_HOLEHIDDEN][SIZE_PATTERN_HOLEHIDDEN]; // рисунки скрытых ручных карт
  char  HOLEHIDDEN_TEXT[COUNT_HOLEHIDDEN][SIZE_HOLEHIDDEN_TEXT];    // текстовые обозначения скрытых ручных карт
} SURJA_CRAPH_DATA, *LPSURJA_GRAPH_DATA;



Так или иначе, – хоть нейросетью мы пользуемся, хоть руками, хоть чем-то третьим, – при распознавании следует учитывать четыре принципиально разных области: 1) статические (те, где по теории никогда ничего не происходит); в т.ч. внешние по отношению к покерному софту (например, вообще вне клиента покера возникает какое-то действие, якобы от системы Windows, а по сути – на ботость проверка); 2) динамические сильно-информативные (размер стека, текущий игрок, карты на столе и т.д.); 3) динамические слабо-информативные (места, где редко сообщения появляются и чат); 4) динамические неинформативные (например, области, в которых анимация раздачи карт происходит).


Невидимый анализ


Аналитическая часть бота, в случае покера, это механизм взаимодействия с базой данных, – поскольку практически все «теллсы», актуальные для онлайн-покера, опираются на анализ поведенческой модели игровых оппонентов. Идеальный вариант здесь – это свой собственный домашний Оракул, который незаменим в интенсивной работе с крупной БД, включающей в себя сотни тысяч записей. А именно столько их и будет, если вы планируете заниматься ботоводством всерьёз и надолго. Если же бот вам нужен просто, чтобы побаловаться, то можно не тратить деньги на Оракула. Вполне подойдёт и СУБД MS Access, – с ней можно легко из своей программы связываться, например посредством ODBC. Вот как это может выглядеть:
Взаимодействие с MS Access
BOOL InitODBC()
{
  SQLRETURN    ret;
  SQLSMALLINT    Length;

  ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_HANDLE_NULL, &hEnv);
  if (SQL_SUCCESS != ret && SQL_SUCCESS_WITH_INFO != ret)
    return FALSE;

  ret = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER) SQL_OV_ODBC3, NULL);
  if (SQL_SUCCESS != ret && SQL_SUCCESS_WITH_INFO != ret)
    return FALSE;

  ret = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hConn);
  if (SQL_SUCCESS != ret && SQL_SUCCESS_WITH_INFO != ret)
    return FALSE;

  lstrcpyn(szConnect, STR_CONNECT, MAXSIZE_CONNECTSTR);
  ret = SQLDriverConnect(hConn, NULL, (SQLTCHAR *) szConnect, lstrlen(szConnect), (SQLTCHAR *) szConnect, MAXSIZE_CONNECTSTR, &Length, SQL_DRIVER_COMPLETE);
  if (SQL_SUCCESS != ret && SQL_SUCCESS_WITH_INFO != ret)
    return FALSE;

  return TRUE;
}

void DoneODBC()
{
  SQLDisconnect(hConn);
  SQLFreeHandle(SQL_HANDLE_DBC, hConn);
  SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
}



BOOL InsertInDB(LPCSTR lpcszText, LPCSTR lpcszTitle, DWORD dwLocale)
{
  SQLHANDLE  hStmt;
  SQLRETURN  ret;
  char    szTitle[SIZE_TITLE];
  char    szQuery[MAXSIZE_QUERYSTR];


  // Открыть запрос        //
  ret = SQLAllocHandle(SQL_HANDLE_STMT, hConn, &hStmt);
  if (SQL_SUCCESS != ret && SQL_SUCCESS_WITH_INFO != ret)
    return FALSE;

  wsprintf(szQuery, "INSERT INTO index (text, title, locale) VALUES ('%s', '%s', %d)", lpcszText, szTitle, dwLocale);

  ret = SQLExecDirect(hStmt, (SQLTCHAR *) szQuery, lstrlen(szQuery));

  if (SQL_SUCCESS != ret && SQL_SUCCESS_WITH_INFO != ret)
    return FALSE;
  SQLFreeHandle(SQL_HANDLE_STMT, hStmt);

  return TRUE;
}


Если же речь идёт не о покере, а например о шахматах, то в качестве аналитической части может быть реализован обмен со сторонним шахматным движком. Например, вот как можно взаимодействовать с Фрицем:
Взаимодействие с аналитическим движком
BOOL SendData2Fritz(char szFritz[100])
// Скармливает ход фрицу //
{
  if (!FritzStr2Clipboard(szFritz))
    return FALSE;
  SendMessage(m_hwndFritz, WM_COMMAND, MAKELONG(IDM_PASTE_POSITION, 0), 0); 
  Sleep(2);
  SendMessage(m_hwndFritz, WM_COMMAND, MAKELONG(IDM_MOVE_NOW, 0), 0); 

  return TRUE;
}


BOOL ReceiveDataFromFritz(int busy[8][8], LPBOOL lpbEndGame)
// Получает ответ от фрица //
{
  char szFritz[100];

  *lpbEndGame = FALSE;

  SendMessage(m_hwndFritz, WM_COMMAND, MAKELONG(IDM_COPY_POSITION, 0), 0);
  if (!Clipboard2FritzStr(szFritz))
    return FALSE;
  return TRUE;
}

BOOL FritzStr2Clipboard(LPCSTR lpcszFritz)
// Передаёт подготовленную для фрица строку в буфер обмена //
{
  HGLOBAL hGlobalMemory;    // Кусок глобальной памяти
  LPVOID pGlobalMemory;     // Указатель на строку

  hGlobalMemory = GlobalAlloc(GHND, lstrlen(lpcszFritz)+1);
  if (hGlobalMemory == NULL)
    return FALSE;
  pGlobalMemory = GlobalLock(hGlobalMemory);
  lstrcpy((LPSTR) pGlobalMemory, lpcszFritz);
  GlobalUnlock(hGlobalMemory);

  if (!OpenClipboard(NULL))
    return FALSE;
  if (!EmptyClipboard())
    return FALSE;
  SetClipboardData(CF_TEXT, hGlobalMemory);
  CloseClipboard();

  return TRUE;
}

BOOL Clipboard2FritzStr(LPSTR lpszFritz)
// Извлекает из буфера обмена переданную фрицем строку //
{
  HANDLE hClipMemory;    // Хендл буфера обмена
  LPVOID pClipMemory;    // Указатель на строку буфера обмена

  if (IsClipboardFormatAvailable(CF_TEXT))
  {
    if (!OpenClipboard(NULL))
      return FALSE;
    hClipMemory = GetClipboardData(CF_TEXT);
    if (hClipMemory == NULL)
      return FALSE;
    pClipMemory = GlobalLock(hClipMemory);
    lstrcpyn(lpszFritz, (LPSTR) pClipMemory, 100);
    GlobalUnlock(hClipMemory);
    CloseClipboard();
  }
  return TRUE;
}



Ещё один инструмент, который может пригодиться при программировании аналитики – т.н. «конечные автоматы». Вот как они могут быть использованы в многопользовательских онлайн-играх (это фрагмент бота для игры «Хранители силы»):
Пример использования конечных автоматов
typedef enum AUTOMATA_BATLE
{
  // Служебные сосотояния
  AS_BATLE_BEGIN,
  AS_BATLE_END,
  AS_BATLE_ERROR,

  AS_BATLE_TEST2MOB,     // определить, есть ли здесь мобы
  AS_BATLE_WAIT2NICK,    // ожидает появления своего ника
  AS_BATLE_WAIT2MOB,     // ожидает появления моба
  AS_BATLE_PROCESS,     // жмёт в центр, пока бой не закончен
  AS_BATLE_CLICK2OK_1,    // жмёт OK
  AS_BATLE_WAITCHANGE,    // ждёт окно выбора
  AS_BATLE_CLICK2MONEY,   // нажать на кнопку "деньги"
  AS_BATLE_TEST2DROP,    // смотрим есть ли дроп (по кнопке "закрыть")
  AS_BATLE_CLICK2DROP,    // нажать на кнопку "дроп"
  AS_BATLE_CLICK2CLOSE,   // нажать на кнопку "закрыть"
  AS_BATLE_WAIT2DROP,    // дождаться загрузки окна с дропом
  AS_BATLE_CLICK2TAKEALL,  // нажать "забрать всё"
  AS_BATLE_WAIT2CONFIRM,   // дождаться подтвержения
  AS_BATLE_CLICK2OK_2,    // жмёт ОК
  AS_BATLE_WAIT2BACK,    // дождаться кнопки вернуться
  AS_BATLE_CLICK2BACK,    // нажать "вернуться"
  AS_BATLE_WAIT2STARTWINDOW, // дождаться появления начального окна
  AS_BATLE_WAIT2NEXT     // подождать 30 секунд
};



AUTOMATA_BATLE CAutomataBatle::GoStep(HDC hdc)
{
  char szMessage[255];

  switch (m_automata_batle)
  {
  // Сбор урожая      //
  case AS_BATLE_BEGIN:
    m_automata_batle = AS_BATLE_WAIT2STARTWINDOW;
    SetWindowText(m_hwndControl, "ожидаю загрузки игры");
    break;
  case AS_BATLE_WAIT2STARTWINDOW:
    if (IsStartPresent(hdc))
    {
      SetWindowText(m_hwndControl, "ищу моба");
      m_automata_batle = AS_BATLE_TEST2MOB;
    }
    break;
  case AS_BATLE_TEST2MOB:
    if (IsMechPresent(hdc))
      if (!ClickMenuItem(INDEX_GOBATLE))
        return AS_BATLE_ERROR;
    if (IsCellEmpty(hdc))
    {
      SetWindowText(m_hwndControl, "здесь нет мобов");
      m_automata_batle = AS_BATLE_END;
    }
    else
    {
      hwndBatle = NULL;
      EnumChildWindows(m_hwndMain, EnumWindowsProcBatle, NULL);

      if (NULL != hwndBatle)
      {
        SetWindowText(m_hwndControl, "ожидаю появления себя");
        m_automata_batle = AS_BATLE_WAIT2NICK;
      }
    }
    break;
  case AS_BATLE_WAIT2NICK:
    if (IsNickPresent(hdc))
    {
      SetWindowText(m_hwndControl, "ожидаю появления моба");
      m_automata_batle = AS_BATLE_WAIT2MOB;
    }
    break;
  case AS_BATLE_WAIT2MOB:
    if (!IsNickOpEmpty(hdc))
    {
      SetWindowText(m_hwndControl, "веду битву");
      m_automata_batle = AS_BATLE_PROCESS;
    }
    break;
  case AS_BATLE_PROCESS:
    if (!IsBatleEnd(hdc))
    {
      if (!ClickMenuItem2Window(hwndBatle, INDEX_STRIKE))
        return AS_BATLE_ERROR;
    }
    else
    {
      SetWindowText(m_hwndControl, "подтверждаю конец битвы");
      m_automata_batle = AS_BATLE_CLICK2OK_1;
    }
    break;
  case AS_BATLE_CLICK2OK_1:
    if (!ClickMenuItem2Window(hwndBatle, INDEX_OK2BATLEEND))
      return AS_BATLE_ERROR;
    SetWindowText(m_hwndControl, "ожидаю окно выбора");
    m_automata_batle = AS_BATLE_WAITCHANGE;
    break;
  case AS_BATLE_WAITCHANGE:
    if (IsChangePresent(hdc))
    {
      SetWindowText(m_hwndControl, "выбираю деньги");
      m_automata_batle = AS_BATLE_CLICK2MONEY;
    }
    break;
  case AS_BATLE_CLICK2MONEY:
    if (!ClickMenuItem(INDEX_MONEY))
      return AS_BATLE_ERROR;
      SetWindowText(m_hwndControl, "жду кнопку");
    m_automata_batle = AS_BATLE_TEST2DROP;
    break;
  case AS_BATLE_TEST2DROP:
    if (IsButtonDropPresent(hdc))
    {
      SetWindowText(m_hwndControl, "жму кнопку \"дроп\"");
      m_automata_batle = AS_BATLE_CLICK2DROP;
    }
    else if (IsButtonClosePresent(hdc))
    {
      SetWindowText(m_hwndControl, "дропа нет, жму кнопку \"закрыть\"");
      m_automata_batle = AS_BATLE_CLICK2CLOSE;
    }
    break;
  case AS_BATLE_CLICK2DROP:
    if (IsButtonDropPresent(hdc))
    {
      if (!ClickMenuItem(INDEX_DROP))
        return AS_BATLE_ERROR;
    }
    else
    if (IsDropPresent(hdc))
    {
      SetWindowText(m_hwndControl, "ожидаю окно дропа");
      m_automata_batle = AS_BATLE_WAIT2DROP;
    }
    else if (IsStartPresent(hdc))
      m_automata_batle = AS_BATLE_WAIT2STARTWINDOW;
    break;
  case AS_BATLE_CLICK2CLOSE:
    if (!ClickMenuItem(INDEX_CLOSE))
      return AS_BATLE_ERROR;
    if (IsStartPresent(hdc))
    {
      SetWindowText(m_hwndControl, "отдыхаю перед боем");
      m_dwWait = 0;
      m_automata_batle = AS_BATLE_WAIT2NEXT;
    }
    break;
  case AS_BATLE_WAIT2DROP:
    if (IsDropPresent(hdc))
    {
      SetWindowText(m_hwndControl, "собираю весь дроп");
      m_automata_batle = AS_BATLE_CLICK2TAKEALL;
    }
    break;
  case AS_BATLE_CLICK2TAKEALL:
    if (!ClickMenuItem(INDEX_TAKEALL))
      return AS_BATLE_ERROR;
    SetWindowText(m_hwndControl, "ожидаю окна подтверждения");
    m_automata_batle = AS_BATLE_WAIT2CONFIRM;
    break;
  case AS_BATLE_WAIT2CONFIRM:
    if (IsConfirmPresent(hdc))
    {
      SetWindowText(m_hwndControl, "подтверждаю");
      m_automata_batle = AS_BATLE_CLICK2OK_2;
    }
    break;
  case AS_BATLE_CLICK2OK_2:
    if (!ClickMenuItem(INDEX_OK2TAKEALL))
      return AS_BATLE_ERROR;
    if (!IsConfirmPresent(hdc))
    {
      SetWindowText(m_hwndControl, "жду кнопку \"вернуться\"");
      m_automata_batle = AS_BATLE_WAIT2BACK;
    }
    break;
  case AS_BATLE_WAIT2BACK:
    if (IsBackPresent(hdc))
    {
      SetWindowText(m_hwndControl, "жму кнопку \"вернуться\"");
      m_automata_batle = AS_BATLE_CLICK2BACK;
    }
    break;
  case AS_BATLE_CLICK2BACK:
    if (!ClickMenuItem(INDEX_BACK))
      return AS_BATLE_ERROR;
    if (IsStartPresent(hdc))
    {
      SetWindowText(m_hwndControl, "отдыхаю перед боем");
      m_dwWait = 0;
      m_automata_batle = AS_BATLE_WAIT2NEXT;
    }
    break;
  case AS_BATLE_WAIT2NEXT:
    if (m_dwWait >= WAIT_2NEXTBATTLE)
    {
      SetWindowText(m_hwndControl, "ожидаю обновления окна");
      m_automata_batle = AS_BATLE_WAIT2STARTWINDOW;
    }
    else
    {
      wsprintf(szMessage, "(%d из %d) жду...", m_dwWait/2, WAIT_2NEXTBATTLE/2);
      SetWindowText(m_hwndControl, szMessage);
      m_dwWait++;
    }
    break;
  case AS_BATLE_END:
    m_automata_batle = AS_BATLE_BEGIN;
    break;
  }




Невидимая эмуляция


Здесь два этапа – подготовительный и эксплуатационный. И тот и другой подразумевают не только написание соответствующих программ, но также и работу с железом. Здесь без погружения в микроконтроллеры уже не обойдёшься. Благо, что велосипед изобретать не придётся – есть готовые программно-аппаратные решения, которые без труда можно найти в Интернете. В том числе с исходными текстами программы для микроконтроллера, которую легко можно адаптировать под свои нужды.

Что касается подготовительного этапа (записи многочасовой работы с мышью и клавиатурой), то здесь при желании можно обойтись без аппаратной примочки. Можно написать драйвер-фильтр, фиксирующий обращения к IOCTL_INTERNAL_I8042_HOOK_KEYBOARD. За основу можно взять программу ctrl2cap, исходные коды которой доступны в DDK. Однако это решение не универсально – лучше всё же аппаратный сниффер использовать.

Что касается эксплуатационного этапа, то здесь без аппаратной примочки уже точно не обойтись. Одним из возможных решений может быть связь с микроконтроллерным устройством (которое по совместительству ещё и сплиттером будет) с одной стороны через RS-232, а с другой – через букет PS/2. Для работы с RS-232 есть два варианта – можно либо штатными средствами Windows воспользоваться и через CreateFile/ReadFile/WriteFile с портом RS-232 общаться, либо – непосредственно через порты ввода/вывода. Во втором случае, надо будет написать простенький драйвер, который разблокирует доступ к портам ввода/вывода, – поскольку по умолчанию в Windows доступ к ним закрыт. От греха подальше. Если вам таки удалось получить доступ к портам ввода/вывода, то вот код для обмена данными по RS-232:
Код для обмена данными по RS-232
#define dataport  0x02F8   // Базовый адрес COM2
#define irport   0x02F9   // Регистр прерываний и старший байт делителя частоты
#define manager   0x02FB   // Регистр управления
#define statline  0x02FD   // Регистр статуса
#define upr1    0x00BB      
#define upr2    0x003B
#define freq    0x000C   // Код делителя частоты (9600 бод)

DWORD OpenPort()
/* Открывает COM-порт */
{
  __asm
  {
    mov  eax, upr1    // Установить регистры 0 и 1 в режим приёма делителя
    mov  edx, manager
    out  dx, al

    mov  eax, freq    // Читаем код делителя частоты
    mov  edx, dataport
    out  dx, al     // Загружаем младший байт делителя в регистр данных

    mov  al, ah
    mov  edx, irport
    out  dx, al     // Загружаем старший байт делителя в регистр прерываний

    mov  eax, upr2
    mov  edx, manager
    out  dx, al

    mov  al, 0      // Запрещаем прерывания
    mov  edx, irport
    out  dx, al
  }
}

DWORD WriteByte(BYTE data)
/* Передаёт байт через COM-порт */
{
  __asm
  {
    mov  al, data    // Читать символ из переменной
    mov  edx, dataport
    out  dx, al     // Передать его в регистр данных
waitout:
    mov  edx, statline
    in   al, dx     // Читаем регистр статуса
    mov  ah, al
    and  al, 0x40    // Проверяем, пуст ли буфер передатчика
    jz   waitout     // Если нет – ждём
  }
} // void WriteByte

BOOL ReadByte(BYTE data)
/* Принимает байт с COM-порта */
{
  int cntWait = 0;

  __asm
  {
    mov  edx, statline
    xor  ecx, ecx
waitread:
    add  cntWait, 1
    cmp  cntWait, 0xFFFF
    jz   error
    in   al, dx     // Читаем регистр статуса
    mov  bl, al     // Проверяем, пришли ли данные
    and  eax, 1
    jz   waitread    // Если не пришли – ждём дальше
    mov  edx, dataport  // Если пришли – читаем регистр данных
    xor  eax, eax
    in   al, dx     // Загружаем принятый байт в переменную
    mov  data, al
error:
  }
  if (cntWait != 0xFFFF)
    return TRUE;
  else
    return FALSE;
} // BYTE ReadByte


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

Одно из возможных решений включает в себя два элемента: 1) настроить нейронную сеть, 2) для корректировки перемещения и обучения нейронной сети работать в паре со «скриннером». При таком подходе можно обучать нейронную сеть с минимальным участием человека. Алгоритм обучений нейронной сети может быть таким:
  1. Выбираем случайные координаты, куда надо мышь переместить.
  2. Подаём их вместе с текущими координатами на нейронную сеть.
  3. Смотрим, насколько нейронная сеть ошиблась.
  4. Подстраиваем нейронную сеть.
  5. Повторяем 1-4 до победного. Учитывая при этом данные, полученные от аппаратного сниффера, – чтобы нейронная сеть не просто точно перемещала курсор мыши, но ещё и биометрически правдоподобно это делала.



Что дальше


Реализация данной нейросетевой схемы, – как и нейросетевой схемы распознавания скринов, о которой упоминалось выше, – тема отдельного разговора, выходящая за рамки данной статьи. Эти две задачи, вышедшие за рамки статьи, – те самые «принципиальные инженерные концепции», которые «инженеру средней руки» предлагается решить самостоятельно. Что же касается «тривиальных задач программирования», то реализация большей их части также не вошла в данную статью. Те 10 листингов, которые приведены в статье – это лишь затравка, призванная помочь нагулять инженерный аппетит.

Ещё одно важное замечание. Пользуясь данной статьёй при разработке своего собственного бота, следует понимать: когда для реализации того или иного функционального узла предлагаются несколько альтернативных вариантов (идеальный и несколько компромиссных), то вопрос выбора – риторический. Подразумевается, что будет использован именно идеальный вариант. Если же идти на компромиссы, пусть даже на один, то бот будет уязвим для контратак «светлой стороны силы».


Возвращение джедая



Рис. 5. Возвращение джедая

Итак, выше описана схема неуязвимого ботовождения, которая перед лицом «борцов за справедливость» – претендует на окончательную и бесповоротную победу. Однако было бы скучно, если бы у джедаев «светлой стороны силы» не осталось возможности что-либо противопоставить ботоводам. Более того, если бы такой возможности не было, автор, – старающийся придерживаться «светлой стороны силы», – не стал бы публиковать эту схему. Кроме того, хочется верить, что значительная часть читателей также придерживается «светлой стороне силы» и их интерес – не в злоупотреблении данной схемой, а в том, как противостоять ей. В статье не приводятся подробные исходные коды и опущены некоторые принципиальные инженерные концепции – именно для того, чтобы избежать злоупотребления.

Несколько слов о том, как можно противостоять описанной схеме ботоводства. Она практически неуязвима. Однако теоретическая возможность её обнаружения – остаётся. Эта возможность связана с отслеживанием нетипичного для мыши поведения, которое может проскальзывать, даже в случае качественной эмуляции. Дело в том, что мышь несёт на своём хвосте очень много «биометрических теллсов», – выражаясь покерным языком. Она ближе всего находится к человеку. Малейшее движение мышц, вызванное сменой эмоционального фона, – всё это отражается на том, как мы держим мышь в руках.

У автора статьи в связи с этим есть оригинальное бионическое исследование, под рабочим названием «Интегральная архитектура Homo Sapiens», включающее в себя описание физиологии и психики человека – в моделях, пригодных для IT-эксплуатации. Именно благодаря положительным результатам данного исследования, я чувствую моральное право обнародовать схему «неуязвимого» ботовождения, – т.к. у «светлой стороны силы» остаётся шанс на победу. Из этого исследования в числе прочего можно понять, как наше пользование мышью, посредством интерфейса спинного мозга, связано с нашей умственной деятельностью. В какой-то степени это позволяет, образно говоря, читать мысли человека, который положил руку на мышь. Именно в этом «чтении мыслей» и видится возможный вектор контратак «борцов за справедливость».

Т.о. битва между тёмной и светлой сторонами силы – продолжается. Одни пытаются претвориться людьми, а другие следят за нетипичным поведением мыши. Создать виртуозный эмулятор мыши – всё равно, что создать джедайский световой меч. А обнаружить факт виртуозной эмуляции – всё равно, что отразить удар этого меча другим световым мечом. Так кто же окажется сильнее?


Пробуждение силы



Рис. 6. Пробуждение силы

Так или иначе, чью бы сторону мы не занимали – за ботов или против них – нам необходимо совершенствовать технику владения световым мечом. В смысле развивать у себя мышцу инженерной смекалки. Главная её составляющая – это способность концентрироваться на задаче, быть «здесь и сейчас». По сути, это йогическое совершенство – самадхи. Самадхи в переводе с санскрита означает – полная концентрация на одном предмете. Полная концентрация! Если вы собираете линзой солнечный пучок, то поверхность, на которую вы его направили – зажигается. Точно также в инжиниринге. Всю имеющуюся у нас энергию, мы должны сконцентрировать как линза, и использовать её в инженерном творчестве. Но не используйте её энергию во вред, – подобно детям, которые берут линзу и начинают муравьёв жечь. Творческая энергия должна быть использована во благо.

# Код #кода

Давным-давно, – то было ещё в прошлом тысячелетии, – когда IDE были синими, а директивы препроцессора зелёными, в уме одного из пионеров программирования всплыл следующий код: «#define QUESTION bb || !bb». А чуть ранее ту же самую мысль в словах «быть или не быть – вот в чём вопрос», выразил Шекспировский Гамлет. Но ещё намного раньше – 5000 лет назад, – та же самая мысль всплыла в уме Вьясадевы, правда на санскрите: атхато брахма джигьяса, – что переводится как «пришло время вопрошать об Абсолютной Истине». Этой мыслью Вьясадев открыл свой непревзойдённый философский труд, «Веданта-сутру», которая чуть позже была откомментирована им же на страницах «Шримад-Бхагаватам». Но в самом начале, на заре творения материального мира, было только одно слово, и слово это было у Бога, и слово это было Бог. И слово это было ОМ. Подобно тому, как вся цифровая электроника является оперением «стрелки пирса», выпущенной из лука Чарльза Пирса в 1880-м году, также и весь материальный космос вырос из этого слова ОМ, источником которого является Бог. Причём очень лаконично вырос – ни без полиморфизма и объектно-ориентированного программирования, о чём подробно можно почитать на страницах «Шримад-Бхагаватам», который соотносится со звуком ОМ примерно так же, как современные высокоуровневые языки программирования соотносятся с ассемблером (подробнее см. «Код #кода»).


Основа концентрации – это духовный стержень, для заточки которого можно воспользоваться таким высокотехнологичным гаджетом, как «книга». Хорошая духовная книга, конечно же. «Книга» – это последнее слово техники: 1) открывается лёгким движением руки, 2) оснащена немерцающим экраном, 3) имеет вечный заряд батареи. Для автора статьи – это «Шримад-Бхагаватам» (строго запрещённый в советские времена). О нём можно почитать в статье «Миссия Бхагаватам».

Кроме того, для концентрации, как это ни банально, нужен здоровый тонус тела и духа. Поднять тонус можно разными способами. Для автора данной статьи, например, это вегетарианская диета (ведическая кухня), спортивная форма (кмс по лыжным гонкам) и мантра-медитация (повторение Харе Кришна, Харе Кришна, Кришна Кришна, Харе Харе / Харе Рама, Харе Рама, Рама Рама, Харе Харе). Об этой мантре можно почитать в статье «Принцип действия Харе Кришна мантры». Можете воспользоваться моим опытом, или же что-то своё использовать. И да прибудет с нами сила.

Комментарии (7)


  1. gmixo
    21.01.2016 13:33

    хочется верить в чудо — что это возможно, но как то не верится… не на много проще чем собрать киборга для игры в покер


  1. AlexTest
    21.01.2016 14:30
    +5

    Я нигде не встречал запрета на установку и использование покерного клиента в виртуальной ОС. Сам иногда играю «по копеечке» на покерстарс из винды запущеной из виртуалбокса в линуксе (т.к. сам не доверяю покерному софту) и никто меня не банил за это до сих пор. А раз так, то сделать бота можно намного проще, учитывая что покерный софт не сможет вылезти за пределы виртуальной ОС и отследить бота работающего в т.н. «наружной» ОС.


  1. mayorovp
    21.01.2016 15:54

    Интересно, ну вот как руткит определит, что к компьютеру «подключено какое-то лишнее оборудование», если устройство-переходник будет выдавать себя за самые обычные мышь с клавиатурой?


  1. tmg
    21.01.2016 16:12

    ну тоже неочем борьба,
    + играем в виртуалке, снимаем весь экран на хосте с ботом
    + играем на ноуте, снимаем экран — вебкамерой, подключеной к обычному компу с ботом
    + играем на компутере, стримим на youtube, с ютуба берeм трансляцию в AWS с ботом

    никто не запрещает иметь неск мыше на компе, а уж найти на github драйвер bot -> network -> HID mouse не составит труда


  1. Ohar
    21.01.2016 16:48

    Большое спасибо за статью.
    Сам недавно открыл для себя огромную область игрового ботописания, когда писал простенький скрипт для взлома кодовых замков в Rust (не дописал, т. к. не смог научить бота поворачиваться без обращений к игровой памяти).


  1. grekmipt
    21.01.2016 18:04
    +3

    1. Как уже отметили выше, виртуальную машину никто не отменял. И поэтому 90% описанного в статье — это из серии «создать себе проблему чтобы затем упорно её решать».

    2. В части трекинга и анализа движений мышки/нажатий клавиатуры/реагирования на вопросы в чате и прочие неожиданности, в случае наличия действительно прибыльного бота, есть намного более брутальное решение — посадить китайца за 100$ в месяц, который будет ручками долбать по кнопкам исходя из рекомендаций бота. И никто его никогда не спалит.

    3. Даже если забанили акк — открываем другой, и продолжаем, достаточно решить вопрос бана по ID-шникам железа. Т.е. в целом «вечного» акка то и не требуется для практических целей.

    3. Ну и наконец, в корне неверен основной посыл статьи, что дескать игра против бота — это заведомо игра в одни ворота. В реальности, маскировка бота это самое простое что связано с темой ботоводства. А вот создание «мозгов» бота, которые обеспечили бы ему статистическое преимущество над полем игроков — это действительно крайне нетривиальная задача, скорее уже из области искусства чем инженерии. И обычно эта задача (написания годного прибыльного «мозга» бота), особенно если говорить не про микролимиты, отнимает у людей годы. Ибо тут и теорвер, и нейронные сети/нечеткая логика, и сложные фильтрации данных, и т.п. и т.д., при этом необходимо уметь кодить высоконагруженные системы с распараллеливанием расчетов ибо их тьма, а всё это непотребство усугубляется тем что тестировать любой конкретный вариант бота можно только на реальном поле — что занимает несколько месяцев и кучу бабок если бот сливается.
    А за эти годы (написания и тюнинга настроек бота) поле игры очень сильно меняется, так что ботовод это человек который «постоянно бежит чтобы оставаться на месте», стоит чуть расслабиться (перестать тюнинговать и адаптировать логику бота под поле) — и бот начинает сливать, причем именно сливать живым людям. Так что читающийся между строк статьи посыл что дескать совершил «подвиг» (сделав крутую маскировку бота) и дальше легко рубишь баблос — это, мягко говоря, неправда Т.к. поддерживать бота в плюсе это постоянная работа, прекратив делать которую — быстро потеряешь бота (в смысле профитности), и эта работа из года в год становится только тяжелее (по мере того как становится жестче само поле игры — ибо реги не дремлют и постоянно повышают свои скилы).

    Итого: хотя в статье и есть дельные мысли, но в целом статья скорее не соответствует реальности, чем соответствует.


  1. covaxi
    25.01.2016 17:18
    +1

    Бред какой-то. А кабель, по которому всю эту шалаберду к сети подключаете, да и саму комнату экранировать надо? Спутники шпионы там, машины гугл, ездящие по улицам и ловящие незащищенных покерных ботов.