Launching Final Fantasy XIV on macOS
FINAL FANTASY XIV is an MMORPG for Windows, macOS, and PlayStation. The Mac version is essentially an identical build of the Windows version, except it runs in a customised version of Wine. The launcher / patcher for FFXIV is pretty famously disliked, and for Windows users there’s a very good custom one, but unfortunately it doesn’t work for Mac. So I made my own.
One of the more interesting challenges in doing so was dealing with how the FFXIV Launcher passes arguments between the launcher and the main game executable. In this post, I’ll do a deep dive into my journey of discovery to reimplement this logic in Swift.
Why A Custom Launcher?
My launcher isn’t anywhere near as featureful as XIVLauncher. I don’t really have the time to dedicate to reimplementing its enormous feature set, plus some of its features would be very difficult to implement in macOS due to its onerous security policies and the fact that the game runs in Wine.
That said, the main things my launcher brings to the table are:
A Native macOS User Interface
The default launcher, already largely just a Web View, also runs in Wine, so the experience is extremely jarring and out of place on a Mac.
Save Password
On macOS username + password combos can be safely and securely stored in the Keychain. The default launcher only allows you to save your username.
The Algorithm
If you load up Process Explorer on a Windows machine while FFXIV is running you will see something like this:
Command line:
"C:/Program Files (x86)/Steam/steamapps/common/FINAL FANTASY XIV Online/game/ffxiv_dx11.exe" "//**sqex0003GLBqsICCnUUr1zHEXvdQZUE-k385enBr_CALwmVqQbuVPSuvbjJlbOvdY1VxxfYsl_l1aNT7LOqY1hXgMFApoeNwsc9knIUdWVWhV7yN_Y-fWjlwbN--IHtqp1Yr_NmtCN9W4CyB7Cn3asasHYWjuLT4KZDY_1JC8sluramSAH3csIL6xvhdkJA1_QoQclBco327gI6s-7SzfhWpqkfXinp0ZiDaufVOoCByrYyDYvyoykEmcOZcgEU81dMCUM_xlS8Fz6MXXkaRhFt3Y0fxQ_M4H0UUJnBRF15wb7Ayw0wQF6tFnwn52b4G36S6nG_wv3aXC-yVZZ_HPTbaaCW9aSefxy1xv5yTTTgC2d5vkSGdMaInqdHkQ7FcNhfZr9jlVfcWRNnPveFijgBG2rb7lWYUESWOBpTTp**//"
Notice how the application is started up with just one argument:
//**sqex0003GLBqsICCnUUr1zHEXvdQZUE-k385enBr_CALwmVqQbuVPSuvbjJlbOvdY1VxxfYsl_l1aNT7LOqY1hXgMFApoeNwsc9knIUdWVWhV7yN_Y-fWjlwbN--IHtqp1Yr_NmtCN9W4CyB7Cn3asasHYWjuLT4KZDY_1JC8sluramSAH3csIL6xvhdkJA1_QoQclBco327gI6s-7SzfhWpqkfXinp0ZiDaufVOoCByrYyDYvyoykEmcOZcgEU81dMCUM_xlS8Fz6MXXkaRhFt3Y0fxQ_M4H0UUJnBRF15wb7Ayw0wQF6tFnwn52b4G36S6nG_wv3aXC-yVZZ_HPTbaaCW9aSefxy1xv5yTTTgC2d5vkSGdMaInqdHkQ7FcNhfZr9jlVfcWRNnPveFijgBG2rb7lWYUESWOBpTTp**//
This whole mess is what we are going to attempt to reimplement. Thankfully, some enterprising developers have already documented how this format works, plus XIVLauncher already exists, and works. So it should be a fairly simple matter of just copying what they’ve done, right?
Dealing with GetTickCount
As per the XIV Dev docs, the plaintext arguments are first encrypted using
Blowfish, using GetTickCount() & 0xFFFF0000
as the encryption key.
Hang on, what is GetTickCount()
?
According to the Windows developer documentation,
GetTickCount
is a function provided by kernel32.dll
, which gets the number
of milliseconds since the system was started.
Well, that’s a problem, it’s easy enough for XIVLauncher to simply DllImport
from kernel32
to call the exact same underlying function that FFXIV itself
uses, but we can’t do that on macOS. The exact semantics of how it works has to
be identical to whatever the Wine shipped with FFXIV for Mac does, otherwise the
keys won’t match and the game won’t be able to decrypt the arguments.
We know that the game will call GetTickCount()
in Wine, so how does Wine
implement it? Thankfully, Wine is open source, so let’s crack open that source
code and get sleuthing. We know the function is provided by kernel32.dll
in
real Windows, so perhaps it’s in wine/dlls/kernel32
?
Sure enough, in sync.c
we see:
1
2
3
4
5
DWORD WINAPI DECLSPEC_HOTPATCH GetTickCount(void)
{
/* note: we ignore TickCountMultiplier */
return user_shared_data->u.TickCount.LowPart;
}
So it reads it from some global variable user_shared_data
, fair enough.
Something else must write into user_shared_data->u.TickCount
then. After a lot
of digging I discovered wine/server/fd.c
.
In fd.c
we see:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void set_user_shared_data_time(void)
{
timeout_t tick_count = monotonic_time / 10000;
#if defined(__i386__) || defined(__x86_64__)
// ... snip ...
user_shared_data->TickCount.High2Time = tick_count >> 32;
user_shared_data->TickCount.LowPart = tick_count;
user_shared_data->TickCount.High1Time = tick_count >> 32;
*(volatile ULONG *)&user_shared_data->TickCountLowDeprecated = tick_count;
#else
// ... snip ...
#endif
}
Getting close, in the same file I can see that the global monotonic_time
is
set in set_current_time
like:
1
monotonic_time = monotonic_counter();
So what is monotonic_counter()
? Turns out, it is in request.c
:
1
2
3
4
5
6
7
8
9
10
11
timeout_t monotonic_counter(void)
{
#ifdef __APPLE__
static mach_timebase_info_data_t timebase;
if (!timebase.denom) mach_timebase_info( &timebase );
#ifdef HAVE_MACH_CONTINUOUS_TIME
if (&mach_continuous_time != NULL)
return mach_continuous_time() * timebase.numer / timebase.denom / 100;
#endif
return mach_absolute_time() * timebase.numer / timebase.denom / 100;
Eureka! However, this just raises another question: in the Wine shipped with
FFXIV, does monotonic_counter()
use mach_continuous_time
, or does it use
mach_absolute_time()
???
Searching for wine mach_continuous_time
brought me to an interesting
patch in the Wine mailing list. Apparently some time in late 2019
they changed GetTickCount()
on macOS to use mach_continuous_time()
,
since that better matches the behaviour of the real Windows implementation.
Versions of Wine prior to that will use mach_absolute_time()
.
How do we know which version of Wine FFXIV uses? This is tricky to answer since it is not a bog standard Wine, but a specialised build from Codeweavers, the company which handled the FFXIV Mac port and are the primary maintainers of Wine. Running:
/Applications/FINAL\ FANTASY\ XIV\ ONLINE.app/Contents/SharedSupport/finalfantasyxiv/bin/wine --version
Very unhelpfully returns:
Product Name: FINAL FANTASY XIV ONLINE
Public Version: 1.0.5
Product Version: 18.5.0.31941local
Build Tag: 3c9addb3fff38fa07712a304ddab359cebc2d69c
Build Timestamp: 20200213T194053Z
See what I mean about a specialised build? Well then nothing for it but to crack open Visual Studio on my Windows machine and code up a test application.
Test Applications
Windows (C++)
I slapped together a very basic Console Application in Visual Studio C++:
1
2
3
4
5
6
7
8
#include <iostream>
#include <Windows.h>
int main()
{
auto ticks = GetTickCount();
std::cout << "ticks = " << ticks << std::endl;
}
Then I compiled it and copied it over to my Mac. Running it with:
/Applications/FINAL\ FANTASY\ XIV\ ONLINE.app/Contents/SharedSupport/finalfantasyxiv/bin/wine GetTickCount.exe
Returns…
… nothing? What’s going on here? I would expect an error message, or a
segfault maybe, but not nothing. After spending entirely too long going down
the wrong rabbit holes I realised that this version of Wine does not emit
anything on the console unless you also provide --verbose
. D’oh.
/Applications/FINAL\ FANTASY\ XIV\ ONLINE.app/Contents/SharedSupport/finalfantasyxiv/bin/wine --verbose GetTickCount.exe
Finally, some console output! An error message:
0026:err:module:import_dll Library MSVCP140D.dll (which is needed by L"C:\\GetTickCount.exe") not found
0026:err:module:import_dll Library VCRUNTIME140_1D.dll (which is needed by L"C:\\GetTickCount.exe") not found
0026:err:module:import_dll Library VCRUNTIME140D.dll (which is needed by L"C:\\GetTickCount.exe") not found
0026:err:module:import_dll Library ucrtbased.dll (which is needed by L"C:\\GetTickCount.exe") not found
0026:err:module:LdrInitializeThunk Importing dlls for L"C:\\GetTickCount.exe" failed, status c0000135
Crap. This is the kind of error you would normally fix in Windows by installing the Visual C++ Runtime. However, we need this to work just with whatever is pre-installed in the Wine Bottle that FFXIV runs in…
Maybe it doesn’t like the 64-bit build? I know lots of software in Windows is still compiled in 32-bit…
/Applications/FINAL\ FANTASY\ XIV\ ONLINE.app/Contents/SharedSupport/finalfantasyxiv/bin/wine --verbose GetTickCount-32.exe
Now returns:
winewrapper.exe:error: cannot execute L"GetTickCount-32.exe"
OK, maybe not. Makes sense, anyway. FFXIV is 64-bit, plus Apple removed all 32-bit support from macOS Catalina, and FFXIV is supposed to be fully supported on macOS Catalina, so it must be a 64-bit bottle.
After some searching I discovered that VCRUNTIME140D.dll
references the
Debug builds of the Visual C++ Runtime, and turns out I did build it in
Visual Studio in Debug mode. A quick recompile later, and…
0026:err:module:import_dll Library VCRUNTIME140_1.dll (which is needed by L"C:\\GetTickCount.exe") not found
0026:err:module:LdrInitializeThunk Importing dlls for L"C:\\GetTickCount.exe" failed, status c0000135
Oh come on! At this point I decided to see if there’s a way to simply statically
link the Visual C++ Runtime so it’d just be embedded in my .exe
. Turns out,
there is. In Visual Studio, right click the project, Properties. Then in
Configuration Properties → C/C++ → Code Generation, change the
Runtime Library
setting to Multi-threaded (/MT)
. A recompile and transfer
later and finally it wor–
wine: Unhandled page fault on read access to 0x00000009 at address 0x140021469 (thread 0026), starting debugger...
Oh. Guess it doesn’t like having the Visual C++ 2019 Runtime statically linked, huh. Hang on a minute, Visual C++ 2019, maybe that’s the problem? Maybe the Wine Bottle only has an older Visual C++ Runtime. What’s the oldest C++ build tools I can install for Visual Studio Community 2019?
Seems like it’s the tools from Visual Studio 2015, v140
. Let’s install that,
retarget the project to it, and try again.
ticks = 141937076
Huzzah! Now, to find out whether this matches mach_continuous_time()
or
mach_absolute_time()
.
macOS (Swift)
I slapped together a very basic single file Swift application like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Darwin
var timebase: mach_timebase_info = mach_timebase_info()
mach_timebase_info(&timebase)
func getTickCount(timeFunc: () -> UInt64) -> UInt64 {
let machtime = timeFunc()
let numer = UInt64(timebase.numer)
let denom = UInt64(timebase.denom)
let monotonic_time = machtime * numer / denom / 100
return monotonic_time / 10000
}
print("getTickCount[absolute]: \(getTickCount(timeFunc: mach_absolute_time))")
print("getTickCount[continuous]: \(getTickCount(timeFunc: mach_continuous_time))")
Compiling this, then running this more or less at the same time as my Wine program above:
/Applications/FINAL\ FANTASY\ XIV\ ONLINE.app/Contents/SharedSupport/finalfantasyxiv/bin/wine GetTickCount.exe && ./MachTime
Returns:
ticks = 168022842
getTickCount[absolute]: 168022847
getTickCount[continuous]: 935308133
So you can see it’s a damn-near exact match for mach_absolute_time()
. Finally!
Blowfish, Endianness, and Swift
I know I need something that can do Blowfish encryption, and the existing cryptography library I used doesn’t expose this algorithm, which means it’s time to go library hunting! My options are:
- Use the underlying
CommonCrypto
framework, which only exposes C functions - Use something pure Swift
As a learning exercise, I decided to pull in a pure Swift library for this: CryptoSwift. I go ahead and start implementing, and write a unit test based on the results generated by XIVLauncher, which are known to work.
However, inexplicably, the cipher text produced with the same key ends up
vastly different from CryptoSwift
compared to the
Blowfish.cs
in XIVLauncher. As a sanity check I whip up
a quick implementation using CommonCrypto
, but it yields the same cipher text
as CryptoSwift
!
I decide to bust open Visual Studio on my PC once again, this time loading up a
small C# project that uses the Blowfish.cs
implementation from XIVLauncher
to encrypt the same parameters as my unit test. Now I have an environment I can
attach debuggers to, to see where the implementations differ. Now, a disclaimer.
I’m going to attempt to explain what I consider to be some very strange
endianness issues, but I barely understand it and it
makes my head hurt.
To start with, understand that Blowfish like many ciphers,
fundamentally works by first breaking up your data into blocks of 8 bytes, and
then each of those blocks into two 32-bit numbers, L
and R
. It then does
some math on those values, and produces two new 32-bit numbers, which you can
then break back apart into your 8 bytes.
First, I put a breakpoint in both projects where the byte array data is finalised, and compare the results. Both implementations agree:
[32, 84, 32, 61, 49, 50, 56, 52, ...]
Now I step forward to where the first block gets encrypted, and it extracts
L
and R
.
C#:
L: 1025528864
R: 876098097
CryptoSwift:
L: 542384189
R: 825374772
Well, well, well, what in the hay is that?! No wonder the cipher text comes out
so radically different if the values it’s reading out of the data disagree so
badly. Since CryptoSwift
and CommonCrypto
both produce the same cipher text,
whatever they’re doing must be logical in some way, but that doesn’t help me
because FFXIV is expecting whatever it is that XIVLauncher is doing! So we’ll
have to dig in deeper to see what the differences are in the Blowfish
implementation.
To derive the L
and R
values, Blowfish.cs
is using the .NET class
BitConverter
, specifically its ToUInt32
static method.
If only there was some way to see the source code for this class.
The answer lies right before my eyes:
1
2
3
4
5
6
if( IsLittleEndian) {
return (*pbyte) | (*(pbyte + 1) << 8) | (*(pbyte + 2) << 16) | (*(pbyte + 3) << 24);
}
else {
return (*pbyte << 24) | (*(pbyte + 1) << 16) | (*(pbyte + 2) << 8) | (*(pbyte + 3));
}
This means that, when running on a little endian machine, such as my Intel Mac, it will interpret the 4 bytes in each half of each block as being little endian!
“A little-endian system, … stores the least-significant byte at the smallest address.”
The C# converts [32, 84, 32, 61]
to a UInt32
by doing:
(32 << 0) | (84 << 8) | (32 << 16) | (61 << 24)
Whereas CryptoSwift / CommonCrypto does:
(32 << 24) | (84 << 16) | (32 << 8) | 61 << 0
No wonder it doesn’t work! Just to test my theory, and in desperation to see
my unit test pass well after midnight, I fork the CryptoSwift
code by
essentially copying its Blowfish class and other needed files directly into my
repo so I can make modifications to it. Unfortunately the
license for CryptoSwift is weird and seemingly nonstandard, so I’m
not comfortable deriving any code from it, so this is all temporary.
I modify the UInt32
extension to treat the input as little endian, and voila!
The ciphertext now matches the .NET version! We did it reddit!
Only thing left now is to try to reimplement this without having a fork of a
cryptography library in my repository. CommonCrypto
, old faithful, is still
there, and I’m not afraid of calling C functions in my Swift, so doing it this
way probably makes the most sense, and maybe it could end up a bit faster than
a Swift implementation too.
The problem is, CommonCrypto
also works only by interpreting the data only in
big endian. My solution was to do some old-school byte order swapping:
1
2
3
4
5
6
7
8
9
10
11
12
func swapByteOrder32(bytes: inout [UInt8]) {
for i in stride(from: 0, to: bytes.count, by: 4) {
let b0 = bytes[i.advanced(by: 0)]
let b1 = bytes[i.advanced(by: 1)]
let b2 = bytes[i.advanced(by: 2)]
let b3 = bytes[i.advanced(by: 3)]
bytes[i.advanced(by: 0)] = b3
bytes[i.advanced(by: 1)] = b2
bytes[i.advanced(by: 2)] = b1
bytes[i.advanced(by: 3)] = b0
}
}
It’s not pretty but it does the job, and without needing to copy the array either. I ran the input byte array through this function and… the cipher text is still wrong, and different again from any other implementation. After inspecting the .NET cipher text, I noticed something…
.NET:
[24, 176, 106, 176, ...]
Swift:
[176, 106, 176, 24, ...]
Looks like the byte order on the emitted cipher text is also wrong! After doing the ol’ switch-a-roo on that:
[24, 176, 106, 176, ...]
… which means our cipher text now matches XIVLauncher exactly, and without
using a CryptoSwift
fork! I quickly connect it up in the main application
logic, making sure to use mach_absolute_time
for wineGetTickCount
, and,
at long last: