Compare commits

...
21 Commits
Author SHA1 Message Date
JingMatrix a1bb3bbfa3 Release TEESimulator 3.1 2026-01-31 22:49:29 +01:00
JingMatrix e13adb925d Correct misunderstanding of takeIf execution order
The previous code incorrectly assumed `takeIf` prevents the execution of the receiver statement. Since `takeIf` is an extension function, the receiver—`InterceptorUtils.getTransactCode`—was evaluated eagerly *before* the version check predicate could run.

This commit replaces the `takeIf` chain with a standard `if/else` block to ensure the reflection call is only executed when the API level supports it.

Additionally, repeated `IKeystoreService.Stub::class.java` references were refactored into a `stubBinderClass` property.
2026-01-31 12:51:09 +01:00
c3f8f087a6 Support key enumeration via listEntries interception (#84)
Previously, generated keys were functional but invisible to enumeration APIs like `KeyStore.aliases()`. Because these keys reside solely in the simulator's memory, the standard database query performed by the system Keystore does not return them.

This commit intercepts `listEntries` and `listEntriesBatched` to inject these generated keys into the results.

Key implementation details:
- ListEntriesHandler: Encapsulates the logic to merge hardware-backed keys with software-backed keys.
- Ordering: Uses a `TreeMap` to ensure merged results are lexicographically sorted, mimicking AOSP behavior.
- Binder Safety: Implements `estimateSafeAmountToReturn` to calculate the response size. The handler truncates the result list if it exceeds the binder transaction limit (~350KB) as done in AOSP.
- Pagination: Respects the `startPastAlias` parameter to support batched listing.

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2026-01-31 11:37:25 +01:00
JingMatrix 51f32b9db2 Move attestation challenge check to certificate generation
Relocate the `attestationChallenge` length validation from `generateSoftwareKeyPair` to `generateCertificateChain`.

The challenge is only utilized during the construction of the certificate chain (via `AttestationBuilder.buildKeyDescription`). Placing the check in the key pair generation stage caused the logic to miss the `attestKey` transaction hook in `KeystoreInterceptor`.

This fixes a bug introduced in ce740542f7 which missed the detection bypass for Android 10 and 11 devices.
2026-01-30 21:13:02 +01:00
JingMatrix d60ad8fe47 Handle swapped attestation lists on certain Android 11 devices (#108)
Observed an abnormal Keymaster attestation structure on certain Android 11 devices where the `softwareEnforced` and `teeEnforced` authorization lists were swapped in order. This is a deviation from the documented specification and the behavior seen on most devices.

This non-compliance caused parsing failures, as the code expected the `teeEnforced` list to be at a fixed index (7). On the affected devices, this index contained the `softwareEnforced` list, which critically lacks the `TAG_ROOT_OF_TRUST` needed for successful validation and patching.

This commit introduces a defensive normalization step to handle this device-specific anomaly gracefully:

1.  Before parsing, the code now inspects the ASN.1 sequence at the expected `softwareEnforced` index (6).
2.  It checks for the presence of the `TAG_ROOT_OF_TRUST`, which can only exist in the TEE-enforced list.
3.  If the tag is found, the code concludes the lists are swapped and corrects the `allFields` array in-place by swapping the elements at indices 6 and 7.

By normalizing the data structure at the beginning, the rest of the parsing and patching logic can proceed without modification, ensuring correct operation on both compliant and non-compliant devices.
2026-01-30 21:10:41 +01:00
JingMatrix 9a1fbe8c79 Correct alias parsing in KeystoreInterceptor (#106)
The `extractAlias` utility was failing to strip `USRCERT_` and `CACERT_` prefixes, causing a cache miss during certificate chain patching. The function is now updated to correctly handle these prefixes.

Moreover, more logs are added to help debugging in the future.
2026-01-29 19:23:57 +01:00
JingMatrixandGitHub 68b660dfe1 Add SELinux rules for libTEESimulator.so loading (#104)
Allow `keystore` to access the `file` class for `adb_data_file` and `shell_data_file` contexts.

The target contexts correspond to the following locations:
- `adb_data_file`: The library path `/data/adb/modules/tricky_store/libTEESimulator.so`, used for FD transfer.
- `shell_data_file`: The fallback mechanism for loading the library by staging it in `/data/local/tmp`.

Note: The rule for the `dir` class (directory search) has been removed because the supporting audit logs were lost. The remaining file access logs were observed on a MEIZU 21 Note.
2026-01-29 15:15:56 +01:00
JingMatrixandGitHub 068188503c Fix multiple crashes and race conditions on Android 12 (#99)
This resolves several critical stability issues observed on Android 12 devices, including race conditions and API compatibility problems.

Key changes include:

-   Resolves Race Condition in TEE Check:
    Fixes a NullPointerException that occurred when the TEE functionality check was executed before the PackageManagerService was ready. The code now explicitly waits for the package manager to become available, preventing the crash on startup.

-   Fixes IllegalStateException on Initialization:
    Eliminates a crash caused by `setTelephonyServiceManager called twice`. This was due to a redundant call to `initializeMainlineModules()` in the DeviceAttestationService, which is now correctly handled a single time during application startup.

-   Fixes NoSuchAlgorithmException in Attestation:
    Adds a normalization function to handle signature algorithm names reported in all-caps by older Android versions (e.g., "SHA256WITHECDSA"). This ensures compatibility with Bouncy Castle, which expects a specific casing (e.g., "SHA256withECDSA").
2026-01-29 15:00:09 +01:00
JingMatrix 1bbc50d138 Prevent recursion when configured to intercept system UID (#100)
When the TEESimulator is configured to intercept UID 1000, accessing the `lazy` `bootKey` property causes a StackOverflowError.

The property's initializer sends a key generation request (UID 0) to probe real hardware. Previously, the C++ layer hijacked this request and spoofed it to UID 1000. This sent the request back to the Kotlin interceptor (if configured so), which attempted to access `bootKey` again to build the response, creating an infinite loop.

This change spoofs UID 0 requests to 1000 (to pass Keystore permissions) but explicitly bypasses hijacking, ensuring the probe request hits the real hardware.
2026-01-28 22:15:27 +01:00
JingMatrix d2492df02e Remove SELinux context manipulations during injection (#87)
After few tests in various devices, it seems that SELinux context modifications are unnecessary for the injection to work.

We thus remove all related manipulations. Further (partial) reverting of the commit must be justified with SELinux logs:

> adb shell su -c 'cat /proc/kmsg | grep avc'
2026-01-28 22:14:08 +01:00
JingMatrix e7d7b21daa Fix ARM ptrace compatibility and improve remote call safety (#94)
- Implement fallbacks to `PTRACE_GETREGS` and `PTRACE_SETREGS` for 32-bit ARM (`__arm__`). Some kernels return `EIO` or `EINVAL` when attempting to access `NT_PRSTATUS` via `PTRACE_GETREGSET`/`PTRACE_SETREGSET`.

- Update `transfer_fd_to_remote` to use `libc_return_addr` instead of `0` as the return address during the `recvmsg` split-call. This ensures the remote process stops predictably at a known non-executable location rather than relying on a potentially unsafe jump to `0x0`.

- Clarify comments regarding i386 argument passing in `utils.cpp`. Correctly note that a linear `write_proc` starting at the new SP matches the `cdecl` Right-to-Left memory layout (since stacks grow downwards while memory writes move upwards), removing the suggestion that arguments needed reversing.
2026-01-28 14:08:28 +01:00
JingMatrixandGitHub c29bc35a36 Fix cache consistency on key overwrite (#97)
Android allows applications to generate a new key using an existing alias without explicitly calling `deleteKey` first. In this scenario, the new key effectively replaces the old one. As a simulator, we must strictly follow this logic to prevent returning stale data.

Previously, `KeyMintSecurityLevelInterceptor` did not enforce mutual exclusion between the software key cache (`generatedKeys`) and the hardware chain cache (`patchedChains`). This led to state desynchronization where a stale software key could shadow a newly patched hardware chain if the alias was reused.

This change ensures `cleanupKeyData` is invoked immediately before caching a new key / chain in both the software (`handleGenerateKey`) and hardware (`onPostTransact`) paths, ensuring the simulator returns the correct key for the most recent generation request.
2026-01-28 13:50:05 +01:00
JingMatrix 549b5cecc2 Fix crash by avoiding hardcoded index for moduleHash
The previous implementation attempted to retrieve `moduleHash` from the `softwareEnforced` sequence using a hardcoded index (index 2).

However, fields in the Key Attestation `AuthorizationList` are optional. In observed crashes, index 2 actually corresponded to `keySize` (Tag 3, ASN1Integer) rather than `moduleHash`, causing an `IllegalArgumentException` when the code attempted to parse it as an `ASN1OctetString`.

This commit replaces the index-based access with a dynamic lookup for Tag 724.
2026-01-26 23:26:52 +01:00
JingMatrix 04d003ff4d Fix x86_64 injection: Red Zone adjustment and fallback logic (#91)
- Strictly adhere to the System V AMD64 ABI by skipping the 128-byte "Red Zone" before modifying the stack, see page 23 of https://gitlab.com/x86-psABIs/x86-64-ABI/-/jobs/artifacts/master/raw/x86-64-ABI/abi.pdf?job=build for details.

- Added `inject_via_staging` as a fallback strategy:
  1. Copies the payload to `/data/local/tmp`.
  2. Sets permissions/context (`u:object_r:system_file:s0`).
  3. Loads via standard `dlopen`.
  4. Immediately unlinks the file for stealth.

- Introduced `RegisterRestorer` RAII class to guarantee original registers are restored even if the injection logic returns early due to error.
2026-01-26 23:15:09 +01:00
JingMatrixandGitHub 0a842c6e07 Fix support for Android 10 (#92)
Users report that the method `waitForService` doesn't exist on Android 10.
Close #90 as completed.
2026-01-26 16:54:59 +01:00
JingMatrixandGitHub 9f77771e7b Fix Android 11 Keystore execution: Init framework and spoof UID 1000 (#85)
This commit resolves `KeyStore` API failures on Android 11 when running as a standalone CLI executable (UID 0), addressing both environment initialization and permission denial issues.

1. Initialize Android Framework Environment:
   Android 11 Keystore APIs expect a fully initialized application context and a Main Looper, which are missing in a raw root process. This patch:
   - Manually bootstraps `ActivityThread` via `systemMain()`.
   - Initializes `Looper.prepareMainLooper()`.
   - Injects a dummy `Application` object attached to the system context to satisfy `KeyStore.getApplicationContext()` checks.
   - Updates framework stubs to allow compilation of these hidden APIs.

2. Bypass Keystore Permission Checks via UID Spoofing:
   `KeyStoreService::generateKey` enforces the `P_INSERT` permission. Analysis of `permissions.cpp` reveals that UID 0 (Root) is explicitly denied this permission (granted only `P_GET`), whereas UID 1000 (System) holds all permissions (`~0`).
   
   To bypass this restriction, the binder interceptor now detects transactions originating from UID 0 and rewrites the `sender_euid` to 1000. This fools `KeyStoreService` into granting the request.
 
3. Refactor Execution Loop:
   Replaces the previous `Thread.sleep()` maintenance loop with `Looper.loop()`.
2026-01-26 14:07:06 +01:00
ab4fe643a3 Intercept updateSubcomponent to fix software key state inconsistency (#82)
Apps attempting to update the certificate chain of a simulated software-based key (e.g., via KeyStore.setKeyEntry) currently trigger a KEY_NOT_FOUND error. This happens because the request is passed to the hardware Keystore daemon, which has no knowledge of keys existing only in the simulator's memory.

To fix detecting points exploiting this inconsistency, we intercept the UPDATE_SUBCOMPONENT_TRANSACTION. If the target is a recognized virtual key, the simulator now:
1. Updates the in-memory certificate/chain metadata.
2. Returns NO_ERROR immediately to the caller.
3. Prevents the transaction from reaching the real hardware service.

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2026-01-23 17:53:34 +01:00
ce740542f7 Enforce attestation challenge length limit (#70)
Throws IllegalArgumentException if the challenge exceeds 128 bytes, per Android specs. Also fixes a duplicate assignment typo in KeystoreInterceptor.

Reference: https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2026-01-20 19:00:48 +01:00
dependabot[bot]andJingMatrix c27523fd97 Update dependencies 2026-01-11 16:24:34 +01:00
JingMatrixandGitHub 5a8454af7b Implement multi-purpose simulation for crypto operations (#59)
This commit introduces a comprehensive simulation engine for Keystore's `createOperation`, enabling the simulator to correctly handle multiple cryptographic purposes (SIGN, VERIFY, ENCRYPT, DECRYPT) for software-generated keys.

The implementation correctly mimics the AOSP framework's internal key identification mechanism. Instead of relying on an alias, a unique `keyId` is generated and embedded in the `nspace` field of the KeyDescriptor during `generateKey`. The `createOperation` hook then uses this `keyId` to dispatch requests: if the ID matches a known software key, the operation is simulated; otherwise, it is forwarded to the hardware service.

To support this, the `SoftwareOperation` engine was architected using a Strategy Pattern. A `CryptoPrimitive` interface defines common actions, with concrete implementations for `Signer`, `Verifier`, and `CipherPrimitive`. The main `SoftwareOperation` class acts as a controller, instantiating the correct primitive based on the `KeyPurpose` tag from the incoming operation parameters. A `JcaAlgorithmMapper` was added to centralize the logic for converting KeyMint constants into JCA algorithm strings.

For operations on real hardware-backed keys, a lightweight `OperationInterceptor` is now used for observation. It attaches to the genuine `iOperation` binder for logging and properly unregisters itself upon completion to prevent resource leaks. This is supported by new binder unregistration capabilities in the core `BinderInterceptor`.

This change also includes necessary stub files and minor regression fixes to make the simulation more robust and accurate.

See AOSP source for key identification logic:
https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java
2025-12-08 19:59:32 +01:00
JingMatrix 83b65f09c9 Ensure mocked replies use native OK status (#60)
Corrects a bug where the native binder `status_t` was being set to application-level error codes (e.g., `KeyStore.NO_ERROR` which is 1).

Moreover, we call method `InterceptorUtils.createTypedObjectReply` to keep the code style consistent.
2025-12-08 04:30:01 +01:00
33 changed files with 1342 additions and 221 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v3.0"
val verName = "v3.1"
android {
namespace = "org.matrix.TEESimulator"
+9 -3
View File
@@ -359,9 +359,15 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
info.transaction_code = intercept::kBackdoorCode;
info.target_binder = nullptr;
hijack = true;
}
// Check 2: Normal interception based on registry of monitored binders
else {
// Check 2: Spoof uid of KeyStore requests from the daemon to bypass permission check
} else if (txn_data->sender_euid == 0) {
// The kernel driver fills sender_euid.
// libbinder.so trusts this value to populate IPCThreadState.
txn_data->sender_euid = 1000;
LOGV("[Hook] Spoofing UID for transaction: 0 -> %d", txn_data->sender_euid);
hijack = false; // Never hijack to avoid recursion
// Check 3: Normal interception based on registry of monitored binders
} else {
// Safe casting based on Binder driver ABI
RefBase::weakref_type *weak_ref = reinterpret_cast<RefBase::weakref_type *>(txn_data->target.ptr);
+204 -63
View File
@@ -7,6 +7,7 @@
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/system_properties.h>
#include <sys/uio.h>
#include <sys/un.h>
@@ -17,6 +18,7 @@
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <optional>
#include <string>
#include <vector>
@@ -95,10 +97,6 @@ constexpr size_t kMagicLength = 16;
constexpr size_t kMaxPathLength = PATH_MAX;
// Maximum length for file paths.
constexpr const char *kSystemFileContext = "u:object_r:system_file:s0";
// SELinux context for system files,
// used for socket creation and library file context.
constexpr const char *kLibcModule = "libc.so";
// Name of the C standard library.
@@ -215,8 +213,8 @@ private:
* @brief Transfers a file descriptor from the injector process to the remote process.
*
* This function uses Unix domain sockets with SCM_RIGHTS to send a file descriptor.
* It involves setting SELinux contexts, creating local and remote sockets, binding,
* and then coordinating sendmsg/recvmsg calls using ptrace.
* It involves creating local and remote sockets, binding, and then coordinating
* sendmsg/recvmsg calls using ptrace.
*
* @param pid The target process ID.
* @param lib_path The path to the library file being transferred.
@@ -233,29 +231,14 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
uintptr_t libc_return_addr) {
LOGD("Attempting to transfer file descriptor for library: %s", lib_path);
// 1. Set SELinux context for socket creation in the injector process.
// This is crucial for Android where SELinux might prevent socket operations.
if (!set_sockcreate_con(constants::kSystemFileContext)) {
LOGE("Failed to set socket creation context.");
return std::nullopt;
}
// 2. Create a local Unix domain socket for FD transfer.
// Create a local Unix domain socket for FD transfer.
UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (local_socket == -1) {
PLOGE("Failed to create local Unix domain socket.");
return std::nullopt;
}
// 3. Set SELinux context for the library file if possible.
// This might be required for the target process to open/access it later if directly opening by path.
// For FD transfer, this is less critical as the FD's context is inherited, but good practice.
if (setfilecon(lib_path, constants::kSystemFileContext) == -1) {
// Log a warning, but don't fail, as FD transfer might still work.
PLOGE("Failed to set context of library file: %s. This might cause issues.", lib_path);
}
// 4. Open the local library file to get a file descriptor.
// Open the local library file to get a file descriptor.
UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC);
if (local_lib_fd == -1) {
PLOGE("Failed to open library file: %s", lib_path);
@@ -271,7 +254,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
void *errno_addr; // Address of __errno for getting remote errno.
} funcs{};
// 5. Resolve required libc functions in the remote process.
// Resolve required libc functions in the remote process.
funcs.socket_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "socket");
funcs.bind_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "bind");
funcs.recvmsg_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "recvmsg");
@@ -306,25 +289,28 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
};
// 6. Create a Unix domain socket in the remote process.
// Create a Unix domain socket in the remote process.
std::vector<uintptr_t> args = {AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0};
int remote_fd = static_cast<int>(
remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.socket_addr), libc_return_addr, args));
if (remote_fd == -1) {
if (remote_fd <= 0) {
// remote_call returns 0 on failure.
// socket() returning 0 is technically possible (if stdin closed),
// but highly unlikely for a daemon. We treat 0 as failure here to catch the injection error.
errno = get_remote_errno(); // Set local errno for PLOGE.
PLOGE("Failed to create remote socket.");
PLOGE("Failed to create remote socket (returned %d).", remote_fd);
return std::nullopt;
}
LOGD("Successfully created remote socket with FD: %d", remote_fd);
// 7. Generate a unique magic string for the abstract Unix domain socket path.
// Generate a unique magic string for the abstract Unix domain socket path.
auto magic = generateMagic(constants::kMagicLength);
struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}};
// Abstract Unix domain sockets have sun_path[0] as null, and the name starts from sun_path[1].
memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size());
socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size(); // Length includes null byte and magic.
// 8. Push the sockaddr_un structure to the remote process's stack.
// Push the sockaddr_un structure to the remote process's stack.
auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr));
if (remote_addr == 0) {
LOGE("Failed to push socket address to remote memory.");
@@ -332,7 +318,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
return std::nullopt;
}
// 9. Bind the remote socket to the abstract Unix domain socket path.
// Bind the remote socket to the abstract Unix domain socket path.
args = {static_cast<uintptr_t>(remote_fd), remote_addr, static_cast<uintptr_t>(addr_len)};
auto bind_result = remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.bind_addr), libc_return_addr, args);
if (bind_result == static_cast<uintptr_t>(-1)) {
@@ -346,7 +332,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
// Prepare control message buffer for SCM_RIGHTS (file descriptor passing).
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0};
// 10. Push the control message buffer to the remote process's stack.
// Push the control message buffer to the remote process's stack.
auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf));
if (remote_cmsgbuf == 0) {
LOGE("Failed to push control message buffer to remote memory.");
@@ -359,7 +345,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
msg_hdr.msg_control = reinterpret_cast<void *>(remote_cmsgbuf);
msg_hdr.msg_controllen = sizeof(cmsgbuf);
// 11. Push the msghdr structure to the remote process's stack.
// Push the msghdr structure to the remote process's stack.
auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr));
if (remote_hdr == 0) {
LOGE("Failed to push message header to remote memory.");
@@ -367,16 +353,16 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
return std::nullopt;
}
// 12. Initiate the remote recvmsg call. This will block the remote process.
// Initiate the remote recvmsg call. This will block the remote process.
args = {static_cast<uintptr_t>(remote_fd), remote_hdr, MSG_WAITALL};
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), 0, args)) {
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), libc_return_addr, args)) {
LOGE("Failed to initiate remote recvmsg call.");
close_remote(remote_fd);
return std::nullopt;
}
LOGD("Remote recvmsg initiated, waiting for FD transfer...");
// 13. Prepare the local msghdr for sending the file descriptor.
// Prepare the local msghdr for sending the file descriptor.
// The msg_control and msg_name fields of the local msghdr are set up.
msg_hdr.msg_control = &cmsgbuf; // Use local cmsgbuf for sending.
msg_hdr.msg_name = &sock_addr;
@@ -396,7 +382,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = local_lib_fd; // The FD to send.
}
// 14. Send the file descriptor from the injector to the remote process.
// Send the file descriptor from the injector to the remote process.
if (sendmsg(local_socket, &msg_hdr, 0) == -1) {
PLOGE("Failed to send file descriptor to remote process.");
// We do not close local_lib_fd here as it might be transferred even if
@@ -407,9 +393,9 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
LOGD("Local FD %d sent to remote process.", local_lib_fd.operator const int &());
// 15. Complete the remote recvmsg call. This will retrieve the return value.
// Complete the remote recvmsg call. This will retrieve the return value.
auto recvmsg_result =
static_cast<ssize_t>(remote_post_call(pid, regs, 0)); // No specific expected return address for recvmsg
static_cast<ssize_t>(remote_post_call(pid, regs, libc_return_addr));
if (recvmsg_result == -1) {
errno = get_remote_errno();
PLOGE("Remote recvmsg call failed.");
@@ -418,7 +404,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
LOGD("Remote recvmsg completed with result: %zd", recvmsg_result);
// 16. Read the control message buffer back from the remote process to extract the FD.
// Read the control message buffer back from the remote process to extract the FD.
if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) {
LOGE("Failed to read control message buffer from remote process.");
close_remote(remote_fd);
@@ -439,7 +425,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
LOGI("Successfully transferred FD %d to remote process, new remote FD: %d", local_lib_fd.operator const int &(),
transferred_fd);
// 17. Close the remote socket.
// Close the remote socket.
close_remote(remote_fd);
return transferred_fd;
@@ -642,6 +628,130 @@ static bool remote_call_entry(int pid, struct user_regs_struct &regs, uintptr_t
return true; // Return true if the call itself completed, regardless of its return value.
}
/**
* @brief RAII wrapper to ensure a temporary file is deleted (unlinked)
* when the object goes out of scope.
*
* This is crucial for stealth: we want the library to exist on the filesystem
* for the shortest time possible.
*/
class ScopedFileDeleter {
public:
explicit ScopedFileDeleter(std::string path) : path_(std::move(path)) {}
~ScopedFileDeleter() {
if (!path_.empty()) {
LOGD("Cleaning up staged file: %s", path_.c_str());
unlink(path_.c_str());
}
}
// Disable copy to prevent double-deletion issues
ScopedFileDeleter(const ScopedFileDeleter&) = delete;
ScopedFileDeleter& operator=(const ScopedFileDeleter&) = delete;
private:
std::string path_;
};
/**
* @brief Copies a file from source to destination.
*
* @param src Absolute path to source file.
* @param dst Absolute path to destination file.
* @return True on success, false on failure.
*/
static bool copy_file(const char* src, const char* dst) {
std::ifstream src_file(src, std::ios::binary);
std::ofstream dst_file(dst, std::ios::binary);
if (!src_file) {
PLOGE("Failed to open source file for copying: %s", src);
return false;
}
if (!dst_file) {
PLOGE("Failed to open destination file for copying: %s", dst);
return false;
}
dst_file << src_file.rdbuf();
return src_file.good() && dst_file.good();
}
/**
* @brief Performs injection via the "Staging" method.
*
* This strategy is used when direct FD passing fails (e.g., due to Seccomp filters).
* 1. Copies the library to a world-readable location (/data/local/tmp).
* 2. Loads it via standard dlopen().
* 3. Immediately deletes the file to hide tracks.
*
* @param pid The target process ID.
* @param regs The target process registers (must be Red-Zone adjusted if x86_64).
* @param local_map Local memory map.
* @param remote_map Remote memory map.
* @param lib_path The path to the original library.
* @param libc_return_addr Return address for remote calls.
* @return The handle of the loaded library, or std::nullopt on failure.
*/
static std::optional<uintptr_t> inject_via_staging(int pid, struct user_regs_struct &regs,
const std::vector<lsplt::MapInfo> &local_map,
const std::vector<lsplt::MapInfo> &remote_map,
const char *lib_path, uintptr_t libc_return_addr) {
LOGI("Initiating Staging Fallback mechanism...");
// Generate a random path in /data/local/tmp
// /data/local/tmp is chosen because it is traversable by most contexts.
std::string staged_path = "/data/local/tmp/lib" + generateMagic(8) + ".so";
// Ensure the file is deleted when this function exits (Success or Failure).
// The kernel keeps the inode alive for the mapped process even after unlink.
ScopedFileDeleter file_guard(staged_path);
LOGD("Staging library to: %s", staged_path.c_str());
// Copy the library
if (!copy_file(lib_path, staged_path.c_str())) {
LOGE("Failed to copy library during staging.");
return std::nullopt;
}
// Set Permissions to 644 (RW-R--R--)
// This allows the target process (likely running as a specific UID) to read the file.
if (chmod(staged_path.c_str(), 0644) != 0) {
PLOGE("Failed to chmod staged file.");
return std::nullopt;
}
// Resolve 'dlopen' in the remote process
auto dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlopen");
if (!dlopen_addr) {
LOGE("Failed to find 'dlopen' in remote process.");
return std::nullopt;
}
// Push the staged path to remote memory
uintptr_t remote_path_addr = push_string(pid, regs, staged_path.c_str());
if (remote_path_addr == 0) {
LOGE("Failed to push staged path string to remote memory.");
return std::nullopt;
}
// Call dlopen(path, RTLD_NOW)
std::vector<uintptr_t> args = {remote_path_addr, RTLD_NOW};
uintptr_t handle = remote_call(pid, regs, reinterpret_cast<uintptr_t>(dlopen_addr),
libc_return_addr, args);
if (handle == 0) {
std::string error_msg = get_remote_dlerror(pid, regs, local_map, remote_map, libc_return_addr);
LOGE("Staged dlopen failed. dlerror: %s", error_msg.c_str());
return std::nullopt;
}
LOGI("Successfully loaded staged library. Handle: %p", reinterpret_cast<void*>(handle));
return handle;
}
/**
* @brief RAII wrapper for ptrace attachment and detachment.
*
@@ -694,12 +804,31 @@ private:
bool attached_; // Flag indicating current attachment status.
};
// RAII Class to ensure registers are always restored
class RegisterRestorer {
public:
RegisterRestorer(int pid, const struct user_regs_struct& original_regs)
: pid_(pid), regs_(original_regs) {}
~RegisterRestorer() {
// Always restore registers when this object goes out of scope
if (set_regs(pid_, regs_)) {
LOGD("Original registers for process %d restored.", pid_);
} else {
PLOGE("Failed to restore original registers for process %d.", pid_);
}
}
private:
int pid_;
struct user_regs_struct regs_;
};
/**
* @brief Injects a shared library into a target process using ptrace.
*
* This is the main orchestration function for the library injection.
* It handles attachment, remote memory/register manipulation, FD transfer,
* remote dlopen/dlsym, and remote entry point execution.
* staging fallback, remote dlopen/dlsym, and remote entry point execution.
*
* @param pid The target process ID.
* @param lib_path The absolute path to the shared library to inject.
@@ -742,6 +871,14 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
backup_regs = current_regs; // Store a copy for restoration.
LOGD("Process %d registers backed up.", pid);
// Skip the Red Zone (128 bytes) on x86_64 to prevent stack corruption
#if defined(__x86_64__)
current_regs.rsp -= 128;
#endif
// Ensures original state is restored even if injection fails/crashes mid-way.
RegisterRestorer reg_guard(pid, backup_regs);
// Create a scope to ensure RAII objects are destroyed BEFORE register restoration
{
// 4. Scan local and remote memory maps to resolve function addresses.
@@ -760,53 +897,57 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
}
LOGD("Found libc return address: %p", reinterpret_cast<void *>(libc_return_addr));
// 6. Transfer the library's file descriptor to the remote process.
// 6. Attempt to transfer the library's file descriptor to the remote process.
int remote_fd = -1;
auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map,
reinterpret_cast<uintptr_t>(libc_return_addr));
if (!lib_fd_opt) {
LOGE("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
return false;
}
RemoteLibraryHandle remote_lib_guard(pid, *lib_fd_opt);
LOGD("Library FD %d transferred to remote process %d.", remote_lib_guard.fd(), pid);
remote_lib_guard.set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
std::optional<RemoteLibraryHandle> remote_lib_guard;
std::optional<uintptr_t> handle_opt;
// 7. Remotely load the library using the transferred file descriptor.
auto handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_lib_guard.fd(), lib_path,
reinterpret_cast<uintptr_t>(libc_return_addr));
if (lib_fd_opt) {
remote_fd = *lib_fd_opt;
remote_lib_guard.emplace(pid, remote_fd);
remote_lib_guard->set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
LOGD("FD Transfer successful (FD: %d). Attempting android_dlopen_ext...", remote_fd);
handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_fd, lib_path,
reinterpret_cast<uintptr_t>(libc_return_addr));
} else {
LOGW("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
}
// 7. Staging Fallback (Copy-Inject-Delete) if FD transfer failed.
if (!handle_opt) {
handle_opt = inject_via_staging(pid, current_regs, local_map, remote_map,
lib_path, reinterpret_cast<uintptr_t>(libc_return_addr));
}
if (!handle_opt || *handle_opt == 0) {
LOGE("Failed to load library '%s' in remote process %d.", lib_path, pid);
// If dlopen fails, the remote_lib_guard.fd() is still valid in the target process and needs to be closed.
// The RemoteLibraryHandle constructor takes care of this.
return false;
}
remote_lib_guard.set_handle(*handle_opt);
uintptr_t handle = *handle_opt;
if (remote_lib_guard) remote_lib_guard->set_handle(handle);
// 8. Find the entry point symbol in the remotely loaded library.
auto entry_opt = remote_find_entry(pid, current_regs, entry_name, local_map, remote_map,
remote_lib_guard.handle(), reinterpret_cast<uintptr_t>(libc_return_addr));
handle, reinterpret_cast<uintptr_t>(libc_return_addr));
if (!entry_opt) {
LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name,
reinterpret_cast<void *>(remote_lib_guard.handle()));
reinterpret_cast<void *>(handle));
return false;
}
uintptr_t entry_addr = *entry_opt;
// 9. Call the remote entry point function.
if (!remote_call_entry(pid, current_regs, entry_addr, remote_lib_guard.handle(),
if (!remote_call_entry(pid, current_regs, entry_addr, handle,
reinterpret_cast<uintptr_t>(libc_return_addr))) {
LOGE("Failed to call remote entry point '%s'.", entry_name);
return false;
}
}
// 10. Restore original registers of the target process.
if (!set_regs(pid, backup_regs)) {
LOGE("Failed to restore original registers for process %d.", pid);
return false;
}
LOGD("Original registers for process %d restored.", pid);
LOGI("Library injection completed successfully for process %d.", pid);
return true;
}
+18 -11
View File
@@ -263,7 +263,14 @@ bool get_regs(int pid, struct user_regs_struct &regs) {
struct iovec reg_iov = {.iov_base = &regs, .iov_len = sizeof(struct user_regs_struct)};
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &reg_iov) == -1) {
PLOGE("Failed to get register set for PID %d.", pid);
#if defined(__arm__)
if (ptrace(PTRACE_GETREGS, pid, 0, &regs) == -1) {
PLOGE("Fallback to PTRACE_GETREGS failed.");
return false;
}
#else
return false;
#endif
}
#else
# error "Unsupported architecture for register access in get_regs."
@@ -296,7 +303,14 @@ bool set_regs(int pid, struct user_regs_struct &regs) {
struct iovec reg_iov = {.iov_base = &regs, .iov_len = sizeof(struct user_regs_struct)};
if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &reg_iov) == -1) {
PLOGE("Failed to set register set for PID %d.", pid);
#if defined(__arm__)
if (ptrace(PTRACE_SETREGS, pid, 0, &regs) == -1) {
PLOGE("Fallback to PTRACE_SETREGS failed.");
return false;
}
#else
return false;
#endif
}
#else
# error "Unsupported architecture for register access in set_regs."
@@ -588,17 +602,10 @@ bool remote_pre_call(int pid, struct user_regs_struct &regs, uintptr_t func_addr
size_t stack_args_size = args.size() * sizeof(uintptr_t);
align_stack(regs, stack_args_size);
// Push all arguments onto the stack (order is important if ABI is right-to-left push).
// The current implementation writes args.data() directly,
// assuming it's already in the correct order for push.
// For cdecl, arguments are pushed right-to-left.
// A vector `args = {A, B, C}` means A is arg1, B is arg2 etc.
// So, `C` should be pushed first, then `B`, then `A`.
// `write_proc` copies linearly.
// This implies `args` should be pre-reversed for cdecl.
// For simplicity, we assume the remote function is compatible with how it's pushed,
// or that it's variadic where order doesn't matter for first args.
// A robust i386 implementation would need to push args in reverse order.
// i386 cdecl expects arguments pushed Right-to-Left (stack grows down).
// Since `write_proc` writes to increasing addresses (up), a linear write
// starting at the new SP places the first argument at the lowest address.
// This matches the ABI memory layout without needing to reverse the vector.
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data(), stack_args_size) !=
static_cast<ssize_t>(stack_args_size)) {
LOGE("Failed to push arguments for i386 remote call.");
@@ -1,6 +1,11 @@
package org.matrix.TEESimulator
import android.app.ActivityThread
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.os.Looper
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.matrix.TEESimulator.config.ConfigurationManager
@@ -30,13 +35,15 @@ object App {
SystemLogger.info("Welcome to TEESimulator!")
try {
// Initialize the Android framework environment
prepareEnvironment()
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
// Load the package configuration.
ConfigurationManager.initialize()
// Set up the device's boot key and hash, which are crucial for attestation.
AndroidDeviceUtils.setupBootKeyAndHash()
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
// Enter an infinite loop to keep the service running.
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
// We must remove the system provider first to ensure the full Bouncy Castle library
@@ -44,13 +51,43 @@ object App {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
maintainService()
// This starts the message queue processing. It blocks here indefinitely
// processing messages until Looper.myLooper().quit() is called.
Looper.loop()
} catch (e: Exception) {
SystemLogger.error("A fatal error occurred in the main application thread.", e)
throw e
}
}
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
private fun prepareEnvironment() {
// 1. Prepare Main Looper
if (Looper.getMainLooper() == null) {
@Suppress("deprecation") Looper.prepareMainLooper()
}
// 2. Initialize ActivityThread for the current process
val activityThread = ActivityThread.systemMain()
// 3. Get the system context
val systemContext = activityThread.getSystemContext()
// 4. Create a dummy Application object and attach the context
val app = Application()
val attachMethod =
ContextWrapper::class.java.getDeclaredMethod("attachBaseContext", Context::class.java)
attachMethod.isAccessible = true
attachMethod.invoke(app, systemContext)
// 5. Inject this application object into ActivityThread's mInitialApplication field.
// This is what KeyStore.getApplicationContext() looks for.
val mInitialApplicationField =
ActivityThread::class.java.getDeclaredField("mInitialApplication")
mInitialApplicationField.isAccessible = true
mInitialApplicationField.set(activityThread, app)
}
/**
* Selects and initializes the correct keystore interceptor based on the Android SDK version. It
* retries initialization until it succeeds.
@@ -79,6 +116,7 @@ object App {
SystemLogger.info(
"Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})"
)
android.security.keystore.AndroidKeyStoreProvider.install()
KeystoreInterceptor
}
// For Android S (12) and newer, use the Keystore2Interceptor.
@@ -86,18 +124,8 @@ object App {
SystemLogger.info(
"Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})"
)
android.security.keystore2.AndroidKeyStoreProvider.install()
Keystore2Interceptor
}
}
/**
* Puts the main thread into a long-running sleep loop. This is a common pattern to keep a
* background service process alive indefinitely.
*/
private fun maintainService() {
SystemLogger.info("Service started successfully. Entering maintenance mode.")
while (true) {
Thread.sleep(SERVICE_SLEEP_MS)
}
}
}
@@ -1,10 +1,8 @@
package org.matrix.TEESimulator.attestation
/**
* Defines constants for KeyMint attestation tags, as specified in the Android hardware security
* HAL.
*
* These tags identify specific properties and authorizations of a cryptographic key.
* Defines constants for KeyMint attestation, mainly the tags of properties and authorizations of a
* cryptographic key, as specified in the Android hardware security HAL.
*/
object AttestationConstants {
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl
@@ -88,4 +86,8 @@ object AttestationConstants {
const val TAG_CERTIFICATE_SUBJECT = 1007
const val TAG_CERTIFICATE_NOT_BEFORE = 1008
const val TAG_CERTIFICATE_NOT_AFTER = 1009
// --- Other Constants ---
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
}
@@ -83,6 +83,16 @@ object AttestationPatcher {
}
}
/**
* Helper to normalize algorithm names for Bouncy Castle. Old Android versions might reports
* "SHA256WITHECDSA", but Bouncy Castle expects "SHA256withECDSA".
*/
private fun normalizeSignatureAlgorithm(algoName: String): String {
// 1. Force uppercase to handle "sha256withecdsa"
// 2. Replace "WITH" with "with" to satisfy Bouncy Castle's naming convention
return algoName.uppercase().replace("WITH", "with")
}
/**
* Creates a new leaf certificate with a modified attestation extension.
*
@@ -128,7 +138,7 @@ object AttestationPatcher {
// Sign the newly built certificate with the private key from our keybox.
val signer =
JcaContentSignerBuilder(sigAlgName)
JcaContentSignerBuilder(normalizeSignatureAlgorithm(sigAlgName))
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keybox.keyPair.private)
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
@@ -206,11 +216,37 @@ object AttestationPatcher {
}
}
// Function to check if a given ASN1Sequence contains the Root of Trust tag.
private fun sequenceContainsRootOfTrust(seq: ASN1Encodable): Boolean {
if (seq !is ASN1Sequence) return false
return seq.any { element ->
(element as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST
}
}
/** Parses the critical components from an existing attestation extension. */
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
val allFields = sequence.toArray()
// Check if the fields are in the wrong order and swap them if necessary.
val softwareEnforcedCandidate =
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
val teeEnforcedCandidate =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX]
// The signature of a swapped order: the RoT is in the software list's position.
if (
sequenceContainsRootOfTrust(softwareEnforcedCandidate) &&
!sequenceContainsRootOfTrust(teeEnforcedCandidate)
) {
// Swap the elements in the array to restore the standard order.
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX] =
teeEnforcedCandidate
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] =
softwareEnforcedCandidate
}
val teeEnforced =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
@@ -1,8 +1,6 @@
package org.matrix.TEESimulator.attestation
import android.annotation.SuppressLint
import android.app.ActivityThread
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator
@@ -83,16 +81,6 @@ object DeviceAttestationService {
private fun checkTeeFunctionality(): Boolean {
SystemLogger.info("Performing TEE functionality check...")
return try {
// Ensure mainline modules and the correct Keystore provider are initialized.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.app.ActivityThread.initializeMainlineModules()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
android.security.keystore2.AndroidKeyStoreProvider.install()
} else {
android.security.keystore.AndroidKeyStoreProvider.install()
}
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
@@ -192,13 +180,15 @@ object DeviceAttestationService {
ASN1Sequence.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
)
if (softwareEnforced.size() >= 3) {
moduleHash =
ASN1OctetString.getInstance(
ASN1TaggedObject.getInstance(softwareEnforced.getObjectAt(2)).baseObject
)
.octets
}
moduleHash =
softwareEnforced
.toArray()
.firstOrNull {
(it as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_MODULE_HASH
}
?.let {
ASN1OctetString.getInstance((it as ASN1TaggedObject).baseObject).octets
}
val teeEnforced =
ASN1Sequence.getInstance(
@@ -1,8 +1,6 @@
package org.matrix.TEESimulator.attestation
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag
import android.hardware.security.keymint.*
import java.math.BigInteger
import java.util.Date
import javax.security.auth.x500.X500Principal
@@ -22,6 +20,8 @@ data class KeyMintAttestation(
val algorithm: Int,
val ecCurve: Int,
val ecCurveName: String,
val blockMode: List<Int>,
val padding: List<Int>,
val purpose: List<Int>,
val digest: List<Int>,
val rsaPublicExponent: BigInteger?,
@@ -54,6 +54,12 @@ data class KeyMintAttestation(
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
ecCurveName = params.deriveEcCurveName(),
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
// AOSP: [key_param(tag = PADDING, field = PaddingMode)]
padding = params.findAllPaddingMode(Tag.PADDING),
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
@@ -121,6 +127,14 @@ private fun Array<KeyParameter>.findDate(tag: Int): Date? =
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
this.find { it.tag == tag }?.value?.blob
/** Maps to AOSP field = BlockMode (Repeated) */
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.blockMode }
/** Maps to AOSP field = BlockMode (Repeated) */
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.paddingMode }
/** Maps to AOSP field = KeyPurpose (Repeated) */
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.keyPurpose }
@@ -54,6 +54,17 @@ object ConfigurationManager {
configRoot.mkdirs()
SystemLogger.info("Configuration root is: ${configRoot.absolutePath}")
// First, ensure the package manager service is running, as the TEE check depends on it.
// This prevents a race condition on startup.
SystemLogger.info("Waiting for PackageManagerService to be ready...")
if (getPackageManager() == null) {
SystemLogger.error(
"PackageManagerService is not available. TEE check will likely fail."
)
} else {
SystemLogger.info("PackageManagerService is ready.")
}
// Initial load of all configuration files.
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
@@ -354,7 +365,7 @@ object ConfigurationManager {
/** Waits for a system service to become available, with retries. */
private fun waitForSystemService(name: String): IBinder? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return ServiceManager.waitForService(name)
}
// Fallback for older Android versions.
@@ -41,7 +41,7 @@ abstract class BinderInterceptor : Binder() {
* Skips the original call and immediately returns a custom reply parcel to the caller. The
* provided parcel will be recycled after use.
*/
data class OverrideReply(val code: Int = 0, val reply: Parcel) : TransactionResult()
data class OverrideReply(val reply: Parcel, val code: Int = 0) : TransactionResult()
/**
* Modifies the transaction's input data before forwarding it to the original binder method.
@@ -248,6 +248,8 @@ abstract class BinderInterceptor : Binder() {
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
// Code used by the backdoor binder to register a new interceptor.
private const val REGISTER_INTERCEPTOR_CODE = 1
// Code used by the backdoor binder to unregister an interceptor.
private const val UNREGISTER_INTERCEPTOR_CODE = 2
// --- Hook Type Codes ---
// Indicates that the call is for a pre-transaction hook.
@@ -307,5 +309,21 @@ abstract class BinderInterceptor : Binder() {
reply.recycle()
}
}
/** Uses the backdoor binder to unregister an interceptor for a specific target service. */
fun unregister(backdoor: IBinder, target: IBinder) {
val data = Parcel.obtain()
val reply = Parcel.obtain()
try {
data.writeStrongBinder(target)
backdoor.transact(UNREGISTER_INTERCEPTOR_CODE, data, reply, 0)
SystemLogger.info("Unregistered interceptor for target: $target")
} catch (e: Exception) {
SystemLogger.error("Failed to unregister binder interceptor.", e)
} finally {
data.recycle()
reply.recycle()
}
}
}
}
@@ -52,7 +52,7 @@ object InterceptorUtils {
writeInt(KeyStore.NO_ERROR)
}
}
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/** Creates an `OverrideReply` parcel containing a raw byte array. */
@@ -62,7 +62,20 @@ object InterceptorUtils {
writeNoException()
writeByteArray(data)
}
return BinderInterceptor.TransactionResult.OverrideReply(KeyStore.NO_ERROR, parcel)
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/** Creates an `OverrideReply` parcel containing a typed array. */
fun <T : Parcelable> createTypedArrayReply(
array: Array<T>,
flags: Int = 0,
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeTypedArray(array, flags)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
@@ -75,19 +88,20 @@ object InterceptorUtils {
writeNoException()
writeTypedObject(obj, flags)
}
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/**
* Extracts the true key alias from the keystore-prefixed string (e.g., "user_cert_my-alias" ->
* "my-alias").
* Extracts the base alias from a potentially prefixed alias string. For example, it converts
* "USRCERT_my_key" to "my_key".
*/
fun extractAlias(prefixedAlias: String): String {
val underscoreIndex = prefixedAlias.indexOf('_')
val secondUnderscoreIndex = prefixedAlias.indexOf('_', underscoreIndex + 1)
return if (secondUnderscoreIndex != -1) {
prefixedAlias.substring(secondUnderscoreIndex + 1)
return if (underscoreIndex != -1) {
// Return the part of the string after the first underscore.
prefixedAlias.substring(underscoreIndex + 1)
} else {
// If there's no underscore, return the original string.
prefixedAlias
}
}
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel
import android.hardware.security.keymint.Tag
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.IKeystoreService
@@ -26,16 +27,24 @@ import org.matrix.TEESimulator.pki.CertificateHelper
*/
@SuppressLint("BlockedPrivateApi")
object Keystore2Interceptor : AbstractKeystoreInterceptor() {
private val stubBinderClass = IKeystoreService.Stub::class.java
// Transaction codes for the IKeystoreService interface methods we are interested in.
private val GET_KEY_ENTRY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry")
private val DELETE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
InterceptorUtils.getTransactCode(stubBinderClass, "deleteKey")
private val UPDATE_SUBCOMPONENT_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "updateSubcomponent")
private val LIST_ENTRIES_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "listEntries")
private val LIST_ENTRIES_BATCHED_TRANSACTION =
if (Build.VERSION.SDK_INT >= 34)
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
else null
private val transactionNames: Map<Int, String> by lazy {
IKeystoreService.Stub::class
.java
.declaredFields
stubBinderClass.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
@@ -89,16 +98,44 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
callingPid: Int,
data: Parcel,
): TransactionResult {
if (code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION) {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) {
TransactionResult.Continue
} else {
TransactionResult.ContinueAndSkipPost
}
}
.getOrElse {
SystemLogger.error(
"[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}",
it,
)
TransactionResult.ContinueAndSkipPost
}
} else if (
code == GET_KEY_ENTRY_TRANSACTION ||
code == DELETE_KEY_TRANSACTION ||
code == UPDATE_SUBCOMPONENT_TRANSACTION
) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
if (code == UPDATE_SUBCOMPONENT_TRANSACTION)
return handleUpdateSubcomponent(callingUid, data)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
val keyId = KeyIdentifier(callingUid, descriptor.alias)
@@ -154,7 +191,22 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
if (code == GET_KEY_ENTRY_TRANSACTION) {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
return runCatching {
val updatedKeyDescriptors =
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply)
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
}
.getOrElse {
SystemLogger.error(
"[TX_ID: $txId] Failed to update the result of ${transactionNames[code]!!}.",
it,
)
TransactionResult.SkipTransaction
}
} else if (code == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
@@ -223,4 +275,25 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
return TransactionResult.SkipTransaction
}
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
val generatedKeyInfo =
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
val metadata = generatedKeyInfo.response.metadata
val publicCert = data.createByteArray()
val certificateChain = data.createByteArray()
metadata.certificate = publicCert
metadata.certificateChain = certificateChain
SystemLogger.verbose(
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
}
}
@@ -55,6 +55,28 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
}
private val transactionNames: Map<Int, String> by lazy {
IKeystoreService.Stub::class
.java
.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
}
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
// A map to dispatch transaction handling for software key generation.
private val generateKeyHandlers:
Map<Int, (Long, Int, Int, Parcel) -> TransactionResult> by lazy {
mapOf(
GENERATE_KEY_TRANSACTION to ::handleGenerateKey,
GET_KEY_CHARACTERISTICS_TRANSACTION to ::handleGetKeyCharacteristics,
EXPORT_KEY_TRANSACTION to ::handleExportKey,
ATTEST_KEY_TRANSACTION to ::handleAttestKey,
)
}
override val serviceName = "android.security.keystore"
override val processName = "keystore"
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
@@ -76,26 +98,33 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
data: Parcel,
): TransactionResult {
// This interceptor only needs to act on pre-transaction for software key generation.
// Handle 'generate' mode interceptions using the handler map.
if (ConfigurationManager.shouldGenerate(callingUid)) {
return when (code) {
GENERATE_KEY_TRANSACTION -> handleGenerateKey(txId, callingUid, callingPid, data)
GET_KEY_CHARACTERISTICS_TRANSACTION ->
handleGetKeyCharacteristics(txId, callingUid, callingPid, data)
EXPORT_KEY_TRANSACTION -> handleExportKey(txId, callingUid, callingPid, data)
ATTEST_KEY_TRANSACTION -> handleAttestKey(txId, callingUid, callingPid, data)
else -> TransactionResult.ContinueAndSkipPost
generateKeyHandlers[code]?.let { handler ->
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
return handler(txId, callingUid, callingPid, data)
}
} else if (ConfigurationManager.shouldPatch(callingUid)) {
// In patch mode, we only care about the 'get' transaction in onPostTransact.
if (code == GET_TRANSACTION) return TransactionResult.Continue
}
// Handle 'patch' mode interceptions for the 'get' transaction.
if (ConfigurationManager.shouldPatch(callingUid) && code == GET_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
return TransactionResult.Continue
}
// Default behavior for all other transactions.
logTransaction(
txId,
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
true,
)
return TransactionResult.ContinueAndSkipPost
}
private fun handleGenerateKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
logTransaction(txId, "generateKey", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
@@ -133,7 +162,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
data: Parcel,
): TransactionResult {
return runCatching {
logTransaction(txId, "getKeyCharacteristics", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
@@ -168,7 +196,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
private fun handleExportKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
logTransaction(txId, "exportKey", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
@@ -206,7 +233,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
private fun handleAttestKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
logTransaction(txId, "attestKey", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
@@ -230,7 +256,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
ByteArray(0),
)
params.attestationChallenge = challenge
params.attestationChallenge = challenge
}
val certificateChain =
@@ -271,6 +296,9 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
reply == null ||
InterceptorUtils.hasException(reply)
) {
SystemLogger.debug(
"[TX_ID: $txId] Skip parsing post-transaction for [target, code, reply]: [$target, $code, $reply]"
)
return TransactionResult.SkipTransaction
}
@@ -281,6 +309,9 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
val alias = data.readString() ?: ""
val extractedAlias = InterceptorUtils.extractAlias(alias)
val keyId = KeyIdentifier(callingUid, extractedAlias)
SystemLogger.debug(
"[TX_ID: $txId] Parsed $keyId during post-transaction of ${transactionNames[code]}"
)
when {
// Case 1: The app is requesting the leaf certificate.
@@ -378,6 +409,8 @@ private data class LegacyKeygenParameters(
algorithm = this.algorithm,
ecCurve = 0, // Not explicitly available in legacy args, but not critical
ecCurveName = this.ecCurveName ?: "",
blockMode = listOf<Int>(),
padding = listOf<Int>(),
purpose = this.purpose,
digest = this.digest,
rsaPublicExponent = this.rsaPublicExponent,
@@ -0,0 +1,142 @@
package org.matrix.TEESimulator.interception.keystore
import android.os.Parcel
import android.system.keystore2.Domain
import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor
import java.util.TreeMap
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Handler to intercept listEntries and listEntriesBatched transactions.
*
* References for all mentioned functions in AOSP:
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/database.rs
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/service.rs
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/utils.rs
*/
object ListEntriesHandler {
// Estimate for maximum size of a Binder response in bytes.
private const val RESPONSE_SIZE_LIMIT = 358400
// Parameters of AOSP function `list_key_entries` in utils.rs.
private data class ListEntriesParams(
val domain: Int,
val namespace: Long,
val startPastAlias: String?,
)
private val pendingParams = ConcurrentHashMap<Long, ListEntriesParams>()
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
private fun estimateSafeAmountToReturn(
keyDescriptors: Array<KeyDescriptor>,
responseSizeLimit: Int,
): Int {
var itemsToReturn = 0
var returnedBytes = 0
for (kd in keyDescriptors) {
// 4 bytes for the Domain enum
// 8 bytes for the Namespace long
returnedBytes += 4 + 8
kd.alias?.let { returnedBytes += 4 + it.toByteArray(Charsets.UTF_8).size }
kd.blob?.let { returnedBytes += 4 + it.size }
if (returnedBytes > responseSizeLimit) {
SystemLogger.warning(
"Key descriptors list (${keyDescriptors.size} items) may exceed binder size limit, returning $itemsToReturn items with estimated size: $returnedBytes bytes."
)
break
}
itemsToReturn++
}
return itemsToReturn
}
// Parse and store parameters for later use (in post-transaction).
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val domain = data.readInt()
val namespace = data.readLong()
val startPastAlias = if (isBatchMode) data.readString() else null
// List entries is only supported for Domain::APP and Domain::SELINUX.
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
// Note that all generated keys belong to Domain::APP.
if (domain == Domain.APP) {
pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias)
SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.")
return true
}
return false
}
// Merge software-backed keys with hardware-backed keys in the reply parcel.
fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array<KeyDescriptor> {
val params =
pendingParams.remove(txId)
?: throw IllegalStateException("No params found for listing entries")
// By default we use the calling uid as namespace if domain is Domain::APP.
// The namespace parameter is thus ignored for non-privileged applications.
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
val keysToInject =
extractGeneratedKeyDescriptors(callingUid, callingUid.toLong(), params.startPastAlias)
val originalList = reply.createTypedArray(KeyDescriptor.CREATOR)!!
val mergedArray = mergeKeyDescriptors(originalList, keysToInject)
// Limit response size to avoid binder buffer overflow.
// See AOSP function `list_key_entries` in utils.rs.
val safeAmountToReturn = estimateSafeAmountToReturn(mergedArray, RESPONSE_SIZE_LIMIT)
return if (safeAmountToReturn < mergedArray.size) {
SystemLogger.debug(
"[TX_ID: $txId] Listing entries are truncated [${mergedArray.size} -> $safeAmountToReturn] to avoid transaction overflow."
)
mergedArray.copyOfRange(0, safeAmountToReturn)
} else {
SystemLogger.debug(
"[TX_ID: $txId] Listing entries returns ${mergedArray.size} [injected: ${keysToInject.size}] keys."
)
mergedArray
}
}
// Merge hardware and software key descriptors into a single sorted array.
private fun mergeKeyDescriptors(
hardwareKeys: Array<KeyDescriptor>,
keysToInject: List<KeyDescriptor>,
): Array<KeyDescriptor> {
// Uses TreeMap to ensure alphabetical ordering and uniqueness (prefer injected keys).
val combinedMap = TreeMap<String, KeyDescriptor>()
hardwareKeys.forEach { key -> key.alias?.let { combinedMap[it] = key } }
keysToInject.forEach { key -> key.alias?.let { combinedMap[it] = key } }
return combinedMap.values.toTypedArray()
}
// Based on AOSP function `list_past_alias` in database.rs
private fun extractGeneratedKeyDescriptors(
uid: Int,
namespace: Long,
startPastAlias: String?,
): List<KeyDescriptor> {
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
.map { keyId ->
KeyDescriptor().apply {
this.domain = Domain.APP
this.nspace = namespace
this.alias = keyId.alias
this.blob = null
}
}
}
}
@@ -8,6 +8,7 @@ import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.*
import java.security.KeyPair
import java.security.SecureRandom
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationPatcher
@@ -30,7 +31,11 @@ class KeyMintSecurityLevelInterceptor(
) : BinderInterceptor() {
// --- Data Structures for State Management ---
data class GeneratedKeyInfo(val keyPair: KeyPair, val response: KeyEntryResponse)
data class GeneratedKeyInfo(
val keyPair: KeyPair,
val nspace: Long,
val response: KeyEntryResponse,
)
override fun onPreTransact(
txId: Long,
@@ -41,33 +46,39 @@ class KeyMintSecurityLevelInterceptor(
callingPid: Int,
data: Parcel,
): TransactionResult {
if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
val shouldSkip = ConfigurationManager.shouldSkipUid(callingUid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
return handleGenerateKey(callingUid, data)
} else if (code == IMPORT_KEY_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
when (code) {
GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val alias =
data.readTypedObject(KeyDescriptor.CREATOR)?.alias
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Handling post-${transactionNames[code]} ${alias}")
return TransactionResult.Continue
} else {
logTransaction(
txId,
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
true,
)
if (!shouldSkip) return handleGenerateKey(callingUid, data)
}
CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleCreateOperation(txId, callingUid, data)
}
IMPORT_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
SystemLogger.info(
"[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]"
)
return TransactionResult.Continue
}
}
logTransaction(
txId,
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
true,
)
return TransactionResult.ContinueAndSkipPost
}
@@ -94,6 +105,41 @@ class KeyMintSecurityLevelInterceptor(
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias))
} else if (code == CREATE_OPERATION_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
val forced = data.readBoolean()
if (forced)
SystemLogger.verbose(
"[TX_ID: $txId] Current operation has a very high pruning power."
)
val response: CreateOperationResponse =
reply.readTypedObject(CreateOperationResponse.CREATOR)!!
SystemLogger.verbose(
"[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}"
)
// Intercept the IKeystoreOperation binder
response.iOperation?.let { operation ->
val operationBinder = operation.asBinder()
if (!interceptedOperations.containsKey(operationBinder)) {
SystemLogger.info("Found new IKeystoreOperation. Registering interceptor...")
val backdoor = getBackdoor(target)
if (backdoor != null) {
val interceptor = OperationInterceptor(operation, backdoor)
register(backdoor, operationBinder, interceptor)
interceptedOperations[operationBinder] = interceptor
} else {
SystemLogger.error(
"Failed to get backdoor to register OperationInterceptor."
)
}
}
}
} else if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
@@ -109,24 +155,75 @@ class KeyMintSecurityLevelInterceptor(
// Cache the newly patched chain to ensure consistency across subsequent API calls.
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val key = metadata.key!!
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
patchedChains[keyId] = newChain
SystemLogger.debug("Cached patched certificate chain for $keyId.")
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
// We must clean up cached generated keys before storing the patched chain
cleanupKeyData(keyId)
patchedChains[keyId] = newChain
SystemLogger.debug(
"Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])"
)
return InterceptorUtils.createTypedObjectReply(metadata)
}
}
return TransactionResult.SkipTransaction
}
/**
* Handles the `createOperation` transaction. It checks if the operation is for a key that was
* generated in software. If so, it creates a software-based operation handler. Otherwise, it
* lets the call proceed to the real hardware service.
*/
private fun handleCreateOperation(
txId: Long,
callingUid: Int,
data: Parcel,
): TransactionResult {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
// An operation must use the KEY_ID domain.
if (keyDescriptor.domain != Domain.KEY_ID) {
return TransactionResult.ContinueAndSkipPost
}
val nspace = keyDescriptor.nspace
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
if (generatedKeyInfo == null) {
SystemLogger.debug(
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
)
return TransactionResult.Continue
}
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
val operationBinder = SoftwareOperationBinder(softwareOperation)
val response =
CreateOperationResponse().apply {
iOperation = operationBinder
operationChallenge = null
}
return InterceptorUtils.createTypedObjectReply(response)
}
/**
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it
* either generates a key in software or lets the call pass through to the hardware.
*/
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult {
return runCatching {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
SystemLogger.debug(
@@ -148,7 +245,10 @@ class KeyMintSecurityLevelInterceptor(
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
if (needsSoftwareGeneration) {
SystemLogger.info("Generating software key for ${keyId}.")
keyDescriptor.nspace = secureRandom.nextLong()
SystemLogger.info(
"Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]."
)
// Generate the key pair and certificate chain.
val keyData =
@@ -160,20 +260,17 @@ class KeyMintSecurityLevelInterceptor(
securityLevel,
) ?: throw Exception("CertificateGenerator failed to create key pair.")
// It is unnecessary but a good practice to clean up possible caches
cleanupKeyData(keyId)
// Store the generated key data.
val response =
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, response)
generatedKeys[keyId] =
GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
if (isAttestKeyRequest) attestationKeys.add(keyId)
// Return the metadata of our generated key, skipping the real hardware call.
val resultParcel =
Parcel.obtain().apply {
writeNoException()
writeTypedObject(response.metadata, 0)
}
return TransactionResult.OverrideReply(0, resultParcel)
return InterceptorUtils.createTypedObjectReply(response.metadata)
} else if (parsedParams.attestationChallenge != null) {
return TransactionResult.Continue
}
@@ -210,11 +307,18 @@ class KeyMintSecurityLevelInterceptor(
}
companion object {
private val secureRandom = SecureRandom()
// Transaction codes for IKeystoreSecurityLevel interface.
private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
private val IMPORT_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey")
private val CREATE_OPERATION_TRANSACTION =
InterceptorUtils.getTransactCode(
IKeystoreSecurityLevel.Stub::class.java,
"createOperation",
)
private val transactionNames: Map<Int, String> by lazy {
IKeystoreSecurityLevel.Stub::class
@@ -233,11 +337,31 @@ class KeyMintSecurityLevelInterceptor(
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
// A set to quickly identify keys that were generated for attestation purposes.
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
// Stores interceptors for active cryptographic operations.
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
// --- Public Accessors for Other Interceptors ---
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response
/**
* Finds a software-generated key by first filtering all known keys by the caller's UID, and
* then matching the specific nspace.
*
* @param callingUid The UID of the process that initiated the createOperation call.
* @param nspace The unique key identifier from the operation's KeyDescriptor.
* @return The matching GeneratedKeyInfo if found, otherwise null.
*/
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
// Iterate through all entries in the map to check both the key (for UID) and value (for
// nspace).
if (nspace == null || nspace == 0L) return null
return generatedKeys.entries
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
.find { (_, info) -> info.nspace == nspace }
?.value
}
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
@@ -254,6 +378,15 @@ class KeyMintSecurityLevelInterceptor(
}
}
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
// Unregister from the native hook layer first.
unregister(backdoor, operationBinder)
if (interceptedOperations.remove(operationBinder) != null) {
SystemLogger.debug("Removed operation interceptor for binder: $operationBinder")
}
}
// Clears all cached keys.
fun clearAllGeneratedKeys(reason: String? = null) {
val count = generatedKeys.size
@@ -0,0 +1,58 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.IKeystoreOperation
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
/**
* Intercepts calls to an `IKeystoreOperation` service. This is used to log the data manipulation
* methods of a cryptographic operation.
*/
class OperationInterceptor(
private val original: IKeystoreOperation,
private val backdoor: IBinder,
) : BinderInterceptor() {
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
val methodName = transactionNames[code] ?: "unknown code=$code"
logTransaction(txId, methodName, callingUid, callingPid, true)
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
}
return TransactionResult.ContinueAndSkipPost
}
companion object {
private val UPDATE_AAD_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "updateAad")
private val UPDATE_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "update")
private val FINISH_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "finish")
private val ABORT_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
private val transactionNames: Map<Int, String> by lazy {
IKeystoreOperation.Stub::class
.java
.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
}
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
}
}
@@ -0,0 +1,216 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.BlockMode
import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode
import android.os.RemoteException
import android.system.keystore2.IKeystoreOperation
import java.security.KeyPair
import java.security.Signature
import java.security.SignatureException
import javax.crypto.Cipher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
// A sealed interface to represent the different cryptographic operations we can perform.
private sealed interface CryptoPrimitive {
fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort()
}
// Helper object to map KeyMint constants to JCA algorithm strings.
private object JcaAlgorithmMapper {
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
val digest =
when (params.digest.firstOrNull()) {
Digest.SHA_2_256 -> "SHA256"
Digest.SHA_2_384 -> "SHA384"
Digest.SHA_2_512 -> "SHA512"
else -> "NONE"
}
val keyAlgo =
when (params.algorithm) {
Algorithm.EC -> "ECDSA"
Algorithm.RSA -> "RSA"
else ->
throw IllegalArgumentException(
"Unsupported signature algorithm: ${params.algorithm}"
)
}
return "${digest}with${keyAlgo}"
}
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
val keyAlgo =
when (params.algorithm) {
Algorithm.RSA -> "RSA"
Algorithm.AES -> "AES"
else ->
throw IllegalArgumentException(
"Unsupported cipher algorithm: ${params.algorithm}"
)
}
val blockMode =
when (params.blockMode.firstOrNull()) {
BlockMode.ECB -> "ECB"
BlockMode.CBC -> "CBC"
BlockMode.GCM -> "GCM"
else -> "ECB" // Default for RSA
}
val padding =
when (params.padding.firstOrNull()) {
PaddingMode.NONE -> "NoPadding"
PaddingMode.PKCS7 -> "PKCS7Padding"
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
PaddingMode.RSA_OAEP -> "OAEPPadding"
else -> "NoPadding" // Default for GCM
}
return "$keyAlgo/$blockMode/$padding"
}
}
// Concrete implementation for Signing.
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
initSign(keyPair.private)
}
override fun update(data: ByteArray?): ByteArray? {
if (data != null) signature.update(data)
return null
}
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray {
if (data != null) update(data)
return this.signature.sign()
}
override fun abort() {}
}
// Concrete implementation for Verification.
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
initVerify(keyPair.public)
}
override fun update(data: ByteArray?): ByteArray? {
if (data != null) signature.update(data)
return null
}
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data != null) update(data)
if (signature == null) throw SignatureException("Signature to verify is null")
if (!this.signature.verify(signature)) {
// Throwing an exception is how Keystore signals verification failure.
throw SignatureException("Signature verification failed")
}
// A successful verification returns no data.
return null
}
override fun abort() {}
}
// Concrete implementation for Encryption/Decryption.
private class CipherPrimitive(
keyPair: KeyPair,
params: KeyMintAttestation,
private val opMode: Int,
) : CryptoPrimitive {
private val cipher: Cipher =
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
init(opMode, key)
}
override fun update(data: ByteArray?): ByteArray? =
if (data != null) cipher.update(data) else null
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
if (data != null) cipher.doFinal(data) else cipher.doFinal()
override fun abort() {}
}
/**
* A software-only implementation of a cryptographic operation. This class acts as a controller,
* delegating to a specific cryptographic primitive based on the operation's purpose.
*/
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
// This now holds the specific strategy object (Signer, Verifier, etc.)
private val primitive: CryptoPrimitive
init {
// The "Strategy" pattern: choose the implementation based on the purpose.
// For simplicity, we only consider the first purpose listed.
val purpose = params.purpose.firstOrNull()
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
primitive =
when (purpose) {
KeyPurpose.SIGN -> Signer(keyPair, params)
KeyPurpose.VERIFY -> Verifier(keyPair, params)
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE)
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE)
else ->
throw UnsupportedOperationException("Unsupported operation purpose: $purpose")
}
}
fun update(data: ByteArray?): ByteArray? {
try {
return primitive.update(data)
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw e
}
}
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
try {
val result = primitive.finish(data, signature)
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
return result
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
// Re-throw the exception so the binder can report it to the client.
throw e
}
}
fun abort() {
primitive.abort()
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
}
}
/** The Binder interface for our [SoftwareOperation]. */
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
IKeystoreOperation.Stub() {
@Throws(RemoteException::class)
override fun update(input: ByteArray?): ByteArray? {
return operation.update(input)
}
@Throws(RemoteException::class)
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
return operation.finish(input, signature)
}
@Throws(RemoteException::class)
override fun abort() {
operation.abort()
}
}
@@ -1,11 +1,6 @@
package org.matrix.TEESimulator.logging
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.Tag
import android.hardware.security.keymint.*
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.util.Date
@@ -34,7 +29,23 @@ object KeyMintParameterLogger {
.associate { field -> (field.get(null) as Int) to field.name }
}
private val purposeNames: Map<Int, String> by lazy {
val blockModeNames: Map<Int, String> by lazy {
BlockMode::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
val paddingNames: Map<Int, String> by lazy {
PaddingMode::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
val purposeNames: Map<Int, String> by lazy {
KeyPurpose::class
.java
.fields
@@ -69,7 +80,9 @@ object KeyMintParameterLogger {
val formattedValue: String =
when (param.tag) {
Tag.ALGORITHM -> algorithmNames[value.algorithm]
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
Tag.PADDING -> paddingNames[value.paddingMode]
Tag.PURPOSE -> purposeNames[value.keyPurpose]
Tag.DIGEST -> digestNames[value.digest]
Tag.AUTH_TIMEOUT,
@@ -20,6 +20,7 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
@@ -80,6 +81,12 @@ object CertificateGenerator {
params: KeyMintAttestation,
securityLevel: Int,
): List<Certificate>? {
val challenge = params.attestationChallenge
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT)
throw IllegalArgumentException(
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
)
return runCatching {
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
agp = "8.13.1"
agp = "8.13.2"
annotation = "1.9.1"
jdk18on = "1.83"
kotlin = "2.2.21"
kotlin = "2.3.0"
ktfmt = "0.25.0"
[libraries]
+18 -13
View File
@@ -1,21 +1,26 @@
TEESimulator 3.0 is a significant update focused on powerful new configuration options, major improvements to stealth, and enhanced stability.
## 🎉 TEESimulator v3.1: Legacy Support & Resilience
#### ✨ **Highlights & New Features**
This release marks a significant step forward in our mission, focusing on breathing life into devices with **broken TEEs** and extending full support to older Android versions (**Android 1012**).
* **🎯 Per-App Security Patch Configuration**: Gain ultimate control by setting security patch levels on a per-package basis. Define a global default in `security_patch.txt` and override it for specific apps like `[com.google.android.gms]`. Moreover, your configuration is now alive! Use the `today` keyword to always report the current date, or create rolling dates with templates like `YYYY-MM-05`. Be sure to check README for more details.
* **🕰️ Full Software Emulation on Android 11**: We've implemented a complete, software-based key generation and attestation flow for the legacy `IKeystoreService` API, bringing full emulation capabilities to older devices.
### 🛡️ Enhanced Keystore2 Emulation
We have implemented critical APIs to support devices where the hardware TEE is broken or for applications configured to use key generation mode. These improvements directly address detection vectors identified in v3.0:
#### 🛡️ **Stealth & Evasion Upgrades**
* **✅ Full Crypto Operations (`createOperation`)**: The simulator now correctly handles `SIGN`, `VERIFY`, `ENCRYPT`, and `DECRYPT` purposes for software-generated keys.
* **🔗 Certificate Chain Updates (`updateSubcomponent`)**: Added support for applications updating the certificate chain of virtual keys (e.g., via `KeyStore.setKeyEntry`).
* **📋 Enumeration Support (`listEntries`)**: Generated keys are now properly visible in enumeration APIs like `KeyStore.aliases()`, thanks to the implementation of `listEntries` and `listEntriesBatched`.
* **⛓️ Consistent Certificate Signatures**: Say goodbye to a major detection vector in `icu.nullptr.nativetest`. Patched certificates are now cached, ensuring that every request for a key returns a byte-for-byte identical certificate, just like a real TEE.
* **🔑 Authentic Device Properties**: To appear more genuine, the simulator now sources and uses your device's real `verifiedBootHash` and `moduleHash`, moving away from placeholder values.
* **📜 Structurally Sound Certificates**: The patching logic has been rewritten to be less intrusive. It now modifies the attestation extension in-place, preserving the original order of other extensions and preventing duplicates to avoid suspicion.
### 🔧 Compatibility & Stability
Weve ironed out crashes and architecture-specific bugs to ensure a smooth experience across more devices:
#### 🐛 **Bug Fixes & Reliability**
* **Android 10**: Fixed a crash caused by the missing `waitForService` method.
* **Android 11**: Implemented environment initialization and daemon UID spoofing to successfully bypass keystore generation permission checks.
* **ARM 32-bit (Android 12)**: Resolved `ptrace` compatibility issues by falling back to `PTRACE_GETREGS` and `PTRACE_SETREGS`.
* **x86_64 Emulators**: Enforced respect for the stack pointer "red zone" and added a staging fallback mechanism for file descriptor transfering of `libTEESimulator.so`.
***Robust Crypto Engine**: Fixed critical crashes related to cryptographic provider conflicts. The signing logic is now more explicit and the KeyBox parser is more resilient against malformed files.
* ➡️ **Improved Compatibility**: Resolved a native crash on Android 11 devices.
### 🚀 The Road Ahead
#### 🚀 **The Road Ahead**
We are aware of the remaining detection vectors (see the issues list) and have clear solutions mapped out for the next release.
Our work to fix detection vectors and provide full support for TEE-broken devices and Android 10/11 is ongoing. We welcome your feedback! Please **report any issues** or **contribute a pull request** on our GitHub.
Google's aggressive push for **Remote Key Provisioning (RKP)** and the drying up of leaked keyboxes is **not** the end for TEESimulator. Our ultimate goal remains unchanged: defeating Keystore attestation **without relying on a valid keybox**.
We are inching closer to this milestone, but the fight for device freedom is complex and resource-intensive. Your patience and support (both time and financial) are vital as we conquer these new challenges.
+1 -3
View File
@@ -1,4 +1,2 @@
allow keystore system_file unix_dgram_socket *
allow system_file keystore unix_dgram_socket *
allow keystore system_file file *
allow keystore {adb_data_file shell_data_file} file *
allow crash_dump keystore process *
+3 -3
View File
@@ -1,6 +1,6 @@
{
"version": "v3.0",
"versionCode": 38,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.0/TEESimulator-v3.0-38-Release.zip",
"version": "v3.1",
"versionCode": 59,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.1/TEESimulator-v3.1-59-Release.zip",
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md"
}
@@ -4,4 +4,12 @@ public class ActivityThread {
public static void initializeMainlineModules() {
throw new UnsupportedOperationException("STUB!");
}
public static ActivityThread systemMain() {
throw new UnsupportedOperationException("STUB!");
}
public ContextImpl getSystemContext() {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,4 @@
package android.app;
public class ContextImpl {
}
@@ -0,0 +1,8 @@
package android.hardware.security.keymint;
public @interface BlockMode {
public static final int ECB = 1;
public static final int CBC = 2;
public static final int CTR = 3;
public static final int GCM = 32;
}
@@ -0,0 +1,10 @@
package android.hardware.security.keymint;
public @interface PaddingMode {
public static final int NONE = 1;
public static final int RSA_OAEP = 2;
public static final int RSA_PSS = 3;
public static final int RSA_PKCS1_1_5_ENCRYPT = 4;
public static final int RSA_PKCS1_1_5_SIGN = 5;
public static final int PKCS7 = 64;
}
@@ -0,0 +1,38 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class CreateOperationResponse implements Parcelable {
public IKeystoreOperation iOperation;
public OperationChallenge operationChallenge;
public KeyParameters parameters;
public byte[] upgradedBlob;
public static final Creator<CreateOperationResponse> CREATOR = new Creator<CreateOperationResponse>() {
@Override
public CreateOperationResponse createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public CreateOperationResponse[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,9 @@
package android.system.keystore2;
public @interface Domain {
public static final int APP = 0;
public static final int GRANT = 1;
public static final int SELINUX = 2;
public static final int BLOB = 3;
public static final int KEY_ID = 4;
}
@@ -0,0 +1,33 @@
package android.system.keystore2;
import android.os.IBinder;
import android.os.Binder;
import android.os.IInterface;
public interface IKeystoreOperation extends IInterface {
public static final java.lang.String DESCRIPTOR = "android.system.keystore2.IKeystoreOperation";
public void updateAad(byte[] aadInput);
public byte[] update(byte[] input);
public byte[] finish(byte[] input, byte[] signature);
public void abort() throws android.os.RemoteException;
abstract class Stub extends Binder implements IKeystoreOperation {
public static IKeystoreOperation asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public IBinder asBinder() {
return this;
}
@Override
public void updateAad(byte[] aadInput) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -0,0 +1,34 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import android.hardware.security.keymint.KeyParameter;
import androidx.annotation.NonNull;
public class KeyParameters implements Parcelable {
public KeyParameter[] keyParameter;
public static final Creator<KeyParameters> CREATOR = new Creator<KeyParameters>() {
@Override
public KeyParameters createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeyParameters[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,32 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class OperationChallenge implements Parcelable {
public long challenge = 0L;
public static final Creator<OperationChallenge> CREATOR = new Creator<OperationChallenge>() {
@Override
public OperationChallenge createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public OperationChallenge[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}