Android进程的so注入--Poison(稳定注入版)

2023-11-21 07:40

本文主要是介绍Android进程的so注入--Poison(稳定注入版),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/53869796

Android进程的so注入已经是老技术了,网上能用的Android注入的工程也有很多,虽然分享代码的作者在测试的时候能注入成功,但是其他的同学使用这些代码的时候总是出现这样或者那样的问题。在Android逆向学习的这段时间里,我也陆续测试了几个作者给出的Android的注入的代码,但是总是效果不明显,今天就学习一下大牛boyliang分享的Android的so注入的代码框架Poison,作者boyliang的注入代码也是基于大牛古河分享的Andorid注入的代码修改过来的,做了一些改进和优化,后面的文章中就对注入代码进行学习一下。


一、注入工程Poison-master代码的下载

1)作者boyliang的注入代码的原下载地址已经失效了,但是从github上还是可以查找的,下载地址为:https://github.com/matrixhawk/Poison(缺少编译的Android.mk文件和Application.mk配置文件,需要自己编写)。

windows环境下,NDK编译需要添加的include头文件(根据编译的版本需要进行修改)
右击项目 --> Properties --> 左侧C/C++ General --> Paths and Symbols --> 右侧Includes --> GNU C++(.cpp) --> Add
${NDKROOT}\platforms\android-19\arch-arm\usr\include
${NDKROOT}\sources\cxx-stl\gnu-libstdc++\4.8\include 
${NDKROOT}\sources\cxx-stl\gnu-libstdc++\4.8\libs\armeabi\include
${NDKROOT}\toolchains\arm-Linux-androideabi-4.8\prebuilt\windows\lib\gcc\arm-linux-androideabi\4.8\include


2)为Poison-master工程添加编译需要的Android.mk文件和Application.mk文件如下:

Android.mk文件:

LOCAL_PATH := $(call my-dir)  include $(CLEAR_VARS)  # 编译生成的模块的名称
LOCAL_MODULE := poison  # 需要被编译的源码文件 
LOCAL_SRC_FILES := poison.c \elf_utils.c \ptrace_utils.c \tools.c # 支持log日志打印android/log.h里函数调用的需要
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog  # 编译模块生成可执行文件 
include $(BUILD_EXECUTABLE)  
Application.mk文件:

# 编译生成的模块运行支持的平台
APP_ABI := armeabi-v7a  
# 设置编译连接的工具的版本
#NDK_TOOLCHAIN_VERSION = 4.9 


3)即便是如此,使用NDK编译源码工程Poison-master,仍然会提示“jni/ptrace_utils.c:12:28: fatal error: cutils/sockets.h: No such file or directory”等的错误,这个问题怎么解决呢?作者boyliang已经为我们解决好了这个问题,需要对Android官方提供的NDK工具进行path,添加源码编译需要的一些系统的头文件,需要添加的Android系统头文件的ndk-patch可以从作者的github地址:https://github.com/boyliang/ndk-patch进行下载。使用的用法如下:



4)哈哈,编译成功了,Android进程so注入的工具就有了。




二、注入工程代码Poison的说明

1)关于Android的so注入的详细的原理和细节,可以参考博文http://blog.csdn.net/qq1084283172/article/details/46859931,这篇博文里已经把Android的so注入的代码分析的很清楚了,基本把古河大牛的LibInject都说明白了。古河大牛的LibInject中涉及到了Android函数的Hook部分的模板的编写,作者boyliang的代码中没有涉及到Android函数的Hook部分的代码,这部分后面再研究,大牛boyliang和古河的so注入部分的思路是一样的,只不过boyliang的so注入代码中考虑到了"zygote"进程注入的特殊情况,在对进程目标pid进程ptrace时,有着特殊的处理。





2)Andorid的so注入时,针对"zygote"进程注入的特殊处理的原因,可以参考《Android so注入》这篇博文给出的解释原因。

A.针对"zygote"进程注入时,so库文件必须存放在“/system/lib/“路径下。



B.针对"zygote"进程注入时,ptrace操作"zygote"进程时的特殊处理(直接搬过来)。


