致谢:

感谢 简行之旅的这篇blog:http://blog.csdn.net/l173864930/article/details/38455951,这篇文章是参考这篇blog的进行一步一步操作的,如果没有这篇好文章的话,貌似我这篇文章的诞生可能就有点难度了。

今天周日,昨天花了一天的时间总算是搞定了,问题还是想对应用程序的行为进行拦截的操作,就是像小米手机一样,哪些应用在获取你什么权限的信息。在之前写过对应用程序的行为进行拦截的方式(C层)实现的博客,在写完这篇之后,本来想是尽快的把Java层拦截的文章结束的,但是由于各种原因吧,所以一直没有时间去弄这些了。今天算是有空,就总结一下吧。下面进入正题:

一、摘要

我们知道现在一些安全软件都会有一个功能就是能够拦截应用的行为(比如地理位置信息,通讯录等),所以这里就来实现以下这样的功能,当然实现这样的功能有两种方式,一种是从底层进行拦截,这个我在之前的博客中已经讲解过了。没看过的同学可以转战:

http://blog.csdn.net/jiangwei0910410003/article/details/39346151

还有一种方式就是从上层进行拦截,也就是我们今天所要说的内容,这种方式都是可以的,当然很多人更多的偏向上层,因为底层拦截需要熟知Binder协议和Binder的数据格式的。上层拦截就简单点了。

二、知识点概要

首先我们需要了解一点知识就是不管是底层拦截还是上层拦截,都需要一个技术支持:进程注入,关于这个知识点,这里就不作解释了,不了解的同学可以转战:http://blog.csdn.net/jiangwei0910410003/article/details/39293635

了解了进程注入之后,这篇文章主要讲解三点知识:

1、如何动态加载so,并且执行其中的函数

2、如何在C层执行Java方法(NDK一般是指Java中调用C层函数)

3、如何修改系统服务(Context.getSystemService(String...)其实返回来的就是Binder对象)对象

当然我们还需要一些预备知识:知道如何使用NDK进行编译项目,不了解的同学可以转战:

http://blog.csdn.net/jiangwei0910410003/article/details/17710243

这篇文章编译环境是Window下的,个人感觉还是不方便,还是在Ubuntu环境下操作比较方便

还有一点需要声明:就是拦截行为是需要root权限的

三、例子

第一个例子:简单的进程注入功能

目的:希望将我们自己的功能模块(so文件)注入到目标进程中,然后修改目标进程中的某个函数的执行过程

文件:注入功能可执行文件poison、目标进程可执行文件demo1、需要注入的模块libmyso.so


注入功能的可执行文件核心代码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;
char *dso;
} process_hook = {0, ""}; int main(int argc, char* argv[]) {
LOGI("argv len:"+argc);
if(argc < 2)
exit(0); struct pt_regs regs; process_hook.dso = strdup(argv[1]);
process_hook.pid = atoi(argv[2]); if (access(process_hook.dso, R_OK|X_OK) < 0) {
LOGE("[-] so file must chmod rx\n");
return 1;
} 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;
} LOGI("[+] pc: %x, r7: %d", regs.ARM_pc, regs.ARM_r7); void* remote_dlsym_addr = get_remote_address(process_hook.pid, (void *)dlsym);
void* remote_dlopen_addr = get_remote_address(process_hook.pid, (void *)dlopen); LOGI("[+] remote_dlopen address %p\n", remote_dlopen_addr);
LOGI("[+] remote_dlsym address %p\n", remote_dlsym_addr); if(ptrace_dlopen(process_hook.pid, remote_dlopen_addr, process_hook.dso) == NULL){
LOGE("[-] Ptrace dlopen fail. %s\n", dlerror());
} if (regs.ARM_pc & 1 ) {
regs.ARM_pc &= (~1u);
regs.ARM_cpsr |= CPSR_T_MASK;
} else {
regs.ARM_cpsr &= ~CPSR_T_MASK;
} if (ptrace_setregs(process_hook.pid, &regs) == -1) {
LOGE("[-] Set regs fail. %s\n", strerror(errno));
goto DETACH;
} LOGI("[+] Inject success!\n"); DETACH:
ptrace_detach(process_hook.pid);
LOGI("[+] Inject done!\n");
return 0;
}

我们看到,这个注入功能的代码和我们之前说的从底层进行拦截的那篇文章中的注入代码(inject.c)不太一样呀?这个是有人在网上从新改写了一下,其实功能上没什么区别的,我们从main函数可以看到,有两个入口参数:

第一个是:需要注入so文件的全路径

第二个是:需要注入进程的pid

也就是说,我们在执行poison程序的时候需要传递这两个值。在之前说道的注入代码(inject.c)中,其实这两个参数是在代码中写死的,如果忘记的同学可以回去看一下,就是前面提到的从底层进行拦截的那篇文章。

那么这样修改之后,貌似灵活性更高了。

当然注入功能的代码不止这一个,其实是一个工程,这里由于篇幅的原因就不做介绍了,工程的下载地址:

http://download.csdn.net/detail/jiangwei0910410003/8138061

使用NDK编译一下,生成可执行文件就OK了。

第一部分:代码实现

1)目标进程依赖的so文件inso.h和inso.c

__attribute__ ((visibility ("default"))) void setA(int i);

__attribute__ ((visibility ("default"))) int getA();

inso.c代码

#include <stdio.h>
#include "inso.h" static int gA = 1; void setA(int i){
gA = i;
} int getA(){
return gA;
}

编译成so文件即可,项目下载:http://download.csdn.net/detail/jiangwei0910410003/8138107

2)目标进程的可执行文件demo1.c

这个就简单了,就是非常简单的代码,起一个循环每个一段时间打印数值,这个项目需要引用上面编译的inso.so文件

头文件inso.h(和上面的头文件是一样的)

__attribute__ ((visibility ("default"))) void setA(int i);

__attribute__ ((visibility ("default"))) int getA();

demo1.c文件

#include <stdio.h>
#include <unistd.h> #include "inso.h"
#include "log.h" int main(){ LOGI("DEMO1 start."); while(1){
LOGI("%d", getA());
setA(getA() + 1);
sleep(2);
} return 0;
}

代码简单吧,就是执行循环打印数值,这里使用的是底层的log方法,在log.h文件中定义了,篇幅原因,这里就不列举了,项目下载地址:http://download.csdn.net/detail/jiangwei0910410003/8138071

3)注入的模块功能源文件myso.c

#include <stdio.h>
#include <stddef.h>
#include <dlfcn.h>
#include <pthread.h>
#include <stddef.h> #include "log.h" __attribute__ ((__constructor__))
void Main() {
LOGI(">>>>>>>>>>>>>Inject Success!!!!<<<<<<<<<<<<<<"); void (*setA_func)(int); void* handle = dlopen("libinso.so", RTLD_NOW);
LOGI("Handle:%p",handle);
//void (*setA_func)(int) = (void (*)(int))dlsym(handle, "setA");
setA_func = (void (*)(int))dlsym(handle,"setA");
LOGI("Func:%p",setA_func);
if (setA_func) {
LOGI("setA is Executing!!!");
(*setA_func)(999);
}
dlclose(handle);
}

说明:

这段代码需要解释一下,首先来看一下:

__attribute__ ((__constructor__))
gcc为函数提供了几种类型的属性,其中包含:构造函数(constructors)和析构函数(destructors)。
程序员应当使用类似下面的方式来指定这些属性:

static void start(void) __attribute__ ((constructor));
static void stop(void) __attribute__ ((destructor));

带有"构造函数"属性的函数将在main()函数之前被执行,而声明为"析构函数"属性的函数则将在main()退出时执行。
用法举例:

#include <iostream>
void breforemain() __attribute__((constructor));
void aftermain() __attribute__((destructor));
class AAA{
public:
AAA(){std::cout << "before main function AAA" << std::endl;}
~AAA(){std::cout << "after main function AAA" << std::endl;}
};
AAA aaa;
void breforemain()
{
std::cout << "before main function" << std::endl;
} void aftermain()
{
std::cout << "after main function" << std::endl;
} int main(int argc,char** argv)
{
std::cout << "in main function" << std::endl;
return 0;
}

输出结果:
before main function AAA
before main function
in main function
after main function AAA
after main function

有点类似于Spring的AOP编程~~

还有一个就是我们开始说的,如何加载so文件,并且执行其中的函数,原理很简单,就是打开so文件,然后返回一个函数指针。

需要的头文件:#include <dlfcn.h>

核心代码:

打开so文件,返回一个句柄handle:

void* handle = dlopen("libinso.so", RTLD_NOW);

得到指定的函数指针:

setA_func = (void (*)(int))dlsym(handle,"setA");

函数指针的定义:

void (*setA_func)(int);

就是这么简单,有点类似Java中动态加载jar包,然后执行其中的方法。

项目下载地址:

http://download.csdn.net/detail/jiangwei0910410003/8138109

第二部分:拷贝文件

好了,到这里离成功不远了,我们保证上面的工程编译都能通过,得到以下文件:

demo1、poison、libmyso.so、libinso.so

然后我们就可以实践了

首先我将这些文件拷贝到手机中的/data/data/目录中

adb push demo1 /data/data/

adb push poison /data/data/

adb push libmyso.so /data/data/

拷贝完之后,还需要进入adb shell,修改他们的权限

chmod 777 demo1

chmod 777 poison

chmod 777 libmyso.so

这里要注意的是,libinso.so文件要单独拷贝到/system/lib中,不然在执行demo1的时候,会报错(找不到libinso.so),当然在拷贝的时候会遇到一点问题

报错:"Failed to push selection: Read-only file system"

这时候只要改变system目录的挂载读写属性就好了

mount -o remount rw /system/

然后进入adb shell在修改一下/system的属性

chmod 777 /system

然后就可以拷贝了:

adb push libinso.so /system/lib/

然后进入到system/lib中,修改libinso.so的属性

chmod 777 libinso.so

第三部分:开始执行

然后进入到data/data目录中,开始执行文件,这时候我们需要开三个终端:一个是监听log信息,一个是执行demo1,一个是执行poison,如下图所示:

1、监听log信息

adb logcat -s TTT

2、执行demo1

./demo1

3、执行poison

./poison /data/data/libmyso.so 1440

这里我们看到,执行poison有两个入口参数:一个是so文件的路径,一个是目标进程(demo1)的pid就是log信息中的显示的pid

到这里我们就实现了我们的第一个例子了。

第二个例子:将目标进程改成Android应用

下面继续:将目标进程改变成一个Android应用

这里和上边的唯一区别就是我们需要将demo1变成一个Android应用

那么来看一下这个Android应用的代码:

package com.demo.host;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log; public final class MainActivity extends Activity {
private static int sA = 1;
public static void setA(int a) {
sA = a;
}
public static int getA() {
return sA;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread() {
public void run() {
while (true) {
Log.i("TTT", "" + getA());
setA(getA() + 1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}; }.start();
}
}

代码和demo1的功能一样的,写个循环,打印数值,工程下载地址:http://download.csdn.net/detail/jiangwei0910410003/8138227

这个应用就变成了我们注入的目标进程,但是有一个问题,我们怎么才能修改setA()方法的行为呢?在第一个案例中,我们是动态加载libinso.so,然后获取到setA()函数,修改器返回值。这里我们需要做的就是怎么动态的去修改上面的MainActivity中的setA()方法的返回值,其实这里就到了我开始说的第二个知识点:如何在底层C中调用Java方法?

我们知道在使用NDK的时候,Java层调用底层C的时候连接的纽带就是那个JNIEnv变量,这个变量是作为函数参数传递过来的。那么如果我们在底层C中获取到这个变量的话,就可以调用Java方法了,但是这里我们又没有定义本地方法,怎么得到JNIEnv变量呢?

答案就是#include <android_runtime/AndroidRuntime.h>这个头文件,得到JVM变量之后,然后得到当前线程的JNIEnv变量:

JavaVM* jvm = AndroidRuntime::getJavaVM();
LOGI("jvm is %p",jvm);
jvm->AttachCurrentThread(&jni_env, NULL);
//TODO 使用JNIEnv
jvm->DetachCurrentThread();

通过AndroidRuntime中的getJavaVM方法获取jvm变量,然后在获取当前线程的JNIEnv变量即可

关于AndroidRuntime这个类的定义和实现是在  AndroidRuntime源码目录/jni/AndroidRuntime.cpp中

好了,当我们拿到JNIEnv变量之后,我们就可以干很多事了,因为我们知道在弄NDK的时候,如果使用JNIEnv变量的时候都清楚,他好比Java中的反射机制,可以动态的加载Java中的类,然后获取其方法,字段等信息,进行操作。

但是现在还有一个问题,就是我们怎么去动态加载MainActivity这个类呢?

但是当我们尝试使用PathClassLoader去加载MainActivity时,会抛ClassNotFoundException

唯一可行的方案是找到host(目标应用)的PathClassLoader,然后通过这个ClassLoader寻找MainActivity

因为我们是注入到MainActivity这个应用的进程中,那么我们的注入代码和MainActivity是在一个进程中的,又因为Android中一个进程对应一个全局Context对象,所以我们只要得到这个进程Context对象的类加载器就可以了

(其实Android中多个应用是可以跑在一个进程中的,他们会拥有一共同的全局Context变量,当然这个Context不是特定的Activity的,而是Application对象持有的Context)
要想得到一个进程中的Context对象。通过阅读源码,发现可以通过下面的方式读取到Context对象:

如果是System_Process,可以通过如下方式获取

Context context = ActivityThread.mSystemContext 

如果是非System_Process(即普通的Android进程),可以通过如下方式获取

Context context = ((ApplicationThread)RuntimeInit.getApplicationObject()).app_obj.this$0 

到这里,我们都知道该怎么办了,没错就是用反射机制,获取到全局的Context变量

上面的思路是有了,下面在来整理一下吧:

首先我们需要注入到MainActivity所在的进程,然后修改他的setA()方法。

但是我们注入的时候是把so文件注入到一个进程中,所以需要在底层修改setA()方法的执行

如果底层想修改/执行Java层方法的话,必须要得到JNIEnv变量

然后可以通过AndroidRuntime类先得到jvm变量,然后在通过jvm变量得到JNIEnv变量

得到JNIEnv变量之后,用JNIEnv的一些方法去动态加载MainActivity类,然后修改他的方法,但是会出现异常找不到MainActivity类

找不到这个类的原因是类加载器找的不对,我们需要找到全局的Context对象的类加载器,因为我们是注入到了MainActivity这个应用的进程中,一个进程有一个全局的Context对象,所以只要得到它的类加载器就可以了。

然后通过查看源码,我们可以在Java层通过反射获取到这个对象。

最后:通过上面的分析,我们还需要一些项目的支持:

1、底层获取JNIEnv对象的项目,也就是我们需要注入的so。

2、在上层还需要一个模块,去获取到全局的Context对象,然后动态的加载MainActivity类,修改他的方法。

第一部分:代码实现

这样分析之后,我们可能制定的方案是:

1)上层模块DemoInject2:

主要是获取进程的Context变量,然后通过这个变量去加载MainActivity类,修改他的setA()方法的逻辑,核心代码:

ContextHunter.java

package com.demo.inject2;

import android.app.Application;
import android.content.Context; public final class ContexHunter {
private static Context sContext = null; public static Context getContext() { if (sContext == null) { synchronized (ContexHunter.class) {
if (sContext == null) {
sContext = searchContextForSystemServer();
if (sContext == null) {
sContext = searchContextForZygoteProcess();
}
}
}
} return sContext;
} private static Context searchContextForSystemServer() {
Context result = null;
try {
result = (Context) ActivityThread.getSystemContext();
} catch (Exception e) {
e.printStackTrace();
return null;
} return result; } private static Context searchContextForZygoteProcess() {
Context result = null;
try {
Object obj = RuntimeInit.getApplicationObject();
if (obj != null) {
obj = ApplicationThread.getActivityThreadObj(obj);
if (obj != null) {
result = (Application) ActivityThread.getInitialApplication(obj);
}
}
} catch (Exception e) {
e.printStackTrace();
} return result;
}
}

动态加载MainActivity类,修改其setA()方法的逻辑功能的

EntryClass.java

package com.demo.inject2;

import java.lang.reflect.Method;

import android.content.Context;
import android.util.Log; public final class EntryClass { public static Object[] invoke(int i) { try {
Log.i("TTT", ">>>>>>>>>>>>>I am in, I am a bad boy 2!!!!<<<<<<<<<<<<<<");
Context context = ContexHunter.getContext();
Class<?> MainActivity_class = context.getClassLoader().loadClass("com.demo.host.MainActivity");
Method setA_method = MainActivity_class.getDeclaredMethod("setA", int.class);
setA_method.invoke(null, 1);
} catch (Exception e) {
e.printStackTrace();
} return null;
}
}

整个项目的下载地址:http://download.csdn.net/detail/jiangwei0910410003/8138735

注:这个项目没有入口的Activity的,所以运行是没有效果的,他的主要功能是一个功能模块apk,下面会说道他的用途。

2)注入的so文件源码:importdex.cpp

