Skip to content

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:

  1. Aplicar patch na funcionalidade anti-debugging e desativar o comportamento indesejado sobrescrevendo o código associado com instruções NOP.
  2. Aplicar patch em qualquer hash armazenado que seja usado para avaliar a integridade do código.
  3. 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:

  1. Obter os dados como NSMutableData.
  2. Obter a chave de dados (normalmente do Keychain).
  3. Calcular o valor de hash.
  4. Acrescentar o valor de hash aos dados reais.
  5. 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];
  1. Extrai a mensagem e os bytes do hmac como NSData separados.
  2. Repete as etapas 1-3 do procedimento para gerar um HMAC no NSData.
  3. 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:

  1. Recuperar os dados do dispositivo, conforme descrito em Vinculação de Dispositivo.
  2. Alterar os dados recuperados e devolvê-los ao armazenamento.