5 подписчиков

[C#] Написание простого чита AIMBOT на примере CS Source

Аннотация


В этой статье описано, как программировать читы категории Аимбот, или же другими словами стороннее ПО, осуществляющее наведение курсора игрока на другие игровые цели.

Дисклеймер:

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

Используемое ПО:

  • Visual Studio 2019(можно любое)
  • Cheat Engine
  • Counter strike source v34
  • Virtual box (по усмотрению)

Механизм расчета


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

В рамках темы нас интересуют следующие переменные

  • Координаты игрока (X, Y, Z)
  • Оси видовой матрицы(XY, XZ)
  • Координаты соперников
Трудности в понимании могут произойти уже на этом этапе, разбираемся
Трудности в понимании могут произойти уже на этом этапе, разбираемся

Зная координаты игрока и координаты соперника можно вычислить необходимые для попадания в цель оси.
А вот и формулы расчета:

  • XY = Arctg(Xp - Xb; Yp - Yb)
  • XZ = Arctg(Zp - Zb; Xp - Xb)

P - игрок
B - бот
Тангенс - это отношения противолежащего катета к прилежащему, зная это отношение можно найти угол.

Пример для оси XZ
D = X (цели) - X (игрока), то есть расстояние до цели только по координате X (движение вперед) H = Z (Цели) - Z (игрока), то есть расстояние до цели только по координате Z (по высоте)
Ранее приведенные формулы расчета формируются именно из этого соотношения.
Для получения оси XY необходимо провести точно такие же действия, только с координатами X,Y
Пример для оси XZ D = X (цели) - X (игрока), то есть расстояние до цели только по координате X (движение вперед) H = Z (Цели) - Z (игрока), то есть расстояние до цели только по координате Z (по высоте) Ранее приведенные формулы расчета формируются именно из этого соотношения. Для получения оси XY необходимо провести точно такие же действия, только с координатами X,Y

Code:

  1. float SwapXY = (float)(Math.Atan2((CrntTarget.Coord_Y - MainPlayer.Coord_Y), (CrntTarget.Coord_X - MainPlayer.Coord_X)) * 180 / Math.PI);

  2. float SwapXZ = (float)(Math.Atan((CrntTarget.Coord_Z - MainPlayer.Coord_Z - PLAYER_HEIGHT) / (CrntTarget.Coord_X - MainPlayer.Coord_X)) * 180 / Math.PI);

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

  • OpenProcess
  • ReadProcessMemory
  • WriteProcessMemory

Немного подробнее о них... Read и Write - означает читать и писать соответственно, такие операции производятся с памятью процесса.
Читая память данная функция возвращает набор байт из указанной области памяти, как и описывалось ранее - это могут быть различные типы данных, например String (строка), Int (целое число) Float (число с плавающей точкой).
Если необходимо извлечь из памяти число - программа читает 4 байта, поскольку SizeOf(Int) = 4 в рамках данной ОС.
Если необходимо извлечь переменную типа String - та же самая операция, только длина может быть значительно больше в зависимости от длины строки.

Далее байты нужно преобразовать в соответствующий тип, например, в Int.
Пример из кода:

Code:

HP = BitConverter.ToInt32(MemTool.ReadReg(BotsAddress + OFFSET_HP + OFFSET_BTWN * i, 4), 0),

Получение значения HP
Можно попытаться преобразовать данные строки в Int, это тоже получится для первых 4х байт, но адекватного значения ждать не стоит, строки преобразуются с использованием кодировки, в данном примере при помощи ASCII.

Пример из кода:

Code:

Name = Encoding.ASCII.GetString(MemTool.ReadReg(BotsAddress + OFFSET_NAME + OFFSET_BTWN * i, 8)),

Получение ника пользователя.
В случае с методом Write ситуация зеркально противоположная - программа выполняет запись в память процесса, тем самым подменяя текущее значение переменной на заранее подготовленное. По такому принципу работают читы класса GodMode, NoRecoil, позволяя использовать бесконечное здоровье игрока и бесконечные патроны.

Пример из кода:

Code:

  1. MemTool.WriteReg(ModuleEngine + OFFSET_CONTROL_AXIS_XY, BitConverter.GetBytes(SwapXY));

  2. MemTool.WriteReg(ModuleEngine + OFFSET_CONTROL_AXIS_XZ, BitConverter.GetBytes(SwapXZ));

Запись значений осей в соответствующие адреса памяти.
Поиск адресов и модульная адресация
Поиск адресов в памяти производится изменением интересующих значений с целью дальнейшей сверки с предыдущими, эта операция проводится до тех пор, пока не будет найден адрес памяти, а их может быть много.
Приведу пример, наша задача найти координату Z игрока - придется подниматься по лестнице, сканировать память, спускаться и так до тех пор, пока не останется несколько адресов. А их может быть много - координата игрока, координата его оружия, координата его ботинок и тд.
Для этой задачи хорошо подходит программа Cheat Engine.

Cheat Engine.
В поле адресов памяти есть два адреса - это найденные оси XY, XZ, как было описано ранее. 89 - значение для оси XY, сейчас курсор направлен вниз до предела.
Cheat Engine. В поле адресов памяти есть два адреса - это найденные оси XY, XZ, как было описано ранее. 89 - значение для оси XY, сейчас курсор направлен вниз до предела.

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

Идея поиска структур плавно переходит к модульной адресации, если в случае со структурой бота найти адрес здоровья можно через прибавления отступа или Offseta, то с модульной адресацией немного иначе.
При перезагрузке программы старые адреса могут измениться и скорее всего изменятся, если они не статические.
Модульная адресация подразумевает под собой адресацию относительно чего-то определенного, например, модулей client.dll и engine.dll

Изображено 6 отступов или Offset'ов
Изображено 6 отступов или Offset'ов

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

Отладка программы, как видно на изображении получено значение бота по имени Yogi, если Yogi начнет двигаться его координаты изменятся, а в случае вступления в бой с соперником может измениться уровень здоровья. Также в структуре бота есть его фракция - 2/3 T/Ct соответственно.
Отладка программы, как видно на изображении получено значение бота по имени Yogi, если Yogi начнет двигаться его координаты изменятся, а в случае вступления в бой с соперником может измениться уровень здоровья. Также в структуре бота есть его фракция - 2/3 T/Ct соответственно.

КОД

Program.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Threading;




namespace SimpleAIM
{
class Player
{
public float Axis_XY;
public float Axis_YZ;

public float Coord_X;
public float Coord_Y;
public float Coord_Z;

public int HP = 0;
public int Team = 0;

public string Name = "n/a";
}


class Program
{


static string NAME_GAME_PROCESS = "hl2";
static string NAME_MODULE_CLIENT = "client.dll";
static string NAME_MODULE_ENGINE = "engine.dll";
static string PLAYER_NAME = "rootmode";
static float PLAYER_HEIGHT = 67.0f;
static float PLAYER_TEAM = 3;


// BOTS OFFSETS

static int MAIN_OFFSET = 0x04035C0;
static int OFFSET_NAME = 0x038;
static int OFFSET_TEAM = 0x58;
static int OFFSET_HP = 0x05C;
static int OFFSET_X = 0x060;
static int OFFSET_Y = OFFSET_X + 4;
static int OFFSET_Z = OFFSET_Y + 4;
static int OFFSET_BTWN = 0x0140;




// MIAN PLAYER OFFSETS

static int OFFSET_MP_AXIS_XZ = 0x039D510;
static int OFFSET_MP_AXIS_XY = OFFSET_MP_AXIS_XZ + 4;
static int OFFSET_MP_X = 0x039D55C;
static int OFFSET_MP_Y = OFFSET_MP_X + 4;
static int OFFSET_MP_Z = OFFSET_MP_Y + 4;




// CONTROLS OFFSET

static int OFFSET_CONTROL_AXIS_XZ = 0x03953F8;
static int OFFSET_CONTROL_AXIS_XY = OFFSET_CONTROL_AXIS_XZ + 4;



static void Main(string[] args)
{
/*
double A = -1.0F;
double B = -1.0F;
double Alpha = Math.Atan2(A,B) * 180/Math.PI - 90;

Console.WriteLine(Alpha.ToString());
Console.ReadKey();
return;
*/




MemReader MemTool = new MemReader(NAME_GAME_PROCESS);


int ModuleClientAddr = MemTool.GetModuleAddr(NAME_MODULE_CLIENT);
int ModuleEngine = MemTool.GetModuleAddr(NAME_MODULE_ENGINE);
int BotsAddress = BitConverter.ToInt32(MemTool.ReadReg(ModuleClientAddr + MAIN_OFFSET, 4), 0);

Player MainPlayer = new Player();
List<Player> Bots = new List<Player>();


while (true)
{

// * PART I
// Parsing main players coord


MainPlayer.Axis_XY = BitConverter.ToSingle(MemTool.ReadReg(ModuleClientAddr + OFFSET_MP_AXIS_XY, 4), 0);
MainPlayer.Axis_YZ = BitConverter.ToSingle(MemTool.ReadReg(ModuleClientAddr + OFFSET_MP_AXIS_XZ, 4), 0);

MainPlayer.Coord_X = BitConverter.ToSingle(MemTool.ReadReg(ModuleClientAddr + OFFSET_MP_X, 4), 0);
MainPlayer.Coord_Y = BitConverter.ToSingle(MemTool.ReadReg(ModuleClientAddr + OFFSET_MP_Y, 4), 0);
MainPlayer.Coord_Z = BitConverter.ToSingle(MemTool.ReadReg(ModuleClientAddr + OFFSET_MP_Z, 4), 0);

/*
* check main player
*
Console.WriteLine($"AXIS: {MainPlayer.Axis_XY} {MainPlayer.Axis_YZ}");
Console.WriteLine($" X: {MainPlayer.Coord_X}\r\n Y: {MainPlayer.Coord_Y}\r\n Z: {MainPlayer.Coord_Z}");
Console.WriteLine("=============");
*/







// * PART II
// Parsing Bots


Bots.Clear();
for (int i = 0; i < 32; i++)
{
var newOne = new Player()
{
HP = BitConverter.ToInt32(MemTool.ReadReg(BotsAddress + OFFSET_HP + OFFSET_BTWN * i, 4), 0),
Name = Encoding.ASCII.GetString(MemTool.ReadReg(BotsAddress + OFFSET_NAME + OFFSET_BTWN * i, 8)),
Team = BitConverter.ToInt32(MemTool.ReadReg(BotsAddress + OFFSET_TEAM + OFFSET_BTWN * i, 4), 0),

Coord_X = BitConverter.ToSingle(MemTool.ReadReg(BotsAddress + OFFSET_X + OFFSET_BTWN * i, 4), 0),
Coord_Y = BitConverter.ToSingle(MemTool.ReadReg(BotsAddress + OFFSET_Y + OFFSET_BTWN * i, 4), 0),
Coord_Z = BitConverter.ToSingle(MemTool.ReadReg(BotsAddress + OFFSET_Z + OFFSET_BTWN * i, 4), 0),
};

if (newOne.Team == 0)
break;

Bots.Add(newOne);
}


/*
* check bots
*
foreach (var bot in Bots)
{
Console.WriteLine($"-HP: {bot.HP} -Name: {bot.Name} -Team: {bot.Team}");
Console.WriteLine($" coord: {bot.Coord_X}; {bot.Coord_Y}; {bot.Coord_Z}");
Console.WriteLine("=============");
}
*/







// * PART III
// Aim
Player CrntTarget = new Player();
double MinDst = 2500;

foreach (var bot in Bots)
{
//bot.Name != PLAYER_NAME & bot.Team != PLAYER_TEAM
if (bot.Name != PLAYER_NAME
&& bot.HP > 0)
{
double dst = Math.Sqrt((MainPlayer.Coord_X - bot.Coord_X) * (MainPlayer.Coord_X - bot.Coord_X) +
(MainPlayer.Coord_Y - bot.Coord_Y) * (MainPlayer.Coord_Y - bot.Coord_Y) +
(MainPlayer.Coord_Z - bot.Coord_Z) * (MainPlayer.Coord_Z - bot.Coord_Z));
if (dst < MinDst)
{
MinDst = dst;
CrntTarget = bot;
}
}
}





float SwapXY = (float)(Math.Atan2((CrntTarget.Coord_Y - MainPlayer.Coord_Y), (CrntTarget.Coord_X - MainPlayer.Coord_X)) * 180 / Math.PI);
float SwapXZ = (float)(Math.Atan((CrntTarget.Coord_Z - MainPlayer.Coord_Z - PLAYER_HEIGHT) / (CrntTarget.Coord_X - MainPlayer.Coord_X)) * 180 / Math.PI);




/*
* check bots
*
Console.WriteLine($"Aim to {CrntTarget.Name} with dst: {MinDst}");
Console.WriteLine($" coord: {CrntTarget.Coord_X}; {CrntTarget.Coord_Y}; {CrntTarget.Coord_Z}");

Console.WriteLine($"SwapXY: {SwapXY}");
Console.WriteLine($"SwapXZ: {SwapXZ}");
*/

Console.WriteLine($"Aim to {CrntTarget.Name} with dst: {MinDst}");

MemTool.WriteReg(ModuleEngine + OFFSET_CONTROL_AXIS_XY, BitConverter.GetBytes(SwapXY));
MemTool.WriteReg(ModuleEngine + OFFSET_CONTROL_AXIS_XZ, BitConverter.GetBytes(SwapXZ));

Thread.Sleep(300);
}

}


}
}

MemReader.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Diagnostics;





namespace SimpleAIM
{
class MemReader
{
const int PROCESS_FLAGS = 0x1F0FFF;


[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

[DllImport("kernel32.dll")]
public static extern bool ReadProcessMemory(int hProcess,
int lpBaseAddress, byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesRead);

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool WriteProcessMemory(int hProcess, int lpBaseAddress,
byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesWritten);




private Process process;
private IntPtr processHandle;
private int PID;


public MemReader(string procName)
{

PID = GetPID(procName);
if (PID == -1)
throw new Exception("Process not found!");


process = Process.GetProcessById(PID);
processHandle = OpenProcess(PROCESS_FLAGS, false, process.Id);
}

public byte[] ReadReg(int addr, int size)
{
int count = 0;
byte[] buffer = new byte[size];

ReadProcessMemory((int)processHandle, addr, buffer, buffer.Length, ref count);

return buffer;
}

public void WriteReg(int addr, byte[] reg)
{
int writen = 0;
WriteProcessMemory((int)processHandle, addr, reg, reg.Length, ref writen);
}


private int GetPID(string GameName)
{
Process[] list = Process.GetProcesses();
foreach (var prc in list)
{
if (prc.ProcessName == GameName)
return prc.Id;
}

return -1;
}


public int GetModuleAddr(string ModuleName)
{

foreach (ProcessModule module in process.Modules)
{
if (module.ModuleName == ModuleName)
return (int)module.BaseAddress;
}

return -1;
}

}
}