Aetherim is the source-code of a shared library that is used to access Unity's IL2CPP methods during run-time. Aetherim should work in both 64-bit and 32-bit programs, as well as on Linux, MacOS, and Windows.
I created this in order to learn more about C++ as well as Unity and how its IL2CPP run-time works.
Check us out on Discord and consider starring the repository if you liked it!
- Table of Contents
- Getting Started
- Usage In Existing Project
- Basic Wrapper Examples
- Initialization
- Getting An Image/DLL
- Getting A Class
- Getting A Nested Class
- Getting All Fields Of A Class
- Getting A Static Field
- Getting A Field's Attribute
- Getting A Method Pointer
- Creating Hooks For Methods
- Invoking A Static Method
- Invoking A Non-Static Method
- Check If A Debugger Is Attached To The Thread
- Contributing
- License
Depending on the operating system that you're using, there's a handful of things to be aware of.
Linux and MacOS users have the best ease-of-use and can just run the compile script.
Windows users have a variety of options, all of which require additional tooling to compile the library.
There is an included script (./compile.sh) that should be used to compile the library.
You should be able to simply run the script and the library will be compiled.
A few different options are open for you.
- Install Visual Studio and compile through it.
- Install Premake and generate a project file, open the project file in the appropriate software, and compile that way.
- Install MinGW and WSL and run the ./compile.sh script.
Options 1 and 2 have been tested and are confirmed to work; option 3 has not been tested, but should work.
If you haven't used Linux before or want the most user-friendly method of compilation, I recommend compiling through Visual Studio.
Add the repository into your project (typically in a /submodules
folder or something).
Consider adding this repository as a git submodule, so that you can update your project easily when Aetherim updates.
git submodule add https://github.com/Toxocious/Aetherim
When Aetherim has updated, you can quickly pull the up-to-date code into your project and continue developing with Aetherim's updated functionality.
Use the following command to pull the up-to-date code into your project.
git submodule update --remote Aetherim
If you would like to remove Aetherim from your project, run the following command and the filetree for Aetherim and its submodule entry in your .gitmodules file will be removed.
git rm <path-to-Aetherim>
Included are some very basic examples of how to use this library, all of which can be found in the ./examples directory.
Windows example file
Linux example file
Mac example file
Be sure to #include
the Aetherim/src/wrapper.hpp
file to your main entrypoint file so that you can access and initialize the IL2CPP wrapper.
Initialize the wrapper by calling the dumper constructor early on in your code.
const auto Wrapper = std::make_unique<Wrapper>();
You now have access to the wrapper and the methods that it provides to you.
The wrapper, upon initialization, automatically gets the IL2CPP domain and attaches it to the thread.
This is necessary in order to prevent some access violation crashes.
Getting an IL2CPP image will provide you access to all classes that it holds, and helper methods to access things within the Image. These images are easily found by dropping a dumped game's files into something like DnSpy.
Get a pointer to an IL2CPP image like so:
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
If found, a pointer is returned, otherwise nullptr
is returned.
After getting an IL2CPP image, you are granted access to any of its classes. You may get a pointer to the class by calling the line below, provided the name of the class that you're looking for.
In this example, we'll get the PlayerHandler class.
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
If found, a pointer is returned, otherwise nullptr
is returned.
From here, PlayerHandler can provide you with various helper methods that allow you to get field and method pointers for fields and methods of the PlayerHandler class, as well as a helper method to invoke methods of the class.
Often times classes will contain subclasses - we can use Aetherim to get the subclass of any class easily.
In this example, we'll get the Inventory subclass of the PlayerHandler.
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
if ( player_handler != nullptr )
{
const auto player_handler_sub_class = Asm_CSharp->get_class("Inventory");
}
If found, a pointer is returned, otherwise nullptr
is returned.
Just like with non-nested classes, you are still able to get all of the fields - static and otherwise - from nested classes.
Aetherim provides an easy way to get every field that a class has.
In the example below, we get all fields of the PlayerHandler class, and print out each field's attribute, name, and offset.
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
const auto player = image->get_class( "PlayerHandler" );
for ( const auto field : player->get_fields() )
{
const auto field_attribute = field->get_attribute();
if ( field_attribute != nullptr )
printf( "\t[Aetherim] PlayerHandler -> %s %s (0x%zx)\n", field_attribute, field->get_name(), field->get_offset() );
else
printf( "\t[Aetherim] PlayerHandler -> %s (0x%zx)\n", field->get_name(), field->get_offset() );
}
Static fields are great. They often provide a pointer to an instance of the class. We can easily get the pointer to a class's static field like so:
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
const auto get_player_instance = player_handler->get_field( "Instance" )->get_as_static();
If found, a pointer is returned, otherwise nullptr
is returned.
These methods may me chained if you don't need to use the initial class or field class for anything, like so:
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_instance = Asm_CSharp->get_class( "PlayerHandler" )->get_field( "Instance" )->get_as_static();
Getting a field's attribute tells you a lot about the field itself and how you can get or use it later.
This has multiple purposes, but the first two that come to mind is using it for SDK generation (to be implemented later on) or to get a field based on its attribute.
What does this mean? The latter will eventually allow Aetherim to fetch a given field through class->get_field()
instead of through both class->get_field()
and class->get_field()->as_static()
, since static fields lie in a different area in memory than non-static fields.
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
const auto get_player_instance_attribute = player_handler->get_field( "Instance" )->get_attribute();
Methods are great, and allow us to do all sorts of things, from hooking based on the returned address, or even invoking the method with whatever parameters we want.
We can get the pointer of a method like so:
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
const auto player_position = player_handler->get_method( "get_position" );
If found, a pointer is returned, otherwise nullptr
is returned.
Once you've gotten a method pointer, you may want to hook it and perform your own logic when the method runs internally.
In this example, we'll use MinHook to hook a method.
#define UFUNC(methodPointer) *(void**)methodPointer
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
const auto player_move = player_handler->get_method( "Move" );
MH_CreateHook(UFUNC(player_move), &playerMove_h, (void**)&playerMove_o);
Invoking a static method is easy. You only need a valid method pointer — no instance or object pointer is required.
You can invoke a static method like so:
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
const auto player_instance = player_handler->get_method( "get_instance" );
if ( player_instance != nullptr )
{
void * params = nullptr;
const auto new_instance =
reinterpret_cast<void *>(
player_instance->invoke_static( params )
);
};
Invoking a non-static method is easy, but can be tricky if you find yourself unable to get a proper instance/object pointer.
You can invoke a non-static method like so:
const auto Asm_CSharp = Wrapper->get_image( "Assembly-CSharp.dll" );
const auto player_handler = Asm_CSharp->get_class( "PlayerHandler" );
const auto get_player_position = player_handler->get_method( "get_position" );
if ( get_player_position != nullptr )
{
const auto instance = player_handler->get_field( "Instance" )->get_static_value();
void * params = nullptr;
const auto position =
reinterpret_cast<Vector3 *>(
get_player_position->invoke(
instance, // instance/object pointer
params // either a void * of params or nullptr
)
);
}
Very basic. Returns a boolean indicating if a debugger is attached to the thread.
const auto Wrapper = std::make_unique<Wrapper>();
const auto is_debugger_active = Wrapper->is_debugger_attached();
In general, we welcome pull requests that fix bugs or builds upon an existing feature.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/feature-name
) - Commit your Changes (
git commit -m 'Add some feature-name'
) - Push to the Branch (
git push origin feature/feature-name
) - Open a Pull Request
There is a dedicated Clang configuration for this repository that will style all code to the required spec of the code-base.
Keep it clean.
This project is licensed under GNU GPL 3.
For more information about the license, check out the LICENSE.