Android逆向-基础与实践 (八) Zygote注入

前言:

Zygote指的是 Android 操作系统的孵化器进程,即 Zygote 进程,这篇文章讲介绍基于 Zygote 的注入技术的实现方式。

一、Zygote注入技术的原理

Zygote 是一个很重要的进程,因为绝大部分的应用程序进程都是由 Zygote 进程 "fork" 生成的。 "fork" 是 Linux 操作系统中的一种进程复用技术,在这里需要了解的是,如果进程 A 执行 fork 操作生成了进程 B,那么进程 B 在创建时便拥有和进程 A 完全相同的模块信息。

Zygote 注入的最终目的是将指定模块注入游戏进程,但该过程并非是直接把指定模块注入游戏进程,而是先把模块注入 Zygote 进程。 在注入 Zygote 完成之后,在操作系统中启动的应用程序,包括需要注入的游戏,都会由 Zygote 进程 fork 生成,因而在新创建的进程中包含了已注入 Zygote 进程的模块,通过这种间接的方式完成了注入。 Zygote 注入的方式更加隐蔽,由于 Zygote 属于系统级进程,所以其注入方式的功能更加强大。

二、Zygote 注入技术的实现流程

Zygote注入流程图:

Zygote注入需要注意两个关键点:

(1) 目标进程需要在注入 Zygote 完成后启动才能成功被注入。

(2) 成功注入 Zygote 之后启动的新进程将包含已注入 Zygote 的模块信息,所以需要在新启动的进程执行前获得控制权,然后判断当前进程是否为目标进程,如果是,则执行其余代码,否则交还控制权。

三、Zygote 注入器的实现方式

注入器是整个流程的开始,也是整个 Zygote 注入流程中最重要的一环,它的作用是把指定的模块注入 Zygote 进程中。

注入器的实现流程:

上图阐述了注入器怎样跨进程执行代码,以及在Linux进程之间通信。

注入器总共执行了三次跨进程调用。

(1)首先调用 mmap 函数申请目标进程的地址空间,用于保存注入的 shellcode 代码。

(2)执行注入的 shellcode (shellcode 是注入目标进程中并执行的汇编代码)。

(3)调用 munmap 函数释放之前申请的内存。

shellcode实现的功能为:将指定模块加载到目标进程中,并运行该模块的函数代码。 

 

注入器各功能的实现过程:

1.关闭 SeLinux

SeLinux 是 Linux 的一个安全子系统,在 Android 4.4 以后的版本中默认打开该功能。SeLinux 功能会影响注入的实现,因此需要事先关闭 SeLinux 功能。

关闭 SeLinux 功能时,需要进行如下三步操作。

(1)获取 SeLinux 的配置目录。有两种方法,一种是查看 "/sys/fs/selinux" 目录的文件系统状态,如果等于 "SELINUX_MAGIC"(0xf97cff8c),则为 SeLinux 的配置目录;另一种是通过 "/proc/mounts" 文件查看是否存在 SeLinux 的路径。

(2)获取 SeLinux 配置文件中 SeLinux 功能的开关状态。

(3)关闭 SeLinux。

2.附加到 Zygote,保存进程现场

附加到 Zygote 进程的关键代码如下:

static int Attach(pid_t pid)
{
    int res = 0;
    int status = 0;

    res = ptrace(PTRACE_ATTACH,pid,NULL,NULL);
    if(res < 0)
    {
        LOGE("<injectso.c:%d> attach failed:%s\n", __LINE__, strerror(errno));
        return -1;
    }
    WaitPid(pid, &status, 0);
    return res;
}

在 Android 系统中使用 ptrace 函数可操控其他进程。ptrace 函数提供了一系列宏来完成不同的功能,可参考 ptrace 注入部分的函数介绍。

附加的操作比较简单,可使用 PTRACE_ATTACH 的方式。涉及进程通信的 WaitPid 函数代码如下:

