Andrdoid中相应用程序的行为拦截实现方式之----从Java层进行拦截
致谢:
感谢 简行之旅的这篇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, ®s) < 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, ®s) == -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)中,事实上这两个參数是在代码中写死的,假设忘记的同学能够回去看一下。就是前面提到的从底层进行拦截的那篇文章。
那么这样改动之后,貌似灵活性更高了。
当然注入功能的代码不止这一个,事实上是一个project。这里由于篇幅的原因就不做介绍了,project的下载地址:
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
这个就简单了,就是非常easy的代码,起一个循环每一个一段时间打印数值,这个项目须要引用上面编译的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文件,而且运行当中的函数,原理非常easy,就是打开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
第二部分:复制文件
好了。到这里离成功不远了,我们保证上面的project编译都能通过,得到以下文件:
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
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamlhbmd3ZWkwOTEwNDEwMDAz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
2、运行demo1
./demo1
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamlhbmd3ZWkwOTEwNDEwMDAz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
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的功能一样的,写个循环,打印数值,project下载地址: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
首先来看一下实现原理吧:
以下我们以AMS为例,做一个说明:
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; //...
}
class JavaBBinder : public BBinder
{ //... jobject object() const
{
return mObject;
} //... private:
JavaVM* const mVM;
jobject const mObject; //这个保存的是AMS的引用
};
}
当中JavaBBinder中的mObject是整个IPC关键的一节。全部的client请求。都是先到达JavaBBinder,然后JavaBBinder再通过JNI调用mObject的execTransact的方法,终于把请求发送到AMS。
在实现这个代理,我们须要获取AMS和及相应用的JavaBBinder两个对象。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvTDE3Mzg2NDkzMA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="460" height="250" alt="" style="border: none; max-width: 100%;" />
获取AMS引用
另外,也有一种比較简单的方式,那就是通过defaultServiceManager的getService方法获取到。
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;
};
事实上底层的注入模块的功能非常easy,就是动态载入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得到应用的名称,这个非常easy。百度一下就能够了)
那么到此我们就实现了在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层进行拦截的更多相关文章
- Andrdoid中对应用程序的行为拦截实现方式之----从Java层进行拦截
致谢: 感谢 简行之旅的这篇blog:http://blog.csdn.net/l173864930/article/details/38455951,这篇文章是参考这篇blog的进行一步一步操作的, ...
- Andrdoid中对应用程序的行为拦截实现方式之----从底层C进行拦截
之前的一篇概要文章中主要说了我这次研究的一些具体情况,这里就不在多说了,但是这里还需要指出的是,感谢一下三位大神愿意分享的知识(在我看来,懂得分享和细致的人才算是大神,不一定是技术牛奥~~) 第一篇: ...
- 利用delve(dlv)在Visual Code中进行go程序的远程调试-debug方式
最近碰到一个问题,如何在Windows的IDE或者文本编辑器上,远程调试Linux服务器上的golang程序. 虽然想说gdb走你,但既然go有dlv这样的类似Java的jdwp的原生方案,而且我用的 ...
- [原]MFC中DIALOG(对话框)程序响应加速键(快捷键)
[原]MFC中DIALOG(对话框)程序响应加速键(快捷键) 2014-8-6阅读266 评论0 新建一个对话框程序,项目名为Test,删除默认确定,取消和静态文本框控件.添加一个按钮,Caption ...
- OSG中的示例程序简介
OSG中的示例程序简介 转自:http://www.cnblogs.com/indif/archive/2011/05/13/2045136.html 1.example_osganimate一)演示 ...
- 在dos中运行java程序,若出现Exception in thread “main" java.lang.NoClassDefFoundError
在dos中运行java程序,若出现Exception in thread “main" java.lang.NoClassDefFoundError,可以检查一下几项: 环境变量配置: 注意 ...
- 如何在windows中编写R程序包(转载)
网上有不少R包的编译过程介绍,挑选了一篇比较详细的,做了稍许修改后转载至此,与大家分享 如何在windows中编写R程序包 created by helixcn modified by binaryf ...
- 【翻译】Anatomy of a Program in Memory—剖析内存中的一个程序(进程的虚拟存储器映像布局详解)
[翻译]Anatomy of a Program in Memory—剖析内存中的一个程序(进程的虚拟存储器映像布局详解) . . .
- 使用ASP.NET操作IIS7中使用应用程序
使用ASP.NET操作IIS7中使用应用程序 在最新发布的启明星Portal里,增加了安装程序,下面说一下.NET对IIS7操作.IIS7的操作和IIS5/6有很大的不同,在IIS7里增加了 Mi ...
随机推荐
- MVC跳转
//RedirectToAction(view?参数,控制器); return RedirectToAction("MyjoinEvent?id=" + eventid + &qu ...
- c# 学习笔记(三)
程序集程序集的私有部署 不用在注册表中注册组件卸载只需要从文件系统中删除他即可 共享程序集和GAC 只有强命名程序集能被添加到GAC中程序集数据签名只需在安装到GAC时检查一次 GAC内的并肩执行GA ...
- 使用WebUploader使用,及使用后测试横拍或竖拍图片图片方向不对等解决方案
WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件.在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览 ...
- 关于NSURL的一些属性的记录
关于NSURL的一些属性的记录 NSLog(@"%@", request.URL.absoluteString); NSLog(@"%@", request.U ...
- 使用PPRevealSideViewController实现侧滑效果
使用起来还是比较简单的, 主要是几个步骤 AppDelegate.m - (BOOL)application:(UIApplication *)application didFinishLaunchi ...
- 【C#学习笔记】一、基础知识
1.1数据类型(P43) 类型 别名 允许的值 sbyte System.SByte -128~127 byte System.Byte 0~255 short System.Int16 -32768 ...
- ASP.NET菜鸟之路之Response小例子
背景 我是一个ASP.NET菜鸟,暂时开始学习ASP.NET,在此记录下我个人敲的代码,没有多少参考价值,请看到的盆友们为我点个赞支持我一下,多谢了. Response.Write Redirect ...
- 『重构--改善既有代码的设计』读书笔记----Extract Method
在编程中,比较忌讳的一件事情就是长函数.因为长函数代表了你这段代码不能很好的复用以及内部可能出现很多别的地方的重复代码,而且这段长函数内部的处理逻辑你也不能很好的看清楚.因此,今天重构第一个手法就是处 ...
- 动态加载下拉框列表并添加onclick事件
1. js动态加载元素并设置属性 摘自(http://www.liangshunet.com/ca/201408/336848696.htm) <div id="parent&quo ...
- PHP文件类型检查类-比较全的
在CSDN上淘来的一个文件类型的类,还不错,留下自己看! <? /** * 检证文件类型类 * * @author */ class FileTypeValidation { // 文件类型,不 ...