Skip to content

spoofing_and_detection

runner edited this page Oct 10, 2026 · 18 revisions

Spoofing and Detection — Engineering Reference

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

Overview

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:

  1. Stage 1 — Protector (VMProtect/TenProtect "tpshell"): EndfieldBase.dll runs 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.

  2. 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.


1. ACE Anti-Cheat Detection Vectors

ACE employs several detection mechanisms that the spoofing patches target. Understanding these vectors is essential for effective spoofing.

1.1 Kernel Driver Presence Check

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.exe backports for 17 kernel routines including KeAcquireGuardedMutex, PsGetProcessImageFileName, PsReferencePrimaryToken, etc.
  • MmGetVirtualForPhysical (semi-stub)
  • PsGetContextThread (implement, no arch #ifdef)

1.2 KiUser*Dispatcher Probe

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.

1.3 Timing Sensitivity — NtDelayExecution

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.

1.4 Environment Fingerprinting

ACE (via TenProtect "tpshell") can detect:

  • CPUID vendor string (GenuineIntel + VirtualApple on Rosetta)
  • AVX/AVX-512 support (missing on Rosetta before macOS 15)
  • wine_get_version / wine_get_build_id exports
  • Registry keys like HideWineExports

Spoofing approach: See [Section 2] for Rosetta 2 signal fixes and environment spoofing techniques.


2. Rosetta 2 Signal Handling Fixes

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.

2.1 Multi-byte NOP (0F 1F) Skip

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.

2.2 Privileged Instruction (mov cr3) Classification

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.

2.3 Verification

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.


3. Module Swapping Architecture

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.

3.1 Swapped Modules

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.

3.2 The LC_RPATH Dependency

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

Clone this wiki locally