本文博客地址: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_poison

adb 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.androiddecod cat /proc/17569/maps /data/local/tmp/poison /data/local/tmp/libmobisec.so 17569 cat /proc/17569/maps | grep libmobisec.so adb 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]);
// 获取注入的目标进程的pid
process_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 ) {
// thumb
regs.ARM_pc &= (~1u);
regs.ARM_cpsr |= CPSR_T_MASK;
} else {
// arm
regs.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; // addr
parameters[1] = size; // size
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; // prot
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flags
parameters[4] = 0; //fd
parameters[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 proms
nexttok(&ptr); // skip offset
nexttok(&ptr); // skip dev
nexttok(&ptr); // skip inode // ptr中最终保存的是加载的内存模块的路径字符串
while (*ptr != '\0') {
if (*ptr == ' ')
ptr++;
else
break;
} // 查找目标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-afe3a000
if (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/maps
fp = fopen( statline, "r" );
if ( fp != NULL ) { // 每次一行,读取文件/proc/pid/maps中内容
while ( fgets( statline, sizeof(statline), fp ) ) { // 解析读取为一行字符串信息
ptr = statline;
// 获取模块的起始和结束地址
address = nexttok(&ptr); // skip address
proms = nexttok(&ptr); // skip proms
nexttok(&ptr); // skip offset
nexttok(&ptr); // skip dev
nexttok(&ptr); // skip inode while(*ptr != '\0') {
if(*ptr == ' ')
ptr++;
else
break;
} p = ptr;
while(*p != '\0') {
if(*p == '\n')
*p = '\0';
p++;
} // 4016a000-4016b000
if(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 address
proms = nexttok(&ptr); // skip proms
nexttok(&ptr); // skip offset
nexttok(&ptr); // skip dev
nexttok(&ptr); // skip inode while(*ptr != '\0') { if(*ptr == ' ')
ptr++;
else
break;
} p = ptr;
while(*p != '\0') { if(*p == '\n')
*p = '\0';
p++;
} // 4016a000-4016b000
if(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; // 获取当前进程中指定系统函数所在的模块的文件路径字符串buf
if(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(稳定注入版)的更多相关文章

  1. Android进程注入

    全部代码在这里下载:http://download.csdn.net/detail/a345017062/8133239 里面有两个exe.inj是一个C层进程注入的样例.inj_dalvik是我写的 ...

  2. Android进程so注入Hook java方法

    本文博客链接:http://blog.csdn.net/qq1084283172/article/details/53769331 Andorid的Hook方式比较多,现在来学习下,基于Android ...

  3. Android Hook框架adbi的分析(1)---注入工具hijack

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/74055505 一.Android Hook框架adbi的基本介绍 adbi是And ...

  4. android hook 框架 libinject 如何实现so注入

    前面两篇 android hook 框架 libinject2 简介.编译.运行 android hook 框架 libinject2 如何实现so注入 实际运行并分析了 Android中的so注入( ...

  5. android hook 框架 libinject2 如何实现so注入

    Android so注入-libinject2 简介.编译.运行 Android so注入-libinject2  如何实现so注入 Android so注入-Libinject 如何实现so注入 A ...

  6. android hook 框架 ADBI 如何实现so注入

    Android so注入-libinject2 简介.编译.运行 Android so注入-libinject2  如何实现so注入 Android so注入-Libinject 如何实现so注入 A ...

  7. Android 进程常驻----开机广播的简单守护以及总结

    这是一个轻量级的库,配置几行代码,就可以实现在Android上实现进程常驻,也就是在系统强杀下,以及360获取root权限下,clean master获取root权限下都无法杀死进程 支持系统2.3到 ...

  8. Android 进程常驻(5)----开机广播的简单守护以及总结

    这是一个轻量级的库,配置几行代码.就能够实如今android上实现进程常驻,也就是在系统强杀下,以及360获取root权限下.clean master获取root权限下都无法杀死进程 支持系统2.3到 ...

  9. 轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI) 依赖注入和控制反转的理解,写的太好了。

    轻松学,浅析依赖倒置(DIP).控制反转(IOC)和依赖注入(DI) 2017年07月13日 22:04:39 frank909 阅读数:14269更多 所属专栏: Java 反射基础知识与实战   ...

随机推荐

  1. kubernetes Pod亲和性

    三种调度粘性,主要根据官方文档说明: NodeSelector(定向调度).NodeAffinity(Node亲和性).PodAffinity(Pod亲和性). 1.      nodeSelecto ...

  2. POJ-2516(最小费用最大流+MCMF算法)

    Minimum Cost POJ-2516 题意就是有n个商家,有m个供货商,然后有k种商品,题目求的是满足商家的最小花费供货方式. 对于每个种类的商品k,建立一个超级源点和一个超级汇点.每个商家和源 ...

  3. Ubuntu小配置

    Ubuntu 拍摄快照 在虚拟机安装好.配置号后各拍摄一次快照,并存储. 可在虚拟机出错后回滚 Root用户 Ubuntu默认不能以 Root用户身份直接登录 因此,正常操作时在需要调用 root权限 ...

  4. SpringBoot自动配置原理源码级别分析

    SpringBoot自动配置原理 前言 后面还会讲到SpringBoot自动配置原理,会主要讲解@EnableAutoConfiguratuon注解帮助我们做了什么事情,是如何自动把自动配置类扫描到容 ...

  5. CSS水平布局

    1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ...

  6. ch1_6_2求解删除公共字符问题

    输入两个字符串,从第一字符串中删除第二个字符串中所有的字符.例如,输入"They are students."和"aeiou",则删除之后的第一个字符串变成&q ...

  7. toastr通知插件的使用

    /显示一个警告,没有标题 toastr.warning('My name is Inigo Montoya. You killed my father, prepare to die!') 显示一个成 ...

  8. CDN域名解析问题

    CDN域名解析问题 之前配置CDN域名解析,碰到一个配置带www的域名和不带www的域名,这里就有个解析的坑,已经将cdn域名都配置好的,但是一直访问不了,白屏现象 后面排除源站问题和cdn配置问题, ...

  9. 「HTML+CSS」--自定义加载动画【009】

    前言 Hello!小伙伴! 首先非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出- 哈哈 自我介绍一下 昵称:海轰 标签:程序猿一只|C++选手|学生 简介:因C语言结识编程,随后转入计算机 ...

  10. spring-cloud-consul 服务注册发现与配置

    下面是 Spring Cloud 支持的服务发现软件以及特性对比(Eureka 已停止更新,取而代之的是 Consul): Feature euerka Consul zookeeper etcd 服 ...