文档章节

Android.Hook框架Cydia篇

henry-zhang
 henry-zhang
发布于 2015/11/11 15:00
字数 3426
阅读 2597
收藏 4

Cydia Substrate是一个代码修改平台.它可以修改任何主进程的代码,不管是用Java还是C/C++(native代码)编写的.而Xposed只支持HOOK app_process中的java函数,因此Cydia Substrate是一款强大而实用的HOOK工具.

官网地址:http://www.cydiasubstrate.com/

官方教程:http://www.cydiasubstrate.com/id/38be592b-bda7-4dd2-b049-cec44ef7a73b

SDK下载地址:http://asdk.cydiasubstrate.com/zips/cydia_substrate-r2.zip

0x00Hook Java 层

之前讲解过 xposed 的用法为啥还要整这个了,下面简单对比两款框架.想了解之前 xposed 篇的可以看这里:http://drops.wooyun.org/tips/7488

劣势:

  • 没啥错误提醒,排错比较麻烦.

  • 需要对 NDK 开发有一定了解,相对 xposed 模块的开发学习成本高一些.

  • 因为不开源网上(github)上可以参考的模块代码很少.

优势:

  • 可以对 native 函数进行 hook .

  • 与 xposed hook 原理不一样,因为不是开源具体原理我也不清楚. 结果就是一些Anti hook 可能对 xposed 有效而对 Cydia 无效.

使用方法

1.安装框架app:http://www.cydiasubstrate.com/download/com.saurik.substrate.apk

2.创建一个空的Android工程.由于创建的工程将以插件的形式被加载,所以不需要activity.将SDK中的substrate-api.jar复制到project/libs文件夹中.

3.配置Manifest文件

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <meta-data android:name="com.saurik.substrate.main"
            android:value=".Main"/>
    </application>
    <uses-permission android:name="cydia.permission.SUBSTRATE"/>
</manifest>

4.创建一个类,类名为Main.类中包含一个static方法initialize,当插件被加载的时候,该方法中的代码就会运行,完成一些必要的初始化工作.

import com.saurik.substrate.MS;
public class Main {
    static void initialize() { 
        // ... code to run when extension is loaded
    }
}

5.hook imei example

import com.saurik.substrate.MS;
public class Main {
    static void initialize() {
        MS.hookClassLoad("android.telephony.TelephonyManager",
                new MS.ClassLoadHook() {
                    @SuppressWarnings("unchecked")
                    public void classLoaded(Class<?> arg0) {
                        Method hookimei;
                        try {
                            hookimei = arg0.getMethod("getDeviceId", null);
                        } catch (NoSuchMethodException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                            hookimei = null;
                        }
                        if (hookimei != null) {
                            final MS.MethodPointer old1 = new MS.MethodPointer();
                            MS.hookMethod(arg0, hookimei, new MS.MethodHook() {
                                @Override
                                public Object invoked(Object arg0,
                                        Object... arg1) throws Throwable {
                                    // TODO Auto-generated method stub
                                    System.out.println("hook imei----------->");
                                    String imei = (String) old1.invoke(arg0,
                                            arg1);
                                    System.out.println("imei-------->" + imei);
                                    imei = "999996015409998";
                                    return imei;
                                }
                            }, old1);
                        }
                    }
                });
    }
}

6.在 cydia app 界面中点击 Link Substrate Files 之后重启手机

7.使用getimei的小程序验证imei是否被改变

public class MainActivity extends ActionBarActivity {
    private static final String tag = "MainActivity";
    TextView mText ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mText = (TextView) findViewById(R.id.text);
        TelephonyManager mtelehonyMgr = (TelephonyManager) getSystemService(this.TELEPHONY_SERVICE);
        Build bd = new Build(); 
        String imei = mtelehonyMgr.getDeviceId(); 
        String imsi = mtelehonyMgr.getSubscriberId();
        //getSimSerialNumber()   获取 SIM 序列号  getLine1Number 获取手机号
        String androidId = Secure.getString(getApplicationContext().getContentResolver(), Secure.ANDROID_ID); 
        String id = UUID.randomUUID().toString();
        String model = bd.MODEL;
        StringBuilder sb = new StringBuilder();
        sb.append("imei = "+ imei);
        sb.append("\nimsi = " + imsi);
        sb.append("\nandroid_id = " + androidId);
        sb.append("\nuuid = " + id);
        sb.append("\nmodel = " + model);
        if(imei!=null)
            mText.setText(sb.toString());
        else
            mText.setText("fail");
    }

