Article
Operation Triangulation
Operation triangulation - Keychain module analysis
7 févr. 2024
Introduction
Operation Triangulation is the name of an attack that has been targeting Kaspersky employees among others. Kaspersky has published a lot of really interesting blogposts detailing the exploit chain and how they caught all the samples.
They also gave a conference detailing the exploit chain used for operation triangulation :
Several modules have been caught by Kaspersky. Many of them have been shared on https://vx-underground.org/ . Even if Kaspersky already published some details about these modules, we wanted to also analyze them to get more knowledge about the internals of a sophisticated spyware implant. In this blogpost we will focus on the keychain module 64f36b0b8ef62634a3ec15b4a21700d32b3d950a846daef5661b8bbca01789dc
.
As this module contains its symbols, we thought it would be the easiest and quickest to analyze, thus we chose to start with it. This blog post is the first one from a series where we will try to go in depth of each available samples.
Before diving in the behavior of the keychain module, let’s quickly remember what is the iOS keychain.
KeyChain
According to Apple’s documentation, “In iOS, apps have access to a single keychain (which logically encompasses the iCloud keychain). This keychain is automatically unlocked when the user unlocks the device and then locked when the device is locked. An app can access only its own keychain items, or those shared with a group to which the app belongs.”. The keychain will contain all the passwords (and others secrets) that are stored on the device.
If an application wants to store/search/delete entries in the keychain, it can uses the SecItem*
APIs. Everything is stored in a database on the device named keychain-2.db
.
We will only summarise what is necessary to understand the keychain module of operation triangulation, but more details about the keychain can be found here : https://shindan.io/kb/ios/keychain/keychain_items_storage/.
Keychain item storing 101
An item is the combination of some metadata (account name, website, notes, etc…) and secret (password, secret key). These items are stored in different tables, depending on their classes. The possible classes are :
kSecClassGenericPassword
for a generic password. stored ingenp
table.kSecClassInternetPassword
for an internet password. stored ininet
table.kSecClassCertificate
for a certificate. stored incert
table.kSecClassKey
for a cryptographic key. stored inkeys
table.kSecClassIdentity
for an identity (certificate paired with its associated private key). stored inidnt
.
Let’s summarise what is happening when an item is added into the keychain :
securityd
receives an item to store.The metadata are encrypted.
The metadata key is wrapped.
The secret is encrypted.
The secret key is wrapped.
The metadata and secret items are serialised.
The blob is written into the
data
field.

The important thing to note here are that there are two different encryption keys, one for the metadata, and one for the secret, and the items are stored as serialised object in the database.
Key wrapping
Key wrapping is a technique used to protect cryptographic keys during storage or transmission by encrypting them with another key, known as a wrapping key. This wrapping key is inaccessible from userland because it is managed by the Secure Enclave. The userland must communicate with the AppleKeyStore
kernel extension to wrap or unwrap a key from userland. Metadata and secret keys are wrapped by the keychain to protect them.
Key classes
Another thing to note is that, in the keychain, keys are classified and protected depending on their classification. The different key classes protection are :
WhenUnlocked
AfterFirstUnlock
Always
WhenUnlockedThisDeviceOnly
AfterFirstUnlockThisDeviceOnly
AlwaysThisDeviceOnly
If the device tries, for example, to unwrap a key having a key class WhenUnlocked
when the device is locked, the calls to AppleKeyStore
will fail.
After this quick introduction on the iOS keychain, let’s dive into the keychain spyware module.
Keychain module main’s logic
This binary contains its symbols so it is pretty straightforward to analyse. The code seems to be a modified and updated version of this project : https://github.com/nabla-c0d3/iphone-dataprotection.keychainviewer/tree/master.
When started, the module follows this logic :
First it deobfuscates its strings using a simple xor cipher.
Then it checks the lock state of the device.
If the device has never been unlocked it will sleep for 0.25s and retry until the device has been unlocked for the first time.
If the device has been unlocked, the module will start dumping the keychain. It will open the database, dump the keychain, encrypt the dumped data and write it in a file.
In depth analysis
The first thing we see when analysing the main
function is that there is a function that deobfuscate strings named demangleString
. The function is a simple xor cipher :
With the help of an IDA script we can retrieve the list of strings, and add a comment for each call to demangleString
to help the reverse engineering process.

