在测试一个插件的时候,发现在iOS14上启动就会触发崩溃。跟了一下,发现如下报错
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSURL initFileURLWithPath:isDirectory:]: nil string parameter'
崩溃的堆栈:
frame #9: 0x0000000197c5ec7c libobjc.A.dylib`objc_exception_throw + 352
frame #10: 0x000000018545cfd0 Foundation`-[NSURL(NSURL) initFileURLWithPath:isDirectory:] + 604
frame #11: 0x000000018545cd68 Foundation`+[NSURL(NSURL) fileURLWithPath:isDirectory:] + 44
frame #12: 0x000000018547c5e0 Foundation`-[NSFileManager _URLForReplacingItemAtURL:error:] + 144
frame #13: 0x000000018556717c Foundation`_NSCreateTemporaryFile_Protected + 184
frame #14: 0x00000001855677f8 Foundation`_NSWriteDataToFileWithExtendedAttributes + 196
frame #15: 0x000000018549d6f4 Foundation`-[NSDictionary(NSDictionary) writeToURL:error:] + 200
写入一个字典到文件的时候,会先写入一个临时文件,然后rename到目前路径,这个崩溃的原因好像是在写入临时路径的时候获取的路径为空导致-[NSURL(NSURL) initFileURLWithPath:isDirectory:]函数触发异常。 在网上搜了一下,发现twitter上也有人提到了这个问题 https://twitter.com/opa334dev/status/1375597191599902721 进过一番分析,我发现是通过NSTemporaryDirectory获取的临时目录,代码如下
NSString *NSTemporaryDirectory(void)
{
size_t v0; // x0
id v1; // x0
id v2; // x0
id v4; // x19
size_t v5; // x0
id v6; // x0
__int64 v7; // x0
char __s[1027]; // [xsp+5h] [xbp-41Bh] BYREF
if ( !confstr(65537, __s, 0x402uLL) )
{
if ( issetugid() || (v7 = _NSGetEnvironmentVariable("TMPDIR")) == 0 )
{
__strlcpy_chk(__s, "/tmp/", 1026LL, 1027LL);
v4 = objc_msgSend(classRef_NSFileManager, (SEL)selRef_defaultManager);
v5 = strlen(__s);
v6 = objc_msgSend(v4, (SEL)selRef_stringWithFileSystemRepresentation_length_, __s, v5);
return (NSString *)objc_msgSend(v6, (SEL)selRef_stringByStandardizingPath);
}
__strlcpy_chk(__s, v7, 1026LL, 1027LL);
}
v0 = strlen(__s);
if ( !v0 || __s[v0 - 1] != 47 )
*(_WORD *)&__s[v0] = 47;
v1 = objc_alloc((Class)classRef_NSString);
v2 = objc_msgSend(v1, (SEL)selRef_initWithUTF8String_, __s);
return (NSString *)objc_autorelease(v2);
}
而confstr函数内部返回到__s有问题,进一步看下confstr函数,这里的65537对应了
#define _CS_DARWIN_USER_TEMP_DIR 65537
内部代码如下
size_t __cdecl confstr(int a1, char *a2, size_t a3)
{
// ...
v5 = v17;
if ( __dirhelper_func && __dirhelper_func(1LL, v17, 1024LL) ){
if ( a2 && a3 )
_platform_strlcpy(a2, v5, a3);
return _platform_strlen(v5) + 1;
}
//...
}
这里的__dirhelper_func函数返回了成功就会直接拷贝字符串到目标地址,__dirhelper_func这个函数是在以下路径时候初始化 __libc_init _libc_initializer __confstr_init
__int64 __fastcall __confstr_init(__int64 result)
{
__dirhelper_func = *(__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD))(result + 32);
return result;
}
传入的地址实际是libsystem_coreservices.dylib里面的_dirhelper函数,代码如下
char *__fastcall _dirhelper(int a1, char *__dst, size_t __size)
{
_QWORD *v5; // x21
const char *v6; // x1
const char *v7; // x1
int v8; // w20
int *v9; // x8
char *result; // x0
int *v11; // x8
char *v12; // x0
stat v13; // [xsp+0h] [xbp-B0h] BYREF
if ( a1 != 1 )
goto LABEL_17;
if ( _os_alloc_once_table[38] == -1LL )
v5 = (_QWORD *)_os_alloc_once_table[39];
else
v5 = (_QWORD *)_os_alloc_once(&_os_alloc_once_table[38], 120LL, 0LL);
if ( v5[2] != -1LL )
_os_once(v5 + 2, v5, _dirhelper_init);
v6 = (const char *)v5[13];
if ( !v6 || !*v6 || strlcpy(__dst, v6, __size) >= __size )
goto LABEL_17;
v7 = (const char *)v5[14];
if ( !v7 || !*v7 )
return __dst;
if ( strlcat(__dst, v7, __size) >= __size )
{
LABEL_17:
v11 = __error();
result = 0LL;
*v11 = 22;
return result;
}
if ( !mkdir(__dst, 0x1C0u) )
{
if ( !lstat(__dst, &v13) && (v13.st_mode & 0x1FF) != 448 && (v13.st_flags & 0x100000) == 0 )
chmod(__dst, 0x1C0u);
LABEL_23:
if ( !__dst )
return 0LL;
goto LABEL_24;
}
if ( *__error() == 17 )
goto LABEL_23;
v8 = *__error();
*__error() = v8;
v9 = __error();
result = 0LL;
if ( __dst && *v9 == 17 )
{
LABEL_24:
v12 = getenv("TMPDIR");
if ( !v12 || strcmp(v12, __dst) )
setenv("TMPDIR", __dst, 1);
return __dst;
}
return result;
}
最终这个函数返回的路径有问题。调试发现,这个函数居然被substitute-loader hook了
libsystem_coreservices.dylib`:
-> 0x1ca85e218 <+0>: adrp x17, -820516
0x1ca85e21c <+4>: add x17, x17, #0xb64
0x1ca85e220 <+8>: br x17
0x1ca85e224 <+12>: stp x29, x30, [sp, #0xb0]
0x1ca85e228 <+16>: add x29, sp, #0xb0
0x1ca85e22c <+20>: cmp w0, #0x1
0x1ca85e230 <+24>: b.ne 0x1ca85e2fc ; <+228>
x17=0x000000010233ab64模块在/usr/lib/substitute-loader.dylib之中 但substitute-loader.dylib内部的代码是被混淆的,难以分析出具体的行为,但通过调试可以发现,他并没有调用回原_dirhelper的代码。 所以目前的猜测可能是,_dirhelper原本是用了_os_alloc_once_table储存的数据,以及在_dirhelper_init初始化的时候考虑了多线程问题,
char *__fastcall _dirhelper_init(__int64 a1)
{
void *v2; // x0
char *result; // x0
char *v4; // x0
const char *v5; // x20
size_t v6; // x21
char *v7; // x22
int v8; // w0
int v9; // w8
v2 = *(void **)(a1 + 24);
if ( v2 )
bzero(v2, 0x400uLL);
else
*(_QWORD *)(a1 + 24) = calloc(0x400uLL, 1uLL);
result = (char *)pthread_mutex_init((pthread_mutex_t *)(a1 + 32), 0LL);
*(_DWORD *)(a1 + 96) = 0;
if ( !*(_QWORD *)(a1 + 104) )
{
v4 = getenv("TMPDIR");
if ( v4 )
{
v5 = v4;
if ( *v4 )
{
v6 = strlen(v4) + 2;
v7 = (char *)calloc(v6, 1uLL);
*(_QWORD *)(a1 + 104) = v7;
if ( v5[strlen(v5) - 1] == 47 )
v8 = snprintf(v7, v6, "%s");
else
v8 = snprintf(v7, v6, "%s/");
v9 = v8;
result = *(char **)(a1 + 104);
if ( v6 <= v9 )
{
free(result);
*(_QWORD *)(a1 + 104) = 0LL;
}
else if ( result )
{
return result;
}
}
}
result = strdup("/var/tmp/");
*(_QWORD *)(a1 + 104) = result;
}
return result;
}
这个崩溃触发的场景仍然不清楚原因,但substitute-loader.dylib应该是跑不了,暂时也不清楚substitute-loader.dylib hook _dirhelper函数的原因。