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

前面深入学习了古河的Libinject注入Android进程,下面来 深入学习一下作者ariesjzj的博文《Android中的so注入(inject)和挂钩(hook)
- For both x86 and arm
》,注入的思路和古河的是一样的,但是代码的兼容性更好更好理解,适用于arm和x86模式下so注入和函数的Hook,这份代码自己也测试了一下,确实可以Hook目标函数成功,只是被Hook修改的目标函数,可能没有被系统调用,导致测试效果和作者ariesjzj给出稍有区别。

一、Android so注入和函数hook代码的说明

1.为inject工程代码添加必要的include文件

按照作者ariesjzj提供的代码文件,在eclipse中构建一个ndk的Inject代码工程(用于实现so的注入)和一个ndk的Inject_so代码工程(被注入的so以及实现函数Hook),并将作者ariesjzj提供的代码分别导入到这两个ndk的代码工程中,并按照我在前面博客中提到的方法为ndk工程添加必要的include头文件(Paths
and Symbols),添加方法如下:


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.在编译Inject的代码工程时,编译可能会报
找不到头文件 的错误

需要修改头文件
#include <asm/user.h> 为 #include <sys/user.h>

3.作者ariesjzj提供的Android
so注入Hook函数是基于ELF文件GOT表的Hook

说明的明白一点,就是被Hook的目标函数在该elf文件格式的so库文件中需要被导出,能被动态查找到(got表里存放的是外部符号的地址,不是所有符号的地址都能在got表找到)。当然了,一般在linux下,函数默认就是被导出的,顺便提一下,在so库文件中,函数没有被导出也是可以Hook的,因为函数调用地址在so库文件中的存储位置偏移是可以手工计算出来,只不过麻烦一点点,通用性差点。有关so文件的非导出函数的调用地址的计算,可以参考《linux下调用共享库非导出函数》里的方法。

生成共享库的代码 share.c :

#include <stdio.h>

// 默认的导出函数
void PrintABCD() //__attribute__((visibility("hidden")))
{
printf("ABCD\n");
sleep(-1);
} // +++++++设置为非导出函数++++++++
__attribute__((visibility("hidden"))) void Printabcd()
{
printf("abcd\n");
sleep(-1);
} // 根据参数来确定print ABCD or abcd(默认的导出函数)
void PrintIt(int isPrintCapital)
{
if(isPrintCapital)
{
PrintABCD();
}
else
{
Printabcd();
}
printf("over!\n");
}

生成共享库 share.so:

gl-linux@ubuntu:~/Desktop/AYR$ gcc -fPIC -shared -o share.so share.c

测试代码 test.c:

#include <stdio.h>
#include <dlfcn.h>
#include<unistd.h> #define DLL_FILE_NAME "/home/gl-linux/Desktop/AYR/share.so" int main()
{
long long addr = 0;
void (*func)(int);
void *handle = dlopen(DLL_FILE_NAME, RTLD_NOW);
if (handle == NULL)
{
fprintf(stderr, "Failed to open libaray %s error:%s\n", DLL_FILE_NAME, dlerror());
return -1;
} addr = (long long)dlsym(handle, "PrintABCD");
printf("%lld ",addr); addr = (long long)dlsym(handle, "Printabcd");
printf("%lld ",addr); addr = (long long)dlsym(handle, "PrintIt");
printf("%lld ",addr); func = addr;
func(1); dlclose(handle);
sleep(-1);
return 0;
}

生成可执行文件 test:

gl-linux@ubuntu:~/Desktop/AYR$ gcc test.c -o test -ldl

运行可执行文件 test:

139725025953621 0 139725025953687 ABCD

从上面的输出结果可以看出,非导出函数Printabcd的地址没有被dlsym函数获取到一般非导出函数也被称作 内部函数,应该和函数的生命周期也就是有效范围类似,当然了这个访问的限制是可以被突破的。

查看share.so中的导出函数偏移:

0000000000201048 B __bss_start
w __cxa_finalize
0000000000201048 D _edata
0000000000201050 B _end
00000000000007cc T _fini
w __gmon_start__
00000000000005e8 T _init
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
w _Jv_RegisterClasses
0000000000000755 T PrintABCD
0000000000000797 T PrintIt
U puts
U sleep

已知导出函数的偏移地址,又知道模块加载基址,怎么能知道非导出函数的地址呢?

使用计算后的地址,调用非导出函数Printabcd(),修改测试程序代码 TEST.c 如下:

#include <stdio.h>
#include <dlfcn.h>
#include<unistd.h> #define DLL_FILE_NAME "/home/gl-linux/Desktop/AYR/share.so" int main()
{
long long addr = 0;
void (*func)();
void *handle = dlopen(DLL_FILE_NAME, RTLD_NOW);
if (handle == NULL)
{
fprintf(stderr, "Failed to open libaray %s error:%s\n", DLL_FILE_NAME, dlerror());
return -1;
} addr = (long long)dlsym(handle, "PrintABCD");
printf("%lld ",addr); addr = (long long)dlsym(handle, "Printabcd");
printf("%lld ",addr); addr = (long long)dlsym(handle, "PrintIt");
printf("%lld ",addr); // 调用共享库share.so中的非导出函数。
func = addr-33;
func(); dlclose(handle);
sleep(-1);
return 0;
}

运行结果:

gl-linux@ubuntu:~/Desktop/AYR$ ./TEST
139670664996693 0 139670664996759 abcd

根据上面的输出结果,很显然调用 share.so文件中的非导出函数Printabcd成功。

有网友提示,下面这个Linux版本的 nm 可以查看so库文件中 非导出函数的位置偏移:

Linux ubuntu 3.17.1 #1 SMP Fri Oct 24 09:08:26 PDT 2014 x86_64 GNU/Linux版本nm -D xx.so,可以看到非导出函数的偏移, 只不过是t不是T

000000000000068c T PrintABCD

00000000000006ce T PrintIt
00000000000006ad t Printabcd

其实so库文件中的非导出函数的调用地址很简单将so文件拖入到IDA中分析找到静态下非导出函数和导出函数的函数调用地址的位置偏移offsetOfFunciton或者非导出函数调用地址offsetOfBaseAddr然后so库文件中非导出函数的地址即为动态内存的导出函数地址+offsetOfFunciton或者offsetOfBaseAddr+base(so文件的内存加载基地址)

感谢连接:

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

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

4.作者ariesjzj提供的Android
so注入Hook目标函数的代码的兼容性的考虑的原因

在arm模式和x86模式下,函数的堆栈工作原理是一样的,但是在具体到工作的实现细节上还是有很多的区别,arm模式的函数是基于寄存器传参,x86模式的函数是基于堆栈传参。

arm模式下,当函数的参数低于4个时,通过R0~R3寄存器传递参数;当函数的参数超过4个时,前4个参数通过R0~R3寄存器传递,超过4个的参数通过函数的堆栈进行传递,函数的程序计数寄存器为PC(存储将要执行的指令),函数执行完成后的函数返回值在R0中,
程序的状态寄存器为CPSR,函数的返回地址放在LR(程序连接寄存器)中,arm的函数调用堆栈图如下:

x86模式下,函数一般通过堆栈进行传递所有函数的参数,C/C++默认的函数调用约定为 __cdecl 调用约定,调用方负责平衡堆栈,不定参数的函数可以使用;参数的入栈方式为从右往左依次压入系统栈中,紧接着函数的返回地址入栈保存;EIP寄存器控制着进程的指令的执行,函数的返回值保存在eax寄存器中,x86下函数的调用堆栈图如下:

示意图2

5.作者ariesjzj提供的Android
so注入Hook目标函数的代码中,x86模式下,导出函数的地址需要+2的讨论。

作者ariesjzj提供的Android
so注入Hook目标函数的代码中,获取目标进程库函数地址的代码如下,x86平台的函数地址=实际函数地址+2。

大牛wdfa给出的解答参考Android
Libinject X86平台EIP-2的分析
》:

注入程序在ptrace_attach目标程序的时候,目标程序因为执行sleep(1)处于休眠状态。此时EIP为系统调用的用户态返回地址,如下的0xb7fe2424。无论是sysenter方式还是int
0x80方式,返回EIP都是该地址。

ptrace_attach首先向目标进程发送SIGSTOP信号,该信号把本来处于休眠的目标进程唤醒,然后进入等待目标进程状态发生改变。目标进程被唤醒后经内核调度开始执行,开始执行其实是从原来的休眠处开始的,显然继续执行将会结束sleep系统调用,并且返回结果不是sleep满足,而是sleep被中断了。目标进程准备退出sleep系统调用,在返回到用户态EIP之前,内核检测自身是否存在信号。显然,赤裸裸的躺着注入程序发送的SIGSTOP信号。于是,转入执行SIGSTOP信号处理,该信号处理把进程状态为STOP,挂起自身,激活注入进程的wait调用。

注入进程wait调用返回,ptrace_getregs 获取的EIP是确确实实的目标进程将来返回到用户态执行的EIP。

注入进程 ptrace_setreg 设置的EIP也确确实实是目标进程将来要返回到用户态执行的EIP。

注入进程 ptrace_continue 给目标进程发送信号SIGCON,信号发送本身将唤醒挂起的目标进程。目标进程因为收到SIGCON信号,恢复执行。此时进程的SIGSTOP信号即将处理完毕,系统检测该到信号来自系统调用,并且该信号处理不是由用户程序处理,这就意味着该信号导致了本系统调用失败,需要自动重新执行该系统调用。

自动重新本系统调用的方法:恢复用户态寄存器EAX为系统调用号,用户态EIP=EIP-2。而EIP-2恰好就是int
80系统调用指令。目标进程处理完信号后,开始执行系统调用返回到用户态。此时用户态的EIP由于-2的原因,不再是原来的pop
ebp指令,而是int 80指令。因此返回到用户态后,自动重新执行本系统调用。

因此,EIP-2的本质原因,在于ptrace_attach的时候,目标进程因系统调用进入了休眠,而attach发送的信号导致了目标进程调用中断返回,系统为了弥补中断返回的系统调用,在信号处理中将EIP-2来迫使中断的系统调用返回后自动重启本系统调用。

为何ptrace_attach+ptrace_syscall就没有EIP-2的问题?

在ptrace_attach后加入ptrace_syscall后不会导致eip-2,原因在于ptrace_syscall仅仅是设置目标进程的标志位,没有发送任何信号,也没有中断目标进程的任何系统调用。目标进程是主动在系统调用之前检查该标志位,主动挂起自己。这种条件下,注入进程设置目标进程eip,目标进程的系统调用自然是返回到设置的eip。

ptrace_attach一定会有问题吗?

NO。attach时刻目标进程,如果不是陷入系统调用,就不会触发自动重启系统调用导致的EIP-2。

ptrace_attach+ptrace_syscall一定安全吗?

NO。在ptrace_syscall+ptrace_setreg+ptrace_continue后,目标进程开始进行系统调用,如果系统调用是可中断的阻塞调用,在阻塞等待过程中,如果因为接受到其它信号,导致系统调用中断返回,那么系统会因为自动重启系统调用而设置eip-2,并且系统期待的eip-2处的代码为int
80。显然,注入进程设置的eip为某个函数,而eip-2就是个不伦不类的东西。