他的功能是获取到当前进程的JVM变量,然后获取到JNIEnv变量,通过JNIEnv变量,加载上面得到的DemoInject2.apk,然后执行EntryClass类中的invoke方法

#include <stdio.h>
#include <stddef.h>
#include <jni.h>
#include <android_runtime/AndroidRuntime.h> #include "log.h"
#include "importdex.h" using namespace android; static const char JSTRING[] = "Ljava/lang/String;";
static const char JCLASS_LOADER[] = "Ljava/lang/ClassLoader;";
static const char JCLASS[] = "Ljava/lang/Class;"; static JNIEnv* jni_env;
static char sig_buffer[512]; //EntryClass entryClass; //ClassLoader.getSystemClassLoader()
static jobject getSystemClassLoader(){ LOGI("getSystemClassLoader is Executing!!"); jclass class_loader_claxx = jni_env->FindClass("java/lang/ClassLoader");
snprintf(sig_buffer, 512, "()%s", JCLASS_LOADER); LOGI("sig_buffer is %s",sig_buffer); jmethodID getSystemClassLoader_method = jni_env->GetStaticMethodID(class_loader_claxx, "getSystemClassLoader", sig_buffer); LOGI("getSystemClassLoader is finished!!"); return jni_env->CallStaticObjectMethod(class_loader_claxx, getSystemClassLoader_method); } __attribute__ ((__constructor__))
void callback() {
LOGI("Main is Executing!!");
JavaVM* jvm = AndroidRuntime::getJavaVM();
LOGI("jvm is %p",jvm);
jvm->AttachCurrentThread(&jni_env, NULL);
//TODO 使用JNIEnv jvm->DetachCurrentThread(); LOGI("jni_env is %p",jni_env); jstring apk_path = jni_env->NewStringUTF("/data/local/tmp/DemoInject2.apk");
jstring dex_out_path = jni_env->NewStringUTF("/data/data/com.demo.host/");
jclass dexloader_claxx = jni_env->FindClass("dalvik/system/DexClassLoader"); //LOGI("apk_path:%s",apk_path);
//LOGI("dex_out_path:%s",dex_out_path); snprintf(sig_buffer, 512, "(%s%s%s%s)V", JSTRING, JSTRING, JSTRING, JCLASS_LOADER); LOGI("sig_buffer is %s",sig_buffer); jmethodID dexloader_init_method = jni_env->GetMethodID(dexloader_claxx, "<init>", sig_buffer); snprintf(sig_buffer, 512, "(%s)%s", JSTRING, JCLASS); LOGI("sig_buffer is %s",sig_buffer); jmethodID loadClass_method = jni_env->GetMethodID(dexloader_claxx, "loadClass", sig_buffer); jobject class_loader = getSystemClassLoader();
//check_value(class_loader);
LOGI("GetClassLoader"); jobject dex_loader_obj = jni_env->NewObject(dexloader_claxx, dexloader_init_method, apk_path, dex_out_path, NULL, class_loader); LOGI("step---1");
LOGI("dex_loader_obj:%s",dex_loader_obj);
jstring class_name = jni_env->NewStringUTF("com.demo.inject2.EntryClass");
jclass entry_class = static_cast<jclass>(jni_env->CallObjectMethod(dex_loader_obj, loadClass_method, class_name));
LOGI("step---2");
LOGI("jni_env:%p",jni_env);
LOGI("step---2-1");
LOGI("entry_class:%s",entry_class);
jmethodID invoke_method = jni_env->GetStaticMethodID(entry_class, "invoke", "(I)[Ljava/lang/Object;");
//check_value(invoke_method);
LOGI("step---3");
jobjectArray objectarray = (jobjectArray) jni_env->CallStaticObjectMethod(entry_class, invoke_method, 0);
LOGI("step---4");
jvm->DetachCurrentThread(); LOGI("Main is finished"); }

这里,我们可以看到,一旦我们拿到了JNIEnv变量之后,真的是什么都可以干,想调用Java世界中的任何类中的方法,就连动态加载apk都是可以的。所以这一点真的很重要,要切记。这个就是底层中调用Java层的方法的最好实现方案了。超实用的。

这个项目的下载地址:http://download.csdn.net/detail/jiangwei0910410003/8138253

第二部分:拷贝文件

好的,上面的介绍工作算是结束了,编译顺利的话,会得到三个文件:

DemoHost.apk、DemoInject2.apk、libimportdex.so

首先需要将DemoHost.apk安装到手机上

然后将DemoInject2.apk拷贝到手机的/data/local/tmp/目录中:adb push DemoInject2.apk  /data/local/tmp/

因为在importdex.cpp代码中的路劲就是这个:

jstring apk_path = jni_env->NewStringUTF("/data/local/tmp/DemoInject2.apk");

当然你可以修改的。

最后还是需要将libimportdex.so文件拷贝到/data/data/目录中

adb push libimportdex.so /data/data/

修改权限:

chmod 777 libimportdex.so

第三部分:开始执行

文件拷贝好了,下面开始执行了,还是需要开启三个终端:监听log信息,执行注入程序,启动MainActivity(当然这个可以不需要,因为你可以手动的打开)

1、监听log信息

adb logcat -s TTT

2、执行注入程序

./poison /data/data/libimportdex.so 15427

3、开启MainActivity

am start com.demo.host/.MainActivity

通过log信息我们可以看到注入成功了。

注:在这个例子中,编译的过程中会遇到一个问题:就是很多头文件,函数定义都找不到的,比如

#include <android_runtime/AndroidRuntime.h>这个头文件,这个就需要我们去Android的系统源码中查找了。关于Android系统源码下载和编译的知识,可以转战:http://blog.csdn.net/jiangwei0910410003/article/details/37988637

当我们加入这个头文件的时候,在编译还是出错,因为找不到指定的函数定义,这时候我们有两种选择:

第一种:将这个头文件的实现源文件也拷贝过来,进行编译(源码中有)

第二种:使用so文件进行编译,关于这个so文件可以到手机设备的/system/lib/目录下找到:libandroid_runtime.so

这里我就是采用这种方式进行编译的,我们将libandroid_runtime.so文件拷贝出来然后将其放到我们之前的NDK配置目录中的:具体目录如下:

最后我们在Android.mk文件进行引用:

LOCAL_LDLIBS := -llog -lbinder -lutils -landroid_runtime

当然,这里我们会看到-lXXX是通用的格式,同样的,我们如果要用到JVM中的函数的话,会用到libandroid_runtime.so文件,头文件:android_runtime.h,也是可以在源码中找到的(后面会提及到)

(注:这里就介绍了我们如何在使用Android系统中底层的一些函数,同时编译的时候引用到了动态链接库文件)

第三个例子:替换系统服务,拦截程序行为

在看一个例子,第一个例子是如何将目标模块注入到目标进程中,然后执行它的函数,不过在这个例子中我们的目标进程是一个简单的C程序编译的可执行文件运行的,第二个例子和第一个例子的区别是将目标进程换成Android中的一个应用,在这个过程中需要解决很多问题的,而且这个例子是很重要的,因为它介绍了如何在底层调用Java方法:可以将Java模块打成jar/dex/apk,然后在底层使用类加载器进行动态加载,然后执行其指定的方法。那么到这里算是结束了吗?答案是否定的,因为我们的目的是拦截,所以最后一个例子我们就需要实现我们的目的了

对应用行为的拦截

需要的项目:注入的so文件proxybinder.cpp,上层模块DemoInject3.apk

首先来看一下实现原理吧:

在日常开发过程中, 我们经常会使用到的ActivityManager、PackageManager就是一个client的调用,只是本身封装得比较好,让你感觉不到。而Service部分的逻辑,主要集中在system_process和com.android.phone这两个进程里头。
broadcastIntent是ActivityManagerService(AMS)的一个方法,AMS的宿主进程是system_process,毫无疑问我们需要先注入到system_process进程,至于接下来怎么做呢,正是本章的内容。
所有NativeService都继承到IBinder接口,BinderProxy原理很简单,就是先到找到要代理的NativeService引用,再通过自己编写的ProxyBinder对象代理NativeService,从而达到截获IPC通讯的目的。下面我们以AMS为例,做一个说明:
AMS跟binder进行通讯,是通过JNI实现的。AMS继承Binder(IBinder的子类,封装了IPC通讯公共部分的逻辑),Binder里保存着一个类型为int的mObject的字段,这个字段正是其C++对象JavaBBinder对象的地址,这个JavaBBinder才是AMS最终跟内核通讯的对象。代码如下: 
public class Binder implements IBinder {
//... /* mObject is used by native code, do not remove or rename */
private int mObject; //这个对象保存的就是JavaBBinder的指针
private IInterface mOwner;
private String mDescriptor; //...
}
同样的,在JavaBBinder中,也保存着一个类型jobject的mObject,指向上层Java对象。看看JavaBBinder的代码:
class JavaBBinder : public BBinder
{ //... jobject object() const
{
return mObject;
} //... private:
JavaVM* const mVM;
jobject const mObject; //这个保存的是AMS的引用
};
}
Java和C++就是通过这两个字段相互连结在一起的。
其中JavaBBinder中的mObject是整个IPC关键的一节,所有的client请求,都是先到达JavaBBinder,然后JavaBBinder再通过JNI调用mObject的execTransact的方法,最终把请求发送到AMS。
因此,我们只要想办法找到AMS的对象的JavaBBinder,再把mObject替换为代理对象(记作ProxyBinder,一个Java对象的引用),即可实现BinderService代理,下面是示意图:

在实现这个代理,我们需要获取AMS和及对应用的JavaBBinder两个对象。

获取AMS引用

要获取AMS引用,通过ServiceManager即可,不过这类是隐藏类,通过反射才可以调用。通过ServiceManager.getService("activity")即可以拿到AMS。
获取JavaBBinder对象
通过前面的介绍,拿到AMS之后,就可以获取其mObject字段,这个对象正好就是JavaBBinder的地址。另外,也有一种比较简单的方式,那就是通过defaultServiceManager的getService方法获取到。
替换mObject对象
JavaBBinder的mObject对象并不能直接替换,因为mObject是const的,我写了一个DummyJavaBBinder的类,可以很容易地处理好这个问题,DummyJavaBBinder的实现如下:
class DummyJavaBBinder : public BBinder{
public:
virtual status_t onTransact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) {
return NO_ERROR;
} jobject object() const {
return mObject;
} JavaVM* javaVM() const {
return mVM;
} void changeObj(jobject newobj){
const jobject* p_old_obj = &mObject;
jobject* p_old_obj_noconst = const_cast<jobject *>(p_old_obj);
*p_old_obj_noconst = newobj;
} private:
JavaVM* const mVM;
jobject const mObject;
};
这个类的作用主要添加了changeObj方法,主要功能是把mObject去掉const限制,并修改为的newobj。

其实底层的注入模块的功能很简单,就是动态加载DemoInject3.apk,然后执行它的invoke方法,得到上层的Binder对象,然后替换底层的Binder对象,这样我们上层定义的Binder对象中的onTransact方法中就可以拦截信息了。

proxybinder.so的项目下载地址:http://download.csdn.net/detail/jiangwei0910410003/8138929

第一部分:代码实现

1)DemoInject3.apk的核心代码

package com.demo.inject3;

import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log; public final class EntryClass { private static final class ProxyActivityManagerServcie extends Binder { private IBinder mBinder; public ProxyActivityManagerServcie(IBinder binder) {
mBinder = binder;
} @Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
//打印值,因为data是请求者带来的数据,reply是接受者返回的数据,这里看data数据
Log.i("TTT", "code="+code);
String callingPackage = data.readString();
Log.i("TTT", "callingPackage:"+callingPackage);
String resolvedType = data.readString();
Log.i("TTT", "resolvedType:"+resolvedType);
String resultWho = data.readString();
Log.i("TTT", "resultWho:"+resultWho);
int requestCode = data.readInt();
Log.i("TTT", "requestCode:"+requestCode);
int startFlags = data.readInt();
Log.i("TTT", "startFlags:"+startFlags);
String profileFile = data.readString();
Log.i("TTT", "profileFile:"+profileFile);
int v1 = data.readInt();
Log.i("TTT","v1:"+v1);
int v2 = data.readInt();
Log.i("TTT","v2:"+v2);
int userId = data.readInt();
Log.i("TTT","userId:"+userId);
return mBinder.transact(code, data, reply, flags);
}
} public static Object[] invoke(int i) {
IBinder activity_proxy = null;
try {
activity_proxy = new ProxyActivityManagerServcie(ServiceManager.getService("activity"));
Log.i("TTT", ">>>>>>>>>>>>>I am in, I am a bad boy 3!!!!<<<<<<<<<<<<<<");
} catch (Exception e) {
e.printStackTrace();
}
//将activity_proxy传递给底层,进行Binder对象的修改
return new Object[] { "activity", activity_proxy };
}
}

ProxyActivityManagerService继承了Binder对象,在onTransact中参数data是请求者携带的数据,所以我们可以打印这个数据的值来看一下,里面都是什么样的值?当然我们肯定不知道这些数据中包含什么信息,所以要到源码中找到一个有这个方法的类,可以定位到ActivityManagerNative.java这个类。在 源码目录/java/android/app/ActivityManagerNative.java。下面就是他的onTransact方法的一部分:

 @Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
switch (code) {
case START_ACTIVITY_TRANSACTION:
{
data.enforceInterface(IActivityManager.descriptor);
IBinder b = data.readStrongBinder();
IApplicationThread app = ApplicationThreadNative.asInterface(b);
String callingPackage = data.readString();
Intent intent = Intent.CREATOR.createFromParcel(data);
String resolvedType = data.readString();
IBinder resultTo = data.readStrongBinder();
String resultWho = data.readString();
int requestCode = data.readInt();
int startFlags = data.readInt();
String profileFile = data.readString();
ParcelFileDescriptor profileFd = data.readInt() != 0
? data.readFileDescriptor() : null;
Bundle options = data.readInt() != 0
? Bundle.CREATOR.createFromParcel(data) : null;
int result = startActivity(app, callingPackage, intent, resolvedType,
resultTo, resultWho, requestCode, startFlags,
profileFile, profileFd, options);
reply.writeNoException();
reply.writeInt(result);
return true;
}
.........

这个直截取了一小部分,因为这个方法的内容太多了,主要是那个code的值有很多值,可以看一下code的取值:

   // Please keep these transaction codes the same -- they are also
// sent by C++ code.
int START_RUNNING_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
int HANDLE_APPLICATION_CRASH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+1;
int START_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+2;
int UNHANDLED_BACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3;
int OPEN_CONTENT_URI_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4; // Remaining non-native transaction codes.
int FINISH_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+10;
int REGISTER_RECEIVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+11;
int UNREGISTER_RECEIVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+12;
int BROADCAST_INTENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+13;
int UNBROADCAST_INTENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+14;
int FINISH_RECEIVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+15;
int ATTACH_APPLICATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+16;
int ACTIVITY_IDLE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+17;
int ACTIVITY_PAUSED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+18;
int ACTIVITY_STOPPED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+19;
int GET_CALLING_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+20;
int GET_CALLING_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+21;
int GET_TASKS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+22;
int MOVE_TASK_TO_FRONT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+23;
int MOVE_TASK_TO_BACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+24;
int MOVE_TASK_BACKWARDS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+25;
int GET_TASK_FOR_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+26;
int REPORT_THUMBNAIL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+27;
int GET_CONTENT_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+28;
int PUBLISH_CONTENT_PROVIDERS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+29;
int REF_CONTENT_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+30;
int FINISH_SUB_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+31;
int GET_RUNNING_SERVICE_CONTROL_PANEL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+32;
int START_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+33;
int STOP_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+34;
int BIND_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+35;
int UNBIND_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+36;
int PUBLISH_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+37;
int ACTIVITY_RESUMED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+38;
int GOING_TO_SLEEP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+39;
int WAKING_UP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+40;
int SET_DEBUG_APP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+41;
int SET_ALWAYS_FINISH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+42;
int START_INSTRUMENTATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+43;
int FINISH_INSTRUMENTATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+44;
int GET_CONFIGURATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+45;
int UPDATE_CONFIGURATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+46;
int STOP_SERVICE_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+47;
int GET_ACTIVITY_CLASS_FOR_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+48;
int GET_PACKAGE_FOR_TOKEN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+49;
int SET_PROCESS_LIMIT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+50;
int GET_PROCESS_LIMIT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+51;
int CHECK_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+52;
int CHECK_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+53;
int GRANT_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+54;
int REVOKE_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+55;
int SET_ACTIVITY_CONTROLLER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+56;
int SHOW_WAITING_FOR_DEBUGGER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+57;
int SIGNAL_PERSISTENT_PROCESSES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+58;
int GET_RECENT_TASKS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+59;
int SERVICE_DONE_EXECUTING_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+60;
int ACTIVITY_DESTROYED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+61;
int GET_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+62;
int CANCEL_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+63;
int GET_PACKAGE_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+64;
int ENTER_SAFE_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+65;
int START_NEXT_MATCHING_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+66;
int NOTE_WAKEUP_ALARM_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+67;
int REMOVE_CONTENT_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+68;
int SET_REQUESTED_ORIENTATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+69;
int GET_REQUESTED_ORIENTATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+70;
int UNBIND_FINISHED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+71;
int SET_PROCESS_FOREGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+72;
int SET_SERVICE_FOREGROUND_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+73;
int MOVE_ACTIVITY_TASK_TO_BACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+74;
int GET_MEMORY_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+75;
int GET_PROCESSES_IN_ERROR_STATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+76;
int CLEAR_APP_DATA_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+77;
int FORCE_STOP_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+78;
int KILL_PIDS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+79;
int GET_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+80;
int GET_TASK_THUMBNAILS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+81;
int GET_RUNNING_APP_PROCESSES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+82;
int GET_DEVICE_CONFIGURATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+83;
int PEEK_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+84;
int PROFILE_CONTROL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+85;
int SHUTDOWN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+86;
int STOP_APP_SWITCHES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+87;
int RESUME_APP_SWITCHES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+88;
int START_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+89;
int BACKUP_AGENT_CREATED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+90;
int UNBIND_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+91;
int GET_UID_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+92;
int HANDLE_INCOMING_USER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+93;
int GET_TASK_TOP_THUMBNAIL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+94;
int KILL_APPLICATION_WITH_APPID_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+95;
int CLOSE_SYSTEM_DIALOGS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+96;
int GET_PROCESS_MEMORY_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+97;
int KILL_APPLICATION_PROCESS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+98;
...........

这些值我们同样可以在 源码目录/java/android/app/IActivityManager.java源码中看到,很多值,上面只是一部分。所以上面的onTransact方法中的switch分支结构是有很多代码的,这里,我们就从那个分支中选择一种可能将他的代码拷贝到我们的onTransact方法中进行分析:

@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
//打印值,因为data是请求者带来的数据,reply是接受者返回的数据,这里看data数据
Log.i("TTT", "code="+code);
String callingPackage = data.readString();
Log.i("TTT", "callingPackage:"+callingPackage);
String resolvedType = data.readString();
Log.i("TTT", "resolvedType:"+resolvedType);
String resultWho = data.readString();
Log.i("TTT", "resultWho:"+resultWho);
int requestCode = data.readInt();
Log.i("TTT", "requestCode:"+requestCode);
int startFlags = data.readInt();
Log.i("TTT", "startFlags:"+startFlags);
String profileFile = data.readString();
Log.i("TTT", "profileFile:"+profileFile);
int v1 = data.readInt();
Log.i("TTT","v1:"+v1);
int v2 = data.readInt();
Log.i("TTT","v2:"+v2);
int userId = data.readInt();
Log.i("TTT","userId:"+userId);
return mBinder.transact(code, data, reply, flags);
}

