十三、iOS逆向之《越獄防護》

概敘

越獄防護是指防止別人修改自己的APP作出的防護手段。

1.了解代碼注入方式

了解防護之前需要了解代碼注入的方式,針對性防護。
代碼注入一般有兩種方式:

  1. 第一種是通過注入動態(tài)庫改變machO文件頭的Load_Command字段,注入dyld或者framework,讓dyld加載我們注入的動態(tài)庫。
  2. 第二種是通過配置DYLD_INSERT_LIBRARIES的環(huán)境變量,在dyld執(zhí)行過程中插入動態(tài)庫。

Theos是一個逆向插件,它可以生成的dylib動態(tài)庫后通過DYLD_INSERT_LIBRARIES的方式插入到app中。采用了第二種注入方式。
了解Theos的工作方式后,我們現(xiàn)在對Theos進行防護!

2 防護

2.1 了解dyld的插入過程

在做防護前我們先了解dyld,dyld是app程序調(diào)用前的執(zhí)行代碼,他會幫我們啟動庫、執(zhí)行machO文件、并執(zhí)行程序代碼入口的程序,是蘋果開源的代碼。可以百度搜索下載得到!

打開dyld,全局搜索DYLD_INSERT_LIBRARIES找到插入動態(tài)庫的函數(shù)調(diào)用位置。
在第5909行有一個loadInsertedDyldb(*lib)加載動態(tài)庫的函數(shù),此函數(shù)是調(diào)用動態(tài)庫的函數(shù)。

dyld在調(diào)用這個函數(shù)的前面做了很多判斷!
但在5693行有一個重要的判斷條件是關(guān)鍵:gLinkContext.processIsRestricted

gLinkContext.processIsRestricted

這個函數(shù)開關(guān)是限制動態(tài)庫的開關(guān),只要符合這個條件就無法插入動態(tài)庫。
那在什么時候這個gLinkContext.processIsRestricted才會等于true呢?
全局搜索一下 processIsRestricted = true
在第一個結(jié)果中可以看到這個判斷條件等于true
processIsRestricted = true

而他前面還有兩個判斷條件( issetugid() || hasRestrictedSegment(mainExecutableMH) )
其中issetugid()是系統(tǒng)調(diào)用公用動態(tài)庫的函數(shù),是私有的,無法知其工作原理。
hasRestrictedSegment(mainExecutableMH)是可以看得到源碼的。
跳轉(zhuǎn)到hasRestrictedSegment(mainExecutableMH)函數(shù)中。
hasRestrictedSegment(mainExecutableMH)

可以看到這里面判斷了兩個東西 :__RESTRICT__restrict。
如果這兩個東西存在就返回true。就符合我們的預期。表示是限制的。

在看看此函數(shù),在函數(shù)的入?yún)⒅杏幸粋€macho_header結(jié)構(gòu)體類型,這個地方傳進來的其實就是machO文件的頭。那么只需要在machO文件的頭添加這兩個字段就可以起到防護的效果!

2.2 開始第一次防護

我們隨意新建一個demo。
然后在Build Settings中搜索other linker flags。
并輸入值-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
這樣就能完成那兩個字段,此乃固定寫法。

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

只需要在此處添加這一句話,編譯后就會在ipa包的machO文件的頭部出現(xiàn)__RESTRICT__restrict判斷條件。

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

MachOView

對此就完成了通過DYLD_INSERT_LIBRARIES方式注入動態(tài)庫的防護。

2.3 攻破防護!

你以為通過添加兩個字段就搞定防護了嗎?太幼稚了。
我只要把這兩個字段隨便改掉,你這個防護一點用都沒有了!
我們可以通過修改machO的二進制進行修改__RESTRICT__restrict。
下面介紹一款工具:Synalyze It! Pro Mac。
Synalyze It! Pro是一款Mac上的16進制編輯工具,能夠?qū)崟r編輯任何二進制文件,簡單易用,支持多種字符編碼、Python和Lua腳本、導出XML和TXT文件等等,可謂反向工程的利器,對于經(jīng)常需要編輯二進制文件的開發(fā)者很有幫助!

Synalyze It! Pro Mac – 二進制編輯器

通過此工具打開ipa內(nèi)的machO文件,通過搜索直接可以定位到修改處!


__restrict

我們對其進行隨意修改,如下圖。


修改二進制

修改完成后記得保存!
然后我們在把修改過對machO文件放回原來ipa內(nèi),覆蓋掉原來的machO文件。

覆蓋完后還是不能直接運行,需要對其進行重簽才能運行在手機中!
使用Monkey可以很輕松的完成重簽過程!推薦使用。

這樣我們又再一次攻破machO的防線!

2.4 再一次防護!

既然被攻破,那就再防護一次!
你修改了__restrict字段,那我監(jiān)測一下你有沒有修改那兩個字段行不行?
答案是肯定的!
事實上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

復制過來后出現(xiàn)了很多警告和錯誤。大部分是類型的問題,我們把相關(guān)的類型定義也復制過來!

#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

這樣就少了很多警告!
其中有一個是入?yún)?code>macho_header的警告
實際上macho_header是有32位和64位架構(gòu)的讀取方式。

/*
 * 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 */
};

這是兩種結(jié)構(gòu)體。
我們需要區(qū)分32架構(gòu)和64架構(gòu)使用的結(jié)構(gòu)體。在下面的構(gòu)架方式定義中添加兩條宏定義。

#if __LP64__
    #define macho_header              mach_header_64   //64為架構(gòu)中添加此條
    #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為架構(gòu)中添加此條
    #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

添加完成!
重新編譯一下,編譯通過!
接下來調(diào)用這個函數(shù)!
我們在load方法中調(diào)用,這個方法是在main后最先調(diào)用的方法了!
判斷如果是有字段存在,表明未被修改,否則被修改!

+ (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);
    }
}

到這里我們就再一次做了防護!

這里有一個問題!
發(fā)現(xiàn)被修改后執(zhí)行exit(0)真的好嗎?
不一定~因為一旦執(zhí)行退出程序操作,就相當于告訴黑客我執(zhí)行了什么代碼。黑客就可以通過這個提示去找到對應(yīng)的方法。降低了防御能力!
微信是如何做的呢?微信被注入后是沒有任何提示!它在你不知不覺中就上報了一段序號,比如89757,我們根本不知道這是什么!一旦被發(fā)現(xiàn),第二天可能就被封號。這種情況下,你很難找到它在什么時候做了防護!

第二種防護方式!《白名單庫》

第二種方式是把正常使用的鏡像庫列表作為字符串保存,然后在dyld加載動態(tài)庫時判斷是否一致實現(xiàn)的。
如果dyld在執(zhí)行過程中插入了任何的動態(tài)庫,都會判斷為被修改了!
怎么做呢?


@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);
        // 通過這個方法判斷l(xiāng)ibs是否包含libPath,如果包含會返回libPath,否則返回空。
        // 后面這個判斷是判斷自己,因為自己所在的是一個沙盒路徑,路徑不是固定的,所以要排除它
        if (!strstr(libPaths, libPath)&&
            !strstr(libPath, "/var/mobile/Containers/Bundle/Application")){
            printf("被修改了!");
        }
        else{
            printf("該庫在白名單內(nèi),安全!");
        }
    }
}

這樣,防護又做了一個,安全又提高了一點點。

但是這樣做其實還是可以被修改,因為libPaths是全局的字符串,還是可以拿到修改的。起到防護效果有限!

結(jié)語

防護的方式還有很多很多,這里只是一些啟發(fā)性的知識點,更多防護還需要更多學習。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容