Skip to content

exploit: AES key recovery via ObfuscatedHexDigest arena scan#44

Closed
Emre-Dinc wants to merge 11 commits intogpu-mode:masterfrom
Emre-Dinc:exploit/aes-key-recovery
Closed

exploit: AES key recovery via ObfuscatedHexDigest arena scan#44
Emre-Dinc wants to merge 11 commits intogpu-mode:masterfrom
Emre-Dinc:exploit/aes-key-recovery

Conversation

@Emre-Dinc
Copy link
Copy Markdown
Contributor

The AES-256-GCM result encryption uses mSignature.data() as the 32-byte key.
The key is recoverable because lock() is never called on the signature page
before user code runs — it stays PROT_READ|PROT_WRITE throughout.

From kernel() call 2 (call 1 is the compile run, arena is PROT_NONE then):

  1. Scan large anonymous rw-p regions for (w0, 32, offset) matching ObfuscatedHexDigest layout
  2. Run slow_unhash(w0) to recover the signature page pointer
  3. Read page_ptr + offset → 32-byte AES key
  4. Encrypt forged timing data, write to pipe, os._exit(0)

The signature page is anonymous — mseal never seals it. Landlock allows
/proc/self/maps reads (read-only filesystem access). Neither defense helps here.

tested with mseal=False, landlock=True on rtx5090.

Errors:     None
Full run:   True
Valid times: 100/100
Timing:     0.4 ± 0.00 µs [0.4 - 0.4 - 0.4]
WARNING:    Suspiciously fast (0.420 us)
Wall time:  9.3s
>> CHEAT SUCCEEDED (bypassed detection)

@ngc92
Copy link
Copy Markdown
Collaborator

ngc92 commented Mar 27, 2026

mseal cannot help here, because we need to read this later, and mseal cannot be undone.
#43 should helps here, as it prevents access during the warmup calls.

For the actual benchmark calls, we need to start looking more carefully at the CPU timing, so that we can detect expensive operations like unhashing.

@Emre-Dinc
Copy link
Copy Markdown
Contributor Author

lock() should fix this but cpu timing is better long term solution.

@ngc92
Copy link
Copy Markdown
Collaborator

ngc92 commented Mar 27, 2026

what would lock() change here?

@Emre-Dinc
Copy link
Copy Markdown
Contributor Author

Emre-Dinc commented Mar 27, 2026

I don't know if you like coupling here but my solution was:
ObfuscatedHexDigest becomes friend of BenchmarkManager.
mSignature.lock() called before protect_range(PROT_NONE) in get_kernel(),
mSignature.unlock() called inside the write path before encrypt_message().
PROT_NONE makes reading segfault and turns into this:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/pygpubench/__init__.py", line 55, in _do_bench_impl
    _pygpubench.do_bench(
  File "/tmp/exploits/submission_aes_key_recovery.py", line 217, in kernel
    key_raw, key_str = _recover_aes_key()
                       ^^^^^^^^^^^^^^^^^^
  File "/tmp/exploits/submission_aes_key_recovery.py", line 168, in _recover_aes_key
    raise RuntimeError("AES key not found")
RuntimeError: AES key not found 

edit: also for this to work unlock must restore PROT_READ|PROT_WRITE, we do PROT_READ atm.
OPENSSL_cleanse writes to the key pointer after encryption to zero it, which segfaults on a read-only page

@ngc92
Copy link
Copy Markdown
Collaborator

ngc92 commented Mar 27, 2026

no, I mean isn't it trivial to adapt the exploit to call mprotect(READ) before the access. Making lock pretty-much pointless?

@Emre-Dinc
Copy link
Copy Markdown
Contributor Author

oh, I was focused on how to protect from this vector that i didn't think about what would be the next vector. yeah you are right.

@ngc92
Copy link
Copy Markdown
Collaborator

ngc92 commented Mar 28, 2026

I've added better obfuscation/hashing for the key pointer (in particular, the size 32 is now also hashed, xor'ed with the offset so you can't just precompute the hash).

@Emre-Dinc
Can you update this exploit, and maybe also make it explicitly call mprotect to make the target readable to make it future proof.

@Emre-Dinc
Copy link
Copy Markdown
Contributor Author

  Full run:   True
  Valid times: 100/100
  Timing:     0.4 ± 0.00 µs [0.4 - 0.4 - 0.4]
  WARNING:    Suspiciously fast (0.420 us)
  Wall time:  9.2s
  >> CHEAT SUCCEEDED (bypassed detection)

======================================================================
SUMMARY
======================================================================
Test                         Status
---------------------------- ---------------------------------------------
  aes_key_recovery           CHEAT SUCCEEDED (bypassed detection)```

@ngc92
Copy link
Copy Markdown
Collaborator

ngc92 commented Mar 28, 2026

hmm, when testing this, I'm getting "Cheat detected". On linux kernel 6.14.
I'd also expect this to fail, because with the recent changes, the warmup calls do happen while BenchmarkManager is still inaccessible

@ngc92
Copy link
Copy Markdown
Collaborator

ngc92 commented Mar 28, 2026

my bad, I had some local changes and forgot to uv pip install before testing the exploit. It works with the current repo.

@Emre-Dinc
Copy link
Copy Markdown
Contributor Author

Interesting, I tried 4-5 different runs and it was consistent but it turned out to be not. Making a small change to retry until exhausts everything. Also these tests were done on 5.15.0-318.199.3.2.el9uek.x86_64 (modal instance).

  Errors:     26214400
  Full run:   True
  Valid times: 100/100
  Timing:     143176.8 ± 1431749.82 µs [1.0 - 2.0 - 14317500.0]
  Wall time:  25.1s
  >> CHEAT FAILED (detected)
======================================================================
SUMMARY
======================================================================
Test                         Status                                       
---------------------------- ---------------------------------------------
  aes_key_recovery           
CHEAT FAILED (detected)

@ngc92
Copy link
Copy Markdown
Collaborator

ngc92 commented Mar 28, 2026

there are some github merge problems, so I manually merged and pushed.

@ngc92 ngc92 closed this Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants