天天看點

Cydia Substrate之hook native代碼

繼上次的Cydia  Substrate  hook  java層,這裡我将去hook  native層的代碼,也即是C/C++代碼。

我在網上找了很多資料,發現關于利用cydia hook native的文章沒幾篇,基本來來去去都是那幾個大同小異的,都是介紹如何去hook  dvm加載dex的,估計也就是同一文章而已,隻是被互相“借鑒”了。

幸好,我的英語還行,google了一把,發現有個外國小哥寫得還挺好的,于是,就“借鑒”了一下~

這裡,我并不是hook dvm的内容,而是自己寫的一個小程式裡的一個函數。

So,首先我們需要利用NDK建立一個要被hook的目标程式。

這裡,需要一定的NDK開發知識,即使不懂,下面也會舉個小例子簡單介紹下NDK的開發,by the way,開發環境是Android Studio。

建立目标程式

1. 建立一個帶MainActivity的工程,在該類内,添加加載so庫的static塊,添加native修飾的函數,然後在onCreate中利用Toast彈窗,調用native 函數,例子如下

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toast.makeText(this, hello(), Toast.LENGTH_SHORT).show();
    }

    static{
        System.loadLibrary("hello");
    }

    public static native String hello();

}
           

2. 在app/src/main目錄下建立名為“jni”的檔案夾,并建立hello.cpp檔案,代碼很簡單, 隻是調用一個createHello函數傳回一個字元串“hello world”:

#include <jni.h>  
#include <stdlib.h>  

extern "C"{

char* createHello(){
    return "hello world";
}

jstring Java_com_samuelzhan_hello_MainActivity_hello
        (JNIEnv *env, jobject obj, jstring str)
{
    return env->NewStringUTF(createHello());
}

}
           

注意,這裡我用extern "C"将上面兩個函數括起來,是因為我建立的是cpp檔案,編譯器用的是C++的,C++編譯時,為了函數的重載而在編譯後會改變函數名,會導緻後面寫的hook子產品不能根據原來的名字找到函數的位址。下面會有詳細解釋,先跳過這裡,繼續下面的步驟。

3. 在jni檔案夾下建立一個檔案,命名為Android.mk,編寫如下代碼,與上面建立的cpp檔案在同一目錄下

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.cpp
LOCAL_LDLIBS = -llog

include $(BUILD_SHARED_LIBRARY)
           

4. 配置三個檔案:

   首先,在項目的gradle.properties檔案的最下面添加一行代碼

android.useDeprecatedNdk=true
           

   接着,在項目的local.properties檔案的最下面添加NDK路徑(實際上SDK的路徑已經在裡面了)

ndk.dir=D\:\\android-ndk32-r10b-windows-x86\\android-ndk-r10b
           

   最後,在app檔案夾下的build.gradle下編輯兩處地方

    一是,在defaultConfig下,添加ndk配置(moduleName就是so庫的名字);  二是, 在android下,添加sourceSets(下一步ndk-build後會生成該路徑),然後點選同步重新整理

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        applicationId "com.samuelzhan.hello"
        minSdkVersion 8
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        ndk{
            moduleName "hello"
            abiFilters "armeabi", "armeabi-v7a", "x86"
        }
    }
    sourceSets{
        main{
            jni.srcDirs = []
            jniLibs.srcDirs = ['src/main/libs']
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
}
           

5. NDK編譯

Alt+F12進入Android Studio的終端模式,cd指令到第二步建立的jni檔案夾内,輸入ndk-build  APP_ABI="armeabi",回車執行。(需要将ndk路徑添加到環境變量)

接着,重新整理工程結構,發現會在app/src/main下多出兩個檔案夾libs和obj,libs裡就存放着so庫。

6. APK打包,安裝到手機上運作。

運作結果,通過本地調用,從C代碼中傳回字元串“hello world”,通過Toast彈出:

Cydia Substrate之hook native代碼

目标小程式已寫好,那麼接下來就是hook它了,将hello world的内容改變(在C代碼裡)。

下載下傳安裝Cydia Substrate架構

架構下載下傳com.saurik.substrate.apk

Cydia Substrate SDK

注意,手機需root,而且android系統在4.4以下

編寫Cydia Substrate子產品

這裡我将編寫一個子產品去hook 上面小程式中的createHello函數,修改傳回值。

和上面的小程式差不多,編寫Substrate子產品也是一個NDK開發的過程:

1. 建立一個沒有Activity的空工程, 并添加Cydia Substrate SDK:

在app/src/main下建立jni檔案夾,并加入三個檔案(兩個so庫和一個頭檔案):

Cydia Substrate之hook native代碼

2.  在jni檔案夾下建立cpp檔案,名字需雙字尾,一定得帶 .cy 字尾,如上面的 module.cy.cpp檔案:

#include "substrate.h"  
#include <android/log.h>  
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "zz", __VA_ARGS__)

//配置,這裡是hook可運作程式(即NDK小程式)的寫法,下面那個就是hook dvm的寫法  
MSConfig(MSFilterExecutable,"/system/bin/app_process")
//MSConfig(MSFilterLibrary, "libdvm.so");  

//舊的函數位址,目的為了保留指向原來函數的入口,在新的函數執行  
//完後,一般會再調用該函數,以確定程式的正常運作  
char* (* hello)(void);

//新的函數,替代hook的函數,傳回修改後的值  
char* newHello(void)
{
    //直接傳回新的字元串
    return "fuck the world";
    //執行原函數,確定程式運作正常,但這裡代碼簡單,可以直接傳回字元串即可  
    //return hello();  
}

//通過so庫的絕對路徑和函數名,找到其函數的映射位址  
void* lookup_symbol(char* libraryname,char* symbolname)
{
    //擷取so庫的句柄
    void *handle = dlopen(libraryname, RTLD_GLOBAL | RTLD_NOW);
    if (handle != NULL){
        //根據so庫句柄和符号名(即函數名)擷取函數位址
        void * symbol = dlsym(handle, symbolname);
        if (symbol != NULL){
            return symbol;
        }else{
            LOGD("dl error: %s", dlerror());
            return NULL;
        }
    }else{
        return NULL;
    }
}

MSInitialize
{
    //擷取hook函數的位址,最好不要用下面MS提供的方法
    void * symbol = lookup_symbol("/data/data/com.samuelzhan.hello/lib/libhello.so","createHello");
//    MSImageRef  image=MSGetImageByName("/data/data/com.samuelzhan.hello/lib/libhello.so");
//    void *symbol=MSFindSymbol(image, "createHello");

    //這裡将舊函數的入口(參數一)指向hello(參數三),然後執行新函數(參數二)  
    MSHookFunction(symbol, (void*)&newHello, (void**)&hello);
}  
           

3. 在jni檔案夾下建立Android.mk檔案,将substrate提供的兩個so庫也加進去一起編譯:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE:= substrate-dvm
LOCAL_SRC_FILES := libsubstrate-dvm.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE:= substrate
LOCAL_SRC_FILES := libsubstrate.so
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE    := module.cy #生成的子產品名
LOCAL_SRC_FILES := module.cy.cpp #源檔案名
LOCAL_LDLIBS = -llog
LOCAL_LDLIBS += -L$(LOCAL_PATH) -lsubstrate-dvm -lsubstrate
include $(BUILD_SHARED_LIBRARY)
           

4. 配置三個檔案,gradle.properties和local.properties的設定和上面編寫目标程式一樣,各添加一句代碼即可, 至于build.gradle基本也一樣,隻不過moduleName改為 “module.cy”,以及加上幾個庫。

gradle.properties:

android.useDeprecatedNdk=true
           

local.properties:

ndk.dir=D\:\\android-ndk32-r10b-windows-x86\\android-ndk-r10b
           

build.gradle:

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        applicationId "com.samuelzhan.cydiahookjni"
        minSdkVersion 8
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        ndk{
            moduleName "module.cy"
            ldLibs "substrate"
            ldLibs "substrate-dvm"
            ldLibs "log"
        }
    }

    sourceSets{
        main{
            jni.srcDirs = []
            jniLibs.srcDirs=['src/main/libs']
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
}
           

5. NDK編譯,和目标程式一樣,cd到jni檔案夾,終端裡輸入指令ndk-build,回車。

6. 配置AndroidManifest.xml檔案,添加installLocation,hasCode, uses-permission:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.samuelzhan.cydiahookjni"
    android:installLocation="internalOnly">

    <uses-permission android:name="cydia.permission.SUBSTRATE"/>
    <application android:hasCode="false">

    </application>

</manifest>
           

7.打包,安裝到手機,并在cydia substrate架構彈出來的消息框選擇軟重新開機。

好了,這裡看看重新開機後,是否能成功hook到createHello這個函數,并修改其傳回内容呢?

Cydia Substrate之hook native代碼

到此,已經成功hook到native代碼的函數并修改。下面将針對期間extern “C”的問題進行詳細說明

-----------------------------------特殊問題補充----------------------------------

在上面的Hello小程式中(目标程式),在編寫cpp檔案時,有一個extern "C"的代碼塊,這個extern "C"就是告訴C++編譯器用C的規範去編譯這個代碼塊的内容。

那麼,兩者編譯後的函數有什麼差別呢?看看下面兩幅圖:

當 extern "C" 的範圍包括 createHello函數時,ndk-build後,利用IDA PRO打開libhello.so庫:

Cydia Substrate之hook native代碼

當 extern "C" 的範圍不包括 createHello函數時,ndk-build後,利用IDA PRO打開libhello.so庫:

Cydia Substrate之hook native代碼

實際上,C++為了實作函數重載,編譯器會在編譯後修改函數的名字,在原函數名字加上字首和字尾,而C編譯器則不會。

這就容易在cydia substrate子產品中查找函數位址lookup_symbol調用 void* dlsym(void* handle, const char* symbolName)造成異常情況。

比如,extern “C” 沒有包括createHello這個函數,那麼我在  dlsym()函數中傳入的函數名“createHello”就沒有太大意義了,因為C++編譯器已經修改過函數名,這會抛出“symbol not defined”的錯誤。

當然,你可以通過IDA PRO檢視到修改過的函數名,将修改過的函數名傳入到 dlsym()函數也是可以找到函數位址的。

由于許多公司對APP的安全性越來越重視,是以很多公司的核心業務處理子產品一般會采用NDK開發,通過jni機制調用C代碼來實作子產品功能。這種用C/C++開發出來的代碼反編譯分析的難度遠遠大于java開發,因為C/C++反編譯過來的彙編語言可讀性很差,這給反編譯人員帶來很大的挑戰。

然而對于我這種初入逆向分析的菜鳥來說,每次看到native修飾的方法都覺得心裡有道坎,盡管如此,我還是想在裡面搞些文章,希望能分析出點成果~