可以看到ptrace_attach只是对ptrace(PTRACE_ATTACH,…)做了一个封装,但是在attach还做了一系列的waitpid和ptrace(PTRACE_SYSCALL,…)的操作,这是为什么呢。这里我们需要复习一下ptrace的执行过程,一旦对某个进程执行了ptrace操作,那么当目标进程执行系统调用,也就是把执行的控制权交给内核的时候,内核会检查当前进程是否被标记为”traced”,如果是,那么内核就会把控制权转交给跟踪进程。而此时跟踪进程正调用了wait函数在等待内核函数的信号,当接受到信号后跟踪进程就能继续执行。但是有时候会遇到被跟踪进程执行的系统调用是一个阻塞函数,比如recv,read,这样当目标进程系统调用开始的时候(PTRACE_ATTACH在系统调用开始暂停目标进程),它就会被暂停,而跟踪进程会被唤醒,一般这个时候跟踪进程会执行ptrace(PTRACE_GETREGS,…)等操作,这需要目标进程从系统调用返回,但是目标进程这个时候已经阻塞在系统调用里面了,无法返回,ptrace就会产生错误。知道这个情况,我们就很容易理解这段代码了。首先使用PTRACE_ATTACH标记目标进程,然后等待目标进程返回,这里的WUNTRACED表示目标进程暂停后就立即返回,而不是等待目标进程结束。当目标进程进入系统调用后,通知跟踪进程,跟踪进程再调用ptrace(PTRACE_SYSCALL,…)然后等待(PTRACE_SYSCALL在目标进程进入/退出系统调用的时候暂停目标进程),表示等待目标进程进入系统调用,然后再调用一次ptrace(PTRACE_SYSCALL,…)再等待,表示等待目标进程从系统调用返回,等第三次的wait返回后((可能会被阻塞),就可以进行系统调用了。
这种做法还是有可能会被阻塞,就是第三次wait会等不到信号,也就是目标进程进入系统调用后一直不返回。什么时候会发生这种情况呢?其实zygote就是个很好的例子,这需要对zygote进程有一些了解,这里只简单的分析一下。zygote启动后会进入一个死循环,用来接收AMS的请求连接,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 * Runs the zygote process's select loop. Accepts new connections as 
 * they happen, and reads commands from connections one spawn-request's 
 * worth at a time. 
 * 
 * @throws MethodAndArgsCaller in a child process when a main() should 
 * be executed. 
 */  
private static void runSelectLoopMode() throws MethodAndArgsCaller {  
   	  ArrayList<FileDescriptor> fds = new ArrayList();
       ArrayList <ZygoteConnection> peers = new ArrayList();
       FileDescriptro[] fdArray = new FileDescriptor[4];
        ...... 
       while (true) {//死循环  
           ......  
             
           if (index < 0) {  
               throw new RuntimeException("Error in select()");  
           } else if (index == 0) {//index==0表示selcet接收到的是Zygote的socket的事件  
               ZygoteConnection newPeer = acceptCommandPeer();  
               peers.add(newPeer);  
               fds.add(newPeer.getFileDesciptor());  
           } else {//调用ZygoteConnection对象的runOnce方法,ZygoteConnection是在index == 0时被添加到peers的  
               boolean done;  
               done = peers.get(index).runOnce();  
 
               if (done) {  
                   peers.remove(index);  
                   fds.remove(index);  
               }  
           }  
       }  
	}

index变量表示此时和zygote进程通信的个数,当index=0时也就是说没有socket连接,此时zygote调用acceptCommandPeer函数,该函数等待一个连接并返回一个ZygoteConnection对象。

1
2
3
4
5
6
7
8
9
10
11
/*
 * Waits for and accepts a single command connection. Throws
 * RuntimeException on failure.
 */
private static ZygoteConnection acceptCommandPeer() {
    try {
        return new ZygoteConnection(sServerSocket.accept());
    } catch (IOException ex) {
        throw new RuntimeException("IOException during accept()", ex);
    }
}

也就是说,当没有应用启动时,zygote进程一直处于阻塞状态。所以我们上面代码中的第三次wait会无法返回,解决办法也很简单,就是主动发起一个zygote的连接。我们看到第二个waitpid后面调用了一个connect_to_zygote函数,下面是它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void* connect_to_zygote(void* arg){
	int s, len;
	struct sockaddr_un remote;
//zygote进程接收socket连接的时间间隔是500ms,2s足以保证此socket连接能连接到zygote socket
	LOGI("[+] wait 2s...");
	sleep(2);
	//sleep(0.5);
	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {
		remote.sun_family = AF_UNIX;
		strcpy(remote.sun_path, "/dev/socket/zygote");
		len = strlen(remote.sun_path) + sizeof(remote.sun_family);
		LOGI("[+] start to connect zygote socket");
		connect(s, (struct sockaddr *) &remote, len);
		LOGI("[+] close socket");
		close(s);
	}

	return NULL ;
}

这个函数的功能很简单,先发起socket连接,然后再关闭连接。看上去没有做什么有用的事情,但是它却非常重要,通过连接zygote,它使zygote进程解除了阻塞状态,我们才得以注入进zygote进程。

说明:暂时对zygote这一块不是很熟悉,参考大牛http://zke1ev3n.me/2015/12/02/Android-so注入/的分析理由。


C.针对调用目标pid进程的函数时,对于等待目标pid进程完成so注入需要注意的地方,大牛zke1ev3n也做了深入的说明和代码的补充。

ptrace_dlopen函数构造dlopen函数的参数,然后调用ptrace_call开始加载so的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
int ptrace_call(pid_t pid, uint32_t addr, long *params, int num_params, struct pt_regs* regs) {
	uint32_t i;

	for (i = 0; i < num_params && i < 4; i++) {
		regs->uregs[i] = params[i];
	}

	if (i < num_params) {
		regs->ARM_sp-= (num_params - i) * sizeof(long);
		ptrace_write(pid, (uint8_t *) regs->ARM_sp, (uint8_t *) &params[i], (num_params - i) * sizeof(long));
	}

	regs->ARM_pc= addr;
	if (regs->ARM_pc& 1) {
		/* thumb */
		regs->ARM_pc &= (~1u);
		regs->ARM_cpsr |= CPSR_T_MASK;
	} else {
		/* arm */
		regs->ARM_cpsr &= ~CPSR_T_MASK;
	}

	regs->ARM_lr= 0;	//置子程序的返回地址为空,以便函数执行完后,返回到null地址,产生SIGSEGV错误

	if (ptrace_setregs(pid, regs) == -1 || ptrace_continue(pid) == -1) {
		return -1;
	}

//	waitpid(pid, NULL, WUNTRACED);	
	
	int status = 0;
//	waitpid(pid,&stat,WUNTRACED);
  	pid_t res;
   	waitpid(pid, NULL, WUNTRACED);  
	/*
	 * Restarts  the stopped child as for PTRACE_CONT, but arranges for
	 * the child to be stopped at the next entry to or exit from a sys‐
	 * tem  call,  or  after execution of a single instruction, respec‐
	 * tively.
	 */
	if (ptrace(PTRACE_SYSCALL, pid, NULL, 0) < 0) {
		LOGE("ptrace_syscall");
		return -1;
	}

	waitpid(pid, NULL, WUNTRACED);

	if (ptrace(PTRACE_SYSCALL, pid, NULL, NULL ) < 0) {
		LOGE("ptrace_syscall");
		return -1;
	}

	res = waitpid(pid, NULL, WUNTRACED);

	LOGI("[+] status is %x",status);
    	if (res != pid || !WIFSTOPPED (status))//WIFSTOPPED(status) 若为当前暂停子进程返回的状态,则为真
        	return 0;
	LOGI("[+]done %d\n",(WSTOPSIG (status) == SIGSEGV)?1:0);
	//设置siginal 11信号处理函数
/*	if(signal(SIGSEGV,handler) == SIG_ERR){
		LOGE("[-]can not set handler for SIGSEGV");
	}*/

	return 0;
}

WUNTRACED告诉waitpid,如果子进程进入暂停状态,那么就立即返回。如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。对于使用PTRACE_CONT运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。这里的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。那么什么时候会发生这种错误呢?显然,当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV
这里还需要了解下arm架构的相关知识。首先是函数参数传递,在arm中,函数的前4个参数分别保存在r0-r3中,当参数大于4个,就依次压入栈中。此外,arm处理器实际上支持两套指令集,即arm和thumb。thumb为16位,arm为32位。这里通过判断pc的最后一位是否是1来确定指令集,这是因为编译器在用thmub指令集编译一个函数时会将函数的符号地址设置成真正的映射地址+1实现arm和thumb混编。此外,在切换arm和thumb指令时,还会修改CPSR处理器。在arm中,出了r0-r15这16个处理器,还有状态寄存器CPSR。关于CPSR的其他位这里先不讨论,我们只要知道CPSR寄存器的第低5位T标识了当前的指令集(T=0表示执行arm指令T=1表示执行Thumb指令),所以在切换指令集时需要修改这一位

Arm与Thumb之间的状态切换是通过专用的转移交换指令BX来实现。BX指令以通用寄存器(R0~R15)为操作数,通过拷贝Rn到PC实现绝对跳转。BX利用Rn寄存器中目的地址值的最后一位判断跳转后的状态,如果为“1”表示跳转到Thumb指令集的函数中,如果为“0”表示跳转到Arm指令集的函数中。而Arm指令集的每条指令是32位,即4个字节,也就是说Arm指令的地址肯定是4的倍数,最后两位必定为“00”。所以,直接就可以将从符号表中获得的调用地址模4,看是否为0来判断要修改的函数是用Arm指令集还是Thumb指令集。

三、注入工程Poison-master代码的注入测试

用到的命令:

cd xxxxx\AndroidProject_Poison-master\use_poisonadb push poison /data/local/tmp
adb push libmobisec.so /data/local/tmp
adb shell chmod 0777 /data/local/tmp/poison
adb shell chmod 0777 /data/local/tmp/libmobisec.so
adb shell 
su
ps | grep com.example.androiddecodcat /proc/17569/maps/data/local/tmp/poison /data/local/tmp/libmobisec.so 17569cat /proc/17569/maps | grep libmobisec.soadb logcat -s TTT

注入so库文件libmobisec.so到com.example.androiddecod进程中成功



能编译成功的项目工程下载地址:http://download.csdn.net/detail/qq1084283172/9721443


四、注入工程Poison-master代码的详细注释说明

整个Poison-master工程的代码的结构图:



整个Poison-master工程代码的详细分析注释:

主文件poison.c

#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/wait.h>#include "ptrace_utils.h"
#include "elf_utils.h"
#include "log.h"
#include "tools.h"struct process_hook {// 被注入的目标进程pid_t 		pid;// 注入到目标进程中so文件的路径char 		*dso;
//	void		*dlopen_addr;
//	void 		*dlsym_addr;
//	void		*mmap_addr;
} process_hook = {0, "", NULL, NULL, NULL};// 主函数
int main(int argc, char* argv[]) {// 对传入的参数的个数进行判断(要求3个参数)if(argc < 2)exit(0);// 保存寄存器的状态信息struct pt_regs regs;// 获取注入到目标进程中的so的文件的路径process_hook.dso = strdup(argv[1]);// 获取注入的目标进程的pidprocess_hook.pid = atoi(argv[2]);//	process_hook.dlopen_addr = (void *)atol(argv[3]);
//	process_hook.dlsym_addr = (void *)atol(argv[4]);
//	process_hook.mmap_addr = (void *)atol(argv[5]);// 判断注入到目标进程中so是否存在并且具有可读可执行权限if (access(process_hook.dso, R_OK|X_OK) < 0) {LOGE("[-] so file must chmod rx\n");return 1;}// 获取指定pid进程的名称const char* process_name = get_process_name(process_hook.pid);// 附加目标进程ptrace_attach(process_hook.pid, strstr(process_name,"zygote"));// 打印附加目标进程的信息LOGI("[+] ptrace attach to [%d] %s\n", process_hook.pid, get_process_name(process_hook.pid));// 读取此时目标进程中所有的寄存器的状态信息if (ptrace_getregs(process_hook.pid, &regs) < 0) {LOGE("[-] Can't get regs %d\n", errno);// 读取失败跳转goto DETACH;}// 打印目标进程的寄存器pc和R7的信息LOGI("[+] pc: %x, r7: %d", regs.ARM_pc, regs.ARM_r7);// dlsym参数为当前进程中的调用地址,获取目标pid进程中dlsy函数的调用地址void* remote_dlsym_addr = get_remote_address(process_hook.pid, (void *)dlsym);// 获取目标pid进程中dlopen函数的调用地址void* remote_dlopen_addr =  get_remote_address(process_hook.pid, (void *)dlopen);//	if(remote_dlopen_addr == NULL && remote_dlsym_addr != NULL){
//		remote_dlopen_addr = (void *)((uint32_t)remote_dlsym_addr - (uint32_t)process_hook.dlsym_addr + (uint32_t)process_hook.dlopen_addr);
//	}else if(remote_dlopen_addr != NULL && remote_dlsym_addr == NULL){
//		remote_dlsym_addr = (void *)((uint32_t)remote_dlopen_addr - (uint32_t)process_hook.dlopen_addr + (uint32_t)process_hook.dlsym_addr);
//	}else if(remote_dlopen_addr == NULL && remote_dlsym_addr == NULL){
//		LOGE("[-] Can not found dlopen_addr & dlsym_addr.\n");
//		goto DETACH;
//	}
//// 打印目标进程的函数dlopen和dlsym的调用地址LOGI("[+] remote_dlopen address %p\n", remote_dlopen_addr);LOGI("[+] remote_dlsym  address %p\n", remote_dlsym_addr);// 调用目标pid进程的dlopen函数加载指定的so库文件,获取返回的加载的模块的基址if(ptrace_dlopen(process_hook.pid, remote_dlopen_addr, process_hook.dso) == NULL){LOGE("[-] Ptrace dlopen fail. %s\n", dlerror());}// 针对此时不同的模式,设置目标pid进程的CPSR寄存器的值if (regs.ARM_pc & 1 ) {// thumbregs.ARM_pc &= (~1u);regs.ARM_cpsr |= CPSR_T_MASK;} else {// armregs.ARM_cpsr &= ~CPSR_T_MASK;}// 恢复目标pid进程的寄存器的状态即恢复到注入前的运行状态if (ptrace_setregs(process_hook.pid, &regs) == -1) {LOGE("[-] Set regs fail. %s\n", strerror(errno));// 失败进行跳转goto DETACH;}// 打印注入成功的消息LOGI("[+] Inject success!\n");DETACH:// 结束对目标pid进程的附加ptrace_detach(process_hook.pid);// 打印注入工作完成的消息LOGI("[+] Inject done!\n");return 0;
}
ptrace_utils.h文件

