Skip to content

MASTG-KNOW-0029: Verificações de Integridade de Arquivos

Existem dois tópicos relacionados à integridade de arquivos:

  1. Verificações de integridade de código: Você pode usar verificações CRC como uma camada adicional de proteção para o bytecode do aplicativo, bibliotecas nativas e arquivos de dados importantes. Dessa forma, o aplicativo só funcionará corretamente em seu estado não modificado, mesmo que a assinatura do código seja válida.
  2. Verificações de integridade de armazenamento de arquivos: A integridade dos arquivos que o aplicativo armazena no cartão SD ou no armazenamento público, bem como a integridade dos pares chave-valor armazenados no SharedPreferences, deve ser protegida.

Implementação de Exemplo - Código-Fonte do Aplicativo

As verificações de integridade geralmente calculam uma soma de verificação (checksum) ou hash sobre arquivos selecionados. Os arquivos comumente protegidos incluem:

  • AndroidManifest.xml,
  • arquivos de classe *.dex,
  • bibliotecas nativas (*.so).

A seguinte implementação de exemplo do blog Android Cracking calcula um CRC sobre o classes.dex e o compara com o valor esperado.

private void crcTest() throws IOException {
 boolean modified = false;
 // valor do crc do dex necessário armazenado como uma string de texto.
 // poderia ser qualquer elemento de layout invisível
 long dexCrc = Long.parseLong(Main.MyContext.getString(R.string.dex_crc));

 ZipFile zf = new ZipFile(Main.MyContext.getPackageCodePath());
 ZipEntry ze = zf.getEntry("classes.dex");

 if ( ze.getCrc() != dexCrc ) {
  // dex foi modificado
  modified = true;
 }
 else {
  // dex não foi adulterado
  modified = false;
 }
}

Implementação de Exemplo - Armazenamento

Ao fornecer integridade no próprio armazenamento, você pode criar um HMAC sobre um determinado par chave-valor (como no SharedPreferences do Android) ou criar um HMAC sobre um arquivo completo fornecido pelo sistema de arquivos.

Ao usar um HMAC, você pode usar uma implementação do Bouncy Castle ou do AndroidKeyStore para aplicar HMAC ao conteúdo fornecido.

Complete o seguinte procedimento ao gerar um HMAC com BouncyCastle:

  1. Certifique-se de que o BouncyCastle ou SpongyCastle esteja registrado como provedor de segurança.
  2. Inicialize o HMAC com uma chave (que pode ser armazenada em um keystore).
  3. Obtenha o array de bytes do conteúdo que precisa de um HMAC.
  4. Chame doFinal no HMAC com o bytecode.
  5. Anexe o HMAC ao array de bytes obtido no passo 3.
  6. Armazene o resultado do passo 5.

Complete o seguinte procedimento ao verificar o HMAC com BouncyCastle:

  1. Certifique-se de que o BouncyCastle ou SpongyCastle esteja registrado como provedor de segurança.
  2. Extraia a mensagem e os bytes do HMAC como arrays separados.
  3. Repita os passos 1-4 do procedimento para gerar um HMAC.
  4. Compare os bytes do HMAC extraídos com o resultado do passo 3.

Ao gerar o HMAC baseado no Android Keystore, é melhor fazer isso apenas para Android 6.0 (API nível 23) e superiores.

A seguir está uma implementação conveniente de HMAC sem AndroidKeyStore:

public enum HMACWrapper {
    HMAC_512("HMac-SHA512"), //observação: esta é a especificação para o provedor BC
    HMAC_256("HMac-SHA256");

    private final String algorithm;

    private HMACWrapper(final String algorithm) {
        this.algorithm = algorithm;
    }

    public Mac createHMAC(final SecretKey key) {
        try {
            Mac e = Mac.getInstance(this.algorithm, "BC");
            SecretKeySpec secret = new SecretKeySpec(key.getKey().getEncoded(), this.algorithm);
            e.init(secret);
            return e;
        } catch (NoSuchProviderException | InvalidKeyException | NoSuchAlgorithmException e) {
            //trate as exceções
        }
    }

    public byte[] hmac(byte[] message, SecretKey key) {
        Mac mac = this.createHMAC(key);
        return mac.doFinal(message);
    }

    public boolean verify(byte[] messageWithHMAC, SecretKey key) {
        Mac mac = this.createHMAC(key);
        byte[] checksum = extractChecksum(messageWithHMAC, mac.getMacLength());
        byte[] message = extractMessage(messageWithHMAC, mac.getMacLength());
        byte[] calculatedChecksum = this.hmac(message, key);
        int diff = checksum.length ^ calculatedChecksum.length;

        for (int i = 0; i < checksum.length && i < calculatedChecksum.length; ++i) {
            diff |= checksum[i] ^ calculatedChecksum[i];
        }

        return diff == 0;
    }

    public byte[] extractMessage(byte[] messageWithHMAC) {
        Mac hmac = this.createHMAC(SecretKey.newKey());
        return extractMessage(messageWithHMAC, hmac.getMacLength());
    }

    private static byte[] extractMessage(byte[] body, int checksumLength) {
        if (body.length >= checksumLength) {
            byte[] message = new byte[body.length - checksumLength];
            System.arraycopy(body, 0, message, 0, message.length);
            return message;
        } else {
            return new byte[0];
        }
    }

    private static byte[] extractChecksum(byte[] body, int checksumLength) {
        if (body.length >= checksumLength) {
            byte[] checksum = new byte[checksumLength];
            System.arraycopy(body, body.length - checksumLength, checksum, 0, checksumLength);
            return checksum;
        } else {
            return new byte[0];
        }
    }

    static {
        Security.addProvider(new BouncyCastleProvider());
    }
}

Outra maneira de fornecer integridade é assinar o array de bytes obtido e adicionar a assinatura ao array de bytes original.