8.关键api介绍

MS.hookClassLoad:该方法实现在指定的类被加载的时候发出通知(改变其实现方式?).因为一个类可以在任何时候被加载,所以Substrate提供了一个方法用来检测用户感兴趣的类何时被加载.

这个api需要实现一个简单的接口MS.ClassLoadHook,该接口只有一个方法classLoaded,当类被加载的时候该方法会被执行.加载的类以参数形式传入此方法.

void hookClassLoad(String name, MS.ClassLoadHook hook);


参数          描述

name 包名+类名,使用java的.符号(被hook的完整类名)

hook MS.ClassLoadHook的一个实例,当这个类被加载的时候,它的classLoaded方法会被执行.

MS.hookClassLoad("java.net.HttpURLConnection",
    new MS.ClassLoadHook() {
        public void classLoaded(Class<?> _class) {
            /* do something with _class argument */
        }
    }
);

MS.hookMethod:该API允许开发者提供一个回调函数替换原来的方法,这个回调函数是一个实现了MS.MethodHook接口的对象,是一个典型的匿名内部类.它包含一个invoked函数.

void hookMethod(Class _class, Member member, MS.MethodHook hook, MS.MethodPointer old);

参数 描述

_class 加载的目标类,为classLoaded传下来的类参数

member 通过反射得到的需要hook的方法(或构造函数). 注意:不能HOOK字段 (在编译的时候会进行检测).

hook MS.MethodHook的一个实例,其包含的invoked方法会被调用,用以代替member中的代码

0x01Hook Native 层

这块的功能 xposed 就不能实现啦.

整个流程大致如下:

  • 创建工程,添加 NDK 支持

  • 将 cydia 的库和头文件加入工程

  • 修改 AndroidManifest配置文件

  • 修改Android.md

  • 开发模块

    • MSGetImageByName or dlopen

    • MSFindSymbol or dlsym or nlist 指定方法,得到开始地址

    • MSHookFunction 替换函数

    • 指定要hook 的 lib 库

    • 保留原来的地址

    • 替换的函数

    • Substrate entry point

使用方法

**第零步:添加 ndk 支持,将 cydia 的库和头文件加入工程

有关 ndk 开发的基础可以参考此文: NDK入门篇

注意要是 xxx.cy.cpp,不要忘记.cy

其实应该是动态链接库名称中的 cy 必须有,所有在 Android.md 中module 处的 .cy 必须带上咯

LOCAL_MODULE    := DumpDex2.cy

第一步:修改配置文件

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:installLocation="internalOnly"
>
    <application android:hasCode="false">
    </application>

    <uses-permission android:name="cydia.permission.SUBSTRATE"/>
</manifest>

设置 android:hasCode 属性 false,设置android:installLocation属性internalOnly"

第二步:指定要 hook 的 lib 库

#include <substrate.h>

MSConfig(MSFilterExecutable, "/system/bin/app_process")  //MSConfig(MSFilterLibrary, "liblog.so")

// this is a macro that uses __attribute__((__constructor__))
MSInitialize {
    // ... code to run when extension is loaded
}

设置要 hook 的可执行文件或者动态库

第三步: 等待 class

static void OnResources(JNIEnv *jni, jclass resources, void *data) {
    // ... code to modify the class when loaded
}

MSInitialize {
    MSJavaHookClassLoad(NULL, "android/content/res/Resources", &OnResources);
}

第四步:修改实现

static jint (*_Resources$getColor)(JNIEnv *jni, jobject _this, ...);

static jint $Resources$getColor(JNIEnv *jni, jobject _this, jint rid) {
    jint color = _Resources$getColor(jni, _this, rid);
    return color & ~0x0000ff00 | 0x00ff0000;
}

static void OnResources(JNIEnv *jni, jclass resources, void *data) {
    jmethodID method = jni->GetMethodID(resources, "getColor", "(I)I");
    if (method != NULL)
        MSJavaHookMethod(jni, resources, method,
            &$Resources$getColor, &_Resources$getColor);
}

下面是步骤是在官网教程基础上对小白同学的一些补充吧.

» file libprocess.so                                                                  
libprocess.so: ELF 32-bit LSB shared object, ARM, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

第五步

复制libsubstrate-dvm.so(注意 arm 和 x86平台的选择)和substrate.h到 jni 目录下.创建SuperMathHook.cy.cpp文件

