Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #121, ЯНВАРЬ 2009 г.

Карты, деньги, компилятор. Пишем крутой бот для игры в интернет-покер

Николай Байбородин (baiborodin@gmail.com)




Aaloha, брат! Позади чумная череда новогодних праздников. Ты, как и я, наверняка, находишься на мели. В такое голодное время любая денежная идея ценится дороже золота. В плане денежных идей в условиях кризиса наш журнал традиционно находится на гребне волны, поэтому встречай крутое решение - online-геймблинг.

Вся наша жизнь - игра

Что такое? Ты противник азартных игр и это не для тебя? Понимаю, я тоже считаю людей, подсевших на гамблинг, гм… мягко говоря, неудачниками. Но когда, совсем недавно, я случайно познакомился с покером, – то с удивлением узнал, что между покером и той же рулеткой пролегает огромная пропасть. Если в рулетке или в BlackJack от тебя ничего не зависит, то в покере победа или поражение зависят в первую очередь от твоих же действий. Покер просчитывается! И это, в отличие от BlackJack, разрешено и поощряется. Можно сказать, что покер – игра статистики и терпения. Более того, во многих странах покер – официально признанный вид спорта. Вот еще одна приятная новость: в списке таких стран значится и Россия, в которой официально зарегистрирована Федерация Спортивного Покера.
Но спорт спортом, а все-таки в основе этой игры лежит финансовый интерес. И, собственно, цель игры в том и заключается, чтобы собрать как можно больший банк. Для этого можно использовать самые разнообразные стратегии, которые доступны любому желающему. Их описание ты без труда найдешь в Сети. Все эти стратегии основаны на расчетах вероятности того или иного расклада и математического ожидания суммы выигрыша… или проигрыша.

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

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

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

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

Стоп! А кто сказал, что играть обязательно должен человек? Если игра просчитывается, если есть готовые стратегии, – что мешает перевести их в нули и единицы и заставить компьютер играть в покер за тебя? Вот та самая мысль, которая и послужила основой для написания статьи.

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

План (не тот, другой)

Затем в моей голове всплыли рассуждения Криса Касперски, что как бы ни наворачивали защиту своего клиента разработчики WebMoney, система все равно остается уязвимой, ведь можно элементарно эмулировать взаимодействие пользователя с графическим интерфейсом. Чем не вариант? Будем пробовать.

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

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

Неуправляемое управление

В качестве теоретического минимума будем осваивать технику DLL-инъекции. А поскольку .Net безгранично царствует на просторах Windows, сделаем это с оглядкой на управляемый код в .Net сборках.

Допустим, у тебя есть некий управляемый код, который ты хотел бы поместить в процесс–жертву. Для большей интриги – пусть процесс-жертва будет организован в виде нативного неуправляемого кода. Оформим его в виде обычной библиотеки классов .Net. Для тех, кто обделен фантазией, ниже приведен архисложный пример:

namespace MyNamespace
{
public class MyClass
{
// Этот метод мы вызовем из процесса-жертвы
public static int MyMethod(String pwzArgument)
{
MessageBox.Show("Hello World");
return 0;
}

}
}

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

Итак, у нас есть процесс – жертва и наш, готовый к внедрению, код. Следующий шаг – написать загрузчик библиотеки. Внимание! Мы не будем ломиться вперед сломя голову и делать инъекцию управляемого кода в неуправляемый. Вместо этого мы выполним инъекцию нативной DLL'ки, которая в свою очередь способна взаимодействовать со средой .Net и загружать наш управляемый код.

Дело за малым - побряцать немного по клаве и написать простенькую DLL-ку:

#include "MSCorEE.h"

void StartTheDotNetRuntime()
{
// Подключаем CLR
ICLRRuntimeHost *pClrHost = NULL;
HRESULT hr = CorBindToRuntimeEx(
NULL, L"wks", 0, CLSID_CLRRuntimeHost,
IID_ICLRRuntimeHost, (PVOID*)&pClrHost);

hr = pClrHost->Start();

// Теперь можно вызвать метод .Net библиотеки
DWORD dwRet = 0;
hr = pClrHost->ExecuteInDefaultAppDomain(
L"c:\\PathToYourManagedAssembly\\
MyManagedAssembly.dll",
L"MyNamespace.MyClass", L"MyMethod",
L"MyParameter", &dwRet);

// Выгружаем CLR
hr = pClrHost->Stop();

// Убираем за собой
pClrHost->Release();
}

