Играя в Unreal Tournament (ut99) я использую тактику "ожидание противника в засаде", таких игроков называют camper.

Я мирно cтою за ящиком и слушаю музыку.

Как только раздаётся сигнал "респавна" (воскрешение игрока).
Я говорю: - Привет!

Во время игры я слушаю музыку. Громкость звуков (в игре) установлены ниже, из-за этого звук respawn'a слышен неотчетливо. Я задумался. Нельзя-ли сделать, что-то в виде информационного датчика?

Зародившееся идея успешно воплотилась. Об этом мой рассказ.

Сформировался такой план.

  1. Запуская UnrealTournament.exe внедрять функцию датчика.

  2. Отслеживаются регистры процессора.

  3. При воскрешении игрока - мигнуть диодом клавиатуры.

В Unreal Tournament, в каталоге System находятся исполняемые и dll-модули.
Я выяснил, модуль Galaxy.dll, воспроизводит звук.

Набросок функции Trigger, на ЯП С с ассемблерной вставкой.
В итоге - функция будет преобразованна в шелл-код, который и будет внедрен.

Как действует Trigger?

Если регистр ESI содержит значение 0x106720F4, значит - нужно проверить регистр EDI, если он содержит одно из значений: 0x412, 0x426, 0x44A, 0x44C, значит сейчас будет сигнал.

void Trigger()
{
	_asm cmp esi, 0x106720F4
	_asm je NEXT
	_asm jmp BACK

NEXT:
	_asm cmp edi, 0x412
	_asm je FOUND

	_asm cmp edi, 0x426
	_asm je FOUND

	_asm cmp edi, 0x44A
	_asm je FOUND

	_asm cmp edi, 0x44C
	_asm je FOUND

	_asm cmp edi, 0x44D
	_asm je FOUND

	_asm cmp edi, 0x446
	_asm je FOUND

	_asm cmp edi, 0x44E
	_asm je FOUND

	_asm cmp edi, 0x44F
	_asm je FOUND

	_asm cmp edi, 0x448
	_asm je FOUND

	_asm cmp edi, 0x450
	_asm je FOUND

	_asm cmp edi, 0xC8F
	_asm je FOUND

	_asm jmp BACK

FOUND:
	//_asm mov [g_found], 1
BACK:
	//_asm jmp
}

Как понятно, Trigger работает динамически (в памяти запущенного процесса).

План предварительной подготовки.

  1. Найти в модуле пространство для записи функции Trigger.

  2. С помощью hex-редактора отредактировать Galaxy.dll.

  3. Загрузить в Ida (Интерактивный дизассемблер) - проверить.

Поиск пространства (для записи функции) в модуле Galaxy.dll (для тех кто знаком с ассемблером) не сложная процедура.
В Ida я задал текстовый поиск по-ключевому слову "align" (добавив двойку) (продолжить поиск далее нажимайте комбинацию клавиш Ctrl+T).

Align - директива ассемблера, выравнивает сегменты (вставляет любые символы).
В этом месте и вставим код функции Trigger.

Поиск не заставил долго ждать, в нашем распоряжении (2000h) 8192 байт!

Узнаем у Ida - где, в файле Galaxy.dll - находится адрес (смещение) относительно
физического расположения?

Установить курсор в интересующую позицию.
В пункте меню "Jmp" выбрать "Jmp to file offset...".

Копируем адрес, еще лучше записать в блокнотик.

Открыть файл Galaxy.dll в редакторе (в котором любите работать).

Если вы как и я используете QView - нажмите пару раз клавишу "Enter" - будет вид дизассемблера.
Нажмите клавишу "F2" показывать 32-битный код.
Нажмите "F5" введите адрес: 361BA нажмите клавишу "Enter".

Вы окажетесь в месте где мы запишем код функции Trigger.