/** ptrace_utils.h**  Created on: 2013-6-19*      Author: boyliang*/#ifndef PTRACE_UTILS_H_
#define PTRACE_UTILS_H_#define CPSR_T_MASK		( 1u << 5 )int ptrace_getregs(pid_t pid, struct pt_regs* regs);int ptrace_setregs(pid_t pid, struct pt_regs* regs);int ptrace_attach( pid_t pid , int zygote);int ptrace_detach( pid_t pid );int ptrace_continue(pid_t pid);int ptrace_syscall(pid_t pid);int ptrace_write(pid_t pid, uint8_t *dest, uint8_t *data, size_t size);int ptrace_read( pid_t pid,  uint8_t *src, uint8_t *buf, size_t size );int ptrace_call(pid_t pid, uint32_t addr, long *params, int num_params, struct pt_regs* regs);void* ptrace_dlopen(pid_t target_pid, void* remote_dlopen_addr, const char*  filename);#endif /* PTRACE_UTILS_H_ */
ptrace_utils.c文件

/** ptrace_utils.c**  Created on: 2013-6-26*      Author: boyliang*/#include <asm/ptrace.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <dlfcn.h>
#include <cutils/sockets.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <pthread.h>#include "ptrace_utils.h"
#include "log.h"/*** read registers' status*/
int ptrace_getregs(pid_t pid, struct pt_regs* regs) {if (ptrace(PTRACE_GETREGS, pid, NULL, regs) < 0) {perror("ptrace_getregs: Can not get register values");return -1;}return 0;
}/*** set registers' status*/
int ptrace_setregs(pid_t pid, struct pt_regs* regs) {if (ptrace(PTRACE_SETREGS, pid, NULL, regs) < 0) {perror("ptrace_setregs: Can not set register values");return -1;}return 0;
}// 解除zygote进程的阻塞状态
static void* connect_to_zygote(void* arg){int s, len;struct sockaddr_un remote;LOGI("[+] wait 2s...");// 休眠一下sleep(2);/**** zygote启动后会进入一个死循环,用来接收AMS的请求连接.* 当没有应用启动时,zygote进程一直处于阻塞状态。* 所以我们后面代码中的第三次wait会无法返回,解决办法也很简单,就是主动发起一个zygote的连接。* 我们看到第二个waitpid后面调用了一个connect_to_zygote函数。*/// 创建socket套接字if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {// 设置连接的套接字的协议类型remote.sun_family = AF_UNIX;// 设置连接的套接字的目标strcpy(remote.sun_path, "/dev/socket/zygote");// 设置传递的参数的字节长度len = strlen(remote.sun_path) + sizeof(remote.sun_family);LOGI("[+] start to connect zygote socket");// 向"/dev/socket/zygote"目标套接字发起连接connect(s, (struct sockaddr *) &remote, len);LOGI("[+] close socket");// 关闭socket套接字close(s);}/**** 这个函数的功能很简单,先发起socket连接,然后再关闭连接。* 看上去没有做什么有用的事情,但是它却非常重要,* 通过连接zygote,它使zygote进程解除了阻塞状态,* 我们才得以注入进zygote进程。* 参考网址:http://zke1ev3n.me/2015/12/02/Android-so%E6%B3%A8%E5%85%A5/*/return NULL ;
}/*** attach to target process 附加目标进程*/
int ptrace_attach(pid_t pid, int zygote) {if (ptrace(PTRACE_ATTACH, pid, NULL, 0) < 0) {LOGE("ptrace_attach");return -1;}waitpid(pid, NULL, WUNTRACED);/** Restarts  the stopped child as for PTRACE_CONT, but arranges for* the child to be stopped at the next entry to or exit from a sys‐* tem  call,  or  after execution of a single instruction, respec‐* tively.*/if (ptrace(PTRACE_SYSCALL, pid, NULL, 0) < 0) {LOGE("ptrace_syscall");return -1;}waitpid(pid, NULL, WUNTRACED);// 针对zygote进程的特殊处理if (zygote) {// 当进程为zygote时,需要考虑为zygote进程解除阻塞状态,使进程注入得以进行connect_to_zygote(NULL);}// 当目标进程在下次进/出系统调用时被附加调试if (ptrace(PTRACE_SYSCALL, pid, NULL, NULL ) < 0) {LOGE("ptrace_syscall");return -1;}// 等待进程附加操作返回waitpid(pid, NULL, WUNTRACED);return 0;
}/*** detach from target process*/
int ptrace_detach( pid_t pid )
{if ( ptrace( PTRACE_DETACH, pid, NULL, 0 ) < 0 ){LOGE( "ptrace_detach" );return -1;}return 0;
}int ptrace_continue(pid_t pid) {if (ptrace(PTRACE_CONT, pid, NULL, 0) < 0) {LOGE("ptrace_cont");return -1;}return 0;
}int ptrace_syscall(pid_t pid) {return ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
}/*** write data to dest 向目标pid进程中写入数据(4字节对齐)*/
int ptrace_write(pid_t pid, uint8_t *dest, uint8_t *data, size_t size) {uint32_t i, j, remain;uint8_t *laddr;union u {long val;char chars[sizeof(long)];} d;j = size / 4;remain = size % 4;laddr = data;for (i = 0; i < j; i++) {memcpy(d.chars, laddr, 4);ptrace(PTRACE_POKETEXT, pid, (void *)dest, (void *)d.val);dest += 4;laddr += 4;}if (remain > 0) {d.val = ptrace(PTRACE_PEEKTEXT, pid, (void *)dest, NULL);for (i = 0; i < remain; i++) {d.chars[i] = *laddr++;}ptrace(PTRACE_POKETEXT, pid, (void *)dest, (void *)d.val);}return 0;
}// 从目标pid进程中读取数据(4字节对齐)
int ptrace_read( pid_t pid,  uint8_t *src, uint8_t *buf, size_t size )
{uint32_t i, j, remain;uint8_t *laddr;union u {long val;char chars[sizeof(long)];} d;j = size / 4;remain = size % 4;laddr = buf;for ( i = 0; i < j; i ++ ){d.val = ptrace( PTRACE_PEEKTEXT, pid, src, 0 );memcpy( laddr, d.chars, 4 );src += 4;laddr += 4;}if ( remain > 0 ){d.val = ptrace( PTRACE_PEEKTEXT, pid, src, 0 );memcpy( laddr, d.chars, remain );}return 0;
}// 调用目标pid进程中的指定函数addr
int ptrace_call(pid_t pid, uint32_t addr, long *params, int num_params, struct pt_regs* regs) {uint32_t i;// 在arm中,函数的前4个参数使用r0-r4的寄存器传递for (i = 0; i < num_params && i < 4; i++) {// 设置调用目标pid进程中的函数需要的参数regs->uregs[i] = params[i];}// 当被调用的函数的参数个数超过4个时,其他的参数通过栈进行传递if (i < num_params) {// 抬高函数的栈顶regs->ARM_sp-= (num_params - i) * sizeof(long);// 向目标pid进程的内存中写入函数调用需要的超过4个的其他参数ptrace_write(pid, (uint8_t *) regs->ARM_sp, (uint8_t *) ¶ms[i], (num_params - i) * sizeof(long));}// 设置目标pid进程的pc为将被调用的函数的地址regs->ARM_pc= addr;// 针对当前进程所处的不同模式,进行不同的处理if (regs->ARM_pc& 1) {/* thumb模式 */regs->ARM_pc &= (~1u);regs->ARM_cpsr |= CPSR_T_MASK;} else {/* arm模式 */regs->ARM_cpsr &= ~CPSR_T_MASK;}// 设置函数调用的返回地址为0,调用的函数执行完,跳回到当前进程中regs->ARM_lr= 0;// 设置目标pid进程的寄存器的状态,并调用addr函数if (ptrace_setregs(pid, regs) == -1 || ptrace_continue(pid) == -1) {return -1;}// 等待函数的调用完成waitpid(pid, NULL, WUNTRACED);return 0;
}//static void* thread_connect_to_zygote(void* arg){
//	int s, len;
//	struct sockaddr_un remote;
//
//	LOGI("[+] wait 2s...");
//	sleep(2);
//
//	if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) != -1) {
//		remote.sun_family = AF_UNIX;
//		strcpy(remote.sun_path, "/dev/socket/zygote");
//		len = strlen(remote.sun_path) + sizeof(remote.sun_family);
//		LOGI("[+] start to connect zygote socket");
//		connect(s, (struct sockaddr *) &remote, len);
//		LOGI("[+] close socket");
//		close(s);
//	}
//
//	return NULL ;
//}// 当目标pid进程为zygote时,加载so库文件之前,需要的测试处理
static int zygote_special_process(pid_t target_pid){LOGI("[+] zygote process should special take care. \n");struct pt_regs regs;// 获取目标pid进程的寄存器的状态值if (ptrace_getregs(target_pid, &regs) == -1)return -1;// 获取目标pid进程的getpid函数的调用地址void* remote_getpid_addr = get_remote_address(target_pid, getpid);LOGI("[+] Remote getpid addr %p.\n", remote_getpid_addr);// 判断获取目标pid进程的getpid函数的调用地址是否成功if(remote_getpid_addr == NULL){return -1;}pthread_t tid = 0;// 创建线程再次调用connect_to_zygote解除zygote进程的阻塞状态pthread_create(&tid, NULL, connect_to_zygote, NULL);// 释放线程pthread_detach(tid);// 调用目标pid进程中的getpid函数if (ptrace_call(target_pid, remote_getpid_addr, NULL, 0, &regs) == -1) {LOGE("[-] Call remote getpid fails");return -1;}// 获取上面的函数调用完后目标pid进程的寄存器的状态,主要是为了获取getpid函数的返回值if (ptrace_getregs(target_pid, &regs) == -1)return -1;// 打印调用getpid函数完后,目标pid进程的寄存器的状态LOGI("[+] Call remote getpid result r0=%x, r7=%x, pc=%x, \n", regs.ARM_r0, regs.ARM_r7, regs.ARM_pc);return 0;
}// 调用目标pid进程的dlopen函数加载指定的so库文件,并返回加载的模块的基址
void* ptrace_dlopen(pid_t target_pid, void* remote_dlopen_addr, const char*  filename){struct pt_regs regs;// 获取目标pid进程的寄存器的状态值if (ptrace_getregs(target_pid, &regs) == -1)return NULL ;// 判断目标pid进程是否是zygote进程;如果是,加载so库文件之前,进行相应的测试处理if (strcmp("zygote", get_process_name(target_pid)) == 0 && zygote_special_process(target_pid) != 0) {return NULL ;}// 在目标pid进程中调用dlopen函数需要的参数long mmap_params[2];// filename为将要加载到目标pid进程中的so的路径字符串// 要将filename字符串写入到目标pid进程中,filename_len即为需要分配的内存空间的大小size_t filename_len = strlen(filename) + 1;// 调用目标pid进程的mmap函数申请内存空间,用以保存filename字符串(即将要加载的so文件的路径)void* filename_addr = find_space_by_mmap(target_pid, filename_len);// 判断在目标pid进程是否调用mmap函数分配内存空间成功if (filename_addr == NULL ) {LOGE("[-] Call Remote mmap fails.\n");return NULL ;}// 将filename字符串(即将要加载的so文件的路径)写入到目标pid进程的内存地址filename_addr中ptrace_write(target_pid, (uint8_t *)filename_addr, (uint8_t *)filename, filename_len);// dlopen函数的参数--需要加载的so文件的路径字符串mmap_params[0] = (long)filename_addr;// dlopen函数的参数--flag,加载的要求mmap_params[1] = RTLD_NOW | RTLD_GLOBAL;// 获取目标pid进程中的dlopen函数的调用地址(调用参数已经准备好)remote_dlopen_addr = (remote_dlopen_addr == NULL) ? get_remote_address(target_pid, (void *)dlopen) : remote_dlopen_addr;if (remote_dlopen_addr == NULL) {LOGE("[-] Get Remote dlopen address fails.\n");return NULL;}// 在目标pid进程调用dlopen函数,加载filename_addr指定的so库文件if (ptrace_call(target_pid, (uint32_t) remote_dlopen_addr, mmap_params, 2, &regs) == -1)return NULL;// 获取目标pid进程的寄存器的状态值,主要是为了获取上面 dlopen函数调用的返回值if (ptrace_getregs(target_pid, &regs) == -1)return NULL;LOGI("[+] Target process returned from dlopen, return r0=%x, r7=%x, pc=%x, \n", regs.ARM_r0, regs.ARM_r7, regs.ARM_pc);// 返回目标pid进程中调用dlopen函数的返回的内存加载的模块基址return regs.ARM_pc == 0 ? (void *) regs.ARM_r0 : NULL;
}
tool.h文件