В основе нашего загрузчика лежит последовательный вызов определенных методов CLR API.

  1. CorBindToRuntimeEx – возвращает указатель на интерфейс ICLRRuntimeHost.
  2. ICLRRuntimeHost::Start – позволяет запустить CLR Runtime на выполнение или подключиться к уже запущенному экземпляру.
  3. ICLRRuntimeHost::ExecuteInDefaultAppDomain – позволяет загрузить в CLR указанную библиотеку с управляемым кодом.

Именно последний из перечисленных методов и выполняет основную работу, ради которой все, собственно говоря, и затеивалось. Думаю, с этим полезным методом тебе уже не раз приходилось иметь дело. Но на всякий случай, для тех, кто впервые взял в руки наш журнал, я напомню, что ExecuteInDefaultAppDomain принимает обязательный строковый параметр, а в качестве своего значения возвращает число типа integer. Гы, а ты думал, что я от нефиг делать в первом примере использовал метод со строковым параметром и целочисленным возвращаемым результатом?

Вызов метода ExecuteInDefaultAppDomain будет успешно работать в большинстве приложений. Но есть одно исключение, которое тебе обязательно нужно знать. Дело в том, что если целевой процесс также является частью .Net-приложения, придется воспользоваться альтернативными методами, которые мы не будем здесь рассматривать в силу ограниченности журнального пространства. Хочешь подробностей – кури MSDN.

Давай подведем промежуточные итоги. Что мы имеем на текущий момент? У нас есть программный код, выполняющий основную работу, есть DLL, осуществляющая его инициализацию. Осталось выполнить инъекцию этой самой библиотеки в процесс клиента выбранного заранее покер-рума:

Сестра, шприц!

Займемся DLL-инъекцией, которая поможет нам порулить клиентом покер-рума. «Почему именно техника DLL Injection?», – спросишь ты. Все просто. Софт для покер-румов создают не пионеры, а серьезные дядьки. Поэтому любая модификация клиента моментально палится сервером, и твоя учетная запись будет забанена без малейшей перспективы получить обратно свое бабло. А если нельзя сделать легкий тюнинг клиента заранее, стало быть, будем тюнинговаться на лету. Для этой цели DLL Injection – то, что доктор прописал.

Технике DLL Injection не один год. Она широко известна в наших узких кругах и неплохо документирована на многих специализированных веб-ресурсах (несколько интересных ссылок ты найдешь на полях). Поэтому рассмотрим лишь самую суть, имеющую отношение к созданию бота для игры в покер.

Вообще, DLL Injection – достаточно обширное понятие, под которым подразумевается несколько родственных технологий. Самая, пожалуй, популярная – внедрение в адресное пространство чужого процесса – неоднократно описывалась на страницах нашего журнала. Правда, это больше относится к соседней рубрике «Взлом», мы же люди мирные, нам бы бабла поднять. Поэтому будем осваивать менее воинственные техники – Windows Hook и CBT Hook. Покурив MSDN на тему Windows Hook, ты обнаружишь, что в основе техники лежит метод SetWindowHookEx из Windows API. Ниже – демонстрация того, как мы можем использовать это для своих грязных целей:

LRESULT CALLBACK PokerBotCBTProc(int nCode,
WPARAM wParam, LPARAM lParam)
{
if (nCode < 0)
{
return CallNextHookEx(g_hHook, nCode,
wParam, lParam);
}
else if (theInjector.getVenue() !=
Venue_Unknown)
{
if (g_bFirstTime)
{
theInjector.inject();
bFirstTime = false;
}

if (nCode == HCBT_ACTIVATE)
return (LRESULT) theInjector.HandleIt
(Hook_Activate, (HWND)wParam);
else if (nCode == HCBT_CREATEWND)
return (LRESULT) theInjector.
HandleIt(Hook_Create, (HWND)wParam);
else if (nCode == HCBT_DESTROYWND)
return theInjector.HandleIt(Hook_Destroy,
(HWND)wParam);
}
return 0;
}
bool OPCHOOK_API InstallHooks()
{
g_hHook = SetWindowsHookEx(WH_CBT, (HOOKPROC)
AutoCBTProc, hInstance, 0);
return g_hHook != NULL;
}