Записываем код (в Qview работает клавиша "Tab"):

  1. 81FEF4206710 cmp esi, 106720F4

  2. Пока не введен весь код - неизвестен точный адрес (т.е. смещение) на которую нужно делать условный переход - поэтому, временно - вводим nop (ничего не делать) две команды; это как набросок - резервирование.

  3. Здесь располагается переход на выход, пока адрес неизвестен,
    временно пропускаем "nop" две команды.

  4. 81FF12040000 cmp edi, 00000412

  5. "nop" две команды

  6. 81FF26040000 cmp edi, 00000426

  7. "nop" две команды

  8. 81FF4A040000 cmp edi, 0000044A

  9. "nop" две команды

  10. 81FF4C040000 cmp edi, 0000044C

  11. "nop" две команды

  12. 81FF4D040000 cmp edi, 0000044D

  13. "nop" две команды

  14. 81FF46040000 cmp edi, 00000446

  15. "nop" две команды

  16. 81FF4E040000 cmp edi, 0000044E

  17. "nop" две команды

  18. 81FF4F040000 cmp edi, 0000044F

  19. "nop" две команды

  20. 81FF48040000 cmp edi, 00000448

  21. "nop" две команды

  22. 81FF50040000 cmp edi, 00000450

  23. "nop" две команды

  24. 81FF8F0C0000 cmp edi, 00000C8F

  25. "nop" две команды

  26. FF15B4BE6710 call dword ptr [1067BEB4]

  • вызов функции воспроизведения звука, она перенесена из места редактирования

  1. E9A1B5FDFF jmp 000117C8

  • это безусловный переход, прыгаем в место адрес которого неизменен.

  1. 893DE0F06510 mov dword ptr [1065F0E0], edi

  • это переменная в которую мы заносим данные из регистра "EDI"

  1. EBED jmp 0003621C

  • переход в пункт #25

Редактируем условные переходы.
Вот что получается:

Теперь идем в место откуда мы переходим в функцию Trigger.

Нажмите в "Qview" клавишу "F5", введите адрес 117C2 и нажмите "Enter".

Запишите:
a) E9F3490200 jmp 000361BA
b) 90 nop

Всё готово. Проверим.

Загрузите Galaxy.dll в Ida (если вы еще не сделали этого).
Если загружен - обновите базу данных (нажмите в меню "Options" - "General" - "Reanalyze program").
Также, ничего не случится, если вы вместо обновления - загрузите модуль "Galaxy.dll" снова.

Идем в функцию Trigger, нажмите в Ida клавишу "G", введите адрес 106361BA.

Cмотрим:

Нажмите на "; CODE XREF: sub_10610C00+BC2" и мы попадем в место откуда вызывается функция, помните - это последнее, что мы редактировали, адрес 106117C2.

Выберите в меню "Debugger" отладчик "Local Win32 Debugger".

Установите точку остановки (BPX) перед переходом (инструкция "jmp").

Нажмите в меню "Debugger" "Start process", установите (чекбокс):
"Don't display this message again" и нажмите "Yes".
Ida попросит указать имя исполняемого приложения, выберите "UnrealTournamet.exe".

Когда программа остановится в точке остановки (BPX).
Пройдите весь путь пошагово (нажимая клавишу F8).
Убедитесь в надежности исправленного участка.

Результат. Программа в сборе.
////////////////////////////////////////////////////////////////////////////////
// WinMain.cpp
//
// По умолчанию в UT99 звук воспроизводит: Galaxy.GalaxyAudioSubsystem.
// Если, что-то будет не так - проверьте файл UnrealTournament.ini,
// установлено-ли: AudioDevice=Galaxy.GalaxyAudioSubsystem

#include <windows.h>
#include <winioctl.h>

char* GAppname="Resp2A Trigger UT99'";
char* GAppname_UT="Unreal Tournament";

