Verifying game files
The official Dauntless servers shut down on 30 May 2025, and the game can no longer be downloaded from any official source. Every copy we have worked with came from a community-shared archive. Before running anything from those archives we wanted three answers:
- Genuine: are the binaries exactly what Phoenix Labs shipped?
- Complete: is every file of the build present and intact?
- Clean: is there anything in the archive that should not be there?
This page describes how we answered those questions for two builds. Nothing from either archive was executed until all checks had passed.
| Build | What we received | Result |
|---|---|---|
| 2.1.1 (final “Awakening” build, UE5, IoStore). Build 682486, signed 18 Dec 2024 | A RAR5 archive, 13.53 GiB, 833 files, 15.52 GB unpacked | Genuine, complete, clean |
1.4.4 (UE4, pak v9). dauntless_rel-1.4.4_Shipping_2020-10-28, signed 29 Oct 2020 | BaseGame144.zip, 10.48 GB, 410 files (471 zip entries counting 61 directory entries). This is the archive the upstream Undaunted launcher downloads | Genuine, complete, clean |
Contents
Ground rules
- Never run anything from an unverified archive. That includes installers, launchers and the game itself.
- Do the analysis on Linux. We used Ubuntu under WSL2. A Windows
.exeor.dllcannot run there by accident. This is protection against running something by mistake, not a security boundary: WSL2 can reach the Windows drives through/mnt/cby default. A dedicated VM isolates better. - Extracted files lose their execute bit (
chmod -R a-x+X <dir>). - Check the code first and the bulk later. For 2.1.1 we extracted only the
.exeand.dllfiles (75 files, about 490 MiB) and verified their signatures. Only then did we unpack the rest.
Tools we used:
| Tool | Version | Used for |
|---|---|---|
osslsigncode | 2.13 (OpenSSL 3.5) | Authenticode verification on Linux |
ClamAV (clamscan, freshclam) | 1.5.3, daily signatures 28129 (20 Sep 2026) | Malware scan |
unrar | 7.20 | RAR5 extraction (2.1.1) |
Python 3 zipfile, hashlib, pefile | Ubuntu packages | Zip extraction, manifest hashing, PE inspection |
Safe extraction
Zip archives (1.4.4)
We extracted with Python’s zipfile and did not use a GUI tool. The file names inside an archive decide where files get written. A crafted name such as ../../somewhere/evil.dll can write outside the target folder. This attack is called zip-slip. Our extractor resolves every destination path and refuses anything that falls outside the target folder:
import zipfile, os, shutil, sys
z = zipfile.ZipFile(sys.argv[1])
out = os.path.realpath(sys.argv[2])
for i in z.infolist():
dest = os.path.realpath(os.path.join(out, i.filename))
if not dest.startswith(out + os.sep): # zip-slip guard
print("REFUSED path escaping target:", i.filename)
continue
if i.is_dir():
os.makedirs(dest, exist_ok=True)
continue
os.makedirs(os.path.dirname(dest), exist_ok=True)
with z.open(i) as src, open(dest, "wb") as dst:
shutil.copyfileobj(src, dst, 1 << 22)
CRC checks come with it. Every zip entry stores a CRC-32 of its uncompressed data. When zipfile reads an entry to the end, it compares that CRC and raises BadZipFile if they differ. So an extraction that finishes without an exception has also checked the CRC of every entry. All 410 files of BaseGame144.zip extracted cleanly, and no entry was refused.
A CRC only proves that the archive was not corrupted in transit. It says nothing about who made the files. The later checks answer that.
RAR5 archives (2.1.1)
- Ubuntu’s
7zippackage (7-Zip 26.00, packaged without the non-free RAR decoder) can list a RAR5 archive but cannot unpack it. Every file we tried failed withUnsupported Method. We installedunrarfrom Ubuntu’s multiverse repository instead. - We checked the
unrarversion: it is 7.20. CVE-2022-30333 is a path-traversal bug inunraron Linux and was fixed in 6.12. - We read the archive listing before extracting. It contained no absolute paths and no
..components. unrarchecks each file’s stored checksum while extracting and exits non-zero on a mismatch. Our extraction exited with 0, and all 833 files were present afterwards.
Authenticode signatures with osslsigncode
Phoenix Labs signed its Windows binaries with Authenticode. This is the strongest single check we have, because it is cryptographic. osslsigncode checks Authenticode signatures on Linux:
osslsigncode verify -CAfile /etc/ssl/certs/ca-certificates.crt -in Dauntless-Win64-Shipping.exe
The first two lines to read are these:
Current message digest : B15C6A3F8900BD367F049260D9E0A005531C54F2C26C668CC16F54A3A67BD0E4
Calculated message digest : B15C6A3F8900BD367F049260D9E0A005531C54F2C26C668CC16F54A3A67BD0E4
- Current is the digest stored inside the signature. The signer computed it when the file was signed.
- Calculated is the digest
osslsigncodecomputes from the file as it is now.
If they match, the signed parts of the file have not changed by a single byte since signing. If they differ, the file was modified afterwards. The remaining output says who signed the file: the signer certificate, the chain up to a trusted root, and the timestamp countersignature.
2.1.1: every signature verifies
All 75 of 75 binaries verified with Signature verification: ok. That is 7 executables and 68 DLLs, with zero failures and zero unsigned files. Details for the main executable:
| Check | Result |
|---|---|
| Digest | Current and calculated digests match |
| Signer | CN=Phoenix Labs Canada ULC, Burnaby, British Columbia |
| Issuer | DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1 |
| Chain | Verified to DigiCert Trusted Root G4 |
| Revocation | CRL fetched from DigiCert; the certificate is not revoked |
| Timestamp | 18 Dec 2024 02:03:21 GMT, countersigned by DigiCert’s timestamping CA |
The signing certificate expired in October 2025. That does not matter. The timestamp proves the signature was made while the certificate was valid, which is how code signing is meant to work.
The 2.1.1 archive contains exactly seven executables: Dauntless-Win64-Shipping.exe, Dauntless.exe, start_protected_game.exe, EasyAntiCheat_EOS_Setup.exe, CrashReportClient.exe, EpicWebHelper.exe and UEPrereqSetup_x64.exe. All seven are signed by Phoenix Labs. The EAC setup program matters most here, because it installs a Windows service that runs with system privileges.
1.4.4: why “FAILED” can still mean untouched
On the 1.4.4 build, osslsigncode reported FAILED for every one of the 39 binaries. That included Microsoft’s own dbghelp.dll and d3dcompiler_47.dll. A file that is really tampered with fails too, so the failure alone tells us nothing. The full output for the main executable shows what is actually going on:
Current message digest : D71ACDA54152B287C361B23B9D60AC45B590982C502D4648EA6600CD618E15AD
Calculated message digest : D71ACDA54152B287C361B23B9D60AC45B590982C502D4648EA6600CD618E15AD
Subject: CN=Phoenix Labs Canada ULC ... Issuer: CN=thawte SHA256 Code Signing CA
Countersignatures:
Timestamp time: Oct 29 04:23:10 2020 GMT Hash Algorithm: sha1
Issuer: CN=Symantec Time Stamping Services CA - G2
Timestamp verified using:
Subject: CN=Symantec Time Stamping Services CA - G2
Issuer : CN=Thawte Timestamping CA ...
Error: unable to get local issuer certificate
Timestamp Server Signature verification: failed
Signing certificate chain verified using:
Subject: CN=thawte SHA256 Code Signing CA
Issuer : CN=thawte Primary Root CA ...
Error: unable to get local issuer certificate
Signature verification: failed
The digests match. Both errors are chain errors, and both have the same cause: a root certificate that the CA bundle does not contain.
- The 2020 timestamp was issued under
Symantec Time Stamping Services CA - G2(SHA-1), which chains up to the oldThawte Timestamping CAroot. - Phoenix’s 2019 code-signing certificate was issued by
thawte SHA256 Code Signing CA, which chains up tothawte Primary Root CA.
Both roots belong to the retired thawte/Symantec hierarchy. The CA bundle of a current Ubuntu contains no thawte certificate at all (we checked), so OpenSSL cannot finish either chain and reports the whole verification as failed. The fault is in our verification environment. It says nothing about the file.
Because the overall verdict was unusable, we compared the digests of every binary:
find . -type f \( -iname '*.exe' -o -iname '*.dll' \) | sort | while read -r f; do
out=$(osslsigncode verify -in "$f" 2>&1)
cur=$(echo "$out" | grep -m1 'Current message digest' | awk '{print $NF}')
cal=$(echo "$out" | grep -m1 'Calculated message digest' | awk '{print $NF}')
if [ -z "$cur" ]; then echo "UNSIGNED $f"
elif [ "$cur" = "$cal" ]; then echo "intact $f"
else echo "MODIFIED $f"; fi
done
Result: 39 intact, 0 modified, 0 unsigned.
| Signer | Files | Timestamps |
|---|---|---|
| Phoenix Labs Canada ULC | 22 | 29 Oct 2020, 04:23:10 to 04:23:19 GMT: one signing run of about nine seconds, the day after the build date in Version.txt |
| Microsoft Corporation | 10 | 2009 to 2020, the vendor’s own dates for redistributable DLLs |
| EasyAntiCheat Oy | 4 | 7 Sep 2020 |
| Mercer Road Corp (Vivox) | 2 | 8 Jan 2020 |
| Overwolf Ltd | 1 | 15 Aug 2017 |
In 1.4.4, Dauntless.exe is the EasyAntiCheat bootstrapper, so EasyAntiCheat Oy signed it, not Phoenix. We traced the cause of the FAILED verdict only on the main executable. For the other 38 files we checked only that the digests match. Their failures presumably come from similar old chains, but we have not confirmed that.
How to read osslsigncode output:
| Output | Meaning |
|---|---|
Signature verification: ok | Unmodified and chained to a trusted root |
| FAILED, digests match, chain error | Unmodified. The chain fails only in your environment, for example because of a retired root |
| FAILED, digests differ | Modified after signing. Do not run it |
No Current message digest line | Unsigned. Not a retail binary unless the vendor shipped it unsigned |
What a digest match does not prove on its own: that the embedded certificate belongs to who it claims to be. For 1.4.4 we accept it for four reasons. The signer is Phoenix’s thawte-issued certificate, valid from September 2019 to October 2022. On the main executable, the only errors osslsigncode reports are the two missing roots. The 22 Phoenix files form one consistent signing run, and each third-party file carries its own vendor’s signature. And the manifest and hash checks below agree. We did not try adding the two retired thawte roots to the CA file to get a clean ok, so we cannot say whether that would work.
Completeness: Phoenix’s install manifest (1.4.4)
The 1.4.4 build ships Manifest.bin and Manifest.bin.json in its root folder. This is Phoenix’s own install manifest, not an Unreal or Epic Games Store file. The JSON form is easy to read:
{ "TargetFiles": [ ...406 entries... ], "DeployedPaths": ["**"] }
entry: { "RelativePath": "Archon\\Binaries\\Win64\\dbgcore.dll",
"FileSize": 166720,
"IsSelfSource": true,
"MD5Chunks": "185bfb1bfb9315633cfaa60e2473772745" }
MD5Chunks is a two-character prefix, always 18, followed by one 32-hex-digit MD5 for each chunk of the file. The manifest does not state the chunk size. We tried every power of two from 2^16 to 2^26. Only 2^24 bytes (16 MiB) gives, for every one of the 406 files, a hash count equal to ceil(FileSize / chunk). Hex 0x18 is 24, so the prefix probably encodes that exponent. This is unconfirmed, but it matches.
The check itself hashes each file in 16 MiB pieces and compares the list:
import json, hashlib, os
d = json.load(open("Manifest.bin.json", encoding="utf-8-sig"))
CHUNK = 1 << 24
for e in d["TargetFiles"]:
rel = e["RelativePath"].replace(chr(92), "/") # backslashes -> slashes
want = e["MD5Chunks"][2:] # drop the "18" prefix
want = [want[i:i+32] for i in range(0, len(want), 32)]
got = []
with open(rel, "rb") as f:
while (b := f.read(CHUNK)):
got.append(hashlib.md5(b).hexdigest())
got = got or [hashlib.md5(b"").hexdigest()]
assert os.path.getsize(rel) == e["FileSize"] and got == want, rel
Result: 406 of 406 files match. None mismatched, none had the wrong size, none were missing. The only files on disk that the manifest does not list are Manifest.bin and Manifest.bin.json themselves, plus two empty debug.log files, one in each Binaries\Win64 folder.
The manifest travels inside the same archive, so on its own it proves consistency, not origin. Someone who altered a pak could regenerate the manifest to match. Its value comes from being combined with the signature check: the code that reads those paks is proven genuine, and the whole install agrees with Phoenix’s own file list.
Two more cross-checks for 1.4.4:
-
The SHA-256 of the main executable and of the zip both equal the values pinned in the upstream Undaunted launcher (
UndauntedLauncher/src/main.ts):File SHA-256 Dauntless-Win64-Shipping.exe(103,673,520 bytes)D3D41E614908D2BEFD518B27046D9822D6130EF12BA3504BABBDB786BEF9CFF4BaseGame144.zip556B9A648A5E5E7E11B6F8DD3D80FF8E88FCEB0D3448297AAF47CE7BF756BC6D -
EasyAntiCheat/Certificates/game.ceris EasyAntiCheat’s stock certificate. No extra CA certificate was slipped into the install.
Completeness: Unreal’s staging manifests (2.1.1)
The 2.1.1 copy came from an Epic Games Store install. It has no Manifest.bin.json, but Unreal’s packaging step leaves three manifests in the root folder:
| Manifest | Lists | Present |
|---|---|---|
Manifest_NonUFSFiles_Win64.txt | 352 loose files: binaries, EasyAntiCheat, CEF and other files outside the containers | 352 of 352 |
Manifest_DebugFiles_Win64.txt | 6 debug files | 5. Only Dauntless-Win64-Shipping.pdb is missing, as expected for a retail install |
Manifest_UFSFiles_Win64.txt | 152,035 packaged asset paths | These are inside the containers and cannot be checked file by file |
These manifests list paths and timestamps, not hashes. They show that the install is complete, not that its contents are intact. For the containers we checked structure:
- All 157
.utocfiles carry the IoStore magic-==--==--==--==-and TOC version 5. - Every
.utochas its.ucasand every.ucasits.utoc: 157 each, with no orphans. There are also 157 Unreal.pakfiles. 156 of them sit beside the containers, since theglobalcontainer has no.pak, and the last one isCrashReportClient.pak. Every one ends in the pak footer magic0x5A6F12E1. - The chunk numbering has no gaps in any of the 18 container groups (
Archon_0..50,Archon_ArmourC_0..40,Archon_UI_0..11and the rest). A missing container would leave a gap. - Correction: our first footer check flagged 56
.pakfiles as invalid. All 56 sit underEngine/Binaries/ThirdParty/CEF3/.../Resourcesand are Chromium resource paks, a different format with the same extension. Each has a valid Chromium pak header (version 5). The check was wrong, not the files. - The 187
.chromafiles (Razer Chroma lighting effects) are listed in the loose-file manifest and all share the same header.
Apart from the containers, only eight files on disk appear in none of the manifests, and all eight are expected: the Epic Games Store install record (.egstore/<id>.manifest), three EasyAntiCheat runtime files written at install time (base.bin, base.cer, runtime.conf), the three manifest files themselves, and the crash reporter’s own CrashReportClient.pak. The archive contains no scripts (.bat, .cmd, .ps1, .vbs, .scr, .lnk, .hta). The EasyAntiCheat Settings.json points at the retail executable, Archon/Binaries/Win64/Dauntless-Win64-Shipping.exe.
ClamAV, and the 2 GiB trap
An antivirus scan is the weakest of these checks. A clean result only means that no known signature matched. We ran it anyway, because asset files cannot be signature-verified and a scan covers them.
The trap
Our first attempt on 2.1.1 scanned the RAR archive directly and “passed”:
LibClamAV Warning: Max file-size was set to 4293918720 bytes. Unfortunately, scanning files
greater than 2147483647 bytes (2 GiB - 1) is not supported.
Scanned files: 1
Infected files: 0
Data scanned: 0 B
Data read: 13.53 GiB (ratio 0.00:1)
That result is meaningless. ClamAV cannot scan any single file larger than 2 GiB − 1. Raising --max-filesize or --max-scansize above that only produces the warning. The oversized file is skipped, and the summary still says Infected files: 0. The giveaway is Data scanned: 0 B. We briefly treated this run as a clean result before we noticed.
How to avoid it:
- Scan the extracted tree, not the archive.
- Make sure no single extracted file is over 2 GiB. The largest file in either build is an audio pak of about 272 MB.
- Keep the limits at 2047M or below, and read
Data scannedin the summary, not onlyInfected files. - Optionally add
--alert-exceeds-max=yes. ClamAV then reports files it skipped because of a limit, so they do not pass silently. We did not use this option in the runs below.
clamscan -r -i --alert-broken \
--max-filesize=2047M --max-scansize=2047M --max-files=10000 --max-recursion=16 \
/root/analysis/full
For the 1.4.4 run we passed 4000M. That printed the same warning but did no harm, because no file came close to 2 GiB.
Results
| Build | Files scanned | Data read | Data scanned | Infected | Time |
|---|---|---|---|---|---|
| 2.1.1, binaries only (first pass) | 75 | 491 MiB | 344 MiB | 0 | 2 m 18 s |
| 2.1.1, full extracted tree | 833 | 14.46 GiB | 30.37 GiB | 0 | 22 m 28 s |
| 1.4.4, binaries and text files | 66 | 257 MiB | 0 | ||
| 1.4.4, full extracted tree | 408 | 10.15 GiB | 21.31 GiB | 0 | 16 m 23 s |
Data scanned is larger than Data read because ClamAV unpacks compressed content it recognises and scans the result. For 1.4.4, the gap between 408 scanned files and 410 extracted files is the two zero-byte debug.log files: ClamAV reported each as Empty file and did not count it.
The prebuilt Undaunted DLLs
The 1.4.4 setup adds two prebuilt files from the upstream Undaunted repository to the game folder: dxgi.dll and UndauntedInternalServer.dll. They are not game files, but they run inside the game process, so we checked them the same way. Both are unsigned, which is normal for a hobby project.
| Check | dxgi.dll | UndauntedInternalServer.dll |
|---|---|---|
| Size | 11,264 bytes | 123,392 bytes |
| SHA-256 | 9A431D7B…4B0D1F | 520EC588…0D0933 |
| Matches the git blob in the repository (not a Git LFS pointer) | Yes | Yes |
| Packing, overlay, TLS callbacks, RWX sections | None | None |
| Imports | Only GetSystemDirectoryA, LoadLibraryA, GetProcAddress and the standard MSVC runtime startup imports | Match dllmain.cpp, Networking.cpp, MinHook and the C runtime. No networking, registry, process-creation or file-writing imports (its one freopen_s use attaches stdout and stdin to its own console) |
| ClamAV | OK | OK |
What dxgi.dll does, from a full disassembly of its DllMain: it loads the real dxgi.dll from System32, looks up CreateDXGIFactory, CreateDXGIFactory1 and CreateDXGIFactory2, and forwards its three exports to them. It then loads UndauntedInternalServer.dll by name. It does nothing else. Those three exports are exactly the set the game needs: the 1.4.4 executable imports CreateDXGIFactory and CreateDXGIFactory1, and the system d3d11.dll (on our Windows 10 machine) imports CreateDXGIFactory2.
Every hard-coded game address in the server DLL either appears in its published source or comes from the offsets of the generated SDK. We found none that the source does not explain. Windows Defender raised no detections when we copied the files into the game folder.
One open issue: the source of dxgi.dll is not in the Undaunted repository. Its debug path points to a separate project whose source we have not found published anywhere. That matters under the AGPL. Replacing it with our own proxy, and building UndauntedInternalServer.dll from source ourselves, are both on the roadmap. Until then we check both files against the pinned hashes before every copy. The full hashes are in the friends’ setup guide.
Checklist
To repeat this on your own copy of 1.4.4:
- Hash the archive and compare it with
556B9A64…6BC6D. - Extract it inside a Linux environment with a zip-slip guard, and let the CRC checks run.
- Run
osslsigncode verifyon every.exeand.dlland compare the current and calculated digests. A FAILED whose only errors are the missing thawte roots is a chain problem, not tampering, as long as the digests match. - Check every file against
Manifest.bin.jsonin 16 MiB MD5 chunks. - Scan the extracted tree with ClamAV. Confirm that
Data scannedis not zero. - Only then copy the install to Windows.
For what the verified files contain, see Game assets and config. For what the executable can and cannot do, see Client internals.