This blog post is the continuation of our series on Operation Triangulation. The previous blog post can be found here : https://shindan.io/posts/audio_module_analysis/.
In this blogpost we will focus on the sms stealing module c2393fceab76776e19848c2ca3c84bea0ed224ac53206c48f1c5fd525ef66306
.
The module is pretty simple. It is opening the SMS database, executing several requests on it, and saving the compressed and encrypted output in a file.
Strings obfuscation #
Like the other modules, the strings are obfuscated. They are deobfuscated using a function located at 0x1000099CC
. The function is a simple routine and is the same that is used in the other modules :
char *__fastcall tm_decrypt_str(char *result, const 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 * ((0x401 * v3) ^ ((unsigned int)(0x401 * v3) >> 6));
v5 = 0x8001 * (v4 ^ (v4 >> 11));
v6 = src[i + 1] ^ v5;
result[i++] = v6;
v3 = v5 ^ v6;
}
while ( v6 );
return result;
}
We created an IDA python script to deobfuscate the strings. The python script and the strings list can be found in the Annexes.
Retrieving configuration #
The module configuration is managed in the same way as the Microphone recording module.
It contains a predefined configuration stored in the .data
segment. The function located at 0x100009840
is used to retrieve configuration parameter value.
__int64 __fastcall tm_retrieve_config_param(
__int64 flag,
char *param_out,
unsigned __int64 *param_out_size,
const char *param_name)
As for the Microphone recording module, we chose to emulate that function to dump the module configuration easily. We used Unicorn Engine for that. The script used can be found in the Annexes.
We retrieved the following configuration with our script :
fp -> /private/var/tmp/adr3
fx -> .dat
ky -> 30f7ad596a12ddee75cba5efa560cda9e1e397bbae71b9c3d9c323929194fa54
sd -> N
qs -> eJytUstOwzAQ/BXfwqPgtD0UhMSl7SFSipCSu7VJlsYQ26m9bcnfk9BYoKIEDvi02pnZHY/NaysPQMgPYLkymayQxzKzYBuebBLulLstsodkHa+XKVtFSRo9tYXbVezFGtUVklAocITW065OWF4CCYXOwRbFq5GaHUu0yIp2H0mFFx7rGnwa+nN9v7ibh4tZGE5YsNfyHWuTl8Ele/ySBtocg0lwM29bjWsxYwu0LGvY96FstU6WLI42Ucpm3egzhz333Ne/+vmLD5F8xrgCggwcPltToyWJrtcsfkq6cMdRUYIuqj75QWaBFRIW/p1GNp7GDeM+eCCCvFSo6ZfVXlBbk7el1FtB4N6GBf1nc7jbo85HrHgiAU1HWI3Oxcj9PwDzBvxW
ul -> 796
More explanation on the module configuration can be found here : https://shindan.io/posts/audio_module_analysis/#retrieving-the-configuration.
It is important to note that this module also has the possilibity to retrieve its configuration from an environment value. This will be explained later.
Main logic #
After getting the strings and the configuration, we started to analyze the main function.
Retrieving SQL requests. #
The module starts by retrieving the ul
parameter (corresponding to796
), and uses this value to allocate memory with calloc
.
param = tm_decrypt_str(v156, "ul\x00\x92");
param_ul = 0LL;
if ( !(unsigned int)tm_retrieve_config_param(0LL, v161, param_out_size, param) )
...
calloc_mem_ul_size = (__int64)calloc(param_ul + 1, 1uLL);
...
Then it retrieves the value of parameter qs
, and uses base64 decode function to decode it.
v10 = tm_decrypt_str(v156, "qs\x003");
v11 = 0LL;
if ( (unsigned int)tm_retrieve_config_param(0LL, v161, param_out_size, v10) )
goto LABEL_5;
v19 = param_out_size[0];
v7 = 1;
v20 = calloc(param_out_size[0], 1uLL);
bzero(v20, v19);
calloc_mem_ul_size = tm_base64_decode(v20, *(_QWORD *)v161);
Following this, the module uses zlibrary to decompress the data :
memset(&strm.zalloc, 0, 24);
strm.avail_in = param_out_size[0];
strm.next_in = (Bytef *)v20;
strm.avail_out = param_ul;
strm.next_out = ul_calloced_memory;
inflateInit_(&strm, "1.2.11", 112);
inflate(&strm, 0);
inflateEnd(&strm);
We used bash and python to retrieve the value :
echo -n 'eJytUstOwzAQ/BXfwqPgtD0UhMSl7SFSipCSu7VJlsYQ26m9bcnfk9BYoKIEDvi02pnZHY/NaysPQMgPYLkymayQxzKzYBuebBLulLstsodkHa+XKVtFSRo9tYXbVezFGtUVklAocITW065OWF4CCYXOwRbFq5GaHUu0yIp2H0mFFx7rGnwa+nN9v7ibh4tZGE5YsNfyHWuTl8Ele/ySBtocg0lwM29bjWsxYwu0LGvY96FstU6WLI42Ucpm3egzhz333Ne/+vmLD5F8xrgCggwcPltToyWJrtcsfkq6cMdRUYIuqj75QWaBFRIW/p1GNp7GDeM+eCCCvFSo6ZfVXlBbk7el1FtB4N6GBf1nc7jbo85HrHgiAU1HWI3Oxcj9PwDzBvxW' | base64 -d | python3 -c 'import zlib, sys; sys.stdout.buffer.write(zlib.decompress(sys.stdin.buffer.read()))'
/private/var/mobile/Library/SMS/sms.db;SELECT DISTINCT sql from sqlite_master;SELECT * from chat_message_join where datetime(message_date/1000000000+978307200, 'unixepoch') > datetime('now','-3 days') order by message_date DESC LIMIT 20000;SELECT * from message where datetime(date/1000000000+978307200, 'unixepoch') > datetime('now','-3 days') order by date DESC LIMIT 20000;SELECT * from _SqliteDatabaseProperties LIMIT 7000;SELECT * from chat LIMIT 7000;SELECT * from chat_handle_join LIMIT 7000;SELECT * from deleted_messages LIMIT 7000;SELECT * from handle LIMIT 7000;SELECT * from message_attachment_join LIMIT 7000;SELECT * from message_processing_task LIMIT 7000;SELECT * from sqlite_sequence LIMIT 7000;SELECT * from sqlite_stat1 LIMIT 7000;SELECT * from sync_deleted_messages LIMIT 7000
We see that the value contains a filename : /private/var/mobile/Library/SMS/sms.db
and several SQL requests. The ;
character is used as separator.
List of SQL Request #
Here is the list of the executed SQL requests :
SELECT DISTINCT sql from sqlite_master;
SELECT * from chat_message_join where datetime(message_date/1000000000+978307200, 'unixepoch') > datetime('now','-3 days') order by message_date DESC LIMIT 20000;
SELECT * from message where datetime(date/1000000000+978307200, 'unixepoch')> datetime('now','-3 days') order by date DESC LIMIT 20000;
SELECT * from _SqliteDatabaseProperties LIMIT 7000;
SELECT * from chat LIMIT 7000;
SELECT * from chat_handle_join LIMIT 7000;
SELECT * from deleted_messages LIMIT 7000;
SELECT * from handle LIMIT 7000;
SELECT * from message_attachment_join LIMIT 7000;
SELECT * from message_processing_task LIMIT 7000;
SELECT * from sqlite_sequence LIMIT 7000;
SELECT * from sqlite_stat1 LIMIT 7000;
SELECT * from sync_deleted_messages LIMIT 7000
These requests are used to dump the /private/var/mobile/Library/SMS/sms.db
database.
Retrieving other parameters’ value #
After that, The module retrieves its other parameters’ value:
fp
(file path) :/private/var/tmp/adr3
fx
(file extension) :.dat
ky
(key) :30f7ad596a12ddee75cba5efa560cda9e1e397bbae71b9c3d9c323929194fa54
sd
(self delete) :N
The logic is pretty much the same each time :
- Decrypts the parameter name string.
- Retrieves the parameter value.
- Allocates memory for the parameter value.
- Copy it into the newly allocated memory.
v12 = tm_decrypt_str(v156, "fp\x00\xF2\x00\xF2\x00\xB9");
if ( (unsigned int)tm_retrieve_config_param(0LL, param_out, param_out_size, v12) )
...
fp_value_ = *(const char **)param_out;
...
fp_value_len = strlen(*(const char **)param_out) + 1;
fp_value_cpy = tm_malloc(fp_value_len);
...
filename_plus_sql = (Bytef *)fp_value_cpy;
memcpy(fp_value_cpy, fp_value_, fp_value_len);
Reading configuration from environment #
The module also reads values from the device environment. It checks the following environment variables :
QS
FP
FX
KY
These values seem to correspond to the module configuration parameters. For each parameter the logic is the same :
- Decrypts the string.
- Retrieves the value from environment using
getenv
. - Allocates memory if value has been found in environment.
- Copy the value into newly allocated memory.
v39 = tm_decrypt_str(v156, "QS\x00g");
v40 = getenv(v39);
if ( v40 )
{
v41 = v40;
v42 = strlen(v40);
v43 = (unsigned int)(v42 + 1);
if ( v11 )
{
calloc_mem_ul_size = sub_10000B340(v11, v43);
v11 = (_DWORD *)calloc_mem_ul_size;
if ( !calloc_mem_ul_size )
goto LABEL_44;
}
else
{
calloc_mem_ul_size = (__int64)tm_malloc(v42 + 1);
v11 = (_DWORD *)calloc_mem_ul_size;
if ( !calloc_mem_ul_size )
goto LABEL_44;
}
memcpy(v11, v41, (unsigned int)v43);
}
If these values are found in the environment they are used instead of the module configuration value. It seems that it is another way to pass configuration value to the module.
Retrieving first argument value #
The module can be started with one argument. The value is retrieved and then copied into allocated memory.
if ( argc == 2 )
{
if ( argv )
{
v59 = argv[1];
if ( v59 )
{
v60 = strlen(v59);
v61 = v60 + 1;
if ( allocated_mem_char )
{
allocated_mem = (__int64)tm_realloc_maybe(allocated_mem_char, v61);
allocated_mem_char = (char *)allocated_mem;
if ( !allocated_mem )
goto LABEL_44;
}
else
{
allocated_mem = (__int64)tm_malloc(v60 + 1);
allocated_mem_char = (char *)allocated_mem;
if ( !allocated_mem )
goto LABEL_44;
}
v55 = (const char *)memcpy(allocated_mem_char, v59, v61);
}
}
}
This argument is used as another alternative for the qs
value. If not specified, the module will uses the QS
from environment, or the qs
from the .data
section configuration.
Self deleting #
Depending on the value of the sd
parameter (Self Delete
), the module decides to delete itself or not.
The function responsible for this is located at 100009BCC
.
It uses NSGetExecutablePath
to retrieve the current module path.
After that, it verifies if the file exists, and calls unlink
to delete it.
__int64 tm_self_delete()
{
...
if ( v1 )
{
v3 = 4026531846LL;
if ( !_NSGetExecutablePath(v1, &bufsize) && realpath_DARWIN_EXTSN(v2, v0) && *v0 )
...
tm_bzero_and_free(v2);
if ( !(_DWORD)v3 )
unlink(v0);
...
}
Convert encryption key to bytes #
The module is converting the ky
string: 30f7ad596a12ddee75cba5efa560cda9e1e397bbae71b9c3d9c323929194fa54
into a byte array.
This is done by the function 10000B144
.
The function uses memmove
to read the string using blocks of two characters. Each block is converted to a long
using strtol
.
__int64 __fastcall string_to_byte(__int64 src, unsigned int size, _BYTE *out)
{
...
while ( 1 )
{
tm_memmove(__str, (const void *)src, 2uLL);
*__error() = 0;
v7 = strtol(__str, &v8, 16);
...
}
This parameter is the AES
key used to encrypt data before writing into the dump file.
Generating temporary dump file path #
Now the module will generate the dump file path.
This is handled by the function 0x100009A5C
that generates a pseudo-random filename.
The generated path is used to create a temporary file used by the module
It takes three parameters :
- the file path (corresponding to
fp
parameter). - a file extension.
- a byte number (equal to
6
in this case).
It will create a string in this format : FILEPATH+XXXXXX+FILE_EXTENSION
. (where. XXXXXX
corresponds to 6 random byte from this charset: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
).
unsigned int *__fastcall tm_generate_random_path(const char *file_path, const char *file_ext, __int64 len_maybe)
{
... if ( file_path )
{
if ( file_ext )
{
len_file_path = strlen(file_path);
len_file_ext = strlen(file_ext);
len_total = len_file_path + len_maybe + len_file_ext;
allocated_mem = tm_malloc(len_total + 1);
allocated_mem_cpy = allocated_mem;
if ( allocated_mem )
{
tm_memmove(allocated_mem, file_path, len_file_path);
charset = tm_decrypt_str(v17, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\x00\xD8war+Zb");
...
ptr_after_filepath = (char *)allocated_mem_cpy + (unsigned int)len_file_path;
if ( (_DWORD)len_maybe )
{
ptr_after_filepath_ = ptr_after_filepath;
v15 = len_maybe;
do
{
*ptr_after_filepath_++ = charset[rand() % 0x3EuLL];
--v15;
}
while ( v15 );
}
...
ptr_after_filepath[len_maybe] = 0;
tm_memmove(&ptr_after_filepath[len_maybe], file_ext, len_file_ext);
*((_BYTE *)allocated_mem_cpy + len_total) = 0;
}
}
}
return allocated_mem_cpy;
}
With the current module configuration it results with : /private/var/tmp/adr3XXXXXX.tmp
.
The module does an extra check to verify if the file extension from configuration is equal to .tmp
. If it’s the case, the temporary file will have the .bak
extension.
Opening and writing to the file #
The module uses a custom fopen
function located at 0x10000A7F4
to open the file. As Kaspersky explained in their blogpost, a custom flag Z
has been added to indicate that the file is AES-encrypted and compressed with zlib.
file_ptr_ = tm_custom_fopen((char *)file_path, "wZ", (__int64)param_out_size);
Encryption is done using CCCryptor
related family function.
We observed that the module uses a system of “header”. Before each write of data in the file, the module writes 4 bytes. These are probably used to help reconstructing the file once it has been exfiltrated.
LODWORD(strm.next_in) = 0x3A84B2EB;
if ( !fwrite(&strm, 4uLL, 1uLL, file_ptr_) )
...
v66 = tm_decrypt_str(&v156[6 * v63], "%d\n\x00h");
v67 = fprintf(file_ptr_, v66, 3LL);
Dumping SQL database #
The module retrieves the sqlite3 library version and write it into the file.
LODWORD(strm.next_in) = 0x3A9EAEEC;
if ( !fwrite(&strm, 4uLL, 1uLL, file_ptr_)
|| (v68 = tm_decrypt_str(&v156[6 * (v63 ^ 1)], "%d\n\x00("),
v69 = sqlite3_libversion_number(),
(v70 = fprintf(file_ptr_, v68, v69)) == 0) )
{
After that, the module starts to process the SQL requests.
Executing SQL request #
The module starts by iterating over all the substring found in qs
parameter. It uses strtok_r
to split qs
with ;
as separator.
The first token is used as the database path. The module opens the database using sqlite3_open_v2
, then it enables sqlite3 extended result code using sqlite3_extended_result_codes
.
v107 = sqlite3_open_v2((const char *)db_file_path_maybe_, &ppDb, 1, 0LL);
if ( (_DWORD)v107 )
...
sqlite3_extended_result_codes(ppDb, 1);
free(db_file_path_maybe_);
...
From the second token, the module uses sqlite3_prepare_v2
and then sqlite3_step
to process each row returned by the request.
v110 = sqlite3_prepare_v2(ppDb, str_tokens, -1, &pStmt, 0LL);
...
v113 = sqlite3_step(pStmt);
if ( (_DWORD)v113 != 101 ) // SQLITE_DONE
{
v111 = v113;
if ( (_DWORD)v113 == 100 ) // SQLITE_ROW
{
v140 = 0LL;
For each rows, the module iterates over the columns and write their type, then their value into the file :
do
{
column_type = sqlite3_column_type(pStmt, column_index);
switch ( column_type )
{
case 1:
value = (double *)&v150;
v150 = sqlite3_column_int64(pStmt, column_index);
value_size = 8;
break;
case 2:
value = &__ptr;
__ptr = sqlite3_column_double(pStmt, column_index);
value_size = 8;
break;
case 3:
v119 = (double *)sqlite3_column_text(pStmt, column_index);
goto LABEL_201;
case 4:
v119 = (double *)sqlite3_column_blob(pStmt, column_index);
LABEL_201:
value = v119;
value_size = sqlite3_column_bytes(pStmt, column_index);
break;
default:
value = 0LL;
value_size = 0;
break;
}
if ( fputc(column_type, file_ptr_) == -1 || !value && value_size )
...
v120 = fwrite(&v155, 4uLL, 1uLL, file_ptr_);
...
v122 = fwrite(value, value_size, 1uLL, file_ptr_);
...
while ( (_DWORD)column_count != ++column_index );
If an error happens (g_sqlite3_errmsg
is not null), the error message and the column names are dumped into the file :
v129 = fprintf(file_ptr_, v128, &g_sqlite3_errmsg);
if ( v129 >= 1 && (unsigned int)v129 <= 0xF4234 )
{
v144 = 0;
while ( 1 )
{
v130 = sqlite3_column_name(pStmt, v144);
if ( !v130 )
break;
v131 = v130;
v132 = strlen(v130);
LODWORD(__ptr) = v132;
v133 = fwrite(&__ptr, 4uLL, 1uLL, file_ptr_);
db_file_path_maybe_ = (unsigned __int8 *)v133;
if ( !v133 )
goto LABEL_133;
if ( v132 )
{
v134 = fwrite(v131, v132, 1uLL, file_ptr_);
v135 = v134 + (_DWORD)db_file_path_maybe_;
if ( !v134 )
v135 = 0;
}
else
{
v135 = v133 + 1;
}
if ( v135 < 1 )
break;
if ( (_DWORD)column_count == ++v144 )
goto LABEL_190;
}
}
This is probably done to retrieve the information if the database schema has changed.
These steps are done for all the sql requests found in the configuration. Once it’s done, the module closes and frees everything it needs, and then exits.
Conclusion #
This module is one of the simplest of operation triangulation. Except the possibility of self deletion, it does not implement any tricks to hide itself. The module is just used to dump the content of the sms database using sql requests.
Annexes #
String decryption script #
import idaapi
import idautils
import idc
import ida_allins
def decrypt_str(src):
i = 0
v3 = (src[0]) | (src[0] << 8)
result = []
while True:
v4 = (9 * (((0x401 * v3) & 0xFFFFFFFF) ^ ((0x401 * v3) & 0xFFFFFFFF) >> 6)) & 0xFFFFFFFF
v5 = (0x8001 * (v4 ^ (v4 >> 11)) & 0xFFFFFFFF)
v6 = ((src[i + 1] ^ v5) & 0xFFFFFFFF)
result.append(chr(v6 & 0xFF))
v3 = (v5 ^ (v6 & 0xFF))
if (v6 & 0xFF) == 0:
break
i = i+1
return result
def retrieve_2nd_args_from_adr_insn(addr):
addr = idaapi.get_arg_addrs(addr)[1]
insn = idaapi.insn_t()
length = idaapi.decode_insn(insn, addr)
if insn.itype == ida_allins.ARM_adr:
if insn.ops[1].type == ida_ua.o_imm:
return insn.ops[1].value
# It's a mov instruction
elif insn.itype == ida_allins.ARM_mov:
# the second operand is a register we will search for an ADR instruction concerning this register before
if insn.ops[1].type == ida_ua.o_reg:
# saving the second operand (register)
reg_old = idc.print_operand(addr, 1)
while True:
addr = addr - 4
idaapi.decode_insn(insn, addr)
# we found an ADR
if insn.itype == ida_allins.ARM_adr:
# let's see if it's affecting our register
reg_new = idc.print_operand(addr, 0)
if reg_new == reg_old:
if insn.ops[1].type == ida_ua.o_imm:
return insn.ops[1].value
else:
return None
def decrypt_strings(func_ea):
done = []
# Iterate over all cross-references to the given function
for xref in idautils.XrefsTo(func_ea):
# Get the calling function's start address
caller_ea = xref.frm
src = retrieve_2nd_args_from_adr_insn(caller_ea)
# we want to do each string only one times
if src not in done:
ret = decrypt_str(idaapi.get_bytes(src, 0x100))
print("".join(ret))
i = 0
for b in ret:
if b == b'\x00':
break
idaapi.patch_byte(src+i, ord(b))
i = i+1
done.append(src)
target_function_address = 0x1000099CC
decrypt_strings(target_function_address)
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(0x10000B20C, 0x10000B238)
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(0x100009A10, 0x100009A58)
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 = "../c2393fceab76776e19848c2ca3c84bea0ed224ac53206c48f1c5fd525ef66306"
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 = 0x1000100c0
offset_1 = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_head + 0x10, 0x8))[0])
while (1):
len_encoded_param_name = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_head+offset_1, 0x8))[0])
offset_to_param_name = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_head+offset_1+0x8, 0x8))[0])
addr_decoded_str = 0x1000
uc.mem_map(addr_decoded_str, 1024)
emulate_decode_string(uc, addr_decoded_str, addr_head + offset_to_param_name, len_encoded_param_name -1)
decoded_name_param = uc.mem_read(addr_decoded_str, len_encoded_param_name-1)
len_encoded_value = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_head+offset_1+0x10, 0x8))[0])
offset_to_param_value = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_head+offset_1+0x18, 0x8))[0])
addr_decoded_param = 0x2000
uc.mem_map(addr_decoded_param, 1024)
emulate_decode_string(uc, addr_decoded_param, addr_head + offset_to_param_value, len_encoded_value -2)
decoded_value_param = uc.mem_read(addr_decoded_param, len_encoded_value-2)
print(f"{decoded_name_param.decode('utf-8')} -> {decoded_value_param.decode('utf-8')}")
offset_1 = emulate_rev_highest_byte(uc, struct.unpack("<Q", uc.mem_read(addr_head+offset_1+0x20, 0x8))[0])
uc.mem_unmap(addr_decoded_str, 1024)
uc.mem_unmap(addr_decoded_param, 1024)
if offset_1 == 0:
break
#
#try:
# uc.emu_start(0x10000bc58, 0x10000bd7c)
#except UcError as e:
# print(f"Error: {e}")
Strings list #
Here is the decrypted strings list :
ul
qs
fp
fx
ky
sd
QS
FP
FX
KY
.tmp
.bak
%d
%d
%s
%s
%s
r
%d
%Y-%m-%dT%T
%s
%s
%d
%d
%d
%s
%d
%s0
%d
- %s
initWithContentsOfFile:
/private/var/mobile/Library/Caches/com.apple.mobile.installation.plist
/var/mobile/Library/BackBoard/applicationState.plist
objectForKey:
System
User
Path
compatibilityInfo
bundlePath
stringByDeletingLastPathComponent
8.0
9.0
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
/System/Library/Frameworks/Security.framework/Security
CCCryptorCreate
CCCryptorUpdate
CCCryptorFinal
CCCryptorRelease
CCCryptorReset
SecRandomCopyBytes