void mb(char* s);
void OnKbdLeds();
void ToggleLed(BOOL toggle, int led);
char* appGetOpenFileName();

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
	CreateMutex(NULL,0,GAppname);
	BOOL AlreadyRunning=(ERROR_ALREADY_EXISTS==GetLastError());
	if(AlreadyRunning)
	return 1;
	
	// Load config
	HKEY key;
	RegCreateKey(HKEY_CURRENT_USER,
	"Software\\MyCompany\\MyAppname",&key);

	DWORD dwLen=MAX_PATH;
	static char CommandLine[MAX_PATH];
	RegQueryValueEx(key,"CommandLine",NULL,NULL,(BYTE*)CommandLine,&dwLen);

	if(!CommandLine[0])
	{
		lstrcpy(CommandLine,appGetOpenFileName());
		if(!CommandLine[0])
		{
			RegCloseKey(key);
			return 0;
		}
	}

	char CurrentDirectory[MAX_PATH];
	lstrcpy(CurrentDirectory,CommandLine);

	int i;
	for(i=lstrlen(CurrentDirectory)-1;i>0;--i)
	{
		if('\\'==CurrentDirectory[i-1] || '//'==CurrentDirectory[i-1])
		break;
	}
	CurrentDirectory[i]=0;

	PROCESS_INFORMATION pi;
	STARTUPINFO si;
	memset(&si,0,sizeof(STARTUPINFO));
	si.cb=sizeof(STARTUPINFO);
	si.dwFlags=STARTF_FORCEOFFFEEDBACK;

	char cmdLine[512];
	wsprintf(cmdLine,"%s",CommandLine);
	//wsprintf(cmdLine,"%s 195.98.73.166:6666",CommandLine);

	CreateProcess(NULL,
	cmdLine,NULL,NULL,0,0,NULL,CurrentDirectory,&si,&pi);
	if(!pi.hProcess)
	{
		CommandLine[0]=0;
		RegSetValueEx(key,"CommandLine",NULL,REG_SZ,(BYTE*)(CommandLine),MAX_PATH);
		RegCloseKey(key);

		char buf[512];
		wsprintf(buf,"Failed CreateProcess\n\n\"%s\"",CommandLine);
		mb(buf);
	}
	CloseHandle(pi.hThread);

	// Save config
	RegSetValueEx(key,"CommandLine",NULL,REG_SZ,(BYTE*)(CommandLine),MAX_PATH);
	RegCloseKey(key);

	HWND hWnd=FindWindow(NULL,GAppname_UT);
	while(!hWnd)
	{
		Sleep(1000);
		hWnd=FindWindow(NULL,GAppname_UT);
	}

	/*-------------------------------------------------------
		The trigger
	-------------------------------------------------------*/
	DWORD addrTrigger=0x106361BA;

	unsigned char codeTrigger[]=
	"\x81\xFE\xF4\x20\x67\x10\x74\x02\xEB\x58\x81\xFF\x12\x04\x00\x00"
	"\x74\x5B\x81\xFF\x26\x04\x00\x00\x74\x53\x81\xFF\x4A\x04\x00\x00"
	"\x74\x4B\x81\xFF\x4C\x04\x00\x00\x74\x43\x81\xFF\x4D\x04\x00\x00"
	"\x74\x3B\x81\xFF\x46\x04\x00\x00\x74\x33\x81\xFF\x4E\x04\x00\x00"
	"\x74\x2B\x81\xFF\x4F\x04\x00\x00\x74\x23\x81\xFF\x48\x04\x00\x00"
	"\x74\x1B\x81\xFF\x50\x04\x00\x00\x74\x13\x81\xFF\x8F\x0C\x00\x00"
	"\x74\x0B\xFF\x15\xB4\xBE\x67\x10\xE9\xA1\xB5\xFD\xFF\x89\x3D\xE0"
	"\xF0\x65\x10\xEB\xED";

	DWORD codeTrigger_Len=117;

	/*-------------------------------------------------------
		The patch
	-------------------------------------------------------*/
	DWORD addrPatch=0x106117C2;

	// E9F349020090
	unsigned char codePatch[]="\xE9\xF3\x49\x02\x00\x90";
	DWORD codePatch_Len=6;

	DWORD dwMagic=0;
	BYTE value[128];

	BOOL bRet=FALSE;
	int n=14; // Wait init "UT" 14 seconds

	while(n)
	{
		ReadProcessMemory(pi.hProcess,
		(LPVOID)addrPatch,&dwMagic,sizeof(dwMagic),NULL);
		if(dwMagic)
		{
			if(0xBEB415FF==dwMagic)
			{
				memcpy(&value,codeTrigger,codeTrigger_Len);
				bRet=WriteProcessMemory(pi.hProcess,
				(LPVOID)addrTrigger,&value,codeTrigger_Len,NULL);

				if(bRet)
				{
					memcpy(&value,codePatch,codePatch_Len);
					bRet=WriteProcessMemory(pi.hProcess,
					(LPVOID)addrPatch,&value,codePatch_Len,NULL);
				}
			}
			break;

		} // End "if dwMagic"

		--n;
		Sleep(1000);

	} // End "while"

	if(!bRet)
	mb("Failed patch");

	DWORD addrFound=0x1065F0E0;
	DWORD dwFound=0;

	hWnd=FindWindow(NULL,GAppname_UT);

	while(hWnd)
	{
		ReadProcessMemory(pi.hProcess,
		(LPVOID)addrFound,&dwFound,sizeof(dwFound),NULL);
		if(dwFound)
		{
			switch(dwFound)
			{
			case 0x412:
			dwFound=0xCDC31337;
			break;

			case 0x426:
			dwFound=0xCDC31337;
			break;

			case 0x44A:
			dwFound=0xCDC31337;
			break;

			case 0x44C:
			dwFound=0xCDC31337;
			break;

			case 0x44D:
			dwFound=0xCDC31337;
			break;

			case 0x446:
			dwFound=0xCDC31337;
			break;

			case 0x44E:
			dwFound=0xCDC31337;
			break;

			case 0x44F:
			dwFound=0xCDC31337;
			break;

			case 0x448:
			dwFound=0xCDC31337;
			break;

			case 0x450:
			dwFound=0xCDC31337;
			break;

			case 0xC8F:
			dwFound=0xCDC31337;
			//break;
			}

			if(0xCDC31337==dwFound)
			{
				OnKbdLeds();
				ToggleLed(1,1);

				dwFound=0;
				bRet=WriteProcessMemory(pi.hProcess,
				(LPVOID)addrFound,&dwFound,sizeof(dwFound),NULL);
			}
			
		} // End "if dwFound"

		Sleep(20); // Give up

		hWnd=FindWindow(NULL,GAppname_UT);

	} // End "while"

	return 0;
}

