Android中通過程式注入技術修改系統返回的Mac地址

姜維的部落格發表於2014-12-19

問題

Android中通過注入技術修改系統返回的Mac地址

技術準備

下面來看一下這個技術需要哪些知識點

1、如何將非native方法變成native方法

2、如何將native方法直接註冊(不需要jni這樣的標頭檔案了)

3、Android中的類載入器相關知識

4、如何編譯Android系統引用系統標頭檔案的NDK專案

雖然這裡有這四個知識點,但是其中有兩個我在之前的blog中已經介紹了:

Android中的類載入器:http://blog.csdn.net/jiangwei0910410003/article/details/41384667

如何編譯Android系統引用系統標頭檔案的NDK專案:http://blog.csdn.net/jiangwei0910410003/article/details/40949475

不過在這篇文章中,我們在介紹一種新的編譯方式

專案測試

第一、Android專案

package com.example.testar;

import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.Button;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		Button btn = (Button) findViewById(R.id.button1);
		btn.setOnClickListener(new View.OnClickListener() {

			@Override
			public void onClick(View v) {
				WifiManager wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE);
				WifiInfo info = wifi.getConnectionInfo();
				System.out.println("Wifi mac :" + info.getMacAddress());
				Log.d("DEMO", "Wifi mac:"+info.getMacAddress());
			}
		});
	}

}

我們看到,這裡的程式碼很簡單,就是列印一下裝置的Mac地址,現在我們要做的就是:注入這個Demo程式,然後修改Mac的值。

第二、底層的實現

首先來看一下inject.c

這個是注入程式的核心檔案,由於程式碼比較多,這裡只看核心的部分:

int main(int argc, char** argv) {
	char *pn = "com.example.testar";
	char *is = "/data/local/libso.so";
	printf("%s\n",pn);
	printf("%s\n",is);

	pid_t target_pid;
	target_pid = find_pid_of(pn);
	printf("pid: %d\n",target_pid);
	int ret = inject_remote_process(target_pid, is, "InjectInterface", (void*)"I'm parameter!", strlen("I'm parameter!") );
	printf("result: %d\n",ret);
}

就是他的主函式程式碼

我們看到有一個重要的函式:

inject_remote_process

第一個引數:注入程式的id

第二個引數:需要注入到目標程式的so檔案

第三個引數:so檔案中需要執行的函式名

第四個引數:執行函式的引數

第五個引數:執行函式的引數的長度

 

主要還是前面三個引數。

這裡我們通過find_pid_of(pn)函式來獲取程式id

傳遞進去的是程式名

Android中應用的程式名就是包名

char *pn = "com.example.testar";

看到上面注入到目標程式的so檔案

char *is = "/data/local/libso.so";

下面再來看一下這個so檔案的原始碼

so.cpp

#include "jni.h"
#include <android_runtime/AndroidRuntime.h>
#include "android/log.h"
#include "stdio.h"
#include "stdlib.h"
#include "MethodHooker.h"
#include <utils/CallStack.h>
#define log(a,b) __android_log_write(ANDROID_LOG_INFO,a,b); // LOGÀàÐÍ:info
#define log_(b) __android_log_write(ANDROID_LOG_INFO,"JNI_LOG_INFO",b); // LOGÀàÐÍ:info

extern "C" void InjectInterface(char*arg){
	log_("*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*");
	log_("*-*-*-*-*-* Injected so *-*-*-*-*-*-*-*");
	log_("*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*");
	Hook();
	log_("*-*-*-*-*-*-*- End -*-*-*-*-*-*-*-*-*-*");
}

extern "C" JNIEXPORT jstring JNICALL Java_com_example_testar_InjectApplication_test(JNIEnv *env, jclass clazz)
{
    return env->NewStringUTF("haha ");
}

在這個檔案中,我們看到了函式InjectInterface了,因為so是C++程式,但是inject是C程式,為了相容,就有這種方式了

 

extern “C”  函式{

//do something

}

這個程式碼沒什麼難度和複雜性

這個函式中呼叫了Hook函式,下面在來看一下Hook函式的定義

MethodHooker.cpp的實現

#include "MethodHooker.h"
#include "jni.h"
#include "android_runtime/AndroidRuntime.h"
#include "android/log.h"
#include "stdio.h"
#include "stdlib.h"
#include "native.h"
#include <dlfcn.h>
#define ANDROID_SMP 0
#include "Dalvik.h"
#include "alloc/Alloc.h"

#define ALOG(...) __android_log_print(ANDROID_LOG_VERBOSE, __VA_ARGS__)

static bool g_bAttatedT;
static JavaVM *g_JavaVM;

void init()
{
	g_bAttatedT = false;
	g_JavaVM = android::AndroidRuntime::getJavaVM();
}

static JNIEnv *GetEnv()
{
	int status;
	JNIEnv *envnow = NULL;
	status = g_JavaVM->GetEnv((void **)&envnow, JNI_VERSION_1_4);
	if(status < 0)
	{
		status = g_JavaVM->AttachCurrentThread(&envnow, NULL);
		if(status < 0)
		{
			return NULL;
		}
		g_bAttatedT = true;
	}
	return envnow;
}

static void DetachCurrent()
{
	if(g_bAttatedT)
	{
		g_JavaVM->DetachCurrentThread();
	}
}

static int computeJniArgInfo(const DexProto* proto)
{
    const char* sig = dexProtoGetShorty(proto);
    int returnType, jniArgInfo;
    u4 hints;

    /* The first shorty character is the return type. */
    switch (*(sig++)) {
    case 'V':
        returnType = DALVIK_JNI_RETURN_VOID;
        break;
    case 'F':
        returnType = DALVIK_JNI_RETURN_FLOAT;
        break;
    case 'D':
        returnType = DALVIK_JNI_RETURN_DOUBLE;
        break;
    case 'J':
        returnType = DALVIK_JNI_RETURN_S8;
        break;
    case 'Z':
    case 'B':
        returnType = DALVIK_JNI_RETURN_S1;
        break;
    case 'C':
        returnType = DALVIK_JNI_RETURN_U2;
        break;
    case 'S':
        returnType = DALVIK_JNI_RETURN_S2;
        break;
    default:
        returnType = DALVIK_JNI_RETURN_S4;
        break;
    }

    jniArgInfo = returnType << DALVIK_JNI_RETURN_SHIFT;

    hints = dvmPlatformInvokeHints(proto);

    if (hints & DALVIK_JNI_NO_ARG_INFO) {
        jniArgInfo |= DALVIK_JNI_NO_ARG_INFO;
    } else {
        assert((hints & DALVIK_JNI_RETURN_MASK) == 0);
        jniArgInfo |= hints;
    }

    return jniArgInfo;
}

int ClearException(JNIEnv *jenv){
	jthrowable exception = jenv->ExceptionOccurred();
	if (exception != NULL) {
		jenv->ExceptionDescribe();
		jenv->ExceptionClear();
		return true;
	}
	return false;
}

bool isArt(){
	return true;
}

static jclass findAppClass(JNIEnv *jenv,const char *apn){
	jclass clazzApplicationLoaders = jenv->FindClass("android/app/ApplicationLoaders");
	jthrowable exception = jenv->ExceptionOccurred();
	if (ClearException(jenv)) {
		ALOG("Exception","No class : %s", "android/app/ApplicationLoaders");
		return NULL;
	}
	jfieldID fieldApplicationLoaders = jenv->GetStaticFieldID(clazzApplicationLoaders,"gApplicationLoaders","Landroid/app/ApplicationLoaders;");
	if (ClearException(jenv)) {
		ALOG("Exception","No Static Field :%s","gApplicationLoaders");
		return NULL;
	}
	jobject objApplicationLoaders = jenv->GetStaticObjectField(clazzApplicationLoaders,fieldApplicationLoaders);
	if (ClearException(jenv)) {
		ALOG("Exception","GetStaticObjectField is failed [%s","gApplicationLoaders");
		return NULL;
	}
	jfieldID fieldLoaders = jenv->GetFieldID(clazzApplicationLoaders,"mLoaders","Ljava/util/Map;");
	if (ClearException(jenv)) {
		ALOG("Exception","No Field :%s","mLoaders");
		return NULL;
	}
	jobject objLoaders = jenv->GetObjectField(objApplicationLoaders,fieldLoaders);
	if (ClearException(jenv)) {
		ALOG("Exception","No object :%s","mLoaders");
		return NULL;
	}
	jclass clazzHashMap = jenv->GetObjectClass(objLoaders);
	jmethodID methodValues = jenv->GetMethodID(clazzHashMap,"values","()Ljava/util/Collection;");
	jobject values = jenv->CallObjectMethod(objLoaders,methodValues);

	jclass clazzValues = jenv->GetObjectClass(values);
	jmethodID methodToArray = jenv->GetMethodID(clazzValues,"toArray","()[Ljava/lang/Object;");
	if (ClearException(jenv)) {
		ALOG("Exception","No Method:%s","toArray");
		return NULL;
	}

	jobjectArray classLoaders = (jobjectArray)jenv->CallObjectMethod(values,methodToArray);
	if (ClearException(jenv)) {
		ALOG("Exception","CallObjectMethod failed :%s","toArray");
		return NULL;
	}

		int size = jenv->GetArrayLength(classLoaders);

		for(int i = 0 ; i < size ; i ++){
			jobject classLoader = jenv->GetObjectArrayElement(classLoaders,i);
			jclass clazzCL = jenv->GetObjectClass(classLoader);
			jmethodID loadClass = jenv->GetMethodID(clazzCL,"loadClass","(Ljava/lang/String;)Ljava/lang/Class;");
			jstring param = jenv->NewStringUTF(apn);
			jclass tClazz = (jclass)jenv->CallObjectMethod(classLoader,loadClass,param);
			if (ClearException(jenv)) {
				ALOG("Exception","No");
				continue;
			}
			return tClazz;
		}
	ALOG("Exception","No");
	return NULL;
}

bool HookDalvikMethod(jmethodID jmethod){
	Method *method = (Method*)jmethod;
	SET_METHOD_FLAG(method, ACC_NATIVE);

	int argsSize = dvmComputeMethodArgsSize(method);
    if (!dvmIsStaticMethod(method))
        argsSize++;

    method->registersSize = method->insSize = argsSize;

    if (dvmIsNativeMethod(method)) {
        method->nativeFunc = dvmResolveNativeMethod;
        method->jniArgInfo = computeJniArgInfo(&method->prototype);
    }
}

