使用Magisk+riru實現全局改機

前言

提到全局改機,我們想到修改的不是修改Android源碼就是利用Xposed改機,前者成本太高,後者只能修改Java層的數據不夠徹底。magisk是Android平台上功能強大的工具,利用它可以隨心所欲的修改系統。它或許幫我們在不修改系統源碼的情況實現相對完美的改機。那麼,riru是什麼?它和magisk有什麼關係?它又和我們的模塊有什麼關係?簡而言之,riru是magisk的模塊,本文寫的模塊也是magisk模塊,但是它又依賴於riru。那riru模塊提供了什麼功能呢?riru模塊能夠感知App的啟動,並且能夠給App注入一些代碼,依賴riru模塊的模塊也能擁有這個能力。我們實現改機,其實就是需要儘早的感知App啟動的,這樣我們才能儘早hook一些函數,使App啟動的時候獲取到的數據是我們修改後的數據。了解了相關知識,我們就可以開始編寫模塊了。

準備

riru模塊的作者寫了riru模塊的模板,我們在這個項目的基礎上改就行,首先將模塊克隆下來並使用Android Studio打開:

git clone //github.com/RikkaApps/Riru-ModuleTemplate

因為改機不僅要改Java層的數據,還要改Native層的數據,所以我們要用Native層的Hook框架,這裡用的Hook框架是Dobby。作者編譯了靜態鏈接文件,我們去下載下來用即可://github.com/jmpews/Dobby/releases/tag/latest

下載好後解壓,將android目錄里的子目錄全部放入Riru-ModuleTemplate項目的module模塊下的libs目錄下(沒有則創建),然後將文件夾重命名:

  • arm64 -> arm64-v8a
  • armv7 -> armeabi-v7a

修改後的目錄結構應如下:

同時在module/src/main/cpp目錄下創建一個dobby目錄,然後去下載dobby.h文件,來放入該目錄下。下載地址://github.com/jmpews/Dobby/blob/master/include/dobby.h

修改cmake文件,使編譯的時候,把dobby的靜態鏈接庫也鏈接上。修改module/src/main/cpp/CMakeLists.txt

include_directories(
        dobby
)

add_library(local_dobby STATIC IMPORTED)

set_target_properties(local_dobby PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${ANDROID_ABI}/libdobby.a)

add_library(${MODULE_NAME} SHARED main.cpp ${CMAKE_CURRENT_BINARY_DIR}/config.cpp)
target_link_libraries(${MODULE_NAME} local_dobby log riru::riru)

準備工作完成,就可以開始編寫代碼了

編寫代碼

轉到module/src/main/cpp/main.cpp文件,這是模塊的核心代碼文件,裏面定義一些不太豐滿的函數,等着我們去填補。

static void forkAndSpecializePost(JNIEnv *env, jclass clazz, jint res) {
    // Called "after" com_android_internal_os_Zygote_nativeForkAndSpecialize in frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
    // "res" is the return value of com_android_internal_os_Zygote_nativeForkAndSpecialize

    if (res == 0) {
        // In app process

        // When unload allowed is true, the module will be unloaded (dlclose) by Riru
        // If this modules has hooks installed, DONOT set it to true, or there will be SIGSEGV
        // This value will be automatically reset to false before the "pre" function is called
        riru_set_unload_allowed(true);
    } else {
        // In zygote process
    }
}

static void specializeAppProcessPre(
        JNIEnv *env, jclass clazz, jint *uid, jint *gid, jintArray *gids, jint *runtimeFlags,
        jobjectArray *rlimits, jint *mountExternal, jstring *seInfo, jstring *niceName,
        jboolean *startChildZygote, jstring *instructionSet, jstring *appDataDir,
        jboolean *isTopApp, jobjectArray *pkgDataInfoList, jobjectArray *whitelistedDataInfoList,
        jboolean *bindMountAppDataDirs, jboolean *bindMountAppStorageDirs) {
    // Called "before" com_android_internal_os_Zygote_nativeSpecializeAppProcess in frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
    // Parameters are pointers, you can change the value of them if you want
    // Some parameters are not exist is older Android versions, in this case, they are null or 0
}