void mb(char* s)
{
	UINT uType=MB_OK | MB_ICONINFORMATION |
	MB_SETFOREGROUND | MB_SYSTEMMODAL;

	int n=0;
	if(strstr(s,"Failed") || strstr(s,"Error"))
	++n;

	if(n) {
		uType &=~MB_ICONINFORMATION;
		uType |=MB_ICONWARNING;
	} MessageBox(GetActiveWindow(),s,GAppname,uType);

	if(n)
	ExitProcess(n);
}

/*
void Trigger()
{
	_asm cmp esi, 0x106720F4
	_asm je NEXT
	_asm jmp BACK

NEXT:
	_asm cmp edi, 0x412
	_asm je FOUND

	_asm cmp edi, 0x426
	_asm je FOUND

	_asm cmp edi, 0x44A
	_asm je FOUND

	_asm cmp edi, 0x44C
	_asm je FOUND

	_asm cmp edi, 0x44D
	_asm je FOUND

	_asm cmp edi, 0x446
	_asm je FOUND

	_asm cmp edi, 0x44E
	_asm je FOUND

	_asm cmp edi, 0x44F
	_asm je FOUND

	_asm cmp edi, 0x448
	_asm je FOUND

	_asm cmp edi, 0x450
	_asm je FOUND

	_asm cmp edi, 0xC8F
	_asm je FOUND

	_asm jmp BACK

FOUND:
	//_asm mov [g_found], 1
BACK:
	//_asm jmp
}
*/

#define IOCTL_KEYBOARD_SET_INDICATORS CTL_CODE(FILE_DEVICE_KEYBOARD, 2, METHOD_BUFFERED,FILE_ANY_ACCESS)
#define IOCTL_KEYBOARD_QUERY_INDICATORS CTL_CODE(FILE_DEVICE_KEYBOARD, 0x10, METHOD_BUFFERED,FILE_ANY_ACCESS)

void OnKbdLeds()
{
	if(!DefineDosDevice(DDD_RAW_TARGET_PATH,"Kbd000000","\\Device\\KeyboardClass0"))
	mb("Failed DefineDosDevice");

	HANDLE hDevice=CreateFile("\\\\.\\Kbd000000",GENERIC_WRITE,FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,0,NULL);
	if(INVALID_HANDLE_VALUE==hDevice)
	mb("Failed open kbd");

	unsigned int InBuffer;
	DWORD OutBufferSize;
	unsigned char p[]={32};

	for(int i=0; i<300; ++i)
	{
		InBuffer=0;
		InBuffer |=p[i] << 16;
		DeviceIoControl(hDevice,IOCTL_KEYBOARD_SET_INDICATORS,&InBuffer,sizeof(InBuffer),NULL,0,&OutBufferSize,NULL);
		Sleep(10);
	}

	DefineDosDevice(DDD_REMOVE_DEFINITION,"Kbd000000",NULL);
	CloseHandle(hDevice);
}

