之前我們演示過了如何在Java層Hook系統的API方法,但是我們都知道很多安全級別較高的操作我們都不會在Java層來完成,而且Java層很多的API都是通過JNI的方式在Native層完成的,所以對Java層的API方法Hook意義不是很大。本節我們就具體來說說在Android中如何使用CydiaSubstrate框架完成Native層的Hook操作。
#### **CydiaSubstrate框架針對Native層Hook的支持**
對于CydiaSubstrate框架來說,其給我們提供了類似在Java中的API方法,如在Native層的MSJavaHookClassLoad函數(類似Java中的hookClassLoad方法)、MSJavaHookMethod函數(類似Java中的hookMethod)。作者的意圖就是為了讓我們能夠在Native層使用JNI完成Java函數的Hook。其中兩個函數的具體定義如下:
~~~
/**
* 通過JNI Hook Java中的ClassLoad
*
* @jni jni指針
* @name 待Hook的類,字符串形式
* @callback Hook后的回調
* @data 自定義參數數據
*/
voidMSJavaHookClassLoad(JNIEnv *jni, constchar*name, void(*callback)(JNIEnv *, jclass, void *), void *data);
/**
* 通過JNI Hook Java中的指定方法
*
* @jni jni指針
* @_class jclass
* @methodId 待Hook方法ID
* @hook Hook后待替換的函數
* @old Hook前原函數的指針
*/
voidMSJavaHookMethod(JNIEnv *jni, jclass _class, jmethodID methodId, void *hook, void **old);
~~~
上述的兩個函數確實比較有用,但是卻不是我們最想要的結果。在Native層Hook我們還是希望針對原生函數進行Hook操作。其實針對Native層的Hook原理,我們在本章的開頭已經給各位讀者介紹了。CydiaSubstrate只是針對其做了一個良好的封裝操作,讓我們更方便地使用。下面是CydiaSubstrate框架提供的Hook函數方法。
~~~
/** 根據具體的地址路徑加載動態庫
* 類似于dlopen
*
* @return 動態庫ImageRef
*/
MSImageRef MSGetImageByName(constchar *file);
/**
* 根據指定庫找到其中函數的偏移量
* 類似于dlsym
*
* @image 指定的動態庫
* @name 指定函數的名稱
* @return 指定函數的指針(兼容ARM/Thumb)找不到返回NULL
*/
void *MSFindSymbol(MSImageRef image, constchar *name);
/**
* Hook Native層中的指定函數
*
* @symbol 待Hook函數指針
* @hook Hook后待替換的函數指針
* @old Hook前函數指針
*/
voidMSHookFunction(void *symbol, void *hook, void **old);
~~~
看到上面的函數說明估計讀者們都躍躍欲試了,而且相信很多讀者已經能夠猜出如何使用CydiaSubstrate框架了。下面我們還是詳細地說明一下,除了了解其提供的API函數之外,使用CydiaSubstrate框架還需要注意的一些注意事項。
* 基于CydiaSubstrate框架的動態庫必須以“.cy.so”(系統默認是“.so”)作為后綴。
* 在AndroidManifest.xml文件中需要聲明cydia.permission.SUBSTRATE權限。
* 需要在Native C/CPP方法前添加Config配置。
~~~
MSConfig(MSFilterExecutable, "/system/bin/app_process")
~~~
#### **通過JNI改變系統顏色**
與之前的嘗試Hook系統API小節一樣,如我們希望完成Hook Android系統中的Resources類,并將系統中的顏色都改為紫羅蘭色。思路也是一樣的,我們只需要拿到系統中Resources類的getColor方法,將其返回值做修改即可。那么我們使用原生方法實現,需要完成以下幾個步驟。
**1.在AndroidManifest.xml中聲明權限與安裝方式**
因為是系統組件代碼,我們需要設置其安裝方式是internalOnly與hasCode=“false”,這樣能夠方便CydiaSubstrate框架獲取我們的邏輯。當然還需要聲明SUBSTRATE權限,具體的操作如下AndroidManifest.xml內容所示。
~~~
<?xml version="1.0" encoding="utf-8"?>
<!-- internalOnly 系統內部安裝,禁止安裝到sd卡 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.hooknative"
android:installLocation="internalOnly"
android:versionCode="1"
android:versionName="1.0" >
<!-- 聲明Substrate權限 -->
<uses-permission android:name="cydia.permission.SUBSTRATE" />
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="21" />
<!-- hasCode=false,系統組件,不運行APP中的邏輯 -->
<application android:hasCode="false" >
</application>
</manifest>
~~~
**2.新創建項目的cpp文件,導入所需的庫**
這里我們新創建一個原生代碼文件HookNative.cy.cpp(后綴必須為.cy.cpp,編譯后則會出現.cy.so文件),并將CydiaSubstrate的庫文件libsubstrate.so、libsubstrate-dvm.so、substrate.h一起復制到jni目錄下(這里需要根據不同平臺選擇,我們這里選擇的是ARM平臺的庫),jni目錄如圖8-20所示。