bool ClassMethodHook(HookInfo info){

	JNIEnv *jenv = GetEnv();

	jclass clazzTarget = jenv->FindClass(info.tClazz);
	if (ClearException(jenv)) {
		ALOG("Exception","ClassMethodHook[Can't find class:%s in bootclassloader",info.tClazz);

	    clazzTarget = findAppClass(jenv,info.tClazz);
	    if(clazzTarget == NULL){
	    	ALOG("Exception","%s","Error in findAppClass");
	    	return false;
	    }
	}

	jmethodID method = jenv->GetMethodID(clazzTarget,info.tMethod,info.tMeihodSig);
	if(method==NULL){
		ALOG("Exception","ClassMethodHook[Can't find method:%s",info.tMethod);
		return false;
	}

	/*
	if(isArt()){
		HookArtMethod(jenv,method);
	}else{
		HookDalvikMethod(method);
	}
	*/

	HookDalvikMethod(method);

    JNINativeMethod gMethod[] = {
        {info.tMethod, info.tMeihodSig, info.handleFunc},
    };

    if(info.handleFunc != NULL){
		if (jenv->RegisterNatives(clazzTarget, gMethod, 1) < 0) {
			ALOG("RegisterNatives","err");
			return false;
		}
    }

	DetachCurrent();
	return true;
}

int Hook(){
	init();
	void* handle = dlopen("/data/local/libTest.so",RTLD_NOW);
	const char *dlopen_error = dlerror();
	if(!handle){
		ALOG("Error","cannt load plugin :%s",dlopen_error);
		return -1;
	}
	SetupFunc setup = (SetupFunc)dlsym(handle,"getpHookInfo");
	const char *dlsym_error = dlerror();
	if (dlsym_error) {
		ALOG("Error","Cannot load symbol 'getpHookInfo' :%s" , dlsym_error);
		dlclose(handle);
		return 1;
	}

	HookInfo *hookInfo;
	setup(&hookInfo);

	ALOG("LOG","Target Class:%s",hookInfo[0].tClazz);
	ALOG("LOG","Target Method:%s",hookInfo[0].tMethod);

	ClassMethodHook(hookInfo[0]);
}

這個程式碼就有點多了,而且核心功能的程式碼都是在這裡實現的。

首先來看一下Hook函式:

int Hook(){
	init();
	void* handle = dlopen("/data/local/libTest.so",RTLD_NOW);
	const char *dlopen_error = dlerror();
	if(!handle){
		ALOG("Error","cannt load plugin :%s",dlopen_error);
		return -1;
	}
	SetupFunc setup = (SetupFunc)dlsym(handle,"getpHookInfo");
	const char *dlsym_error = dlerror();
	if (dlsym_error) {
		ALOG("Error","Cannot load symbol 'getpHookInfo' :%s" , dlsym_error);
		dlclose(handle);
		return 1;
	}

	HookInfo *hookInfo;
	setup(&hookInfo);

	ALOG("LOG","Target Class:%s",hookInfo[0].tClazz);
	ALOG("LOG","Target Method:%s",hookInfo[0].tMethod);

	ClassMethodHook(hookInfo[0]);
}

這個函式中,我們看到使用了dlopen系列的函式,主要是用來開啟so檔案,然後執行檔案中的指定函式

我們看到主要還是執行getpHookInfo函式,我們就去看一下這個函式的定義

Test.c

#include "native.h"
#include <android/log.h>
#include "stdio.h"
#include "stdlib.h"
#include "MethodHooker.h"

#define log(a,b) __android_log_print(ANDROID_LOG_VERBOSE,a,b); 
#define log_(b) __android_log_print(ANDROID_LOG_VERBOSE,"JNI_LOG_INFO",b); 

int getpHookInfo(HookInfo** pInfo);

JNIEXPORT void JNICALL Java_com_example_testar_InjectClassloader_hookMethodNative
  (JNIEnv * jenv, jobject jboj, jobject jobj, jclass jclazz, jint slot)
{
	//log("TestAE","start Inject other process");
}

JNIEXPORT jstring JNICALL test(JNIEnv *env, jclass clazz)  
{  
    //__android_log_print(ANDROID_LOG_VERBOSE, "tag", "call <native_printf> in java");
    return (*env)->NewStringUTF(env,"haha ");;
}

HookInfo hookInfos[] = {
		{"android/net/wifi/WifiInfo","getMacAddress","()Ljava/lang/String;",(void*)test},
		//{"com/example/testar/MainActivity","test","()Ljava/lang/String;",(void*)test},
		//{"android/app/ApplicationLoaders","getText","()Ljava/lang/CharSequence;",(void*)test},
};

int getpHookInfo(HookInfo** pInfo){
	*pInfo = hookInfos;
	return sizeof(hookInfos) / sizeof(hookInfos[0]);
}

看一下getHookInfo函式

int getpHookInfo(HookInfo** pInfo){
	*pInfo = hookInfos;
	return sizeof(hookInfos) / sizeof(hookInfos[0]);
}

傳遞的引數是HookInfo的二級指標型別,我們在看一下HookInfo型別的定義

MethodHooker.h

typedef struct{
	const char *tClazz;
	const char *tMethod;
	const char *tMeihodSig;
	void *handleFunc;
} HookInfo;

typedef int(*SetupFunc)(HookInfo**);

int Hook();

HookInfo是一個結構體

有四個成員欄位

tClazz:類的全稱

tMethod:方法名

tMethodSig:方法簽名

handleFounc:函式的指標

關於這四個欄位的作用,我們來看一下HookInfo的內容:

Test.c

HookInfo hookInfos[] = {
		{"android/net/wifi/WifiInfo","getMacAddress","()Ljava/lang/String;",(void*)test},
		//{"com/example/testar/MainActivity","test","()Ljava/lang/String;",(void*)test},
		//{"android/app/ApplicationLoaders","getText","()Ljava/lang/CharSequence;",(void*)test},
};

這裡看到了,我們現在需要修改Mac地址,Android中提供給我的的介面是WifiInfo這個類中的getMacAddress方法

