Post

osu! Reverse Engineering | Section 1

Motivation

The idea behind this project was to play around with osu!, to see what hidden features or potentially interesting things I could uncover as well as looking into how in-game modifications work, and how external programs can read the currently playing song and progress, etc. Stream Companion

Overall, I plan on continuing the posts with any more cool finds I discover.

Anti Cheat

The anti-cheat behind osu! is loaded on runtime in a native module osu!auth.dll. Unlike the main executable, this Dll isn’t made in .NET, although the game itself seems to be protected with Eazfuscator.

I will not be diving specifically into the anti-cheat today, however, osu!auth.dll does prevent any debuggers from being attached to the parent process. To focus on the game itself, the contents of the Dll can be removed leaving just the file, since the osu! executable does not perform any form of checksum or integrity checks for the anticheat.

For this writeup I will be using Cheat Engine as my debugger of choice, however obviously that isnt a requirement.

Game Logic

General Game Logic

In order to be able to play around with the game, it is very useful to be able to understand what game-state the game is currently in. This is especially true for a game such as osu! since there are various different states that the game could be in such as: in the editor, in song select, in a multiplayer lobby, etc. This can be useful to judge whether or not we want to modify a certain address or initiate a certain hook.

Rhythm Game Logic

Given that osu! is a rhythm game, it is very likely that its current audio track has some form of timer that dictates what part of the audio / map to play. Since osu! has to sync raw audio and the beatmap together, they must be in sync from an audio timer. This logic can be abused since it is a linearly increasing value as long as an audio track is playing, hence if you can track constantly increasing linear values that only increase while audio is playing, you can be confident that you have found the audio track memory address. This is one of the pieces of logic that we will abuse later in the writeup.

Memory Scanning

Finding the audio timer

As previously stated, the audio timer can be found by scanning for values that linearly increase while audio is playing. This is done by scanning for an initial value of 0 when no audio is playing, and continuously scanning for increasing values at runtime, while audio plays.

From this process I managed to find 6 memory addresses which increased by roughly 1000 every second, hinting at a timer counting in milliseconds. This, confirmed with the fact that it was 0 every single time a track stopped, and when a new one started, I was pretty confident it was the current track timer.

Memory Addresses

As we attach the Cheat Engine debugger to the game, we can see the instructions that write to our audio timer pointer. 0x15D6C44

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
07A59C49 - DB 5C 24 34  - fistp dword ptr [esp+34]
07A59C4D - 8B 44 24 34  - mov eax,[esp+34]
07A59C51 - A3 446C5D01 - mov [015D6C44],eax <<
07A59C56 - D9EE - fldz 
07A59C58 - DD 5C 24 10  - fstp qword ptr [esp+10]

EAX 00000000
EBX 00000000
ECX 46D74F38
EDX 00000000
ESI 211E3298
EDI 211E3328
EBP 00F9F128
ESP 00F9F0E8
EIP 07A59C56

Pattern Scanning

In order to dynamically fetch the audio timer memory address, we can use the hexadecimal values of the instructions that write to the audio pointer to be able to fetch it every time the game starts. To do this, we can use pattern-scanning.

In our case we can generate the pattern DB 5C 24 34 8B 44 24 34 A3 ? ? ? ?. With the ending 4 bytes being our desired pointer represented by wildcards. When scanning for this address in cheat engine with our new pattern, we see that only one result is found, with the memory region being exactly where we disassembled originally.

Pattern Scanning

This is great! However, we seem to have one problem, our pattern takes us to the address 0x07A59C49 which is the start of our original disassembled memory region, but we need to get a pointer to our desired audio timer pointer. If we take a look back at the disassembly

1
2
3
4
5
07A59C49 - DB 5C 24 34  - fistp dword ptr [esp+34]
07A59C4D - 8B 44 24 34  - mov eax,[esp+34]
07A59C51 - A3 446C5D01 - mov [015D6C44],eax <<
07A59C56 - D9EE - fldz 
07A59C58 - DD 5C 24 10  - fstp qword ptr [esp+10]

The pointer that we want, 0x015D6C44, is actually 9 bytes ahead of where the disassembly starts, meaning we actually need the address at 0x07A59C49 + 9.

Pattern Offset

If we test this in cheat engine, we see that if we add 9 to the address found by our signature, we successfully get our desired pointer address 0x015D6C44 which means our pattern works!

Implementation

Now, the idea was to implement these findings into some form of program that we can use as a form of tool to manipulate the game, without having to interact and manually mess around with values in cheat engine, this will be even more apparent when we want to perform any hooks of in-game functions.

In my case I will be using C++ since I’m relatively comfortable with the Windows API however you can use any language you wish to r/w memory. I will also be injecting a Dll into the process as a means of interacting with the game. This will also help with hooking functions etc.

In our entry point for our Dll we have

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "osu/osu.hpp"

int __stdcall DllMain(HMODULE module, DWORD reason, LPVOID reserved)
{
	switch (reason)
	{
	case DLL_PROCESS_ATTACH:
	{
		DisableThreadLibraryCalls(module);
		HANDLE thread = CreateThread(0, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(osu::init), module, 0, 0);
		if (thread)
		{
			CloseHandle(thread);
		}
		break;
	}
	default:
		break;
	}

	return TRUE; // must return 1
}

This is the standard entry point for our Dll, which will create a thread to our main entry point upon injection.

More documentation on this here

osu.hpp and osu.cpp will contain our desired code which we want to execute within the game, such as pattern scanning and manipulating any features we want to play around with.

Currently the thread created executes this these functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "osu.hpp"

void __stdcall osu::init(HMODULE module)
{
	FILE* console_buffer = attach_console();

	osu::execute();

	while (!GetAsyncKeyState(VK_END) & 1)
		Sleep(1000);
	
	if (console_buffer)
		fclose(console_buffer);
	FreeConsole();
	FreeLibraryAndExitThread(module, 0);
}

FILE* osu::attach_console()
{
	AllocConsole();
	AttachConsole(GetCurrentProcessId());
	FILE* console_buffer{};
	freopen_s(&console_buffer, "CONOUT$", "w", stdout);
	SetConsoleTitle("osu!writeup");

	return console_buffer;
}

void osu::execute()
{
	// here we will pattern scan for the audio timer
}

We attach a console to the process and re-route stdout to that console in order to log any useful memory addresses, along with any debugging messages we want to output using the standard library. There is also a loop which breaks upon a key press to prevent the Dll from instantly freeing itself

Now, our current main goal is to implement our earlier findings, and find our audio timer when injecting into the game. This can be done by implementing a simple pattern scanning function, and using it with the pattern we found earlier. As mentioned earlier, we must add 9 bytes to our found signature, in order to get to our desired pointer address.

1
2
3
4
5
6
7
void osu::execute()
{
	// uintptr_t utils::pattern_scan(const char* module, const char* signature)
	uintptr_t audio_pointer = utils::pattern_scan("osu!.exe", "DB 5C 24 34 8B 44 24 34 A3 ? ? ? ?") + 9;

	std::cout << "Audio Pointer - " << std::hex << audio_pointer << "\n";
}

Injecting this into the game, we see a console window attach to the process with the correct memory address output! Memory output

Since we now have the current audio time pointer, we can now start reading it. To constantly read this however, we will need to execute osu::execute constantly while the Dll is injected. To do this, we make audio_pointer static to prevent constant redefinition and futile pattern scanning, and place osu::execute within the loop in osu::init like so:

osu::init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __stdcall osu::init(HMODULE module)
{
	FILE* console_buffer = attach_console();

	while (!GetAsyncKeyState(VK_END))
	{
		osu::execute();
		Sleep(10);
	}

	if (console_buffer)
		fclose(console_buffer);
	FreeConsole();
	FreeLibraryAndExitThread(module, 0);
}

osu::execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void osu::execute()
{
	/*
	07A59C49 - DB 5C 24 34  - fistp dword ptr [esp+34]
	07A59C4D - 8B 44 24 34  - mov eax,[esp+34]
	07A59C51 - A3 446C5D01 - mov [015D6C44],eax <<
	07A59C56 - D9EE - fldz 
	07A59C58 - DD 5C 24 10  - fstp qword ptr [esp+10]
	*/

	// uintptr_t utils::pattern_scan(const char* module, const char* signature)
	static uintptr_t audio_pointer = utils::pattern_scan("osu!.exe", "DB 5C 24 34 8B 44 24 34 A3 ? ? ? ?") + 9;
	static uintptr_t audio_address = *reinterpret_cast<uintptr_t*>(audio_pointer); // address of current audio time

	int current_time = *reinterpret_cast<int*>(audio_address);
	std::cout << "Current audio track time - " << current_time << std::endl;
}

With this new code, lets un-inject the currently loaded Dll by pressing END and re-inject back into the game.

Final audio timer

As you can see, it successfully reads the memory address, and its incrementing while audio is playing!

This could be used to make an in-game replay editor, or for stream overlays to get the current audio track playing. In the future I will also delve into how to get the current song and artist playing, and show how stream overlays for this game actually work.

On another note, you may be wondering, will this bypass any of the anti-cheat? Well… to cut it short. no. I will be going deeper into how osu!auth.dll works in the future, but honestly, this current way of injecting and uninjecting is extremely easy to detect, especially with LoadLibrary injection. So dont get your hopes up thinking its this easy to put together a cheat :D

Final Thoughts

Although you may think that finding the audio timer in this case wasnt that useful, its mostly just a proof of concept to show how reverse-engineering can be applied to concepts and games like this. In the future sections, I will implement some other features and actually manipulate the game, however this was a light introduction to how osu!’s game structure can be reverse-engineered.

Overall, I enjoyed writing the code and the write-up for this project, mostly as a fun exercise and to improve my reverse-engineering skills and to apply some of what I was learning to a game I enjoy. This type of system could be used for stream overlays and in-game overlays and such, more of which I will go into in future articles.

If you have any questions or queries, feel free to contact me on discord just.cabbage!

This post is licensed under CC BY 4.0 by the author.

Trending Tags