int WaitPid(pid_t pid, int *status, int option)
{
    while(waitpid(pid, status, option) == -1)
    {
        if(errno == EINTR)
            continue;
        else
            return -1;
    }
    return 0;
}

在 Android 系统中进程同步的信号机制跟 Linux 相似。注入器对 Zygote 进程附加之后,Zygote 进程会接到一个 SIGSTOP 信号,接下来它会处理这个信号。这时注入器就需要等待 Zygote 进程,一般是阻塞式的等待,需要调用 WaitPid 函数处理等待操作。

waitpid 是一个 Linux 系统函数,它有几个选项,具体可以参考 http://linux.die.net/man/3/waitpid。waitpid 最常用的功能是阻塞当前进程,等待指定进程的状态发生变化。

在 options 为 0 时,将使用默认的阻塞式等待,直到 Zygote 进程处理完 SIGSTOP 信息。当 Zygote 进程停止时,进程的状态发生了变化, waitpid 函数将返回 Zygote 的进程状态,此时注入器被唤醒。

在 WaitPid 函数的内部调用了 waitpid 函数,同时包括一个循环和 errno 的判断处理。 WaitPid 函数的功能为:如果注入器收到使 waitpid 中断的信号,则不处理该信号并继续等待;若为其他错误(如参数错误等),则直接返回。

Zygote 停止后,注入器利用 PTRACE_GETREGS 方式获取寄存器并备份执行环境,在恢复进程时可获得原始的执行环境。

3.获取 Zygote 进程中关键函数的地址

以 mprotect 函数为例(mprotect 函数实现于 libc.so 模块中),讲解如何获取 Zygote 进程中关键函数的地址。

首先在注入器的进程中加载 libc.so 模块,并获得 libc.so 模块的基址(定义为 cbase 变量);然后获取注入器进程中 mprotect 函数的内存地址(定义为 maddr 变量),将 mprotect 函数的内存地址减去 libc.so 模块的基址,得到该函数相对偏移(定义为 offset 变量),计算方式为:offset = maddr - cbase;最后获取 libc.so 模块在 Zygote 进程中的基址(定义为 zcbase 变量),Zygote 进程中 mprotect 函数的地址计算方式为: zcbase + offset。

在 Zygote 进程中获取关键函数的地址代码如下:

// get remote mmap, munmap, mprotect func addr
libcHandler = dlopen(LIBC_PATH, RTLD_NOW);
mmap_self_addr = dlsym(libcHandler, MMAP_NAME);
munmap_self_addr = dlsym(libcHandler, MUNMAP_NAME);
mprotect_self_addr = dlsym(libcHandler, MPROTECT_NAME);

mmap_remote_addr = getRemoteSymbolAddress(pid, LIBC_PATH, mmap_self_addr);
munmap_remote_addr = getRemoteSymbolAddress(pid, LIBC_PATH, munmap_self_addr);
mprotect_remote_addr = getRemoteSymbolAddress(pid, LIBC_PATH, mprotect_self_addr);

其中,getRemoteSymbolAddress 函数用于获取 Zygote 进程中的函数地址。

getRemoteSymbolAddress 函数的实现如下:

void *getRemoteSymbolAddress(pid_t pid, char *moduleName, void *selfSymbolAddress)
{
    void *selfModuleBase = getModuleBase(-1, moduleName);
    void *remoteModuleBase = getModuleBase(pid, moduleName);
    
    if(remoteModuleBase == (void *) -1)
        return 0;

    
    return (selfSymbolAddress - selfModuleBase + remoteModuleBase);
}

其中,getModuleBase 函数通过读取 "/proc/%d/maps" 文件获取模块的基址,具体实现代码如下:

void *getModuleBase(pid_t pid, char *moduleName) {
	FILE *fp;
	unsigned long baseValue;
	char mapFilePath[256];
	char fileLineBuffer[1024];

	if (pid < 0)
	{
		sprintf(mapFilePath, "/proc/self/maps");
	}
	else
	{
		sprintf(mapFilePath, "/proc/%d/maps", pid);
	}

	fp = fopen(mapFilePath, "r");
	if (fp == NULL)
		return (void *)-1;

	baseValue = -1;
	while (fgets(fileLineBuffer, sizeof(fileLineBuffer), fp) != NULL)
	{
		if (strstr(fileLineBuffer, moduleName))
		{
			char *pszModuleAddress = strtok(fileLineBuffer, "-");
			if (pszModuleAddress)
			{
				baseValue = strtoul(pszModuleAddress, NULL, 16);

				if (baseValue == 0x8000)
					baseValue = 0;

				break;
			}
		}
	}

	fclose(fp);

	return (void *)baseValue;
}

4.调用 mmap 函数

mmap 函数的定义如下:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)

根据 ARM 编译器的规则,前4个参数会被直接放入 r0~r3 寄存器中,其余参数会被放入堆栈中,因此如果要调用 mmap 函数,则需要设置调用环境,包括 寄存器和堆栈。

解决堆栈布局的思路为:将参数信息布置到 Zygote 进程的寄存器和堆栈中,设置 PC 寄存器为 mmap 函数的地址,将 lr 寄存器赋值为 0;然后执行 PTRACE_CONT 操作,让 Zygote 进程从 mmap 的函数头开始执行,并让注入器进入暂停状态;当 Zygote 进程执行完 mmap 函数时,会跳转至 lr 寄存器保存的地址中,当跳转地址为 0 时, Zygote 进程进入异常停止状态;最后,注入器调用的 WaitPid 函数将返回 Zygote 进程状态改变的信息。

设置参数的代码实现如下:

// prepare call mmap in the remote process
usingRegisters.uregs[0] = 0;
usingRegisters.uregs[1] = 0x4000;
usingRegisters.uregs[2] = PROT_EXEC | PROT_READ | PROT_WRITE;
usingRegisters.uregs[3] = MAP_ANONYMOUS | MAP_PRIVATE;
usingRegisters.uregs[13] -= sizeof(long);
ptrace(PTRACE_POKEDATA, pid, (void *)(usingRegisters.uregs[13]), 0);
usingRegisters.uregs[13] -= sizeof(long);
ptrace(PTRACE_POKEDATA, pid, (void *)(usingRegisters.uregs[13]), (void *)0xffffffff);

usingRegisters.uregs[15] = (long)mmap_remote_addr;
if (usingRegisters.uregs[15] & 1u) {
	usingRegisters.uregs[15] &= (~1u);
	usingRegisters.uregs[16] |= CPSR_T_MASK;
} else {
	usingRegisters.uregs[16] &= ~CPSR_T_MASK;
}

// call mmap in the remote process
	ret = invokeRemoteSyscall(pid, &usingRegisters);

5.配置 shellcode

前面获取了关键函数(包括 dlopen 、 mprotect等)在 Zygote 进程中的地址,然后用 shellcode 配置函数地址。

shellcode 是一段汇编代码,对函数地址、字符串地址的引用都使用了相对地址。

shellcode 的 ARM 汇编代码如下:

_inject_code_start:
	NOP
	NOP

	LDR R1, =0
	LDR R0, _so_path_addr
	LDR R3, _dlopen_addr
	BLX R3
	MOV R1, #1
	SUBS R4, R0, #0
	BEQ 3f

	LDR R1, _so_init_func_addr
	LDR R3, _dlsym_addr
	BLX R3
	MOV R1, #2
	SUBS R3, R0, #0
	BEQ 1f
	MOV R1, #3
	LDR R0, _so_func_arg_addr
	BLX R3
	SUBS R0, R0, #0
	BEQ 2f
	MOV R1, #4
1:
	MOV R0, R4
	LDR R3, _dlclose_addr
	BLX R3
2:
	MOV PC, #0
3:	LDR R3, _dlerror_addr
	BLX R3
	MOV R2, R0
	MOV PC, #0

其中的标号为相对地址,指向的位置存放了前面已获取的函数和字符串地址。shellcode 代码总共执行了如下三个操作。

(1) 调用 dlopen 函数加载指定模块。

(2) 调用 dlsym 函数获取模块中关键的函数地址。

(3) 调用已获取的关键函数。

其中,shellcode 进行简单的容错处理,最后将 PC 寄存器设置为 0,主动使进程停止,其执行过程与前面调用 mmap 的过程相似。

6. 远程调用 shellcode

接下来需要远程调用 shellcode,跟前面远程调用 mmap 的原理基本一致。唯一的区别在于:在调用 mmap 前,需要将 lr 寄存器设置为 0; shellcode 主动将 PC 寄存器赋值为 0;

shellcode 的调用在 invokeRemoteShellcode函数中实现,其实现代码及解释如下:

// this function do a routing of executing shellcodeDataBuffer in the remote process with ptrace
static int invokeRemoteShellcode(pid_t pid, struct pt_regs *regs) {
	int ret=0, waitStatus=0;

	do {
		// this put our regs setting to the remote process memory space,
			//the code must have been stored with POKEDATA at first
		ret = ptrace(PTRACE_SETREGS, pid, NULL, regs);
		if (ret < 0) {
			LOGE("<injectso.c:%d> set regs failed:%s\n", __LINE__, strerror(errno));

			break;
		}

		// let shellcodeDataBuffer enter executing routing
		ret = ptrace(PTRACE_CONT, pid, NULL, NULL);
		if (ret < 0) {
			LOGE("<injectso.c:%d> CONT failed:%s\n", __LINE__, strerror(errno));
			
			break;
		}

		// wait for the shellcodeDataBuffer to finish executing,this must be triggled by shellcodeDataBuffer
		ret = WaitPid(pid, &waitStatus, 0);
		if (ret < 0) {
			LOGE("<injectso.c:%d> waitpid failed:%s\n", __LINE__, strerror(errno));
			
			break;
		}
	} while (0);

	return ret;
}

7. 调用 munmap 释放内存

在执行完前面的6个步骤后,整个流程的主要功能已经实现。接下来调用 munmap 函数释放之前申请的内存。

通过 PTRACE_CONT 方式调用 munmap 函数代码如下:

// prepare call munmap in the remote process
memcpy(&usingRegisters, &orignalRegisters, sizeof(orignalRegisters));
			
usingRegisters.uregs[0] = (long)mmap_return;
usingRegisters.uregs[1] = 0x4000;

usingRegisters.uregs[15] = (long)munmap_remote_addr;
if (usingRegisters.uregs[15] & 1u) {
	usingRegisters.uregs[15] &= (~1u);
	usingRegisters.uregs[16] |= CPSR_T_MASK;
} else {
	usingRegisters.uregs[16] &= ~CPSR_T_MASK;
}

// call munmap in the remote process, don't need check result
//because we will restore remote process to the original status right after now.
ret = invokeRemoteSyscall(pid, &usingRegisters);

8. 恢复进程到初始状态

将之前保存的进程寄存器等环境信息备份至 Zygote 进程中,最后调用 Detach 操作,Zygote 进程将继续执行。

四、注入 Zygote的模块功能实现

注入 Zygote 进程的模块也会注入游戏进程中,该模块除包含需要执行的特定功能外,还需要实现如下两个功能:

1.劫持新启动的进程并获得控制权,可通过 Hook 的关键系统函数感知新进程启动事件。

2.获取当前进程的信息并识别是否为目标进程。

针对这两个功能有很多种实现方法,下面介绍其中1种实现方法:

(1)步骤一:通过 Hook 系统模块 /system/lib/libdvm.so 的导出函数 _Z17dvmLoadNativeCodePKcP6ObjectPPc,来拦截进程加载模块的操作。

(2)步骤二:根据 Hook 的函数传入的模块路径,可以判断当前进程是否为目标进程。

五、完整代码已经上传到附件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值