/** tool.h**  Created on: 2013-7-5*      Author: boyliang*/#ifndef TOOL_H_
#define TOOL_H_#include <stdio.h>
#include <dlfcn.h>// 获取指定内存加载模块的导出函数的地址
void *get_method_address(const char *soname, const char *methodname);// 获取目标pid进程的名称字符串
const char* get_process_name(pid_t pid);#endif /* TOOL_H_ */
tool.c文件

/** tool.c**  Created on: 2013-7-5*      Author: boyliang*/#include <stdio.h>
#include <dlfcn.h>
#include <stddef.h>// 获取指定内存加载模块的导出函数的地址
void *get_method_address(const char *soname, const char *methodname) {void *handler = dlopen(soname, RTLD_NOW | RTLD_GLOBAL);return dlsym(handler, methodname);
}// 获取目标pid进程的名称字符串
const char* get_process_name(pid_t pid) {static char buffer[255];FILE* f;char path[255];// 格式化得到字符串"/proc/pid/cmdline"snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);// 读取文件"/proc/pid/cmdline"的内容,获取进程的命令行参数if ((f = fopen(path, "r")) == NULL) {return NULL;}// 读取文件"/proc/pid/cmdline"的第1行字符串内容--进程的名称if (fgets(buffer, sizeof(buffer), f) == NULL) {return NULL;}// 关闭文件fclose(f);return buffer;
}
log.h文件

