iOS 攻防(一)DYLD_INSERT_LIBRARIES

上篇文章中已经清楚了Tweak是通过DYLD_INSERT_LIBRARIES来插入动态库的,那么它是怎么做到的呢?这就需要去dyld源码中探究了。

一、 DYLD_INSERT_LIBRARIES原理

由于dyld源码中b不同版本有变动,需要分别看下新老版本的实现。dyld源码

1.1 dyld-519.2.2 源码

打开dyld源码工程,搜索DYLD_INSERT_LIBRARIES关键字,在dyld.cpp5906行有如下代码:

// load any inserted libraries
if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
    for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
        loadInsertedDylib(*lib);
}

这段代码是判断DYLD_INSERT_LIBRARIES不为空就循环加载插入动态库。

继续查找在5692行:

if ( gLinkContext.processIsRestricted ) {
    pruneEnvironmentVariables(envp, &apple);
    // set again because envp and apple may have changed or moved
    setContext(mainExecutableMH, argc, argv, envp, apple);
}

这里判断进程如果受限制(processIsRestricted不为空)执行pruneEnvironmentVariablespruneEnvironmentVariables会移除DYLD_INSERT_LIBRARIES中的数据,相当于被清空了。这样插入的动态库就不会被加载了。

既然越狱插件是通过DYLD_INSERT_LIBRARIES插入的,那么只要让自己的进程受限就能起到保护作用了。

搜索processIsRestricted = true是在4696行设置值的:

// any processes with setuid or setgid bit set or with __RESTRICT segment is restricted
if ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) {
    gLinkContext.processIsRestricted = true;
}

issetugid不能在上架的App中设置,那么就只能设置hasRestrictedSegment了,这里传入的参数是主程序:

static bool hasRestrictedSegment(const macho_header* mh)
{
    //load command 数量
    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);
                //读取__RESTRICT SEGMENT
                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) {
                        //读取__restrict SECTION
                        if (strcmp(sect->sectname, "__restrict") == 0) 
                            return true;
                    }
                }
            }
            break;
        }
        cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    }
        
    return false;
}

这段代码的意思是判断load commands中有没有__RESTRICT SECTIONSECTION中有没有__restrict SEGMENT

image.png

也就是说只要有这个SECTION就会开启进程受限了。

1.2 dyld-851.27源码

dyld2.cpp7120行中仍然有DYLD_INSERT_LIBRARIES的判断:

// load any inserted libraries
if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
    for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
        loadInsertedDylib(*lib);
}

processIsRestricted变成了一个函数(5391):

bool processIsRestricted()
{
#if TARGET_OS_OSX
    return !gLinkContext.allowEnvVarsPath;
#else
    return false;
#endif
}

这里可以看到只在OSX下才有效。

6667行也只有OSX下才有可能清空环境变量:

#if TARGET_OS_OSX
    if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
        pruneEnvironmentVariables(envp, &apple);
        // set again because envp and apple may have changed or moved
        setContext(mainExecutableMH, argc, argv, envp, apple);
    }
    else
#endif
    {
        checkEnvironmentVariables(envp);
        defaultUninitializedFallbackPaths(envp);
    }

hasRestrictedSegment也变成了OSX下专属:

#if TARGET_OS_OSX
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

结论:iOS 10以前dyld会判断主程序是否有__RESTRICT,__restrict来决定是否加载DYLD_INSERT_LIBRARIESiOS 10及以后并不会进行判断直接进行了加载。

二、 DYLD_INSERT_LIBRARIES 攻防

2.1 iOS10以前攻防

2.1.1 RESTRIC段防护

Other Linker Flags中输入-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null

RESTRICT设置

sectcreate:意思是创建一个SEGEMNT__RESTRICT,__restrict,值为/dev/null

这么配置后在MachO文件中就有对应的SECTION了:

image.png

这样通过DYLD_INSERT_LIBRARIES注入的库就无效了。越狱手机上的插件就无效了。(仅在iOS 10以下有效)。

2.1.2 修改二进制破解

针对RESTRIC的防护可以用二进制修改器将段名称修改掉,就可以绕过检测了。
修改Data中的任意一位这个值就变了:

image.png

image.png

image.png

修改后重签就可以了。

2.1.3 防止RESTRICT被修改

针对RESTRICT被修改可以在代码中判断MachO中是否有对应的RESTRIC,如果没有就证明被修改了。参考dyld源码修改判断如下:

#import <mach-o/dyld.h>

#if __LP64__
    #define macho_header              mach_header_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
    #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

