Repository navigation
spoofing_and_detection
Page:
spoofing_and_detection— Engineering Reference section Project: Endfield_FineWine — Arknights: Endfield on Apple Silicon Related: installation.md, troubleshooting.md, 01-ace-anticheat-and-endfield.md, 02-dwproton-ace-patches.md, 07-rosetta-and-windows-spoofing.md, 10-milestone-1-results.md
This page documents the anti-cheat spoofing and detection mechanics for running Arknights: Endfield on Apple Silicon macOS via CrossOver Wine. The anti-cheat system (ACE — Anti-Cheat Expert, a rebranded TenProtect/TenProtect "tpshell") employs multiple detection vectors that must be spoofed or bypassed for the game to launch.
The spoofing strategy addresses two distinct layers:
-
Stage 1 — Protector (VMProtect/TenProtect "tpshell"):
EndfieldBase.dllruns environment probes before ACE loads. This is the immediate blocker on macOS — a Rosetta 2 exception-handling loop that causes stack overflow before ACE even initializes. -
Stage 2 — ACE Anti-Cheat: Once the protector passes, ACE's user-mode code and kernel driver (
ACE-BASE.sys) perform further environment checks. This is the layer that dw-proton (Dawn Winery) patches on Linux.
Key insight from milestone 1 (10-milestone-1-results.md): The macOS blocker is Stage 1, not Stage 2. The game dies inside EndfieldBase.dll's exception-based obfuscation before ACE loads. The dw-proton patches are necessary but not sufficient — they address a later stage we never reach on macOS without first clearing Stage 1.
Confidence: high — milestone 1 captured the actual failure signature on M3/CrossOver 26/27.
ACE employs several detection mechanisms that the spoofing patches target. Understanding these vectors is essential for effective spoofing.
ACE ships a real Windows kernel driver ACE-BASE.sys. On Linux under Proton, this driver cannot actually load (Wine cannot load kernel drivers), but ACE's user-mode code checks for its presence via exported NT kernel functions.
Spoofing approach: Port the ntoskrnl.exe em-backports from dw-proton, which implement stub implementations of the kernel functions ACE calls. This makes ACE believe the driver is present and functional.
Relevant patches (from 02-dwproton-ace-patches.md):
-
ntoskrnl.exebackports for 17 kernel routines includingKeAcquireGuardedMutex,PsGetProcessImageFileName,PsReferencePrimaryToken, etc. -
MmGetVirtualForPhysical(semi-stub) -
PsGetContextThread(implement, no arch#ifdef)
ACE resolves KiUserApcDispatcher and KiUserCallbackDispatcher via GetProcAddress from ntdll, then uses these to install hooks and probe the environment.
Spoofing approach: Replace the real dispatcher addresses with a naked int3 (0xCC) stub. When ACE calls GetProcAddress for these symbols, it receives the int3 stub instead of the real address, defeating the probe.
Patch location: dlls/kernel32/module.c — the int3_stub function and needs_int3_hack() gate.
Architecture constraint: The #ifdef __x86_64__ guard means this patch only compiles in an x86_64 Wine build under Rosetta 2. A native arm64 CrossOver build would not include it.
ACE performs timing-sensitive checks using NtDelayExecution. If the wait granularity is too coarse, ACE may time out and abort.
Spoofing approach: Reimplement NtDelayExecution using NtQueryPerformanceCounter + busy-wait select() loop, as done in dw-proton's sync.c patch. This provides high-resolution timing that ACE expects.
ACE (via TenProtect "tpshell") can detect:
- CPUID vendor string (
GenuineIntel+VirtualAppleon Rosetta) - AVX/AVX-512 support (missing on Rosetta before macOS 15)
-
wine_get_version/wine_get_build_idexports - Registry keys like
HideWineExports
Spoofing approach: See [Section 2] for Rosetta 2 signal fixes and environment spoofing techniques.
The Stage 1 protector fault (the immediate macOS blocker) is caused by Rosetta 2's handling of certain x86_64 instructions. Two novel fixes in dlls/ntdll/unix/signal_x86_64.c resolve this.
Problem: VMProtect/TenProtect emits hundreds of thousands of multi-byte 0F 1F NOPs (e.g., 0F 1F C1 = nop ecx). Under Rosetta 2, some forms erroneously raise EXCEPTION_ILLEGAL_INSTRUCTION, triggering Wine's SEH handler. This causes a recursive "collided unwind" loop and stack overflow.
Fix: When an illegal-instruction fault occurs on 0x0F 0x1F, decode the instruction length (mod/rm, SIB byte, displacement) and advance RIP past the instruction, treating it as a valid no-op.
Code location: dlls/ntdll/unix/signal_x86_64.c — segv_handler, the TRAP_x86_PRIVINFLT case. Add a case 0x1F that decodes and skips the NOP.
Result: The protector runs smoothly without crashing. This is the primary Stage 1 fix.
Problem: ACE's kernel driver reads CR3 control register (mov rbx, cr3) as an anti-virtual-machine probe. On real x86 hardware or Linux KVM/Wine, this causes a General Protection Fault (#GP), which Wine converts to EXCEPTION_PRIV_INSTRUCTION. ACE's SEH handler catches this and continues.
Under Rosetta 2, however, executing mov rbx, cr3 generates an invalid opcode fault instead of #GP. Wine translates this into EXCEPTION_ILLEGAL_INSTRUCTION. ACE receives the wrong exception code, fails internal verification, and aborts with "driver error 13".
Fix: In segv_handler, before defaulting to EXCEPTION_ILLEGAL_INSTRUCTION, inspect the faulting opcode with Wine's is_privileged_instr(). If the instruction is privileged, deliver EXCEPTION_PRIV_INSTRUCTION matching Linux behavior.
Result: ACE's anti-VM check passes completely. This complements the NOP skip fix.
Both fixes are general CrossOver-on-Apple-Silicon bugs affecting any VMProtect/TenProtect-protected game. They are worth upstreaming to CodeWeavers with Bug 45083 as reference.
Confidence: high — both fixes were validated on the target hardware (M3/M4, macOS 26.5/27.0) and are the difference between the game failing at Stage 1 vs. reaching ACE initialization.
Instead of compiling a full CrossOver app, the project builds a minimal 64-bit-only Wine and surgically swaps only three core modules into a copy of CrossOver. This preserves CrossOver's proprietary graphics stack (D3DMetal, fonts, TLS) while injecting the anti-cheat fixes.
| Module | CrossOver Location | Responsibility |
|---|---|---|
ntdll.so |
lib/wine/x86_64-unix/ntdll.so |
Rosetta 2 signal fixes, NOP skip, QPC timing |
kernel32.dll |
lib/wine/x86_64-windows/kernel32.dll |
KiUser*Dispatcher int3 spoof |
ntoskrnl.exe |
lib/wine/x86_64-windows/ntoskrnl.exe |
17 backported NT kernel functions |
All other libraries (graphics, audio, window management, fonts, TLS) remain stock CodeWeavers binaries.
CrossOver's ntdll.so dynamically loads cxcompatdb.so at process startup to configure graphics backends (e.g., CX_GRAPHICS_BACKEND=d3dmetal). cxcompatdb.so depends on @rpath/libgnutls.30.dylib in lib64/.
Problem: A minimal Wine build may not carry the custom rpath, causing cxcompatdb.so to silently fail, falling back to WineD3D and producing error 80004005 (DirectX device creation failure).
Fix: Bake @loader_path/../../../lib64 directly into ntdll.so's LC_RPATH using install_name_tool:
install_name_tool -add_rpath "@loader_path/../../../lib64" \
"$CXR/lib/wine/x86_64-unix/ntdll.so"
```bash
This ensures D3DMetal initializes correctly per process.
### 3.3 Code-Signing After Swap
After swapping the three modules, the bundle signature is broken. The repair procedure:
```bash
# Remove quarantine xattr (critical — without this, every binary is SIGKILLed)
xattr -drs com.apple.quarantine /Applications/CrossOver_Endfield_Patch.app
# Strip code signature so loader falls back to unsigned-load behavior
codesign --force --sign - --preserve-metadata=entitlements \
/Applications/CrossOver_Endfield_Patch.app
# Verify before first launch
codesign --verify --deep --strict /Applications/CrossOver_Endfield_Patch.app && echo "patched app verifies"
```html
> **Verified recipe (2026-09, CrossOver 26.3.0 / macOS 27.0 / M4):** The above sequence works cleanly. Nested binaries keep CodeWeavers' Developer ID signatures. The swapped `ntdll.so`, `kernel32.dll`, and `ntoskrnl.exe` need no signature of their own — stock CrossOver's PE files have none; the bundle seal covers them.
---
## 4. Detection Evasion Techniques
Beyond the core patches, several environment spoofing techniques can help avoid ACE detection.
### 4.1 Windows Version Spoofing
Set the bottle's Windows version to Windows 10 or 11 via `winecfg`. This writes `CurrentVersion` registry keys that ACE reads for build validation.
```bash
# Via winecfg GUI
# Or via registry edit
<CrossOver wine> reg add "HKCU\Software\Wine" /v HideWineExports /d Y /f
```html
**Note:** `HideWineExports` filters telltale Wine exports out of `LdrGetProcedureAddress`. dw-proton does **not** include this patch by default (it was real historically but removed from the tree). On Linux, ACE tolerates Wine's fingerprints; on macOS, it's a "try-it" lever for free (no rebuild).
### 4.2 Process Name Gate
The int3 hack is gated to processes named `Endfield.exe` or `EM-Win64-Shipping.exe`:
```c
// From dw-proton misc/0009+0010
static BOOL needs_int3_hack(void) {
static volatile int cache = -1;
if (cache == -1) {
const WCHAR *p, *name = NtCurrentTeb()->Peb->ProcessParameters->ImagePathName.Buffer;
WCHAR env[8];
BOOL ret;
if ((p = wcsrchr(name, '/'))) name = p + 1;
if ((p = wcsrchr(name, '\\'))) name = p + 1;
ret = ((!wcsicmp(name, L"Endfield.exe")) ||
(!wcsicmp(name, L"EM-Win64-Shipping.exe")));
if (GetEnvironmentVariableW(L"PROTON_ENABLE_INT3_HACK", env, ARRAY_SIZE(env)))
if (_wtoi(env) == 1) ret = TRUE;
cache = ret;
}
return cache;
}
```text
**CrossOver adaptation:** Under CrossOver, the process name may differ slightly. Set `PROTON_ENABLE_INT3_HACK=1` as an environment variable to force-enable the hack regardless of process name.
### 4.3 `NtQueryInformationProcess` DEP State
The protector's SEH handler relies on `NtQueryInformationProcess(ProcessExecuteFlags)` reporting DEP enabled. On macOS under CrossOver, this may behave differently than on Linux.
**Troubleshooting:** If ACE trips on timing or DEP checks, instrument the `info[0]=8` execute-fault path. See [12-stage1-protector-fault.md](12-stage1-protector-fault.md) for the full diagnosis.
---
## 5. Troubleshooting Detection Issues
### 5.1 "CrossOver_Endfield_Patch is damaged and can't be opened"
**Cause:** The patched bundle's signature seal is broken, and macOS has tagged it with `com.apple.provenance` xattr (from a previous failed launch or AI agent spawn).
**Fix — Two-step:**
1. **If the copy has never been launched** (the script stages it in `$TMPDIR`, then `mv`s it into place):
```bash
xattr -drs com.apple.quarantine /Applications/CrossOver_Endfield_Patch.app
xattr -rd com.apple.FinderInfo /Applications/CrossOver_Endfield_Patch.app
codesign --force --sign - --preserve-metadata=entitlements \
/Applications/CrossOver_Endfield_Patch.app
codesign --verify --deep --strict /Applications/CrossOver_Endfield_Patch.app && echo "patched app verifies"
```html
2. **If the copy has already been launched** (macOS tags it with `com.apple.provenance` and blocks further edits):
- The only remedy is to **delete the bundle and rebuild** from a clean stage.
- Never launch a patched CrossOver bundle before it verifies — a failed first launch leaves it tagged and read-only.
> **Verified principle (2026-09):** Stripping the signature only works while the copy has no `com.apple.provenance` xattr. Re-sealing the outer bundle ad-hoc works either way — see the [verified recipe](#33-code-signing-after-swap).
### 5.2 ACE "driver error 13"
**Cause:** The patched `ntdll.so`/`ntoskrnl.exe` aren't loading, or the