[Android] 在不需要Root权限且只使用Java的情况下,钩取Java函数
首先
我认为如果在Java中也能够实现函数钩子,那将会非常有趣,所以我试着做了一下。
环境
・Android 11(真机)
・非Root
挂钩前的准备工作。
首先,我们要创建一个简单的库。
文件/文件夹的结构是这样的。
...
jinterceptor
┗ EvacuatedMethodStorage.java
┗ Interceptor.java
┗ Memory.java
┗ ArtMethod.java
┗ Reflection.java
┗ Unsafe.java
...
MainActivity.java
...
以下的说明和来源
public class EvacuatedMethodStorage {
private static int mReferencedCount = 0;
public static int referencedCount(){
return mReferencedCount++;
}
public static Object method0(Object receiver, Object...params){
return null;
}
public static Object method1(Object receiver, Object...params){
return null;
}
public static Object method2(Object receiver, Object...params){
return null;
}
public static Object method3(Object receiver, Object...params){
return null;
}
public static Object method4(Object receiver, Object...params){
return null;
}
...
public static Object method255(Object receiver, Object...params){
return null;
}
}
这个课程中实现了一组用于保存钩子函数的原始函数的方法。
public class Interceptor {
private static final Class<?> class_EvacuatedMethodStorage = EvacuatedMethodStorage.class;
private static Map<Pair<String, String>, Integer> mBackupMap = new ConcurrentHashMap<>();
public static void override(Method original, Method replace) throws NoSuchMethodException {
long originalAddress = Unsafe.getMethodAddress(original), replaceAddress = Unsafe.getMethodAddress(replace);
int c = backupMethod(originalAddress); //cは保存先の関数の番号
Memory.copy(originalAddress, replaceAddress, ArtMethod.getSize());
mBackupMap.put(new Pair<>(replace.getDeclaringClass().getName(), replace.getName()), c);
//...<>(original.getDeclaringClass().getName(), original.getName())... ではないのは、元の関数が上書きされるため
}
public static Object callOriginal(Object receiver, Object...params){
StackTraceElement current = Thread.currentThread().getStackTrace()[3]; //どのフックされた関数を通過したのかのトレースを取得
int originalNum = mBackupMap.get(Pair.create(current.getClassName(), current.getMethodName())); //保存先の関数の番号を取得
switch (originalNum){
case 0:
return EvacuatedMethodStorage.method0(receiver, params);
case 1:
return EvacuatedMethodStorage.method1(receiver, params);
case 2:
return EvacuatedMethodStorage.method2(receiver, params);
...
case 255:
return EvacuatedMethodStorage.method255(receiver, params);
}
return null;
}
private static int backupMethod(long original) throws NoSuchMethodException {
int count = EvacuatedMethodStorage.referencedCount();
Method evacuationDst = class_EvacuatedMethodStorage.getDeclaredMethod("method" + count, Object.class, Object[].class);
Memory.copy(Unsafe.getMethodAddress(evacuationDst), original, ArtMethod.getSize()); //退避先の関数をoriginaに上書き
return count; //保存先の番号
}
}
这个类实现了钩子函数。
顺便提一下,在callOriginal函数中,不使用反射并且使用switch语句来判断和调用已经存储的原始函数,是有原因的。
后面会说明原因。
public class Memory {
private static byte peekByte(long address){
return (Byte) Reflection.call(null, "libcore.io.Memory", "peekByte", null, new Class[]{long.class}, new Object[]{address});
}
private static void pokeByte(long address, byte value) {
Reflection.call(null, "libcore.io.Memory", "pokeByte", null, new Class[]{long.class, byte.class}, new Object[]{address, value});
}
public static void copy(long dst, long src, long length) {
for (long i = 0; i < length; i++) {
pokeByte(dst, peekByte(src));
dst++;
src++;
}
}
}
这是一个用于进行内存操作的类。
public class ArtMethod {
private static long mSize;
static {
try {
Method r1 = ArtMethod.class.getDeclaredMethod("r1"),
r2 = ArtMethod.class.getDeclaredMethod("r2");
mSize = Unsafe.getMethodAddress(r2) - Unsafe.getMethodAddress(r1);
} catch (Throwable throwable){
throwable.printStackTrace();
}
}
public static long getSize(){
return mSize;
}
private static void r1(){}
private static void r2(){}
}
这个类提供了ArtMethod的大小。
首先,ArtMethod 是什么意思?
ArtMethod 是 Android 的运行时系统(ART:Android Runtime)内部的一个类的组成部分。这个类用于表示 Java 方法,在 Android 应用程序的运行过程中存储和操作方法的信息。
ChatGPT告诉我,
也就是说,它类似于用于存储Java方法的各种信息的包装器。
链接到ArtMethod的头文件
链接到ArtMethod的源代码
现在,我想要谈谈获取关键大小的代码。
在这里获取大小的是这段代码。
mSize = Unsafe.getMethodAddress(r2) - Unsafe.getMethodAddress(r1);
Unsafe.getMethodAddress的实现如下所示。
public class Unsafe {
public static long getMethodAddress(Method method){
return (Long) Reflection.get(method.getClass().getSuperclass(), null, "artMethod", method);
}
}
通过反射,我们可以获取到Method类的地址。这个地址存储在Method类的超类Executable中的名为artMethod的long型变量中。确切来说,变量artMethod中存储的是将该方法封装的ArtMethod的地址或指针。
如果图表也不太清楚的话,真的非常抱歉。
public class Reflection {
public static Object call(Class<?> clazz, String className, String methodName, Object receiver,
Class[] types, Object[] params) {
try {
if (clazz == null) clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName, types);
method.setAccessible(true);
return method.invoke(receiver, params);
} catch (Throwable throwable) {
throw new RuntimeException("reflection error:", throwable);
}
}
public static Object get(Class<?> clazz, String className, String fieldName, Object receiver) {
try {
if (clazz == null) clazz = Class.forName(className);
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(receiver);
} catch (Throwable e) {
throw new RuntimeException("reflection error:", e);
}
}
}
这个班级是为了方便使用反射功能而设计的。
确认动作
主活动写成了这样。
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
try {
//original関数をreplace関数に上書き
Interceptor.override(MainActivity.class.getDeclaredMethod("original"), MainActivity.class.getDeclaredMethod("replace"));
} catch (Throwable e) {
throw new RuntimeException(e);
}
original(); //呼び出し
TextView tv = binding.sampleText;
tv.setText("あいうえお");
}
void original(){
System.out.println("original");
}
void replace() {
System.out.println("replace");
}
}
执行结果
更换
原本应该调用的是original函数,但是却调用了replace函数。钩子函数执行成功。
当然,仍然可以调用原本的函数。
void replace() {
System.out.println("replace");
Interceptor.callOriginal(this, new Object[]{}); //元の関数呼び出し
}
执行结果
用中文翻译以下英文句子,只需一种选项:
替换
原文
顺便一提,在连接后,请确认MainActivity类中声明的方法,会发生有趣的事情。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
try {
Interceptor.override(MainActivity.class.getDeclaredMethod("original"), MainActivity.class.getDeclaredMethod("replace"));
} catch (Throwable e) {
throw new RuntimeException(e);
}
System.out.println(" ==== Declared Methods ==== ");
for (Method method : MainActivity.class.getDeclaredMethods()){
System.out.println(method.getName());
}
System.out.println(" ========================== ");
TextView tv = binding.sampleText;
tv.setText("あいうえお");
}
执行结果
====声明的方法====
onCreate
替换
替换
=====================
Memory.copy(originalAddress, replaceAddress, ArtMethod.getSize());
因为我们还没有试过这个方法,所以需要进行调查以了解可能产生的副作用。
请留意
我将介绍我目前所了解到的注意事项和副作用。
・JNI不支持调用被挂钩的函数
由于将C ++等原始函数的前几个字节重写为jmp而不是使用trampoline技巧,因此不得不完全覆盖函数,所以没办法。请注意,无法钩住从本地调用的函数。
将保存了原始方法的函数通过反射调用会导致进入无限循环的状态。
我对这个原因不清楚。
举个例子,假设我们之前的例子中,original函数的备份被保存在EvacuatedMethodStorage::method0中,但是当通过反射调用method0函数时,调用了replace方法而不是原始函数,导致发生了无限循环。
我在hook之前进行了备份,所以我认为不会受到影响,但是即使尝试获取了hook后的method0的地址,也和original和replace的地址不一致,所以更加不清楚了。
正如之前提到的,在callOriginal函数内,通过switch语句判断并调用反射前备份的原始函数,是因为这个原因。
如果有很多勾连函数,就需要逐个在EvacuatedMethodStorage中添加方法。
这真的很麻烦啊。要为Interceptor::callOriginal函数的switch语句添加新的选项…
结束
我从来没有想过通过Java就能实现函数钩子,通常我会认为这方面可能会涉及JNI。
我非常喜欢这种实验,所以如果有机会的话,我还想写类似这次内容的东西。
如果有任何错误,请毫不犹豫地指出来。
赠品
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
try {
Interceptor.override(TextView.class.getDeclaredMethod("setText", CharSequence.class), MainActivity.class.getDeclaredMethod("setText_hook", CharSequence.class));
} catch (Throwable throwable){
throwable.printStackTrace();
}
TextView tv = binding.sampleText;
tv.setText("あいうえお");
}
void setText_hook(CharSequence text){
System.out.println("setText_hook -> text : " + text);
Interceptor.callOriginal(this, text);
}
}
執行結果 (zhí jié guǒ)
致命信号11(SIGSEGV),代码1(SEGV_MAPERR),错误地址0x83efd50fdc在线程13503(ec.javahooktest)中,进程13503(ec.javahooktest)。
我觉得如果钩住了View系列的函数,大概就会导致崩溃。
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
try {
Interceptor.override(System.class.getDeclaredMethod("loadLibrary", String.class), MainActivity.class.getDeclaredMethod("loadLibrary_hook", String.class));
} catch (Throwable throwable){
throwable.printStackTrace();
}
TextView tv = binding.sampleText;
tv.setText("あいうえお");
System.loadLibrary("javahooktest");
}
static void loadLibrary_hook(String libname){
System.out.println("System::loadLibrary -> libname : " + libname);
//Interceptor.callOriginal(null, libname);
}
}
虽然不需要展示执行结果,但是我发现调用callOriginal时会导致崩溃。不过我认为这种方法非常实用,可以在加载游戏等应用程序所需库后,通过钩子技术拦截loadLibrary函数,防止外部库的注入等,以加强对作弊的防范作用。只要不从内存中读取,这种方法可能对防作弊有所帮助。
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
try {
Interceptor.override(TextView.class.getDeclaredMethod("setText", CharSequence.class), MainActivity.class.getDeclaredMethod("setText_hook", CharSequence.class));
} catch (Throwable throwable){
throwable.printStackTrace();
}
Log.d("ABC", "YO");
TextView tv = binding.sampleText;
tv.setText("あいうえお");
}
void d_hook(String tag, String msg){
System.out.println("Log.d -> hook : tag | " + tag + ", msg | " + msg);
Interceptor.callOriginal(null, tag, msg);
}
}
执行结果 (shí jié guǒ)
运行时.cc:663] JNI检测到应用程序中的错误:jstring具有错误类型:java.lang.Object[]
不行。
顺便提一下,如果不调用原始文件的话
void d_hook(String tag, String msg){
System.out.println("Log.d -> hook | tag : " + tag + ", msg : " + msg);
//Interceptor.callOriginal(null, tag, msg);
}
执行结果
打印.d -> 钩子 | 标签: ABC,消息: 嘿
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
try {
Interceptor.override(ContextWrapper.class.getDeclaredMethod("getPackageName"), MainActivity.class.getDeclaredMethod("getPackageName_hook"));
} catch (Throwable throwable){
throwable.printStackTrace();
}
System.out.println("Package : " + getPackageName());
TextView tv = binding.sampleText;
tv.setText("あいうえお");
}
String getPackageName_hook(){
return "ai.ue.o";
}
执行结果
包装:ai.ue.o
可能有各种用途的吗….?