Как стать автором
Обновить

Читы на CS:GO? А как же VAC?

Здравствуйте, меня зовут Борис и я играю с читами. О том, что это и как работает в онлайн играх — подробнее под катом.

image

Итак, что такое «чит»? «Чит» (от английского cheat) — это программа, которая позволяет упростить игру, предоставляя игроку дополнительные возможности или информацию во время игрового процесса. Читы бывают разных видов — от встроенных игровых команд (как правило, только в режимах single, к ним относятся всем известные коды iddqd, impulse 101, god и прочие) до программ, изменяющих память в заранее найденных областях, к ним относятся все трейнеры и такие инструменты как ArtMoney (уже забыт и не развивается) и Cheat Engine.

В Multiplayer`е конечно же никто не позволит воспользоваться каким-то кодом на бессмертие и бесконечные патроны, всё это по умолчанию отключено, а Cheat Engine не сможет взломать сервер, всё же он работает локально. Но что тогда можно сделать?

В памяти игры всегда хранится информация об игровом процессе, важно понять, какая информация нам интересна и может помочь, а какая бесполезна или и так уже есть на экране. В таких шутерах, как Counter Strike, важно знать, где находится враг: два точных выстрела решают исход игры. Но разве так можно? Можно!

В памяти клиента всегда хранятся структуры, описывающие игроков, находящихся в игре на данный момент. Это справедливо для союзников и не всегда для противников. Обо всем по порядку.

В движке Source есть два типа структур — LocalPlayerEntity (сам игрок) и GeneralPlayerEntity (другие игроки), описываются они примерно так:

	struct tLocalPlayerEntity
	{
		byte Flags;//флаги положения, в основном описывает состояние прыжка
		int TeamNumber;//команда
		int CrosshairEntityIndex;//индекс в массиве прицелов
		int Index;//индекс в массиве всех игроков
		D3DXVECTOR3 Origin;//положение в координатах XYZ
		D3DXVECTOR3 PunchAngle;//угол направления передвижения
		D3DXVECTOR3 ViewOffset;//смещение взгляда
		D3DXVECTOR3 ViewAngle;//угол взгляда
		D3DXVECTOR3 Velocity;//скорость перемещения
	};	

Но нам про себя не очень интересно, как там поживают все остальные на карте?

struct tGeneralPlayerEntityInfo
	{
		int TeamNumber;//команда
		int Health;//состояние здоровья
		D3DXVECTOR3 Origin;//положение в координатах XYZ
		D3DXVECTOR3 Velocity;//скорость перемещения
		int Kills;//сколько убил
		int Deaths;//сколько умер
		int HasC4;//несет C4?
		int Armor;//состояние брони
		bool HasDefuser;//есть ли набор разминирования?
		bool Dormant;//активен ли в настоящий момент, а точнее жив или мертв
		byte Flags;//то же самое, про состояние прыжка
	};

Это всё очень занимательно, и на самом деле эти структуры несколько больше, но как их получить, а тем более отобразить на экране игрока?

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

— С помощью Cheat Engine находим адрес нужных нам переменных и структур, вычисляем их постоянное смещение
— Во время работы игры постоянно считываем значения переменных по смещениям
— На основе полученных данных, представляем игроку информацию обо всем, что можем

О каждом пункте можно говорить долго, сейчас я покажу как выполнить второй, а для первого и второго нужны отдельные темы.

Итак, предположим, что кто-то за нас уже нашел все нужные смещения, а точнее местонахождение двух заветных структур, что нужно сделать нам?

1. Находим процесс игры в памяти, а точнее получаем HANDLE:

HANDLE GetProcessHandleByName(const std::wstring &ProcessName)
{
    PROCESSENTRY32 ProcessEntry; //Структура, хранящая данные о процессе
    ProcessEntry.dwSize = sizeof(PROCESSENTRY32);
    HANDLE SnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);//Получаем снимок всех процессов в системе

    if (Process32First(SnapHandle, &ProcessEntry) == TRUE) //Получаем первый процесс из списка
        {
            if (!_wcsicmp(ProcessEntry.szExeFile, ProcessName.c_str())) //Сравниваем с нужным именем
                {
                    HANDLE ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessEntry.th32ProcessID);
                    CloseHandle(SnapHandle);
                    return ProcessHandle; //если оно, получаем права на процесс и возвращаем хендл
                }
            else //в противном случае идем до конца списка, пока не найдем нужный процесс
                {
                    while (Process32Next(SnapHandle, &ProcessEntry) == TRUE)
                        {
                            if (!_wcsicmp(ProcessEntry.szExeFile, ProcessName.c_str()))
                                {
                                    HANDLE ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessEntry.th32ProcessID);
                                    CloseHandle(SnapHandle);
                                    return ProcessHandle;
                                }
                        }
                }
        }	
    CloseHandle(SnapHandle);
    return nullptr ; //увы, игры нет, вернем нулевой указатель.
}

Хендл позволяет прочитать любую информацию внутри процесса, а также список его модулей. В Source описанные выше структуры создаются в модуле client.dll, давайте получим его адрес:

2. Получаем адрес модуля:

DWORD GetModuleAddressByName(const std::wstring &ModuleName, HANDLE Process)
{
    MODULEENTRY32 ModuleEntry;
    ModuleEntry.dwSize = sizeof(MODULEENTRY32);
    HANDLE SnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetProcessId(Process));
    if (Module32First(SnapHandle, &ModuleEntry) == TRUE)
        {
            if (!_wcsicmp(ModuleEntry.szModule, ModuleName.c_str()))
                {
	                auto ModuleAddress = reinterpret_cast<DWORD>(ModuleEntry.modBaseAddr);
                    return ModuleAddress;
                }
            else
                {
                    while (Module32Next(SnapHandle, &ModuleEntry) == TRUE)
                        {
                            if (!_wcsicmp(ModuleEntry.szModule, ModuleName.c_str()))
                                {
	                                auto ModuleAddress = reinterpret_cast<DWORD>(ModuleEntry.modBaseAddr);
                                    return ModuleAddress;
                                }
                        }
                }
        }
    CloseHandle(SnapHandle);
    return NULL;
}

Работает это аналогично функции выше, так что описывать не буду.

Всё что мы получили дает нам стартовую базу для дальнейших действий. Чтобы что-то узнать, нужно об этом прочитать, давайте прочтем информацию из процесса:

3. Чтение переменных по адресу:

template <typename ReadType> void Read(HANDLE ProcessHandle, ReadType* Buffer, DWORD Address)
		{
			ReadProcessMemory(ProcessHandle, reinterpret_cast<LPVOID>(Address), Buffer, sizeof(ReadType), nullptr);
		}

WinAPI функция ReadProcessMemory позволяет получить в буфер любой отрезок памяти процесса по заданному адресу, указанного размера. Достаточно просто и удобно, а данная обертка в виде шаблона избавит нас от необходимости задавать каждый раз считываемый тип. Функция несколько изменена для понятности, откуда берется хендл, в последствии его будем хранить как глобальную переменную, чтобы не передавать сотни раз в функцию одно и тоже значение, и обязательно даем обещание только читать его, не перезаписывать!

Осталось немного, просто взять то, что хранится в нашей оперативной памяти, а значит по праву принадлежит нам!

4. Считывание структуры из памяти:

void tLocalPlayerEntity::tLocalPlayerEntityFunctions::GetLocalPlayerEntityInfo(tLocalPlayerEntityInfo* LocalPlayerEntityInfo)
{
	if (LocalPlayerEntityInfo)//получаем заготовку с адресом для чтения
	{
	LocalPlayerEntityInfo->Valid = false;//заранее говорим, что эта структура еще неверна, и может не оказаться верной
		if (pIO.ClientModuleBaseAddress)//если у нас нет адреса client.dll, значит мы еще не присоединились, читать нечего
		{
			pIO.Functions.Read<DWORD>(&LocalPlayerEntityInfo->BaseAddress, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.m_dwLocalPlayer);//читаем базовый адрес
			if (LocalPlayerEntityInfo->BaseAddress && pIO.EngineModuleBaseAddress)
			{
// чтобы не гонять много раз системные функции, считаем один раз огромный кусок данных, а из него локально возьмем необходимое. DataChunk - массив из 5000 байтов, заполним его нулями, чтобы данные были гарантированно чистыми
				ZeroMemory(&LocalPlayerEntityInfo->DataChunk, LocalPlayerEntityInfo->Sizes.Data);
				pIO.Functions.Read<tDataStructs::tDataChunk>(&LocalPlayerEntityInfo->DataChunk, LocalPlayerEntityInfo->BaseAddress);
				pIO.Functions.Read<DWORD>(&LocalPlayerEntityInfo->ClientStateBaseAddress, pIO.EngineModuleBaseAddress + pGlobalVars.Offsets.m_dwClientState);
				if (LocalPlayerEntityInfo->ClientStateBaseAddress)
				{				
					//считываем то, что осталось за бортом 
					pIO.Functions.Read<int>(&LocalPlayerEntityInfo->Index, LocalPlayerEntityInfo->ClientStateBaseAddress + pGlobalVars.Offsets.m_dwLocalPlayerIndex);
					pIO.Functions.Read<D3DXVECTOR3>(&LocalPlayerEntityInfo->ViewAngle, LocalPlayerEntityInfo->ClientStateBaseAddress + pGlobalVars.Offsets.m_dwViewAngle);
					pIO.Functions.Read<tDataStructs::tViewMatrix>(&LocalPlayerEntityInfo->ViewMatrix, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.m_dwViewMatrix);

					//А всё остальное берем из полученного массива.
					memmove_s(&LocalPlayerEntityInfo->Flags, (LocalPlayerEntityInfo->Sizes.Flags), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_fFlags], (LocalPlayerEntityInfo->Sizes.Flags));
					memmove_s(&LocalPlayerEntityInfo->CrosshairEntityIndex, (LocalPlayerEntityInfo->Sizes.CrosshairEntityIndex), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_iCrossHairID], (LocalPlayerEntityInfo->Sizes.CrosshairEntityIndex));
					memmove_s(&LocalPlayerEntityInfo->LifeState, (LocalPlayerEntityInfo->Sizes.LifeState), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_lifeState], (LocalPlayerEntityInfo->Sizes.LifeState));
					memmove_s(&LocalPlayerEntityInfo->Origin, (LocalPlayerEntityInfo->Sizes.Origin), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_vecOrigin], (LocalPlayerEntityInfo->Sizes.Origin));
					memmove_s(&LocalPlayerEntityInfo->PunchAngle, (LocalPlayerEntityInfo->Sizes.PunchAngle), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_vecPunch], (LocalPlayerEntityInfo->Sizes.PunchAngle));
					memmove_s(&LocalPlayerEntityInfo->ShotsFired, (LocalPlayerEntityInfo->Sizes.ShotsFired), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_iShotsFired], (LocalPlayerEntityInfo->Sizes.ShotsFired));
					memmove_s(&LocalPlayerEntityInfo->TeamNumber, (LocalPlayerEntityInfo->Sizes.TeamNumber), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_iTeamNum], (LocalPlayerEntityInfo->Sizes.TeamNumber));
					memmove_s(&LocalPlayerEntityInfo->Velocity, (LocalPlayerEntityInfo->Sizes.Velocity), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_vecVelocity], (LocalPlayerEntityInfo->Sizes.Velocity));
					memmove_s(&LocalPlayerEntityInfo->ViewOffset, (LocalPlayerEntityInfo->Sizes.ViewOffset), &LocalPlayerEntityInfo->DataChunk.Data[pGlobalVars.Offsets.m_vecViewOffset], (LocalPlayerEntityInfo->Sizes.ViewOffset));
					//считано успешно, значит структура правильная, запишем, что ее можно использовать
					LocalPlayerEntityInfo->Valid = true;
				}

			}

		}
	}
}

Вот и всё, это выглядит страшновато, но под пристальным взглядом смысл у этой копипасты простой. То же самое выполняется и для GeneralPlayerEntity, но несколько иначе:

void tGeneralPlayerEntity::tGeneralPlayerEntityFunctions::GetGeneralPlayerEntityInfo(tGeneralPlayerEntityInfo* GeneralPlayerEntityInfo, const int& PlayerNumber)
{
	if (GeneralPlayerEntityInfo)
	{
		GeneralPlayerEntityInfo->Valid = false;
		if (pIO.ClientModuleBaseAddress)
		{
			pIO.Functions.Read<DWORD>(&GeneralPlayerEntityInfo->BaseAddress, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.m_dwEntityList + (pGlobalVars.Offsets.EntitySize * PlayerNumber));
			if (GeneralPlayerEntityInfo->BaseAddress)
			{
				//Читаем первый кусок
				ZeroMemory(&GeneralPlayerEntityInfo->DataChunk1, GeneralPlayerEntityInfo->Sizes.DataChunk1);
				pIO.Functions.Read<tDataStructs::tDataChunk>(&GeneralPlayerEntityInfo->DataChunk1, GeneralPlayerEntityInfo->BaseAddress);
				memmove(&GeneralPlayerEntityInfo->Dormant, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_bDormant], GeneralPlayerEntityInfo->Sizes.Dormant);
				//Если игрок мертв или он наблюдатель, смысла читать его информацию нет
				if (!GeneralPlayerEntityInfo->Dormant)
				{
				memmove(&GeneralPlayerEntityInfo->LifeState, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_lifeState], GeneralPlayerEntityInfo->Sizes.LifeState);
				if (!GeneralPlayerEntityInfo->LifeState)
				{
					pIO.Functions.Read<DWORD>(&GeneralPlayerEntityInfo->GameResourcesBaseAddress, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.CSPlayerResource);
					if (GeneralPlayerEntityInfo->GameResourcesBaseAddress)
					{
					//Если нам доступны его игровые данные - читаем
					pIO.Functions.Read<tDataStructs::tDataChunk>(&GeneralPlayerEntityInfo->DataChunk2, GeneralPlayerEntityInfo->GameResourcesBaseAddress);	
					memmove_s(&GeneralPlayerEntityInfo->Kills, GeneralPlayerEntityInfo->Sizes.Kills, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_iKills + ((PlayerNumber + 1) * GeneralPlayerEntityInfo->Sizes.Kills)], GeneralPlayerEntityInfo->Sizes.Kills);
					memmove_s(&GeneralPlayerEntityInfo->CompetetiveRankNumber, GeneralPlayerEntityInfo->Sizes.CompetetiveRankNumber, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_iCompetitiveRanking + ((PlayerNumber + 1) * GeneralPlayerEntityInfo->Sizes.CompetetiveRankNumber)], GeneralPlayerEntityInfo->Sizes.CompetetiveRankNumber);
					memmove_s(&GeneralPlayerEntityInfo->Deaths, GeneralPlayerEntityInfo->Sizes.Deaths, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_iDeaths + ((PlayerNumber + 1) * GeneralPlayerEntityInfo->Sizes.Deaths)], GeneralPlayerEntityInfo->Sizes.Deaths);
					memmove_s(&GeneralPlayerEntityInfo->HasC4, GeneralPlayerEntityInfo->Sizes.HasC4, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_iPlayerC4], GeneralPlayerEntityInfo->Sizes.HasC4);
					memmove_s(&GeneralPlayerEntityInfo->HasDefuser, GeneralPlayerEntityInfo->Sizes.HasDefuser, &GeneralPlayerEntityInfo->DataChunk2.Data[pGlobalVars.Offsets.m_bHasDefuser + ((PlayerNumber + 1) * GeneralPlayerEntityInfo->Sizes.HasDefuser)], GeneralPlayerEntityInfo->Sizes.HasDefuser);
					pIO.Functions.Read<DWORD>(&GeneralPlayerEntityInfo->RadarBaseAddress, pIO.ClientModuleBaseAddress + pGlobalVars.Offsets.m_dwRadarBase);
					if (GeneralPlayerEntityInfo->RadarBaseAddress)
					{
						pIO.Functions.Read<DWORD>(&GeneralPlayerEntityInfo->RadarPointerBaseAddress, GeneralPlayerEntityInfo->RadarBaseAddress + pGlobalVars.Offsets.m_dwRadarBasePointer);
						if (GeneralPlayerEntityInfo->RadarPointerBaseAddress)
						{
							memmove_s(&GeneralPlayerEntityInfo->BoneMatrixBaseAddress, sizeof(GeneralPlayerEntityInfo->BoneMatrixBaseAddress), &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_dwBoneMatrix], sizeof(GeneralPlayerEntityInfo->BoneMatrixBaseAddress));
							if (GeneralPlayerEntityInfo->BoneMatrixBaseAddress)
							{
								pIO.Functions.Read<tDataStructs::tBoneMatrix>(&GeneralPlayerEntityInfo->BoneMatrix, GeneralPlayerEntityInfo->BoneMatrixBaseAddress);
								pIO.Functions.Read<tDataStructs::tPlayerName>(&GeneralPlayerEntityInfo->PlayerName, GeneralPlayerEntityInfo->RadarPointerBaseAddress + (pGlobalVars.Offsets.RadarName1 * (PlayerNumber + 1) + pGlobalVars.Offsets.RadarName2));										
								memmove_s(&GeneralPlayerEntityInfo->Health, GeneralPlayerEntityInfo->Sizes.Health, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_iHealth], GeneralPlayerEntityInfo->Sizes.Health);
								memmove_s(&GeneralPlayerEntityInfo->Flags, GeneralPlayerEntityInfo->Sizes.Flags, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_fFlags], GeneralPlayerEntityInfo->Sizes.Flags);
								memmove_s(&GeneralPlayerEntityInfo->Origin, GeneralPlayerEntityInfo->Sizes.Origin, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_vecOrigin], GeneralPlayerEntityInfo->Sizes.Origin);
								memmove_s(&GeneralPlayerEntityInfo->TeamNumber, GeneralPlayerEntityInfo->Sizes.TeamNumber, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_iTeamNum], GeneralPlayerEntityInfo->Sizes.TeamNumber);
								memmove_s(&GeneralPlayerEntityInfo->Velocity, GeneralPlayerEntityInfo->Sizes.Velocity, &GeneralPlayerEntityInfo->DataChunk1.Data[pGlobalVars.Offsets.m_vecVelocity], GeneralPlayerEntityInfo->Sizes.Velocity);
								GeneralPlayerEntityInfo->Valid = true;
								}
							}
						}
					}
				}
			}
		}
	}
}
}

Единственное принципиальное отличие — что LocalPlayerEntity в памяти одна, а GeneralPlayerEntity — 64 штуки подряд (массив собственно). Причем заполнены они абсолютно случайным образом (могу ошибаться, если кто поправит — напишите) — к примеру, если на сервере 12 игроков, один из них вы, 5 союзников и шесть противников — индексы в этой структуре могут быть абсолютно разными — вплоть до того, что все могут получить индексы от 51 до 63, так что не получится просто считать первые 12 структур, нужно проверять каждую. Как видно, мы берем смещение, и с помощью адресной арифметики получаем нужную по номеру структуру, это главное отличие от предыдущей функции. Это позволяет воспользоваться циклом, в котором мы будем непрерывно читать все 64 структуры и сразу же выводить все полученные данные.

На этом всё, если данная тема будет интересна Хабру, напишу еще несколько статей о том, как получать смещения из клиента, как непосредственно показывать игроку всё, что мы получили в этой статье в удобоваримом виде, и как написать простой AimBot.

По материалам форума unknowncheats.me и чита с исходным кодом от Puddin Poppin.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.