Android免Root|Android免Root 修改程序运行时内存指令逻辑(Hook系统函数)
前言
如今的加固方案基本都有类方法抽取了,但是应该是由于公司的商业性质问题,网上的类方法抽取资料还是比较少的。于是我就想实现一下类方法抽取,实现之前我们需要知道如何修改程序运行时内存指令逻辑这是类方法抽取的核心。所以本文对此进行讨论。
阅读此文你需要掌握(我之前的文章都有提到)
- Android inline Hook
- JNI开发 之 Eclipse中静态注册
- ELF探究 之 ELF文件介绍(一)
- Android的加壳与脱壳 之 Android类加载器(一)(这里主要关注如何动态加载DEX)
开发环境 Android4.4.4
Nexus5手机(ARM)
Android Studio3.5.1
思路 整体思路就是Hook dexFindClass函数(一个方法执行之前肯定需要解析类信息加载到内存,而这个函数就是加载类必定运行的函数),在hook逻辑中拿到DexCode方法数据结构体信息,最后查阅虚拟机指令集,找到加法指令码替换原来的乘法指令码,然后覆盖内存中的原始指令。
这里我们使用DexClassLoader去加载DEX文件,然后调用指定方法,在项目初始化时就完成hook。所以我们需要开发一个动态加载的DEX项目
动态加载的DEX项目
文章图片
image.png
这里只简单开发了一个工具类,就是一个乘法运算,到时候Hook改成加法就ok了。
将其编译好后解压出其中的dex文件,改名为CoreDex.dex并拷贝到手机的/sdcard/CoreDex.dex
项目java层代码 创建一个DexUtils工具类用来动态加载调用dex里面的方法
DexUtils.java
package com.shark.androidinlinehook;
import android.content.Context;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class DexUtils {public static final String SHARK = "shark";
public static void exeCoreMethod(Context context) {
try {
//创建文件夹
File optfile = context.getDir("opt_dex", 0);
File libfile = context.getDir("lib_path", 0);
//得到当前Activity 的ClassLoader 以下的方法得到的都是同一个ClassLoader
ClassLoader parentClassloader = MainActivity.class.getClassLoader();
ClassLoader tmpClassLoader = context.getClassLoader();
//创建我们自己的DexClassLoader 指定其父节点为当前Activity 的ClassLoader
/*dexPath:目标所在的apk或者jar文件的路径,装载器将从路径中寻找指定的目标类。
dexOutputDir:由于dex 文件在APK或者 jar文件中,所以在装载前面前先要从里面解压出dex文件,这个路径就是dex文件存放的路径,
在 android系统中,一个应用程序对应一个linux用户id ,应用程序只对自己的数据目录有写的权限,所以我们存放在这个路径中。
libPath :目标类中使用的C/C++库。
最后一个参数是该装载器的父装载器,一般为当前执行类的装载器。*/
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/CoreDex.dex",
optfile.getAbsolutePath(), libfile.getAbsolutePath(), MainActivity.class.getClassLoader());
Class> clazz=dexClassLoader.loadClass("com.shark.calculate.CoreUtils");
Method calculateMoney=clazz.getDeclaredMethod("calculateMoney",int.class,int.class);
Object obj=clazz.newInstance();
int result = (int)calculateMoney.invoke(obj,2,3);
Log.i(SHARK, "calculateMoney result:" + result);
} catch (Exception e) {
Log.i(SHARK, "exec exeCoreMethod err:" + Log.getStackTraceString(e));
}
}
}
上面就是简单的加载反射调用,其中传参为2和3,其结果应该是6。但是我们hook后就应该是5
在MainActivity中调用
MainActivity.java
package com.shark.androidinlinehook;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {static {
System.loadLibrary("inlineHook");
}@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//设置点击事件 加载调用Dex中的计算方法
findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DexUtils.exeCoreMethod(getApplicationContext());
}
});
startInlineHook();
}//开启hook方法
public static native void startInlineHook();
}
上面代码就是加了个点击事件来触发
现在主要来关注startInlineHook这个native方法
项目so层代码 先将开源Hook项目拷贝到cpp中(不清楚的可以看上一章)
文章图片
image.png
使用javah得到MainActivity的头文件com_shark_androidinlinehook_MainActivity.h,拷贝到cpp中
文章图片
image.png
修改CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)add_library(inlineHook SHARED
hooktest.cpp
inlineHook.c
relocate.c)target_link_libraries(
inlineHook
android
log)
find_library(
log-lib
log )
因为我们要使用到源码中的一些结构和函数,所以我们要将其中的一些文件拷贝过来
文章图片
image.png DexClass.h: /dalvik/libdex/DexClass.h
DexFile.h:/dalvik/libdex/DexFile.h
Leb128.h:/dalvik/libdex/Leb128.h
Common.h:/dalvik/vm/Common.h
去除一些不用的头文件
Common.h
//去除
#include "cutils/log.h"
DexFile.h
//去除
#include "libdex/SysUtil.h"
准备工作都做完了现在就来看看hooktest.cpp中的代码吧
先介绍一下需要用到的两个函数
//打印指令函数
void printMethodInsns(const DexFile *pFile, DexMethod *pDexMethod) {
const DexCode *dexCode = dexGetCode(pFile, pDexMethod);
LOGI("method insns size:%d", dexCode->insnsSize);
const u2 *insns = dexCode->insns;
for (int i = 0;
i < dexCode->insnsSize;
i++) {
LOGI("insns:%d", (*(insns++)));
}
}//修改内存读写属性
int changeMemWrite(int start_add) {
/* 获取操作系统一个页的大小, 一般是 4KB == 4096 */
long page_size = sysconf(_SC_PAGESIZE);
//必须是整页修改,不然无效
start_add = start_add - (start_add % page_size);
LOGI("code add:0x%x, pagesize:%d", start_add, (int)page_size);
int result = mprotect((void *) start_add, page_size, PROT_READ | PROT_WRITE);
return result;
}
printMethodInsns用来打印指令
changeMemWrite函数是用来修改内存读写属性
修改的起始地址一定是系统内存页的整数倍,所以需要做一次转化。
【Android免Root|Android免Root 修改程序运行时内存指令逻辑(Hook系统函数)】现在来看看HOOK的代码
#include
#include
#include
#include
#include
#include
#include "vm/Common.h"
#include
#include "inlineHook.h"
#include "DexFile.h"
#include "DexClass.h"
#import "com_shark_androidinlinehook_MainActivity.h"#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "SharkChilli", __VA_ARGS__))//定义DexReadAndVerifyClassData方法类型
typedef DexClassData *(*DexReadAndVerifyClassData)(const u1 **, const u1 *);
//定义DexClassDef方法类型
typedef const DexClassDef *(*OldDexFindClass)(const DexFile *pFile, const char *descriptor);
//定义OldDexFindClass用来保存被hook的方法
OldDexFindClass oldDexFindClass;
//打开的so文件
void *dvmLib;
const DexClassDef *newDexFindClass(const DexFile *pFile, const char *descriptor) {
...
}void *getDexFindClass(void *funclib) {
//得到方法的内存地址
void *func = dlsym(funclib, "_Z12dexFindClassPK7DexFilePKc");
LOGI("func:%p", func);
if (registerInlineHook((uint32_t) func,
(uint32_t) newDexFindClass, (uint32_t **) &oldDexFindClass) !=
ELE7EN_OK) {
LOGI("registerInlineHook ERROR");
return NULL;
}
return func;
}void hookDvm() {
//打开so文件
dvmLib = dlopen("/system/lib/libdvm.so", RTLD_LAZY);
LOGI("dvmLib:%p", dvmLib);
void *func = getDexFindClass(dvmLib);
if (inlineHook((uint32_t) func) != ELE7EN_OK) {
LOGI("inlineHook ERROR");
return;
}
}/*
* Class:com_shark_androidinlinehook_MainActivity
* Method:startInlineHook
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_shark_androidinlinehook_MainActivity_startInlineHook
(JNIEnv *, jclass) {
LOGI("hook start");
hookDvm();
LOGI("hook end");
}
如何Hook在上一章已经说明。这里的问题是hook哪个函数呢?
dexFindClass函数(一个方法执行之前肯定需要解析类信息加载到内存,而这个函数就是加载类必定运行的函数)
- 第一个参数:DexFile结构体指针
- 第二个参数:是加载类的名称
要得到dexFindClass函数的地址,我们需要知道这个函数的导出名称。这个函数在设备的/system/lib/libdvm.so下
为了获取正确的函数名称,我们需要导出设备中的libdvm.so文件,拷贝出来,然后用IDA打开libdvm.so文件:
文章图片
image.png 查询DexFindClass,找到其导出名称
所以
void *func = dlsym(funclib, "_Z12dexFindClassPK7DexFilePKc");
这样就得到了DexFindClass在内存中的地址了
这里我省略了newDexFindClass的代码,这里就是我们hook成功后,怎么去修改内存中的指令逻辑了。
const DexClassDef *newDexFindClass(const DexFile *pFile, const char *descriptor) {
//只关注需要修改的类Lcom/shark/calculate/CoreUtils;
int cmp = strcmp("Lcom/shark/calculate/CoreUtils;
", descriptor);
if (cmp == 0) {
//执行原来的逻辑得到类结构信息
const DexClassDef *pClassDef = oldDexFindClass(pFile, descriptor);
if (pClassDef == NULL) {
return pClassDef;
}
//打印信息
LOGI("class def:%d", (int)pClassDef);
LOGI("class dex find class name:%s", descriptor);
//我们需要调用DexReadAndVerifyClassData得到DexClassData代码结构,所以需要得到其地址
//依然需要用IDA打开libdvm.so文件查看DexReadAndVerifyClassData函数的导出名称:
DexReadAndVerifyClassData getClassData = https://www.it610.com/article/(DexReadAndVerifyClassData) dlsym(
dvmLib,"_Z25dexReadAndVerifyClassDataPPKhS0_");
const u1 *pEncodedData = https://www.it610.com/article/dexGetClassData(pFile, pClassDef);
DexClassData *pClassData = getClassData(&pEncodedData, NULL);
DexClassDataHeader header = pClassData->header;
//打印对象方法数量
LOGI("method size:%d", header.virtualMethodsSize);
//得到首个对象方法的指针
DexMethod *pDexVirtualMethod = pClassData->virtualMethods;
u1 *ptr = (u1 *) pDexVirtualMethod;
//循环遍历每个方法
for (int i = 0;
i < header.virtualMethodsSize;
i++) {
//这里每个方法都是相邻的,每个大小都是DexMethod结构体的大小
pDexVirtualMethod = (DexMethod *) (ptr + sizeof(DexMethod) * i);
//得到方法名称
const DexMethodId *methodId = dexGetMethodId(pFile, pDexVirtualMethod->methodIdx);
const char *methodName = dexStringById(pFile, methodId->nameIdx);
//如果是calculateMoney方法就进行替换逻辑
if (strcmp("calculateMoney", methodName) == 0) {
LOGI("pDexVirtualMethod methodName:%s", methodName);
//打印指令
printMethodInsns(pFile, pDexVirtualMethod);
//修改内存页属性
int start_add = (int) (pFile->baseAddr + pDexVirtualMethod->codeOff);
int result = changeMemWrite(start_add);
LOGI("mp result:%d", result);
//获取方法对应DexCode结构
DexCode *dexCode = (DexCode *) dexGetCode(pFile, pDexVirtualMethod);
//下面就是覆盖指令了
u2 new_ins[3] = {144, 770, 15};
memcpy(dexCode->insns, &new_ins, 3 * sizeof(u2));
printMethodInsns(pFile,pDexVirtualMethod);
}
}
return pClassDef;
} else{
//执行原来的逻辑
return oldDexFindClass(pFile,descriptor);
}}
DexReadAndVerifyClassData函数依然需要用IDA打开libdvm.so文件查看DexReadAndVerifyClassData函数的导出名称:
文章图片
image.png 因为我们知道那个calculateMoney方法是对象方法,所以这里直接获取对象方法结构体信息,然后依次遍历获取每个方法,通过系统函数dexGetMethodId获取DexMethodIds结构体信息,
文章图片
image.png 然后在利用系统函数dexStringById获取方法名称,
文章图片
image.png 有了方法名就需要进行过滤了,只处理我们的那个calculateMoney方法,然后在获取方法对应的数据结构信息DexCode了,
文章图片
image.png 然后我们因为需要修改内存指令,所以还需要把内存修改为可读属性
文章图片
image.png 然后利用系统函数dexGetCode通过DexMethod结构体获取DexCode结构体信息
文章图片
image.png
接下来就可以构造指令,然后替换内存指令即可。那么如何获取原始指令,怎么把乘法改成加法呢?这里就需要利用010Editor软件了,直接查看这个方法的指令数据:
文章图片
image.png 这里看到,这个方法有三条指令,但是一条指令是两个字节,所以一共是6个字节,这里看到的是十进制的数据了,我们可以把这三个十进制数据转化成6个十六进制数据:
文章图片
image.png
然后我们现在只需要把乘法指令码改成加法指令码即可,这个需要参考Bytecode of Dalvik了:
文章图片
image.png
所以覆盖指令代码如下:
文章图片
image.png
运行 点击Test按钮后
文章图片
image.png 引用 姜维大佬写的真的牛啊(可惜没有源码)
Android免Root权限通过Hook系统函数修改程序运行时内存指令逻辑
推荐阅读
- android第三方框架(五)ButterKnife
- MongoDB,Wondows下免安装版|MongoDB,Wondows下免安装版 (简化版操作)
- Android中的AES加密-下
- 带有Hilt的Android上的依赖注入
- android|android studio中ndk的使用
- SSH|SSH 免密
- Android事件传递源码分析
- 探索免费开源服务器tomcat的魅力
- RxJava|RxJava 在Android项目中的使用(一)
- Android7.0|Android7.0 第三方应用无法访问私有库