第一個欄位類的名稱:android/net/wifi/WifiInfo,是全稱

第二個欄位方法名:getMacAddress

第三個欄位方法的簽名:()Ljava/lang/String;

第四個欄位函式指標:test函式

因為我們是通過WifiInfo這個類中的getMacAddress方法來獲取Mac地址的

看一下test函式

JNIEXPORT jstring JNICALL test(JNIEnv *env, jclass clazz)  
{  
    //__android_log_print(ANDROID_LOG_VERBOSE, "tag", "call <native_printf> in java");
    return (*env)->NewStringUTF(env,"haha ");
}

這個函式直接返回一個字串:“haha “

再回到MethodHooker.cpp中的Hook函式

int Hook(){
	init();
	void* handle = dlopen("/data/local/libTest.so",RTLD_NOW);
	const char *dlopen_error = dlerror();
	if(!handle){
		ALOG("Error","cannt load plugin :%s",dlopen_error);
		return -1;
	}
	SetupFunc setup = (SetupFunc)dlsym(handle,"getpHookInfo");
	const char *dlsym_error = dlerror();
	if (dlsym_error) {
		ALOG("Error","Cannot load symbol 'getpHookInfo' :%s" , dlsym_error);
		dlclose(handle);
		return 1;
	}

	HookInfo *hookInfo;
	setup(&hookInfo);

	ALOG("LOG","Target Class:%s",hookInfo[0].tClazz);
	ALOG("LOG","Target Method:%s",hookInfo[0].tMethod);

	ClassMethodHook(hookInfo[0]);
}

使用dlsym來獲取函式指標:

SetupFunc是一個函式指標型別的,在MethodHooker.h中定義的

typedef int(*SetupFunc)(HookInfo**);

然後我們就開始執行函式了

HookInfo *hookInfo;  
setup(&hookInfo);

因為我們之前看了getpHookInfo函式,他的引數是一個HookInfo的二級指標,所以可以進行值傳遞的。

執行完這個函式之後,hookInfo就有值了

其實上面的那段程式碼的功能就是:

獲取HookInfo型別的內容

下面在來看一下ClassMethodHook函式

我們傳遞進去的是hookInfo[0],在Test.c程式碼中,我們定義了HookInfo陣列,大小就是1,所以這裡就直接傳遞第一個元素值。

HookInfo hookInfos[] = {  
        {"android/net/wifi/WifiInfo","getMacAddress","()Ljava/lang/String;",(void*)test},  
        //{"com/example/testar/MainActivity","test","()Ljava/lang/String;",(void*)test},  
        //{"android/app/ApplicationLoaders","getText","()Ljava/lang/CharSequence;",(void*)test},  
};

看一下ClassMethodHook函式的定義

bool ClassMethodHook(HookInfo info){

	//獲取JNIEnv物件
	JNIEnv *jenv = GetEnv();

	//查詢類
	jclass clazzTarget = jenv->FindClass(info.tClazz);
	if (ClearException(jenv)) {
		ALOG("Exception","ClassMethodHook[Can't find class:%s in bootclassloader",info.tClazz);

	    clazzTarget = findAppClass(jenv,info.tClazz);
	    if(clazzTarget == NULL){
	    	ALOG("Exception","%s","Error in findAppClass");
	    	return false;
	    }
	}

	//在類中查詢方法
	jmethodID method = jenv->GetMethodID(clazzTarget,info.tMethod,info.tMeihodSig);
	if(method==NULL){
		ALOG("Exception","ClassMethodHook[Can't find method:%s",info.tMethod);
		return false;
	}

	//將這個方法變成native
	HookDalvikMethod(method);

    JNINativeMethod gMethod[] = {
        {info.tMethod, info.tMeihodSig, info.handleFunc},
    };

    //註冊native方法
    if(info.handleFunc != NULL){
		if (jenv->RegisterNatives(clazzTarget, gMethod, 1) < 0) {
			ALOG("RegisterNatives","err");
			return false;
		}
    }

	DetachCurrent();
	return true;
}

這個函式中有其他的函式呼叫,我們先來看看他們是怎麼定義的

1、GetEnv()

static JNIEnv *GetEnv()  
{  
    int status;  
    JNIEnv *envnow = NULL;  
    status = g_JavaVM->GetEnv((void **)&envnow, JNI_VERSION_1_4);  
    if(status < 0)  
    {  
        status = g_JavaVM->AttachCurrentThread(&envnow, NULL);  
        if(status < 0)  
        {  
            return NULL;  
        }  
        g_bAttatedT = true;  
    }  
    return envnow;  
}

這個函式的功能是通過JVM來獲取當前執行緒的JNIEnv物件,我們知道JVM是程式級的,一個程式對應一個JVM,JNIEnv是執行緒級的,一個執行緒對應一個JNIEnv,因為這裡沒有使用上層Java中的native方法,所以無法得到JNIEnv物件,但是我們可以通過另外的一種方式:引入AndroidRuntime.h(這個系統標頭檔案),通過其中系統定義的函式來獲取JVM物件,有了JVM物件,就可以得到當前執行緒的JNIEnv物件了:

static JavaVM *g_JavaVM;

void init()
{
	g_bAttatedT = false;
	g_JavaVM = android::AndroidRuntime::getJavaVM();
}

所以這裡就介紹了,以後如果在底層沒有和上層打交道,但是又想得到JNIEnv物件,這就是一種方法。

2、findClass函式

static jclass findAppClass(JNIEnv *jenv,const char *apn){  
    //通過類的全稱來查詢這個類,返回jclass物件  
    jclass clazzApplicationLoaders = jenv->FindClass("android/app/ApplicationLoaders");  
    jthrowable exception = jenv->ExceptionOccurred();  
    if (ClearException(jenv)) {  
        ALOG("Exception","No class : %s", "android/app/ApplicationLoaders");  
        return NULL;  
    }  
    jfieldID fieldApplicationLoaders =   
            jenv->GetStaticFieldID(clazzApplicationLoaders,"gApplicationLoaders","Landroid/app/ApplicationLoaders;");  
    if (ClearException(jenv)) {  
        ALOG("Exception","No Static Field :%s","gApplicationLoaders");  
        return NULL;  
    }  
    jobject objApplicationLoaders = jenv->GetStaticObjectField(clazzApplicationLoaders,fieldApplicationLoaders);  
    if (ClearException(jenv)) {  
        ALOG("Exception","GetStaticObjectField is failed [%s","gApplicationLoaders");  
        return NULL;  
    }  
    jfieldID fieldLoaders = jenv->GetFieldID(clazzApplicationLoaders,"mLoaders","Ljava/util/Map;");  
    if (ClearException(jenv)) {  
        ALOG("Exception","No Field :%s","mLoaders");  
        return NULL;  
    }  
    jobject objLoaders = jenv->GetObjectField(objApplicationLoaders,fieldLoaders);  
    if (ClearException(jenv)) {  
        ALOG("Exception","No object :%s","mLoaders");  
        return NULL;  
    }  

    jclass clazzHashMap = jenv->GetObjectClass(objLoaders);  
    jmethodID methodValues = jenv->GetMethodID(clazzHashMap,"values","()Ljava/util/Collection;");  
    jobject values = jenv->CallObjectMethod(objLoaders,methodValues);  

    jclass clazzValues = jenv->GetObjectClass(values);  
    jmethodID methodToArray = jenv->GetMethodID(clazzValues,"toArray","()[Ljava/lang/Object;");  
    if (ClearException(jenv)) {  
        ALOG("Exception","No Method:%s","toArray");  
        return NULL;  
    }  

    jobjectArray classLoaders = (jobjectArray)jenv->CallObjectMethod(values,methodToArray);  
    if (ClearException(jenv)) {  
        ALOG("Exception","CallObjectMethod failed :%s","toArray");  
        return NULL;  
    }  

        int size = jenv->GetArrayLength(classLoaders);  

        for(int i = 0 ; i < size ; i ++){  
            jobject classLoader = jenv->GetObjectArrayElement(classLoaders,i);  
            jclass clazzCL = jenv->GetObjectClass(classLoader);  
            jmethodID loadClass = jenv->GetMethodID(clazzCL,"loadClass","(Ljava/lang/String;)Ljava/lang/Class;");  
            jstring param = jenv->NewStringUTF(apn);  
            jclass tClazz = (jclass)jenv->CallObjectMethod(classLoader,loadClass,param);  
            if (ClearException(jenv)) {  
                ALOG("Exception","No");  
                continue;  
            }  
            return tClazz;  
        }  
    ALOG("Exception","No");  
    return NULL;  
}

這個函式的主要功能就是通過傳遞進來的類的全稱字串,然後進行查詢這個類,返回jclass.

這裡的原理是通過Android中的類載入器中來獲取這個類物件

其他就沒什麼難度了,就是JNIEnv的操作,這個就和Java中反射機制很類似。

我們看到函式中有一個這樣的類:

jclass clazzApplicationLoaders = jenv->FindClass("android/app/ApplicationLoaders");

我們去看一下這個類的原始碼(android/app/ApplicationLoaders):

/* 
 * Copyright (C) 2006 The Android Open Source Project 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */  

package android.app;  

import android.os.Trace;  
import android.util.ArrayMap;  
import dalvik.system.PathClassLoader;  

class ApplicationLoaders  
{  
    public static ApplicationLoaders getDefault()  
    {  
        return gApplicationLoaders;  
    }  

    public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)  
    {  
        /* 
         * This is the parent we use if they pass "null" in.  In theory 
         * this should be the "system" class loader; in practice we 
         * don't use that and can happily (and more efficiently) use the 
         * bootstrap class loader. 
         */  
        ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();  

        synchronized (mLoaders) {  
            if (parent == null) {  
                parent = baseParent;  
            }  

            /* 
             * If we're one step up from the base class loader, find 
             * something in our cache.  Otherwise, we create a whole 
             * new ClassLoader for the zip archive. 
             */  
            if (parent == baseParent) {  
                ClassLoader loader = mLoaders.get(zip);  
                if (loader != null) {  
                    return loader;  
                }  

                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);  
                PathClassLoader pathClassloader =  
                    new PathClassLoader(zip, libPath, parent);  
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  

                mLoaders.put(zip, pathClassloader);  
                return pathClassloader;  
            }  

            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);  
            PathClassLoader pathClassloader = new PathClassLoader(zip, parent);  
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);  
            return pathClassloader;  
        }  
    }  

    private final ArrayMap<String, ClassLoader> mLoaders = new ArrayMap<String, ClassLoader>();  

    private static final ApplicationLoaders gApplicationLoaders  
        = new ApplicationLoaders();  
}

這個類的作用就是用來維護應用中的類載入器

看到他有一個私有的變數mLoaders.是ArrayMap型別的

ArrayMap型別就把他看成是ArrayList和Map的結合體。具體使用自行研究。

這個mLoaders變數中維護了ClassLoader物件,現在我們就需要獲取這個ClassLoader物件