static void specializeAppProcessPost(
        JNIEnv *env, jclass clazz) {
    // Called "after" com_android_internal_os_Zygote_nativeSpecializeAppProcess in frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

    // When unload allowed is true, the module will be unloaded (dlclose) by Riru
    // If this modules has hooks installed, DONOT set it to true, or there will be SIGSEGV
    // This value will be automatically reset to false before the "pre" function is called
    riru_set_unload_allowed(true);
}

static void forkSystemServerPre(
        JNIEnv *env, jclass clazz, uid_t *uid, gid_t *gid, jintArray *gids, jint *runtimeFlags,
        jobjectArray *rlimits, jlong *permittedCapabilities, jlong *effectiveCapabilities) {
    // Called "before" com_android_internal_os_Zygote_forkSystemServer in frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
    // Parameters are pointers, you can change the value of them if you want
    // Some parameters are not exist is older Android versions, in this case, they are null or 0
}

static void forkSystemServerPost(JNIEnv *env, jclass clazz, jint res) {
    // Called "after" com_android_internal_os_Zygote_forkSystemServer in frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

    if (res == 0) {
        // In system server process
    } else {
        // In zygote process
    }
}

static void onModuleLoaded() {
    // Called when this library is loaded and "hidden" by Riru (see Riru's hide.cpp)

    // If you want to use threads, start them here rather than the constructors
    // __attribute__((constructor)) or constructors of static variables,
    // or the "hide" will cause SIGSEGV
}

函數名和注釋都非常清晰,雖然函數很多,但是我們只需要在forkAndSpecializePost函數中編寫hook代碼就可以。因為我們要hook代碼運行在App進程中

if (res == 0) {
// In app process

// When unload allowed is true, the module will be unloaded (dlclose) by Riru
// If this modules has hooks installed, DONOT set it to true, or there will be SIGSEGV
// This value will be automatically reset to false before the "pre" function is called
riru_set_unload_allowed(true);
}

當我們編寫Native層代碼的時候,想要獲取當前手機的機型信息,最好的方式是通過調用__system_property_get來獲取,那麼我們可以hook這個函數。但在這之前,我們需要知道要修改哪個字段,這些字段又分別賦予什麼值。那這些信息通過讀取文件來得到好了,由模塊使用者來定義,編寫一個讀取配置的函數:

void readPropFile(const char* filename,std::map<std::string,std::string> &map){
    FILE *fp = fopen(filename,"r");
    if(fp == NULL){
        return;
    }
    char buf[256];

    while(fgets(buf,256,fp) != NULL){
        char *sep = strchr(buf,'=');
        *sep = '\0';
        std::string key(buf);
        std::string value(sep + 1);
        map[key] = value;
    }
    fclose(fp);
}

readPropFile函數讀取一個文件,然後將每一行等號前面的內容作為key,等號後面的內容作為value放入傳進來的map,假設一個文件內容如下:

MODEL=Mi5

那麼它讀到的key是MODEL,value是Mi5

知道要修改成什麼,就可以hook __system_property_get函數,下面是hook代碼

static int my__system_property_get(const char* name, char* value){
    if(NULL == name || NULL == value){
        return origin__system_property_get(name,value);
    }
    auto ret = propMap.find(name);
    if(ret != propMap.end()){

        const char* valueChs = ret->second.c_str();
        strcpy(value,valueChs);
        return strlen(valueChs);
    }
    return origin__system_property_get(name,value);
}

void initDobby(){
    void* sym = DobbySymbolResolver(NULL,"__system_property_get");
    if(NULL != sym) {
        DobbyHook(sym, (void *) my__system_property_get, (void **) &origin__system_property_get);
    }
}

其中:

  • DobbySymbolResolver 是尋找函數符號地址
  • DobbyHook 是啟用hook的函數
  • my__system_property_get 是跳板函數,就是原函數被調用時會調用的函數
  • origin__system_property_get 是原函數的地址

Hook代碼編寫完成後,在forkAndSpecializePost函數中調用initDobby函數開始我們的hook

