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