其中key是類的全稱,value就是類載入器

因為這個類是包訪問許可權,又是單例模式,我們只能去呼叫他的getDefault方法,得到其物件,然後在獲取他的mLoaders變數值

好了關於findClass函式的後續程式碼我就不解讀了,因為沒什麼難度,說白了就是反射機制

1)、通過反射獲取ApplicationLoaders物件中的mLoaders值

2)、通過反射去獲取mLoaders中指定key的類載入器ClassLoader物件

3)、然後通過反射去呼叫類載入器中的loadClass方法,返回一個jclass物件,最後返回即可

3、HookDalvikMethod函式

bool HookDalvikMethod(jmethodID jmethod){  
    Method *method = (Method*)jmethod;  
    //將方法method設定變成native  
    SET_METHOD_FLAG(method, ACC_NATIVE);  

    //計算這個native方法需要的空間大小  
    int argsSize = dvmComputeMethodArgsSize(method);  
    if (!dvmIsStaticMethod(method))  
        argsSize++;  

    method->registersSize = method->insSize = argsSize;  

    if (dvmIsNativeMethod(method)) {  
        method->nativeFunc = dvmResolveNativeMethod;  
        method->jniArgInfo = computeJniArgInfo(&method->prototype);  
    }  
}

這個函式程式碼不多,但是他的功能是最關鍵的。

將傳遞進來的jmethodID方法變成native方法

這個就是可以將一個非native方法變成一個native方法

其實這段程式碼中有幾個重要的函式:

SET_METHOD_FLAG

dvmComputeMethodArgsSize

dvmIsStaticMethod

dvmIsNativeMethos

devResolveNativeMethod

這些函式都是在系統中定義的,我們需要引入這個標頭檔案:Dalvik.h

 

看完了,這些函式,我們還是需要回到我們開始的地方ClassMethosHook函式:

bool ClassMethodHook(HookInfo info){  

    //獲取JNIEnv物件  
    JNIEnv *jenv = GetEnv();  

    //查詢類  
    jclass clazzTarget = jenv->FindClass(info.tClazz);  
    if (ClearException(jenv)) {  
        ALOG("Exception","ClassMethodHook[Can't find class:%s in bootclassloader",info.tClazz);  

        clazzTarget = findAppClass(jenv,info.tClazz);  
        if(clazzTarget == NULL){  
            ALOG("Exception","%s","Error in findAppClass");  
            return false;  
        }  
    }  

    //在類中查詢方法  
    jmethodID method = jenv->GetMethodID(clazzTarget,info.tMethod,info.tMeihodSig);  
    if(method==NULL){  
        ALOG("Exception","ClassMethodHook[Can't find method:%s",info.tMethod);  
        return false;  
    }  

    //將這個方法變成native  
    HookDalvikMethod(method);  

    JNINativeMethod gMethod[] = {  
        {info.tMethod, info.tMeihodSig, info.handleFunc},  
    };  

    //註冊native方法  
    if(info.handleFunc != NULL){  
        if (jenv->RegisterNatives(clazzTarget, gMethod, 1) < 0) {  
            ALOG("RegisterNatives","err");  
            return false;  
        }  
    }  

    DetachCurrent();  
    return true;  
}

還有一部分,呼叫JNIEnv物件中的RegisterNatives方法,進行註冊native方法。

上面的程式碼我們就看完了

下面來總結一下流程吧

1、首先執行inject.c中的main函式,在這個函式中我們將我們自己的libso.so檔案注入到目標程式中,然後執行InjectInterface函式

2、在InjectInterface函式中,我們在執行MethodHooker.cpp中的Hook函式

3、在Hook函式中,我們通過dlopen函式開啟libTest.so檔案,然後執行其中的getpHookInfo函式,獲取HookInfo結構體型別的內容

4、在getpHookInfo函式中主要的功能是將初始化好的HookInfo結構體返回給Hook函式中

5、在Hook函式中拿到getpHookInfo函式返回的HookInfo結構體內容,然後開始做兩部分內容

A:將結構體中的欄位tMethod標示的方法變成native的

      在這個過程中,我們首先需要獲取到這個方法所在的類,然後通過這個類來得到jmethod物件,然後進行操作

B:將結構體中的欄位tMethod標示的方法和欄位handleFunc進行關聯註冊,呼叫JNIEnv物件中的RegisterNatives函式

現在我們會想一下為什麼我們要這麼做呢?先把方法變成native的,然後在進行註冊

這個就需要了解一下Dalvik在執行指定方法的流程了

Dalvik在執行函式時會先呼叫dvmIsNativeMethod來判斷一個method是否是native方法。如果是native函式的話,那麼它所指向的一個Method物件的成員變數nativeFunc就指向該JNI方法的地址,因此就可以直接對它進行呼叫。否則的話,就說明引數method描述的是一個Java函式,這時候就需要繼續呼叫函式dvmInterpret來執行它的程式碼。因此我們可以把一個非native的java函式變成native method,讓Dalvik執行我們的native方法而達到hook的目的。

在來看一下loadMethodFromDex原始碼:

if (pDexCode != NULL) {  
        /* integer constants, copy over for faster access */  
        meth->registersSize = pDexCode->registersSize;  
        meth->insSize = pDexCode->insSize;  
        meth->outsSize = pDexCode->outsSize;  

        /* pointer to code area */  
        meth->insns = pDexCode->insns;  
    } else {  
        /* 
         * We don't have a DexCode block, but we still want to know how 
         * much space is needed for the arguments (so we don't have to 
         * compute it later).  We also take this opportunity to compute 
         * JNI argument info. 
         * 
         * We do this for abstract methods as well, because we want to 
         * be able to substitute our exception-throwing "stub" in. 
         */  
        int argsSize = dvmComputeMethodArgsSize(meth);  
        if (!dvmIsStaticMethod(meth))  
            argsSize++;  
        meth->registersSize = meth->insSize = argsSize;  
        assert(meth->outsSize == 0);  
        assert(meth->insns == NULL);  

        if (dvmIsNativeMethod(meth)) {  
            meth->nativeFunc = dvmResolveNativeMethod;  
            meth->jniArgInfo = computeJniArgInfo(&meth->prototype);  
       }  
    }

我們直接看else中的程式碼:

該函式會從dex 檔案中解析DexMethod 成dalvik中執行的method,if(pDexCode != NULL) 判斷是否存在dex程式碼,看else部分說明,可以知道該部分是dalvik對java native method處理過程。
其中dvmResolveNativeMethod呼叫了dvmLookupInternalNativeMethodlookupSharedLibMethod來查詢jni中註冊的native函式。 dalvik最後將執行得到的java native函式.

通過上面的程式碼片段,我們瞭解到要對一個java函式進行hook需要步驟有
[1] 把修改method的屬性修改成native
[2] 修改method的registersSize、insSize、nativeFunc、computeJniArgInfo
[3] RegisterNatives註冊目標method的native函式

測試執行

好了,到這裡我們就把程式碼都分析完了,原理也說清楚了,下面就開始動手測試了。

從上面我們可以看到在原始檔中我們引入了很多系統的標頭檔案,所以在這裡編譯會報錯的,所以我們需要將這些標頭檔案拷貝到編譯工程中來,但是在次編譯還是有問題,因為只有標頭檔案,沒有實現還是報錯的,所以我們需要把標頭檔案的實現也匯入進來,這時候我們就需要去Android系統中拷貝這些so檔案了(是對這些標頭檔案的實現,然後編譯成動態庫so,我們任然可以使用的)。這些so檔案是很多的,但是有一個規律的,就是每個so檔案的名字是:lib+標頭檔案名.so。比如AndroidRuntime.h標頭檔案對應的實現檔案:

libandroid_runtime.so,簡單吧,那麼這些so檔案我們從哪裡進行拷貝呢?我們可以啟動一個Android模擬器,然後從模擬器的

/system/lib/目錄下進行拷貝:

這裡為了防止出錯,把lib資料夾都拷貝過來了。

下面就可以進行編譯了

看一下Android.mk檔案

LOCAL_PATH := $(call my-dir)  

include $(CLEAR_VARS)  

LOCAL_MODULE:= so  

LOCAL_SRC_FILES := so.cpp MethodHooker.cpp  

LOCAL_LDLIBS+=   

LOCAL_CFLAGS    := -I./include/ -I./dalvik/vm/ -I./dalvik -DHAVE_LITTLE_ENDIAN  

LOCAL_LDFLAGS   :=  -L./lib/  -L$(SYSROOT)/usr/lib -llog -ldvm -landroid_runtime  -lart  

LOCAL_STATIC_LIBRARIES := hookart  

LOCAL_SHARED_LIBRARIES :=  
include $(BUILD_SHARED_LIBRARY)  

#------------------------------------------------------------------------  

include $(CLEAR_VARS)  

LOCAL_MODULE:= Test  

LOCAL_SRC_FILES := Test.c  

LOCAL_LDLIBS+= -L./lib -llog  

LOCAL_CFLAGS    := -I./include/ -I./dalvik/vm/ -I./dalvik -fPIC -shared  

LOCAL_SHARED_LIBRARIES :=   

include $(BUILD_SHARED_LIBRARY)  

#------------------------------------------------------------------------  

include $(CLEAR_VARS)  

LOCAL_MODULE:= inject  

LOCAL_SRC_FILES := inject.c shellcode.s  

LOCAL_LDLIBS :=   

LOCAL_CFLAGS :=    

include $(BUILD_EXECUTABLE)

這裡對so.cpp,Test.c,inject.c進行編譯。

看一下so.cpp的編譯模組

LOCAL_PATH := $(call my-dir)  

include $(CLEAR_VARS)  

LOCAL_MODULE:= so  

LOCAL_SRC_FILES := so.cpp MethodHooker.cpp  

LOCAL_LDLIBS+=   

LOCAL_CFLAGS    := -I./include/ -I./dalvik/vm/ -I./dalvik -DHAVE_LITTLE_ENDIAN  

LOCAL_LDFLAGS   :=  -L./lib/  -L$(SYSROOT)/usr/lib -llog -ldvm -landroid_runtime  -lart  

LOCAL_STATIC_LIBRARIES := hookart  

LOCAL_SHARED_LIBRARIES :=  
include $(BUILD_SHARED_LIBRARY)

我們需要用到的原始檔為:so.cpp、MethodHooker.cpp

編譯的過程中我們需要引入的標頭檔案我們都放到了include資料夾下:

所以寫法很簡單:

LOCAL_CFLAGS    := -I./include/ -I./dalvik/vm/ -I./dalvik -DHAVE_LITTLE_ENDIAN

還有一些標頭檔案放在dalvik資料夾下

這樣就引入了需要的標頭檔案

還需要匯入so檔案路徑:

這裡我把模擬器中的整個lib資料夾都拷貝過來了

LOCAL_LDFLAGS   :=  -L./lib/  -L$(SYSROOT)/usr/lib -llog -ldvm -landroid_runtime  -lart

這樣就可以編譯so.cpp了

