Introduction
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;
int v3;
unsigned int v4;
int v5;
unsigned __int8 v6;
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
char *__fastcall tm_decrypt_str(char *result, const char *src)
{
int i;
int v3;
unsigned int v4;
int v5;
unsigned __int8 v6;
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
char *__fastcall tm_decrypt_str(char *result, const char *src)
{
int i;
int v3;
unsigned int v4;
int v5;
unsigned __int8 v6;
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
__int64 __fastcall tm_retrieve_config_param(
__int64 flag,
char *param_out,
unsigned __int64 *param_out_size,
const char *param_name
__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 :
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
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
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
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
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
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
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
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
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 :
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
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
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
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
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
__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
__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
__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
__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
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
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
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
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
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
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_
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_
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 )
{
v111 = v113;
if ( (_DWORD)v113 == 100 )
{
v140 = 0LL
v110 = sqlite3_prepare_v2(ppDb, str_tokens, -1, &pStmt, 0LL);
...
v113 = sqlite3_step(pStmt);
if ( (_DWORD)v113 != 101 )
{
v111 = v113;
if ( (_DWORD)v113 == 100 )
{
v140 = 0LL
v110 = sqlite3_prepare_v2(ppDb, str_tokens, -1, &pStmt, 0LL);
...
v113 = sqlite3_step(pStmt);
if ( (_DWORD)v113 != 101 )
{
v111 = v113;
if ( (_DWORD)v113 == 100 )
{
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
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
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
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
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
elif insn.itype == ida_allins.ARM_mov:
if insn.ops[1].type == ida_ua.o_reg:
reg_old = idc.print_operand(addr, 1)
while True:
addr = addr - 4
idaapi.decode_insn(insn, addr)
if insn.itype == ida_allins.ARM_adr:
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 = []
for xref in idautils.XrefsTo(func_ea):
caller_ea = xref.frm
src = retrieve_2nd_args_from_adr_insn(caller_ea)
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)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
elif insn.itype == ida_allins.ARM_mov:
if insn.ops[1].type == ida_ua.o_reg:
reg_old = idc.print_operand(addr, 1)
while True:
addr = addr - 4
idaapi.decode_insn(insn, addr)
if insn.itype == ida_allins.ARM_adr:
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 = []
for xref in idautils.XrefsTo(func_ea):
caller_ea = xref.frm
src = retrieve_2nd_args_from_adr_insn(caller_ea)
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)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
elif insn.itype == ida_allins.ARM_mov:
if insn.ops[1].type == ida_ua.o_reg:
reg_old = idc.print_operand(addr, 1)
while True:
addr = addr - 4
idaapi.decode_insn(insn, addr)
if insn.itype == ida_allins.ARM_adr:
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 = []
for xref in idautils.XrefsTo(func_ea):
caller_ea = xref.frm
src = retrieve_2nd_args_from_adr_insn(caller_ea)
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)
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
binary_path = "../c2393fceab76776e19848c2ca3c84bea0ed224ac53206c48f1c5fd525ef66306"
with open(binary_path, "rb") as f:
binary_data = f.read()
start_address = 0x100000000
uc.mem_map(start_address, 8 * 1024 * 1024)
uc.mem_write(start_address, binary_data)
STACK_SIZE = 0x1000
STACK_ADDRESS = 0x20000000
uc.mem_map(STACK_ADDRESS, STACK_SIZE)
uc.reg_write(UC_ARM64_REG_SP, STACK_ADDRESS + STACK_SIZE)
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
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)
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
binary_path = "../c2393fceab76776e19848c2ca3c84bea0ed224ac53206c48f1c5fd525ef66306"
with open(binary_path, "rb") as f:
binary_data = f.read()
start_address = 0x100000000
uc.mem_map(start_address, 8 * 1024 * 1024)
uc.mem_write(start_address, binary_data)
STACK_SIZE = 0x1000
STACK_ADDRESS = 0x20000000
uc.mem_map(STACK_ADDRESS, STACK_SIZE)
uc.reg_write(UC_ARM64_REG_SP, STACK_ADDRESS + STACK_SIZE)
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
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)
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
binary_path = "../c2393fceab76776e19848c2ca3c84bea0ed224ac53206c48f1c5fd525ef66306"
with open(binary_path, "rb") as f:
binary_data = f.read()
start_address = 0x100000000
uc.mem_map(start_address, 8 * 1024 * 1024)
uc.mem_write(start_address, binary_data)
STACK_SIZE = 0x1000
STACK_ADDRESS = 0x20000000
uc.mem_map(STACK_ADDRESS, STACK_SIZE)
uc.reg_write(UC_ARM64_REG_SP, STACK_ADDRESS + STACK_SIZE)
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
Strings list
Here is the decrypted strings list :