當然,我們還需要編寫Makefile文件Android.mk,指定Substrate庫參與編譯,并引入一些必要的庫。內容如下所示。
~~~
LOCAL_PATH := $(call my-dir)
# substrate-dvm 庫
include $(CLEAR_VARS)
LOCAL_MODULE:= substrate-dvm
LOCAL_SRC_FILES := libsubstrate-dvm.so
include $(PREBUILT_SHARED_LIBRARY)
# substrate 庫
include $(CLEAR_VARS)
LOCAL_MODULE:= substrate
LOCAL_SRC_FILES := libsubstrate.so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := HookNative.cy
LOCAL_SRC_FILES := HookNative.cy.cpp
LOCAL_LDLIBS+= -L$(SYSROOT)/usr/lib -llog
LOCAL_LDLIBS+= -L$(LOCAL_PATH) -lsubstrate-dvm -lsubstrate
include $(BUILD_SHARED_LIBRARY)
~~~
**3.載入配置文件與CydiaSubstrate入口**
在HookNative.cy.cpp代碼文件中,使用CydiaSubstrate框架的API,還需要在其中聲明一些東西,如MSConfig配置app_process的路徑,聲明MSInitialize作為一個CydiaSubstrate插件的入口。我們還會應用一些開發中必要的頭文件與LOG聲明,如這里我們的HookNative.cy.cpp內容為:
~~~
#include<android/log.h>
#include<substrate.h>
#define LOG_TAG "native_hook"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 載入配置文件
MSConfig(MSFilterExecutable, "/system/bin/app_process")
// Cydia初始化入口
MSInitialize {
}
~~~
**4.Hook并替換其方法**
其修改方法與上一節的Hook Java中的方法類似,我們只需要修改相關的函數完成Hook即可。如這里我們使用MSJavaHookClassLoad方法Hook系統的Resources類,并使用MSJavaHookMethod方法Hook其getColor方法,替換其方法。具體實現如下所示。
~~~
#include<android/log.h>
#include<substrate.h>
#define LOG_TAG "native_hook"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 載入配置文件
MSConfig(MSFilterExecutable, "/system/bin/app_process")
// getColor方法Hook前原函數指針
static jint (*_Resources$getColor)(JNIEnv *jni, jobject _this, ...);
// getColor方法Hook后被替換的函數
static jint $Resources$getColor(JNIEnv *jni, jobject _this, jint rid) {
jint color = _Resources$getColor(jni, _this, rid);
returncolor & ~0x0000ff00 | 0x00ff0000;
}
// Hook住Resources class的回調
static void OnResources(JNIEnv *jni, jclass resources, void*data) {
// hook其對應的getColor方法
jmethodID method = jni->GetMethodID(resources, "getColor", "(I)I");
if(method != NULL)
MSJavaHookMethod(jni, resources, method,
&$Resources$getColor, &_Resources$getColor);
}
// Cydia初始化入口
MSInitialize {
// Hook Java中的Resources
MSJavaHookClassLoad(NULL, "android/content/res/Resources", &OnResources);
}
~~~
**5.編譯、安裝、重啟驗證**
同樣,因為CydiaSubstrate是Hook Zygote進程,而且我們Hook的又是系統的Resources方法,所以我們希望驗證都需要重啟一下設備。我們可以選擇CydiaSubstrate中的軟重啟,這里我們對系統的設置頁面Hook前后都做了一個截圖,對比截圖如圖8-21所示。
:-: 