通过打印的值,来分析这些字段的含义。

DemoInject3.apk的项目下载地址:http://download.csdn.net/detail/jiangwei0910410003/8138233
好了,如果上面的项目都编译成功的话,会得到两个文件:

libproxybinder.so、DemoInject3.apk

第二部分:拷贝文件

将libproxybinder.so文件拷贝到/data/data/目录下

adb push libproxybinder.so /data/data/

修改权限

chmod 777 libproxybinder.so

将DemoInject3.apk拷贝到/data/local/tmp/目录下

adb push DemoInject3.apk /data/local/tmp/

第三部分:开始执行

那么下面开始执行吧。但是想一想,我们应该注入到哪个进程中呢?这个在开头的时候也说过了,是system_process,但是当我们使用ps命令去查看进程信息的时候,会发现找不到这个进程名,其实google一下之后,会发现,其实我们真正要注入的进程名是:system_server,而system_process只是这个进程的应用名称,相当于我们普通应用的名字和我们的应用的包名(一般包名是进程的名称),所以我们再次查找一下system_server进程的时候就找到了:


进程的pid是599。

好的下面就开始执行了,我们需要开启两个终端即可:一个监听log信息,一个执行注入程序

1、执行注入程序

./poison /data/data/libproxybinder.so 599

为了能看到哪些应用在使用权限,这里我打开了百度地图app.

2、监听log信息

adb logcat -s TTT

通过打印的结果,我们看到两个重要的信息,一个resultWho字段,他的值就是应用使用的权限,百度地图使用了位置信息权限。还有就是requestCode字段,他的值就是应用的进程pid,不信的话,我们在使用ps查看一下进程列表:

看到了,进程pid为5611,看到进程名为:com.baidu.BaiduMap.pad:remote

所以我们可以得到两个重要的字段:

resultWho:应用使用的权限

requestCode:应用的进程id

有了这两个值,我们就可以得到哪个应用正在使用什么权限(可以通过pid得到应用的名称,这个很简单,百度一下就可以了)

那么到此我们就实现了在Java层堆应用程序的行为拦截,在第三个例子中主要就是在上次我们自定义一个Binder,然后将其传给底层,然后底层在替换JavaBBinder对象即可。在这个例子中我们可以看到我之前讲到的第三个知识点:如何替换系统服务(Binder).

四、总结

这篇文章总算是结束了,篇幅有点长,主要是通过三个例子,徐徐渐进的讲解一下如何拦截应用的行为:

第一个例子:如何将我们的功能模块libmyso.so注入到简单的应用程序中demo1

技术点:动态加载so文件,然后执行其中的指定函数

第二个例子:如何将我们的功能模块libimportdex.so注入到我们的Android应用中

技术点:获取JNIEnv对象,获取进程中的Context对象

第三个例子:如何将我们的功能模块注入到系统进程system_process(system_server)中

技术点:上层自定义一个Binder,在onTransact方法中分析数据,底层替换JavaBBinder对象

当然这篇文章讲的知识点可能比较多,但是主要就是我开头讲到的三个知识点:

1、如何动态加载so,并且执行其中的函数