:最后两个问题,没有实际测试,只是推断;SIGCON信号对于目标进程只是简单的忽略;不知道ARM平台存不存在因为自动重启系统调用而导致的EIP-2的问题。

大牛netsniffer给出的 重启系统调用 的解答

arm下重启系统调用,也有调整用户态pc位置,目前bionic中libc.so采用ARM方式编译,svc
0指令占用4Bytes,所以会-4,arch\arm\kernel\signal.c

static void do_signal(struct pt_regs *regs, int syscall)
{
......
/*
* If we were from a system call, check for system call restarting...
*/
if (syscall) {
continue_addr = regs->ARM_pc;
restart_addr = continue_addr - (thumb_mode(regs) ? 2 : 4);
retval = regs->ARM_r0; /*
* Prepare for system call restart. We do this here so that a
* debugger will see the already changed PSW.
*/
switch (retval) {
case -ERESTARTNOHAND:
case -ERESTARTSYS:
case -ERESTARTNOINTR:
regs->ARM_r0 = regs->ARM_ORIG_r0;
regs->ARM_pc = restart_addr;
break;
case -ERESTART_RESTARTBLOCK:
regs->ARM_r0 = -EINTR;
break;
}
} signr = get_signal_to_deliver(&info, &ka, regs, NULL);
if (signr > 0) {
// 用户未设置SA_RESTART sigaction,继续执行以-EINTR返回给调用者
if (regs->ARM_pc == restart_addr) {
if (retval == -ERESTARTNOHAND
|| (retval == -ERESTARTSYS
&& !(ka.sa.sa_flags & SA_RESTART))) {
regs->ARM_r0 = -EINTR;
regs->ARM_pc = continue_addr;
}
}

x86下重启系统调用,int
0x80 指令2 Bytes,在文件 arch\x86\kernel\signal.c中:

static void do_signal(struct pt_regs *regs)
{
......
// x86下处理信号和arm下不同,先直接转到用户空间处理信号
signr = get_signal_to_deliver(&info, &ka, regs, NULL);
if (signr > 0) {
/* Whee! Actually deliver the signal. */
handle_signal(signr, &info, &ka, regs);
return;
} // 后判断系统调用结果,根据情况PC-2
if (syscall_get_nr(current, regs) >= 0) {
/* Restart the system call - no handlers present */
switch (syscall_get_error(current, regs)) { case -ERESTARTNOHAND:
case -ERESTARTSYS:
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break; case -ERESTART_RESTARTBLOCK:
regs->ax = NR_restart_syscall;
regs->ip -= 2;
break; }
}

网友justlovemm提出的问题

感谢连接:

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

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

二、Android so注入和函数hook代码的工程结构说明

1.Android
so注入工具inject工程的代码:

inject.c文件

#include <stdio.h>
#include <stdlib.h>
#include <sys/user.h> // 修改头文件#include <asm/user.h>为#include <sys/user.h>
#include <asm/ptrace.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <dlfcn.h>
#include <dirent.h>
#include <unistd.h>
#include <string.h>
#include <elf.h>
#include <android/log.h> #if defined(__i386__)
#define pt_regs user_regs_struct
#endif // 调试模式
#define ENABLE_DEBUG 1 // log日志打印的支持
#if ENABLE_DEBUG
#define LOG_TAG "INJECT"
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG, fmt, ##args)
#define DEBUG_PRINT(format,args...) \
LOGD(format, ##args)
#else
#define DEBUG_PRINT(format,args...)
#endif #define CPSR_T_MASK ( 1u << 5 ) const char *libc_path = "/system/lib/libc.so";
const char *linker_path = "/system/bin/linker"; // 读取目标进程中内存数据
int ptrace_readdata(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;
} // 写目标进程的内存写入数据
int ptrace_writedata(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, dest, d.val); dest += 4;
laddr += 4;
} if (remain > 0) {
d.val = ptrace(PTRACE_PEEKTEXT, pid, dest, 0);
for (i = 0; i < remain; i ++) {
d.chars[i] = *laddr ++;
} ptrace(PTRACE_POKETEXT, pid, dest, d.val);
} return 0;
} // arm模式下,在目标pid进程中调用指定目标函数
#if defined(__arm__)
int ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct pt_regs* regs)
{
uint32_t i; // 设置目标pid进程中被调用的函数的参数(arm的函数调用中前4个函数参数,通过r0-r3寄存器传递)
for (i = 0; i < num_params && i < 4; i ++) {
regs->uregs[i] = params[i];
} //
// push remained params onto stack
// 设置目标pid进程中被调用的函数的超过4个参数的参数(arm的函数调用中超过4个参数之后的函数参数,通过栈进行传递)
if (i < num_params) {
regs->ARM_sp -= (num_params - i) * sizeof(long) ;
ptrace_writedata(pid, (void *)regs->ARM_sp, (uint8_t *)¶ms[i], (num_params - i) * sizeof(long));
} // 设置将被调用的函数的调用地址(pc为指令指针寄存器,控制着进程的具体执行)
regs->ARM_pc = addr; // 根据当前进程的运行模式,设置进程的状态寄存器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;
} // 设置函数调用完的返回地址为0,触发地址0异常,程序的控制权又从目标pid进程回到了当前进程中
regs->ARM_lr = 0; // 设置目标pid进程的寄存器的状态值--实现在目标pid进程中调用指定的目标函数
if (ptrace_setregs(pid, regs) == -1
// 让目标pid进程继续执行代码指令
|| ptrace_continue(pid) == -1) { printf("error\n");
return -1;
} int stat = 0; // 等待在目标pid进程中,调用指定的目标函数完成
waitpid(pid, &stat, WUNTRACED); /***
WUNTRACED告诉waitpid,如果子进程进入暂停状态,那么就立即返回。
如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。
对于使用PTRACE_CONT运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。
这里的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
那么什么时候会发生这种错误呢?
显然,当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV。
***/
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) { printf("error\n");
return -1;
} // 进程等待
waitpid(pid, &stat, WUNTRACED);
} return 0;
} // x86模式下,在目标pid进程中调用指定目标函数
#elif defined(__i386__)
long ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct user_regs_struct * regs)
{
// x86模式下,C函数调用约定一般是通过栈进行传递参数
// 抬高目标pid进程的栈顶,用于保存函数调用需要的参数
regs->esp -= (num_params) * sizeof(long) ;
// 将指定目标函数调用需要的函数参数,写入到函数栈中
ptrace_writedata(pid, (void *)regs->esp, (uint8_t *)params, (num_params) * sizeof(long)); // 无效的地址
long tmp_addr = 0x00;
// 再次抬高栈顶,用于保存指定目标函数调用完后的函数的返回地址,此处设置为 0x00,将触发无效0地址访问异常
// 目标pid进程中指定目标函数调用完成以后,进程的控制权从目标pid进程又回到了当前进程中
regs->esp -= sizeof(long);
// 将无效的0地址值,写入到指定目标函数的函数栈中
ptrace_writedata(pid, regs->esp, (char *)&tmp_addr, sizeof(tmp_addr)); // x86模式下,控制进程的执行流程的是eip寄存器
// 设置eip为将被调用的函数的调用地址
regs->eip = addr; // 设置目标pid进程的寄存器状态值,用以调用目标pid进程中的指定目标函数
if (ptrace_setregs(pid, regs) == -1
// 让目标pid进程继续执行指令,调用目标函数
|| ptrace_continue( pid) == -1) {
printf("error\n");
return -1;
} int stat = 0;
// 等待调用目标函数完成
waitpid(pid, &stat, WUNTRACED);
// 对0地址异常的处理
while (stat != 0xb7f) {
=
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
} // 等待操作的完成
waitpid(pid, &stat, WUNTRACED);
} return 0;
}
#else
#error "Not supported"
#endif // 获取目标进程寄存器的状态值
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;
} // 设置目标进程的寄存器的状态值
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;
} // 让目标进程继续运行执行代码
int ptrace_continue(pid_t pid)
{
if (ptrace(PTRACE_CONT, pid, NULL, 0) < 0) { perror("ptrace_cont");
return -1;
} return 0;
} // ++++++++++++++++++++++++++++++++++ ptrace附加调试目标进程 +++++++++++++++++++++++++++++++++++
int ptrace_attach(pid_t pid)
{
if (ptrace(PTRACE_ATTACH, pid, NULL, 0) < 0) { perror("ptrace_attach");
return -1;
} int status = 0;
waitpid(pid, &status , WUNTRACED); return 0;
}
// ++++++++++++++++++++++++++++++++++ ptrace附加调试目标进程 +++++++++++++++++++++++++++++++++++ // 释放对目标进程的附加调试
int ptrace_detach(pid_t pid)
{
if (ptrace(PTRACE_DETACH, pid, NULL, 0) < 0) {
perror("ptrace_detach");
return -1;
} return 0;
} // 获取进程中指定名称模块的基址
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", pid);
} else {
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
} 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 (void *)addr;
} // 获取目标进程中,指定函数的调用地址
void* get_remote_addr(pid_t target_pid, const char* module_name, void* local_addr)
{
void* local_handle, *remote_handle; // 获取该so文件在当前进程中加载基址
local_handle = get_module_base(-1, module_name); // 获取该so文件在目标pid进程中加载基址
remote_handle = get_module_base(target_pid, module_name); DEBUG_PRINT("[+] get_remote_addr: local[%x], remote[%x]\n", local_handle, remote_handle); // 获取指定函数在目标pid进程中调用地址
void * ret_addr = (void *)((uint32_t)local_addr + (uint32_t)remote_handle - (uint32_t)local_handle); // 增加了对x86的支持,x86模式针对"/system/lib/libc.so"中,函数调用地址的特殊处理
#if defined(__i386__)
if (!strcmp(module_name, libc_path)) { // 函数的调用地址+2
ret_addr += 2;
}
#endif return ret_addr;
} // 通过进程的文件路径名称,获取进程的pid
int find_pid_of(const char *process_name)
{
int id;
pid_t pid = -1;
DIR* dir;
FILE *fp;
char filename[32];
char cmdline[256]; struct dirent * entry; if (process_name == NULL)
return -1; dir = opendir("/proc");
if (dir == NULL)
return -1; while((entry = readdir(dir)) != NULL) {
id = atoi(entry->d_name);
if (id != 0) {
sprintf(filename, "/proc/%d/cmdline", id);
fp = fopen(filename, "r");
if (fp) {
fgets(cmdline, sizeof(cmdline), fp);
fclose(fp); if (strcmp(process_name, cmdline) == 0) {
/* process found */
pid = id;
break;
}
}
}
} closedir(dir); return pid;
} // 对x86和arm模式的函数的返回值兼容处理(获取函数调用的返回值)
long ptrace_retval(struct pt_regs * regs)
{
#if defined(__arm__)
return regs->ARM_r0;
#elif defined(__i386__)
return regs->eax;
#else
#error "Not supported"
#endif
} // 对x86和arm模式的指令指针的处理(获取函数调用完后的,指令指针值)
long ptrace_ip(struct pt_regs * regs)
{
#if defined(__arm__)
return regs->ARM_pc;
#elif defined(__i386__)
return regs->eip;
#else
#error "Not supported"
#endif
} // 在目标pid进程中调用指定的函数并获取函数返回值
int ptrace_call_wrapper(pid_t target_pid, const char * func_name, void * func_addr, long * parameters, int param_num, struct pt_regs * regs)
{
DEBUG_PRINT("[+] Calling %s in target process.\n", func_name); // 在目标pid进程中调用指定的函数
if (ptrace_call(target_pid, (uint32_t)func_addr, parameters, param_num, regs) == -1)
return -1; // 在目标pid进程中,调用完指定函数以后,获取此时寄存器的状态值(函数返回值、指令指针寄存器值)
if (ptrace_getregs(target_pid, regs) == -1)
return -1;
DEBUG_PRINT("[+] Target process returned from %s, return value=%x, pc=%x \n",
func_name, ptrace_retval(regs), ptrace_ip(regs)); return 0;
} // --------------将指定的so文件注入到目标进程中并执行注入so文件中导出函数function_name-------------------------
int inject_remote_process(pid_t target_pid, const char *library_path, const char *function_name, const char *param, size_t param_size)
{
int ret = -1;
void *mmap_addr, *dlopen_addr, *dlsym_addr, *dlclose_addr, *dlerror_addr;
void *local_handle, *remote_handle, *dlhandle;
uint8_t *map_base = 0;
uint8_t *dlopen_param1_ptr, *dlsym_param2_ptr, *saved_r0_pc_ptr, *inject_param_ptr, *remote_code_ptr, *local_code_ptr; // 用于保存目标pid进程的寄存器状态值
struct pt_regs regs, original_regs; // 用于保存目标pid进程中的dopen函数调用地址以及参数的值、dlsym函数调用地址以及参数的值、以及诸如so的导出函数function_name以及参数值
extern uint32_t _dlopen_addr_s, _dlopen_param1_s, _dlopen_param2_s, _dlsym_addr_s, \
_dlsym_param2_s, _dlclose_addr_s, _inject_start_s, _inject_end_s, _inject_function_param_s, \
_saved_cpsr_s, _saved_r0_pc_s; uint32_t code_length;
long parameters[10]; // 打印将被so注入的pid进程的值
DEBUG_PRINT("[+] Injecting process: %d\n", target_pid); // ptrace附加调试目标pid进程
if (ptrace_attach(target_pid) == -1)
// 附加失败,跳转
goto exit; // 获取目标pid进程当前寄存器的状态值
if (ptrace_getregs(target_pid, &regs) == -1)
goto exit; /* save original registers 保存目标pid进程当前寄存器的状态值 */
memcpy(&original_regs, &regs, sizeof(regs)); // 获取目标pid进程中"/system/lib/libc.so"中的mmap函数的调用地址
mmap_addr = get_remote_addr(target_pid, libc_path, (void *)mmap);
DEBUG_PRINT("[+] Remote mmap address: %x\n", mmap_addr); /* call mmap 准备调用目标pid进程中的mmap函数需要的参数 */
parameters[0] = 0; // addr
parameters[1] = 0x4000; // size--在目标pid进程中申请的内存空间的大小
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函数,在目标pid进程的内存空间中申请内存空间
if (ptrace_call_wrapper(target_pid, "mmap", mmap_addr, parameters, 6, &regs) == -1) // 调用失败,跳转
goto exit; // 获取调用目标pid进程的mmap函数完成的函数返回值即申请的内存空间地址
map_base = ptrace_retval(&regs); // 获取目标pid进程中dlopen函数的调用地址
dlopen_addr = get_remote_addr( target_pid, linker_path, (void *)dlopen ); // 获取目标pid进程中dlsym函数的调用地址
dlsym_addr = get_remote_addr( target_pid, linker_path, (void *)dlsym ); // 获取目标pid进程中dlclose函数的调用地址
dlclose_addr = get_remote_addr( target_pid, linker_path, (void *)dlclose ); // 获取目标pid进程中dlerror函数的调用地址
dlerror_addr = get_remote_addr( target_pid, linker_path, (void *)dlerror ); // 打印获取到目标pid进程中dlopen等函数的地址
DEBUG_PRINT("[+] Get imports: dlopen: %x, dlsym: %x, dlclose: %x, dlerror: %x\n",
dlopen_addr, dlsym_addr, dlclose_addr, dlerror_addr); // 打印即将被注入的so库文件的文件路径
printf("library path = %s\n", library_path); // 将要被注入到目标pid进程中的so库文件的路径字符串library_path写入到前面mmap申请的内存空间中
ptrace_writedata(target_pid, map_base, library_path, strlen(library_path) + 1); // 设置调用dlopen函数的函数参数
parameters[0] = map_base; // library_path将被加载到目标pid进程中的so文件路径
parameters[1] = RTLD_NOW| RTLD_GLOBAL; // 调用目标pid进程中的dlopen函数,加载library_path路径的so文件到目标pid进程中,实现so注入
if (ptrace_call_wrapper(target_pid, "dlopen", dlopen_addr, parameters, 2, &regs) == -1) // 失败进行跳转
goto exit; // 获取dlopen函数调用后的返回值即library_path指定的so文件在目标pid进程中的加载基址
void * sohandle = ptrace_retval(&regs); // 设置map_base中保存library_path指定的so文件的导出函数function_name字符串的内存偏移
#define FUNCTION_NAME_ADDR_OFFSET 0x100
// 将library_path指定的so文件的导出函数function_name的函数名称字符串写入到目标pid进程中前面mmap申请的内存空间offset=0x100的位置
ptrace_writedata(target_pid, map_base + FUNCTION_NAME_ADDR_OFFSET, function_name, strlen(function_name) + 1); // 设置dlsym函数调用的函数参数
parameters[0] = sohandle; // so基址模块句柄
parameters[1] = map_base + FUNCTION_NAME_ADDR_OFFSET; // 将被获取的导出函数的调用地址 // 在目标pid进程中调用dlsym函数,获取上面加载的so文件中的导出函数function_name的调用地址
if (ptrace_call_wrapper(target_pid, "dlsym", dlsym_addr, parameters, 2, &regs) == -1) // 失败,跳转
goto exit; // 获取调用dlsym函数后,返回的导出函数function_name的调用地址
void * hook_entry_addr = ptrace_retval(&regs); // 打印获取到的导出函数function_name的调用地址
DEBUG_PRINT("hook_entry_addr = %p\n", hook_entry_addr); // 设置map_base中保存调用导出函数function_name需要的函数参数的内存偏移
#define FUNCTION_PARAM_ADDR_OFFSET 0x200
// 将调用hook_entry_addr函数需要的函数参数保存到前面在目标pid进程中mmap申请的内存空间offset=0x200的位置
ptrace_writedata(target_pid, map_base + FUNCTION_PARAM_ADDR_OFFSET, param, strlen(param) + 1); // 设置调用目标pid进程中hook_entry函数的函数参数
parameters[0] = map_base + FUNCTION_PARAM_ADDR_OFFSET; // 调用注入到目标pid进程中的so库的导出函数hook_entry实现我们自定义的代码,可以是Hook目标pid进程的函数
if (ptrace_call_wrapper(target_pid, "hook_entry", hook_entry_addr, parameters, 1, &regs) == -1)
goto exit; // 等待用户的输入
printf("Press enter to dlclose and detach\n");
getchar(); // 设置dlclose函数调用的函数参数
parameters[0] = sohandle; // 调用目标pid进程中的dlclose函数卸载上面加载的library_path指定的so文件(实现so注入的卸载)
if (ptrace_call_wrapper(target_pid, "dlclose", dlclose, parameters, 1, &regs) == -1)
goto exit; /* restore 恢复目标pid进程被ptrace附加时的运行状态 */
ptrace_setregs(target_pid, &original_regs); // 释放对目标pid进程的附加
ptrace_detach(target_pid); // 设置函数返回值
ret = 0; exit:
return ret;
} // ++++++++++++++++++++++++++++++++ 主函数 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
int main(int argc, char** argv) { pid_t target_pid; // 获取目标进程的pid
target_pid = find_pid_of("/system/bin/surfaceflinger");
if (-1 == target_pid) { printf("Can't find the process\n");
return -1;
} // 向目标进程注入so库文件,执行导出函数hook_entry,其中"I'm parameter!"为传入参数
inject_remote_process(target_pid, "/data/local/tmp/libinject_so.so", "hook_entry", "I'm parameter!", strlen("I'm parameter!")); return 0;
}
// ++++++++++++++++++++++++++++++++ 主函数 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Android.mk文件

