@@ -1,30 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<!--
|
||||
Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TrickyStoreOSS">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
<manifest/>
|
||||
@@ -1,3 +1,33 @@
|
||||
# Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
project(TrickyStoreOSS)
|
||||
cmake_minimum_required(VERSION 3.28)
|
||||
project(TrickyStoreOSS)
|
||||
|
||||
find_package(cxx REQUIRED CONFIG)
|
||||
link_libraries(cxx::cxx)
|
||||
|
||||
add_library(my_logging STATIC logging/logging.cpp)
|
||||
target_include_directories(my_logging PUBLIC logging/include)
|
||||
target_link_libraries(my_logging log)
|
||||
|
||||
set(LSPLT_SOURCES external/LSPlt/lsplt/src/main/jni/lsplt.cc external/LSPlt/lsplt/src/main/jni/elf_util.cc)
|
||||
add_library(lsplt STATIC ${LSPLT_SOURCES})
|
||||
target_include_directories(lsplt PUBLIC external/LSPlt/lsplt/src/main/jni/include)
|
||||
target_include_directories(lsplt PRIVATE external/LSPlt/lsplt/src/main/jni)
|
||||
target_link_libraries(lsplt PUBLIC my_logging)
|
||||
|
||||
# libutils stub
|
||||
add_library(utils SHARED stub/stub_utils.cpp)
|
||||
target_include_directories(utils PUBLIC external/AOSP/include)
|
||||
|
||||
# libbinder stub
|
||||
add_library(binder SHARED stub/stub_binder.cpp)
|
||||
target_include_directories(binder PUBLIC external/AOSP/include)
|
||||
target_link_libraries(binder PRIVATE utils)
|
||||
|
||||
add_executable(libinject.so inject/main.cpp inject/utils.cpp)
|
||||
target_link_libraries(libinject.so PRIVATE lsplt my_logging)
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE log binder utils lsplt my_logging)
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
// Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <android/binder.h>
|
||||
#include <binder/Binder.h>
|
||||
#include <binder/Common.h>
|
||||
#include <binder/IPCThreadState.h>
|
||||
#include <binder/IServiceManager.h>
|
||||
#include <binder/Parcel.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <utils/StrongPointer.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <shared_mutex>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "logging.hpp"
|
||||
#include "lsplt.hpp"
|
||||
|
||||
using namespace android;
|
||||
|
||||
namespace {
|
||||
namespace intercept_constants {
|
||||
constexpr uint32_t kRegisterInterceptor = 1;
|
||||
constexpr uint32_t kUnregisterInterceptor = 2;
|
||||
|
||||
constexpr uint32_t kPreTransact = 1;
|
||||
constexpr uint32_t kPostTransact = 2;
|
||||
|
||||
constexpr uint32_t kActionSkip = 1;
|
||||
constexpr uint32_t kActionContinue = 2;
|
||||
constexpr uint32_t kActionOverrideReply = 3;
|
||||
constexpr uint32_t kActionOverrideData = 4;
|
||||
|
||||
constexpr uint32_t kBackdoorCode = 0xdeadbeef;
|
||||
} // namespace intercept_constants
|
||||
} // namespace
|
||||
|
||||
class BinderInterceptor : public BBinder {
|
||||
struct InterceptorRegistration {
|
||||
wp<IBinder> target_binder{};
|
||||
sp<IBinder> interceptor_binder;
|
||||
|
||||
InterceptorRegistration() = default;
|
||||
InterceptorRegistration(wp<IBinder> target, sp<IBinder> interceptor)
|
||||
: target_binder(std::move(target)), interceptor_binder(std::move(interceptor)) {}
|
||||
};
|
||||
using RwLock = std::shared_mutex;
|
||||
using WriteGuard = std::unique_lock<RwLock>;
|
||||
using ReadGuard = std::shared_lock<RwLock>;
|
||||
|
||||
mutable RwLock interceptor_registry_lock_;
|
||||
std::map<wp<IBinder>, InterceptorRegistration> interceptor_registry_{};
|
||||
|
||||
public:
|
||||
status_t onTransact(uint32_t code, const android::Parcel &data, android::Parcel *reply, uint32_t flags) override;
|
||||
|
||||
bool handleInterceptedTransaction(sp<BBinder> target_binder, uint32_t transaction_code, const Parcel &request_data,
|
||||
Parcel *reply_data, uint32_t transaction_flags, status_t &result);
|
||||
|
||||
bool shouldInterceptBinder(const wp<BBinder> &target_binder) const;
|
||||
|
||||
private:
|
||||
status_t handleRegisterInterceptor(const android::Parcel &data);
|
||||
status_t handleUnregisterInterceptor(const android::Parcel &data);
|
||||
|
||||
template <typename ParcelWriter>
|
||||
status_t writeInterceptorCallData(ParcelWriter &writer, sp<BBinder> target_binder, uint32_t transaction_code,
|
||||
uint32_t transaction_flags, const Parcel &data) const;
|
||||
|
||||
status_t validateInterceptorResponse(const Parcel &response, int32_t &action_type) const;
|
||||
};
|
||||
|
||||
static sp<BinderInterceptor> g_binder_interceptor = nullptr;
|
||||
|
||||
struct ThreadTransactionInfo {
|
||||
uint32_t transaction_code;
|
||||
wp<BBinder> target_binder;
|
||||
|
||||
ThreadTransactionInfo() = default;
|
||||
ThreadTransactionInfo(uint32_t code, wp<BBinder> target) : transaction_code(code), target_binder(std::move(target)) {}
|
||||
};
|
||||
|
||||
thread_local std::queue<ThreadTransactionInfo> g_thread_transaction_queue;
|
||||
|
||||
class BinderStub : public BBinder {
|
||||
status_t onTransact(uint32_t code, const android::Parcel &data, android::Parcel *reply, uint32_t flags) override {
|
||||
LOGD("BinderStub transaction: %u", code);
|
||||
|
||||
if (g_thread_transaction_queue.empty()) {
|
||||
LOGW("No pending transaction info for stub");
|
||||
return UNKNOWN_TRANSACTION;
|
||||
}
|
||||
|
||||
auto transaction_info = g_thread_transaction_queue.front();
|
||||
g_thread_transaction_queue.pop();
|
||||
|
||||
if (transaction_info.target_binder == nullptr && transaction_info.transaction_code == intercept_constants::kBackdoorCode &&
|
||||
reply != nullptr) {
|
||||
LOGD("Backdoor access requested - providing interceptor reference");
|
||||
reply->writeStrongBinder(g_binder_interceptor);
|
||||
return OK;
|
||||
}
|
||||
|
||||
if (auto promoted_target = transaction_info.target_binder.promote()) {
|
||||
LOGD("Processing intercepted transaction");
|
||||
status_t result;
|
||||
if (!g_binder_interceptor->handleInterceptedTransaction(promoted_target, transaction_info.transaction_code, data, reply,
|
||||
flags, result)) {
|
||||
LOGD("Forwarding to original binder");
|
||||
result = promoted_target->transact(transaction_info.transaction_code, data, reply, flags);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
LOGE("Failed to promote weak reference to target binder");
|
||||
return DEAD_OBJECT;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static sp<BinderStub> g_binder_stub = nullptr;
|
||||
|
||||
int (*original_ioctl_function)(int fd, int request, ...) = nullptr;
|
||||
|
||||
namespace {
|
||||
bool processBinderTransaction(binder_transaction_data *transaction_data) {
|
||||
if (!transaction_data || transaction_data->target.ptr == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool should_intercept = false;
|
||||
ThreadTransactionInfo transaction_info{};
|
||||
|
||||
if (transaction_data->code == intercept_constants::kBackdoorCode && transaction_data->sender_euid == 0) {
|
||||
transaction_info.transaction_code = intercept_constants::kBackdoorCode;
|
||||
transaction_info.target_binder = nullptr;
|
||||
should_intercept = true;
|
||||
LOGD("Backdoor transaction detected from root user");
|
||||
} else {
|
||||
auto *weak_ref = reinterpret_cast<RefBase::weakref_type *>(transaction_data->target.ptr);
|
||||
if (weak_ref->attemptIncStrong(nullptr)) {
|
||||
auto *target_binder = reinterpret_cast<BBinder *>(transaction_data->cookie);
|
||||
auto weak_binder = wp<BBinder>::fromExisting(target_binder);
|
||||
|
||||
if (g_binder_interceptor->shouldInterceptBinder(weak_binder)) {
|
||||
transaction_info.transaction_code = transaction_data->code;
|
||||
transaction_info.target_binder = weak_binder;
|
||||
should_intercept = true;
|
||||
LOGD("Interception required for transaction code=%u target=%p", transaction_data->code, target_binder);
|
||||
}
|
||||
target_binder->decStrong(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
if (should_intercept) {
|
||||
LOGD("Redirecting transaction through stub");
|
||||
transaction_data->target.ptr = reinterpret_cast<uintptr_t>(g_binder_stub->getWeakRefs());
|
||||
transaction_data->cookie = reinterpret_cast<uintptr_t>(g_binder_stub.get());
|
||||
transaction_data->code = intercept_constants::kBackdoorCode;
|
||||
g_thread_transaction_queue.push(std::move(transaction_info));
|
||||
}
|
||||
|
||||
return should_intercept;
|
||||
}
|
||||
|
||||
void processBinderWriteRead(const binder_write_read &write_read_data) {
|
||||
if (write_read_data.read_buffer == 0 || write_read_data.read_size == 0 || write_read_data.read_consumed <= sizeof(uint32_t)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGD("Processing binder read buffer: ptr=%p size=%zu consumed=%zu", reinterpret_cast<void *>(write_read_data.read_buffer),
|
||||
write_read_data.read_size, write_read_data.read_consumed);
|
||||
|
||||
auto buffer_ptr = write_read_data.read_buffer;
|
||||
auto remaining_bytes = write_read_data.read_consumed;
|
||||
|
||||
while (remaining_bytes > 0) {
|
||||
if (remaining_bytes < sizeof(uint32_t)) {
|
||||
LOGE("Insufficient bytes for command header: %llu", static_cast<unsigned long long>(remaining_bytes));
|
||||
break;
|
||||
}
|
||||
|
||||
auto command = *reinterpret_cast<const uint32_t *>(buffer_ptr);
|
||||
buffer_ptr += sizeof(uint32_t);
|
||||
remaining_bytes -= sizeof(uint32_t);
|
||||
|
||||
auto command_size = _IOC_SIZE(command);
|
||||
LOGD("Processing binder command: %u (size: %u)", command, command_size);
|
||||
|
||||
if (remaining_bytes < command_size) {
|
||||
LOGE("Insufficient bytes for command data: %llu < %u", static_cast<unsigned long long>(remaining_bytes), command_size);
|
||||
break;
|
||||
}
|
||||
|
||||
if (command == BR_TRANSACTION_SEC_CTX || command == BR_TRANSACTION) {
|
||||
binder_transaction_data *transaction_data = nullptr;
|
||||
|
||||
if (command == BR_TRANSACTION_SEC_CTX) {
|
||||
LOGD("Processing BR_TRANSACTION_SEC_CTX");
|
||||
auto *secctx_data = reinterpret_cast<const binder_transaction_data_secctx *>(buffer_ptr);
|
||||
transaction_data = const_cast<binder_transaction_data *>(&secctx_data->transaction_data);
|
||||
} else {
|
||||
LOGD("Processing BR_TRANSACTION");
|
||||
transaction_data = reinterpret_cast<binder_transaction_data *>(buffer_ptr);
|
||||
}
|
||||
|
||||
if (transaction_data) {
|
||||
processBinderTransaction(transaction_data);
|
||||
} else {
|
||||
LOGE("Failed to extract transaction data");
|
||||
}
|
||||
}
|
||||
|
||||
buffer_ptr += command_size;
|
||||
remaining_bytes -= command_size;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int intercepted_ioctl_function(int fd, int request, ...) {
|
||||
va_list args;
|
||||
va_start(args, request);
|
||||
auto *argument = va_arg(args, void *);
|
||||
va_end(args);
|
||||
|
||||
auto result = original_ioctl_function(fd, request, argument);
|
||||
|
||||
if (result >= 0 && request == BINDER_WRITE_READ && argument) {
|
||||
const auto &write_read_data = *static_cast<const binder_write_read *>(argument);
|
||||
processBinderWriteRead(write_read_data);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool BinderInterceptor::shouldInterceptBinder(const wp<BBinder> &target_binder) const {
|
||||
ReadGuard guard{interceptor_registry_lock_};
|
||||
return interceptor_registry_.find(target_binder) != interceptor_registry_.end();
|
||||
}
|
||||
|
||||
status_t BinderInterceptor::onTransact(uint32_t code, const android::Parcel &data, android::Parcel *reply, uint32_t flags) {
|
||||
switch (code) {
|
||||
case intercept_constants::kRegisterInterceptor:
|
||||
return handleRegisterInterceptor(data);
|
||||
case intercept_constants::kUnregisterInterceptor:
|
||||
return handleUnregisterInterceptor(data);
|
||||
default:
|
||||
return UNKNOWN_TRANSACTION;
|
||||
}
|
||||
}
|
||||
|
||||
status_t BinderInterceptor::handleRegisterInterceptor(const android::Parcel &data) {
|
||||
sp<IBinder> target_binder, interceptor_binder;
|
||||
|
||||
if (data.readStrongBinder(&target_binder) != OK) {
|
||||
LOGE("Failed to read target binder from registration data");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
|
||||
if (!target_binder->localBinder()) {
|
||||
LOGE("Target binder is not a local binder");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
|
||||
if (data.readStrongBinder(&interceptor_binder) != OK) {
|
||||
LOGE("Failed to read interceptor binder from registration data");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
|
||||
{
|
||||
WriteGuard write_guard{interceptor_registry_lock_};
|
||||
wp<IBinder> weak_target = target_binder;
|
||||
|
||||
auto iterator = interceptor_registry_.lower_bound(weak_target);
|
||||
if (iterator == interceptor_registry_.end() || iterator->first != weak_target) {
|
||||
iterator =
|
||||
interceptor_registry_.emplace_hint(iterator, weak_target, InterceptorRegistration{weak_target, interceptor_binder});
|
||||
} else {
|
||||
iterator->second.interceptor_binder = interceptor_binder;
|
||||
}
|
||||
|
||||
LOGI("Registered interceptor for binder %p", target_binder.get());
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
|
||||
status_t BinderInterceptor::handleUnregisterInterceptor(const android::Parcel &data) {
|
||||
sp<IBinder> target_binder, interceptor_binder;
|
||||
|
||||
if (data.readStrongBinder(&target_binder) != OK) {
|
||||
LOGE("Failed to read target binder from unregistration data");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
|
||||
if (!target_binder->localBinder()) {
|
||||
LOGE("Target binder is not a local binder");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
|
||||
if (data.readStrongBinder(&interceptor_binder) != OK) {
|
||||
LOGE("Failed to read interceptor binder from unregistration data");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
|
||||
{
|
||||
WriteGuard write_guard{interceptor_registry_lock_};
|
||||
wp<IBinder> weak_target = target_binder;
|
||||
|
||||
auto iterator = interceptor_registry_.find(weak_target);
|
||||
if (iterator != interceptor_registry_.end()) {
|
||||
if (iterator->second.interceptor_binder != interceptor_binder) {
|
||||
LOGE("Interceptor mismatch during unregistration");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
interceptor_registry_.erase(iterator);
|
||||
LOGI("Unregistered interceptor for binder %p", target_binder.get());
|
||||
return OK;
|
||||
}
|
||||
|
||||
LOGW("Attempted to unregister non-existent interceptor");
|
||||
return BAD_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
bool BinderInterceptor::handleInterceptedTransaction(sp<BBinder> target_binder, uint32_t transaction_code, const Parcel &request_data,
|
||||
Parcel *reply_data, uint32_t transaction_flags, status_t &result) {
|
||||
#define VALIDATE_STATUS(expr) \
|
||||
do { \
|
||||
auto __result = (expr); \
|
||||
if (__result != OK) { \
|
||||
LOGE("Operation failed: " #expr " = %d", __result); \
|
||||
return false; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
sp<IBinder> interceptor_binder;
|
||||
{
|
||||
ReadGuard read_guard{interceptor_registry_lock_};
|
||||
auto iterator = interceptor_registry_.find(target_binder);
|
||||
if (iterator == interceptor_registry_.end()) {
|
||||
LOGE("No interceptor found for target binder %p", target_binder.get());
|
||||
return false;
|
||||
}
|
||||
interceptor_binder = iterator->second.interceptor_binder;
|
||||
}
|
||||
|
||||
LOGD("Intercepting transaction: binder=%p code=%u flags=%u reply=%s", target_binder.get(), transaction_code, transaction_flags,
|
||||
reply_data ? "true" : "false");
|
||||
|
||||
Parcel pre_request_data, pre_response_data, modified_request_data;
|
||||
|
||||
VALIDATE_STATUS(writeInterceptorCallData(pre_request_data, target_binder, transaction_code, transaction_flags, request_data));
|
||||
VALIDATE_STATUS(interceptor_binder->transact(intercept_constants::kPreTransact, pre_request_data, &pre_response_data));
|
||||
|
||||
int32_t pre_action_type;
|
||||
VALIDATE_STATUS(validateInterceptorResponse(pre_response_data, pre_action_type));
|
||||
|
||||
LOGD("Pre-transaction action type: %d", pre_action_type);
|
||||
|
||||
switch (pre_action_type) {
|
||||
case intercept_constants::kActionSkip:
|
||||
return false;
|
||||
|
||||
case intercept_constants::kActionOverrideReply:
|
||||
result = pre_response_data.readInt32();
|
||||
if (reply_data) {
|
||||
size_t reply_size = pre_response_data.readUint64();
|
||||
VALIDATE_STATUS(reply_data->appendFrom(&pre_response_data, pre_response_data.dataPosition(), reply_size));
|
||||
}
|
||||
return true;
|
||||
|
||||
case intercept_constants::kActionOverrideData: {
|
||||
size_t data_size = pre_response_data.readUint64();
|
||||
VALIDATE_STATUS(modified_request_data.appendFrom(&pre_response_data, pre_response_data.dataPosition(), data_size));
|
||||
break;
|
||||
}
|
||||
|
||||
case intercept_constants::kActionContinue:
|
||||
default:
|
||||
VALIDATE_STATUS(modified_request_data.appendFrom(&request_data, 0, request_data.dataSize()));
|
||||
break;
|
||||
}
|
||||
|
||||
result = target_binder->transact(transaction_code, modified_request_data, reply_data, transaction_flags);
|
||||
|
||||
Parcel post_request_data, post_response_data;
|
||||
|
||||
VALIDATE_STATUS(post_request_data.writeStrongBinder(target_binder));
|
||||
VALIDATE_STATUS(post_request_data.writeUint32(transaction_code));
|
||||
VALIDATE_STATUS(post_request_data.writeUint32(transaction_flags));
|
||||
VALIDATE_STATUS(post_request_data.writeInt32(IPCThreadState::self()->getCallingUid()));
|
||||
VALIDATE_STATUS(post_request_data.writeInt32(IPCThreadState::self()->getCallingPid()));
|
||||
VALIDATE_STATUS(post_request_data.writeInt32(result));
|
||||
VALIDATE_STATUS(post_request_data.writeUint64(request_data.dataSize()));
|
||||
VALIDATE_STATUS(post_request_data.appendFrom(&request_data, 0, request_data.dataSize()));
|
||||
|
||||
size_t reply_size = reply_data ? reply_data->dataSize() : 0;
|
||||
VALIDATE_STATUS(post_request_data.writeUint64(reply_size));
|
||||
LOGD("Transaction sizes: request=%zu reply=%zu", request_data.dataSize(), reply_size);
|
||||
|
||||
if (reply_data && reply_size > 0) {
|
||||
VALIDATE_STATUS(post_request_data.appendFrom(reply_data, 0, reply_size));
|
||||
}
|
||||
|
||||
VALIDATE_STATUS(interceptor_binder->transact(intercept_constants::kPostTransact, post_request_data, &post_response_data));
|
||||
|
||||
int32_t post_action_type;
|
||||
VALIDATE_STATUS(validateInterceptorResponse(post_response_data, post_action_type));
|
||||
|
||||
LOGD("Post-transaction action type: %d", post_action_type);
|
||||
|
||||
if (post_action_type == intercept_constants::kActionOverrideReply) {
|
||||
result = post_response_data.readInt32();
|
||||
if (reply_data) {
|
||||
size_t new_reply_size = post_response_data.readUint64();
|
||||
reply_data->freeData();
|
||||
VALIDATE_STATUS(reply_data->appendFrom(&post_response_data, post_response_data.dataPosition(), new_reply_size));
|
||||
LOGD("Reply overridden: original_size=%zu new_size=%zu", reply_size, new_reply_size);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
#undef VALIDATE_STATUS
|
||||
}
|
||||
|
||||
template <typename ParcelWriter>
|
||||
status_t BinderInterceptor::writeInterceptorCallData(ParcelWriter &writer, sp<BBinder> target_binder, uint32_t transaction_code,
|
||||
uint32_t transaction_flags, const Parcel &data) const {
|
||||
auto status = writer.writeStrongBinder(target_binder);
|
||||
if (status != OK)
|
||||
return status;
|
||||
|
||||
status = writer.writeUint32(transaction_code);
|
||||
if (status != OK)
|
||||
return status;
|
||||
|
||||
status = writer.writeUint32(transaction_flags);
|
||||
if (status != OK)
|
||||
return status;
|
||||
|
||||
status = writer.writeInt32(IPCThreadState::self()->getCallingUid());
|
||||
if (status != OK)
|
||||
return status;
|
||||
|
||||
status = writer.writeInt32(IPCThreadState::self()->getCallingPid());
|
||||
if (status != OK)
|
||||
return status;
|
||||
|
||||
status = writer.writeUint64(data.dataSize());
|
||||
if (status != OK)
|
||||
return status;
|
||||
|
||||
return writer.appendFrom(&data, 0, data.dataSize());
|
||||
}
|
||||
|
||||
status_t BinderInterceptor::validateInterceptorResponse(const Parcel &response, int32_t &action_type) const {
|
||||
auto status = response.readInt32(&action_type);
|
||||
if (status != OK) {
|
||||
LOGE("Failed to read action type from interceptor response");
|
||||
return status;
|
||||
}
|
||||
|
||||
switch (action_type) {
|
||||
case intercept_constants::kActionSkip:
|
||||
case intercept_constants::kActionContinue:
|
||||
case intercept_constants::kActionOverrideReply:
|
||||
case intercept_constants::kActionOverrideData:
|
||||
return OK;
|
||||
default:
|
||||
LOGE("Invalid action type from interceptor: %d", action_type);
|
||||
return BAD_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr std::string_view kBinderLibraryName = "/libbinder.so";
|
||||
constexpr std::string_view kIoctlFunctionName = "ioctl";
|
||||
} // namespace
|
||||
|
||||
bool initializeBinderInterception() {
|
||||
auto memory_maps = lsplt::MapInfo::Scan();
|
||||
|
||||
dev_t binder_device_id;
|
||||
ino_t binder_inode;
|
||||
bool binder_library_found = false;
|
||||
|
||||
for (const auto &memory_map : memory_maps) {
|
||||
if (memory_map.path.ends_with(kBinderLibraryName)) {
|
||||
binder_device_id = memory_map.dev;
|
||||
binder_inode = memory_map.inode;
|
||||
binder_library_found = true;
|
||||
LOGD("Found binder library: %s (dev=0x%lx, inode=%lu)", memory_map.path.c_str(),
|
||||
static_cast<unsigned long>(binder_device_id), static_cast<unsigned long>(binder_inode));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!binder_library_found) {
|
||||
LOGE("Failed to locate libbinder.so in process memory maps");
|
||||
return false;
|
||||
}
|
||||
|
||||
g_binder_interceptor = sp<BinderInterceptor>::make();
|
||||
g_binder_stub = sp<BinderStub>::make();
|
||||
|
||||
if (!g_binder_interceptor || !g_binder_stub) {
|
||||
LOGE("Failed to create binder interceptor components");
|
||||
return false;
|
||||
}
|
||||
|
||||
lsplt::RegisterHook(binder_device_id, binder_inode, kIoctlFunctionName.data(),
|
||||
reinterpret_cast<void *>(intercepted_ioctl_function), reinterpret_cast<void **>(&original_ioctl_function));
|
||||
|
||||
if (!lsplt::CommitHook()) {
|
||||
LOGE("Failed to commit binder ioctl hook");
|
||||
g_binder_interceptor.clear();
|
||||
g_binder_stub.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGI("Binder interception initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" [[gnu::visibility("default")]] [[gnu::used]]
|
||||
bool entry(void *library_handle) {
|
||||
LOGI("TrickyStore binder interceptor loaded (handle: %p)", library_handle);
|
||||
|
||||
bool success = initializeBinderInterception();
|
||||
if (success) {
|
||||
LOGI("Binder interception entry point completed successfully");
|
||||
} else {
|
||||
LOGE("Binder interception initialization failed");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
// Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <android/dlext.h>
|
||||
#include <dlfcn.h>
|
||||
#include <elf.h>
|
||||
#include <fcntl.h>
|
||||
#include <link.h>
|
||||
#include <sys/auxv.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/ptrace.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/system_properties.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cinttypes>
|
||||
#include <climits>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "logging.hpp"
|
||||
#include "lsplt.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
namespace inject {
|
||||
|
||||
namespace constants {
|
||||
constexpr size_t kMagicLength = 16;
|
||||
constexpr size_t kMaxPathLength = 4096;
|
||||
constexpr const char *kSystemFileContext = "u:object_r:system_file:s0";
|
||||
constexpr const char *kLibcModule = "libc.so";
|
||||
constexpr const char *kLibdlModule = "libdl.so";
|
||||
constexpr const char *kEntrySymbol = "entry";
|
||||
} // namespace constants
|
||||
|
||||
class RemoteLibraryHandle {
|
||||
public:
|
||||
RemoteLibraryHandle(int pid, int fd, uintptr_t handle) : pid_(pid), fd_(fd), handle_(handle) {}
|
||||
|
||||
~RemoteLibraryHandle() {
|
||||
if (fd_ != -1) {
|
||||
struct user_regs_struct regs{};
|
||||
std::vector<lsplt::MapInfo> local_map, remote_map;
|
||||
if (get_regs(pid_, regs)) {
|
||||
local_map = lsplt::MapInfo::Scan();
|
||||
remote_map = lsplt::MapInfo::Scan(std::to_string(pid_));
|
||||
if (auto close_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "close")) {
|
||||
std::vector<uintptr_t> args = {static_cast<uintptr_t>(fd_)};
|
||||
remote_call(pid_, regs, reinterpret_cast<uintptr_t>(close_addr), 0, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoteLibraryHandle(const RemoteLibraryHandle &) = delete;
|
||||
RemoteLibraryHandle &operator=(const RemoteLibraryHandle &) = delete;
|
||||
RemoteLibraryHandle(RemoteLibraryHandle &&other) noexcept : pid_(other.pid_), fd_(other.fd_), handle_(other.handle_) {
|
||||
other.fd_ = -1;
|
||||
other.handle_ = 0;
|
||||
}
|
||||
|
||||
uintptr_t handle() const {
|
||||
return handle_;
|
||||
}
|
||||
int fd() const {
|
||||
return fd_;
|
||||
}
|
||||
|
||||
private:
|
||||
int pid_;
|
||||
int fd_;
|
||||
uintptr_t handle_;
|
||||
};
|
||||
|
||||
static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, struct user_regs_struct ®s,
|
||||
const std::vector<lsplt::MapInfo> &local_map,
|
||||
const std::vector<lsplt::MapInfo> &remote_map) {
|
||||
if (!set_sockcreate_con(constants::kSystemFileContext)) {
|
||||
LOGE("Failed to set socket creation context");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||
if (local_socket == -1) {
|
||||
PLOGE("create local socket");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (setfilecon(lib_path, constants::kSystemFileContext) == -1) {
|
||||
PLOGE("set context of lib");
|
||||
}
|
||||
|
||||
UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC);
|
||||
if (local_lib_fd == -1) {
|
||||
PLOGE("open lib: %s", lib_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
struct RemoteFunctions {
|
||||
void *socket_addr;
|
||||
void *bind_addr;
|
||||
void *recvmsg_addr;
|
||||
void *close_addr;
|
||||
void *errno_addr;
|
||||
} funcs{};
|
||||
|
||||
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");
|
||||
funcs.close_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "close");
|
||||
funcs.errno_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "__errno");
|
||||
|
||||
if (!funcs.socket_addr || !funcs.bind_addr || !funcs.recvmsg_addr || !funcs.close_addr) {
|
||||
LOGE("Failed to resolve required libc functions");
|
||||
return std::nullopt;
|
||||
}
|
||||
std::vector<uintptr_t> args;
|
||||
auto get_remote_errno = [&]() -> int {
|
||||
if (!funcs.errno_addr)
|
||||
return 0;
|
||||
args.clear();
|
||||
auto addr = remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.errno_addr), 0, args);
|
||||
int err = 0;
|
||||
if (!read_proc(pid, addr, &err, sizeof(err)))
|
||||
return 0;
|
||||
return err;
|
||||
};
|
||||
|
||||
auto close_remote = [&](int fd) {
|
||||
args = {static_cast<uintptr_t>(fd)};
|
||||
if (remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.close_addr), 0, args) != 0) {
|
||||
LOGE("Failed to close remote fd: %d", fd);
|
||||
}
|
||||
};
|
||||
|
||||
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), 0, args));
|
||||
if (remote_fd == -1) {
|
||||
errno = get_remote_errno();
|
||||
PLOGE("remote socket creation failed");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto magic = generateMagic(constants::kMagicLength);
|
||||
struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}};
|
||||
memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size());
|
||||
socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size();
|
||||
|
||||
auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr));
|
||||
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), 0, args);
|
||||
if (bind_result == static_cast<uintptr_t>(-1)) {
|
||||
errno = get_remote_errno();
|
||||
PLOGE("remote bind failed");
|
||||
close_remote(remote_fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0};
|
||||
auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf));
|
||||
|
||||
struct msghdr msg_hdr{};
|
||||
msg_hdr.msg_control = reinterpret_cast<void *>(remote_cmsgbuf);
|
||||
msg_hdr.msg_controllen = sizeof(cmsgbuf);
|
||||
auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr));
|
||||
|
||||
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)) {
|
||||
LOGE("Failed to start remote recvmsg call");
|
||||
close_remote(remote_fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
msg_hdr.msg_control = &cmsgbuf;
|
||||
msg_hdr.msg_name = &sock_addr;
|
||||
msg_hdr.msg_namelen = addr_len;
|
||||
|
||||
{
|
||||
auto *cmsg = CMSG_FIRSTHDR(&msg_hdr);
|
||||
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
|
||||
cmsg->cmsg_level = SOL_SOCKET;
|
||||
cmsg->cmsg_type = SCM_RIGHTS;
|
||||
*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = local_lib_fd;
|
||||
}
|
||||
|
||||
if (sendmsg(local_socket, &msg_hdr, 0) == -1) {
|
||||
PLOGE("Failed to send fd to remote process");
|
||||
close_remote(remote_fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto recvmsg_result = static_cast<ssize_t>(remote_post_call(pid, regs, 0));
|
||||
if (recvmsg_result == -1) {
|
||||
errno = get_remote_errno();
|
||||
PLOGE("Remote recvmsg failed");
|
||||
close_remote(remote_fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) {
|
||||
LOGE("Failed to read control message from remote process");
|
||||
close_remote(remote_fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto *cmsg = CMSG_FIRSTHDR(&msg_hdr);
|
||||
if (!cmsg || cmsg->cmsg_len != CMSG_LEN(sizeof(int)) || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) {
|
||||
LOGE("Invalid control message received from remote process");
|
||||
close_remote(remote_fd);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int transferred_fd = *reinterpret_cast<int *>(CMSG_DATA(cmsg));
|
||||
LOGD("Successfully transferred fd %d to remote process", transferred_fd);
|
||||
close_remote(remote_fd);
|
||||
return transferred_fd;
|
||||
}
|
||||
|
||||
static std::optional<uintptr_t> remote_dlopen(int pid, struct user_regs_struct ®s, const std::vector<lsplt::MapInfo> &local_map,
|
||||
const std::vector<lsplt::MapInfo> &remote_map, int lib_fd, const char *lib_path,
|
||||
uintptr_t libc_return_addr) {
|
||||
auto dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "android_dlopen_ext");
|
||||
if (!dlopen_addr) {
|
||||
LOGE("Failed to find android_dlopen_ext in %s", constants::kLibdlModule);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
android_dlextinfo dlext_info{};
|
||||
dlext_info.flags = ANDROID_DLEXT_USE_LIBRARY_FD;
|
||||
dlext_info.library_fd = lib_fd;
|
||||
|
||||
uintptr_t remote_info = push_memory(pid, regs, &dlext_info, sizeof(dlext_info));
|
||||
uintptr_t remote_path = push_string(pid, regs, lib_path);
|
||||
|
||||
std::vector<uintptr_t> args = {remote_path, RTLD_NOW, remote_info};
|
||||
uintptr_t remote_handle = remote_call(pid, regs, reinterpret_cast<uintptr_t>(dlopen_addr), libc_return_addr, args);
|
||||
|
||||
if (remote_handle == 0) {
|
||||
LOGE("Remote dlopen failed for library: %s", lib_path);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
LOGD("Successfully loaded library with handle: %p", reinterpret_cast<void *>(remote_handle));
|
||||
return remote_handle;
|
||||
}
|
||||
|
||||
static std::optional<uintptr_t> remote_find_entry(int pid, struct user_regs_struct ®s, const std::vector<lsplt::MapInfo> &local_map,
|
||||
const std::vector<lsplt::MapInfo> &remote_map, uintptr_t remote_handle,
|
||||
uintptr_t libc_return_addr) {
|
||||
auto dlsym_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlsym");
|
||||
if (!dlsym_addr) {
|
||||
LOGE("Failed to find dlsym in %s", constants::kLibdlModule);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uintptr_t remote_symbol = push_string(pid, regs, constants::kEntrySymbol);
|
||||
|
||||
std::vector<uintptr_t> args = {remote_handle, remote_symbol};
|
||||
uintptr_t entry_addr = remote_call(pid, regs, reinterpret_cast<uintptr_t>(dlsym_addr), libc_return_addr, args);
|
||||
|
||||
if (entry_addr == 0) {
|
||||
LOGE("Failed to find entry symbol '%s' in remote library", constants::kEntrySymbol);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
LOGD("Found entry point at: %p", reinterpret_cast<void *>(entry_addr));
|
||||
return entry_addr;
|
||||
}
|
||||
|
||||
static bool remote_call_entry(int pid, struct user_regs_struct ®s, uintptr_t entry_addr, uintptr_t remote_handle,
|
||||
uintptr_t libc_return_addr) {
|
||||
std::vector<uintptr_t> args = {remote_handle};
|
||||
uintptr_t result = remote_call(pid, regs, entry_addr, libc_return_addr, args);
|
||||
|
||||
LOGD("Entry point called with result: %p", reinterpret_cast<void *>(result));
|
||||
return true;
|
||||
}
|
||||
|
||||
class PtraceAttachment {
|
||||
public:
|
||||
explicit PtraceAttachment(int target_pid) : pid_(target_pid), attached_(false) {
|
||||
if (ptrace(PTRACE_ATTACH, pid_, 0, 0) == -1) {
|
||||
PLOGE("Failed to attach to process %d", pid_);
|
||||
return;
|
||||
}
|
||||
attached_ = true;
|
||||
LOGD("Successfully attached to process %d", pid_);
|
||||
}
|
||||
|
||||
~PtraceAttachment() {
|
||||
if (attached_) {
|
||||
if (ptrace(PTRACE_DETACH, pid_, 0, 0) == -1) {
|
||||
PLOGE("Failed to detach from process %d", pid_);
|
||||
} else {
|
||||
LOGD("Successfully detached from process %d", pid_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool is_attached() const {
|
||||
return attached_;
|
||||
}
|
||||
|
||||
PtraceAttachment(const PtraceAttachment &) = delete;
|
||||
PtraceAttachment &operator=(const PtraceAttachment &) = delete;
|
||||
|
||||
private:
|
||||
int pid_;
|
||||
bool attached_;
|
||||
};
|
||||
|
||||
bool inject_library(int pid, const char *lib_path, const char *entry_name) {
|
||||
LOGI("Starting injection of %s (entry: %s) into process %d", lib_path, entry_name, pid);
|
||||
|
||||
PtraceAttachment ptrace_guard(pid);
|
||||
if (!ptrace_guard.is_attached()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int status;
|
||||
if (!wait_for_trace(pid, &status, __WALL)) {
|
||||
LOGE("Failed to wait for trace");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGSTOP) {
|
||||
LOGE("Process stopped for unexpected reason: %s", parse_status(status).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
struct user_regs_struct current_regs{}, backup_regs{};
|
||||
if (!get_regs(pid, current_regs)) {
|
||||
LOGE("Failed to get process registers");
|
||||
return false;
|
||||
}
|
||||
backup_regs = current_regs;
|
||||
LOGD("Process stopped and registers backed up");
|
||||
|
||||
auto remote_map = lsplt::MapInfo::Scan(std::to_string(pid));
|
||||
auto local_map = lsplt::MapInfo::Scan();
|
||||
|
||||
auto libc_return_addr = find_module_return_addr(remote_map, constants::kLibcModule);
|
||||
if (!libc_return_addr) {
|
||||
LOGE("Failed to find return address for %s", constants::kLibcModule);
|
||||
return false;
|
||||
}
|
||||
LOGD("Found libc return address: %p", libc_return_addr);
|
||||
|
||||
auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map);
|
||||
if (!lib_fd_opt) {
|
||||
LOGE("Failed to transfer library fd to remote process");
|
||||
return false;
|
||||
}
|
||||
int lib_fd = *lib_fd_opt;
|
||||
|
||||
auto handle_opt =
|
||||
remote_dlopen(pid, current_regs, local_map, remote_map, lib_fd, lib_path, reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||
if (!handle_opt) {
|
||||
LOGE("Failed to load library in remote process");
|
||||
return false;
|
||||
}
|
||||
uintptr_t remote_handle = *handle_opt;
|
||||
|
||||
auto close_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "close");
|
||||
if (close_addr) {
|
||||
std::vector<uintptr_t> args = {static_cast<uintptr_t>(lib_fd)};
|
||||
if (remote_call(pid, current_regs, reinterpret_cast<uintptr_t>(close_addr), 0, args) != 0) {
|
||||
LOGW("Failed to close remote library fd: %d", lib_fd);
|
||||
}
|
||||
}
|
||||
|
||||
auto entry_opt =
|
||||
remote_find_entry(pid, current_regs, local_map, remote_map, remote_handle, reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||
if (!entry_opt) {
|
||||
LOGE("Failed to find entry point in remote library");
|
||||
return false;
|
||||
}
|
||||
uintptr_t entry_addr = *entry_opt;
|
||||
|
||||
if (!remote_call_entry(pid, current_regs, entry_addr, remote_handle, reinterpret_cast<uintptr_t>(libc_return_addr))) {
|
||||
LOGE("Failed to call entry point");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!set_regs(pid, backup_regs)) {
|
||||
LOGE("Failed to restore original registers");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGI("Library injection completed successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace inject
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
#ifndef NDEBUG
|
||||
logging::setPrintEnabled(true);
|
||||
#endif
|
||||
|
||||
if (argc < 4) {
|
||||
fprintf(stderr, "Usage: %s <pid> <lib_path> <entry_name>\n", argv[0]);
|
||||
fprintf(stderr, " pid - Target process ID\n");
|
||||
fprintf(stderr, " lib_path - Path to shared library to inject\n");
|
||||
fprintf(stderr, " entry_name - Entry point symbol name in library\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
char *endptr;
|
||||
long pid_long = strtol(argv[1], &endptr, 10);
|
||||
if (*endptr != '\0' || pid_long <= 0 || pid_long > INT_MAX) {
|
||||
fprintf(stderr, "Error: Invalid PID '%s'\n", argv[1]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
int pid = static_cast<int>(pid_long);
|
||||
|
||||
char resolved_path[inject::constants::kMaxPathLength];
|
||||
if (realpath(argv[2], resolved_path) == nullptr) {
|
||||
fprintf(stderr, "Error: Failed to resolve library path '%s': %s\n", argv[2], strerror(errno));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (access(resolved_path, R_OK) != 0) {
|
||||
fprintf(stderr, "Error: Library file '%s' is not readable: %s\n", resolved_path, strerror(errno));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const char *entry_name = argv[3];
|
||||
if (strlen(entry_name) == 0) {
|
||||
fprintf(stderr, "Error: Entry name cannot be empty\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
LOGI("TrickyStore injector starting...");
|
||||
bool success = inject::inject_library(pid, resolved_path, entry_name);
|
||||
|
||||
if (success) {
|
||||
LOGI("Injection completed successfully");
|
||||
return EXIT_SUCCESS;
|
||||
} else {
|
||||
LOGE("Injection failed");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
// Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "utils.hpp"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <elf.h>
|
||||
#include <fcntl.h>
|
||||
#include <link.h>
|
||||
#include <sched.h>
|
||||
#include <sys/auxv.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/ptrace.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/xattr.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "logging.hpp"
|
||||
|
||||
namespace {
|
||||
constexpr size_t kMaxPathLength = 256;
|
||||
constexpr size_t kMsgBufferSize = 64;
|
||||
constexpr size_t kStatusBufferSize = 128;
|
||||
constexpr int kInvalidFd = -1;
|
||||
constexpr uintptr_t kStackAlignment = 0xf;
|
||||
constexpr int kMaxArguments = 8;
|
||||
|
||||
constexpr char kReadPerm = 'r';
|
||||
constexpr char kWritePerm = 'w';
|
||||
constexpr char kExecPerm = 'x';
|
||||
constexpr char kNoPerm = '-';
|
||||
|
||||
constexpr std::string_view kRandomChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
} // namespace
|
||||
|
||||
bool switch_mnt_ns(int pid, int *fd) {
|
||||
if (pid == 0) {
|
||||
if (!fd || *fd == kInvalidFd) {
|
||||
LOGE("Invalid file descriptor for namespace switch");
|
||||
return false;
|
||||
}
|
||||
|
||||
UniqueFd nsfd(*fd);
|
||||
*fd = kInvalidFd;
|
||||
|
||||
std::string path = "/proc/self/fd/" + std::to_string(nsfd);
|
||||
if (setns(nsfd, CLONE_NEWNS) == -1) {
|
||||
PLOGE("Failed to switch to namespace: %s", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGD("Successfully switched back to original namespace");
|
||||
return true;
|
||||
} else {
|
||||
int old_nsfd = kInvalidFd;
|
||||
|
||||
if (fd) {
|
||||
old_nsfd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
|
||||
if (old_nsfd == kInvalidFd) {
|
||||
PLOGE("Failed to open current namespace");
|
||||
return false;
|
||||
}
|
||||
*fd = old_nsfd;
|
||||
}
|
||||
|
||||
std::string target_path = "/proc/" + std::to_string(pid) + "/ns/mnt";
|
||||
UniqueFd target_nsfd = open(target_path.c_str(), O_RDONLY | O_CLOEXEC);
|
||||
if (target_nsfd == kInvalidFd) {
|
||||
PLOGE("Failed to open target namespace: %s", target_path.c_str());
|
||||
if (fd)
|
||||
*fd = kInvalidFd;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (setns(target_nsfd, CLONE_NEWNS) == -1) {
|
||||
PLOGE("Failed to switch to target namespace: %s", target_path.c_str());
|
||||
if (fd)
|
||||
*fd = kInvalidFd;
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGD("Successfully switched to namespace for PID %d", pid);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t write_proc(int pid, uintptr_t remote_addr, const void *buf, size_t len, bool use_proc_mem) {
|
||||
if (!buf || len == 0) {
|
||||
LOGE("Invalid parameters for write_proc");
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOGV("Writing %zu bytes to PID %d at address %" PRIxPTR " (use_proc_mem=%s)", len, pid, remote_addr,
|
||||
use_proc_mem ? "true" : "false");
|
||||
|
||||
ssize_t bytes_written;
|
||||
|
||||
if (use_proc_mem) {
|
||||
char proc_path[kMaxPathLength];
|
||||
snprintf(proc_path, sizeof(proc_path), "/proc/%d/mem", pid);
|
||||
|
||||
UniqueFd proc_fd = open(proc_path, O_WRONLY | O_CLOEXEC);
|
||||
if (proc_fd == kInvalidFd) {
|
||||
PLOGE("Failed to open %s", proc_path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
bytes_written = pwrite(proc_fd, buf, len, static_cast<off_t>(remote_addr));
|
||||
if (bytes_written == -1) {
|
||||
PLOGE("pwrite failed for address %" PRIxPTR, remote_addr);
|
||||
}
|
||||
} else {
|
||||
struct iovec local_iov = {.iov_base = const_cast<void *>(buf), .iov_len = len};
|
||||
struct iovec remote_iov = {.iov_base = reinterpret_cast<void *>(remote_addr), .iov_len = len};
|
||||
|
||||
bytes_written = process_vm_writev(pid, &local_iov, 1, &remote_iov, 1, 0);
|
||||
if (bytes_written == -1) {
|
||||
PLOGE("process_vm_writev failed for address %" PRIxPTR, remote_addr);
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes_written != -1 && static_cast<size_t>(bytes_written) != len) {
|
||||
LOGW("Partial write: %zd bytes written, %zu expected", bytes_written, len);
|
||||
}
|
||||
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
ssize_t read_proc(int pid, uintptr_t remote_addr, void *buf, size_t len) {
|
||||
if (!buf || len == 0) {
|
||||
LOGE("Invalid parameters for read_proc");
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOGV("Reading %zu bytes from PID %d at address %" PRIxPTR, len, pid, remote_addr);
|
||||
|
||||
struct iovec local_iov = {.iov_base = buf, .iov_len = len};
|
||||
struct iovec remote_iov = {.iov_base = reinterpret_cast<void *>(remote_addr), .iov_len = len};
|
||||
|
||||
ssize_t bytes_read = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0);
|
||||
if (bytes_read == -1) {
|
||||
PLOGE("process_vm_readv failed for address %" PRIxPTR, remote_addr);
|
||||
} else if (static_cast<size_t>(bytes_read) != len) {
|
||||
LOGW("Partial read: %zd bytes read, %zu expected", bytes_read, len);
|
||||
}
|
||||
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
bool get_regs(int pid, struct user_regs_struct ®s) {
|
||||
LOGV("Getting registers for PID %d", pid);
|
||||
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
if (ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1) {
|
||||
PLOGE("Failed to get registers for PID %d", pid);
|
||||
return false;
|
||||
}
|
||||
#elif defined(__aarch64__) || defined(__arm__)
|
||||
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
||||
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
||||
PLOGE("Failed to get register set for PID %d", pid);
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
# error "Unsupported architecture for register access"
|
||||
#endif
|
||||
|
||||
LOGV("Successfully retrieved registers for PID %d", pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_regs(int pid, struct user_regs_struct ®s) {
|
||||
LOGV("Setting registers for PID %d", pid);
|
||||
|
||||
#if defined(__x86_64__) || defined(__i386__)
|
||||
if (ptrace(PTRACE_SETREGS, pid, 0, ®s) == -1) {
|
||||
PLOGE("Failed to set registers for PID %d", pid);
|
||||
return false;
|
||||
}
|
||||
#elif defined(__aarch64__) || defined(__arm__)
|
||||
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
||||
if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
||||
PLOGE("Failed to set register set for PID %d", pid);
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
# error "Unsupported architecture for register access"
|
||||
#endif
|
||||
|
||||
LOGV("Successfully set registers for PID %d", pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string get_addr_mem_region(const std::vector<lsplt::MapInfo> &map_info, uintptr_t addr) {
|
||||
for (const auto &map : map_info) {
|
||||
if (map.start <= addr && map.end > addr) {
|
||||
std::string perms_str;
|
||||
perms_str.reserve(4);
|
||||
|
||||
perms_str += (map.perms & PROT_READ) ? kReadPerm : kNoPerm;
|
||||
perms_str += (map.perms & PROT_WRITE) ? kWritePerm : kNoPerm;
|
||||
perms_str += (map.perms & PROT_EXEC) ? kExecPerm : kNoPerm;
|
||||
|
||||
return map.path + ' ' + perms_str;
|
||||
}
|
||||
}
|
||||
return "<unknown>";
|
||||
}
|
||||
|
||||
void *find_module_base(const std::vector<lsplt::MapInfo> &map_info, std::string_view module_suffix) {
|
||||
for (const auto &map : map_info) {
|
||||
if (map.offset == 0 && map.path.ends_with(module_suffix)) {
|
||||
LOGV("Found module base for '%.*s' at %p", static_cast<int>(module_suffix.length()), module_suffix.data(),
|
||||
reinterpret_cast<void *>(map.start));
|
||||
return reinterpret_cast<void *>(map.start);
|
||||
}
|
||||
}
|
||||
|
||||
LOGV("Module base not found for suffix '%.*s'", static_cast<int>(module_suffix.length()), module_suffix.data());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void *find_func_addr(const std::vector<lsplt::MapInfo> &local_map_info, const std::vector<lsplt::MapInfo> &remote_map_info,
|
||||
std::string_view module_name, std::string_view function_name) {
|
||||
LOGV("Resolving function '%.*s' in module '%.*s'", static_cast<int>(function_name.length()), function_name.data(),
|
||||
static_cast<int>(module_name.length()), module_name.data());
|
||||
|
||||
void *lib_handle = dlopen(module_name.data(), RTLD_NOW);
|
||||
if (!lib_handle) {
|
||||
LOGE("Failed to open library '%.*s': %s", static_cast<int>(module_name.length()), module_name.data(), dlerror());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto lib_closer = [lib_handle]() {
|
||||
dlclose(lib_handle);
|
||||
};
|
||||
|
||||
auto *symbol_addr = reinterpret_cast<uint8_t *>(dlsym(lib_handle, function_name.data()));
|
||||
if (!symbol_addr) {
|
||||
LOGE("Failed to find symbol '%.*s' in library '%.*s': %s", static_cast<int>(function_name.length()), function_name.data(),
|
||||
static_cast<int>(module_name.length()), module_name.data(), dlerror());
|
||||
lib_closer();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
LOGV("Found symbol '%.*s' at local address %p", static_cast<int>(function_name.length()), function_name.data(), symbol_addr);
|
||||
lib_closer();
|
||||
|
||||
auto *local_base = reinterpret_cast<uint8_t *>(find_module_base(local_map_info, module_name));
|
||||
if (!local_base) {
|
||||
LOGE("Failed to find local base address for module '%.*s'", static_cast<int>(module_name.length()), module_name.data());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto *remote_base = reinterpret_cast<uint8_t *>(find_module_base(remote_map_info, module_name));
|
||||
if (!remote_base) {
|
||||
LOGE("Failed to find remote base address for module '%.*s'", static_cast<int>(module_name.length()), module_name.data());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ptrdiff_t symbol_offset = symbol_addr - local_base;
|
||||
auto *remote_symbol_addr = remote_base + symbol_offset;
|
||||
|
||||
LOGV("Address translation: local_base=%p remote_base=%p offset=%td -> "
|
||||
"remote_addr=%p",
|
||||
local_base, remote_base, symbol_offset, remote_symbol_addr);
|
||||
|
||||
return remote_symbol_addr;
|
||||
}
|
||||
|
||||
void align_stack(struct user_regs_struct ®s, uintptr_t preserve_bytes) {
|
||||
regs.REG_SP = (regs.REG_SP - preserve_bytes) & ~kStackAlignment;
|
||||
LOGV("Stack aligned to %" PRIxPTR " (preserved %zu bytes)", static_cast<uintptr_t>(regs.REG_SP), preserve_bytes);
|
||||
}
|
||||
|
||||
uintptr_t push_memory(int pid, struct user_regs_struct ®s, const void *data, size_t length) {
|
||||
if (!data || length == 0) {
|
||||
LOGE("Invalid parameters for push_memory: data=%p, length=%zu", data, length);
|
||||
return 0;
|
||||
}
|
||||
|
||||
regs.REG_SP -= length;
|
||||
align_stack(regs);
|
||||
|
||||
auto stack_addr = static_cast<uintptr_t>(regs.REG_SP);
|
||||
|
||||
if (write_proc(pid, stack_addr, data, length) != static_cast<ssize_t>(length)) {
|
||||
LOGE("Failed to push %zu bytes to remote stack at %" PRIxPTR, length, stack_addr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOGV("Pushed %zu bytes to remote stack at %" PRIxPTR, length, stack_addr);
|
||||
return stack_addr;
|
||||
}
|
||||
|
||||
uintptr_t push_string(int pid, struct user_regs_struct ®s, const char *str) {
|
||||
if (!str) {
|
||||
LOGE("Null string pointer passed to push_string");
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t str_length = strlen(str) + 1;
|
||||
|
||||
regs.REG_SP -= str_length;
|
||||
align_stack(regs);
|
||||
|
||||
auto stack_addr = static_cast<uintptr_t>(regs.REG_SP);
|
||||
|
||||
if (write_proc(pid, stack_addr, str, str_length) != static_cast<ssize_t>(str_length)) {
|
||||
LOGE("Failed to push string '%s' to remote stack", str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOGV("Pushed string '%s' (%zu bytes) to remote stack at %" PRIxPTR, str, str_length, stack_addr);
|
||||
return stack_addr;
|
||||
}
|
||||
|
||||
namespace {
|
||||
#if defined(__x86_64__)
|
||||
constexpr size_t kMaxRegisterArgs = 6;
|
||||
void setup_x86_64_args(struct user_regs_struct ®s, const std::vector<uintptr_t> &args) {
|
||||
if (args.size() >= 1)
|
||||
regs.rdi = args[0];
|
||||
if (args.size() >= 2)
|
||||
regs.rsi = args[1];
|
||||
if (args.size() >= 3)
|
||||
regs.rdx = args[2];
|
||||
if (args.size() >= 4)
|
||||
regs.rcx = args[3];
|
||||
if (args.size() >= 5)
|
||||
regs.r8 = args[4];
|
||||
if (args.size() >= 6)
|
||||
regs.r9 = args[5];
|
||||
}
|
||||
#elif defined(__i386__)
|
||||
constexpr size_t kMaxRegisterArgs = 0;
|
||||
#elif defined(__aarch64__)
|
||||
constexpr size_t kMaxRegisterArgs = 8;
|
||||
void setup_aarch64_args(struct user_regs_struct ®s, const std::vector<uintptr_t> &args) {
|
||||
for (size_t i = 0; i < std::min(args.size(), kMaxRegisterArgs); i++) {
|
||||
regs.regs[i] = args[i];
|
||||
}
|
||||
}
|
||||
#elif defined(__arm__)
|
||||
constexpr size_t kMaxRegisterArgs = 4;
|
||||
void setup_arm_args(struct user_regs_struct ®s, const std::vector<uintptr_t> &args) {
|
||||
for (size_t i = 0; i < std::min(args.size(), kMaxRegisterArgs); i++) {
|
||||
regs.uregs[i] = args[i];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
bool remote_pre_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr, uintptr_t return_addr,
|
||||
std::vector<uintptr_t> &args) {
|
||||
align_stack(regs);
|
||||
|
||||
LOGV("Setting up remote function call to %" PRIxPTR " with %zu arguments", func_addr, args.size());
|
||||
for (size_t i = 0; i < args.size(); i++) {
|
||||
LOGV(" arg[%zu] = %p", i, reinterpret_cast<void *>(args[i]));
|
||||
}
|
||||
|
||||
#if defined(__x86_64__)
|
||||
setup_x86_64_args(regs, args);
|
||||
|
||||
if (args.size() > kMaxRegisterArgs) {
|
||||
size_t stack_args_size = (args.size() - kMaxRegisterArgs) * sizeof(uintptr_t);
|
||||
align_stack(regs, stack_args_size);
|
||||
|
||||
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data() + kMaxRegisterArgs, stack_args_size) !=
|
||||
static_cast<ssize_t>(stack_args_size)) {
|
||||
LOGE("Failed to push stack arguments for x86_64");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
regs.REG_SP -= sizeof(uintptr_t);
|
||||
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), &return_addr, sizeof(return_addr)) != sizeof(return_addr)) {
|
||||
LOGE("Failed to write return address");
|
||||
return false;
|
||||
}
|
||||
|
||||
regs.REG_IP = func_addr;
|
||||
|
||||
#elif defined(__i386__)
|
||||
if (args.size() > 0) {
|
||||
size_t stack_args_size = args.size() * sizeof(uintptr_t);
|
||||
align_stack(regs, stack_args_size);
|
||||
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
regs.REG_SP -= sizeof(uintptr_t);
|
||||
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), &return_addr, sizeof(return_addr)) != sizeof(return_addr)) {
|
||||
LOGE("Failed to write return address for i386");
|
||||
return false;
|
||||
}
|
||||
|
||||
regs.REG_IP = func_addr;
|
||||
|
||||
#elif defined(__aarch64__)
|
||||
setup_aarch64_args(regs, args);
|
||||
|
||||
if (args.size() > kMaxRegisterArgs) {
|
||||
size_t stack_args_size = (args.size() - kMaxRegisterArgs) * sizeof(uintptr_t);
|
||||
align_stack(regs, stack_args_size);
|
||||
|
||||
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data() + kMaxRegisterArgs, stack_args_size) !=
|
||||
static_cast<ssize_t>(stack_args_size)) {
|
||||
LOGE("Failed to push stack arguments for aarch64");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
regs.regs[30] = return_addr;
|
||||
regs.REG_IP = func_addr;
|
||||
|
||||
#elif defined(__arm__)
|
||||
setup_arm_args(regs, args);
|
||||
|
||||
if (args.size() > kMaxRegisterArgs) {
|
||||
size_t stack_args_size = (args.size() - kMaxRegisterArgs) * sizeof(uintptr_t);
|
||||
align_stack(regs, stack_args_size);
|
||||
|
||||
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data() + kMaxRegisterArgs, stack_args_size) !=
|
||||
static_cast<ssize_t>(stack_args_size)) {
|
||||
LOGE("Failed to push stack arguments for ARM");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
regs.uregs[14] = return_addr;
|
||||
regs.REG_IP = func_addr;
|
||||
|
||||
constexpr auto CPSR_T_MASK = 1lu << 5;
|
||||
if ((regs.REG_IP & 1) != 0) {
|
||||
regs.REG_IP = regs.REG_IP & ~1;
|
||||
regs.uregs[16] = regs.uregs[16] | CPSR_T_MASK;
|
||||
} else {
|
||||
regs.uregs[16] = regs.uregs[16] & ~CPSR_T_MASK;
|
||||
}
|
||||
|
||||
#else
|
||||
# error "Unsupported architecture for remote function calls"
|
||||
#endif
|
||||
|
||||
if (!set_regs(pid, regs)) {
|
||||
LOGE("Failed to set registers for remote function call");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ptrace(PTRACE_CONT, pid, 0, 0) == -1) {
|
||||
PLOGE("Failed to continue remote process execution");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGV("Remote function call initiated successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
uintptr_t remote_post_call(int pid, struct user_regs_struct ®s, uintptr_t expected_return_addr) {
|
||||
LOGV("Waiting for remote function call completion");
|
||||
|
||||
int status;
|
||||
if (!wait_for_trace(pid, &status, __WALL)) {
|
||||
LOGE("Failed to wait for remote function completion");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!get_regs(pid, regs)) {
|
||||
LOGE("Failed to get registers after remote call");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int stop_signal = WSTOPSIG(status);
|
||||
LOGV("Remote function stopped with signal: %s(%d) at address %p", sigabbrev_np(stop_signal), stop_signal,
|
||||
reinterpret_cast<void *>(regs.REG_IP));
|
||||
|
||||
if (stop_signal == SIGSEGV) {
|
||||
if (static_cast<uintptr_t>(regs.REG_IP) != expected_return_addr) {
|
||||
LOGE("Function returned to unexpected address %p (expected %p)", reinterpret_cast<void *>(regs.REG_IP),
|
||||
reinterpret_cast<void *>(expected_return_addr));
|
||||
|
||||
siginfo_t crash_info;
|
||||
if (ptrace(PTRACE_GETSIGINFO, pid, 0, &crash_info) == 0) {
|
||||
LOGE("Crash details: si_code=%d si_addr=%p", crash_info.si_code, crash_info.si_addr);
|
||||
} else {
|
||||
PLOGE("Failed to get crash signal info");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uintptr_t return_value = regs.REG_RET;
|
||||
LOGV("Remote function completed with return value: %p", reinterpret_cast<void *>(return_value));
|
||||
return return_value;
|
||||
} else {
|
||||
LOGE("Remote function stopped unexpectedly: %s at address %p", parse_status(status).c_str(),
|
||||
reinterpret_cast<void *>(regs.REG_IP));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
uintptr_t remote_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr, uintptr_t return_addr,
|
||||
std::vector<uintptr_t> &args) {
|
||||
if (!remote_pre_call(pid, regs, func_addr, return_addr, args)) {
|
||||
LOGE("Failed to prepare remote function call");
|
||||
return 0;
|
||||
}
|
||||
return remote_post_call(pid, regs, return_addr);
|
||||
}
|
||||
|
||||
int fork_dont_care() {
|
||||
int first_pid = fork();
|
||||
if (first_pid < 0) {
|
||||
PLOGE("Failed first fork for daemon process");
|
||||
return first_pid;
|
||||
}
|
||||
|
||||
if (first_pid == 0) {
|
||||
int second_pid = fork();
|
||||
if (second_pid < 0) {
|
||||
PLOGE("Failed second fork for daemon process");
|
||||
exit(EXIT_FAILURE);
|
||||
} else if (second_pid > 0) {
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
int status;
|
||||
waitpid(first_pid, &status, __WALL);
|
||||
return first_pid;
|
||||
}
|
||||
}
|
||||
|
||||
bool wait_for_trace(int pid, int *status, int flags) {
|
||||
if (!status) {
|
||||
LOGE("Null status pointer passed to wait_for_trace");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
pid_t result = waitpid(pid, status, flags);
|
||||
if (result == -1) {
|
||||
if (errno == EINTR) {
|
||||
LOGV("waitpid interrupted, retrying");
|
||||
continue;
|
||||
} else {
|
||||
PLOGE("waitpid failed for PID %d", pid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!WIFSTOPPED(*status)) {
|
||||
LOGE("Process %d not stopped for trace: %s", pid, parse_status(*status).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGV("Process %d stopped for trace with status: %s", pid, parse_status(*status).c_str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string parse_status(int status) {
|
||||
char status_buf[kStatusBufferSize];
|
||||
|
||||
if (WIFEXITED(status)) {
|
||||
snprintf(status_buf, sizeof(status_buf), "0x%x exited with code %d", status, WEXITSTATUS(status));
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
snprintf(status_buf, sizeof(status_buf), "0x%x terminated by signal %s(%d)", status, sigabbrev_np(WTERMSIG(status)),
|
||||
WTERMSIG(status));
|
||||
} else if (WIFSTOPPED(status)) {
|
||||
int stop_signal = WSTOPSIG(status);
|
||||
snprintf(status_buf, sizeof(status_buf), "0x%x stopped by signal=%s(%d), event=%s", status, sigabbrev_np(stop_signal),
|
||||
stop_signal, parse_ptrace_event(status));
|
||||
} else {
|
||||
snprintf(status_buf, sizeof(status_buf), "0x%x unknown status", status);
|
||||
}
|
||||
|
||||
return std::string(status_buf);
|
||||
}
|
||||
|
||||
std::string get_program(int pid) {
|
||||
std::string exe_path = "/proc/" + std::to_string(pid) + "/exe";
|
||||
char resolved_path[kMaxPathLength + 1];
|
||||
|
||||
ssize_t link_size = readlink(exe_path.c_str(), resolved_path, kMaxPathLength);
|
||||
if (link_size == -1) {
|
||||
PLOGE("Failed to read executable path for PID %d", pid);
|
||||
return "";
|
||||
}
|
||||
|
||||
resolved_path[link_size] = '\0';
|
||||
return std::string(resolved_path);
|
||||
}
|
||||
|
||||
void *find_module_return_addr(const std::vector<lsplt::MapInfo> &map_info, std::string_view module_suffix) {
|
||||
for (const auto &map : map_info) {
|
||||
if ((map.perms & PROT_EXEC) == 0 && map.path.ends_with(module_suffix)) {
|
||||
LOGV("Found return address region for '%.*s' at %p", static_cast<int>(module_suffix.length()), module_suffix.data(),
|
||||
reinterpret_cast<void *>(map.start));
|
||||
return reinterpret_cast<void *>(map.start);
|
||||
}
|
||||
}
|
||||
|
||||
LOGV("No return address region found for module suffix '%.*s'", static_cast<int>(module_suffix.length()), module_suffix.data());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string generateMagic(size_t length) {
|
||||
if (length == 0) {
|
||||
LOGW("Zero length requested for magic string");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::mt19937 random_generator{std::random_device{}()};
|
||||
std::uniform_int_distribution<size_t> char_distribution(0, kRandomChars.length() - 1);
|
||||
|
||||
std::string magic_string;
|
||||
magic_string.reserve(length);
|
||||
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
magic_string += kRandomChars[char_distribution(random_generator)];
|
||||
}
|
||||
|
||||
LOGV("Generated magic string of length %zu", length);
|
||||
return magic_string;
|
||||
}
|
||||
|
||||
int setfilecon(const char *file_path, const char *security_context) {
|
||||
if (!file_path || !security_context) {
|
||||
LOGE("Invalid parameters for setfilecon: path=%p, context=%p", file_path, security_context);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t context_len = strlen(security_context) + 1;
|
||||
int result = syscall(__NR_setxattr, file_path, XATTR_NAME_SELINUX, security_context, context_len, 0);
|
||||
|
||||
if (result == 0) {
|
||||
LOGV("Successfully set SELinux context '%s' for file '%s'", security_context, file_path);
|
||||
} else {
|
||||
PLOGE("Failed to set SELinux context '%s' for file '%s'", security_context, file_path);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool set_sockcreate_con(const char *security_context) {
|
||||
if (!security_context) {
|
||||
LOGE("Null security context passed to set_sockcreate_con");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t context_size = strlen(security_context) + 1;
|
||||
|
||||
UniqueFd sockcreate_fd = open("/proc/thread-self/attr/sockcreate", O_WRONLY | O_CLOEXEC);
|
||||
if (sockcreate_fd != kInvalidFd && write(sockcreate_fd, security_context, context_size) == static_cast<ssize_t>(context_size)) {
|
||||
LOGV("Successfully set socket creation context via thread-self: '%s'", security_context);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGV("Thread-self sockcreate failed, trying process-specific fallback");
|
||||
char process_path[kMaxPathLength];
|
||||
snprintf(process_path, sizeof(process_path), "/proc/%d/attr/sockcreate", gettid());
|
||||
|
||||
sockcreate_fd = open(process_path, O_WRONLY | O_CLOEXEC);
|
||||
if (sockcreate_fd == kInvalidFd || write(sockcreate_fd, security_context, context_size) != static_cast<ssize_t>(context_size)) {
|
||||
PLOGE("Failed to set socket creation context via fallback path '%s'", process_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGV("Successfully set socket creation context via fallback: '%s'", security_context);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
#include <sys/ptrace.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "lsplt.hpp"
|
||||
|
||||
#define LOG_TAG "TrickyStore"
|
||||
|
||||
#define SYSCALL_IS_ERR(e) (((unsigned long)e) > -4096UL)
|
||||
#define SYSCALL_ERR(e) (-(int)(e))
|
||||
|
||||
#if defined(__x86_64__)
|
||||
# define REG_SP rsp
|
||||
# define REG_IP rip
|
||||
# define REG_RET rax
|
||||
# define REG_NR orig_rax
|
||||
# define REG_SYS_ARG0 rdi
|
||||
#elif defined(__i386__)
|
||||
# define REG_SP esp
|
||||
# define REG_IP eip
|
||||
# define REG_RET eax
|
||||
# define REG_NR orig_eax
|
||||
# define REG_SYS_ARG0 ebx
|
||||
#elif defined(__aarch64__)
|
||||
# define REG_SP sp
|
||||
# define REG_IP pc
|
||||
# define REG_RET regs[0]
|
||||
# define REG_NR regs[8]
|
||||
# define REG_SYS_ARG0 regs[0]
|
||||
#elif defined(__arm__)
|
||||
# define REG_SP uregs[13]
|
||||
# define REG_IP uregs[15]
|
||||
# define REG_RET uregs[0]
|
||||
# define REG_NR uregs[7]
|
||||
# define REG_SYS_ARG0 uregs[0]
|
||||
# define user_regs_struct user_regs
|
||||
# define SYS_mmap SYS_mmap2
|
||||
#endif
|
||||
|
||||
ssize_t write_proc(int pid, uintptr_t remote_addr, const void *buf, size_t len, bool use_proc_mem = false);
|
||||
ssize_t read_proc(int pid, uintptr_t remote_addr, void *buf, size_t len);
|
||||
|
||||
bool get_regs(int pid, struct user_regs_struct ®s);
|
||||
bool set_regs(int pid, struct user_regs_struct ®s);
|
||||
|
||||
std::string get_addr_mem_region(const std::vector<lsplt::MapInfo> &map_info, uintptr_t addr);
|
||||
void *find_module_base(const std::vector<lsplt::MapInfo> &map_info, std::string_view module_suffix);
|
||||
void *find_func_addr(const std::vector<lsplt::MapInfo> &local_map_info, const std::vector<lsplt::MapInfo> &remote_map_info,
|
||||
std::string_view module_name, std::string_view function_name);
|
||||
void align_stack(struct user_regs_struct ®s, uintptr_t preserve_bytes = 0);
|
||||
uintptr_t push_memory(int pid, struct user_regs_struct ®s, const void *data, size_t length);
|
||||
uintptr_t push_string(int pid, struct user_regs_struct ®s, const char *str);
|
||||
|
||||
uintptr_t remote_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr, uintptr_t return_addr,
|
||||
std::vector<uintptr_t> &args);
|
||||
bool remote_pre_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr, uintptr_t return_addr, std::vector<uintptr_t> &args);
|
||||
uintptr_t remote_post_call(int pid, struct user_regs_struct ®s, uintptr_t expected_return_addr);
|
||||
|
||||
int fork_dont_care();
|
||||
bool wait_for_trace(int pid, int *status, int flags);
|
||||
std::string parse_status(int status);
|
||||
std::string get_program(int pid);
|
||||
void *find_module_return_addr(const std::vector<lsplt::MapInfo> &map_info, std::string_view module_suffix);
|
||||
bool switch_mnt_ns(int pid, int *fd);
|
||||
std::vector<std::string> get_cmdline(int pid);
|
||||
std::string parse_exec(int pid);
|
||||
bool skip_syscall(int pid);
|
||||
bool do_syscall(int pid, uintptr_t &ret, int nr, uintptr_t arg0 = 0, uintptr_t arg1 = 0, uintptr_t arg2 = 0, uintptr_t arg3 = 0,
|
||||
uintptr_t arg4 = 0, uintptr_t arg5 = 0);
|
||||
|
||||
uintptr_t remote_mmap(int pid, uintptr_t addr, size_t size, int prot, int flags, int fd, off_t offset);
|
||||
bool remote_munmap(int pid, uintptr_t addr, size_t size);
|
||||
int remote_open(int pid, uintptr_t path_addr, int flags);
|
||||
bool remote_close(int pid, int fd);
|
||||
int wait_for_child(int pid);
|
||||
int get_elf_class(std::string_view path);
|
||||
|
||||
constexpr size_t kMainMagicLength = 16;
|
||||
std::string generateMagic(size_t length);
|
||||
int setfilecon(const char *file_path, const char *security_context);
|
||||
|
||||
class UniqueFd {
|
||||
using Fd = int;
|
||||
|
||||
public:
|
||||
UniqueFd() = default;
|
||||
UniqueFd(Fd fd) : fd_(fd) {}
|
||||
~UniqueFd() {
|
||||
if (fd_ >= 0)
|
||||
close(fd_);
|
||||
}
|
||||
UniqueFd(const UniqueFd &) = delete;
|
||||
UniqueFd &operator=(const UniqueFd &) = delete;
|
||||
UniqueFd(UniqueFd &&other) {
|
||||
std::swap(fd_, other.fd_);
|
||||
}
|
||||
UniqueFd &operator=(UniqueFd &&other) {
|
||||
std::swap(fd_, other.fd_);
|
||||
return *this;
|
||||
}
|
||||
operator const Fd &() const {
|
||||
return fd_;
|
||||
}
|
||||
|
||||
private:
|
||||
Fd fd_ = -1;
|
||||
};
|
||||
|
||||
bool set_sockcreate_con(const char *security_context);
|
||||
|
||||
#define WPTEVENT(x) (x >> 16)
|
||||
#define CASE_CONST_RETURN(x) \
|
||||
case x: \
|
||||
return #x;
|
||||
inline const char *parse_ptrace_event(int status) {
|
||||
status = status >> 16;
|
||||
switch (status) {
|
||||
CASE_CONST_RETURN(PTRACE_EVENT_FORK)
|
||||
CASE_CONST_RETURN(PTRACE_EVENT_VFORK)
|
||||
CASE_CONST_RETURN(PTRACE_EVENT_CLONE)
|
||||
CASE_CONST_RETURN(PTRACE_EVENT_EXEC)
|
||||
CASE_CONST_RETURN(PTRACE_EVENT_VFORK_DONE)
|
||||
CASE_CONST_RETURN(PTRACE_EVENT_EXIT)
|
||||
CASE_CONST_RETURN(PTRACE_EVENT_SECCOMP)
|
||||
CASE_CONST_RETURN(PTRACE_EVENT_STOP)
|
||||
default:
|
||||
return "(no event)";
|
||||
}
|
||||
}
|
||||
inline const char *sigabbrev_np(int sig) {
|
||||
if (sig > 0 && sig < NSIG)
|
||||
return sys_signame[sig];
|
||||
return "(unknown)";
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <android/log.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#ifndef LOG_TAG
|
||||
# define LOG_TAG "TrickyStore"
|
||||
#endif
|
||||
|
||||
#ifndef NDEBUG
|
||||
# define LOGD(...) logging::log(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
# define LOGV(...) logging::log(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
|
||||
#else
|
||||
# define LOGD(...) (void)0
|
||||
# define LOGV(...) (void)0
|
||||
#endif
|
||||
#define LOGI(...) logging::log(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGW(...) logging::log(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGE(...) logging::log(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
#define LOGF(...) logging::log(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__)
|
||||
#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno))
|
||||
|
||||
namespace logging {
|
||||
void setPrintEnabled(bool print);
|
||||
|
||||
[[gnu::format(printf, 3, 4)]]
|
||||
void log(int prio, const char *tag, const char *fmt, ...);
|
||||
} // namespace logging
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <android/log.h>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "logging.hpp"
|
||||
|
||||
namespace logging {
|
||||
static bool use_print = false;
|
||||
static char prio_str[] = {'V', 'D', 'I', 'W', 'E', 'F'};
|
||||
|
||||
void setPrintEnabled(bool print) {
|
||||
use_print = print;
|
||||
}
|
||||
|
||||
void log(int prio, const char *tag, const char *fmt, ...) {
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
__android_log_vprint(prio, tag, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
if (use_print) {
|
||||
char buf[BUFSIZ];
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buf, sizeof(buf), fmt, ap);
|
||||
va_end(ap);
|
||||
auto prio_char = (prio > ANDROID_LOG_DEFAULT && prio <= ANDROID_LOG_FATAL) ? prio_str[prio - ANDROID_LOG_VERBOSE] : '?';
|
||||
printf("[%c][%d:%d][%s]:%s\n", prio_char, getpid(), gettid(), tag, buf);
|
||||
}
|
||||
}
|
||||
} // namespace logging
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "binder/Binder.h"
|
||||
#include "binder/BpBinder.h"
|
||||
#include "binder/IPCThreadState.h"
|
||||
#include "binder/IServiceManager.h"
|
||||
#include "binder/Status.h"
|
||||
|
||||
namespace android {
|
||||
|
||||
IBinder::IBinder() {}
|
||||
IBinder::~IBinder() {}
|
||||
sp<IInterface> IBinder::queryLocalInterface(const String16&) { return nullptr; }
|
||||
BBinder* IBinder::localBinder() { return nullptr; }
|
||||
BpBinder* IBinder::remoteBinder() { return nullptr; }
|
||||
bool IBinder::checkSubclass(const void*) const { return false; }
|
||||
void IBinder::withLock(const std::function<void()>&) {}
|
||||
|
||||
#ifdef __LP64__
|
||||
static_assert(sizeof(IBinder) == 24);
|
||||
static_assert(sizeof(BBinder) == 40);
|
||||
#else
|
||||
static_assert(sizeof(IBinder) == 12);
|
||||
static_assert(sizeof(BBinder) == 20);
|
||||
#endif
|
||||
|
||||
BBinder::BBinder() {}
|
||||
BBinder::~BBinder() {}
|
||||
|
||||
const String16& BBinder::getInterfaceDescriptor() const { __builtin_unreachable(); }
|
||||
bool BBinder::isBinderAlive() const { return false; }
|
||||
status_t BBinder::pingBinder() { return 0; }
|
||||
status_t BBinder::dump(int, const Vector<String16>&) { return 0; }
|
||||
status_t BBinder::transact(uint32_t, const Parcel&, Parcel*, uint32_t) { return 0; }
|
||||
status_t BBinder::linkToDeath(const sp<DeathRecipient>&, void*, uint32_t) { return 0; }
|
||||
status_t BBinder::unlinkToDeath(const wp<DeathRecipient>&, void*, uint32_t, wp<DeathRecipient>*) { return 0; }
|
||||
void* BBinder::attachObject(const void*, void*, void*, object_cleanup_func) { return nullptr; }
|
||||
void* BBinder::findObject(const void*) const { return nullptr; }
|
||||
void* BBinder::detachObject(const void*) { return nullptr; }
|
||||
void BBinder::withLock(const std::function<void()>&) {}
|
||||
BBinder* BBinder::localBinder() { return nullptr; }
|
||||
status_t BBinder::onTransact(uint32_t, const Parcel&, Parcel*, uint32_t) { return 0; }
|
||||
|
||||
IPCThreadState* IPCThreadState::self() { return nullptr; }
|
||||
IPCThreadState* IPCThreadState::selfOrNull() { return nullptr; }
|
||||
pid_t IPCThreadState::getCallingPid() const { return 0; }
|
||||
const char* IPCThreadState::getCallingSid() const { return nullptr; }
|
||||
uid_t IPCThreadState::getCallingUid() const { return 0; }
|
||||
|
||||
#ifdef __LP64__
|
||||
static_assert(sizeof(Parcel) == 120);
|
||||
#else
|
||||
static_assert(sizeof(Parcel) == 60);
|
||||
#endif
|
||||
|
||||
Parcel::Parcel() {}
|
||||
Parcel::~Parcel() {}
|
||||
const uint8_t* Parcel::data() const { return nullptr; }
|
||||
size_t Parcel::dataSize() const { return 0; }
|
||||
size_t Parcel::dataAvail() const { return 0; }
|
||||
size_t Parcel::dataPosition() const { return 0; }
|
||||
size_t Parcel::dataCapacity() const { return 0; }
|
||||
size_t Parcel::dataBufferSize() const { return 0; }
|
||||
status_t Parcel::setDataSize(size_t) { return 0; }
|
||||
void Parcel::setDataPosition(size_t) const {}
|
||||
status_t Parcel::setDataCapacity(size_t) { return 0; }
|
||||
status_t Parcel::setData(const uint8_t*, size_t) { return 0; }
|
||||
status_t Parcel::appendFrom(const Parcel*, size_t, size_t) { return 0; }
|
||||
binder::Status Parcel::enforceNoDataAvail() const { return {}; }
|
||||
void Parcel::setEnforceNoDataAvail(bool) {}
|
||||
void Parcel::freeData() {}
|
||||
status_t Parcel::write(const void*, size_t) { return 0; }
|
||||
void* Parcel::writeInplace(size_t) { return nullptr; }
|
||||
status_t Parcel::writeInt32(int32_t) { return 0; }
|
||||
status_t Parcel::writeUint32(uint32_t) { return 0; }
|
||||
status_t Parcel::writeInt64(int64_t) { return 0; }
|
||||
status_t Parcel::writeUint64(uint64_t) { return 0; }
|
||||
status_t Parcel::writeFloat(float) { return 0; }
|
||||
status_t Parcel::writeDouble(double) { return 0; }
|
||||
status_t Parcel::writeCString(const char*) { return 0; }
|
||||
status_t Parcel::writeString8(const char*, size_t) { return 0; }
|
||||
status_t Parcel::writeStrongBinder(const sp<IBinder>&) { return 0; }
|
||||
status_t Parcel::writeBool(bool) { return 0; }
|
||||
status_t Parcel::writeChar(char16_t) { return 0; }
|
||||
status_t Parcel::writeByte(int8_t) { return 0; }
|
||||
status_t Parcel::writeNoException() { return 0; }
|
||||
status_t Parcel::read(void*, size_t) const { return 0; }
|
||||
const void* Parcel::readInplace(size_t) const { return nullptr; }
|
||||
int32_t Parcel::readInt32() const { return 0; }
|
||||
status_t Parcel::readInt32(int32_t*) const { return 0; }
|
||||
uint32_t Parcel::readUint32() const { return 0; }
|
||||
status_t Parcel::readUint32(uint32_t*) const { return 0; }
|
||||
int64_t Parcel::readInt64() const { return 0; }
|
||||
status_t Parcel::readInt64(int64_t*) const { return 0; }
|
||||
uint64_t Parcel::readUint64() const { return 0; }
|
||||
status_t Parcel::readUint64(uint64_t*) const { return 0; }
|
||||
float Parcel::readFloat() const { return 0; }
|
||||
status_t Parcel::readFloat(float*) const { return 0; }
|
||||
double Parcel::readDouble() const { return 0; }
|
||||
status_t Parcel::readDouble(double*) const { return 0; }
|
||||
bool Parcel::readBool() const { return 0; }
|
||||
status_t Parcel::readBool(bool*) const { return 0; }
|
||||
char16_t Parcel::readChar() const { return 0; }
|
||||
status_t Parcel::readChar(char16_t*) const { return 0; }
|
||||
int8_t Parcel::readByte() const { return 0; }
|
||||
status_t Parcel::readByte(int8_t*) const { return 0; }
|
||||
sp<IBinder> Parcel::readStrongBinder() const { return nullptr; }
|
||||
status_t Parcel::readStrongBinder(sp<IBinder>*) const { return 0; }
|
||||
status_t Parcel::readNullableStrongBinder(sp<IBinder>*) const { return 0; }
|
||||
int32_t Parcel::readExceptionCode() const { return 0; }
|
||||
int Parcel::readFileDescriptor() const { return 0; }
|
||||
|
||||
IServiceManager::IServiceManager() {}
|
||||
IServiceManager::~IServiceManager() {}
|
||||
const String16& IServiceManager::getInterfaceDescriptor() const { __builtin_unreachable(); }
|
||||
sp<IServiceManager> defaultServiceManager() { return nullptr; }
|
||||
void setDefaultServiceManager(const sp<IServiceManager>&) {}
|
||||
|
||||
} // namespace android
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "utils/StrongPointer.h"
|
||||
#include "utils/RefBase.h"
|
||||
#include "utils/String16.h"
|
||||
|
||||
namespace android {
|
||||
void RefBase::incStrong(const void *id) const {
|
||||
|
||||
}
|
||||
|
||||
void RefBase::incStrongRequireStrong(const void *id) const {
|
||||
|
||||
}
|
||||
|
||||
void RefBase::decStrong(const void *id) const {
|
||||
|
||||
}
|
||||
|
||||
void RefBase::forceIncStrong(const void *id) const {
|
||||
|
||||
}
|
||||
|
||||
RefBase::weakref_type* RefBase::createWeak(const void* id) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefBase::weakref_type* RefBase::getWeakRefs() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefBase::RefBase(): mRefs(nullptr) {}
|
||||
RefBase::~RefBase() {}
|
||||
|
||||
void RefBase::onFirstRef() {}
|
||||
void RefBase::onLastStrongRef(const void* id) {}
|
||||
bool RefBase::onIncStrongAttempted(uint32_t flags, const void* id) { return false; }
|
||||
void RefBase::onLastWeakRef(const void* id) {}
|
||||
|
||||
RefBase* RefBase::weakref_type::refBase() const { return nullptr; }
|
||||
|
||||
void RefBase::weakref_type::incWeak(const void* id) {}
|
||||
void RefBase::weakref_type::incWeakRequireWeak(const void* id) {}
|
||||
void RefBase::weakref_type::decWeak(const void* id) {}
|
||||
|
||||
bool RefBase::weakref_type::attemptIncStrong(const void* id) { return false; }
|
||||
|
||||
bool RefBase::weakref_type::attemptIncWeak(const void* id) { return false; }
|
||||
|
||||
void sp_report_race() {}
|
||||
|
||||
String16::String16() {}
|
||||
|
||||
String16::String16(const String16 &o) {}
|
||||
|
||||
String16::String16(String16 &&o) noexcept {}
|
||||
|
||||
String16::String16(const char *o) {}
|
||||
|
||||
String16::~String16() {}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import android.content.pm.IPackageManager
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.ServiceManager
|
||||
import android.os.SystemProperties
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.CustomPatchLevel
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.DEROctetString
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
|
||||
fun getTransactCode(clazz: Class<*>, method: String): Int =
|
||||
clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }
|
||||
.getInt(null)
|
||||
|
||||
val bootHash: ByteArray by lazy {
|
||||
getBootHashFromProp() ?: randomBytes()
|
||||
}
|
||||
|
||||
val bootKey: ByteArray by lazy {
|
||||
randomBytes()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun getBootHashFromProp(): ByteArray? {
|
||||
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) ?: return null
|
||||
return if (digest.length == 64) digest.hexToByteArray() else null
|
||||
}
|
||||
|
||||
private fun randomBytes(): ByteArray = ByteArray(32).also {
|
||||
ThreadLocalRandom.current().nextBytes(it)
|
||||
}
|
||||
|
||||
val patchLevel: Int
|
||||
get() = getCustomPatchLevel("system", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val patchLevelLong: Int
|
||||
get() = getCustomPatchLevel("system", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
val vendorPatchLevel: Int
|
||||
get() = getCustomPatchLevel("vendor", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val vendorPatchLevelLong: Int
|
||||
get() = getCustomPatchLevel("vendor", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
val bootPatchLevel: Int
|
||||
get() = getCustomPatchLevel("boot", false)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(false)
|
||||
|
||||
val bootPatchLevelLong: Int
|
||||
get() = getCustomPatchLevel("boot", true)
|
||||
?: Build.VERSION.SECURITY_PATCH.convertPatchLevel(true)
|
||||
|
||||
private val customPatchLevel: CustomPatchLevel?
|
||||
get() = Config._customPatchLevel
|
||||
|
||||
private fun getCustomPatchLevel(component: String, isLong: Boolean): Int? {
|
||||
val config = customPatchLevel ?: return null
|
||||
val value = when (component) {
|
||||
"system" -> config.system ?: config.all
|
||||
"vendor" -> config.vendor ?: config.all
|
||||
"boot" -> config.boot ?: config.all
|
||||
else -> config.all
|
||||
} ?: return null
|
||||
|
||||
when {
|
||||
value.equals("no", ignoreCase = true) -> return null
|
||||
value.equals("prop", ignoreCase = true) -> return null
|
||||
}
|
||||
|
||||
return parsePatchLevelValue(value, component, isLong)
|
||||
}
|
||||
|
||||
private fun parsePatchLevelValue(value: String, component: String, isLong: Boolean): Int? {
|
||||
val normalized = value.replace("-", "")
|
||||
|
||||
return try {
|
||||
when (normalized.length) {
|
||||
8 -> {
|
||||
val year = normalized.substring(0, 4).toInt()
|
||||
val month = normalized.substring(4, 6).toInt()
|
||||
val day = normalized.substring(6, 8).toInt()
|
||||
if (isLong) year * 10000 + month * 100 + day
|
||||
else year * 100 + month
|
||||
}
|
||||
6 -> {
|
||||
val year = normalized.substring(0, 4).toInt()
|
||||
val month = normalized.substring(4, 6).toInt()
|
||||
if (isLong) year * 10000 + month * 100
|
||||
else year * 100 + month
|
||||
}
|
||||
else -> {
|
||||
Logger.e("Invalid patch level length for $component: $normalized")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
Logger.e("Patch level parse error for $component=$value", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val osVersion: Int
|
||||
get() = getOsVersion(Build.VERSION.SDK_INT)
|
||||
|
||||
private val osVersionMap = mapOf(
|
||||
Build.VERSION_CODES.BAKLAVA to 160000,
|
||||
Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000,
|
||||
Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 140000,
|
||||
Build.VERSION_CODES.TIRAMISU to 130000,
|
||||
Build.VERSION_CODES.S_V2 to 120100,
|
||||
Build.VERSION_CODES.S to 120000,
|
||||
Build.VERSION_CODES.R to 110000,
|
||||
Build.VERSION_CODES.Q to 100000
|
||||
)
|
||||
|
||||
private fun getOsVersion(sdkVersion: Int): Int = osVersionMap[sdkVersion] ?: 160000
|
||||
|
||||
fun String.convertPatchLevel(isLong: Boolean): Int = runCatching {
|
||||
val parts = split("-")
|
||||
when {
|
||||
isLong && parts.size >= 3 -> parts[0].toInt() * 10000 + parts[1].toInt() * 100 + parts[2].toInt()
|
||||
parts.size >= 2 -> parts[0].toInt() * 100 + parts[1].toInt()
|
||||
else -> throw IllegalArgumentException("Invalid patch level format: $this")
|
||||
}
|
||||
}.onFailure {
|
||||
Logger.e("Invalid patch level format: $this", it)
|
||||
}.getOrDefault(202404)
|
||||
|
||||
fun IPackageManager.getPackageInfoCompat(name: String, flags: Long, userId: Int) =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getPackageInfo(name, flags, userId)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getPackageInfo(name, flags.toInt(), userId)
|
||||
}
|
||||
|
||||
val apexInfos: List<Pair<String, Long>> by lazy {
|
||||
runCatching {
|
||||
val packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"))
|
||||
val packages = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getInstalledPackages(PackageManager.MATCH_APEX, 0)
|
||||
}
|
||||
|
||||
packages.list
|
||||
.map { it.packageName to it.longVersionCode }
|
||||
.sortedBy { it.first }
|
||||
}.getOrElse {
|
||||
Logger.e("Failed to get APEX package information")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
val moduleHash: ByteArray by lazy {
|
||||
runCatching {
|
||||
val encodables = apexInfos.flatMap { (packageName, versionCode) ->
|
||||
listOf(
|
||||
DEROctetString(packageName.toByteArray()),
|
||||
ASN1Integer(versionCode)
|
||||
)
|
||||
}
|
||||
|
||||
val sequence = DERSequence(encodables.toTypedArray())
|
||||
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
|
||||
}.getOrElse {
|
||||
Logger.e("Failed to compute module hash", it)
|
||||
ByteArray(32)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.trimLine(): String = trim().split("\n").joinToString("\n") { it.trim() }
|
||||
@@ -0,0 +1,868 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.EcCurve
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.util.Pair
|
||||
import io.github.beakthoven.TrickyStoreOSS.*
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.SecurityLevelInterceptor
|
||||
import org.bouncycastle.asn1.*
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.asn1.x509.KeyUsage
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.openssl.PEMKeyPair
|
||||
import org.bouncycastle.openssl.PEMParser
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import org.bouncycastle.util.io.pem.PemReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.StringReader
|
||||
import java.math.BigInteger
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.*
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.CertificateParsingException
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import java.security.spec.RSAKeyGenParameterSpec
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
object CertificateHacker {
|
||||
|
||||
private val ATTESTATION_OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17")
|
||||
|
||||
private val certificateFactory: CertificateFactory by lazy {
|
||||
try {
|
||||
CertificateFactory.getInstance("X.509")
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to initialize certificate factory", t)
|
||||
throw RuntimeException("Cannot initialize certificate factory", t)
|
||||
}
|
||||
}
|
||||
|
||||
data class KeyBox(
|
||||
val pemKeyPair: PEMKeyPair,
|
||||
val keyPair: KeyPair,
|
||||
val certificates: List<Certificate>
|
||||
)
|
||||
|
||||
data class KeyIdentifier(
|
||||
val alias: String,
|
||||
val uid: Int
|
||||
)
|
||||
|
||||
sealed class ParseResult<out T> {
|
||||
data class Success<T>(val data: T) : ParseResult<T>()
|
||||
data class Error(val message: String, val cause: Throwable? = null) : ParseResult<Nothing>()
|
||||
}
|
||||
|
||||
sealed class HackResult<out T> {
|
||||
data class Success<T>(val data: T) : HackResult<T>()
|
||||
data class Error(val message: String, val cause: Throwable? = null) : HackResult<Nothing>()
|
||||
}
|
||||
|
||||
data class KeyGenParameters(
|
||||
var keySize: Int = 0,
|
||||
var algorithm: Int = 0,
|
||||
var certificateSerial: BigInteger? = null,
|
||||
var certificateNotBefore: Date? = null,
|
||||
var certificateNotAfter: Date? = null,
|
||||
var certificateSubject: X500Name? = null,
|
||||
var rsaPublicExponent: BigInteger? = null,
|
||||
var ecCurve: Int = 0,
|
||||
var ecCurveName: String? = null,
|
||||
var purpose: MutableList<Int> = mutableListOf(),
|
||||
var digest: MutableList<Int> = mutableListOf(),
|
||||
var attestationChallenge: ByteArray? = null,
|
||||
var brand: ByteArray? = null,
|
||||
var device: ByteArray? = null,
|
||||
var product: ByteArray? = null,
|
||||
var manufacturer: ByteArray? = null,
|
||||
var model: ByteArray? = null,
|
||||
var imei1: ByteArray? = null,
|
||||
var imei2: ByteArray? = null,
|
||||
var meid: ByteArray? = null,
|
||||
var serialno: ByteArray? = null
|
||||
) {
|
||||
|
||||
constructor(params: Array<KeyParameter>) : this() {
|
||||
parseKeyParameters(params)
|
||||
}
|
||||
|
||||
private fun parseKeyParameters(params: Array<KeyParameter>) {
|
||||
params.forEach { param ->
|
||||
Logger.d("Processing key parameter: ${param.tag}")
|
||||
val value = param.value
|
||||
|
||||
when (param.tag) {
|
||||
Tag.KEY_SIZE -> keySize = value.integer
|
||||
Tag.ALGORITHM -> algorithm = value.algorithm
|
||||
Tag.CERTIFICATE_SERIAL -> certificateSerial = BigInteger(value.blob)
|
||||
Tag.CERTIFICATE_NOT_BEFORE -> certificateNotBefore = Date(value.dateTime)
|
||||
Tag.CERTIFICATE_NOT_AFTER -> certificateNotAfter = Date(value.dateTime)
|
||||
Tag.CERTIFICATE_SUBJECT -> certificateSubject = X500Name(X500Principal(value.blob).name)
|
||||
Tag.RSA_PUBLIC_EXPONENT -> rsaPublicExponent = BigInteger(value.blob)
|
||||
Tag.EC_CURVE -> {
|
||||
ecCurve = value.ecCurve
|
||||
ecCurveName = getEcCurveName(ecCurve)
|
||||
}
|
||||
Tag.PURPOSE -> purpose.add(value.keyPurpose)
|
||||
Tag.DIGEST -> digest.add(value.digest)
|
||||
Tag.ATTESTATION_CHALLENGE -> attestationChallenge = value.blob
|
||||
Tag.ATTESTATION_ID_BRAND -> brand = value.blob
|
||||
Tag.ATTESTATION_ID_DEVICE -> device = value.blob
|
||||
Tag.ATTESTATION_ID_PRODUCT -> product = value.blob
|
||||
Tag.ATTESTATION_ID_MANUFACTURER -> manufacturer = value.blob
|
||||
Tag.ATTESTATION_ID_MODEL -> model = value.blob
|
||||
Tag.ATTESTATION_ID_IMEI -> imei1 = value.blob
|
||||
Tag.ATTESTATION_ID_SECOND_IMEI -> imei2 = value.blob
|
||||
Tag.ATTESTATION_ID_MEID -> meid = value.blob
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setEcCurveName(curveSize: Int) {
|
||||
ecCurveName = when (curveSize) {
|
||||
224 -> "secp224r1"
|
||||
256 -> "secp256r1"
|
||||
384 -> "secp384r1"
|
||||
521 -> "secp521r1"
|
||||
else -> "secp256r1"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun getEcCurveName(curve: Int): String = when (curve) {
|
||||
EcCurve.CURVE_25519 -> "CURVE_25519"
|
||||
EcCurve.P_224 -> "secp224r1"
|
||||
EcCurve.P_256 -> "secp256r1"
|
||||
EcCurve.P_384 -> "secp384r1"
|
||||
EcCurve.P_521 -> "secp521r1"
|
||||
else -> throw IllegalArgumentException("Unknown EC curve: $curve")
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as KeyGenParameters
|
||||
|
||||
return keySize == other.keySize &&
|
||||
algorithm == other.algorithm &&
|
||||
certificateSerial == other.certificateSerial &&
|
||||
certificateNotBefore == other.certificateNotBefore &&
|
||||
certificateNotAfter == other.certificateNotAfter &&
|
||||
certificateSubject == other.certificateSubject &&
|
||||
rsaPublicExponent == other.rsaPublicExponent &&
|
||||
ecCurve == other.ecCurve &&
|
||||
ecCurveName == other.ecCurveName &&
|
||||
purpose == other.purpose &&
|
||||
digest == other.digest &&
|
||||
attestationChallenge.contentEquals(other.attestationChallenge) &&
|
||||
brand.contentEquals(other.brand) &&
|
||||
device.contentEquals(other.device) &&
|
||||
product.contentEquals(other.product) &&
|
||||
manufacturer.contentEquals(other.manufacturer) &&
|
||||
model.contentEquals(other.model) &&
|
||||
imei1.contentEquals(other.imei1) &&
|
||||
imei2.contentEquals(other.imei2) &&
|
||||
meid.contentEquals(other.meid) &&
|
||||
serialno.contentEquals(other.serialno)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = keySize
|
||||
result = 31 * result + algorithm
|
||||
result = 31 * result + (certificateSerial?.hashCode() ?: 0)
|
||||
result = 31 * result + (certificateNotBefore?.hashCode() ?: 0)
|
||||
result = 31 * result + (certificateNotAfter?.hashCode() ?: 0)
|
||||
result = 31 * result + (certificateSubject?.hashCode() ?: 0)
|
||||
result = 31 * result + (rsaPublicExponent?.hashCode() ?: 0)
|
||||
result = 31 * result + ecCurve
|
||||
result = 31 * result + (ecCurveName?.hashCode() ?: 0)
|
||||
result = 31 * result + purpose.hashCode()
|
||||
result = 31 * result + digest.hashCode()
|
||||
result = 31 * result + (attestationChallenge?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (brand?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (device?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (product?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (manufacturer?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (model?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (imei1?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (imei2?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (meid?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (serialno?.contentHashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private val keyboxes = ConcurrentHashMap<String, KeyBox>()
|
||||
private val leafAlgorithm = ConcurrentHashMap<KeyIdentifier, String>()
|
||||
|
||||
private const val ATTESTATION_APPLICATION_ID_PACKAGE_INFOS_INDEX = 0
|
||||
private const val ATTESTATION_APPLICATION_ID_SIGNATURE_DIGESTS_INDEX = 1
|
||||
private const val ATTESTATION_PACKAGE_INFO_PACKAGE_NAME_INDEX = 0
|
||||
private const val ATTESTATION_PACKAGE_INFO_VERSION_INDEX = 1
|
||||
|
||||
fun canHack(): Boolean = keyboxes.isNotEmpty()
|
||||
|
||||
private data class Digest(val digest: ByteArray) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as Digest
|
||||
return digest.contentEquals(other.digest)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = digest.contentHashCode()
|
||||
}
|
||||
|
||||
private fun parseKeyPair(keyContent: String): ParseResult<PEMKeyPair> {
|
||||
return try {
|
||||
PEMParser(StringReader(keyContent.trimLine())).use { parser ->
|
||||
val pemObject = parser.readObject()
|
||||
if (pemObject is PEMKeyPair) {
|
||||
ParseResult.Success(pemObject)
|
||||
} else {
|
||||
ParseResult.Error("Invalid PEM key pair format")
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
ParseResult.Error("Failed to parse PEM key pair", t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseCertificate(certContent: String): ParseResult<Certificate> {
|
||||
return try {
|
||||
PemReader(StringReader(certContent.trimLine())).use { reader ->
|
||||
val pemObject = reader.readPemObject()
|
||||
val certificate = certificateFactory.generateCertificate(
|
||||
ByteArrayInputStream(pemObject.content)
|
||||
)
|
||||
ParseResult.Success(certificate)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
ParseResult.Error("Failed to parse certificate", t)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(CertificateParsingException::class)
|
||||
private fun getByteArrayFromAsn1(asn1Encodable: ASN1Encodable): ByteArray {
|
||||
return when (asn1Encodable) {
|
||||
is DEROctetString -> asn1Encodable.octets
|
||||
else -> throw CertificateParsingException("Expected DEROctetString, got ${asn1Encodable::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
fun readFromXml(xmlData: String?) {
|
||||
keyboxes.clear()
|
||||
leafAlgorithm.clear()
|
||||
|
||||
if (xmlData == null) {
|
||||
Logger.i("Clearing all keyboxes")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val xmlParser = XmlParser(xmlData)
|
||||
|
||||
val numberOfKeyboxesResult = xmlParser.obtainPath("AndroidAttestation.NumberOfKeyboxes")
|
||||
val numberOfKeyboxes = when (numberOfKeyboxesResult) {
|
||||
is XmlParser.ParseResult.Success -> numberOfKeyboxesResult.attributes["text"]?.toIntOrNull()
|
||||
?: throw IllegalArgumentException("Invalid number of keyboxes")
|
||||
is XmlParser.ParseResult.Error -> throw Exception(numberOfKeyboxesResult.message, numberOfKeyboxesResult.cause)
|
||||
}
|
||||
|
||||
repeat(numberOfKeyboxes) { i ->
|
||||
processKeybox(xmlParser, i)
|
||||
}
|
||||
|
||||
Logger.i("Successfully updated $numberOfKeyboxes keyboxes")
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Error loading XML file (keyboxes cleared)", t)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processKeybox(xmlParser: XmlParser, index: Int) {
|
||||
try {
|
||||
val algorithmResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index]")
|
||||
val keyboxAlgorithm = when (algorithmResult) {
|
||||
is XmlParser.ParseResult.Success -> algorithmResult.attributes["algorithm"]
|
||||
?: throw IllegalArgumentException("Missing algorithm attribute")
|
||||
is XmlParser.ParseResult.Error -> throw Exception(algorithmResult.message, algorithmResult.cause)
|
||||
}
|
||||
|
||||
val privateKeyResult = xmlParser.obtainPath("AndroidAttestation.Keybox.Key[$index].PrivateKey")
|
||||
val privateKeyContent = when (privateKeyResult) {
|
||||
is XmlParser.ParseResult.Success -> privateKeyResult.attributes["text"]
|
||||
?: throw IllegalArgumentException("Missing private key text")
|
||||
is XmlParser.ParseResult.Error -> throw Exception(privateKeyResult.message, privateKeyResult.cause)
|
||||
}
|
||||
|
||||
val numberOfCertificatesResult = xmlParser.obtainPath(
|
||||
"AndroidAttestation.Keybox.Key[$index].CertificateChain.NumberOfCertificates"
|
||||
)
|
||||
val numberOfCertificates = when (numberOfCertificatesResult) {
|
||||
is XmlParser.ParseResult.Success -> numberOfCertificatesResult.attributes["text"]?.toIntOrNull()
|
||||
?: throw IllegalArgumentException("Invalid number of certificates")
|
||||
is XmlParser.ParseResult.Error -> throw Exception(numberOfCertificatesResult.message, numberOfCertificatesResult.cause)
|
||||
}
|
||||
|
||||
val certificateChain = mutableListOf<Certificate>()
|
||||
repeat(numberOfCertificates) { j ->
|
||||
val certResult = xmlParser.obtainPath(
|
||||
"AndroidAttestation.Keybox.Key[$index].CertificateChain.Certificate[$j]"
|
||||
)
|
||||
val certContent = when (certResult) {
|
||||
is XmlParser.ParseResult.Success -> certResult.attributes["text"]
|
||||
?: throw IllegalArgumentException("Missing certificate text")
|
||||
is XmlParser.ParseResult.Error -> throw Exception(certResult.message, certResult.cause)
|
||||
}
|
||||
|
||||
when (val certParseResult = parseCertificate(certContent)) {
|
||||
is ParseResult.Success -> certificateChain.add(certParseResult.data)
|
||||
is ParseResult.Error -> throw Exception(certParseResult.message, certParseResult.cause)
|
||||
}
|
||||
}
|
||||
|
||||
val pemKeyPair = when (val keyParseResult = parseKeyPair(privateKeyContent)) {
|
||||
is ParseResult.Success -> keyParseResult.data
|
||||
is ParseResult.Error -> throw Exception(keyParseResult.message, keyParseResult.cause)
|
||||
}
|
||||
|
||||
val keyPair = JcaPEMKeyConverter().getKeyPair(pemKeyPair)
|
||||
|
||||
val algorithmName = when (keyboxAlgorithm.lowercase()) {
|
||||
"ecdsa" -> KeyProperties.KEY_ALGORITHM_EC
|
||||
"rsa" -> KeyProperties.KEY_ALGORITHM_RSA
|
||||
else -> keyboxAlgorithm
|
||||
}
|
||||
|
||||
keyboxes[algorithmName] = KeyBox(pemKeyPair, keyPair, certificateChain)
|
||||
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Error processing keybox $index", t)
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
fun hackCertificateChain(certificateChain: Array<Certificate>?): Array<Certificate> {
|
||||
if (certificateChain == null) {
|
||||
throw UnsupportedOperationException("Certificate chain is null!")
|
||||
}
|
||||
|
||||
return try {
|
||||
val leaf = certificateFactory.generateCertificate(
|
||||
ByteArrayInputStream(certificateChain[0].encoded)
|
||||
) as X509Certificate
|
||||
|
||||
val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id)
|
||||
?: return certificateChain // No attestation extension, return original
|
||||
|
||||
hackCertificateWithAttestation(leaf, certificateChain)
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to hack certificate chain", t)
|
||||
certificateChain
|
||||
}
|
||||
}
|
||||
|
||||
fun hackCertificateChainCA(caList: ByteArray?, alias: String, uid: Int): ByteArray {
|
||||
if (caList == null) {
|
||||
throw UnsupportedOperationException("CA list is null!")
|
||||
}
|
||||
|
||||
return try {
|
||||
val key = KeyIdentifier(alias, uid)
|
||||
val algorithm = leafAlgorithm.remove(key)
|
||||
?: throw UnsupportedOperationException("No algorithm found for key $key")
|
||||
|
||||
val keybox = keyboxes[algorithm]
|
||||
?: throw UnsupportedOperationException("Unsupported algorithm: $algorithm")
|
||||
|
||||
CertificateUtils.run { keybox.certificates.toByteArray() } ?: caList
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to hack CA certificate chain", t)
|
||||
caList
|
||||
}
|
||||
}
|
||||
|
||||
fun hackCertificateChainUSR(certificate: ByteArray?, alias: String, uid: Int): ByteArray {
|
||||
if (certificate == null) {
|
||||
throw UnsupportedOperationException("Leaf certificate is null!")
|
||||
}
|
||||
|
||||
return try {
|
||||
val leaf = certificateFactory.generateCertificate(
|
||||
ByteArrayInputStream(certificate)
|
||||
) as X509Certificate
|
||||
|
||||
val extensionBytes = leaf.getExtensionValue(ATTESTATION_OID.id)
|
||||
?: return certificate // No attestation extension, return original
|
||||
|
||||
val keyIdentifier = KeyIdentifier(alias, uid)
|
||||
leafAlgorithm[keyIdentifier] = leaf.publicKey.algorithm
|
||||
|
||||
hackSingleCertificate(leaf)?.encoded ?: certificate
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to hack user certificate", t)
|
||||
certificate
|
||||
}
|
||||
}
|
||||
|
||||
fun generateKeyPair(params: KeyGenParameters): KeyPair? {
|
||||
return try {
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> {
|
||||
Logger.d("Generating EC keypair of size ${params.keySize}")
|
||||
buildECKeyPair(params)
|
||||
}
|
||||
Algorithm.RSA -> {
|
||||
Logger.d("Generating RSA keypair of size ${params.keySize}")
|
||||
buildRSAKeyPair(params)
|
||||
}
|
||||
else -> {
|
||||
Logger.e("Unsupported algorithm: ${params.algorithm}")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to generate key pair", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun generateChain(uid: Int, params: KeyGenParameters, keyPair: KeyPair): List<ByteArray>? {
|
||||
return try {
|
||||
val keybox = getKeyboxForAlgorithm(params.algorithm)
|
||||
?: return null
|
||||
|
||||
val issuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
|
||||
val leaf = buildCertificate(keyPair, keybox, params, issuer, uid)
|
||||
|
||||
val chain = mutableListOf<Certificate>().apply {
|
||||
add(leaf)
|
||||
addAll(keybox.certificates)
|
||||
}
|
||||
|
||||
CertificateUtils.run { chain.toByteArrayList() }
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to generate certificate chain", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun generateKeyPair(
|
||||
uid: Int,
|
||||
descriptor: KeyDescriptor,
|
||||
attestKeyDescriptor: KeyDescriptor?,
|
||||
params: KeyGenParameters
|
||||
): Pair<KeyPair, List<Certificate>>? {
|
||||
Logger.i("Requested KeyPair with alias: ${descriptor.alias}")
|
||||
|
||||
val isAttestPurpose = attestKeyDescriptor != null
|
||||
if (isAttestPurpose) {
|
||||
Logger.i("Requested KeyPair with attestKey: ${attestKeyDescriptor?.alias}")
|
||||
}
|
||||
|
||||
return try {
|
||||
val keyPair = generateKeyPair(params) ?: return null
|
||||
val keybox = getKeyboxForAlgorithm(params.algorithm) ?: return null
|
||||
|
||||
val (rootKeyPair, issuer) = if (isAttestPurpose) {
|
||||
val attestInfo = getAttestationKeyInfo(uid, attestKeyDescriptor!!)
|
||||
if (attestInfo != null) {
|
||||
attestInfo.first to attestInfo.second
|
||||
} else {
|
||||
keybox.keyPair to X509CertificateHolder(keybox.certificates[0].encoded).subject
|
||||
}
|
||||
} else {
|
||||
keybox.keyPair to X509CertificateHolder(keybox.certificates[0].encoded).subject
|
||||
}
|
||||
|
||||
val leaf = buildCertificate(keyPair, keybox, params, issuer, uid, rootKeyPair)
|
||||
val chain = if (isAttestPurpose) mutableListOf() else mutableListOf<Certificate>().apply { addAll(keybox.certificates) }
|
||||
chain.add(0, leaf)
|
||||
|
||||
Logger.d("Successfully generated certificate for alias: ${descriptor.alias}")
|
||||
Pair(keyPair, chain)
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to generate key pair with certificates", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildECKeyPair(params: KeyGenParameters): KeyPair {
|
||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
val spec = ECGenParameterSpec(params.ecCurveName)
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME)
|
||||
keyPairGenerator.initialize(spec)
|
||||
return keyPairGenerator.generateKeyPair()
|
||||
}
|
||||
|
||||
private fun buildRSAKeyPair(params: KeyGenParameters): KeyPair {
|
||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
val spec = RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME)
|
||||
keyPairGenerator.initialize(spec)
|
||||
return keyPairGenerator.generateKeyPair()
|
||||
}
|
||||
|
||||
private fun getKeyboxForAlgorithm(algorithm: Int): KeyBox? {
|
||||
val algorithmName = when (algorithm) {
|
||||
Algorithm.EC -> KeyProperties.KEY_ALGORITHM_EC
|
||||
Algorithm.RSA -> KeyProperties.KEY_ALGORITHM_RSA
|
||||
else -> {
|
||||
Logger.e("Unsupported algorithm: $algorithm")
|
||||
return null
|
||||
}
|
||||
}
|
||||
return keyboxes[algorithmName]
|
||||
}
|
||||
|
||||
private fun getAttestationKeyInfo(uid: Int, attestKeyDescriptor: KeyDescriptor): Pair<KeyPair, X500Name>? {
|
||||
Logger.d("Looking for attestation key: uid=$uid alias=${attestKeyDescriptor.alias}")
|
||||
|
||||
val keyInfo = SecurityLevelInterceptor.getKeyPairs(uid, attestKeyDescriptor.alias)
|
||||
return if (keyInfo != null) {
|
||||
val issuer = X509CertificateHolder(keyInfo.second[0].encoded).subject
|
||||
Pair(keyInfo.first, issuer)
|
||||
} else {
|
||||
Logger.e("Attestation key info not found, falling back to default keybox")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun hackCertificateWithAttestation(leaf: X509Certificate, originalChain: Array<Certificate>): Array<Certificate> {
|
||||
val leafHolder = X509CertificateHolder(leaf.encoded)
|
||||
val extension = leafHolder.getExtension(ATTESTATION_OID)
|
||||
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||
val encodables = sequence.toArray()
|
||||
val teeEnforced = encodables[7] as ASN1Sequence
|
||||
|
||||
val vector = ASN1EncodableVector()
|
||||
var rootOfTrust: ASN1Encodable? = null
|
||||
|
||||
teeEnforced.forEach { element ->
|
||||
val taggedObject = element as ASN1TaggedObject
|
||||
if (taggedObject.tagNo == 704) {
|
||||
rootOfTrust = taggedObject.baseObject.toASN1Primitive()
|
||||
} else {
|
||||
vector.add(taggedObject)
|
||||
}
|
||||
}
|
||||
|
||||
val keybox = keyboxes[leaf.publicKey.algorithm]
|
||||
?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}")
|
||||
|
||||
val certificates = LinkedList(keybox.certificates)
|
||||
val builder = X509v3CertificateBuilder(
|
||||
X509CertificateHolder(certificates[0].encoded).subject,
|
||||
leafHolder.serialNumber,
|
||||
leafHolder.notBefore,
|
||||
leafHolder.notAfter,
|
||||
leafHolder.subject,
|
||||
leafHolder.subjectPublicKeyInfo
|
||||
)
|
||||
|
||||
val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private)
|
||||
|
||||
val hackedExtension = createHackedAttestationExtension(rootOfTrust, vector, encodables)
|
||||
builder.addExtension(hackedExtension)
|
||||
|
||||
leafHolder.extensions.extensionOIDs.forEach { oid ->
|
||||
if (oid.id != ATTESTATION_OID.id) {
|
||||
builder.addExtension(leafHolder.getExtension(oid))
|
||||
}
|
||||
}
|
||||
|
||||
certificates.addFirst(JcaX509CertificateConverter().getCertificate(builder.build(signer)))
|
||||
return certificates.toTypedArray()
|
||||
}
|
||||
|
||||
private fun hackSingleCertificate(leaf: X509Certificate): Certificate? {
|
||||
return try {
|
||||
val leafHolder = X509CertificateHolder(leaf.encoded)
|
||||
val extension = leafHolder.getExtension(ATTESTATION_OID)
|
||||
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||
val encodables = sequence.toArray()
|
||||
val teeEnforced = encodables[7] as ASN1Sequence
|
||||
|
||||
val vector = ASN1EncodableVector()
|
||||
var rootOfTrust: ASN1Encodable? = null
|
||||
|
||||
teeEnforced.forEach { element ->
|
||||
val taggedObject = element as ASN1TaggedObject
|
||||
if (taggedObject.tagNo == 704) {
|
||||
rootOfTrust = taggedObject.baseObject.toASN1Primitive()
|
||||
} else {
|
||||
vector.add(taggedObject)
|
||||
}
|
||||
}
|
||||
|
||||
val keybox = keyboxes[leaf.publicKey.algorithm]
|
||||
?: throw UnsupportedOperationException("Unsupported algorithm: ${leaf.publicKey.algorithm}")
|
||||
|
||||
val builder = X509v3CertificateBuilder(
|
||||
X509CertificateHolder(keybox.certificates[0].encoded).subject,
|
||||
leafHolder.serialNumber,
|
||||
leafHolder.notBefore,
|
||||
leafHolder.notAfter,
|
||||
leafHolder.subject,
|
||||
leafHolder.subjectPublicKeyInfo
|
||||
)
|
||||
|
||||
val signer = JcaContentSignerBuilder(leaf.sigAlgName).build(keybox.keyPair.private)
|
||||
|
||||
val hackedExtension = createHackedAttestationExtension(rootOfTrust, vector, encodables)
|
||||
builder.addExtension(hackedExtension)
|
||||
|
||||
leafHolder.extensions.extensionOIDs.forEach { oid ->
|
||||
if (oid.id != ATTESTATION_OID.id) {
|
||||
builder.addExtension(leafHolder.getExtension(oid))
|
||||
}
|
||||
}
|
||||
|
||||
JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to hack single certificate", t)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHackedAttestationExtension(
|
||||
originalRootOfTrust: ASN1Encodable?,
|
||||
vector: ASN1EncodableVector,
|
||||
originalEncodables: Array<ASN1Encodable>
|
||||
): Extension {
|
||||
val verifiedBootKey = bootKey
|
||||
var verifiedBootHash: ByteArray? = null
|
||||
|
||||
try {
|
||||
if (originalRootOfTrust is ASN1Sequence) {
|
||||
verifiedBootHash = getByteArrayFromAsn1(originalRootOfTrust.getObjectAt(3))
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to get verified boot hash from original, using generated", t)
|
||||
}
|
||||
|
||||
if (verifiedBootHash == null) {
|
||||
verifiedBootHash = bootHash
|
||||
}
|
||||
|
||||
val rootOfTrustElements = arrayOf(
|
||||
DEROctetString(verifiedBootKey),
|
||||
ASN1Boolean.TRUE,
|
||||
ASN1Enumerated(0),
|
||||
DEROctetString(verifiedBootHash)
|
||||
)
|
||||
val hackedRootOfTrust = DERSequence(rootOfTrustElements)
|
||||
|
||||
vector.add(DERTaggedObject(true, 718, ASN1Integer(vendorPatchLevelLong.toLong())))
|
||||
vector.add(DERTaggedObject(true, 719, ASN1Integer(bootPatchLevelLong.toLong())))
|
||||
vector.add(DERTaggedObject(true, 706, ASN1Integer(patchLevel.toLong())))
|
||||
vector.add(DERTaggedObject(true, 705, ASN1Integer(osVersion.toLong())))
|
||||
vector.add(DERTaggedObject(704, hackedRootOfTrust))
|
||||
|
||||
val hackEnforced = DERSequence(vector)
|
||||
originalEncodables[7] = hackEnforced
|
||||
val hackedSequence = DERSequence(originalEncodables)
|
||||
val hackedSequenceOctets = DEROctetString(hackedSequence)
|
||||
|
||||
return Extension(ATTESTATION_OID, false, hackedSequenceOctets)
|
||||
}
|
||||
|
||||
private fun buildCertificate(
|
||||
keyPair: KeyPair,
|
||||
keybox: KeyBox,
|
||||
params: KeyGenParameters,
|
||||
issuer: X500Name,
|
||||
uid: Int,
|
||||
signingKeyPair: KeyPair = keybox.keyPair
|
||||
): Certificate {
|
||||
val builder = JcaX509v3CertificateBuilder(
|
||||
issuer,
|
||||
params.certificateSerial ?: BigInteger.ONE,
|
||||
params.certificateNotBefore ?: Date(),
|
||||
params.certificateNotAfter ?: (keybox.certificates[0] as X509Certificate).notAfter,
|
||||
params.certificateSubject ?: X500Name("CN=Android KeyStore Key"),
|
||||
keyPair.public
|
||||
)
|
||||
|
||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign))
|
||||
builder.addExtension(createAttestationExtension(params, uid))
|
||||
|
||||
val contentSigner = when (params.algorithm) {
|
||||
Algorithm.EC -> JcaContentSignerBuilder("SHA256withECDSA").build(signingKeyPair.private)
|
||||
Algorithm.RSA -> JcaContentSignerBuilder("SHA256withRSA").build(signingKeyPair.private)
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
||||
}
|
||||
|
||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||
}
|
||||
|
||||
private fun createAttestationExtension(params: KeyGenParameters, uid: Int): Extension {
|
||||
try {
|
||||
val key = bootKey
|
||||
val hash = bootHash
|
||||
|
||||
val rootOfTrustEncodables = arrayOf(
|
||||
DEROctetString(key),
|
||||
ASN1Boolean.TRUE,
|
||||
ASN1Enumerated(0),
|
||||
DEROctetString(hash)
|
||||
)
|
||||
val rootOfTrustSeq = DERSequence(rootOfTrustEncodables)
|
||||
|
||||
val purpose = DERSet(fromIntList(params.purpose))
|
||||
val algorithm = ASN1Integer(params.algorithm.toLong())
|
||||
val keySize = ASN1Integer(params.keySize.toLong())
|
||||
val digest = DERSet(fromIntList(params.digest))
|
||||
val ecCurve = ASN1Integer(params.ecCurve.toLong())
|
||||
val noAuthRequired = DERNull.INSTANCE
|
||||
|
||||
val osVersion = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.osVersion.toLong())
|
||||
val osPatchLevel = ASN1Integer(io.github.beakthoven.TrickyStoreOSS.patchLevel.toLong())
|
||||
val applicationID = createApplicationId(uid)
|
||||
val bootPatchLevel = ASN1Integer(bootPatchLevelLong.toLong())
|
||||
val vendorPatchLevel = ASN1Integer(vendorPatchLevelLong.toLong())
|
||||
val creationDateTime = ASN1Integer(System.currentTimeMillis())
|
||||
val origin = ASN1Integer(0L)
|
||||
val moduleHash = DEROctetString(io.github.beakthoven.TrickyStoreOSS.moduleHash)
|
||||
|
||||
val teeEnforcedObjects = mutableListOf(
|
||||
DERTaggedObject(true, 1, purpose),
|
||||
DERTaggedObject(true, 2, algorithm),
|
||||
DERTaggedObject(true, 3, keySize),
|
||||
DERTaggedObject(true, 5, digest),
|
||||
DERTaggedObject(true, 10, ecCurve),
|
||||
DERTaggedObject(true, 503, noAuthRequired),
|
||||
DERTaggedObject(true, 702, origin),
|
||||
DERTaggedObject(true, 704, rootOfTrustSeq),
|
||||
DERTaggedObject(true, 705, osVersion),
|
||||
DERTaggedObject(true, 706, osPatchLevel),
|
||||
DERTaggedObject(true, 718, vendorPatchLevel),
|
||||
DERTaggedObject(true, 719, bootPatchLevel),
|
||||
DERTaggedObject(true, 724, moduleHash)
|
||||
)
|
||||
|
||||
params.brand?.let { teeEnforcedObjects.add(DERTaggedObject(true, 710, DEROctetString(it))) }
|
||||
params.device?.let { teeEnforcedObjects.add(DERTaggedObject(true, 711, DEROctetString(it))) }
|
||||
params.product?.let { teeEnforcedObjects.add(DERTaggedObject(true, 712, DEROctetString(it))) }
|
||||
params.manufacturer?.let { teeEnforcedObjects.add(DERTaggedObject(true, 716, DEROctetString(it))) }
|
||||
params.model?.let { teeEnforcedObjects.add(DERTaggedObject(true, 717, DEROctetString(it))) }
|
||||
|
||||
params.serialno?.let { teeEnforcedObjects.add(DERTaggedObject(true, 713, DEROctetString(it))) }
|
||||
params.imei1?.let { teeEnforcedObjects.add(DERTaggedObject(true, 714, DEROctetString(it))) }
|
||||
params.imei2?.let { teeEnforcedObjects.add(DERTaggedObject(true, 715, DEROctetString(it))) }
|
||||
params.meid?.let { teeEnforcedObjects.add(DERTaggedObject(true, 723, DEROctetString(it))) }
|
||||
|
||||
teeEnforcedObjects.sortBy { it.tagNo }
|
||||
|
||||
val softwareEnforcedObjects = arrayOf<ASN1Encodable>(
|
||||
DERTaggedObject(true, 709, applicationID),
|
||||
DERTaggedObject(true, 701, creationDateTime)
|
||||
)
|
||||
|
||||
return Extension(
|
||||
ATTESTATION_OID,
|
||||
false,
|
||||
getAsn1OctetString(teeEnforcedObjects.toTypedArray(), softwareEnforcedObjects, params)
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("Failed to create attestation extension", t)
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
private fun fromIntList(list: List<Int>): Array<ASN1Encodable> {
|
||||
return list.map { ASN1Integer(it.toLong()) }.toTypedArray()
|
||||
}
|
||||
|
||||
private fun getAsn1OctetString(
|
||||
teeEnforcedEncodables: Array<ASN1Encodable>,
|
||||
softwareEnforcedEncodables: Array<ASN1Encodable>,
|
||||
params: KeyGenParameters
|
||||
): ASN1OctetString {
|
||||
val attestationVersion = ASN1Integer(400L)
|
||||
val attestationSecurityLevel = ASN1Enumerated(1)
|
||||
val keymasterVersion = ASN1Integer(400L)
|
||||
val keymasterSecurityLevel = ASN1Enumerated(1)
|
||||
val attestationChallenge = DEROctetString(params.attestationChallenge ?: ByteArray(0))
|
||||
val uniqueId = DEROctetString(ByteArray(0))
|
||||
val softwareEnforced = DERSequence(softwareEnforcedEncodables)
|
||||
val teeEnforced = DERSequence(teeEnforcedEncodables)
|
||||
|
||||
val keyDescriptionEncodables = arrayOf(
|
||||
attestationVersion,
|
||||
attestationSecurityLevel,
|
||||
keymasterVersion,
|
||||
keymasterSecurityLevel,
|
||||
attestationChallenge,
|
||||
uniqueId,
|
||||
softwareEnforced,
|
||||
teeEnforced
|
||||
)
|
||||
|
||||
val keyDescriptionSeq = DERSequence(keyDescriptionEncodables)
|
||||
return DEROctetString(keyDescriptionSeq.encoded)
|
||||
}
|
||||
|
||||
@Throws(Throwable::class)
|
||||
private fun createApplicationId(uid: Int): DEROctetString {
|
||||
val pm = Config.getPm() ?: throw IllegalStateException("PackageManager not found!")
|
||||
val packages = pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||
|
||||
val packageInfoArray = Array(packages.size) { i ->
|
||||
val packageName = packages[i]
|
||||
val packageInfo = pm.getPackageInfoCompat(packageName, PackageManager.GET_SIGNING_CERTIFICATES.toLong(), uid / 100000)
|
||||
|
||||
DERSequence(arrayOf(
|
||||
DEROctetString(packageName.toByteArray(StandardCharsets.UTF_8)),
|
||||
ASN1Integer(packageInfo.longVersionCode)
|
||||
))
|
||||
}
|
||||
|
||||
val signatures = mutableSetOf<Digest>()
|
||||
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
packages.forEach { packageName ->
|
||||
val packageInfo = pm.getPackageInfoCompat(packageName, PackageManager.GET_SIGNING_CERTIFICATES.toLong(), uid / 100000)
|
||||
packageInfo.signingInfo?.apkContentsSigners?.forEach { signature ->
|
||||
signatures.add(Digest(messageDigest.digest(signature.toByteArray())))
|
||||
}
|
||||
}
|
||||
|
||||
val signaturesArray = signatures.map { DEROctetString(it.digest) }.toTypedArray()
|
||||
|
||||
val applicationIdArray = arrayOf(
|
||||
DERSet(packageInfoArray),
|
||||
DERSet(signaturesArray)
|
||||
)
|
||||
|
||||
return DEROctetString(DERSequence(applicationIdArray).encoded)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import android.system.keystore2.KeyMetadata
|
||||
import android.util.Log
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateUtils.putCertificateChain
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
object CertificateUtils {
|
||||
private const val TAG = "CertificateUtils"
|
||||
|
||||
sealed class CertificateResult<out T> {
|
||||
data class Success<T>(val data: T) : CertificateResult<T>()
|
||||
data class Error(val message: String, val cause: Throwable? = null) : CertificateResult<Nothing>()
|
||||
|
||||
inline fun <R> map(transform: (T) -> R): CertificateResult<R> = when (this) {
|
||||
is Success -> Success(transform(data))
|
||||
is Error -> this
|
||||
}
|
||||
|
||||
fun getOrNull(): T? = when (this) {
|
||||
is Success -> data
|
||||
is Error -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray?.toCertificate(): X509Certificate? {
|
||||
return this?.let { bytes ->
|
||||
try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
certFactory.generateCertificate(ByteArrayInputStream(bytes)) as? X509Certificate
|
||||
} catch (e: CertificateException) {
|
||||
Log.w(TAG, "Couldn't parse certificate in keystore", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toCertificateResult(): CertificateResult<X509Certificate> {
|
||||
return try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
val certificate = certFactory.generateCertificate(ByteArrayInputStream(this)) as X509Certificate
|
||||
CertificateResult.Success(certificate)
|
||||
} catch (e: CertificateException) {
|
||||
CertificateResult.Error("Failed to parse certificate", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun ByteArray?.toCertificates(): Collection<X509Certificate> {
|
||||
return this?.let { bytes ->
|
||||
try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
certFactory.generateCertificates(ByteArrayInputStream(bytes)) as Collection<X509Certificate>
|
||||
} catch (e: CertificateException) {
|
||||
Log.w(TAG, "Couldn't parse certificates in keystore", e)
|
||||
emptyList()
|
||||
}
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
fun Collection<Certificate>.toByteArray(): ByteArray? = runCatching {
|
||||
ByteArrayOutputStream().use { outputStream ->
|
||||
forEach { cert -> outputStream.write(cert.encoded) }
|
||||
outputStream.toByteArray()
|
||||
}
|
||||
}.onFailure {
|
||||
Log.w(TAG, "Failed to convert certificates to byte array", it)
|
||||
}.getOrNull()
|
||||
|
||||
fun Collection<Certificate>.toByteArrayList(): List<ByteArray>? = runCatching {
|
||||
map { it.encoded }
|
||||
}.onFailure {
|
||||
Log.w(TAG, "Failed to convert certificates to byte array list", it)
|
||||
}.getOrNull()
|
||||
|
||||
fun KeyEntryResponse?.getCertificateChain(): Array<Certificate>? {
|
||||
val metadata = this?.metadata ?: return null
|
||||
val leafCert = metadata.certificate?.toCertificate() ?: return null
|
||||
|
||||
return when (val chainBytes = metadata.certificateChain) {
|
||||
null -> arrayOf(leafCert)
|
||||
else -> {
|
||||
val additionalCerts = chainBytes.toCertificates()
|
||||
buildList {
|
||||
add(leafCert)
|
||||
addAll(additionalCerts)
|
||||
}.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun KeyEntryResponse.putCertificateChain(chain: Array<Certificate>): Result<Unit> {
|
||||
return runCatching {
|
||||
metadata.putCertificateChain(chain)
|
||||
}
|
||||
}
|
||||
|
||||
fun KeyMetadata.putCertificateChain(chain: Array<Certificate>): Result<Unit> {
|
||||
return runCatching {
|
||||
if (chain.isEmpty()) return@runCatching
|
||||
|
||||
certificate = chain[0].encoded
|
||||
|
||||
if (chain.size > 1) {
|
||||
ByteArrayOutputStream().use { output ->
|
||||
for (i in 1 until chain.size) {
|
||||
output.write(chain[i].encoded)
|
||||
}
|
||||
certificateChain = output.toByteArray()
|
||||
}
|
||||
} else {
|
||||
certificateChain = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray?.toX509Certificate(): X509Certificate? = CertificateUtils.run { this@toX509Certificate.toCertificate() }
|
||||
|
||||
fun ByteArray?.toX509Certificates(): Collection<X509Certificate> = CertificateUtils.run { this@toX509Certificates.toCertificates() }
|
||||
|
||||
fun Collection<Certificate>.encodedBytes(): ByteArray? = CertificateUtils.run { this@encodedBytes.toByteArray() }
|
||||
|
||||
fun Collection<Certificate>.encodedBytesList(): List<ByteArray>? = CertificateUtils.run { this@encodedBytesList.toByteArrayList() }
|
||||
|
||||
fun KeyEntryResponse.putCertificateChain(chain: Array<Certificate>): Result<Unit> {
|
||||
return runCatching {
|
||||
metadata.putCertificateChain(chain).getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
fun KeyMetadata.putCertificateChain(chain: Array<Certificate>): Result<Unit> {
|
||||
return runCatching {
|
||||
if (chain.isEmpty()) return@runCatching
|
||||
|
||||
certificate = chain[0].encoded
|
||||
|
||||
if (chain.size > 1) {
|
||||
ByteArrayOutputStream().use { output ->
|
||||
for (i in 1 until chain.size) {
|
||||
output.write(chain[i].encoded)
|
||||
}
|
||||
certificateChain = output.toByteArray()
|
||||
}
|
||||
} else {
|
||||
certificateChain = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import android.os.Build
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.KeystoreInterceptor
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.Keystore2Interceptor
|
||||
|
||||
private const val RETRY_DELAY_MS = 1000L
|
||||
private const val SERVICE_SLEEP_MS = 1000000L
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Logger.i("Welcome to TrickyStoreOSS!")
|
||||
|
||||
try {
|
||||
initializeInterceptors()
|
||||
maintainService()
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Fatal error in main", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeInterceptors() {
|
||||
val interceptor = selectKeystoreInterceptor()
|
||||
|
||||
while (!interceptor.tryRunKeystoreInterceptor()) {
|
||||
Logger.d("Retrying interceptor initialization...")
|
||||
Thread.sleep(RETRY_DELAY_MS)
|
||||
}
|
||||
|
||||
Config.initialize()
|
||||
Logger.i("Interceptors initialized successfully")
|
||||
}
|
||||
|
||||
private fun selectKeystoreInterceptor() = when {
|
||||
Build.VERSION.SDK_INT in Build.VERSION_CODES.Q..Build.VERSION_CODES.R -> {
|
||||
Logger.i("Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})")
|
||||
KeystoreInterceptor
|
||||
}
|
||||
else -> {
|
||||
Logger.i("Using Keystore2Interceptor for Android S+ (SDK ${Build.VERSION.SDK_INT})")
|
||||
Keystore2Interceptor
|
||||
}
|
||||
}
|
||||
|
||||
private fun maintainService() {
|
||||
Logger.i("Service started, entering maintenance mode")
|
||||
while (true) {
|
||||
Thread.sleep(SERVICE_SLEEP_MS)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
|
||||
class XmlParser(private val xmlContent: String) {
|
||||
|
||||
sealed class ParseResult {
|
||||
data class Success(val attributes: Map<String, String>) : ParseResult()
|
||||
data class Error(val message: String, val cause: Throwable? = null) : ParseResult()
|
||||
}
|
||||
|
||||
fun obtainPath(path: String): ParseResult {
|
||||
return try {
|
||||
val factory = XmlPullParserFactory.newInstance()
|
||||
val parser = factory.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(StringReader(xmlContent))
|
||||
|
||||
val tags = path.split(".").toTypedArray()
|
||||
val result = readData(parser, tags, 0, mutableMapOf())
|
||||
ParseResult.Success(result)
|
||||
} catch (e: XmlPullParserException) {
|
||||
ParseResult.Error("XML parsing error: ${e.message}", e)
|
||||
} catch (e: IOException) {
|
||||
ParseResult.Error("IO error while parsing XML: ${e.message}", e)
|
||||
} catch (e: Exception) {
|
||||
ParseResult.Error("Unexpected error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun obtainPathLegacy(path: String): Map<String, String> {
|
||||
when (val result = obtainPath(path)) {
|
||||
is ParseResult.Success -> return result.attributes
|
||||
is ParseResult.Error -> throw result.cause ?: Exception(result.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readData(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: Int,
|
||||
tagCounts: MutableMap<String, Int>
|
||||
): Map<String, String> {
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
|
||||
val currentTag = parser.name ?: continue
|
||||
val targetTag = tags[index]
|
||||
val tagParts = targetTag.split("[")
|
||||
val baseTagName = tagParts[0]
|
||||
|
||||
if (currentTag == baseTagName) {
|
||||
return if (tagParts.size > 1) {
|
||||
handleIndexedTag(parser, tags, index, tagCounts, currentTag, tagParts[1])
|
||||
} else {
|
||||
handleRegularTag(parser, tags, index)
|
||||
}
|
||||
} else {
|
||||
skipCurrentElement(parser)
|
||||
}
|
||||
}
|
||||
|
||||
throw XmlPullParserException("Path not found: ${tags.joinToString(".")}")
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun handleIndexedTag(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: Int,
|
||||
tagCounts: MutableMap<String, Int>,
|
||||
currentTag: String,
|
||||
indexPart: String
|
||||
): Map<String, String> {
|
||||
val targetIndex = indexPart.replace("]", "").toIntOrNull()
|
||||
?: throw XmlPullParserException("Invalid index in tag: $indexPart")
|
||||
|
||||
val currentCount = tagCounts.getOrDefault(currentTag, 0)
|
||||
|
||||
return if (currentCount < targetIndex) {
|
||||
tagCounts[currentTag] = currentCount + 1
|
||||
readData(parser, tags, index, tagCounts)
|
||||
} else {
|
||||
if (index == tags.size - 1) {
|
||||
readAttributes(parser)
|
||||
} else {
|
||||
readData(parser, tags, index + 1, tagCounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun handleRegularTag(
|
||||
parser: XmlPullParser,
|
||||
tags: Array<String>,
|
||||
index: Int
|
||||
): Map<String, String> {
|
||||
return if (index == tags.size - 1) {
|
||||
readAttributes(parser)
|
||||
} else {
|
||||
readData(parser, tags, index + 1, mutableMapOf())
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun readAttributes(parser: XmlPullParser): Map<String, String> {
|
||||
val attributes = mutableMapOf<String, String>()
|
||||
|
||||
for (i in 0 until parser.attributeCount) {
|
||||
val name = parser.getAttributeName(i)
|
||||
val value = parser.getAttributeValue(i)
|
||||
if (name != null && value != null) {
|
||||
attributes[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
parser.text?.let { text ->
|
||||
attributes["text"] = text
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class)
|
||||
private fun skipCurrentElement(parser: XmlPullParser) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
throw IllegalStateException("Parser must be positioned at START_TAG")
|
||||
}
|
||||
|
||||
var depth = 1
|
||||
while (depth != 0) {
|
||||
when (parser.next()) {
|
||||
XmlPullParser.END_TAG -> depth--
|
||||
XmlPullParser.START_TAG -> depth++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toXmlParser(): XmlParser = XmlParser(this)
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.core.config
|
||||
|
||||
import android.content.pm.IPackageManager
|
||||
import android.os.Build
|
||||
import android.os.FileObserver
|
||||
import android.os.ServiceManager
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import java.io.File
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
|
||||
object Config {
|
||||
private val hackPackages = mutableSetOf<String>()
|
||||
private val generatePackages = mutableSetOf<String>()
|
||||
private val packageModes = mutableMapOf<String, Mode>()
|
||||
|
||||
enum class Mode {
|
||||
AUTO, LEAF_HACK, GENERATE
|
||||
}
|
||||
|
||||
private fun updateTargetPackages(f: File?) = runCatching {
|
||||
hackPackages.clear()
|
||||
generatePackages.clear()
|
||||
packageModes.clear()
|
||||
// Default: always generate for these
|
||||
listOf("com.google.android.gsf", "com.google.android.gms", "com.android.vending").forEach {
|
||||
generatePackages.add(it)
|
||||
packageModes[it] = Mode.GENERATE
|
||||
}
|
||||
f?.readLines()?.forEach {
|
||||
if (it.isNotBlank() && !it.startsWith("#")) {
|
||||
val n = it.trim()
|
||||
when {
|
||||
n.endsWith("!") -> {
|
||||
val pkg = n.removeSuffix("!").trim()
|
||||
generatePackages.add(pkg)
|
||||
packageModes[pkg] = Mode.GENERATE
|
||||
}
|
||||
n.endsWith("?") -> {
|
||||
val pkg = n.removeSuffix("?").trim()
|
||||
hackPackages.add(pkg)
|
||||
packageModes[pkg] = Mode.LEAF_HACK
|
||||
}
|
||||
else -> {
|
||||
// Auto mode
|
||||
packageModes[n] = Mode.AUTO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.i("update hack packages: $hackPackages, generate packages=$generatePackages, packageModes=$packageModes")
|
||||
}.onFailure {
|
||||
Logger.e("failed to update target files", it)
|
||||
}
|
||||
|
||||
private fun updateKeyBox(f: File?) = runCatching {
|
||||
CertificateHacker.readFromXml(f?.readText())
|
||||
}.onFailure {
|
||||
Logger.e("failed to update keybox", it)
|
||||
}
|
||||
|
||||
private const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||
private const val TARGET_FILE = "target.txt"
|
||||
private const val KEYBOX_FILE = "keybox.xml"
|
||||
private const val TEE_STATUS_FILE = "tee_status"
|
||||
private const val PATCHLEVEL_FILE = "security_patch.txt"
|
||||
private val root = File(CONFIG_PATH)
|
||||
|
||||
@Volatile
|
||||
private var teeBroken: Boolean? = null
|
||||
|
||||
private fun isTEEWorking(): Boolean {
|
||||
val alias = "tee_attest_test_key"
|
||||
return try {
|
||||
|
||||
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")
|
||||
keyStore.load(null)
|
||||
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||
|
||||
val challenge = ByteArray(16).apply {
|
||||
SecureRandom().nextBytes(this)
|
||||
}
|
||||
|
||||
val parameterSpec = KeyGenParameterSpec.Builder(
|
||||
alias,
|
||||
KeyProperties.PURPOSE_SIGN
|
||||
)
|
||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setAttestationChallenge(challenge)
|
||||
.setIsStrongBoxBacked(false)
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(parameterSpec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
|
||||
keyStore.deleteEntry(alias)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Logger.e("TEE check failure: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun storeTEEStatus(root: File) {
|
||||
val statusFile = File(root, TEE_STATUS_FILE)
|
||||
val status = isTEEWorking()
|
||||
teeBroken = !status
|
||||
try {
|
||||
statusFile.writeText("teeBroken=${!status}")
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to write TEE status: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTEEStatus(root: File) {
|
||||
val statusFile = File(root, TEE_STATUS_FILE)
|
||||
if (statusFile.exists()) {
|
||||
val line = statusFile.readText().trim()
|
||||
teeBroken = line == "teeBroken=true"
|
||||
} else {
|
||||
teeBroken = null
|
||||
}
|
||||
}
|
||||
|
||||
object ConfigObserver : FileObserver(root, CLOSE_WRITE or DELETE or MOVED_FROM or MOVED_TO) {
|
||||
override fun onEvent(event: Int, path: String?) {
|
||||
path ?: return
|
||||
val f = when (event) {
|
||||
CLOSE_WRITE, MOVED_TO -> File(root, path)
|
||||
DELETE, MOVED_FROM -> null
|
||||
else -> return
|
||||
}
|
||||
when (path) {
|
||||
TARGET_FILE -> updateTargetPackages(f)
|
||||
KEYBOX_FILE -> updateKeyBox(f)
|
||||
PATCHLEVEL_FILE -> updatePatchLevel(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun initialize() {
|
||||
root.mkdirs()
|
||||
val scope = File(root, TARGET_FILE)
|
||||
if (scope.exists()) {
|
||||
updateTargetPackages(scope)
|
||||
} else {
|
||||
Logger.e("target.txt file not found, please put it to $scope !")
|
||||
}
|
||||
val keybox = File(root, KEYBOX_FILE)
|
||||
if (!keybox.exists()) {
|
||||
Logger.e("keybox file not found, please put it to $keybox !")
|
||||
} else {
|
||||
updateKeyBox(keybox)
|
||||
}
|
||||
storeTEEStatus(root)
|
||||
val patchFile = File(root, PATCHLEVEL_FILE)
|
||||
updatePatchLevel(if (patchFile.exists()) patchFile else null)
|
||||
ConfigObserver.startWatching()
|
||||
}
|
||||
|
||||
private var iPm: IPackageManager? = null
|
||||
|
||||
fun getPm(): IPackageManager? {
|
||||
if (iPm == null) {
|
||||
iPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"))
|
||||
}
|
||||
return iPm
|
||||
}
|
||||
|
||||
fun needHack(callingUid: Int): Boolean = kotlin.runCatching {
|
||||
val ps = getPm()?.getPackagesForUid(callingUid) ?: return false
|
||||
if (teeBroken == null) loadTEEStatus(root)
|
||||
for (pkg in ps) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.LEAF_HACK -> return true
|
||||
Mode.AUTO -> {
|
||||
if (teeBroken == false) return true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false
|
||||
|
||||
fun needGenerate(callingUid: Int): Boolean = kotlin.runCatching {
|
||||
val ps = getPm()?.getPackagesForUid(callingUid) ?: return false
|
||||
if (teeBroken == null) loadTEEStatus(root)
|
||||
for (pkg in ps) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.GENERATE -> return true
|
||||
Mode.AUTO -> {
|
||||
if (teeBroken == true) return true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}.onFailure { Logger.e("failed to get packages", it) }.getOrNull() ?: false
|
||||
|
||||
@Volatile
|
||||
var _customPatchLevel: CustomPatchLevel? = null
|
||||
|
||||
fun updatePatchLevel(f: File?) = runCatching {
|
||||
if (f == null || !f.exists()) {
|
||||
_customPatchLevel = null
|
||||
return@runCatching
|
||||
}
|
||||
val lines = f.readLines().map { it.trim() }.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
if (lines.isEmpty()) {
|
||||
_customPatchLevel = null
|
||||
return@runCatching
|
||||
}
|
||||
if (lines.size == 1 && !lines[0].contains("=")) {
|
||||
_customPatchLevel = CustomPatchLevel(all = lines[0])
|
||||
return@runCatching
|
||||
}
|
||||
val map = mutableMapOf<String, String>()
|
||||
for (line in lines) {
|
||||
val idx = line.indexOf('=')
|
||||
if (idx > 0) {
|
||||
val key = line.substring(0, idx).trim().lowercase()
|
||||
val value = line.substring(idx + 1).trim()
|
||||
map[key] = value
|
||||
}
|
||||
}
|
||||
val all = map["all"]
|
||||
_customPatchLevel = CustomPatchLevel(
|
||||
system = map["system"] ?: all,
|
||||
vendor = map["vendor"] ?: all,
|
||||
boot = map["boot"] ?: all,
|
||||
all = all
|
||||
)
|
||||
}.onFailure {
|
||||
Logger.e("failed to update patch level", it)
|
||||
}
|
||||
}
|
||||
|
||||
data class CustomPatchLevel(
|
||||
val system: String? = null,
|
||||
val vendor: String? = null,
|
||||
val boot: String? = null,
|
||||
val all: String? = null
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.core.logging
|
||||
|
||||
import android.util.Log
|
||||
|
||||
object Logger {
|
||||
const val TAG = "TrickyStore"
|
||||
|
||||
sealed class LogLevel(val priority: Int) {
|
||||
object Debug : LogLevel(Log.DEBUG)
|
||||
object Info : LogLevel(Log.INFO)
|
||||
object Warning : LogLevel(Log.WARN)
|
||||
object Error : LogLevel(Log.ERROR)
|
||||
object Verbose : LogLevel(Log.VERBOSE)
|
||||
}
|
||||
|
||||
fun d(message: String) {
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
|
||||
fun e(message: String) {
|
||||
Log.e(TAG, message)
|
||||
}
|
||||
|
||||
fun e(message: String, throwable: Throwable) {
|
||||
Log.e(TAG, "wtf: $message", throwable)
|
||||
}
|
||||
|
||||
fun i(message: String) {
|
||||
Log.i(TAG, message)
|
||||
}
|
||||
|
||||
fun w(message: String) {
|
||||
Log.w(TAG, message)
|
||||
}
|
||||
|
||||
fun w(message: String, throwable: Throwable) {
|
||||
Log.w(TAG, message, throwable)
|
||||
}
|
||||
|
||||
fun v(message: String) {
|
||||
Log.v(TAG, message)
|
||||
}
|
||||
|
||||
fun log(level: LogLevel, message: String, throwable: Throwable? = null) {
|
||||
when (level) {
|
||||
is LogLevel.Debug -> if (throwable != null) Log.d(TAG, message, throwable) else Log.d(TAG, message)
|
||||
is LogLevel.Info -> if (throwable != null) Log.i(TAG, message, throwable) else Log.i(TAG, message)
|
||||
is LogLevel.Warning -> if (throwable != null) Log.w(TAG, message, throwable) else Log.w(TAG, message)
|
||||
is LogLevel.Error -> if (throwable != null) Log.e(TAG, message, throwable) else Log.e(TAG, message)
|
||||
is LogLevel.Verbose -> if (throwable != null) Log.v(TAG, message, throwable) else Log.v(TAG, message)
|
||||
}
|
||||
}
|
||||
|
||||
fun logIf(level: LogLevel, condition: Boolean = true, messageProvider: () -> String) {
|
||||
if (condition && Log.isLoggable(TAG, level.priority)) {
|
||||
log(level, messageProvider())
|
||||
}
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
|
||||
open class BinderInterceptor : Binder() {
|
||||
|
||||
sealed class Result
|
||||
|
||||
data object Skip : Result()
|
||||
|
||||
data object Continue : Result()
|
||||
|
||||
data class OverrideData(val data: Parcel) : Result()
|
||||
|
||||
data class OverrideReply(val code: Int = 0, val reply: Parcel) : Result()
|
||||
|
||||
companion object {
|
||||
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
|
||||
|
||||
private const val REGISTER_INTERCEPTOR_CODE = 1
|
||||
|
||||
private const val PRE_TRANSACT_CODE = 1
|
||||
private const val POST_TRANSACT_CODE = 2
|
||||
|
||||
private const val RESULT_SKIP = 1
|
||||
private const val RESULT_CONTINUE = 2
|
||||
private const val RESULT_OVERRIDE_REPLY = 3
|
||||
private const val RESULT_OVERRIDE_DATA = 4
|
||||
|
||||
fun getBinderBackdoor(binder: IBinder): IBinder? {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
|
||||
return try {
|
||||
val success = binder.transact(BACKDOOR_TRANSACTION_CODE, data, reply, 0)
|
||||
if (success) {
|
||||
Logger.d("Backdoor access granted for binder: $binder")
|
||||
reply.readStrongBinder()
|
||||
} else {
|
||||
Logger.d("Backdoor access denied for binder: $binder")
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to access binder backdoor", e)
|
||||
null
|
||||
} finally {
|
||||
data.recycle()
|
||||
reply.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun registerBinderInterceptor(
|
||||
backdoor: IBinder,
|
||||
target: IBinder,
|
||||
interceptor: BinderInterceptor
|
||||
) {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
|
||||
try {
|
||||
data.writeStrongBinder(target)
|
||||
data.writeStrongBinder(interceptor)
|
||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||
Logger.d("Registered interceptor for target: $target")
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Failed to register binder interceptor", e)
|
||||
} finally {
|
||||
data.recycle()
|
||||
reply.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result = Skip
|
||||
|
||||
open fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result = Skip
|
||||
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
val result = when (code) {
|
||||
PRE_TRANSACT_CODE -> handlePreTransact(data)
|
||||
POST_TRANSACT_CODE -> handlePostTransact(data)
|
||||
else -> return super.onTransact(code, data, reply, flags)
|
||||
}
|
||||
|
||||
writeResultToReply(result, reply!!)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handlePreTransact(data: Parcel): Result {
|
||||
val target = data.readStrongBinder()
|
||||
val transactionCode = data.readInt()
|
||||
val transactionFlags = data.readInt()
|
||||
val callingUid = data.readInt()
|
||||
val callingPid = data.readInt()
|
||||
val dataSize = data.readLong()
|
||||
|
||||
val transactionData = Parcel.obtain()
|
||||
return try {
|
||||
transactionData.appendFrom(data, data.dataPosition(), dataSize.toInt())
|
||||
transactionData.setDataPosition(0)
|
||||
onPreTransact(target, transactionCode, transactionFlags, callingUid, callingPid, transactionData)
|
||||
} finally {
|
||||
transactionData.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostTransact(data: Parcel): Result {
|
||||
val target = data.readStrongBinder()
|
||||
val transactionCode = data.readInt()
|
||||
val transactionFlags = data.readInt()
|
||||
val callingUid = data.readInt()
|
||||
val callingPid = data.readInt()
|
||||
val resultCode = data.readInt()
|
||||
|
||||
val transactionData = Parcel.obtain()
|
||||
val transactionReply = Parcel.obtain()
|
||||
|
||||
return try {
|
||||
val dataSize = data.readLong().toInt()
|
||||
transactionData.appendFrom(data, data.dataPosition(), dataSize)
|
||||
transactionData.setDataPosition(0)
|
||||
data.setDataPosition(data.dataPosition() + dataSize)
|
||||
|
||||
val replySize = data.readLong().toInt()
|
||||
val reply = if (replySize > 0) {
|
||||
transactionReply.appendFrom(data, data.dataPosition(), replySize)
|
||||
transactionReply.setDataPosition(0)
|
||||
transactionReply
|
||||
} else null
|
||||
|
||||
onPostTransact(target, transactionCode, transactionFlags, callingUid, callingPid, transactionData, reply, resultCode)
|
||||
} finally {
|
||||
transactionData.recycle()
|
||||
transactionReply.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeResultToReply(result: Result, reply: Parcel) {
|
||||
when (result) {
|
||||
Skip -> reply.writeInt(RESULT_SKIP)
|
||||
Continue -> reply.writeInt(RESULT_CONTINUE)
|
||||
is OverrideReply -> {
|
||||
reply.writeInt(RESULT_OVERRIDE_REPLY)
|
||||
reply.writeInt(result.code)
|
||||
reply.writeLong(result.reply.dataSize().toLong())
|
||||
reply.appendFrom(result.reply, 0, result.reply.dataSize())
|
||||
result.reply.recycle()
|
||||
}
|
||||
is OverrideData -> {
|
||||
reply.writeInt(RESULT_OVERRIDE_DATA)
|
||||
reply.writeLong(result.data.dataSize().toLong())
|
||||
reply.appendFrom(result.data, 0, result.data.dataSize())
|
||||
result.data.recycle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.os.ServiceManager
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
abstract class BaseKeystoreInterceptor : BinderInterceptor() {
|
||||
|
||||
protected lateinit var keystore: IBinder
|
||||
protected var triedCount = 0
|
||||
protected var injected = false
|
||||
protected open val maxRetries: Int = 3
|
||||
|
||||
protected abstract val serviceName: String
|
||||
protected abstract val injectionCommand: String
|
||||
protected abstract val processName: String
|
||||
|
||||
fun tryRunKeystoreInterceptor(): Boolean {
|
||||
Logger.i("Trying to register ${this::class.simpleName} (attempt $triedCount)...")
|
||||
|
||||
val service = getService() ?: return false
|
||||
val backdoor = getBinderBackdoor(service)
|
||||
|
||||
return if (backdoor != null) {
|
||||
setupInterceptor(service, backdoor)
|
||||
} else {
|
||||
handleMissingBackdoor()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getService(): IBinder? = ServiceManager.getService(serviceName)
|
||||
|
||||
protected open fun setupInterceptor(service: IBinder, backdoor: IBinder): Boolean {
|
||||
keystore = service
|
||||
Logger.i("Registering for $serviceName: $keystore")
|
||||
|
||||
registerBinderInterceptor(backdoor, service, this)
|
||||
service.linkToDeath(createDeathRecipient(), 0)
|
||||
onInterceptorSetup(service, backdoor)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleMissingBackdoor(): Boolean {
|
||||
if (triedCount >= maxRetries) {
|
||||
Logger.e("Tried injection $maxRetries times but still no backdoor, exiting")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
if (!injected) {
|
||||
performInjection()
|
||||
injected = true
|
||||
}
|
||||
|
||||
triedCount++
|
||||
return false
|
||||
}
|
||||
|
||||
protected open fun performInjection() {
|
||||
Logger.i("Attempting to inject into $processName...")
|
||||
|
||||
val command = arrayOf("/system/bin/sh", "-c", injectionCommand)
|
||||
Logger.d("Injection command: ${command.joinToString(" ")}")
|
||||
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
|
||||
if (process.waitFor() != 0) {
|
||||
Logger.e("Injection failed! Daemon will exit")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
Logger.i("Injection completed successfully")
|
||||
}
|
||||
|
||||
protected open fun createDeathRecipient(): IBinder.DeathRecipient = object : IBinder.DeathRecipient {
|
||||
override fun binderDied() {
|
||||
Logger.d("$serviceName died, daemon restarting")
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onInterceptorSetup(service: IBinder, backdoor: IBinder) {
|
||||
// Default implementation does nothing
|
||||
}
|
||||
}
|
||||
|
||||
object InterceptorUtils {
|
||||
|
||||
fun createSuccessKeystoreResponse(): KeystoreResponse {
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
parcel.writeInt(KeyStore.NO_ERROR)
|
||||
parcel.writeString("")
|
||||
parcel.setDataPosition(0)
|
||||
return KeystoreResponse.CREATOR.createFromParcel(parcel)
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun createSuccessReply(resultCode: Int = KeyStore.NO_ERROR): BinderInterceptor.OverrideReply {
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.writeNoException()
|
||||
parcel.writeInt(resultCode)
|
||||
return BinderInterceptor.OverrideReply(0, parcel)
|
||||
}
|
||||
|
||||
fun createByteArrayReply(data: ByteArray, resultCode: Int = KeyStore.NO_ERROR): BinderInterceptor.OverrideReply {
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.writeNoException()
|
||||
parcel.writeByteArray(data)
|
||||
return BinderInterceptor.OverrideReply(resultCode, parcel)
|
||||
}
|
||||
|
||||
fun <T : Parcelable?> createTypedObjectReply(obj: T, flags: Int = 0, resultCode: Int = 0): BinderInterceptor.OverrideReply {
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.writeNoException()
|
||||
parcel.writeTypedObject(obj, flags)
|
||||
return BinderInterceptor.OverrideReply(resultCode, parcel)
|
||||
}
|
||||
|
||||
fun String.extractAlias(): String {
|
||||
return when {
|
||||
contains("_") -> split("_")[1]
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
fun Parcel.hasException(): Boolean {
|
||||
return kotlin.runCatching { readException() }.exceptionOrNull() != null
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateUtils
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createTypedObjectReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException
|
||||
import io.github.beakthoven.TrickyStoreOSS.putCertificateChain
|
||||
|
||||
@SuppressLint("BlockedPrivateApi")
|
||||
object Keystore2Interceptor : BaseKeystoreInterceptor() {
|
||||
private val getKeyEntryTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
|
||||
private val deleteKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
|
||||
|
||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||
override val processName = "keystore2"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTrickyStoreOSS.so entry"
|
||||
|
||||
private var teeInterceptor: SecurityLevelInterceptor? = null
|
||||
private var strongBoxInterceptor: SecurityLevelInterceptor? = null
|
||||
|
||||
override fun onInterceptorSetup(service: IBinder, backdoor: IBinder) {
|
||||
setupSecurityLevelInterceptors(service, backdoor)
|
||||
}
|
||||
|
||||
private fun setupSecurityLevelInterceptors(service: IBinder, backdoor: IBinder) {
|
||||
val ks = IKeystoreService.Stub.asInterface(service)
|
||||
|
||||
val tee = kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT) }
|
||||
.getOrNull()
|
||||
if (tee != null) {
|
||||
Logger.i("Registering for TEE SecurityLevel: $tee")
|
||||
val interceptor = SecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||
registerBinderInterceptor(backdoor, tee.asBinder(), interceptor)
|
||||
teeInterceptor = interceptor
|
||||
} else {
|
||||
Logger.i("No TEE SecurityLevel found")
|
||||
}
|
||||
|
||||
val strongBox = kotlin.runCatching { ks.getSecurityLevel(SecurityLevel.STRONGBOX) }
|
||||
.getOrNull()
|
||||
if (strongBox != null) {
|
||||
Logger.i("Registering for StrongBox SecurityLevel: $strongBox")
|
||||
val interceptor = SecurityLevelInterceptor(strongBox, SecurityLevel.STRONGBOX)
|
||||
registerBinderInterceptor(backdoor, strongBox.asBinder(), interceptor)
|
||||
strongBoxInterceptor = interceptor
|
||||
} else {
|
||||
Logger.i("No StrongBox SecurityLevel found")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (code == getKeyEntryTransaction) {
|
||||
if (CertificateHacker.canHack()) {
|
||||
Logger.d("intercept pre $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()}")
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching
|
||||
if (Config.needGenerate(callingUid)) {
|
||||
val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias)
|
||||
?: return@runCatching
|
||||
Logger.i("Generate key for uid=$callingUid alias=${descriptor.alias}")
|
||||
return createTypedObjectReply(response)
|
||||
} else if (Config.needHack(callingUid)) {
|
||||
if (SecurityLevelInterceptor.shouldSkipLeafHack(callingUid, descriptor.alias)) {
|
||||
Logger.i("skip leaf hack for uid=$callingUid alias=${descriptor.alias}")
|
||||
val response = SecurityLevelInterceptor.getKeyResponse(callingUid, descriptor.alias)
|
||||
if (response != null) {
|
||||
Logger.i("Found generated response for uid=$callingUid alias=${descriptor.alias}")
|
||||
return createTypedObjectReply(response)
|
||||
} else {
|
||||
Logger.e("No generated response found for uid=$callingUid alias=${descriptor.alias}")
|
||||
return@runCatching
|
||||
}
|
||||
} else {
|
||||
Logger.i("proceeding with leaf hack for uid=$callingUid alias=${descriptor.alias}")
|
||||
return Continue
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
override fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result {
|
||||
if (target != keystore || reply == null) return Skip
|
||||
if (reply.hasException()) return Skip
|
||||
val p = Parcel.obtain()
|
||||
Logger.d("intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}")
|
||||
|
||||
if (code == deleteKeyTransaction && resultCode == 0) {
|
||||
data.enforceInterface("android.system.keystore2.IKeystoreService")
|
||||
|
||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
if (keyDescriptor == null || keyDescriptor.domain == 0) return Skip
|
||||
|
||||
SecurityLevelInterceptor.keys.remove(SecurityLevelInterceptor.Key(callingUid, keyDescriptor.alias))
|
||||
|
||||
return Skip
|
||||
} else if (code == getKeyEntryTransaction) {
|
||||
try {
|
||||
data.enforceInterface("android.system.keystore2.IKeystoreService")
|
||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)
|
||||
if (response != null) {
|
||||
val chain = CertificateUtils.run { response.getCertificateChain() }
|
||||
if (chain != null) {
|
||||
val newChain = CertificateHacker.hackCertificateChain(chain)
|
||||
response.putCertificateChain(newChain).getOrThrow()
|
||||
Logger.i("Hacked certificate for uid=$callingUid")
|
||||
return createTypedObjectReply(response)
|
||||
} else {
|
||||
p.recycle()
|
||||
}
|
||||
} else {
|
||||
p.recycle()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t)
|
||||
p.recycle()
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.security.Credentials
|
||||
import android.security.KeyStore
|
||||
import android.security.keymaster.ExportResult
|
||||
import android.security.keymaster.KeyCharacteristics
|
||||
import android.security.keymaster.KeymasterArguments
|
||||
import android.security.keymaster.KeymasterCertificateChain
|
||||
import android.security.keymaster.KeymasterDefs
|
||||
import android.security.keystore.IKeystoreCertificateChainCallback
|
||||
import android.security.keystore.IKeystoreExportKeyCallback
|
||||
import android.security.keystore.IKeystoreKeyCharacteristicsCallback
|
||||
import android.security.keystore.IKeystoreService
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createByteArrayReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessKeystoreResponse
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.createSuccessReply
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.extractAlias
|
||||
import io.github.beakthoven.TrickyStoreOSS.interceptors.InterceptorUtils.hasException
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.util.Date
|
||||
|
||||
@SuppressLint("BlockedPrivateApi")
|
||||
object KeystoreInterceptor : BaseKeystoreInterceptor() {
|
||||
private val getTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "get")
|
||||
private val generateKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "generateKey")
|
||||
private val getKeyCharacteristicsTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "getKeyCharacteristics")
|
||||
private val exportKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "exportKey")
|
||||
private val attestKeyTransaction =
|
||||
getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
|
||||
|
||||
override val serviceName = "android.security.keystore"
|
||||
override val processName = "keystore"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore` libTrickyStoreOSS.so entry"
|
||||
|
||||
private const val DESCRIPTOR = "android.security.keystore.IKeystoreService"
|
||||
|
||||
private val keyArguments = HashMap<Key, CertificateHacker.KeyGenParameters>()
|
||||
private val keyPairs = HashMap<Key, KeyPair>()
|
||||
|
||||
data class Key(val uid: Int, val alias: String)
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (CertificateHacker.canHack()) {
|
||||
if (code == getTransaction) {
|
||||
if (Config.needHack(callingUid)) {
|
||||
return Continue
|
||||
} else if (Config.needGenerate(callingUid)) {
|
||||
return Skip
|
||||
}
|
||||
} else if (Config.needGenerate(callingUid)) {
|
||||
when (code) {
|
||||
generateKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("generateKeyTransaction uid $callingUid alias $alias")
|
||||
val check = data.readInt()
|
||||
val kma = KeymasterArguments()
|
||||
val kgp = CertificateHacker.KeyGenParameters()
|
||||
if (check == 1) {
|
||||
kma.readFromParcel(data)
|
||||
kgp.algorithm = kma.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0)
|
||||
kgp.keySize = kma.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt()
|
||||
kgp.setEcCurveName(kgp.keySize)
|
||||
kgp.purpose = kma.getEnums(KeymasterDefs.KM_TAG_PURPOSE)
|
||||
kgp.digest = kma.getEnums(KeymasterDefs.KM_TAG_DIGEST)
|
||||
kgp.certificateNotBefore = kma.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date())
|
||||
if (kgp.algorithm == KeymasterDefs.KM_ALGORITHM_RSA) {
|
||||
try {
|
||||
val getArgumentByTag = KeymasterArguments::class.java.getDeclaredMethods().first { it.name == "getArgumentByTag" }
|
||||
getArgumentByTag.isAccessible = true
|
||||
val rsaArgument = getArgumentByTag.invoke(kma, KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT)
|
||||
|
||||
val getLongTagValue = KeymasterArguments::class.java.getDeclaredMethods().first { it.name == "getLongTagValue" }
|
||||
getLongTagValue.isAccessible = true
|
||||
kgp.rsaPublicExponent = getLongTagValue.invoke(kma, rsaArgument) as BigInteger
|
||||
} catch (ex: Exception) {
|
||||
Logger.e("Read rsaPublicExponent error", ex)
|
||||
}
|
||||
}
|
||||
keyArguments[Key(callingUid, alias)] = kgp
|
||||
}
|
||||
|
||||
val kc = KeyCharacteristics()
|
||||
kc.swEnforced = KeymasterArguments()
|
||||
kc.hwEnforced = kma
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
callback.onFinished(ksr, kc)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("generateKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
getKeyCharacteristicsTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("getKeyCharacteristicsTransaction uid $callingUid alias $alias")
|
||||
val kc = KeyCharacteristics()
|
||||
val kma = KeymasterArguments()
|
||||
kma.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, keyArguments[Key(callingUid, alias)]!!.algorithm)
|
||||
kc.swEnforced = KeymasterArguments()
|
||||
kc.hwEnforced = kma
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
callback.onFinished(ksr, kc)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("getKeyCharacteristicsTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
exportKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("exportKeyTransaction uid $callingUid alias $alias")
|
||||
val kp = CertificateHacker.generateKeyPair(keyArguments[Key(callingUid, alias)]!!)
|
||||
keyPairs[Key(callingUid, alias)] = kp!!
|
||||
|
||||
val erP = Parcel.obtain()
|
||||
erP.writeInt(KeyStore.NO_ERROR)
|
||||
erP.writeByteArray(kp.public.encoded)
|
||||
erP.setDataPosition(0)
|
||||
val er = ExportResult.CREATOR.createFromParcel(erP)
|
||||
erP.recycle()
|
||||
|
||||
callback.onFinished(er)
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("exportKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
|
||||
attestKeyTransaction -> {
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val callback = IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
|
||||
val alias = data.readString()!!.extractAlias()
|
||||
Logger.i("attestKeyTransaction uid $callingUid alias $alias")
|
||||
val check = data.readInt()
|
||||
val kma = KeymasterArguments()
|
||||
if (check == 1) {
|
||||
kma.readFromParcel(data)
|
||||
val attestationChallenge = kma.getBytes(KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, ByteArray(0))
|
||||
|
||||
val ksr = createSuccessKeystoreResponse()
|
||||
|
||||
val key = Key(callingUid, alias)
|
||||
val ka = keyArguments[key]!!
|
||||
ka.attestationChallenge = attestationChallenge
|
||||
val chain = CertificateHacker.generateChain(callingUid, ka, keyPairs[key]!!)
|
||||
|
||||
val kcc = KeymasterCertificateChain(chain)
|
||||
callback.onFinished(ksr, kcc)
|
||||
}
|
||||
|
||||
return createSuccessReply()
|
||||
}.onFailure {
|
||||
Logger.e("attestKeyTransaction error", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
override fun onPostTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
reply: Parcel?,
|
||||
resultCode: Int
|
||||
): Result {
|
||||
if (target != keystore || code != getTransaction || reply == null) return Skip
|
||||
if (reply.hasException()) return Skip
|
||||
val p = Parcel.obtain()
|
||||
Logger.d("intercept post $target uid=$callingUid pid=$callingPid dataSz=${data.dataSize()} replySz=${reply.dataSize()}")
|
||||
try {
|
||||
data.enforceInterface(DESCRIPTOR)
|
||||
val alias = data.readString() ?: ""
|
||||
var response = reply.createByteArray()
|
||||
when {
|
||||
alias.startsWith(Credentials.USER_CERTIFICATE) -> {
|
||||
response = CertificateHacker.hackCertificateChainUSR(response!!, alias.extractAlias(), callingUid)
|
||||
Logger.i("Hacked leaf certificate for uid=$callingUid")
|
||||
return createByteArrayReply(response)
|
||||
}
|
||||
alias.startsWith(Credentials.CA_CERTIFICATE) -> {
|
||||
response = CertificateHacker.hackCertificateChainCA(response!!, alias.extractAlias(), callingUid)
|
||||
Logger.i("Hacked CA certificate chain for uid=$callingUid")
|
||||
return createByteArrayReply(response)
|
||||
}
|
||||
else -> p.recycle()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Logger.e("failed to hack certificate chain of uid=$callingUid pid=$callingPid!", t)
|
||||
p.recycle()
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2025 Dakkshesh <beakthoven@gmail.com>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package io.github.beakthoven.TrickyStoreOSS.interceptors
|
||||
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.Authorization
|
||||
import android.system.keystore2.IKeystoreSecurityLevel
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import android.system.keystore2.KeyMetadata
|
||||
import androidx.annotation.Keep
|
||||
import io.github.beakthoven.TrickyStoreOSS.CertificateHacker
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.config.Config
|
||||
import io.github.beakthoven.TrickyStoreOSS.core.logging.Logger
|
||||
import io.github.beakthoven.TrickyStoreOSS.getTransactCode
|
||||
import io.github.beakthoven.TrickyStoreOSS.putCertificateChain
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class SecurityLevelInterceptor(
|
||||
private val original: IKeystoreSecurityLevel,
|
||||
private val level: Int
|
||||
) : BinderInterceptor() {
|
||||
companion object {
|
||||
private val generateKeyTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
|
||||
private val deleteKeyTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "deleteKey")
|
||||
private val createOperationTransaction =
|
||||
getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "createOperation")
|
||||
|
||||
@Keep
|
||||
val keys = ConcurrentHashMap<Key, Info>()
|
||||
|
||||
@Keep
|
||||
val keyPairs = ConcurrentHashMap<Key, Pair<KeyPair, List<Certificate>>>()
|
||||
|
||||
@Keep
|
||||
val skipLeafHacks = ConcurrentHashMap<Key, Boolean>()
|
||||
|
||||
@Keep
|
||||
fun getKeyResponse(uid: Int, alias: String): KeyEntryResponse? =
|
||||
keys[Key(uid, alias)]?.response
|
||||
|
||||
@Keep
|
||||
fun getKeyPairs(uid: Int, alias: String): Pair<KeyPair, List<Certificate>>? =
|
||||
keyPairs[Key(uid, alias)]
|
||||
|
||||
@Keep
|
||||
fun shouldSkipLeafHack(uid: Int, alias: String): Boolean =
|
||||
skipLeafHacks[Key(uid, alias)] ?: false
|
||||
}
|
||||
|
||||
data class Key(val uid: Int, val alias: String)
|
||||
data class Info(val keyPair: KeyPair, val response: KeyEntryResponse)
|
||||
|
||||
override fun onPreTransact(
|
||||
target: IBinder,
|
||||
code: Int,
|
||||
flags: Int,
|
||||
callingUid: Int,
|
||||
callingPid: Int,
|
||||
data: Parcel
|
||||
): Result {
|
||||
if (code == generateKeyTransaction) {
|
||||
Logger.i("intercept key gen uid=$callingUid pid=$callingPid")
|
||||
kotlin.runCatching {
|
||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||
val keyDescriptor =
|
||||
data.readTypedObject(KeyDescriptor.CREATOR) ?: return@runCatching
|
||||
val attestationKeyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val aFlags = data.readInt()
|
||||
val entropy = data.createByteArray()
|
||||
val kgp = CertificateHacker.KeyGenParameters(params)
|
||||
if (Config.needGenerate(callingUid)) {
|
||||
val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp)
|
||||
?: return@runCatching
|
||||
keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second)
|
||||
val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor)
|
||||
keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response)
|
||||
val p = Parcel.obtain()
|
||||
p.writeNoException()
|
||||
p.writeTypedObject(response.metadata, 0)
|
||||
return OverrideReply(0, p)
|
||||
} else if (Config.needHack(callingUid)) {
|
||||
if ((kgp.purpose.contains(7)) || (attestationKeyDescriptor != null)) {
|
||||
Logger.i("Generating key in generation mode for attestation: uid=$callingUid alias=${keyDescriptor.alias}")
|
||||
val pair = CertificateHacker.generateKeyPair(callingUid, keyDescriptor, attestationKeyDescriptor, kgp)
|
||||
?: return@runCatching
|
||||
keyPairs[Key(callingUid, keyDescriptor.alias)] = Pair(pair.first, pair.second)
|
||||
val response = buildResponse(pair.second, kgp, attestationKeyDescriptor ?: keyDescriptor)
|
||||
keys[Key(callingUid, keyDescriptor.alias)] = Info(pair.first, response)
|
||||
SecurityLevelInterceptor.skipLeafHacks[Key(callingUid, keyDescriptor.alias)] = true
|
||||
val p = Parcel.obtain()
|
||||
p.writeNoException()
|
||||
p.writeTypedObject(response.metadata, 0)
|
||||
return OverrideReply(0, p)
|
||||
} else {
|
||||
skipLeafHacks.remove(Key(callingUid, keyDescriptor.alias))
|
||||
Logger.i("Cleared skip flag for non-attestation key: uid=$callingUid alias=${keyDescriptor.alias}")
|
||||
return Skip
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Logger.e("parse key gen request", it)
|
||||
}
|
||||
}
|
||||
return Skip
|
||||
}
|
||||
|
||||
private fun buildResponse(
|
||||
chain: List<Certificate>,
|
||||
params: CertificateHacker.KeyGenParameters,
|
||||
descriptor: KeyDescriptor
|
||||
): KeyEntryResponse {
|
||||
val response = KeyEntryResponse()
|
||||
val metadata = KeyMetadata()
|
||||
metadata.keySecurityLevel = level
|
||||
metadata.putCertificateChain(chain.toTypedArray()).getOrThrow()
|
||||
val d = KeyDescriptor()
|
||||
d.domain = descriptor.domain
|
||||
d.nspace = descriptor.nspace
|
||||
metadata.key = d
|
||||
val authorizations = ArrayList<Authorization>()
|
||||
var a: Authorization
|
||||
for (i in params.purpose.toList()) {
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.PURPOSE
|
||||
a.keyParameter.value = KeyParameterValue.keyPurpose(i)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
}
|
||||
for (i in params.digest.toList()) {
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.DIGEST
|
||||
a.keyParameter.value = KeyParameterValue.digest(i)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
}
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.ALGORITHM
|
||||
a.keyParameter.value = KeyParameterValue.algorithm(params.algorithm)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.KEY_SIZE
|
||||
a.keyParameter.value = KeyParameterValue.integer(params.keySize)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.EC_CURVE
|
||||
a.keyParameter.value = KeyParameterValue.ecCurve(params.ecCurve)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
a = Authorization()
|
||||
a.keyParameter = KeyParameter()
|
||||
a.keyParameter.tag = Tag.NO_AUTH_REQUIRED
|
||||
a.keyParameter.value = KeyParameterValue.boolValue(true)
|
||||
a.securityLevel = level
|
||||
authorizations.add(a)
|
||||
metadata.authorizations = authorizations.toTypedArray<Authorization>()
|
||||
response.metadata = metadata
|
||||
response.iSecurityLevel = original
|
||||
return response
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user