The script can be found at the end of the blogpost.
After some checks to see if the strings got deobfuscated correctly, The module is calling snprintf
on the format string /private/var/tmp/S5L%08x-%d.kcd
and /private/var/tmp/S5L%08x-%d.kcd.tmp
Four path are created :
/private/var/tmp/S5LRANDOM-0.kcd
/private/var/tmp/S5LRANDOM-1.kcd
/private/var/tmp/S5LRANDOM-0.kcd.tmp
/private/var/tmp/S5LRANDOM-1.kcd.tmp
These are the files that will contain the dumped data.
AppleKeyStore GetLockState
The module is verifying the “lock state” of the device. To do so, It’s calling the function named AppleKeyStore_getLockState()
.
This function is calling the selector 0x7
from the AppleKeyStore
kernel extension. This selector allows to retrieve the lock state of the device. From our experimentation, the possible values for the lock state are :
0x1 -> never unlocked
0x5 -> locked with passcode
0x4 -> unlocked with passcode
0x6 -> no passcode This check is done because trying to unwrap keys from a never unlocked device would fail for most items.
To call this selector, the module is using two helper functions named IOKit_call
and IOKit_getConnect
. These function are calling IO*
function from Apple’s IOKit
(IOServiceMatching
, IOServiceGetMatchingService
, IOConnectCallMethod
, etc..) to communicate with the kernel extension. All other communications with the AppleKeyStore
kernel extension will be done using these functions.
Dumping the keyChain
Before dumping the keychain, a call to the function AppleKeyStoreKeyBagInit()
is made. It is calling the selector 0x0
of the AppleKeyStore
kext.
If this call fails, the module will stop. From our experimentation, this call is not required to be able to unwrap keys using AppleKeyStore
kext.
After that, the module is opening the keychain located at /private/var/Keychains/keychain-2.db
using the function keychain_open
. It is allocating a structure defined as :
This structure is based on the structure used in https://github.com/nabla-c0d3/iphone-dataprotection.keychainviewer/blob/master/Keychain/keychain.h#L11, but a function pointer has been added.
After that, It is executing the request SELECT version from tversion
to retrieve the version and set its structure member with it. It is also defining the function pointer keychain_get_meta_item_fnptr
. This function pointer will be used later. Depending of the version it will also define the keychain_get_item_fnptr
pointer in the structure :
if version equals 4, it is defined to
keychain_get_item_ios4
function address.Else if version is inferior to 5 or superior to 11 the pointer is set to NULL.
Else (version between 5 and 11 included) the pointer is set to
keychain_get_item_ios5
function address.
The mapping between version from tversion
table and iOS version does not seems to be available, but from experimentation we observed that :
on iOS 16.7.2 -> version equal 12.
on iOS 16.0 -> version equal 12.
on iOS 15.7.9 -> version equal 11.
on iOS 15.7.3 -> version equal 11.
on IOS 12.5.7 -> version equal 11.
It’s possible to find traces of these versioning in the securityd
source code published by Apple https://github.com/apple-oss-distributions/Security/blob/Security-61040.1.3/keychain/securityd/SecItemSchema.c, but it does not give a clear iOS version <-> keychain schema version mapping :
tversion 4
The process concerning tversion
4 is slightly different to the process when tversion
is between 5 and 11. When tversion
is 4, in addition of retrieving data and unwrapping keys, the module is decrypting the data using CCCrypt
. This part will not be detailed as it seems to concern only really old iOS versions.
Retrieving items
Let’s follow what is happening when tversion
is between 5 to 11.
First, the function keychain_metadata_keys
is called. This function is in charge of dumping the metadata keys. After that, keychain_get_items
is called severals times. This function is in charge of dumping items from these tables :
genp
inet
cert
keys
Dumping metadata key
The module retrieves the wrapped metadata keys using the request SELECT * from metadatakeys
:
For each rows, it retrieves the actualKeyclass
and data
columns, and then executes the selector 0xB
on the AppleKeyStore
service. This selector seems to correspond to the kAppleKeyStoreKeyUnwrap
call. This allows the module to unwrap the metadata keys for each of the key classes.
After that, it is filling a dictionary with the unwrapped keys, and their corresponding key class :
Once this is done, the module starts to dump the keychain items.
Dumping keychains items
Severals calls are made to the function keychain_get_items
in order to retrieve items.
This function is executing the query passed in it’s 2nd argument. In total, four queries are executed :
For each rows returned by the query, it executes the keychain_get_item_fnptr
function defined by keychain_open
. When tversion
is between 5 and 11, it is defined with the function’s address keychain_get_item_ios5
.
The function keychain_get_item_ios5
is retrieving the rowid
, the protection class and the processed item’s data, and then, is filling a dictionary with these values. The item’s data are processed using the function decrypt_data_ios5
.
Item’s data processing
As explained in the introduction to keychain item storage, the item data is stored in a serialised object. The behaviour of the item’s data processing depends of the item version. The version is stored in the first four bytes of the item data.
When the version is 7 or 8, the module uses a custom parsing function parseKeychainData
that retrieves the key class and the wrapped key from the serialised object. When it has been done, the key is unwrapped using the AppleKeyStore_keyUnwrap
function. A dictionary is then filled with the unwrapped key, and the item’s data blob.
For lower versions, the code is the same as this : https://github.com/nabla-c0d3/iphone-dataprotection.keychainviewer/blob/master/Keychain/keychain5.c#L21. It retrieves the unwrapped key and the data blob, and then uses CCCryptorGCM
to decrypt the blob.
Once this is done, all the data is merged into a serialised plist data object with the function saveResults
.
Encrypting the dump
When all the tables has been dumped, the module closes the keychain database, and calls _encryptData
to encrypt the dumped data.
Analysing the function shows that first, it compresses the data using compress2
. Then, it uses SecRandomCopyBytes
to generate its IV, and then uses CCCrypt
to encrypt the data. The key used is : 73 ab 2f 3f a0 7e 77 30 61 2b 25 fc 66 2a 73 50
.
Saving the data
Finally it saves the encrypted data into one of its .tmp
file, depending on the lock state either : /private/var/tmp/S5LRANDOM-0.kcd.tmp
or /private/var/tmp/S5LRANDOM-1.kcd.tmp
. If everything went correctly, it renames the file into a .kcd
.
These file are probably retrieved and exfiltrated by another module.
Conclusion
This module of the operation triangulation spyware seems to be code retrieved from github and modified. The behaviour of the module, raises some questions, for example, why it does not support tversion
superior to 11 while it seems that on tversion
12 the items are stored the same way. Also, why the module isn’t waiting for the device to be unlocked, but only waits for the first unlock making it missing items classes protected by device locking. Also it is possible that this module has been included by mistake, because as Kaspersky showed, a keychain module is already existing in TriangleDB. Besides that, this module is fairly simple and uses known techniques.
There are other module to analyse, in the next blogpost, we will see how the audio is recorded and how traces are hidden by analysing the audio module.