LOCAL_PATH := $(call my-dir)  

include $(CLEAR_VARS)  

# 编译后生成的模块的名称
LOCAL_MODULE := inject # 参与编译的源码文件
LOCAL_SRC_FILES := inject.c # 支持log日志打印需要加载链接的库
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog #LOCAL_FORCE_STATIC_EXECUTABLE := true
# 编译生成可执行文件
include $(BUILD_EXECUTABLE)

Application.mk文件

# 编译后,生成模块运行支持的平台
APP_ABI := x86 armeabi-v7a

2.被注入so以及Hook目标函数的inject_so的工程代码:

inject_so.c文件

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <android/log.h>
#include <EGL/egl.h>
#include <GLES/gl.h>
#include <elf.h>
#include <fcntl.h>
#include <sys/mman.h> #define LOG_TAG "INJECT"
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args) EGLBoolean (*old_eglSwapBuffers)(EGLDisplay dpy, EGLSurface surf) = -1; // 当调用eglSwapBuffers函数时,将调用我们自定的new_eglSwapBuffers函数
EGLBoolean new_eglSwapBuffers(EGLDisplay dpy, EGLSurface surface)
{
LOGD("New eglSwapBuffers\n");
if (old_eglSwapBuffers == -1)
LOGD("error\n"); return old_eglSwapBuffers(dpy, surface);
} // 获取目标进程中指定名称模块的加载基址
void* get_module_base(pid_t pid, const char* module_name)
{
FILE *fp;
long addr = 0;
char *pch;
char filename[32];
char line[1024]; // 格式化字符串得到 "/proc/pid/maps"
if (pid < 0) {
/* self process */
snprintf(filename, sizeof(filename), "/proc/self/maps", pid);
} else {
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
} // 打开文件 /proc/pid/maps,获取指定pid进程加载的内存模块信息
fp = fopen(filename, "r");
if (fp != NULL) { // 每次一行,读取文件 /proc/pid/maps中内容
while (fgets(line, sizeof(line), fp)) { // 查找指定的so模块
if (strstr(line, module_name)) { // 分割字符串
pch = strtok( line, "-" );
// 字符串转长整形
addr = strtoul( pch, NULL, 16 ); // 特殊内存地址的处理
if (addr == 0x8000)
addr = 0; break;
}
} // 关闭文件
fclose(fp) ;
} return (void *)addr;
} // Hook库/system/lib/libsurfaceflinger.so中的eglSwapBuffers函数
#define LIBSF_PATH "/system/lib/libsurfaceflinger.so" int hook_eglSwapBuffers()
{
// 保存被Hook的目标函数的原始调用地址
old_eglSwapBuffers = eglSwapBuffers;
LOGD("Orig eglSwapBuffers = %p\n", old_eglSwapBuffers); // 获取目标pid进程中"/system/lib/libsurfaceflinger.so"模块的加载地址
void * base_addr = get_module_base(getpid(), LIBSF_PATH);
LOGD("libsurfaceflinger.so address = %p\n", base_addr); int fd;
// 打开内存模块文件"/system/lib/libsurfaceflinger.so"
fd = open(LIBSF_PATH, O_RDONLY);
if (-1 == fd) { LOGD("error\n");
return -1;
} // elf32文件的文件头结构体Elf32_Ehdr
Elf32_Ehdr ehdr;
// 读取elf32格式的文件"/system/lib/libsurfaceflinger.so"的文件头信息
read(fd, &ehdr, sizeof(Elf32_Ehdr)); // elf32文件中区段表信息结构的文件偏移
unsigned long shdr_addr = ehdr.e_shoff;
// elf32文件中区段表信息结构的数量
int shnum = ehdr.e_shnum;
// elf32文件中每个区段表信息结构中的单个信息结构的大小(描述每个区段的信息的结构体的大小)
int shent_size = ehdr.e_shentsize; // elf32文件区段表中每个区段的名称存放的字符串区段,在区段表中的序号index
unsigned long stridx = ehdr.e_shstrndx; // elf32文件中区段表的每个单元信息结构体(描述每个区段的信息的结构体)
Elf32_Shdr shdr;
// elf32文件中定位到存放每个区段名称的字符串表的信息结构体位置.shstrtab
lseek(fd, shdr_addr + stridx * shent_size, SEEK_SET);
// 读取elf32文件中的描述每个区段的信息的结构体(这里是保存elf32文件的每个区段的名称字符串的)
read(fd, &shdr, shent_size); // 为保存elf32文件的所有的区段的名称字符串申请内存空间
char * string_table = (char *)malloc(shdr.sh_size);
// 定位到具体存放elf32文件的所有的区段的名称字符串的文件偏移处
lseek(fd, shdr.sh_offset, SEEK_SET);
// 从elf32内存文件中读取所有的区段的名称字符串到申请的内存空间中
read(fd, string_table, shdr.sh_size); // 重新设置elf32文件的文件偏移为区段信息结构的起始文件偏移处
lseek(fd, shdr_addr, SEEK_SET); int i;
uint32_t out_addr = 0;
uint32_t out_size = 0;
uint32_t got_item = 0;
int32_t got_found = 0; // 循环遍历elf32文件的区段表(描述每个区段的信息的结构体)
for (i = 0; i < shnum; i++) { // 依次读取区段表中每个描述区段的信息的结构体
read(fd, &shdr, shent_size); // 判断当前区段描述结构体描述的区段是否是SHT_PROGBITS类型
if (shdr.sh_type == SHT_PROGBITS) { // 获取区段的名称字符串在保存所有区段的名称字符串段.shstrtab中的序号
int name_idx = shdr.sh_name; // 判断区段的名称是否为".got.plt"或者".got"
if (strcmp(&(string_table[name_idx]), ".got.plt") == 0
|| strcmp(&(string_table[name_idx]), ".got") == 0) { // 获取区段".got"或者".got.plt"在内存中实际数据存放地址
out_addr = base_addr + shdr.sh_addr;
// 获取区段".got"或者".got.plt"的大小
out_size = shdr.sh_size;
LOGD("out_addr = %lx, out_size = %lx\n", out_addr, out_size); // 遍历区段".got"或者".got.plt"获取保存的全局的函数调用地址
for (i = 0; i < out_size; i += 4) { // 获取区段".got"或者".got.plt"中的单个函数的调用地址
got_item = *(uint32_t *)(out_addr + i); // 判断区段".got"或者".got.plt"中函数调用地址是否是将要被Hook的目标函数地址
if (got_item == old_eglSwapBuffers) { LOGD("Found eglSwapBuffers in got\n");
// 查找到要被Hook的目标函数的地址
got_found = 1; // 获取当前内存分页的大小
uint32_t page_size = getpagesize();
// 获取内存分页的起始地址(需要内存对齐)
uint32_t entry_page_start = (out_addr + i) & (~(page_size - 1));
LOGD("entry_page_start = %lx, entry_page_start = %lx\n", entry_page_start, page_size); // 修改内存属性为可读可写可执行
if (mprotect((uint32_t *)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) { LOGD("mprotect false\n");
return -1;
} // Hook目标函数之前
LOGD("%s, old_eglSwapBuffers = %lx, new_eglSwapBuffers = %lx\n", "befor hook function", got_item, new_eglSwapBuffers); // Hook函数为我们自己定义的函数
//*(uint32_t *)(out_addr + i) = new_eglSwapBuffers; // Hook函数的作用,等价的
got_item = new_eglSwapBuffers; // Hook目标函数之后
LOGD("%s, old_eglSwapBuffers = %lx, new_eglSwapBuffers = %lx\n", "after hook function", got_item, new_eglSwapBuffers); // 恢复内存属性为可读可执行
if (mprotect((uint32_t *)entry_page_start, page_size, PROT_READ | PROT_EXEC) == -1) { LOGD("mprotect false\n");
return -1;
} break; // 此时,目标函数的调用地址已经被Hook了
} else if (got_item == new_eglSwapBuffers) { LOGD("Already hooked\n");
break;
}
} // Hook目标函数成功,跳出循环
if (got_found)
break;
}
}
} free(string_table); close(fd);
} // 注入so的导出函数(默认导出)--将被调用
int hook_entry(char * a){ LOGD("Hook into success\n");
LOGD("Start hooking\n"); // Hook目标pid进程的eglSwapBuffer函数
hook_eglSwapBuffers(); return 0;
}

Android.mk文件

LOCAL_PATH := $(call my-dir)  

include $(CLEAR_VARS)  

#LOCAL_ARM_MODE := arm  

# 编译生成的模块的名称
LOCAL_MODULE := inject_so # 参与编译的源码文件
LOCAL_SRC_FILES := inject_so.c # 支持log日志的打印
LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -llog -lEGL # 编译生成动态库文件libinject_so.so
include $(BUILD_SHARED_LIBRARY)

Application.mk文件

# 编译生成的模块文件运行支持的平台
APP_ABI := x86 armeabi-v7a
# 编译生成模块运行支持的Andorid版本
APP_PLATFORM := android-19

三、Android
so注入和函数hook的效果测试

执行Android so 注入和函数Hook的需要的文件截图:

log.bat查看注入输出日志的脚本文件:

adb logcat -s  INJECT

运行Android so注入的脚本文件(真机)run_inject.bat:

adb push libinject_so.so /data/local/tmp
adb push inject /data/local/tmp
adb shell chmod 0777 /data/local/tmp/libinject_so.so
adb shell chmod 0777 /data/local/tmp/inject
adb shell su -c /data/local/tmp/inject
pause

Nexus 5,Android 4.4.4的测试效果如下图:

Android
so注入和函数Hook的结果和作者ariesjzj的稍有不同,也有网友在作者ariesjzj的博客下面留言说Hook函数不成功,我修改了一下作者ariesjzj的代码,将Hook后目标函数的地址打印出来了,其实呢,Hook目标函数是成功的,只是没有被系统调用,触发新的new_eglSwapBuffers函数被调用而已。

关于Android的so注入和Hook还有很多值得讨论的问题,感谢博文中提到的作者深入研究知识的精神,让我在学习的道路上又进了一步,后面还会继续深入讨论有关Android的so注入和函数Hook的问题。

Android的so注入( inject)和函数Hook(基于got表) - 支持arm和x86的更多相关文章

  1. Android so注入( inject)和Hook(挂钩)的实现思路讨论

    本文博客:http://blog.csdn.net/qq1084283172/article/details/54095995 前面的博客中分析一些Android的so注入和Hook目标函数的代码,它 ...

  2. Android下so注入和hook

    一.前言 总结一下这两天学习的Android注入so文件,通过遍历got表hook函数调用 1.注入so文件 2.so文件中遍历got表hook函数 二.注入so文件 1)注入进程 1.编程思路分为以 ...

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

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

  4. Android so注入(inject)和Hook技术学习(三)——Got表hook之导出表hook

    前文介绍了导入表hook,现在来说下导出表的hook.导出表的hook的流程如下.1.获取动态库基值 void* get_module_base(pid_t pid, const char* modu ...

  5. Android so注入(inject)和Hook技术学习(二)——Got表hook之导入表hook

    全局符号表(GOT表)hook实际是通过解析SO文件,将待hook函数在got表的地址替换为自己函数的入口地址,这样目标进程每次调用待hook函数时,实际上是执行了我们自己的函数. GOT表其实包含了 ...

  6. Android so注入(inject)和Hook技术学习(一)

    以前对Android so的注入只是通过现有的框架,并没有去研究so注入原理,趁现在有时间正好拿出来研究一下. 首先来看注入流程.Android so的注入流程如下: attach到远程进程 -> ...

  7. Android Dagger依赖注入框架浅析

    今天接触了Dagger这套android的依赖注入框架(DI框架).感觉跟Spring 的IOC差点儿相同吧.这个框架它的优点是它没有採用反射技术(Spring是用反射的),而是用预编译技术.因为基于 ...

  8. Android下so注入汇总

    /**  作者:蟑螂一号*  原文链接:http://www.sanwho.com/133.html*  转载请注明出处*/ Android下so注入是基于ptrace系统调用,因此要想学会andro ...

  9. [Android]Android MVP&依赖注入&单元测试

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5422443.html Android MVP&依赖注入 ...

随机推荐

  1. 八. SpringCloud消息总线

    1. 消息总线概述 1.1 分布式配置的动态刷新问题 Linux运维修改Github上的配置文件内容做调整 刷新3344,发现ConfigServer配置中心立刻响应 刷新3355,发现ConfigC ...

  2. 通过序列号Sequence零代码实现订单流水号

    序列号管理 本文通过产品编码和订单流水号介绍一下序列号(Sequence)在crudapi中的应用. 概要 序列号 MySQL数据库没有单独的Sequence,只支持自增长(increment)主键, ...

  3. django Form 效验

    Django 登入效验 .py from django import forms from student import models from django.core.exceptions impo ...

  4. C# 应用 - 使用 HttpListener 接受 Http 请求

    1. 库类: \Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6\System.dll System.Net.HttpListen ...

  5. wireshark如何抓取分析https的加密报文

    [问题概述] https流量基于ssl/tls加密,无法直接对报文进行分析. [解决方案] 方案1 -- 利用"中间人攻击"的代理方式抓包分析.整个方案过程比较简单,这里不赘述,大 ...

  6. asp.net core 实现支持自定义 Content-Type

    asp.net core 实现支持自定义 Content-Type Intro 我们最近有一个原本是内网的服务要上公网,在公网上有一层 Cloudflare 作为网站的公网流量提供者,CloudFla ...

  7. dk.exe自动填报程序的反编译

    dk.exe自动填报程序的反编译 dk.exe用于学校每日健康报的自动填写.

  8. 滑动窗口解决最小子串问题 leetcode3. Longest Substring Without Repeating Characters

    问题描述: Given a string, find the length of the longest substring without repeating characters. Example ...

  9. beego框架panic: 'GetSecurityInf' method doesn't exist in the controller CorporateInfcontroller问题解决

    在使用beego框架时,出现类似于panic: 'GetSecurityInf' method doesn't exist in the controller CorporateInfcontroll ...

  10. Python之内存泄漏和内存溢出

    预习知识:python之MRO和垃圾回收机制 一.内存泄漏 像Java程序一样,虽然Python本身也有垃圾回收的功能,但是同样也会产生内存泄漏的问题.对于一个用 python 实现的,长期运行的后台 ...