-
Notifications
You must be signed in to change notification settings - Fork 5
Technical Details
The loader EEex.exe
will inject the EEex.dll
into the Enhanced Edition (EE) game.
The EEex.dll
searches the EE game for known EE game functions and EE game global variables by matching byte patterns defined in the EEex.db
pattern database. EEex.dll
also looks for a specific address to patch a redirection to a EEexLuaInit
function located within itself.
The EEexLuaInit
function, when called by the EE game engine, registers the Lua EEex_Init
function with the Lua state and sets it to globally visible.
The Lua EEexInit
function in turn registers and sets global visibility to other Lua functions defined in EEex.dll
. These Lua functions are then available for usage from within EEex Lua files and game script files.
- Check game executable exists
- Verify game executable
- Check other files exists
- Launch game executable
- Inject EEex.dll
EEex.exe
is the loader. It loads the game executable and injects the EEex.dll
. It can be ran from a command console or by double clicking on the EEex.exe
file.
EEex.exe
checks if a known EE game executable can be located in the current directory. It looks for one of the following: Baldur.exe
, SiegeOfDragonspear.exe
, Icewind.exe
or Torment.exe
. If none are found a message is displayed to the user and the program exits.
Once the EE game executable is located, it's File Version is compared to determine if it is the latest known version. If it is not a message is displayed to the user and the program exits. The File Version can be viewed by right clicking on the EE game executable, selecting properties and the details tab.
Additional checks are done to verify if EEex.dll
, EEex.db
and M__EEex.lua
files exists. If any are not found a message is displayed to the user and the program exits.
The EE game executable that was previously found, is launched using the CreateProcess, but with the dwCreationFlags
parameter set to CREATE_SUSPENDED
.
CREATE_SUSPENDED
- The primary thread of the new process is created in a suspended state, and does not run until the ResumeThread function is called.
By creating the EE game process in a suspended state we easily obtain the process handle for the EE game process and prevent it from running until we are finished with it.
A small size of memory is allocated inside the suspended EE game process using VirtualAllocEx - exactly enough is allocated for the length of the full path and filename to the EEex.dll
file. The address of this allocated memory is stored for later use. The full path and filename is written into the EE game process memory (at the location allocated) using WriteProcessMemory. For example: C:\BGIIEE\Data\00783\EEex.dll
The address of the LoadLibraryA function is obtained and stored for later use.
A new remote thread is created to run inside the EE game process using CreateRemoteThread with the lpStartAddress
parameter being set to the address of the LoadLibraryA function and the lpParameter
parameter being set to the address of the full path and filename to the EEex.dll
file (which was allocated and written to the EE game process earlier).
When the remote thread is executed in the EE game process it will allow for the LoadLibraryA function to be called with it's parameter being the address of the full path and filename to the EEex.dll
file. This will then load the EEex.dll
into the EE game process.
ResumeThread is then called to continue the EE game process that was suspended earlier (via CreateProcess with dwCreationFlags
set to CREATE_SUSPENDED
)
The EE game continues as normal, calling the DllMain in EEex.dll
via the LoadLibraryA call created by the CreateRemoteThread
- Initialization
- Game module information
- Import patterns
- Verify imported patterns
- Verify Type 2 patterns
- Search for patterns
- Patch luaL_loadstring
- Patch SDL_LogMessageV
- Function addresses
- Variable values
- The EEexLuaInit function
- The Lua EEex_Init function
Execution begins once the EE game process calls DllMain in EEex.dll
.
Options are read from the EEex.ini
. If no EEex.ini
file exists a new one is created with default values for the options it contains. The EE game type is detected and the addresses of some exported functions in the EE game are obtained and stored for later. Basic game information is logged to the EEex.log
file.
Some detailed information about the EE game executable is collected and logged (for debugging purposes). Information is also collected relating to the address of and size of the .TEXT
section in the EE game, which is used later on for searching and comparing byte patterns for functions and variables used in the EE game.
The EEex.db
pattern database file is read to collect all the section names and to count how many patterns are to be imported. See the EEex.db Pattern Database file for details.
An array (table) of PATTERN
entries (records), multiplied by the total patterns to import, is created in memory for storing all the imported patterns in a structured format, this table is pointed to by the PatternsDatabase
variable. Storing the patterns in a structured format allows for easier processing of the patterns later on. Each pattern entry (record) is stored in the following structure:
PATTERN STRUCT
bFound DD 0 ; Found flag
PatType DD 0 ; Pattern type
PatBytes DD 0 ; Pointer to pattern byte
PatLength DD 0 ; Length of PatBytes
VerBytes DD 0 ; Pointer to verify bytes
VerLength DD 0 ; Length of VerLength
PatAdj DD 0 ; +/- if PatBytes matched
VerAdj DD 0 ; +/- from PatBytes matched
PatName DD 0 ; Pointer to function/variable name
PatAddress DD 0 ; Address of function/variable
PATTERN ENDS
Each pattern name (ini section name [<PatName>]
) is read to get the pattern bytes (PatBytes
) key value. This is converted from the text representation of hexadecimal characters to raw byte values and stored in an allocated block of memory. The address of this block of memory is stored in the PatBytes
field of the current PATTERN
entry (record). The length of the pattern bytes is stored in the PatLength
field.
If the verify bytes (VerBytes
) key value exists this is converted from text to raw bytes, stored in allocated memory and the address to this is stored in the VerBytes
field, along with the length of the verify bytes in the VerLength
field.
The PatAdj
, VerAdj
and PatType
key value's are also read and stored in the similarly named fields in the current PATTERN
entry.
The EEex.ini
file is read to obtain the address of previously found patterns. The key value pair is in the format of <PatName>=<address>
. If successfully read, the address is stored in the PatAddress
field in the the current PATTERN
entry. The exception to this is for patterns that are Type 2 (PatType
= 2
) patterns.
Type 2 patterns are stored in the EEex.ini
as an ini section name [<PatName>]
with a Count=<x>
key value pair followed by multiple entries in the format of <n>=<address>
where n
is 0 - (x-1)
. For example:
[CreStructExpand]
Count=42
0=0x0051C21E
1=0x0051E0A4
...
n=0x01234ABC
The Count
value is stored in the VerAdj
field of the current PATTERN
entry. The PatAddress
field is not set for Type 2 patterns and these Type 2 patterns are skipped for the verification process. The multiple entries in Type 2 patterns are processed separately later on.
The bFound
in PATTERN
is not set at this stage, this is used later on.
The total patterns imported is logged in the EEex.log
file
EEex.dll
logs found patterns to EEex.ini
with the address of the found pattern as key value pairs in the format of <PatName>=<address>
. <PatName>
is typically the name of the function or global variable. Examples:
PatchLocation=0x005167E0
_lua_pushnumber=0x004B5960
CGameSprite::~CGameSprite=0x006D5D90
_g_lua=0x005167DC
g_pChitin=0x00785207
Note: PatchLocation
is a special case as it is the location used to hook the EE game to implement the Lua support for EEex.
Searching for patterns is time consuming and as such is is preferable to verify that a logged pattern is found at the address specified.
Each PATTERN
entry in the PatternsDatabase
table is processed. For entries with PatAddress
that are not 0
(new patterns and Type 2 patterns have PatAddress
as 0
) these are checked to verify that the raw bytes at this location matches the pattern bytes pointed to by the PatBytes
field. The PatAdj
field value is used to increment/decrement the starting offset of the address to begin the comparison at.
If the byte comparison matches a further verification can occur if the VerBytes
field is not 0
. Using the verify bytes pointed to by theVerBytes
field, and using VerAdj
to adjust the starting offset from the found address, these bytes are compared to verify that the pattern is indeed correct.
Once a pattern has been successfully verified the bFound
field of the PATTERN
entry is set to TRUE
(except for Type 2 patterns).
Type 2 (PatType
= 2
) patterns are not processed or verified at this stage.
On a first run there will not be any found patterns logged, which will force a search for patterns.
After the imported patterns are verified, Type 2 (PatType
= 2
) patterns are then processed next for verification.
Each PATTERN
entry in the PatternsDatabase
table is processed. Only entries with PatType
= 2
are processed.
The Count
value as stored in the VerAdj
field of the current PATTERN
entry is read and used to create an array (pType2Array
) to store addresses in.
Type 2 patterns are stored in the EEex.ini
as an ini section name [<PatName>]
with a Count=<x>
key value pair followed by multiple entries in the format of <n>=<address>
where n
is 0 - (x-1)
. For example:
Each address entry in the ini section name [<PatName>]
for the Type 2 pattern is read and these are checked to verify that the raw bytes at this location matches the pattern bytes pointed to by the PatBytes
field. The PatAdj
field value is used to increment/decrement the starting offset of the address to begin the comparison at. Note: the same PatBytes
and PatAdj
field values are used to compare each address entry as there is expected to be multiple addresses that match.
If all the addresses in the Type 2 pattern match successfully then the bFound
field of the PATTERN
entry is set to TRUE
. The PatAddress
field of the PATTERN
entry is set to the array of addresses pointed to by pType2Array
.
If any of the addresses of the Type 2 pattern fail to verify or the Count
/ PatAdj
value is 0
(first run) then verification will fail, forcing a search for patterns.
Each PATTERN
entry in the PatternsDatabase
table is processed. For entries that have the bFound
field of the PATTERN
entry is set to TRUE
these are skipped, having previously been verified.
The EE game memory is searched for a pattern, starting at the address of the .TEXT
section (found previously during the [Game module information](#Game module information) stage). The bytes in memory are checked against the bytes pointed to byPatBytes
in the current PATTERN
entry.
If the byte comparison matches, a further verification can occur if the VerBytes
field is not 0
. Using the verify bytes pointed to by theVerBytes
field, and using VerAdj
to adjust the starting offset from the found address, these bytes are compared to verify that the pattern is indeed correct.
Once a pattern has been found the bFound
field of the PATTERN
entry is set to TRUE
(except for Type 2 patterns).
Type 2 (PatType
= 2
) patterns don't use the VerBytes
as they are expected to have multiple occurrences of the same pattern. For Type 2 patterns that are found, a dynamic array is increased on each iteration found, (the pointer to this array is stored in the PatAddress
field), the address at which the iteration is matched is stored in the array and the count of the total matches is stored in the VerAdj
field of the current PATTERN
entry.
Once the entire EE game memory has been searched for the current PATTERN
entries PatBytes
the pointer is moved to the next PATTERN
entry in the PatternDatabase
table and the whole looped process occurs again searching for and comparing bytes in memory.
Once all PATTERN
entries have been searched, all Type 2 patterns are marked as found, and some checking is done to see if any patterns previously imported and not verified have been missed. If so a message is displayed to user that an error has occurred. Any missing patterns may result in strange and unpredictable behavior by the EE game using EEex.
If all patterns have been found, then the addresses of the found patterns are written to the EEex.ini
file to speed up subsequent runs, by just verifying memory addresses for PatBytes
instead of having to re-search the entire EE game memory each time.
New patterns added to the EEex.db
pattern database will trigger the full search, but will be limited to only those PATTERN
entries that do not have the bFound
field set to TRUE
after verification.
The PatchLocation
pattern is defined as:
[PatchLocation]
PatBytes=83C43085C075145050506AFF50FF35
PatAdj=-5
VerBytes=83C4186A00
VerAdj=24
The matching bytes 83C43085C075145050506AFF50FF35
are highlighted in red in the image above. Adjusting the address at which this pattern is found by the PatAdj
value (-5
) gives us the location to patch the call to _luaL_loadstring
.
Note: The same pattern can actually be found in another location in the EE game, and thus we use the VerBytes
and VerAdj
values to further refine the matching to a specific location, by matching the verify bytes (VerBytes
) at this additional location (adjusting found pattern address by the VerAdj
value).
The bytes at the PatchLocation
address shown above in blue are modified in the EE game executable to change the following function call:
FF35 0C019400 | push dword ptr ds:[<_g_lua>]
E8 0B1AFAFF | call <baldur._luaL_loadstring>
to:
FF35 0C019400 | push dword ptr ds:[<_g_lua>]
E8 3BB4CA68 | call eeex.EEexLuaInit
Note: if disassembled it will actually show a specific address, for example: E8 3BB4CA68 | call eeex.691C1C20
(replaced in example above with EEexLuaInit
to help with understanding). Additionally the reference to the modulebaldur
in the example above may be different depending on the EE game that is using EEex.
When the EE game continues its normal execution, after EEex.dll
has finished loading and processing, it will at a later time come to the PatchLocation
address which is now redirecting execution to the EEexLuaInit
function located within EEex.dll
To allow for console output if EEex.exe
is started via a command line, and also to log output to the EEex.log
file, the call SDL_LogMessageV
in the SDL_Log
exported function in the EE game is patched to redirect to functions in EEex.dll
to provide that additional logging support.
The address of the exported SDL_Log
function is obtained via GetProcAddress early on during Initialization. This address is taken and adjusted to point to the location where the call SDL_LogMessageV
in the SDL_Log
function is.
The bytes at the SDL_LogMessageV location are modified in the EE game executable to change the following function call:
E8 2D010000 | call <baldur.SDL_LogMessageV>
to:
E8 6DE3C674 | call <eeex.EEexSDL_LogMessageV>
Note: if disassembled it will actually show a specific address, for example: E8 6DE3C674 | call eeex.75422190
(replaced in example above with EEexSDL_LogMessageV
to help with understanding). Additionally the reference to the modulebaldur
in the example above may be different depending on the EE game that is using EEex.
Each PATTERN
entry in the PatternsDatabase
table is processed. The pattern name (PatName
) string is first checked to see if it begins with _lua
and if it does it is then compared against specific Lua function name strings. If a match occurs the PatAddress
value is saved to a global variable.
These global variables are prefixed with F_
for function. Prototypes for each of the functions are previously defined to allow for calling these functions directly in the EE game via the use of Invoke
or with the call
instruction. For example the address of the _lua_pushnumber
function is stored in a F_Lua_pushnumber
global variable, and can be called as:
push eax
fild dword ptr [esp]
sub esp, 4h
fstp qword ptr [esp]
push dword ptr [ebp+8h]
call F_Lua_pushnumber
or via Invoke
like so:
fild dwPatternAddress
fstp qword ptr [qwAddress]
Invoke F_Lua_pushnumber, lua_State, qwAddress
Type 1 (PatType
= 1
), Type 3 (PatType
= 3
) and Type 4 (PatType
= 4
) patterns are processed after all patterns have been imported, verified and/or searched for.
These pattern types are used to read a value at the address of the pattern found (stored in the PatAddress
field of the PATTERN
entry).
For Type 1 patterns, a DWORD
is read at the PatAddress
address and this value is stored in the PatAddress
field, replacing the previous value.
For Type 3 patterns, a BYTE
is read at the PatAddress
address and this value is stored in the PatAddress
field, replacing the previous value.
For Type 4 patterns, a WORD
is read at the PatAddress
address and this value is stored in the PatAddress
field, replacing the previous value.
Note: the original PatAddress
value is not required after reading the address, thus it is save to re-use the same field in the PATTERN
entry to store the new content
Additionally the _g_lua
Type 1 pattern when found during this process is stored in a global variable for later use with using the Lua state in the EE game and using the Lua functions stored in the EEex.dll
. It is stored in a p_lua
variable, which is itself a pointer to the g_lua
Lua state.
EEexLuaInit
is a cdecl
function that is stored in the EEex.dll
and is called by the EE game due to the redirection placed earlier in the EE game process memory, which changed the call _luaL_loadstring
to a call EEexLuaInit
instead.
EEexLuaInit
resolves the p_lua
variable to obtain the g_lua
EE game Lua state.
EEexLuaInit
registers with the EE game's global Lua state a new Lua function: EEex_Init
, which is located in the EEex.dll
. It then calls the _luaL_loadstring
that was replaced by the patch (hook) and resumes execution back to just after the PatchLocation
to continue as normal.
When the EE game processes its own Lua files as normal and encounters an EEex_Init
function, it will call the EEex_Init
Lua function - which was previously registered and is located in the EEex.dll
EEex_Init
is a cdecl
function that registers additional Lua helper cdecl
functions for the EEex project: EEexExposeToLua
, EEexWriteByte
, EEexCall
and EEex_AddressList
. In addition EEex_Init
also allocates an additional block of memory via VirtualAlloc and returns the address pointing to this block of memory on the Lua stack. This memory is used by the EEex project for various means.