void ToggleLed(BOOL toggle, int led)
{
	if(!DefineDosDevice(DDD_RAW_TARGET_PATH,"Kbd000000","\\Device\\KeyboardClass0"))
	mb("Failed DefineDosDevice");

	HANDLE hDevice=CreateFile("\\\\.\\Kbd000000",GENERIC_WRITE,FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,0,NULL);
	if(INVALID_HANDLE_VALUE==hDevice)
	mb("Failed open kbd");

	DWORD OutBufferSize;
	unsigned int InBuffer=0, output=0;

	if(!DeviceIoControl(hDevice,IOCTL_KEYBOARD_QUERY_INDICATORS,&InBuffer,sizeof(InBuffer),&output, sizeof(output),&OutBufferSize, NULL))
	{
		CloseHandle(hDevice);
		mb("Failed query kbd");
	}

	InBuffer=output;

	if(toggle)
	InBuffer &= ~(led << 16);
	else
	InBuffer |=led << 16;

	DeviceIoControl(hDevice,IOCTL_KEYBOARD_SET_INDICATORS,&InBuffer,sizeof(InBuffer),NULL,0,&OutBufferSize,NULL);

	CloseHandle(hDevice);
}

char* appGetOpenFileName()
{
	static char fname[MAX_PATH];

	OPENFILENAME ofn;
	memset(&ofn,0,sizeof(OPENFILENAME));
	fname[0]=0;

	ofn.lStructSize=sizeof(OPENFILENAME);
	ofn.hInstance=GetModuleHandle(NULL);
	ofn.lpstrFile=fname;
	ofn.lpstrInitialDir="D:\\Games\\ut99\\System";
	ofn.nMaxFile=MAX_PATH;
	ofn.lpstrFileTitle=NULL;
	ofn.nMaxFileTitle=0;
	ofn.lpstrTitle="Select UnrealTournament.exe";
	ofn.lpstrFilter="Applications (*.exe)\0*.exe\0";
	ofn.Flags=OFN_FILEMUSTEXIST |
	OFN_HIDEREADONLY | OFN_PATHMUSTEXIST;

	GetOpenFileName(&ofn);	
	return fname;
}

////////////////////////////////////////////////////////////////////////////////
// <<eof>> WinMain.cpp
////////////////////////////////////////////////////////////////////////////////

Пытливые умы конечно же спросят: - "как ты вычислил адрес, где происходят звуковые события?".

Об этом я расскажу во второй части.

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


  1. gmtd
    10.01.2023 10:45

    Не проще было попытаться распознать маркеры звука респауна в аудиопотоке?


    1. baldr
      10.01.2023 11:06
      +20

      Или поискать звуковой файл в папке с игрой и сделать в нем звук погромче.


  1. lymes
    10.01.2023 11:10
    +2

    ИМХО, проще и чище было бы форкнуть https://github.com/JimmieKJ/unrealTournament, добавить нужный функционал и собрать игру.


    1. VBKesha
      10.01.2023 11:27
      +1

      Это не UT99 а совершенно другая игра, на другом движке(по крайней мере версии) с названием синонимом.


      1. Fen1kz
        10.01.2023 12:29
        +2

        *с названием омонимом

        Игра с названием синонимом называлась бы, например, "Surreal Competition"


        1. VBKesha
          10.01.2023 12:36
          +1

          Точно перепутал.


  1. kin4stat
    10.01.2023 12:02
    +3

    Куча непонятных телодвижений и танцев с бубном, ради непонятно чего.

    Краткий гайд: берем minhook, берем Си, используем обычный ассемблер встраиваемый в код MSVC. Готово. И никаки танцев с бубном с отладчиками, реверс тулзами, и ручной записи опкодов в память, чтобы просто их транслировать в байты


  1. gajdarenkosasha
    10.01.2023 12:49
    -1

    Ахаха, гений !

    Мне понравилась идея в world of tanks такое бы)


  1. Didimus
    11.01.2023 09:02
    +3

    Встретились два кемпера, и всю игру так просидели в засаде


  1. RetardedWolfy
    11.01.2023 14:19

    Крысерство за ящиком заставило автора двигаться в реальной жизни, лишь бы только дальше продолжать крысить)