static bool hp_hasRestrictedSegment(const struct macho_header* mh) {
    const uint32_t cmd_count = mh->ncmds;
    const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(struct 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;
                printf("seg->segname: %s\n",seg->segname);
                //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) {
                        printf("sect->sectname: %s\n",sect->sectname);
                        if (strcmp(sect->sectname, "__restrict") == 0)
                            return true;
                    }
                }
            }
            break;
        }
        cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    }
    return false;
}

调用:

+ (void)load {
    //获取主程序 macho_header
    const struct macho_header *header = _dyld_get_image_header(0);
    if (hp_hasRestrictedSegment(header)) {
        NSLog(@"没有修改");
    } else {
        NSLog(@"被修改了");
    }
}

这样就能知道RESTRICT有没有被修改。要Hook检测逻辑就需要找到hp_hasRestrictedSegment函数的地址进行inline hook。或者找到调用hp_hasRestrictedSegment的地方,那么在检测过程中就不能有明显的特征。一般将结果告诉服务端。或者做一些破坏功能的逻辑,比如网络请求相关的内容。

2.2 iOS10及以后攻防

2.2.1 使用DYLD源码防护(黑白名单)

既然iOS10以上系统不进行判断检测了,那么我们可以自己扫描判断哪些应该被加载哪些不能被加载。

#import <mach-o/dyld.h>

const char *whiteListLibStrs =
"/usr/lib/substitute-inserter.dylib/System/Library/Frameworks/Foundation.framework/Foundation/usr/lib/libobjc.A.dylib/usr/lib/libSystem.B.dylib/System/Library/Frameworks/UIKit.framework/UIKit/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation/System/Library/PrivateFrameworks/CoreAutoLayout.framework/CoreAutoLayout/usr/lib/libcompression.dylib/System/Library/Frameworks/CFNetwork.framework/CFNetwork/usr/lib/libarchive.2.dylib/usr/lib/libicucore.A.dylib/usr/lib/libxml2.2.dylib/usr/lib/liblangid.dylib/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit/usr/lib/libCRFSuite.dylib/System/Library/PrivateFrameworks/SoftLinking.framework/SoftLinking/usr/lib/libc++abi.dylib/usr/lib/libc++.1.dylib/usr/lib/system/libcache.dylib/usr/lib/system/libcommonCrypto.dylib/usr/lib/system/libcompiler_rt.dylib/usr/lib/system/libcopyfile.dylib/usr/lib/system/libcorecrypto.dylib";

const char *blackListLibStrs =
"/usr/lib/libsubstitute.dylib/usr/lib/substitute-loader.dylib/usr/lib/libsubstrate.dylib/Library/MobileSubstrate/DynamicLibraries/RHRevealLoader";

void imageListCheck() {
    //进程依赖的库数量
    int count = _dyld_image_count();
    //第一个为自己。过滤掉,因为每次执行的沙盒路径不一样。
    for (int i = 1; i < count; i++) {
        const char *image_name =  _dyld_get_image_name(i);
//        printf("%s",image_name);
        //黑名单检测
        if (strstr(blackListLibStrs, image_name)) {//不在白名单
            printf("image_name in black list: %s\n",image_name);
            break;
        }
        //白名单检测
        if (!strstr(whiteListLibStrs, image_name)) {
            printf("image_name not in white list: %s\n",image_name);
        }
    }
}

调用:

+ (void)load {
    imageListCheck();
}
  • 白名单可以直接通过_dyld_get_image_name获取,这里和系统版本有关。需要跑支持的系统版本获取得到并集。维护起来比较麻烦。
  • 黑名单中可以将一些越狱库和检测到的异常库放入其中。
  • 一般检测到问题直接上报服务端。不要直接表现出异常。

黑白名单一般都通过服务端下发,黑名单直接检测出问题上报服务端处理,白名单维护用来检测上报未知的库供分析更新黑白名单。

这种防护方式可以通过fishhook Hook _dyld_image_count_dyld_get_image_name来做排查是哪块做的检测从而去绕过。

  • 对于检测代码最好混淆函数名称。
  • 返回值不要返回一个布尔值,函数被hook之后或者被修改成返回YES 之后很多判断代码都没用了。最好返回特定字符串加密这种。
  • 检测到被注入时不要exit(0)完事,太明显了,这种很容易被绕过。攻防的核心不在于防护技术,而在于会不会被对方发现。微信的做法就是上报服务端封号处理。
  • 在检测到时可以悄悄对业务逻辑做一些处理,比如网络请求正常返回但是页面显示异常或者功能不全等。

没有绝对安全的代码,只不过在与会不会被对方发现以及破解的代价。如果破解代价大于收益很少有人去破解的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容