The "Universal Injector Framework" (UIF) is an extensible library that can be used to modify the behavior of existing Windows applications.
UIF comes bundled with several features, primarily designed for localization of Japanese games.
The behavior of all features can be enabled and controlled with a config.json
file located in the same directory as the proxy dll. Some features are mutually exclusive, because any one game function can only be modified by one feature at a time.
The configuration is a JSON file, which is easily readable by both people and machines. All config options must be contained within a root object, denoted by a pair of curly braces.
The following sections will explain all the default features and their configuration options.
- Injector
- Allocate Console
- Start Suspended
- Character Substitution
- Tunnel Decoder
- Font Manager
- Locale Emulator
- Window Manager
- File Monitor
- Play Timer
This isn't actually a feature, but rather the general configuration for the injector itself (the component that is responsible for managing the other features). These are the available options:
If /injector/enable
is set to false, the injector will just load the original dll and terminate immediately. If the field does not exist or contains any other value, the injection process will continue as usual.
The target_module
option lets you specify which module's entry point should be highjacked to perform the injector initialization. In most cases this will be the app executable, so omitting the option will default to that. This is mainly useful when the injector is not a direct dependency of the .exe, but loaded at runtime - either via an explicitly LoadLibrary call or via delay-loading.
The print_loade_modules
option - if set to true - will print a list of all loaded modules before injector initialization. This is purely meant for debugging purposes.
hook_modules
is an array of names referring to modules loaded by the target executable. When a feature attempts to hook an import, the imported function will be hooked in the main executable as well as in all modules listed here. Make sure this list does not contain the name of your proxy dll, as that would mess with the imports of the injector itself.
load_modules
is an array of strings, each of which contains the path to a dll file that should be loaded on startup. You might want to hook functions in a dependency of the target executable via hook_modules
. This will fail if the feature initialization is run before said dependency is loaded, so add it to this list to force it to be loaded before initialization.
{
"injector": {
"enable": true,
"target_module": "Engine.dll",
"print_loaded_modules": false,
"load_modules": [
"plugin/EngineHelpers.dll"
],
"hook_modules": [
"EngineHelpers.dll"
]
}
}
The first feature is very simple and has just one config option. It opens a console window to display relevant information. It is recommended to enable this feature during testing, but disable it once you distribute the proxy.
When starting the program through the command line, the feature will first attempt to attach to the existing console.
To enable the console allocation, set the option /allocate_console
to true:
{
"allocate_console": true
}
Another fairly simple feature, which simply adds a delay before the target application has a chance to load. This can be used to attach external tools like a debugger or capturing software like RenderDoc, which needs to be injected before the graphics API is initialized.
To enable the feature, set the option /start_suspended/enable
to true. You can either specify a time to wait (in milliseconds) with the wait_time
option, or wait for the user to press enter in the console by setting wait_for_input
to true.
Please note that for the second option to work, you need to enable console allocation. Otherwise the program will wait indefinitely since there is no way to press enter in a nonexisting console.
{
"start_suspended": {
"enable": true,
"wait_time": 3000,
"wait_for_input": false
}
}
This feature allows to replace certain characters by others. It can be used to print characters not supported by the application due to the character set. Many Japanese game engines use the Shift-JIS charset, which doesn't have support for some characters used in the English language.
The character substitution feature can be used to replace supported characters you don't need by characters you do need.
To enable the feature, set the /character_substitution/enable
option to true. You will also need to set the source_characters
and target_characters
options to tell the feature which characters should be mapped to which.
The configuration file below would replace all occurrences of ア
by á
, イ
by í
, and so on.
{
"character_substitution": {
"enable": true,
"source_characters": "アイウエオ",
"target_characters": "áíúéó"
}
}
The tunnel decoder is a more powerful, but more complicated alternative to the character substitution feature. It implements the Shift-JIS tunnel encoding developed by arcusmaximus, which maps up to 3422 characters to unassigned Shift-JIS codepoints.
To enable the feature, set the /tunnel_decoder/enable
option to true. Other than that, there is only one option: the mapping
. This should be the same as returned by the corresponding tunnel encoder.
{
"tunnel_decoder": {
"enable": true,
"mapping": "éáäöüß"
}
}
The font manager is one of the more complicated features and has a lot of options to configure. To enable it, set the /font_manager/enable
option to true.
The resource_files
option accepts an array of strings, each of which refers to a font file on disk. These will be added to the list of fonts available to the application, without the need to install them in Windows.
Some Japanese game engines will filter the fonts available to the player. Which fonts will be listed can be modified by the spoof_enumeration
sub-feature of the font manager. You can enable it by setting /font_manager/spoof_enumeration/enable
to true.
When enabled, enumeration spoofing will allow you to define your own filters for which fonts should be available to the engine.
filter_pitch_and_family
allows you to filter by font pitch type (whether it is monospaced or not) and style (for example only sans-serif fonts).
To only allow monospaced fonts, set the value to 1, for variable pitch set it to 2. To allow both, set it to 0.
For more information read the description of the iPitchAndFamily
parameter in the Microsoft Documentation.
filter_charset
allows you to only allow fonts, which support a certain character set. Japanese engines often set this to SHIFTJIS_CHARSET (128), which excludes most fonts, since they don't contain kanji.
You probably want this to be either 1 (allow only ansi-compatible fonts), or 0 (allow all fonts).
spoof_charset
specifies a value that will be reported to the engine as the real charset of each font. This is necessary in case the engine applies its own filter logic to the returned list of fonts. For Japanese engines, this value should likely be 128.
If you use the spoof_charset
option mentioned above, you need to enable this sub-feature as well, because the engine will now try to create fonts with the spoofed charset value, which Windows will not accept. In this case, enable this sub-feature by setting /font_manager/spoof_creation/enable
to true.
The override_charset
option will be used to override the spoofed charset value from earlier. Basically we lied to the engine, pretending that all fonts support a certain charset, so now the engine will try to load a font with support for a charset it doesn't have. To mitigate this issue, set the override_charset
option to 0.
Finally there is the override_face
option, which allows you to override which font will be loaded, no matter what font is requested by the engine.
{
"font_manager": {
"enable": true,
"resource_files": [
"ARLRDBD.TTF"
],
"spoof_enumeration": {
"enable": true,
"filter_pitch_and_family": 0,
"filter_charset": 1,
"spoof_charset": 128
},
"spoof_creation": {
"enable": true,
"override_face": "Arial Rounded MT Bold",
"override_charset": 0
}
}
}
This feature allows you to automatically relaunch the application if it is not launched with the correct ansi code page. To enable it, set the option /locale_emulator/enable
to true.
In order to use this feature you will also need to download LoaderDll.dll
and LocaleEmulator.dll
from the Locale Emulator project and place them next to your app executable.
There are several options to specify the desired locale environment.
codepage
specifies which code page your application expects to run with. The default value is932
, which specifies the JapaneseShift-JIS
code page. If the application is not stared with this code page, it will attempt to relaunch itself via Locale Emulator.locale
specifies the desired LCID, with a default value of1041
(Japanese).charset
specifies the default character set. Its default value is128
, the value of theSHIFTJIS_CHARSET
constant.timezone
allows you to specify a time zone to be emulated. The default value isTokyo Standard Time
. Available time zone names can be found underHKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones
.
Finally, the wait_for_exit
option controls when the parent process terminates. If set to true, it will wait until the child process ended, otherwise it will exit immediately.
{
"locale_emulator": {
"enable": true,
"codepage": 932,
"locale": 1041,
"charset": 128,
"timezone": "Tokyo Standard Time",
"wait_for_exit": false
}
}
The window manager currently only allows changing the title of windows opened by the application. To enable it, set the option /window_manager/enable
to true.
If the option overwrite_title
is set, all newly created windows will use the specified title instead.
In case the window was already created before the proxy dll is loaded, set the process_existing_windows
option to true. This will update the titles of all existing top-level windows of the current process.
{
"window_manager": {
"enable": true,
"overwrite_title": "My Window Title",
"process_existing_windows": true
},
}
The file monitor feature hooks into the Windows CreateFile API and allows you to perform a number of actions when specific files are accessed. To enable the feature, set the option /file_monitor/enable
to true.
You can simply log all file accesses to the console by setting the log_all
option to true, but the primary function is controlled by the actions
option.
It is an array of objects, where each object specifies a file name filter and an action to apply when a file matching the filter is accessed.
There are two ways of specifying the file name filter. The first option is to declare a path
property, which contains a file path using Windows wildcard notation (*
for any string, ?
for any character). Backslash characters are automatically normalized to forward slashes. The matcher will check if the wildcards can be expanded in such a way that the result is a suffix of the accessed file path. In case this is not flexible enough for your needs, the second way is to instead declare a pattern
property containing a regular expression.
Apart from the file path, you can also filter by the dwDesiredAccess flags by defining the access
property. The string may contain the characters r
, w
and x
(case insensitive), which match accesses using the GENERIC_READ
, GENERIC_WRITE
and GENERIC_EXECUTE
flags respectively. Omitting the property will ignore the flags.
First and foremost, you can log accesses matching the filters by setting the log
property to true. You can also trigger a software breakpoint (__debugbreak()
) by setting the breakpoint
property to true.
Finally, you can redirect the access to a different path by specifying the redirect
property.
It is used as the format parameter to std::regex_replace
and can contain back-references ($1
) to capture groups defined in the filter pattern. If the path
property was used instead of pattern
, capture group 1 refers to the wildcard-expanded path (excluding any prefix not part of the filter path).
{
"file_monitor": {
"enable": true,
"log_all": false,
"actions": [
{
"path": "*.png",
"log": true,
"breakpoint": false
},
{
"pattern": "^.*config\\/(.*)$",
"access": "w",
"redirect": "config-sink/$1"
}
]
}
}
The play timer allows you to track how long a particular application has been running. It creates a separate file containing a list of all sessions with their duration as well as start and end times, as well as a cumulative total.
To enable the play timer, set the option /play_timer/enable
to true. By default, the play time information will be written to play_timer.json
, but the file location can be changed with the save_file_path
option.
The play timer will automatically update this file every time the application exits. In addition to this, you can enable the auto save sub-feature by setting /play_timer/auto_save/enable
to true. This will update the save file every 60 seconds. The number of seconds between saves can be changed with the interval
option.
The auto save feature is useful in case the application crashes, because then the current session's time would otherwise be lost. The play timer will automatically recover and synchronize the information on the next start.
{
"play_timer": {
"enable": true,
"save_file_path": "playtime.json",
"auto_save": {
"enable": true,
"interval": 10
}
}
}
UIF comes as a Visual Studio solution, so building should be as simple as loading it in VS and hitting build. Note that it is targeted at platform toolset version 143, so make sure the correct C++ build tools are installed. To use the generated dll on older Windows versions, you might need to install the VC++ Redistributables on the target machine.
Make sure to select the build configuration applicable to your chosen proxy and the correct target platform. If none of the provided proxies suit your needs, you will need to add your own one.
The generated library will be located in src\Build\UniversalInjectorFramework\$(Platform)\$(Configuration)\
It is possible to run your target application right inside of Visual Studio, which allows you to debug your injected code while it is running in the target process. To do this, create a symbolic link to your proxy dll inside the target application's directory (for example with Link Shell Extension). This way the app is always going to load the most recent build of the dll. Next, open the project properties dialog, navigate to the "Debugging" tab and set the "Command" and "Working Directory" fields to the path of your target application and its containing directory respectively.
Now you can simply set a breakpoint and start the program through the VS Debugger.
All feature implementations are located inside the "features" directory and are derived from the feature_base
class. They provide an initialize
and a finalize
method, which are invoked when the dll is loaded and unloaded respectively.
The initialize
method will usually parse the feature configuration and install any hooks required for it to work.
The finalize
method should make sure to uninstall all hooks added during initialization.
An empty feature stub exists, called custom_feature
. It is already pre-configured, so you can simply start writing your custom logic in there.
Should you choose to add a new feature anyway, make sure to add a call to initialize_feature
in injector::attach
.
A proxy library is a .dll (dynamic link library) file that pretends to be a dependency of your target application.
When the application starts, the Windows image loader loads all libraries the program requires to function. By placing our proxy dll in the same directory as the executable, it shadows the actual file and will be loaded instead.
This allows us to execute our own code within the application process.
Since the application tried to load the original library in order to import some of its functions, our proxy dll will need to export these functions or the process will be unable to start.
UIF can mimic the following Windows dlls by default:
- d3d8.dll
- d3d9.dll
- d3d11.dll
- d3dcompiler_43.dll
- d3dcompiler_47.dll
- d3dx9_43.dll
- dxgi.dll
- opengl.dll
- winmm.dll
If your target application doesn't depend on any of these, you will need to add another proxy using the following steps:
To create a new proxy, build the included "ProxyGenerator" project and run ProxyGenerator.exe library.dll
, where "library.dll" is the library you want to generate a proxy for.
This will generate three files (.h, .cpp and .def), which you need to copy to src/UniversalInjectorFramework/proxies
and add to the project within Visual Studio.
Next, open libraries.cpp
, include the newly added header and add new load_library
and unload_library
calls to the existing ones.
Now you need to create a build configuration for the new proxy. Open the Visual Studio Configuration Manager located in the "Build" menu, click on the dropdown in the top left and choose "New...". Enter the name of your dll file (without the extension) in the Name field and select one of the existing configurations to copy from.
Finally, we need to add some custom settings to the project file. Right click the "UniversalInjectorFramework" project (not the solution itself!) in the Solution Explorer and choose "Unload project". This should open the XML project file in the editor.
Find the section labeled <!-- configurations -->
and add the following XML snippet:
<ItemDefinitionGroup Condition="'$(Configuration)'=='yourlibname'">
<ClCompile>
<PreprocessorDefinitions>UIF_LIB_YOURLIBNAME;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<ModuleDefinitionFile>proxies\yourlibname.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
Make sure the first yourlibname
exactly matches the name of the build configuration and the last one is the name of the .def file you copied earlier.
The second YOURLIBNAME
should be the same string, but with all lowercase letters replaced by uppercase ones. The whole string UIF_LIB_YOURLIBNAME
must be the same as in line 3 of the .cpp file we generated at the start.
Now load the project again and you are ready to continue with the steps described in the build section.