С использованием глобальных CBT-хуков есть одна досадная проблема (это справедливо и для других глобальных хуков – на то он и глобальный, чтобы никому сладко не было). DLL в этом случае будет загружена в адресное пространство ВСЕХ процессов. И если эта библиотека весит более чем «Hello, World!» (а так оно и будет после того, как ты напишешь логику своего бота), приличные тормоза системе обеспечены. Поэтому мы с тобой поступим следующим образом: напишем легкую библиотечку, которая будет выполнять только одно действие – проверять, в адресном пространстве какого процесса она находится. И если этот процесс принадлежит клиенту online покер-рума, то – подгружать основную библиотеку, зарабатывающую бабло.

Закатаем рукава

Довольно лирики, пора браться за дело. Начинаем с DLL Injection:

bool XPOKERBOTHOOK_API InstallHook()
{
g_hHook = SetWindowsHookEx(WH_CBT,
(HOOKPROC) CBTProc, g_hInstance, 0);

return g_hHook != NULL;
}

Следующий шаг – создание механизма обнаружения открытия и закрытия интересующих нас окон клиента покер-рума:

LRESULT CALLBACK CBTProc(int nCode, WPARAM
wParam, LPARAM lParam)
{
if (nCode < 0)
return CallNextHookEx(g_hHook, nCode,
wParam, lParam);
else if (!g_pClient)
return 0;
HWND hWnd = (HWND)wParam;
if (!hWnd)
return 0;

if (nCode == HCBT_ACTIVATE)
{
if (!g_pClient->IsRegisteredWindow(hWnd))
g_pClient->TryRegisterWindow(hWnd, NULL);
}
else if (nCode == HCBT_DESTROYWND)
{
if (g_pClient->IsRegisteredWindow(hWnd))
g_pClient->UnregisterWindow(hWnd);
}
return 0;
}

И – пара комментариев. В момент активации главного окна приложения посылается сообщение HCBT_ACTIVATE. Соответственно, в момент закрытия окна посылается сообщение HCBT_DESTROYWND. Остальной код достаточно прост для понимания. Он просто-напросто получает контроль над окном или освобождает его от своей опеки. Да, чуть не забыл. Мы не можем использовать стандартное сообщение HCBT_CREATEWND для ответа на вопрос о том, создано ли уже окно клиента. Поскольку к процессу клиента мы еще не подключились, то не можем и захватить управление окном. А значит, HCBT_CREATEWND здесь не подходит. Вместо этого мы воспользуемся сообщением HCBT_ACTIVATE для поиска активного окна.

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

PokerTimeTableWindow::PokerTimeTableWindow
(HWND hWnd, PokerTimePokerClient* client) :
OnlineTableWindow(hWnd, client)
{
HWND hwndChat = ::FindWindowEx(hWnd, NULL,
_T("RichEdit20W"), NULL);
if (hwndChat)
{
PokerTimeTableWindow::OldRichWndProc = (WNDPROC)::GetWindowLongPtr(hwndChat,
GWL_WNDPROC);
::SetWindowLongPtr(hwndChat, GWL_WNDPROC, (LONG_PTR)PokerTimeTableWindow::MyRichWndProc);
}
}

Теперь, док, давай научимся взаимодействовать с окном чата. Здесь конкретные действия будут зависеть от того, на основе какого элемента управления оно реализовано. В частности, клиент для такого покер-рума, как PokerTime, использует для этого компонент RichEdit. Для отображения текста в окне чата используется стандартное сообщение EM_STREAMIN, которое мы и будем перехватывать:

LRESULT PokerTimeTableWindow::MyRichWndProc
(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
EDITSTREAM* es = (EDITSTREAM*) lParam;
if (msg == EM_STREAMIN)
{
PokerTimeTableWindow::OldRichEditCB
= es->pfnCallback;
es->pfnCallback = PokerTimeTableWindow
::MyEditStreamCallback;
PokerTimeTableWindow::CurrentChatWindow = hWnd;
}
LRESULT lRet = ::CallWindowProc(PokerTimeTableWindow
::OldRichWndProc, hWnd, msg, wParam, lParam);
if (msg == EM_STREAMIN)
{
es->pfnCallback = PokerTimeTableWindow::OldRichEditCB;
}
return lRet;
}

Если тебе уже приходилось раньше сталкиваться с EM_STRAEMING, то ты должен помнить, что вместе с ней необходимо использовать callback-функцию EDITSTREAMCALLBACK. Нам нужно заменить исходную callback-функцию (А) своей собственной (В). Ну а мы уже передадим управление функции А после того, как сделаем свои темные делишки, чтобы клиент не потерял связи с реальностью и не выпал в глубокую нирвану :).

PokerTimeTableWindow::OldRichEditCB =
es->pfnCallback;
es->pfnCallback = PokerTimeTableWindow
::MyEditStreamCallback;

Финальный аккорд

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

  • «Dealing Hole Cards (Ah, Ad)» (нам раздали карманные карты и можно начинать анализ игровой ситуации);
  • «Meowt, you have 10 seconds to respond» (наш ход, пора действовать).

Регулярные выражения – твой лучший друг:

DWORD CALLBACK PokerTimeTableWindow
::MyEditStreamCallback(DWORD_PTR dwCookie,
LPBYTE pbBuff, LONG numberOfBytes, LONG* actualBytes)
{
DWORD dwRet = PokerTimeTableWindow
::OldRichEditCB(dwCookie, pbBuff,
numberOfBytes, actualBytes);
if (0 == dwRet && actualBytes &&
*actualBytes > 0)
{
boost::smatch what;
if( boost::regex_match(line,
what, regHoleCards, boost::match_default|boost
::match_single_line) && what.size() == 3)
{
string sCard1 = what[1];
string sCard2 = what[2];
ApplicationProxy::TransmitHoleCards
((sCard1 + sCard2).c_str(), hPokerTable);
}

else if (boost::regex_match(line, what, regWakeUp,
boost::match_default|boost::match_single_line)
&& what.size() == 3)
{
string theActor = what[1];
if (theActor == g_pClient->LoggedInAs)
OnlinePokerExecutor::PerformAction(hPokerTable);
}

Обработаем полученные данные и симулируем действия пользователя. В первом нет ничего сложного, второе – тоже из серии «для чайников», поэтому отдельно останавливаться на этих моментах мы не будем. Если вдруг возникнут трудности – грызи исходники, которые есть на диске.

Ахтунг!

Так, чтобы все гладко и без единого сучка, бывает только в кино. В реальной жизни за каждым углом нас поджидает лохматый писец. Не хочу капать на мозг (он и так изрядно загажен нытьем о всемирном экономическом кризисе, раздающимся из каждой подворотни), но ты это должен знать. Иначе было бы просто не честно.
Дело в том, что все без исключения покер-румы прилагают максимум усилий к тому, чтобы пресечь на корню разведение покер-ботов. Этим занимаются отдельные люди, именуемые аналитиками. Основной их инструмент – анализ историй игровых сессий. Главное палево, выдающее бота с потрохами – шаблонность и предсказуемость игры. Человек может ошибиться, войти в азарт и отступиться от любой системы, какой бы он ни придерживался. Бот – никогда. У него железные, точнее, кремниевые нервы.

Отсюда – несколько маленьких финальных советов:

  1. Не жадничай – много мелких банков привлекут меньше внимания, чем несколько крупных.
  2. Стабильный плюс – повод внимательнее приглядеться к игроку. Поэтому позволь своему боту иногда проигрывать.
  3. Как в той рекламе («Теплее, Виктор, еще теплее…»), добавь боту немного человечности. Пусть он время от времени удивляется своим удачам или огорчается проигрышам, отпуская соответствующие замечания в окне чата.

Удачи в игре, гринго!

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

CD

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

WARNING

Мы настоятельно рекомендуем тебе не использовать полученные из статьи знания на практике. В противном случае ни автор, ни редакция не несут ответственности за твои действия. Будь умницей :).

WWW

Содержание