Skip to content

Instantly share code, notes, and snippets.

@4ch12dy
Created May 6, 2023 02:55
Show Gist options
  • Save 4ch12dy/f66f7d0a0413920d5bca9de6a6b7a727 to your computer and use it in GitHub Desktop.
Save 4ch12dy/f66f7d0a0413920d5bca9de6a6b7a727 to your computer and use it in GitHub Desktop.
NSTemporaryDirectory return nil?

在测试一个插件的时候,发现在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函数的原因。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment