使用Riru修改手游

2019-06-25 4,463 ℃

前篇

手游的注入与修改

使用VirtualXposed修改手游

前言

时隔半年终于把这坑填上了,一开始只是打算随便写写,不过现在看来刚好可以凑出一个系列,之前的文章也重新编辑了一下,感兴趣的人可以翻回去看看。

这次要介绍的Riru模块对于一些人应该并不陌生,Riru+EdXposed是现在的安卓9以上Xposed环境的解决方案之一。Riru的原理是通过替换会被Zygote加载的libmemtrack.so从而实现Zygote注入,而安卓应用进程都是从Zygote fork的,注入了Zygote也就等同于注入了接下来会启动的游戏,也就可以轻松实现修改了。

Riru是Magisk的模块,所以首先要安装Magisk,不过现在手机root应该都是选择Magisk了,然后去Riru的Github的Releases页面下载最新的riru-core包,在magisk里安装,安装好Riru后就可以动手写自己的修改模块了。

写自己的Riru模块

去Riru的Github上下载所有代码,根据官方README,先给自己的模块取个名字,比如perfare,然后修改riru-module-template/jni/main/Android.mk中的模块名字

LOCAL_MODULE := libriru_perfare

接着修改riru-module-template/build.gradle中的模块信息

def moduleName = "perfare"

这两个地方的模块名字一定要一样,其他模块信息自己看着修改就好,修改好后就开始写代码吧,打开riru-module-template/jni/main/main.cpp

在main.cpp中本身已经写好了一些函数,我们只需要关注两个函数
nativeForkAndSpecializePre 通过解析参数appDataDir从而得到当前启动的进程包名,判断是不是我们要修改的游戏
nativeForkAndSpecializePost 在这里创建新线程进行修改

首先写个函数判断包名

static int enable_hack;
static const char* game_name = "com.aniplex.fategrandorder";

int isGame(JNIEnv *env, jstring appDataDir) {
	if (!appDataDir)
		return 0;

	const char *app_data_dir = env->GetStringUTFChars(appDataDir, NULL);

	int user = 0;
	static char package_name[256];
	if (sscanf(app_data_dir, "/data/%*[^/]/%d/%s", &user, package_name) != 2) {
		if (sscanf(app_data_dir, "/data/%*[^/]/%s", package_name) != 1) {
			package_name[0] = '\0';
			LOGW("can't parse %s", app_data_dir);
			return 0;
		}
	}
	env->ReleaseStringUTFChars(appDataDir, app_data_dir);
	if (strcmp(package_name, game_name) == 0) {
		LOGD("detect game: %s", package_name);
		return 1;
	}
	else {
		return 0;
	}
}

然后在nativeForkAndSpecializePre里调用这个函数

void nativeForkAndSpecializePre(
        JNIEnv *env, jclass clazz, jint *_uid, jint *gid, jintArray *gids, jint *runtime_flags,
        jobjectArray *rlimits, jint *_mount_external, jstring *se_info, jstring *se_name,
        jintArray *fdsToClose, jintArray *fdsToIgnore, jboolean *is_child_zygote,
        jstring *instructionSet, jstring *appDataDir, jstring *packageName,
        jobjectArray *packagesForUID, jstring *sandboxId) {
    // packageName, packagesForUID, sandboxId exists from Android Q
    enable_hack = isGame(env, *appDataDir);
}

之后就可以在nativeForkAndSpecializePost里根据enable_hack来修改了,这里选择启动一个新线程来修改

int nativeForkAndSpecializePost(JNIEnv *env, jclass clazz, jint res) {
    if (res == 0) {
        // in app process
        if (enable_hack) {
            int ret;
            pthread_t ntid;
            if ((ret = pthread_create(&ntid, NULL, hack_thread, NULL))) {
                LOGE("can't create thread: %s\n", strerror(ret));
            }
        }
    } else {
        // in zygote process, res is child pid
        // don't print log here, see https://github.com/RikkaApps/Riru/blob/77adfd6a4a6a81bfd20569c910bc4854f2f84f5e/riru-core/jni/main/jni_native_method.cpp#L55-L66
    }
    return 0;
}

至于hack_thread里要怎么修改游戏就看你自己了,这里随便示范一个简单的libil2cpp.so修改,通过不断读取/proc/self/maps确定libil2cpp.so的载入和获取基址,然后直接通过指针修改text段代码

unsigned long get_module_base(const char* module_name)
{
	FILE *fp;
	unsigned long addr = 0;
	char *pch;
	char filename[32];
	char line[1024];

	snprintf(filename, sizeof(filename), "/proc/self/maps");

	fp = fopen(filename, "r");

	if (fp != NULL) {
		while (fgets(line, sizeof(line), fp)) {
			if (strstr(line, module_name)) {
				pch = strtok(line, "-");
				addr = strtoul(pch, NULL, 16);
				if (addr == 0x8000)
					addr = 0;
				break;
			}
		}
		fclose(fp);
	}
	return addr;
}

void *hack_thread(void *arg)
{
	LOGD("hack thread :%d", gettid());
	unsigned long base_addr;
	while (true)
	{
		base_addr = get_module_base("libil2cpp.so");
		if (base_addr != 0) {
			break;
		}
	}
	LOGD("detect libil2cpp.so %lx", base_addr);
	LOGD("hack game begin");

	unsigned long hack_addr = base_addr + 偏移;

	//设置属性可写
	void* page_start = (void*)(hack_addr - hack_addr % PAGE_SIZE);
	if (-1 == mprotect(page_start, PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) {
		LOGE("mprotect failed(%d)", errno);
		return NULL;
	}

	unsigned char* tmp = (unsigned char*)(void*)hack_addr;
	tmp[0] = 0x00;
	tmp[1] = 0x00;
	tmp[2] = 0x00;
	tmp[3] = 0x00;

	LOGD("hack game finish");
	return NULL;
}

到这里代码就写完了,可以使用gradlew.bat assembleMagiskRelease命令直接编译或者用Android Studio,在release文件夹下就会生成zip包,在Magisk安装即可

后记

这个系列到这里就暂时结束了,虽然只介绍了两个方案,不过这两个方案对付现在市面上99%的游戏都是毫无问题的,所以其他偏门方案也就没有介绍的必要了。接下来可能会摸些详细介绍修改的文章,比如在本博客评论区突然火起来的NetHTProtect,对付这玩意实际上就可以考虑使用dll注入反射修改的方式,这样就可以直接无视它的加密以及其他一些检测,当然文章感觉大概率还是会咕咕咕。另外最近已经有些保护厂商做了内存中text段的检验,如何找到并绕过这些检测可能会成为接下来修改游戏的重点。

使用VirtualXposed修改手游

前篇 手游的注入与修改 前言 这篇后续文章原本是打算很快就写完的,但是不知怎么一转眼就已经12月了,眼看今年都要过了,还是赶紧把这篇文章水出来吧。 在上...

阅读全文

手游的注入与修改

已经有很长一段时间没写过游戏修改的文章了,一个原因是现在越来越多的手游厂商都开始给游戏上各种各样的保护,以前直接修改dll或者直接修改so再打包回去的方...

阅读全文

浅谈某NetHTProtect

声明,本文只稍微提一下思路,不提供任何代码~ 这次是第二次写它了,上篇文章讲的也是这玩意,不过这次它加了个新东西,直接暴力dump dll的话会发现method co...

阅读全文

12 条评论

  1. 能否问一下写出来的模块不加载是怎么回事?
    按照教程弄出来的东西编译安装都没问题
    结果调试的时候log什么都不输出

    adb logcat -s “xxxx”
    ——— beginning of system
    ——— beginning of main

    看magisk本身自己的so又已经加载了

    1. 建议检查一下游戏内是否有自己的so,换几个地方打log,如果还有问题也只能你自己检查

  2. mark :mrgreen:
    个人感觉网易盾的自己写dll去反射玩法限制太低了。而且有so注入检测最新网易盾。NetHTProtect,我研究到网易盾的我直接还原方法体后修改放回去加载。干掉了替换检测。也可以写Va进行注入。xposed!都行。有兴趣可以扣扣聊

  3. 有点好奇实时检验内存的Text段不会降低执行效能吗?
    还是有什麽我不懂的黑科技 :lol:

欢迎留言

1 + 2 =