/** log.h**  Created on: 2013-6-25*      Author: boyliang*/#ifndef LOG_H_
#define LOG_H_#include <android/log.h>// 主要用于消息的log打印#define LOG_TAG "TTT"#ifdef DEBUG
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#else
#define LOGI(...) while(0)
#define LOGE(...) while(0)
#endif#endif /* LOG_H_ */
elf_utils.h文件

/** elf_utils.h**  Created on: 2013-6-19*      Author: boyliang*/#ifndef ELF_UTILS_H_
#define ELF_UTILS_H_#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/mman.h>// 获取目标pid进程中指定so模块的加载基址
void* get_module_base(pid_t pid, const char* module_name);// 在目标pid进程的内存空间中申请内存,申请成功返回的内存地址保存在r0中
void* find_space_by_mmap(int target_pid, int size);// 在目标pid进程的"/system/lib/libc.so"的内存范围内(从内存结束地址往回的方向)查找内存空间
void* find_space_in_maps(int pid, int size);// 通过系统函数的地址查找到该函数所在的模块的名称
int find_module_info_by_address(pid_t pid, void* addr, char *module, void** start, void** end);// 通过指定的内存模块so的路径字符串,获取该内存模块的在目标进程pid中起始地址和结束地址
int find_module_info_by_name(pid_t pid, const char *module, void** start, void** end);// 获取目标pid进程中指定函数的调用地址
void* get_remote_address(pid_t pid, void *local_addr);#endif /* ELF_UTILS_H_ */
elf_utils.c文件

