Pull to refresh

Читы на 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.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.