本例中我們繼續之前Java中Hook的思想,完成了在原生代碼中使用JNI針對Java中的API進行Hook操作。因為CydiaSubstrate框架中的hookClassLoad方法、hookMethod方法底層實現也是如此,所以我們使用起來很類似。
#### **Hook后替換指定應用中的原生方法**
討論了太久的Java層面的API Hook工作,也舉了很多例子,本節中我們就看看如何使用CydiaSubstrate框架完成原生函數的Hook。
例如,現在我們有一個應用程序(包名為:com.example. testndklib),其主要功能就是按下界面上的“test”按鈕后,通過JNI調用Native的test函數,在系統的Log中輸入一個當前我有多少錢的整數值。界面如圖8-22所示。
:-: 
使用JNI調用的test函數,寫在NDK庫testNDKlib中,會調用一個名叫getMoney的函數,顯示我當前有多少錢。當然,這里我們直接硬編碼了,返回值為100的整數。中間,我們還將 getMoney 函數的地址通過 Log 打印出來。testNDKlib.cpp內容如下所示。
~~~
#include<stdio.h>
#include<jni.h>
#include<android/log.h>
extern"C" {
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "cydia_native", __VA_ARGS__)
/**
* 測試函數getMoney,返回一個整數
*/
int getMoney(void) {
// 打印方法函數地址
LOGI(" getMoney() function address in : %p\n", &getMoney);
return100;
}
// 一個JNI的test函數
jstring Java_com_example_testndklib_MainActivity_test(JNIEnv* env,
jobject thiz) {
LOGI(" I have %d money.\n", getMoney());
return0;
}
}
~~~
運行一下程序,單擊“test”按鈕,拿到了系統輸出的 Log,與我們輸出的預期一樣。筆者將DDMS上輸出的Log截圖,如圖8-23所示。
:-: 
現在我們希望Hook此so文件,找到其中的getMoney函數,替換它讓它給我們返回整數值999999(類似一個游戲修改金幣的外掛)。針對之前我們討論的Hook的原理,我們需要做如下幾步操作。
1. 加載原生庫,libtestNDKlib.so。
2. 找到對應的函數符號地址,MSFindSymbol。
3. 替換函數,MSHookFunction。
這里我們在完成1、2步驟的時候,我們同時也用dlopen與dlsym方式實現給大家演示一下。之前的環境配置邏輯以及權限聲明邏輯與上一個例子類似,這里我們不做贅述,直接看一下cpp文件中的內容,具體如下:
~~~
#include<android/log.h>
#include<substrate.h>
#include<stdio.h>
#define LOG_TAG "cydia_native"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 初始化CydiaSubstrate
MSConfig(MSFilterExecutable, "/system/bin/app_process")
// 原函數指針
int (*original_getMoney)(void);
/**
* 替換后的函數
*/
int replaced_getMoney(void)
{
LOGI(" replaced_getMoney() function address in : %p\n", &replaced_getMoney);
return999999;
}
/**
*
* 找到指定鏈接庫中的函數地址
*
* @libraryname 鏈接庫地址
* @symbolname 函數名
* @return 對應的函數地址
*/
void* lookup_symbol(char* libraryname, char* symbolname)
{
// dlopen打開指定庫,獲得句柄
void*imagehandle = dlopen(libraryname, RTLD_GLOBAL | RTLD_NOW);
if(imagehandle != NULL) {
// 獲得具體函數
void* sym = dlsym(imagehandle, symbolname);
if(sym != NULL) {
returnsym;
} else{
LOGI("(lookup_symbol) dlsym didn't work");
returnNULL;
}
} else{
LOGI("(lookup_symbol) dlerror: %s", dlerror());
returnNULL;
}
}
//初始化
MSInitialize
{
// 獲得libtestNDKlib.so動態庫中getMoney函數的地址
MSImageRef image;
image = MSGetImageByName(
"/data/data/com.example.testndklib/lib/libtestNDKlib.so");
void* getAgeSym = MSFindSymbol(image, "getMoney");
// MSImageRef與 MSFindSymbol 也可以寫為如下所示找到getMoney函數的地址
//
// void * getAgeSym = lookup_symbol(
// "/data/data/com.example.testndklib/lib/libtestNDKlib.so",
// "getMoney");
// 將getMoney函數替換為 replaced_getMoney函數
MSHookFunction(getAgeSym, (void*) &replaced_getMoney,
(void**) &original_getMoney);
}
~~~
編譯后安裝到已經安裝了CydiaSubstrate框架的系統中,重啟Android設備。如果在整個系統編譯與配置沒有什么錯誤的情況下,我們發現CydiaSubstrate框架會打出Log說Loding什么什么 so 文件了。這里我們看見,LodinglibnativeHook.cy.so 說明我們之前開發的 Hook 其中的getMoney方法已經生效了,如圖8-24所示。
:-: 
這個時候我們繼續運行程序,進入我們剛才的test應用程序。單擊“test”按鈕,獲取調用JNI中的test函數打印我有多少錢。我們能夠在DDMS中清楚地看到,getMoney函數已經被一個名為“replace_getMoney”的函數替換了,其地址也已經被替換了。我們也看到使用替換后的值輸出為“I have 999999 money”,如圖8-25所示
:-: 
#### **使用Hook進行廣告攔截**
對于Android操作系統我們知道,Java層都是建立在原生C/C++語言上的,特別是針對一些系統級別的API函數。上面我們演示了如何對用戶自定義函數進行Hook,下面我們演示一下如何對Native層的系統API進行Hook。
如這里我們希望對系統中的網絡請求API進行Hook,然后過濾掉一些廣告相關的請求,完成廣告攔截功能,其具體的流程如圖8-26所示。
:-: 
這里我們查看Android操作系統的POSIX定義的源碼Poxis.Java文件,發現其針對網絡請求函數的定義是在native完成的,如圖8-27所示。
:-: 
對于此類的問題,我們就不得不在native完成Hook與函數替換工作了。所以我們還是使用CydiaSubstrate框架,對系統的API函數connect進行Hook與替換。如下代碼所示,我們使用MSHookFunction對native層中的connect函數進行Hook替換至newConnect函數。
~~~
#define LOG_TAG "cydia_native"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 初始化CydiaSubstrate
MSConfig(MSFilterExecutable, "/system/bin/app_process")
// 原connect函數指針
int *(*oldConnect)(int,const sockaddr *, socklen_t);
int *newConnect(int socket, const sockaddr *address, socklen_t length) {
char ip[128] = { 0 };
int port = -1;
if(address->sa_family == AF_INET) {
sockaddr_in *address_in = (sockaddr_in*) address;
// 獲取 ip
inet_ntop(AF_INET, (void*) (structsockaddr*) &address_in->sin_addr, ip,128);
// 獲取端口
port = ntohs(address_in->sin_port);
// 過濾掉172.22.156.129的請求
if(strcmp(ip, "172.22.156.129") == 0) {
LOGI("發現廣告請求");
struct sockaddr_in my_addr;
int my_len = sizeof(struct sockaddr_in);
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(80);
my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
return oldConnect(socket, (const sockaddr*) &my_addr, sizeof(my_addr));
}
return oldConnect(socket, address, length);
}
}
//初始化
MSInitialize{
MSHookFunction((void*) &connect, (void*) &newConnect,(void**) &oldConnect);
}
~~~
這里我們只是簡單地將IP為172.22.156.129的請求重定向到本地127.0.0.1,即讓類似的廣告請求拿不到數據。此類方式的廣告過濾屬于比較原始暴力類型的過濾方法,但卻也簡單有效。熟悉connect的讀者應該會發現,如果已經能夠替換掉connect函數,其實我們能做到的事情遠遠比攔截一個廣告請求大得多,比如跳轉至釣魚網站,收集私密發送數據等。
#### **參考文章:**
[《Android安全技術揭秘與防范》—第8章8.4節Hook原生應用程序](https://yq.aliyun.com/articles/99873?spm=5176.100239.blogcont99909.21.6fd61614MywzAl#)
- 前言
- Android系統的體系結構
- Dalvik VM 和 JVM 的比較
- Android 打包應用程序并安裝的過程
- Android ADB工具
- Android應用開發
- Android UI相關知識總結
- Android 中window 、view、 Activity的關系
- Android應用界面
- Android中的drawable和bitmap
- AndroidUI組件adapterView及其子類和Adapter的關系
- Android四大組件
- Android 數據存儲
- SharedPreference
- Android應用的資源
- 數組資源
- 使用Drawable資源
- Material Design
- Android 進程和線程
- 進程
- 線程
- Android Application類的介紹
- 意圖(Intent)
- Intent 和 Intent 過濾器(Google官網介紹)
- Android中關于任務棧的總結
- 任務和返回棧(官網譯文)
- 總結
- Android應用安全現狀與解決方案
- Android 安全開發
- HTTPS
- 安卓 代碼混淆與打包
- 動態注入技術(hook技術)
- 一、什么是hook技術
- 二、常用的Hook 工具
- Xposed源碼剖析——概述
- Xposed源碼剖析——app_process作用詳解
- Xposed源碼剖析——Xposed初始化
- Xposed源碼剖析——hook具體實現
- 無需Root也能Hook?——Depoxsed框架演示
- 三、HookAndroid應用
- 四、Hook原生應用程序
- 五、Hook 檢測/修復
- Android 應用的逆向與加固保護技術
- OpenCV在Android中的開發
- Android高級開發進階
- 高級UI
- UI繪制流程及原理
- Android新布局ConstraintLayout約束布局
- 關鍵幀動畫
- 幀動畫共享元素變換
- Android異步消息處理機制完全解析,帶你從源碼的角度徹底理解
- Android中為什么主線程不會因為Looper.loop()里的死循環卡死?
- 為什么 Android 要采用 Binder 作為 IPC 機制?
- JVM 中一個線程的 Java 棧和寄存器中分別放的是什么?
- Android源碼的Binder權限是如何控制?
- 如何詳解 Activity 的生命周期?
- 為什么Android的Handler采用管道而不使用Binder?
- ThreadLocal,你真的懂了嗎?
- Android屏幕刷新機制