/** elf_utils.c**  Created on: 2013-6-25*      Author: boyliang*/#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/ptrace.h>#include "tools.h"
#include "elf_utils.h"
#include "log.h"// 获取目标pid进程中指定so模块的加载基址
void* get_module_base(pid_t pid, const char* module_name) {FILE *fp;long addr = 0;char *pch;char filename[32];char line[1024];if (pid < 0) {/* self process */snprintf(filename, sizeof(filename), "/proc/self/maps");} else {snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);}fp = fopen(filename, "r");if (fp != NULL) {while (fgets(line, sizeof(line), fp)) {// 判断是否是在目标pid进程的内存中要查找到的so模块if (strstr(line, module_name)) {pch = strtok(line, "-");// 获取目标pid进程中指定模块的基址addr = strtoul(pch, NULL, 16);if (addr == 0x8000)addr = 0;break;}}fclose(fp);}return (void *) addr;
}// 在目标pid进程的内存空间中申请内存,申请成功返回的内存地址保存在r0中
void* find_space_by_mmap(int target_pid, int size) {struct pt_regs regs;// 获取目标pid进程的寄存器的状态if (ptrace_getregs(target_pid, &regs) == -1)return 0;long parameters[10];/* call mmap */parameters[0] = 0;  // addrparameters[1] = size; // sizeparameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC;  // protparameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flagsparameters[4] = 0; //fdparameters[5] = 0; //offset// 获取目标pid进程中的mmap函数的调用地址void *remote_mmap_addr = get_remote_address(target_pid, get_method_address("/system/lib/libc.so", "mmap"));LOGI("[+] Calling mmap in target process. mmap addr %p.\n", remote_mmap_addr);if (remote_mmap_addr == NULL) {LOGE("[-] Get Remote mmap address fails.\n");return 0;}// 调用目标pid进程的mmap函数,在目标pid进程的内存中申请内存空间if (ptrace_call(target_pid, (uint32_t) remote_mmap_addr, parameters, 6, &regs) == -1)return 0;// 获取目标pid进程的寄存器的状态if (ptrace_getregs(target_pid, &regs) == -1)return 0;LOGI("[+] Target process returned from mmap, return r0=%x, r7=%x, pc=%x, \n", regs.ARM_r0, regs.ARM_r7, regs.ARM_pc);// arm中,函数的返回值保存在寄存器r0中,返回在目标pid进程中申请的内存空间的地址return regs.ARM_pc == 0 ? (void *) regs.ARM_r0 : 0;
}// 分割字符串
static char* nexttok(char **strp) {// 以" "为基准分解字符串,将原字符串中第一个" "替换为'\0'// 第一个" "前面的字符串返回在p中,第一个" "后面的字符串在strp中char *p = strsep(strp, " ");// 返回分割的字符串return p == NULL ? "" : p;
}// 在目标pid进程的"/system/lib/libc.so"的内存范围内(从内存结束地址往回的方向)查找内存空间
void* find_space_in_maps(int pid, int size) {char statline[1024];FILE * fp;uint32_t* addr = (uint32_t*) 0x40008000;char *address, *proms, *ptr;const char* tname = "/system/lib/libc.so";const char* tproms = "r-xp";// 获取字符串"/system/lib/libc.so"的长度int tnaem_size = strlen(tname);// 获取字符串"r-xp"的长度int tproms_size = strlen(tproms);// 内存以4字节对齐size = ((size / 4) + 1) * 4;// 格式化得到字符串"/proc/pid/maps"sprintf(statline, "/proc/%d/maps", pid);// 打开文件"/proc/pid/maps"fp = fopen(statline, "r");if (fp == 0)return 0;// 读取文件"/proc/pid/maps"中内容(每次读一行)while (fgets(statline, sizeof(statline), fp)) {// 分割字符串ptr = statline;// 得到内存模块的起始和结束地址address = nexttok(&ptr); // skip address// 内存模块的属性proms = nexttok(&ptr); // skip promsnexttok(&ptr); // skip offsetnexttok(&ptr); // skip devnexttok(&ptr); // skip inode// ptr中最终保存的是加载的内存模块的路径字符串while (*ptr != '\0') {if (*ptr == ' ')ptr++;elsebreak;}// 查找目标so模块if (ptr && proms && address) {// 判断是否是"r-xp"属性的模块if (strncmp(tproms, proms, tproms_size) == 0) {// 判断是否是"/system/lib/libc.so"模块if (strncmp(tname, ptr, tnaem_size) == 0) {// address like afe00000-afe3a000if (strlen(address) == 17) {// 获取内存加载模块/system/lib/libc.so的内存范围的结束地址(方便后面查找内存空间)addr = (uint32_t*) strtoul(address + 9, NULL, 16);// 在目标pid进程的/system/lib/libc.so的内存范围内查找到size大小内存空间addr -= size;printf("proms=%s address=%s name=%s", proms, address, ptr);break;}}}}}// 关闭文件fclose(fp);// 返回在目标进程中查找到的内存空间的地址return (void*) addr;
}// 通过系统函数的地址查找到该函数所在的模块的名称
int find_module_info_by_address(pid_t pid, void* addr, char *module, void** start, void** end) {char statline[1024];FILE *fp;char *address, *proms, *ptr, *p;// 格式化字符串得到"/proc/pid/maps"if ( pid < 0 ) {/* self process */snprintf( statline, sizeof(statline), "/proc/self/maps");} else {snprintf( statline, sizeof(statline), "/proc/%d/maps", pid );}// 打开文件 /proc/pid/mapsfp = fopen( statline, "r" );if ( fp != NULL ) {// 每次一行,读取文件/proc/pid/maps中内容while ( fgets( statline, sizeof(statline), fp ) ) {// 解析读取为一行字符串信息ptr = statline;// 获取模块的起始和结束地址address = nexttok(&ptr); // skip addressproms = nexttok(&ptr); // skip promsnexttok(&ptr); // skip offsetnexttok(&ptr); // skip devnexttok(&ptr); // skip inodewhile(*ptr != '\0') {if(*ptr == ' ')ptr++;elsebreak;}p = ptr;while(*p != '\0') {if(*p == '\n')*p = '\0';p++;}// 4016a000-4016b000if(strlen(address) == 17) {address[8] = '\0';// 获取内存加载模块的起始地址*start = (void*)strtoul(address, NULL, 16);// 获取内存加载模块的结束地址*end   = (void*)strtoul(address+9, NULL, 16);// printf("[%p-%p] %s | %p\n", *start, *end, ptr, addr);// 判断该系统函数的地址是否在该模块的内存范围内if(addr > *start && addr < *end) {// 找到该系统函数所在的内存模块// 保存该内存加载的so模块的文件路径strcpy(module, ptr);fclose( fp );return 0;}}}fclose( fp ) ;}return -1;
}// 通过指定的内存模块so的路径字符串,获取该内存模块的在目标进程pid中起始地址和结束地址
int find_module_info_by_name(pid_t pid, const char *module, void** start, void** end) {char statline[1024];FILE *fp;char *address, *proms, *ptr, *p;if ( pid < 0 ) {/* self process */snprintf( statline, sizeof(statline), "/proc/self/maps");} else {snprintf( statline, sizeof(statline), "/proc/%d/maps", pid );}fp = fopen( statline, "r" );if ( fp != NULL ) {while ( fgets( statline, sizeof(statline), fp ) ) {ptr = statline;address = nexttok(&ptr); // skip addressproms = nexttok(&ptr); // skip promsnexttok(&ptr); // skip offsetnexttok(&ptr); // skip devnexttok(&ptr); // skip inodewhile(*ptr != '\0') {if(*ptr == ' ')ptr++;elsebreak;}p = ptr;while(*p != '\0') {if(*p == '\n')*p = '\0';p++;}// 4016a000-4016b000if(strlen(address) == 17) {address[8] = '\0';*start = (void*)strtoul(address, NULL, 16);*end   = (void*)strtoul(address+9, NULL, 16);// printf("[%p-%p] %s | %p\n", *start, *end, ptr, addr);// 通过内存模块的路径字符串,判读是否是要查找的目标内存so模块if(strncmp(module, ptr, strlen(module)) == 0) {fclose( fp ) ;return 0;}}}fclose( fp ) ;}return -1;
}// 获取目标pid进程中指定函数的调用地址
void* get_remote_address(pid_t pid, void *local_addr) {// 保存加载的内存so模块的文件路径字符串char buf[256];// 当前进程中指定模块的起始地址void* local_start = 0;// 当前进程中指定模块的结束地址void* local_end = 0;// 目标pid进程中指定模块的起始地址void* remote_start = 0;// 目标pid进程中指定模块的结束地址void* remote_end = 0;// 获取当前进程中指定系统函数所在的模块的文件路径字符串bufif(find_module_info_by_address(-1, local_addr, buf, &local_start, &local_end) < 0) {LOGI("[-] find_module_info_by_address FAIL");return NULL;}LOGI("[+] the local module is %s", buf);// 通过指定的内存模块so的路径字符串,获取该内存模块的在目标进程pid中起始地址和结束地址if(find_module_info_by_name(pid, buf, &remote_start, &remote_end) < 0) {LOGI("[-] find_module_info_by_name FAIL");return NULL;}// 目标pid进程的local_addr函数的调用地址return (void *)( (uint32_t)local_addr + (uint32_t)remote_start - (uint32_t)local_start );
}
Android.mk文件

LOCAL_PATH := $(call my-dir)  include $(CLEAR_VARS)  # 编译生成的模块的名称
LOCAL_MODULE := poison  # 需要被编译的源码文件 
LOCAL_SRC_FILES := poison.c \elf_utils.c \ptrace_utils.c \tools.c # 支持log日志打印android/log.h里函数调用的需要
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog  # 编译模块生成可执行文件 
include $(BUILD_EXECUTABLE)  
Application.mk文件

# 编译生成的模块运行支持的平台
APP_ABI := armeabi-v7a  
# 设置编译连接的工具的版本
#NDK_TOOLCHAIN_VERSION = 4.9  


感谢链接

https://github.com/matrixhawk/Poison

https://github.com/boyliang/ndk-patch

http://zke1ev3n.me/2015/12/02/Android-so注入/

http://blog.csdn.net/qq1084283172/article/details/46859931

http://www.cnblogs.com/leaven/archive/2011/01/25/1944688.html

http://bbs.pediy.com/showthread.php?t=141355

http://blog.csdn.net/jinzhuojun/article/details/9900105



这篇关于Android进程的so注入--Poison(稳定注入版)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/400866

相关文章

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

pip-tools:打造可重复、可控的 Python 开发环境,解决依赖关系,让代码更稳定

在 Python 开发中,管理依赖关系是一项繁琐且容易出错的任务。手动更新依赖版本、处理冲突、确保一致性等等,都可能让开发者感到头疼。而 pip-tools 为开发者提供了一套稳定可靠的解决方案。 什么是 pip-tools? pip-tools 是一组命令行工具,旨在简化 Python 依赖关系的管理,确保项目环境的稳定性和可重复性。它主要包含两个核心工具:pip-compile 和 pip

跨系统环境下LabVIEW程序稳定运行

在LabVIEW开发中,不同电脑的配置和操作系统(如Win11与Win7)可能对程序的稳定运行产生影响。为了确保程序在不同平台上都能正常且稳定运行,需要从兼容性、驱动、以及性能优化等多个方面入手。本文将详细介绍如何在不同系统环境下,使LabVIEW开发的程序保持稳定运行的有效策略。 LabVIEW版本兼容性 LabVIEW各版本对不同操作系统的支持存在差异。因此,在开发程序时,尽量使用

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

Android 10.0 mtk平板camera2横屏预览旋转90度横屏拍照图片旋转90度功能实现

1.前言 在10.0的系统rom定制化开发中,在进行一些平板等默认横屏的设备开发的过程中,需要在进入camera2的 时候,默认预览图像也是需要横屏显示的,在上一篇已经实现了横屏预览功能,然后发现横屏预览后,拍照保存的图片 依然是竖屏的,所以说同样需要将图片也保存为横屏图标了,所以就需要看下mtk的camera2的相关横屏保存图片功能, 如何实现实现横屏保存图片功能 如图所示: 2.mtk

android应用中res目录说明

Android应用的res目录是一个特殊的项目,该项目里存放了Android应用所用的全部资源,包括图片、字符串、颜色、尺寸、样式等,类似于web开发中的public目录,js、css、image、style。。。。 Android按照约定,将不同的资源放在不同的文件夹中,这样可以方便的让AAPT(即Android Asset Packaging Tool , 在SDK的build-tools目

Android fill_parent、match_parent、wrap_content三者的作用及区别

这三个属性都是用来适应视图的水平或者垂直大小,以视图的内容或尺寸为基础的布局,比精确的指定视图的范围更加方便。 1、fill_parent 设置一个视图的布局为fill_parent将强制性的使视图扩展至它父元素的大小 2、match_parent 和fill_parent一样,从字面上的意思match_parent更贴切一些,于是从2.2开始,两个属性都可以使用,但2.3版本以后的建议使