static void forkAndSpecializePost(JNIEnv *env, jclass clazz, jint res) {
    // Called "after" com_android_internal_os_Zygote_nativeForkAndSpecialize in frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
    // "res" is the return value of com_android_internal_os_Zygote_nativeForkAndSpecialize
        LOGE("forkAndSpecializePost");

    if (res == 0) {
        // In app process
        readPropFile(PROP_CONF_PATH,propMap);
        initDobby();
        // When unload allowed is true, the module will be unloaded (dlclose) by Riru
        // If this modules has hooks installed, DONOT set it to true, or there will be SIGSEGV
        // This value will be automatically reset to false before the "pre" function is called
        riru_set_unload_allowed(false);
    } else {
        // In zygote process
    }
}

注意:riru_set_unload_allowed(false);這一行,注釋明確寫道如果要進行hook必須設為false,不然會崩潰,事實也確實如此。

hook了native層的函數,實現修改層的機型數據,還需要修改Java層的數據,Java層的機型信息我們只要修改android.os.Build類的字段。而這些字段不用Hook,直接設置即可。

static void setBuild(JNIEnv* env){
    jclass BuildClass = env->FindClass("android/os/Build");
    std::map<std::string,std::string>::iterator it;
    for(it = buildMap.begin();it != buildMap.end();it++){
        jstring key = env->NewStringUTF(it->first.c_str());
        jstring value = env->NewStringUTF(it->second.c_str());
        jfieldID buildField = env->GetStaticFieldID(BuildClass,it->first.c_str(),"Ljava/lang/String;");
        if(env->ExceptionCheck()){
            env->ExceptionClear();
            continue;
        }
        env->SetStaticObjectField(BuildClass,buildField,value);
        if(env->ExceptionCheck()){
            env->ExceptionClear();
        }
    }
}

注意:在編寫代碼時要十分注意,如果代碼崩潰,會導致手機無法開機。

這樣我們的代碼基本寫完了

配置模塊信息

上文我們提到過,我們編寫的模塊雖然是依賴riru模塊,但是它本質也是Magisk模塊。那麼它也會有它的名字,版本,說明,作者信息等,然而這些是由我們開發者去配置的

這些配置信息可以在Riru-ModuleTemplate項目的module.gradle中配置,這個文件的模板在module.example.gradle,可以將module.example.gradle重命名為module.gradle然後進一步修改

ext {
    /*
       This name will be used in the name of the so file ("lib${moduleLibraryName}.so").
    */
    moduleLibraryName = "template"

    /* Minimal supported Riru API version, used in the version check of riru.sh */
    moduleMinRiruApiVersion = 24

    /* The version name of minimal supported Riru, used in the version check of riru.sh */
    moduleMinRiruVersionName = "v24.0.0"

    /* Maximum supported Riru API version, used in the version check of riru.sh */
    moduleRiruApiVersion = 26

    /*
       Magisk module ID
       Since Magisk use it to distinguish different modules, you should never change it.

       Note, the older version of the template uses '-' instead of '_', if your are upgrading from
       the older version, please pay attention.
    */
    magiskModuleId = "riru_template"

    moduleName = "Template"
    moduleAuthor = "Template"
    moduleDescription = "Riru module template. Requires Riru $moduleMinRiruVersionName or above."
    moduleVersion = "v26.0.0"
    moduleVersionCode = 26
}

編譯

Riru-ModuleTemplate項目編譯需要Jdk11以上,裝好後在項目的根目錄執行:

gradlew :module:assembleRelease

如果沒有錯誤,就會在項目的out目錄生成一個zip文件,這個就是我們編譯好的magisk模塊

刷入模塊

在手機上,安裝Magisk和riru模塊,安裝後,將編譯好的模塊zip文件推入手機,在MagiskManager中安裝我們的模塊:

效果

安裝後,重啟手機。打開MagiskManager看到我們的模塊已正常啟用:

去AIDA看看效果:

機型已經被我們修改,說明改機生效了。最後附上我寫的改機模塊://github.com/luoyesiqiu/Riru-gaiji

Tags: