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彈出:


目標小程序已寫好,那麼接下來就是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庫和一個頭文件):



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這個函數,並修改其返回內容呢?


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



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

在上面的Hello小程序中(目標程序),在編寫cpp文件時,有一個extern "C"的代碼塊,這個extern "C"就是告訴C++編譯器用C的規範去編譯這個代碼塊的內容。

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

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


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


實際上,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修飾的方法都覺得心裏有道坎,儘管如此,我還是想在裏面搞些文章,希望能分析出點成果~


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章