Introdução
Bem-vindo à Parte 1 da Série Avançada de Frida. Nesta série, vamos analisar como podemos usar o poder do Frida para realizar análises avançadas em aplicativos e demônios. A primeira parte mergulhará na análise de uma biblioteca iOS de terceiros usada para criptografia de dados.
Os aplicativos iOS às vezes querem persistir algum tipo de informação no sistema. Uma das soluções fornecidas pela Apple é o chamado CoreData.
Ao consultar a documentação:
“O CoreData abstrai os detalhes da mapeamento de seus objetos para um armazenamento, tornando fácil salvar dados do Swift e Objective-C sem administrar um banco de dados diretamente.”
Informações sensíveis não devem ser armazenadas dentro do CoreData porque os dados não são criptografados. Para esse tipo de informação, devemos usar Keychain.
Outra solução que os desenvolvedores às vezes usam é o banco de dados sqlite. O problema com o sqlite é que ele não está criptografado por padrão, significando que qualquer um/qualquer coisa que possa acessar o banco de dados pode ler.
Um dos projetos que usa um banco de dados sqlite com criptografia é EncryptedStore. Embora o projeto pareça abandonado/não mantido, ainda está sendo usado em alguns aplicativos.
O EncryptedStore permite que os aplicativos usem um banco de dados sqlite criptografado da mesma maneira como usariam o CoreData.
Analisando o EncryptedStore
Por padrão, o banco de dados sqlite é armazenado no diretório Documents do aplicativo em formato APPLICATION_NAME.sqlite.
Caso tentemos carregar o arquivo dentro do DB Browser for SQLite, veremos que somos imediatamente solicitados por uma senha, indicando que o banco de dados é protegido por senha e os dados nele são criptografados.
Ao consultar a documentação, ficamos principalmente interessados na classe + [EncryptedStore makeDescriptionWithOptions:configuration:error:]. O primeiro parâmetro do método é um NSDictionary contendo a passphrase dentro da chave EncryptedStorePassphrase.
Podemos confirmar isso usando o Frida e interceptando no método para examinar o segundo argumento.
8kSec > cat encrypted_store.js
var conf = ObjC.classes.EncryptedStore["+ makeDescriptionWithOptions:configuration:error:"]\.implementation\;
Interceptor.attach(conf, {
onEnter(args) {
console.log(ObjC.Object(args[2]));
}
});
8kSec > frida -U Gadget -l ./encrypted_store.js
____
/ _ | Frida 16.0.19 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to iPhone (id=00008110-001C78960A50401E)
[iPhone::Gadget ]-> {
EncryptedStoreFileManagerOption = "";
EncryptedStorePassphrase = THISISSECRETPASSPHRASE;
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
}
[iPhone::Gadget ]->
Agora, se tentarmos acessar o banco de dados e fornecer a passphrase, podemos ver que a passphrase está correta.
Próximo passo: vamos usar as funções nativas sqlite para interagir com o banco de dados.
Explorando O Banco De Dados Com Frida E Funções Nativas
O que precisamos são:
- manipulador do banco de dados
sqlite3_errmsg- opcional, mas é bom ler a mensagem em vez do número de errosqlite3_exec- para executar nossa consulta
Para obter o manipulador do banco de dados, podemos usar o ivar da EncryptedStore chamado database, que contém o manipulador do banco de dados e pode ser visto no trecho de código abaixo.
@implementation EncryptedStore {
// recursos do banco de dados
sqlite3 *database;
// cache dinheiro
NSMutableDictionary *objectIDCache;
NSMutableDictionary *nodeCache;
NSMutableDictionary *objectCountCache;
NSMutableDictionary *entityTypeCache;
Para pesquisar ou selecionar objetos do EncryptedStore, podemos usar a função ObjC.chooseSync.

Agora temos uma maneira de obter um manipulador do banco de dados válido.
Se olharmos para a definição da função sqlite3_errmsg, podemos ver que ela aceita um único parâmetro (sqlite3*) que será nosso manipulador do banco de dados e retorna const char * ou representação em string se ocorrer um erro (valor de retorno das funções sqlite3 não foi bem-sucedido).

No mundo da Frida, tanto sqlite3* quanto const char* são simplesmente representados como ponteiro.
Portanto, para realmente chamar essa função, precisamos “criá-la” na Frida. Fazemos isso criando um novo objeto NativeFunction. O primeiro parâmetro é o endereço real da função sqlite3_errmsg, o segundo é seu valor de retorno e o último argumento é um array de tipos de argumentos.
Juntando tudo isso, fica assim:
var sqlite3_errmsg = new NativeFunction(Module.findExportByName(null, "sqlite3_errmsg"), "pointer", ["pointer"]);
Agora, vamos para a função sqlite3_exec. Esta função é mais interessante porque espera uma chamada de volta de nossa parte.

Necessário:
- manipulador de banco de dados - sabemos como obter isso
- consulta SQL para executar - podemos alocar com
Memory.allocUtf8String - chamada de volta - ponteiro de função
void *- o primeiro argumento do ponteiro não é necessário para nóserrmsg- podemos ignorar essa também
Nossa sqlite3_exec NativeFunction vai ficar assim:
var sqlite3_exec = new NativeFunction(Module.findExportByName(null, "sqlite3_exec"), "int", ["pointer", "pointer", "pointer", "int", "pointer"]);
Para passar o terceiro argumento, ou a chamada de volta, utilizaremos CModule. Ele nos permite usar a API C para interagir com o aplicativo.
Analisando o exemplo básico do uso da sqlite3, podemos ver que a chamada de volta aceita 4 argumentos:
void*- o argumento que podemos passar da chamadasqlite3_execargc- contagem de argumentos retornadosargv- array contendo valoresazColName- array contendo nomes de colunas
Para criar um novo CModule, simplesmente passamos o código para new CModule e tudo dentro dele ficará disponível no objeto resultante.
O CModule real que usaremos vai ficar assim:
var cm = new CModule(`
#include <stdio.h>
int callback(void *not_used, int argc, char **argv, char **col_name){
for (int i = 0; i < argc; i++) {
printf("data %s => %s\n", col_name[i], argv[i]);
}
return 0;
`);
Para passar essa função callback para o sqlite3_exec como terceiro argumento, podemos fazer isso acessando cm.callback.
var sqlite3_errmsg = new NativeFunction(Module.findExportByName(null, "sqlite3_errmsg"), "pointer", ["pointer"]);
var sqlite3_exec = new NativeFunction(Module.findExportByName(null, "sqlite3_exec"), "int", ["pointer", "pointer", "pointer", "int", "pointer"]);
var cm = new CModule(`
#include <stdio.h>
var sqlite3_errmsg = new NativeFunction(Module.findExportByName(null, "sqlite3_errmsg"), "pointer", ["pointer"]);
var sqlite3_exec = new NativeFunction(Module.findExportByName(null, "sqlite3_exec"), "int", ["pointer", "pointer", "pointer", "int", "pointer"]);
var jsCallback = new NativeCallback((column, val) => {
console.log("data", Memory.readUtf8String(column), "=>", Memory.readUtf8String(val));
}, 'void', ['pointer', 'pointer']);
var cm = new CModule(`
#include <stdio.h>
extern void jsCallback(char *, char*);
int callback(void *not_used, int argc, char **argv, char **col_name){
for (int i = 0; i < argc; i++){
jsCallback(col_name[i], argv[i]);
}
return 0;
}
`, {jsCallback});
var query = Memory.allocUtf8String("SELECT * FROM CREDENTIALS");
var store = ObjC.chooseSync(ObjC.classes.EncryptedStore)[0];
var db = store.$ivars["database"];
var ret = sqlite3_exec(db, query, cm.callback, 0, NULL);
if (ret != 0) {
console.log(Memory.readUtf8String(sqlite3_errmsg(db)));
}