後面的Test.c和inject.c編譯方法類似,這裡就不解釋了。

注:這裡其實說到了一種引入系統標頭檔案的編譯方式,之前在我的另外一篇文章中:

http://blog.csdn.net/jiangwei0910410003/article/details/40949475

在這篇文章中,我用的方式是將so檔案拷貝到NDK的目錄中的。

但是這個方式貌似更方便點,而且移植性比較好。本身就是一個專案了,不需要額外的工作就可以編譯這個專案了。

編譯:

專案的下載地址:http://download.csdn.net/detail/jiangwei0910410003/8263113

因為每個人的編譯環境都是不一樣的,所以如果在編譯過程中遇到什麼問題,請給我留言,我儘量幫助解決一下。

編譯工作完成之後,我們應該有三個檔案:

inject
libTest.so
libso.so

下面我們需要將這三個檔案拷貝到裝置的/data/local/目錄下,為什麼要拷貝到這個目錄呢?因為上面程式碼中寫的是這個目錄呀。不記得的同學在回過頭去看一下程式碼:inject.c中的main函式中以及so.cpp中的Hook函式中

我們先將這三個檔案拷貝到指定的磁碟中(這裡我是Q盤)

開始拷貝:

adb push inject /data/local/

adb push libso.so /data/local/

adb push libTest.so /data/local/

在修改一下他們的許可權

chmod 777 inject

chmod 777 libso.so

chmod 777 libTest.so

當然我們還可以寫一個簡單的指令碼檔案一步到位

adb push ..\libs\armeabi\libTest.so  /data/local/  
adb push ..\libs\armeabi\libso.so  /data/local/  
adb push ..\libs\armeabi\inject /data/local/  
adb shell chmod 777 /data/local/inject  
adb shell chmod 777 /data/local/libso.so  
adb shell chmod 777 /data/local/libTest.so  
adb shell su -c /data/local/inject  
pause

儲存.bat檔案,然後放到編譯專案的目錄下,直接執行即可。

拷貝工作完成了,下面來執行一下Android專案

注意應用的包名為:com.example.testar

這個在inject.c中的main函式中我們寫死了這個,因為這個包名就是程式名,我們需要通過程式名來獲取程式id的。

執行結果:

這時候我們開啟三個終端:

第一個終端:執行inject程式進行注入

./inject

第二個終端:監聽log資訊

adb logcat -s LOG

這個log資訊是底層列印的結果的

第三個終端:監聽log資訊

adb logcat -s DEMO

這時候我們會發現,列印的結果是“haha “,那麼我們就成功了修改了系統返回的Mac地址了。

這個結果其實是底層test函式返回的結果。說明系統在執行getMacAddress()方法的時候,其實呼叫了我們在底層定義的test函式。

感覺很爽,我們既然可以修改系統返回的一些裝置資訊了。哈哈!!

同樣的我們可以修改系統返回的IMEI等資訊。

我們是將WifiInfo類中的getMacAddress()方法首先變成native方法,然後再將底層的test函式和這個方法進行一一對應進行註冊。

系統在執行這個getMacAddress()方法的時候,發現他是一個native方法,就會去執行其對應的jni函式,所以這裡就做到了通過程式注入來修改系統方法返回的結果。

擴充

上面的例子算是結束了,也達到了我們的需求了,下面在繼續看

上面我們將系統呼叫的getMacAddress()方法執行的過程轉化成執行test函式了,但是這個test是在底層實現的,現在假如我們想在上層去修改這個具體的返回值,那不能修改一次,就去重新編譯底層專案,然後還有拷貝工作,同時還需要重新注入。這個操作就太複雜了,所以我們需要將這些工作移動到上層應用來。所以我們可以這麼做:

因為在上面的程式碼中我們看到即使上層沒有native方法,也可以獲取到JNIEnv物件的,那麼我們還是用這個JNIEnv物件通過反射機制,去獲取呼叫上層的方法來獲取值。

這裡由於篇幅的原因就不在演示了,程式碼實現起來不難。

總結

終於說完了,其實這個問題我早就接觸到了,只是一直沒有時間去解決,今天就有點時間,爭取把他搞定,我之所以說這個問題。原因是現在網上有兩個流行的框架:Xposed和Cydia,他們的作用就是注入到各種程式:

注入到系統程式修改系統的各種api值

注入到使用者程式修改特定方法的返回值,從而做到破解的效果:比如現在又一個遊戲金幣的遊戲,那麼我只要知道這個遊戲金幣的獲取方法,比如是:getMoney()類似的方法,那麼我就可以用這個方法進行注入到這個遊戲程式,然後修改這個方法的返回值。那麼就可以獲取到用不完的金幣了,當然這個說起來很容易,當用這個框架去操作的時候,會發現有很多問題的。這個我在後面的文章中會用這個框架進行操作的。

那麼我現在想說的是:其實這兩個框架的實現原理就是我今天講的這種方式實現的,只是上面的兩個框架在效率上比我這個好多了,優化工作也做的很好。我說了這篇文章就是想去解析他的原理。

如果你想去替換一個程式中執行的api:

將這個api方法變成native的,然後在用一個方法將其進行註冊

原因就是虛擬機器在執行的時候,發現這個api方法如果是native的話,就去執行它註冊之後的那個jni方法了。

在這篇文章中我們學習到了幾個知識點:

1、如何將一個非native方法變成一個native方法

2、如何手動的去註冊一個native方法

3、學會了使用另外的一種編譯專案的方式(引入標頭檔案)

4、注入程式的相關知識

相關文章