第六步

配置Android.mk文件

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE:= substrate-dvm
LOCAL_SRC_FILES := libsubstrate-dvm.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE    := SuperMathHook.cy
LOCAL_SRC_FILES := SuperMathHook.cy.cpp
LOCAL_LDLIBS := -llog
LOCAL_LDLIBS += -L$(LOCAL_PATH) -lsubstrate-dvm //-L指定库文件的目录,-l指定库文件名,-I指定头文件的目录.
include $(BUILD_SHARED_LIBRARY)

加入 c 的 lib

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE:= substrate-dvm
LOCAL_SRC_FILES := libsubstrate-dvm.so
include $(PREBUILT_SHARED_LIBRARY)


include $(CLEAR_VARS)
LOCAL_MODULE:= substrate
LOCAL_SRC_FILES := libsubstrate.so
include $(PREBUILT_SHARED_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE    := CydiaN.cy
LOCAL_SRC_FILES := CydiaN.cy.cpp
LOCAL_LDLIBS := -llog
LOCAL_LDLIBS += -L$(LOCAL_PATH) -lsubstrate-dvm -lsubstrate

include $(BUILD_SHARED_LIBRARY)

strings 查看下里面的函数.

/data/data/com.jerome.jni/lib # strings libprocess.so                                                               <
/system/bin/linker
__cxa_finalize
__cxa_atexit
Jstring2CStr
malloc
memcpy
__aeabi_unwind_cpp_pr0
Java_com_jerome_jni_JNIProcess_getInfoMD5
....

脱壳机模块发开

网上流传的 IDA dump 脱壳流程大致如下:

  • 对/system/lib/libdvm.so 方法JNI_OnLoad/dvmLoadNativeCode/dvmDexFileOpenPartial下断点分析

  • IDA 附加 app (IDA6.5以及之后版本)

  • Ctrl+s 查看基地址+偏移

  • IDA 分析寻找 dump 点

  • F8/F9执行到dex完全被解密到内存中时候进行 dump

现在目标就是通过 Cydia 的模块来自动化完成这个功能.这里咱选择对dvmDexFileOpenPartial函数进行 hook.至于为什么要选择这里了?这就需要分析下 android dex优化过程

Android会对每一个安装的应用的dex文件进行优化,生成一个odex文件.相比于dex文件,odex文件多了一个optheader,依赖库信息(dex文件所需要的本地函数库)和辅助信息(类索引信息等).

dex的优化过程是一个独立的功能模块来实现的,位于http://androidxref.com/4.4.3_r1.1/xref/dalvik/dexopt/OptMain.cpp#57 其中extractAndProcessZip()函数完成优化操作.

http://androidxref.com/4.1.1/xref/dalvik/dexopt/OptMain.cpp

OptMain中的main函数就是加载dex的最原始入口

int main(int argc, char* const argv[])
{
    set_process_name("dexopt");
 
    setvbuf(stdout, NULL, _IONBF, 0);
 
    if (argc > 1) {
        if (strcmp(argv[1], "--zip") == 0)
            return fromZip(argc, argv);
        else if (strcmp(argv[1], "--dex") == 0)
            return fromDex(argc, argv);
        else if (strcmp(argv[1], "--preopt") == 0)
            return preopt(argc, argv);
    }
    ...
    return 1;
}

可以看到,这里会分别对3中类型的文件做不同处理,我们关心的是dex文件,所以接下来看看fromDex函数:

static int fromDex(int argc, char* const argv[])
{
...
if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode, flags) != 0) {
    ALOGE("VM init failed");
    goto bail;
}
 
vmStarted = true;
 
/* do the optimization */
if (!dvmContinueOptimization(fd, offset, length, debugFileName,
        modWhen, crc, (flags & DEXOPT_IS_BOOTSTRAP) != 0))
{
    ALOGE("Optimization failed");
    goto bail;
}
...
}

这个函数先初始化了一个虚拟机,然后调用dvmContinueOptimization函数/dalvik/vm/analysis/DexPrepare.cpp,进入这个函数:

bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength,
    const char* fileName, u4 modWhen, u4 crc, bool isBootstrap)
{
    ...
    /*
         * Rewrite the file.  Byte reordering, structure realigning,
         * class verification, and bytecode optimization are all performed
         * here.
         *
         * In theory the file could change size and bits could shift around.
         * In practice this would be annoying to deal with, so the file
         * layout is designed so that it can always be rewritten in place.
         *
         * This creates the class lookup table as part of doing the processing.
         */
        success = rewriteDex(((u1*) mapAddr) + dexOffset, dexLength,
                    doVerify, doOpt, &pClassLookup, NULL);
 
        if (success) {
            DvmDex* pDvmDex = NULL;
            u1* dexAddr = ((u1*) mapAddr) + dexOffset;
 
            if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) {
                ALOGE("Unable to create DexFile");
                success = false;
            } else {
    ...
}

这个函数中对Dex文件做了一些优化(如字节重排序,结构对齐等),然后重新写入Dex文件.如果优化成功的话接下来调用dvmDexFileOpenPartial,而这个函数中调用了真正的Dex文件.在具体看看这个函数/dalvik/vm/DvmDex.cpp

/*
 * Create a DexFile structure for a "partial" DEX.  This is one that is in
 * the process of being optimized.  The optimization header isn't finished
 * and we won't have any of the auxillary data tables, so we have to do
 * the initialization slightly differently.
 *
 * Returns nonzero on error.
 */
int dvmDexFileOpenPartial(const void* addr, int len, DvmDex** ppDvmDex)
{
    DvmDex* pDvmDex;
    DexFile* pDexFile;
    int parseFlags = kDexParseDefault;
    int result = -1;
 
    /* -- file is incomplete, new checksum has not yet been calculated
    if (gDvm.verifyDexChecksum)
        parseFlags |= kDexParseVerifyChecksum;
    */
 
    pDexFile = dexFileParse((u1*)addr, len, parseFlags);
    if (pDexFile == NULL) {
        ALOGE("DEX parse failed");
        goto bail;
    }
    pDvmDex = allocateAuxStructures(pDexFile);
    if (pDvmDex == NULL) {
        dexFileFree(pDexFile);
        goto bail;
    }
 
    pDvmDex->isMappedReadOnly = false;
    *ppDvmDex = pDvmDex;
    result = 0;
 
bail:
    return result;
}

这个函数的前两个参数非常关键,第一个参数是dex文件的起始地址,第二个参数是dex文件的长度,有了这两个参数,就可以从内存中将这个dex文件dump下来了,这也是在此函数下断点的原因.该函数会调用dexFileParse()对dex文件进行解析

所以在dexFileParse函数处来进行 dump 也是可行的.但是因为这个函数的原型是

DexFile* dexFileParse(const u1* data, size_t length, int flags)

其返回值为一个结构体指针struct DexFile { ... },要 hook 这个函数得把结构体从 android 源码中扣出来或者直接改镜像.

找到dvmDexFileOpenPartial函数在 libdvm.so 对应的名称

» strings libdvm_arm.so|grep dvmDexFileOpenPartial
_Z21dvmDexFileOpenPartialPKviPP6DvmDex
 
» strings libdvm_arm.so|grep dexFileParse
_Z12dexFileParsePKhji

有了上述理论基础,现在可以正式开发模块了.大致流程如下

  • 指定要hook 的 lib 库

  • Original method template 原函数模板

  • Modified method 替换的函数

  • Substrate entry point

    • MSGetImageByName or dlopen 载入lib得到 image

    • MSFindSymbol or dlsym or nlist 指定方法,得到开始地址

    • MSHookFunction 替换函数

完整代码

#include "substrate.h"
#include <android/log.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
 
#define BUFLEN 1024
#define TAG "DEXDUMP"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
 
//get packagename from pid
int getProcessName(char * buffer){
    char path_t[256]={0};
    pid_t pid=getpid();
    char str[15];
    sprintf(str, "%d", pid);
    memset(path_t, 0 , sizeof(path_t));
    strcat(path_t, "/proc/");
    strcat(path_t, str);
    strcat(path_t, "/cmdline");
    //LOG_ERROR("zhw", "path:%s", path_t);
    int fd_t = open(path_t, O_RDONLY);
    if(fd_t>0){
        int read_count = read(fd_t, buffer, BUFLEN);
 
        if(read_count>0){
              int  processIndex=0;
              for(processIndex=0;processIndex<strlen(buffer);processIndex++){
                  if(buffer[processIndex]==':'){
                      buffer[processIndex]='_';
                  }
 
              }
            return 1;
        }
    }
    return 0;
}
 
//指定要hook 的 lib 库
MSConfig(MSFilterLibrary,"/system/lib/libdvm.so")
 
//保留原来的地址  DexFile* dexFileParse(const u1* data, size_t length, int flags)
int (* oldDexFileParse)(const void * addr,int len,int flags);
 
//替换的函数
int myDexFileParse(const void * addr,int len,void ** dvmdex)
{
    LOGD("call my dvm dex!!:%d",getpid());
 
    {
        //write to file
        //char buf[200];
        // 导出dex文件
        char dexbuffer[64]={0};
        char dexbufferNamed[128]={0};
        char * bufferProcess=(char*)calloc(256,sizeof(char));
        int  processStatus= getProcessName(bufferProcess);
        sprintf(dexbuffer, "_dump_%d", len);
        strcat(dexbufferNamed,"/sdcard/");
        if (processStatus==1) {
          strcat(dexbufferNamed,bufferProcess);
            strcat(dexbufferNamed,dexbuffer);
 
        }else{
            LOGD("FAULT pid not  found\n");
        }
 
        if(bufferProcess!=NULL)
        {
 
          free(bufferProcess);
        }
 
        strcat(dexbufferNamed,".dex");
 
        //sprintf(buf,"/sdcard/dex.%d",len);
        FILE * f=fopen(dexbufferNamed,"wb");
        if(!f)
        {
            LOGD(dexbuffer + " : error open sdcard file to write");
        }
        else{
            fwrite(addr,1,len,f);
            fclose(f);
        }
 
 
 
    }
    //进行原来的调用,不影响程序运行
    return oldDexFileParse(addr,len,dvmdex);
}
 
//Substrate entry point
MSInitialize
{
    LOGD("Substrate initialized.");
    MSImageRef image;
    //载入lib
    image = MSGetImageByName("/system/lib/libdvm.so");
    if (image != NULL)
    {
 
        void * dexload=MSFindSymbol(image,"_Z21dvmDexFileOpenPartialPKviPP6DvmDex");
        if(dexload==NULL)
        {
            LOGD("error find _Z21dvmDexFileOpenPartialPKviPP6DvmDex ");
 
        }
        else{
            //替换函数
            //3.MSHookFunction
            MSHookFunction(dexload,(void*)&myDexFileParse,(void **)&oldDexFileParse);
        }
    }
    else{
        LOGD("ERROR FIND LIBDVM");
    }
}

效果如下:

shell@hammerhead:/sdcard $ l |grep dex
app_process_classes_3220.dex
com.ali.tg.testapp_classes_606716.dex
com.chaozh.iReaderFree_classes_4673256.dex
com.secken.app_xg_service_v2_classes_6327832.dex

脱壳机模块改进一

更改 hook 点为 dexFileParse,上文已经讲解了为啥也可以选择这里.也分析了 dex 优化的过程,这里在分析下 dex 加载的过程.

DexClassLoader广泛被开发者用于插件的动态加载.而PathClassLoader几乎没怎么见过.

因为PathClassLoader 没有提供优化 dex 的目录而是固定将 odex 存放到 /data/dalvik-cache 中 ,故它只能加载已经安装到 Android 系统中的 apk 文件,也就是 /data/app 目录下的 apk 文件.

PathClassLoader 和 DexClassLoader 父类为 BaseDexClassLoader

http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

 public BaseDexClassLoader(String dexPath, File optimizedDirectory,
       String libraryPath, ClassLoader parent) {

http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

DexPathList(this, dexPath, libraryPath, optimizedDirectory);
private static DexFile loadDexFile(File file, File optimizedDirectory)

http://androidxref.com/4.4.2_r1/xref/libcore/dalvik/src/main/java/dalvik/system/DexFile.java

static public DexFile loadDex(String sourcePathName, String outputPathName, int flags)

调用 native 函数 native private static int openDexFileNative(String sourceName, String outputName, int flags)

294    private static int openDexFile(String sourceName, String outputName,
295        int flags) throws IOException {
296        return openDexFileNative( new File(sourceName).getCanonicalPath(),
297                                 (outputName == null) ? null : new File(outputName).getCanonicalPath(),
298                                 flags);
299    }

脱壳机模块改进二

  • 加入encode

  • 优化输出

  • ...

github 地址如下,里面已经有一个编译好但是没有签名的 apk 了...

https://github.com/WooyunDota/DumpDex

如果提取的是 encode 版的,需要 decode 一下:

base64 -D -i com.ali.tg.testapp_606716.dex.encode.dex -o my.dex

0x03参考


http://www.cnblogs.com/goodhacker/p/4014617.html

http://www.cnblogs.com/goodhacker/p/4014617.html

http://www.cnblogs.com/baizx/p/4254359.html

http://www.gitzx.com/android-cydiasubstrate/

从源码中跟踪Dex的加载流程

https://github.com/bunnyblue/DexExtractor

Android逆向之动态调试总结

dex文件的优化解析及装载

Android系统ODEX文件格式解析

DexClassLoader4.4.2动态加载分析(磁盘加载分析)

Android4.0内存Dex数据动态加载技术


本文转载自:http://drops.wooyun.org/tips/8084

henry-zhang
粉丝 2
博文 62
码字总数 1431
作品 0
海淀
私信 提问
加载中

评论(2)

henry-zhang
henry-zhang 博主

引用来自“鸡蛋炒番茄”的评论

dump 出来为啥是odex 0 还不能直接转dex 的0
因为运行时是优化后的dex文件,也就是odex,所以dump出来就是odex了
鸡蛋炒番茄
dump 出来为啥是odex 0 还不能直接转dex 的0
安卓动态调试七种武器之离别钩 – Hooking(下)

0x00 序 随着移动安全越来越火,各种调试工具也都层出不穷,但因为环境和需求的不同,并没有工具是万能的。另外工具是死的,人是活的,如果能搞懂工具的原理再结合上自身的经验,你也可以创造...

阿里聚安全
2016/06/22
677
1
CSDN日报20170731——《程序员内部培训与个人发展杂谈》

程序人生 | 程序员内部培训与个人发展杂谈 作者:hursing 牛叉的技术可以带来优质的体验,好的体验吸引更多用户,用户规模引发的赚钱效应驱动更多资本和人力投入,投入带来经验总结,总结产出...

blogdevteam
2017/07/31
0
0
Android Hook工具Cydia Substrate使用

Hook简介: Hook就是钩子,在安卓中,就是在事件传送到终点前截获并监控事件的传输,像个钩子勾上事件一样,并且能够在勾上事件时,处理一些自己特定的事件。 Cydia Substrate的官网定义:T...

雪狼的开发故事
2015/08/20
2.2K
0
Cydia 之父 Saurik 分享越狱经验

在首届MyGreatFest 2011越狱开发者大会上,越狱高手们如Cydia之父Saurik、Chronic Dev的p0sixninja等互相交流和分享越狱的经验。相信其中最令人期待的莫过于Cydia之父Saurik(Jay Freeman)。...

小卒过河
2011/09/19
1K
1
iOS逆向开发学习之前

为什么要学习逆向开发 为什么要学习逆向开发,这是个问题。如果你上网查一定会搜到这样的解释: 了解整个iOS系统和架构,能够站在更高的维度看问题 学习其他App优秀的设计和实现方法 提高开发...

LvesLi
2017/08/10
0
0

没有更多内容

加载失败,请刷新页面

加载更多

500行代码,教你用python写个微信飞机大战

这几天在重温微信小游戏的飞机大战,玩着玩着就在思考人生了,这飞机大战怎么就可以做的那么好,操作简单,简单上手。 帮助蹲厕族、YP族、饭圈女孩在无聊之余可以有一样东西让他们振作起来!...

上海小胖
22分钟前
3
0
关于AsyncTask的onPostExcute方法是否会在Activity重建过程中调用的问题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/XG1057415595/article/details/86774575 假设下面一种情况...

shzwork
今天
7
0
object 类中有哪些方法?

getClass(): 获取运行时类的对象 equals():判断其他对象是否与此对象相等 hashcode():返回该对象的哈希码值 toString():返回该对象的字符串表示 clone(): 创建并返此对象的一个副本 wait...

happywe
今天
6
0
Docker容器实战(七) - 容器中进程视野下的文件系统

前两文中,讲了Linux容器最基础的两种技术 Namespace 作用是“隔离”,它让应用进程只能看到该Namespace内的“世界” Cgroups 作用是“限制”,它给这个“世界”围上了一圈看不见的墙 这么一...

JavaEdge
今天
8
0
文件访问和共享的方法介绍

在上一篇文章中,你了解到文件有三个不同的权限集。拥有该文件的用户有一个集合,拥有该文件的组的成员有一个集合,然后最终一个集合适用于其他所有人。在长列表(ls -l)中这些权限使用符号...

老孟的Linux私房菜
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部