上篇文章中已经清楚了Tweak
是通过DYLD_INSERT_LIBRARIES
来插入动态库的,那么它是怎么做到的呢?这就需要去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
不为空就循环加载插入动态库。
继续查找在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
不为空)执行pruneEnvironmentVariables
。pruneEnvironmentVariables
会移除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 = §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
下才有可能清空环境变量:
#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
:意思是创建一个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
#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
以上系统不进行判断检测了,那么我们可以自己扫描判断哪些应该被加载哪些不能被加载。
#import
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)
完事,太明显了,这种很容易被绕过。攻防的核心不在于防护技术,而在于会不会被对方发现。微信的做法就是上报服务端封号处理。 - 在检测到时可以悄悄对业务逻辑做一些处理,比如网络请求正常返回但是页面显示异常或者功能不全等。
没有绝对安全的代码,只不过在与会不会被对方发现以及破解的代价。如果破解代价大于收益很少有人去破解的。