上篇文章中已經清楚了Tweak
是通過DYLD_INSERT_LIBRARIES
來插入動態(tài)庫的稽亏,那么它是怎么做到的呢到旦?這就需要去dyld
源碼中探究了匆赃。
一北戏、 DYLD_INSERT_LIBRARIES原理
由于dyld
源碼中b不同版本有變動师郑,需要分別看下新老版本的實現环葵。dyld源碼
1.1 dyld-519.2.2 源碼
打開dyld
源碼工程,搜索DYLD_INSERT_LIBRARIES
關鍵字宝冕,在dyld.cpp
的5906
行有如下代碼:
// 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
不為空就循環(huán)加載插入動態(tài)庫张遭。
繼續(xù)查找在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
不為空)執(zhí)行pruneEnvironmentVariables
。pruneEnvironmentVariables
會移除DYLD_INSERT_LIBRARIES
中的數據地梨,相當于被清空了帝璧。這樣插入的動態(tài)庫就不會被加載了。
既然越獄插件是通過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 = §ionsStart[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
SECTION
渴庆,SECTION
中有沒有__restrict
SEGMENT
。
也就是說只要有這個
SECTION
就會開啟進程受限了雅镊。
1.2 dyld-851.27源碼
在dyld2.cpp
的7120
行中仍然有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
下才有可能清空環(huán)境變量:
#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 = §ionsStart[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_LIBRARIES
。iOS 10
及以后并不會進行判斷直接進行了加載仁烹。
二耸弄、 DYLD_INSERT_LIBRARIES 攻防
2.1 iOS10以前攻防
2.1.1 RESTRIC段防護
在Other Linker Flags
中輸入-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null
:
sectcreate
:意思是創(chuàng)建一個SEGEMNT
為__RESTRICT,__restrict
,值為/dev/null
。
這么配置后在MachO
文件中就有對應的SECTION
了:
這樣通過DYLD_INSERT_LIBRARIES
注入的庫就無效了卓缰。越獄手機上的插件就無效了计呈。(僅在iOS 10
以下有效)。
2.1.2 修改二進制破解
針對RESTRIC
的防護可以用二進制修改器將段名稱修改掉征唬,就可以繞過檢測了捌显。
修改Data
中的任意一位這個值就變了:
修改后重簽就可以了。
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 = §ionsStart[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
以上系統(tǒng)不進行判斷檢測了囱怕,那么我們可以自己掃描判斷哪些應該被加載哪些不能被加載。
#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();
//第一個為自己毫别。過濾掉娃弓,因為每次執(zhí)行的沙盒路徑不一樣。
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
獲取岛宦,這里和系統(tǒng)版本有關台丛。需要跑支持的系統(tǒng)版本獲取得到并集。維護起來比較麻煩砾肺。 - 黑名單中可以將一些越獄庫和檢測到的異常庫放入其中挽霉。
- 一般檢測到問題直接上報服務端。不要直接表現出異常变汪。
黑白名單一般都通過服務端下發(fā)侠坎,黑名單直接檢測出問題上報服務端處理,白名單維護用來檢測上報未知的庫供分析更新黑白名單裙盾。
這種防護方式可以通過
fishhook
Hook
_dyld_image_count
和_dyld_get_image_name
來做排查是哪塊做的檢測從而去繞過实胸。
- 對于檢測代碼最好混淆函數名稱。
- 返回值不要返回一個布爾值番官,函數被
hook
之后或者被修改成返回YES
之后很多判斷代碼都沒用了庐完。最好返回特定字符串加密這種。 - 檢測到被注入時不要
exit(0)
完事徘熔,太明顯了门躯,這種很容易被繞過。攻防的核心不在于防護技術酷师,而在于會不會被對方發(fā)現讶凉。微信的做法就是上報服務端封號處理。 - 在檢測到時可以悄悄對業(yè)務邏輯做一些處理窒升,比如網絡請求正常返回但是頁面顯示異匙罕椋或者功能不全等。
沒有絕對安全的代碼饱须,只不過在與會不會被對方發(fā)現以及破解的代價。如果破解代價大于收益很少有人去破解的台谊。