Перейти к содержанию

[Amnesia The Dark Descent] - Изменение режима заполнения полигонов текстурами


Рекомендуемые сообщения

Всем привет, решил я написать простенький хук для OpengGL. Игрушка, которую я хочу ломать использует как раз OpenGL, это можно понять если присоединиться через Cheat Engine к процессу игры и вызвать функцию для перечисления всех DLL загруженных в процесс (Enumerate DLL's). Игрушка использует opengl32.dll соответственно и ломать я буду OpengGL.

 

И так вот небольшой кусок кода для функции GetOpenGLFunctions, весь код написан на C++, он естественно закоментирован на английском языке так как английский это круто и по хорошему код нужно коментить:

 

Спойлер

//Gets address of function from OpenGL library
void GetOpenGLFunctions()
{
	HMODULE Moduleandle = GetModuleHandle(L"opengl32.dll");
	if (Moduleandle != nullptr)
	{
		glBeginAddress = (DWORD)GetProcAddress(Moduleandle, "glBegin");
	}
	else
	{
		MessageBox(0, L"Module handle not found", L"Message", MB_OK);
	}
}

 

Эта функция получает адрес функции glBegin из модуля opengl32.dll, который подргужен самой игрой и сохраняет его в переменную glBeginAddress.

 

После этого вызывается функция HookOpenGLFunctions:

Спойлер

//Performs code injection
void HookOpenGLFunctions()
{
	oglBegin = (tglBegin)glBeginAddress; //Gets address of original function

	jump_glBegin[0] = 0xE9;
	DWORD addr_glBegin = (DWORD)hkglBegin - (DWORD)oglBegin - 5; //Gets address of injection (where to jump)
	memcpy(jump_glBegin + 1, &addr_glBegin, sizeof(DWORD)); //Adds address after 'jmp' opcode to get assembler instruction jmp [address]
	memcpy(codeFragment_glBegin, oglBegin, 5); //Saves address of original function
	VirtualProtect(oglBegin, 8, PAGE_EXECUTE_READWRITE, &savedProtection_glBegin);
	memcpy(oglBegin, jump_glBegin, 5); //Replaces address of original function with assembler instruction jmp [address] (jump to injection where hook is)
}

 

По сути это инъекция кода то есть jmp [address] вместо оригинальной функции glBegin. Я не использую Detours от Microsoft потому что вижу в них смысл если требуется перехват функции в x64 играх, так как там способ перехвата и инъекции кода немножко отличается от x86.

 

И вот собственно сам код инъекции:

Спойлер

//Hooked function that will be executed instead of original function in the process
void WINAPI hkglBegin(GLenum mode)
{
	//Restores bytes of original function
	BYTE* codeDest = (BYTE*)oglBegin;
	codeDest[0] = codeFragment_glBegin[0];
	*((DWORD*)(codeDest + 1)) = *((DWORD*)(codeFragment_glBegin + 1));

	if (isEnabled)
	{
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	}
	else
	{
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	}

	oglBegin(mode);

	//Restores bytes of hooked function so process of the game will execute hooked function instead of original function 
	codeDest[0] = jump_glBegin[0];
	*((DWORD*)(codeDest + 1)) = *((DWORD*)(jump_glBegin + 1));
}

 

Тут происходит восстановление байт оригинальной функции, чтобы она смогла выполнится в конце хука, потом идёт проверка включён ли чит и вызов функции glPolygonMode, котоая устанавливает режим отрисовки полигонов грубо говоря. Соответственно GL_FRONT_AND_BACK - полигоны которые рисуются спереди и сзади, GL_LINE - отрисовка линиями и GL_FILL - полная заливка полигонов текстурами. Ну а дальше вызов оригинальной функции glBegin и восстановление байт для инъекции и всё это крутится в цикле.

 

Вот тут ссылки на документацию по функциям только не с сайта Microsoft а уже с сайта OpenGL документации: glBeginglPolygonMode

Уж не знаю кому как удобнее ну вот сайтик с документацией от разработчика: Khronos

 

В чём может быть проблема?

Всё работает только вот при отрисовке линиями игра почему отображает мне предыдущий кадр в цикле, как будто изображение заморожено, вот пример до:

uPDlN8F.png

 

И вот как это выглядит после хука:

vuFuE2C.png

 

Тут видно наверное плохо но изображение как бы замараживается и не обновляется (даже сохраняется и рисуется предыдущая позиция курсора), но в момент выключения на долю секунды можно заметить отрисовку только линиями как здесь (пример из d3d9 игры):

LTR2b8R.png

Ссылка на комментарий
Поделиться на другие сайты

6 часов назад, Sergey99 сказал:

изображение как бы замараживается и не обновляется

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

Ссылка на комментарий
Поделиться на другие сайты

3 минуты назад, Xipho сказал:

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

Если я пытаюсь очистить буфер экрана и буфер глубины к примеру зелёным цветом без рисования линиями, то оно как бы работает, но вот так вот:

 

Код:

OpenGL Microsoft docs - glColorMask, glClearColor, glClear

glColorMask(GL_FALSE, GL_TRUE, GL_FALSE, GL_TRUE);
glClearColor(0.0, 1.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

 

И пример после очистки зелёным цветом:

rqVytkh.png

 

Примечательно то что что если я буду рисовать линиями GL_LINE то экран будет весь зелёный будто весь очищается цветом.

 

Также переменная glBeginAddress хранит адрес функции которую собственно я и перехватываю, вот тут даже jmp на инъекцию есть так как DLL уже загружена:

yWr6Tn9.png

Ссылка на комментарий
Поделиться на другие сайты

Кстати, сейчас только вспомнил (давно не ковырял огл). glBegin и glEnd - это же, если я правильно помню, что-то вроде "операторских скобок", отделяющих некоторые блоки отрисовки. То есть, совершенно не обязательно на кадр будет строго один glBegin и строго один glEnd. В отличие от DirectX, упомянутых функций в рамках построения одного кадра может быть несколько. Возможно, у тебя хук срабатывает на тех вызовах, на которых не должен срабатывать, от того и результат такой "замороженный". Попробуй под отладкой найти именно тот, который тебе нужен, чтобы отрисовать объекты сеткой.

Ссылка на комментарий
Поделиться на другие сайты

3 часа назад, Xipho сказал:

Кстати, сейчас только вспомнил (давно не ковырял огл). glBegin и glEnd - это же, если я правильно помню, что-то вроде "операторских скобок", отделяющих некоторые блоки отрисовки. То есть, совершенно не обязательно на кадр будет строго один glBegin и строго один glEnd. В отличие от DirectX, упомянутых функций в рамках построения одного кадра может быть несколько. Возможно, у тебя хук срабатывает на тех вызовах, на которых не должен срабатывать, от того и результат такой "замороженный". Попробуй под отладкой найти именно тот, который тебе нужен, чтобы отрисовать объекты сеткой.

Да действительно между glBegin и glEnd обычно указывают вершины, по типу такого:

glBegin(GL_QUADS);
glVertex3f(x, y, z);
glVertex3f(x, y, z);  
glVertex3f(x, y, z);
glVertex3f(x, y, z); 
glEnd();

Тут указываются вершины, так как в аргументах glBegin пришёл GL_QUADS то и вершин соответственно 4

 

По поводу перехватов, я пытался перехватить wglSwapBuffers из той же библиотеки opengl32.dll, читал на зарубежных форумах, там её используют но для рисования всяких там квадратиков и менюшек на эране. Но результат практически тот же что и при перехвате glBegin. К тому же очень странно но справочки по wglSwapBuffers я не нашёл однако она как бы работает и в opengl32.dll как бы есть.

 

Вы предлагаете искать через отладку а в каком смысле? Я же DLL инжектирую, а VS не позволяет отлаживать так как DLL это не исполняемый файл. И разумеется функция GetProcAddress не найдёт больше адресов функции glBegin так как в библиотеке opengl32.dll вроде как не может быть больше таких функций, она же как бы одна и зачем там больше.

glBeginAddress = (DWORD)GetProcAddress(Moduleandle, "glBegin");
Ссылка на комментарий
Поделиться на другие сайты

18 минут назад, Sergey99 сказал:

странно но справочки по wglSwapBuffers я не нашёл

не оно https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-wglswaplayerbuffers

Ссылка на комментарий
Поделиться на другие сайты

21 минуту назад, Sergey99 сказал:

Я же DLL инжектирую, а VS не позволяет отлаживать так как DLL это не исполняемый файл

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

Ссылка на комментарий
Поделиться на другие сайты

26 минут назад, Sergey99 сказал:

И разумеется функция GetProcAddress не найдёт больше адресов функции glBegin так как в библиотеке opengl32.dll вроде как не может быть больше таких функций, она же как бы одна и зачем там больше.

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

 

Ну и можно заюзать вот эту [штуку], чтобы выяснить, как вообще в твоей игре работает конвеер OpenGL. Возможно, это облегчит тебе задачу.

Ссылка на комментарий
Поделиться на другие сайты

6 минут назад, Garik66 сказал:

Функция wglSwapBuffers принимает один параметр HDC контекст устройства, а wglSwapLayerBuffers два параметра, контекст HDC и собственно для чего выполнить смену буферов будто плоскости или overlay. Сами функции разные хотя бы потому что принимают разное число аргументов и разные имена имеют.

Ссылка на комментарий
Поделиться на другие сайты

3 минуты назад, Sergey99 сказал:

Функция wglSwapBuffers принимает один параметр HDC контекст устройства

Очень похоже на стандартную обертку над GDI функцией SwapBuffers.

Ссылка на комментарий
Поделиться на другие сайты

В 21.06.2021 в 13:15, Xipho сказал:

Очень похоже на стандартную обертку над GDI функцией SwapBuffers.

Решил проблемку, как оказалось перехватывать нужно было не glBegin а функцию именно для рисования как в том же Direct3D. Функция называется glDrawElements и рисует примитивы из массива, который приходит четвёртым аргументом.

 

Конечно если почитать справочки от Microsoft и от Khronos, то из описания параметром понятно что где лежит, а вот если почитать справочку [здесь] по этой функции, то немножко запутаться можно в описании четвёртого параметра ну да ладно.

 

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

Спойлер

//Hooked function that will be executed instead of original function in the process
void WINAPI hkglDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices)
{
	//Restores bytes of original function
	BYTE* codeDest = (BYTE*)oglDrawElements;
	codeDest[0] = codeFragment_glDrawElements[0];
	*((DWORD*)(codeDest + 1)) = *((DWORD*)(codeFragment_glDrawElements + 1));

	if (isEnabled)
	{
		if (count > 260) 
		{
			glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		}
		else
		{
			glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
		}
	}
	else
	{
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	}

	oglDrawElements(mode, count, type, indices);

	//Restores bytes of hooked function so process of the game will execute hooked function instead of original function 
	codeDest[0] = jump_glDrawElements[0];
	*((DWORD*)(codeDest + 1)) = *((DWORD*)(jump_glDrawElements + 1));
}

 

При таком раскладе функция будет рисовать линиями только если число примитивов больше 260, а если нет, то рисовать сплошной заливкой. Вопрос теперь в том как сама функция это понимает? Ведь это хук и выполняется также в потоке или в цикле в игре. Эта функция вызывается и ей приходят аргументы и вот пришли ей на пример 261 примитив, то она рисует только один примитив линиями, хотя по логике должна все ведь условие стоит на это. Как такое происходит? Если посмотреть справочку то можно понять что второй параметр это число примитивов для рисования (то есть они все рисуются в этой функции), а условие будто выполняется для конкретного диапазона от 260 как так?

 

Вот результат стараний, темновато но думаю видно:

CrZPdoN.png

8GeeAqH.png

  • Плюс 1
Ссылка на комментарий
Поделиться на другие сайты

Вот тут у тебя избыточное условие. Это видно по дублированию блока else

Спойлер

if (isEnabled)
{
	if (count > 260) 
	{
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	}
	else
	{
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	}
}
else
{
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

 


Предлагаю поменять на 

Спойлер

if (isEnabled && count > 260) 
{
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}
else
{
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

 

А в остальном - молодец, что разобрался и поделился решением. 

 

1 час назад, Sergey99 сказал:

как сама функция это понимает?

Извини, я не совсем понимаю твой вопрос

 

 

ЗЫ. Не забывай код прятать под спойлер.

Ссылка на комментарий
Поделиться на другие сайты

3 часа назад, Xipho сказал:

Извини, я не совсем понимаю твой вопрос

Вот в функции перехвата я проверяю параметр count, который пришёл в функцию glDrawElements, он хранит количество примитивов для рендера и вот по идее рисование линиями должно работать для всех примитивов так как условие выполняется.

 

К примеру если count равен 100 (то есть в функцию пришёл аргумент count со значением 100 то функция не будет рисовать линиями так как условие count > 260, а вот если в функцию придёт 261 то по идее он должен всё нарисовать линиями а получается только один примитив так как 261 > 260. Это то что нужно но сам алгоритм не понятен. Так как по логике условие сработало и выполняется glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); (по идее для всех примитивов)

Изменено пользователем Sergey99
Ссылка на комментарий
Поделиться на другие сайты

1 час назад, Xipho сказал:

Это я, увы, без исследования игры, сказать не смогу

Такая же штучка и в D3D работает к примеру в 9 версии, я пробовал, проверяя значение параметра primCount метода DrawIndexedPrimitive, похоже что функция не рендерит всё сразу а как то по очереди обрабатывает каждый примитив.

Ссылка на комментарий
Поделиться на другие сайты

В 22.06.2021 в 17:07, Xipho сказал:

Это я, увы, без исследования игры, сказать не смогу

Разобрался как работают функции рисования примитивов в этих библиотечках. Всё очень просто, оказывается, что так как функция вызывается очень много раз, практически каждый кадр в игре, то и соответственно рисует она каждый кадр и каждый кадр количество примитивов, которые необходимо нарисовать может отличаться. К примеру в первый кадр рисуется мир, а во второй рисуется картинка с камеры игрока, через которую мы и видим мир игры и штука вся в том, что количество примитивов для рисования картинки с камеры нужно меньше чем для рисования мира (мир - от 100 примитивов, камера от 2 до ~10 примитивов).

  • Понравилось 1
Ссылка на комментарий
Поделиться на другие сайты

×
×
  • Создать...

Важная информация

Находясь на нашем сайте, Вы автоматически соглашаетесь соблюдать наши Условия использования.