十三、iOS逆向之《越狱防护》

作者: Hanfank | 来源:发表于2019-02-08 10:22 被阅读5次

    概叙

    越狱防护是指防止别人修改自己的APP作出的防护手段。

    1.了解代码注入方式

    了解防护之前需要了解代码注入的方式,针对性防护。
    代码注入一般有两种方式:

    1. 第一种是通过注入动态库改变machO文件头的Load_Command字段,注入dyld或者framework,让dyld加载我们注入的动态库。
    2. 第二种是通过配置DYLD_INSERT_LIBRARIES的环境变量,在dyld执行过程中插入动态库。

    Theos是一个逆向插件,它可以生成的dylib动态库后通过DYLD_INSERT_LIBRARIES的方式插入到app中。采用了第二种注入方式。
    了解Theos的工作方式后,我们现在对Theos进行防护!

    2 防护

    2.1 了解dyld的插入过程

    在做防护前我们先了解dyld,dyld是app程序调用前的执行代码,他会帮我们启动库、执行machO文件、并执行程序代码入口的程序,是苹果开源的代码。可以百度搜索下载得到!

    打开dyld,全局搜索DYLD_INSERT_LIBRARIES找到插入动态库的函数调用位置。
    在第5909行有一个loadInsertedDyldb(*lib)加载动态库的函数,此函数是调用动态库的函数。

    dyld在调用这个函数的前面做了很多判断!
    但在5693行有一个重要的判断条件是关键:gLinkContext.processIsRestricted

    gLinkContext.processIsRestricted
    这个函数开关是限制动态库的开关,只要符合这个条件就无法插入动态库。
    那在什么时候这个gLinkContext.processIsRestricted才会等于true呢?
    全局搜索一下 processIsRestricted = true
    在第一个结果中可以看到这个判断条件等于true
    processIsRestricted = true
    而他前面还有两个判断条件( issetugid() || hasRestrictedSegment(mainExecutableMH) )
    其中issetugid()是系统调用公用动态库的函数,是私有的,无法知其工作原理。
    hasRestrictedSegment(mainExecutableMH)是可以看得到源码的。
    跳转到hasRestrictedSegment(mainExecutableMH)函数中。
    hasRestrictedSegment(mainExecutableMH)
    可以看到这里面判断了两个东西 :__RESTRICT__restrict
    如果这两个东西存在就返回true。就符合我们的预期。表示是限制的。

    在看看此函数,在函数的入参中有一个macho_header结构体类型,这个地方传进来的其实就是machO文件的头。那么只需要在machO文件的头添加这两个字段就可以起到防护的效果!

    2.2 开始第一次防护

    我们随意新建一个demo。
    然后在Build Settings中搜索other linker flags。
    并输入值-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
    这样就能完成那两个字段,此乃固定写法。

    -Wl,-sectcreate,__RESTRICT,__restrict,/dev/null

    只需要在此处添加这一句话,编译后就会在ipa包的machO文件的头部出现__RESTRICT__restrict判断条件。

    通过MachOView工具就可以看到效果

    MachOView
    对此就完成了通过DYLD_INSERT_LIBRARIES方式注入动态库的防护。

    2.3 攻破防护!

    你以为通过添加两个字段就搞定防护了吗?太幼稚了。
    我只要把这两个字段随便改掉,你这个防护一点用都没有了!
    我们可以通过修改machO的二进制进行修改__RESTRICT__restrict
    下面介绍一款工具:Synalyze It! Pro Mac。
    Synalyze It! Pro是一款Mac上的16进制编辑工具,能够实时编辑任何二进制文件,简单易用,支持多种字符编码、Python和Lua脚本、导出XML和TXT文件等等,可谓反向工程的利器,对于经常需要编辑二进制文件的开发者很有帮助!

    Synalyze It! Pro Mac – 二进制编辑器

    通过此工具打开ipa内的machO文件,通过搜索直接可以定位到修改处!


    __restrict

    我们对其进行随意修改,如下图。


    修改二进制
    修改完成后记得保存!
    然后我们在把修改过对machO文件放回原来ipa内,覆盖掉原来的machO文件。

    覆盖完后还是不能直接运行,需要对其进行重签才能运行在手机中!
    使用Monkey可以很轻松的完成重签过程!推荐使用。

    这样我们又再一次攻破machO的防线!

    2.4 再一次防护!

    既然被攻破,那就再防护一次!
    你修改了__restrict字段,那我监测一下你有没有修改那两个字段行不行?
    答案是肯定的!
    事实上mach-o程序是公开使用的,导入头文件!

    #import <mach-o/dyld.h>
    #import <mach-o/loader.h>
    

    再把dyld的这段代码复制过来!

    //
    // Look for a special segment in the mach header. 
    // Its presences means that the binary wants to have DYLD ignore
    // DYLD_ environment variables.
    //
    #if __MAC_OS_X_VERSION_MIN_REQUIRED
    static bool hasRestrictedSegment(const macho_header* mh)
    {
        const uint32_t cmd_count = mh->ncmds;
        const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
        const struct load_command* cmd = cmds;
        for (uint32_t i = 0; i < cmd_count; ++i) {
            switch (cmd->cmd) {
                case LC_SEGMENT_COMMAND:
                {
                    const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
                    
                    //dyld::log("seg name: %s\n", seg->segname);
                    if (strcmp(seg->segname, "__RESTRICT") == 0) {
                        const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
                        const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
                        for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
                            if (strcmp(sect->sectname, "__restrict") == 0) 
                                return true;
                        }
                    }
                }
                break;
            }
            cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
        }
            
        return false;
    }
    #endif
    

    复制过来后出现了很多警告和错误。大部分是类型的问题,我们把相关的类型定义也复制过来!

    #if __LP64__
        #define LC_SEGMENT_COMMAND      LC_SEGMENT_64
        #define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT
        #define LC_ENCRYPT_COMMAND      LC_ENCRYPTION_INFO
        #define macho_segment_command   segment_command_64
        #define macho_section           section_64
    #else
        #define LC_SEGMENT_COMMAND      LC_SEGMENT
        #define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT_64
        #define LC_ENCRYPT_COMMAND      LC_ENCRYPTION_INFO_64
        #define macho_segment_command   segment_command
        #define macho_section           section
    #endif
    

    这样就少了很多警告!
    其中有一个是入参macho_header的警告
    实际上macho_header是有32位和64位架构的读取方式。

    /*
     * The 32-bit mach header appears at the very beginning of the object file for
     * 32-bit architectures.
     */
    struct mach_header {
        uint32_t    magic;      /* mach magic number identifier */
        cpu_type_t  cputype;    /* cpu specifier */
        cpu_subtype_t   cpusubtype; /* machine specifier */
        uint32_t    filetype;   /* type of file */
        uint32_t    ncmds;      /* number of load commands */
        uint32_t    sizeofcmds; /* the size of all the load commands */
        uint32_t    flags;      /* flags */
    };
    
    /*
     * The 64-bit mach header appears at the very beginning of object files for
     * 64-bit architectures.
     */
    struct mach_header_64 {
        uint32_t    magic;      /* mach magic number identifier */
        cpu_type_t  cputype;    /* cpu specifier */
        cpu_subtype_t   cpusubtype; /* machine specifier */
        uint32_t    filetype;   /* type of file */
        uint32_t    ncmds;      /* number of load commands */
        uint32_t    sizeofcmds; /* the size of all the load commands */
        uint32_t    flags;      /* flags */
        uint32_t    reserved;   /* reserved */
    };
    

    这是两种结构体。
    我们需要区分32架构和64架构使用的结构体。在下面的构架方式定义中添加两条宏定义。

    #if __LP64__
        #define macho_header              mach_header_64   //64为架构中添加此条
        #define LC_SEGMENT_COMMAND      LC_SEGMENT_64
        #define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT
        #define LC_ENCRYPT_COMMAND      LC_ENCRYPTION_INFO
        #define macho_segment_command   segment_command_64
        #define macho_section           section_64
    #else
        #define macho_header              mach_header //32为架构中添加此条
        #define LC_SEGMENT_COMMAND      LC_SEGMENT
        #define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT_64
        #define LC_ENCRYPT_COMMAND      LC_ENCRYPTION_INFO_64
        #define macho_segment_command   segment_command
        #define macho_section           section
    #endif
    

    添加完成!
    重新编译一下,编译通过!
    接下来调用这个函数!
    我们在load方法中调用,这个方法是在main后最先调用的方法了!
    判断如果是有字段存在,表明未被修改,否则被修改!

    + (void)load{
        //获取镜像image的头。
        //image是一个列表,列表中第一个就是自己的镜像
        //还记得通过lldb的`image list`指令获取的镜像列表吗?就是它!
        const struct macho_header* header = _dyld_get_image_header(0);
        if (hasRestrictedSegment(header)) {
            NSLog(@"安全!");
        }else{
            NSLog(@"被修改了__RESTRICT或__restrict");
            exit(0);
        }
    }
    

    到这里我们就再一次做了防护!

    这里有一个问题!
    发现被修改后执行exit(0)真的好吗?
    不一定~因为一旦执行退出程序操作,就相当于告诉黑客我执行了什么代码。黑客就可以通过这个提示去找到对应的方法。降低了防御能力!
    微信是如何做的呢?微信被注入后是没有任何提示!它在你不知不觉中就上报了一段序号,比如89757,我们根本不知道这是什么!一旦被发现,第二天可能就被封号。这种情况下,你很难找到它在什么时候做了防护!

    第二种防护方式!《白名单库》

    第二种方式是把正常使用的镜像库列表作为字符串保存,然后在dyld加载动态库时判断是否一致实现的。
    如果dyld在执行过程中插入了任何的动态库,都会判断为被修改了!
    怎么做呢?

    
    @implementation ViewController
    const char * libPaths = "/var/containers/Bundle/Application/3C9EDF66-A49D-4CF2-BDC8-03EA2E26E3E6/demo.app/demo
    /Developer/usr/lib/libBacktraceRecording.dylib
    /Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
    /System/Library/Frameworks/Foundation.framework/Foundation/usr/lib/libobjc.A.dylib/usr/lib/libSystem.B.dylib
    /System/Library/Frameworks/UIKit.framework
    /UIKit/usr/lib/libarchive.2.dylib/usr/lib/libicucore.A.dylib
    /usr/lib/libxml2.2.dylib/usr/lib/libz.1.dylib
    ..."
    
    + (void)load{
        int count = _dyld_image_count();
        for (int i = 0; i<count; i++) {
            // 在打包前先把库链接打印粗来,复制保存到libPaths,用于对比
            const char * libPath = _dyld_get_image_name(i);
            // 通过这个方法判断libs是否包含libPath,如果包含会返回libPath,否则返回空。
            // 后面这个判断是判断自己,因为自己所在的是一个沙盒路径,路径不是固定的,所以要排除它
            if (!strstr(libPaths, libPath)&&
                !strstr(libPath, "/var/mobile/Containers/Bundle/Application")){
                printf("该库在白名单内,安全!");
            }
            else{
                printf("被修改了!");
            }
        }
    }
    

    这样,防护又做了一个,安全又提高了一点点。

    但是这样做其实还是可以被修改,因为libPaths是全局的字符串,还是可以拿到修改的。起到防护效果有限!

    结语

    防护的方式还有很多很多,这里只是一些启发性的知识点,更多防护还需要更多学习。

    相关文章

      网友评论

        本文标题:十三、iOS逆向之《越狱防护》

        本文链接:https://www.haomeiwen.com/subject/slmwsqtx.html