2、如何在C层执行Java方法(NDK一般是指Java中调用C层函数)

3、如何修改系统服务(Context.getSystemService(String...)其实返回来的就是Binder对象)对象

注:上面讲到的三个例子中的项目代码都有相应的下载地址,如果download下来之后,运行有问题的话,可以给我留言,我会尽可能的帮助解决一下。

最后:其实这篇写完这篇文章之后,发现了我还有两个地方没有理解清楚:

1、同一个进程中的Application对象以及Context对象的含义

2、Android中的Binder机制

这个我会在后续的文章中再次进行详细的总结。

(PS:总算是写完了,真心写了一下午还没写完,一大早上来接着写的,这个文章的篇幅有点长,可能需要很有耐心的去看,当然如果看到有错误的地方,还请各位大哥能指出来,谢谢~~)

Andrdoid中对应用程序的行为拦截实现方式之----从Java层进行拦截的更多相关文章

  1. Andrdoid中相应用程序的行为拦截实现方式之----从Java层进行拦截

    致谢: 感谢 简行之旅的这篇blog:http://blog.csdn.net/l173864930/article/details/38455951,这篇文章是參考这篇blog的进行一步一步操作的, ...

  2. Andrdoid中对应用程序的行为拦截实现方式之----从底层C进行拦截

    之前的一篇概要文章中主要说了我这次研究的一些具体情况,这里就不在多说了,但是这里还需要指出的是,感谢一下三位大神愿意分享的知识(在我看来,懂得分享和细致的人才算是大神,不一定是技术牛奥~~) 第一篇: ...

  3. 利用delve(dlv)在Visual Code中进行go程序的远程调试-debug方式

    最近碰到一个问题,如何在Windows的IDE或者文本编辑器上,远程调试Linux服务器上的golang程序. 虽然想说gdb走你,但既然go有dlv这样的类似Java的jdwp的原生方案,而且我用的 ...

  4. [原]MFC中DIALOG(对话框)程序响应加速键(快捷键)

    [原]MFC中DIALOG(对话框)程序响应加速键(快捷键) 2014-8-6阅读266 评论0 新建一个对话框程序,项目名为Test,删除默认确定,取消和静态文本框控件.添加一个按钮,Caption ...

  5. OSG中的示例程序简介

    OSG中的示例程序简介 转自:http://www.cnblogs.com/indif/archive/2011/05/13/2045136.html 1.example_osganimate一)演示 ...

  6. 在dos中运行java程序,若出现Exception in thread “main" java.lang.NoClassDefFoundError

    在dos中运行java程序,若出现Exception in thread “main" java.lang.NoClassDefFoundError,可以检查一下几项: 环境变量配置: 注意 ...

  7. 如何在windows中编写R程序包(转载)

    网上有不少R包的编译过程介绍,挑选了一篇比较详细的,做了稍许修改后转载至此,与大家分享 如何在windows中编写R程序包 created by helixcn modified by binaryf ...

  8. 【翻译】Anatomy of a Program in Memory—剖析内存中的一个程序(进程的虚拟存储器映像布局详解)

    [翻译]Anatomy of a Program in Memory—剖析内存中的一个程序(进程的虚拟存储器映像布局详解) . . .

  9. 使用ASP.NET操作IIS7中使用应用程序

    使用ASP.NET操作IIS7中使用应用程序   在最新发布的启明星Portal里,增加了安装程序,下面说一下.NET对IIS7操作.IIS7的操作和IIS5/6有很大的不同,在IIS7里增加了 Mi ...

随机推荐

  1. PHP curl_getinfo函数

    curl_getinfo — 获取一个cURL连接资源句柄的信息 说明 mixed curl_getinfo ( resource $ch [, int $opt = 0 ] ) 获取最后一次传输的相 ...

  2. 【Vue】axios post提交请求转为form data

    axios.js import axios from 'axios'; import qs from 'qs'; // axios 配置 axios.defaults.timeout = 8000; ...

  3. MySqL rownum 序号 类似于 oracle的rownum

    mysql中没有 rownum 序号的功能,所以需要自己去实现序号的功能. @rownum 只是一个变量 可以换为 @i 等其他变量,但必须有@符号 SELECT @rownum:=@rownum+1 ...

  4. 27 October in ss

    Contest A. chrono 计算某年的干支纪年法年份. Too easy. 然而我忘记 C++ 取模运算是向0取整.然而数据太水,还是有 90 分. B. clock 计算某时刻时针和分针的夹 ...

  5. python中socket理论

    Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单 ...

  6. python中冒泡 排序法练习题

    # 第四题:写出冒泡排序函数,可以排序任意类型的元素,可以逆序 # 1.实现冒泡排序算法 # 2.可以排序任意类型的元素 # 3.能够通过参数设置进行逆序,默认升序 def my_sort(lt,ke ...

  7. 用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能

    Blog 项目源码:https://github.com/JmilkFan/JmilkFan-s-Blog 目录 目录 前文列表 扩展阅读 实现文章管理功能 实现效果 前文列表 用 Flask 来写个 ...

  8. yii之relations关联非主键

    yii的relations里self::BELONGS_TO默认是用当前指定的键跟关联表的主键进行join,例如: return array('reply' => array(self::BEL ...

  9. win10下cmd备注

    要复制cmd里的字符串,右键选标记,选中待复制的字符串,ctrl+c 复制,ctrl+v粘贴内容(或者右键选择复制).这项操作支持复制cmd里的内容到其他地方,如txt里 win10之前cmd不支持c ...

  10. LeetCode刷题: 【120】三角形最小路径和

    1. 题目: 给定一个三角形,找出自顶向下的最小路径和.每一步只能移动到下一行中相邻的结点上. 例如,给定三角形: [ [2], [3,4], [6,5,7], [4,1,8,3] ] 自顶向下的最小 ...