MASTG-KNOW-0086: Verificações de Integridade de Arquivos
Existem duas abordagens comuns para verificar a integridade de arquivos: usando verificações de integridade do código-fonte da aplicação e usando verificações de integridade do armazenamento de arquivos.
Verificações de Integridade do Código-Fonte da Aplicação¶
Em "Debugging" ( Depuração), discutimos a verificação de assinatura da aplicação IPA no iOS. Também aprendemos que engenheiros reversos determinados podem contornar essa verificação ao reempacotar e reassinar um aplicativo usando um certificado de desenvolvedor ou empresarial. Uma forma de dificultar isso é adicionar uma verificação personalizada que determine se as assinaturas ainda correspondem durante a execução.
A Apple cuida das verificações de integridade com DRM. No entanto, controles adicionais (como no exemplo abaixo) são possíveis. O mach_header é analisado para calcular o início dos dados de instrução, que são usados para gerar a assinatura. Em seguida, a assinatura é comparada com a assinatura fornecida. Certifique-se de que a assinatura gerada seja armazenada ou codificada em outro lugar.
int xyz(char *dst) {
const struct mach_header * header;
Dl_info dlinfo;
if (dladdr(xyz, &dlinfo) == 0 || dlinfo.dli_fbase == NULL) {
NSLog(@" Error: Could not resolve symbol xyz");
[NSThread exit];
}
while(1) {
header = dlinfo.dli_fbase; // Ponteiro para o cabeçalho Mach-O
struct load_command * cmd = (struct load_command *)(header + 1); // Primeiro comando de carga
// Agora iterar pelos comandos de carga
// para encontrar a seção __text do segmento __TEXT
for (uint32_t i = 0; cmd != NULL && i < header->ncmds; i++) {
if (cmd->cmd == LC_SEGMENT) {
// O comando de carga __TEXT é um comando de carga LC_SEGMENT
struct segment_command * segment = (struct segment_command *)cmd;
if (!strcmp(segment->segname, "__TEXT")) {
// Parar no comando de carga do segmento __TEXT e percorrer as seções
// para encontrar a seção __text
struct section * section = (struct section *)(segment + 1);
for (uint32_t j = 0; section != NULL && j < segment->nsects; j++) {
if (!strcmp(section->sectname, "__text"))
break; // Parar no comando de carga da seção __text
section = (struct section *)(section + 1);
}
// Obter aqui o endereço da seção __text, o tamanho da seção __text
// e o endereço de memória virtual para calcular
// um ponteiro para a seção __text
uint32_t * textSectionAddr = (uint32_t *)section->addr;
uint32_t textSectionSize = section->size;
uint32_t * vmaddr = segment->vmaddr;
char * textSectionPtr = (char *)((int)header + (int)textSectionAddr - (int)vmaddr);
// Calcular a assinatura dos dados,
// armazenar o resultado em uma string
// e comparar com a original
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(textSectionPtr, textSectionSize, digest); // calcular a assinatura
for (int i = 0; i < sizeof(digest); i++) // preencher assinatura
sprintf(dst + (2 * i), "%02x", digest[i]);
// return strcmp(originalSignature, signature) == 0; // verificar se as assinaturas coincidem
return 0;
}
}
cmd = (struct load_command *)((uint8_t *)cmd + cmd->cmdsize);
}
}
}
Contorno:
- Aplicar patch na funcionalidade anti-debugging e desativar o comportamento indesejado sobrescrevendo o código associado com instruções NOP.
- Aplicar patch em qualquer hash armazenado que seja usado para avaliar a integridade do código.
- Usar Frida para interceptar APIs do sistema de arquivos e retornar um handle para o arquivo original em vez do arquivo modificado.
Verificações de Integridade do Armazenamento de Arquivos¶
Aplicativos podem optar por garantir a integridade do próprio armazenamento da aplicação, criando um HMAC ou assinatura sobre um par chave-valor específico ou um arquivo armazenado no dispositivo, por exemplo, no Keychain, UserDefaults/NSUserDefaults ou qualquer banco de dados.
Por exemplo, um aplicativo pode conter o seguinte código para gerar um HMAC com CommonCrypto:
// Alocar um buffer para conter o digest e realizar o digest.
NSMutableData* actualData = [getData];
// obter a chave do keychain
NSData* key = [getKey];
NSMutableData* digestBuffer = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, [actualData bytes], (CC_LONG)[key length], [actualData bytes], (CC_LONG)[actualData length], [digestBuffer mutableBytes]);
[actualData appendData: digestBuffer];
Este script executa as seguintes etapas:
- Obter os dados como
NSMutableData. - Obter a chave de dados (normalmente do Keychain).
- Calcular o valor de hash.
- Acrescentar o valor de hash aos dados reais.
- Armazenar os resultados da etapa 4.
Depois disso, pode estar verificando os HMACs fazendo o seguinte:
NSData* hmac = [data subdataWithRange:NSMakeRange(data.length - CC_SHA256_DIGEST_LENGTH, CC_SHA256_DIGEST_LENGTH)];
NSData* actualData = [data subdataWithRange:NSMakeRange(0, (data.length - hmac.length))];
NSMutableData* digestBuffer = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, [actualData bytes], (CC_LONG)[key length], [actualData bytes], (CC_LONG)[actualData length], [digestBuffer mutableBytes]);
return [hmac isEqual: digestBuffer];
- Extrai a mensagem e os bytes do hmac como
NSDataseparados. - Repete as etapas 1-3 do procedimento para gerar um HMAC no
NSData. - Compara os bytes do HMAC extraídos com o resultado da etapa 1.
Nota: se o aplicativo também criptografar arquivos, certifique-se de que ele criptografa e depois calcula o HMAC conforme descrito em Authenticated Encryption.
Contorno:
- Recuperar os dados do dispositivo, conforme descrito em Vinculação de Dispositivo.
- Alterar os dados recuperados e devolvê-los ao armazenamento.