Atualização de Kernel segura via USB no Linux embarcado

Este artigo foi escrito pelos autores Jefferson Capovilla e Pedro Bertoleti.

Introdução

Devido a sua versatilidade, suporte, robustez e ser possível sua execução em hardware com pouco poder computacional, o Linux embarcado está sendo cada vez mais utilizado em soluções eletrônicas dos mais diversos tipos e portes. 

Ainda, com a popularização das LPWAN (Low-Power Wide Area Network), soluções com conectividade fantástica em termos de alcance, consumo energético e preço foram viabilizados, e com isso também cresceu o número de soluções no conceito de Edge Computing, onde os dispositivos em campo ( = na borda, Edge) são capazes de fazer processamentos pesados dos dados e enviar para a nuvem somente poucos bytes (eventos, contadores, etc.), permitindo a união do universo Linux embarcado e das LPWANs. 

Tais soluções, infelizmente, tem um ponto negativo: inviabilidade de atualização de sistema remota (OTA). Isso acontece pois, em uma LPWAN, tipicamente o tamanho dos pacotes (tanto uplink quanto downlink) enviado em uma mensagem é restrito a algumas dezenas de bytes, com poucas mensagens ao dia. Dessa forma, fica totalmente inviável fazer uma atualização remota de algo crítico, como um update de Kernel, por exemplo. Nessas situações, muitas vezes há um único caminho para isso: atualização em campo, diretamente via USB. 

Mas como garantir a segurança nestes casos, impedindo que qualquer um insira um pendrive e descarregue qualquer software no dispositivo? Este artigo mostrará algumas técnicas básicas de garantir a segurança de update de software nestes casos, bem como sugerirá mais medidas para incrementar a segurança nestes casos.

Pré-requisitos

Para compreender de forma satisfatória este artigo, é recomendado que você possua os seguintes pré-requisitos:

  1. Ter familiaridade com shell script
  2. Ter familiaridade com Linux, sobretudo com linhas de comando / terminal
  3. Já ter compilado o Kernel Linux ao menos uma vez, para qualquer computador ou Single-Board Computer

Se você não possui algum deles, recomendo se informar sobre este e, daí sim, prosseguir neste artigo. Dessa forma, você compreenderá 100% do que é tratado no artigo.

Situação: atualização de kernel Linux em dispositivos sem acesso direto à Internet

Imagine a seguinte situação: um dispositivo eletrônico em campo, com Linux embarcado, utilizando uma LPWAN (ou seja, sem acesso direto à Internet e com grandes restrições de tamanho de dados nos pacotes) que precisa ter seu kernel atualizado. Nesta situação, conforme dito na introdução, muitas vezes há um único caminho para isso: atualização em campo, de forma presencial, diretamente via USB.

Tal solução para o update de kernel Linux pode parecer arcaica, mas acredite, em alguns casos não é. As LPWANs, além de demandarem baixíssima energia elétrica e possuírem alcance fantástico, possuem planos de conectividade baratíssimos – algumas vezes, por R$30,00/ano/dispositivo na data de escrita deste artigo, conforme pode ser visto neste link –  fazendo valer a pena “sacrificar” a flexibilidade de conectividade direta à Internet.

Sendo assim, o update via USB de forma presencial é um dos únicos caminhos possíveis para a atualização, o que implica que esta interface precisa estar habilitada no produto final. Ao mesmo tempo que soluciona o problema de atualização, cria um problema sério de segurança: e se alguém mal intencionado chegar ao dispositivo com um pendrive, por exemplo, e inserir um kernel (ou outro software qualquer) com falhas / hacks propositais? Isso pode ter consequências desastrosas, sobretudo se o dispositivo em questão for responsável pelo monitoramento e/ou controle de algo crítico. Portanto, cuidar da segurança neste tipo de atualização via USB é mais do que uma boa ideia, é fundamental.

Hardware utilizado neste artigo

Por razões de grande popularidade e permitir, portanto, fácil reprodução pelos leitores, foi escolhido como hardware-alvo (representando o dispositivo em campo) uma Raspberry Pi 3B, conforme ilustra a figura 1.

Atualização de Kernel
Figura 1 – Raspberry Pi 3B (fonte da imagem: https://www.raspberrypi.com/products/raspberry-pi-3-model-b/

Como pendrive contendo o software seguro para update do Kernel, foi utilizado um pendrive SanDisk Cruzer Blade 16GB, conforme ilustrado na figura 2.

Atualização de Kernel
Figura 2 – SanDisk Cruzer Blade 16GB

 

Além da Raspberry Pi 3B e do pendrive, também é necessário um LED (qualquer cor), um resistor de 470R / 1/4W, um protoboard (pequeno, de 400 pontos) e dois jumpers macho-fêmea.

Diagrama de funcionamento

O diagrama de funcionamento do update de Kernel proposto neste artigo encontra-se na figura 3.

Atualização de Kernel
Figura 3 – diagrama de funcionamento

Atualização de Kernel: o que é necessário?

A compilação (nesse caso, cross-compilação) do Kernel Linux, embora seja algo não muito trivial, é um processo largamente documentado. Como o hardware-alvo deste artigo é a Raspberry Pi 3B, este processo está completamente documentado no site oficial da Raspberry Pi Foundation em: https://www.raspberrypi.com/documentation/computers/linux_kernel.html .

Porém, quando se fala em update de Kernel Linux, é preciso ter em mente que os seguintes itens precisam ser atualizados:

  1. Imagem do Kernel propriamente dita (zImage)
  2. Arquivos DTB (Device-Tree Blobs)
  3. Overlays de DTB
  4. Kernel modules (arquivos .ko referentes a cada subsistema do Kernel)

Se um dos quatro itens descritos acima for esquecido no update, muito provavelmente o Kernel apresentará mau funcionamento ou, ainda, nem conseguirá ser executado (gerando Kernel Panics e afins e impedindo o boot). 

Os itens 1, 2 e 3, após a compilação seguindo o processo descrito em https://www.raspberrypi.com/documentation/computers/linux_kernel.html, são facilmente obtidos nas pastas arch/arm/boot, arch/arm/boot/dts e arch/arm/boot/dts/overlays, respectivamente. Copie-os para uma pasta à parte. No exemplo utilizado no artigo, foi utilizado o caminho /mnt/hd_dados/kernel_modules_raspberrypi/lib/module.

Já para o item 4 (Kernel modules) é necessário um procedimento além da compilação clássica. É preciso instalá-los em uma pasta intermediária do seu computador para, em um momento posterior, atualizá-los em /lib/modules do hardware-alvo. Para fazer isso, siga o procedimento abaixo:

  • Após a cross-compilação do Kernel, cria uma pasta em seu computador para receber toda a estrutura de arquivos e sub-pastas referentes aos Kernel modules. No caso do artigo, foi criada a pasta kernel_modules_raspberrypi em: /mnt/hd_dados/kernel_modules_raspberrypi. Essa pasta será utilizada nos comandos a seguir.
  • Agora, no terminal, na pasta linux, onde foi clonado o repositório do Kernel da Raspberry Pi, conforme especifica o processo de compilação, faça a instalação dos Kernel modules nesta pasta recém criada:
  • Após a execução do comando, que pode demorar 30 segundos ou mais, a depender da quantidade de Kernel modules gerados, dentro da pasta (/mnt/hd_dados/kernel_modules_raspberrypi) os Kernel modules estarão disponíveis na sub-pasta /lib/modules. Logo, os Kernel modules desejados (a serem substituídos em /lib/modules no hardware-alvo, ou seja, a Raspberry Pi 3B) estão em  /mnt/hd_dados/kernel_modules_raspberrypi/lib/modules
  • Copie a pasta /mnt/hd_dados/kernel_modules_raspberrypi/lib/modules para a mesma pasta que foi criada nos itens de 1 a 3.

Neste artigo, o arquivo de update de Kernel (ainda sem proteção de confidencialidade) consiste em um tarball com tudo descrito nos itens 1 a 4. Como exemplo, observe a figura 4.

Atualização de Kernel
Figura 4 – estrutura da pasta kernel_dtbs_modules contendo tudo que é necessário para o update de Kernel

Observação: é interessante, para maior diferenciação do antes e depois do upgrade, que o Kernel a ser atualizado seja compilado com o EXTRA contendo algum texto ou identificador familiar ao usuário. No caso do artigo, foi utilizado o texto “DCE_CyberSecurity” no EXTRA.

Gerando e cifrando o tarball de atualização de Kernel

Uma vez que todo o conteúdo necessário para ser realizada a atualização de Kernel no hardware-alvo foi obtido, é preciso gerar o tarball e cifrá-lo. Para a geração deste, assumindo que estes estejam em /mnt/hd_dados/kernel_modules_raspberrypi/, utilize com o comando abaixo:

Sinta-se livre para gerar este tarball onde desejar, desde que consiga fácil acesso a esta pasta com o tarball. No caso utilizado no artigo, ele foi gerado diretamente na home do computador, conforme mostrado no comando acima.

Agora o tarball foi gerado, será necessário cifrá-lo. Para isso, será feito uso do GPG (GnuPrivacy Guard), um conjunto de ferramentas Gnu (https://gnupg.org/) para comunicação segura, com várias opções de algoritmos criptográficos e gerenciamento de chaves. Ele é largamente utilizado em várias distribuições Linux, logo é comum já vir instalado por padrão. Caso o GPG ainda não esteja instalado no computador que será utilizado, pode-se fazer isso através do comando abaixo (comando válido para distros Ubuntu e derivadas):

Por razões de simplificação da solução, neste artigo será considerado o uso da cifra AES-256 (padrão do GPG), algoritmo hash criptográfico SHA256 (referenciado como “hash” no restante do texto), e usando como senha de proteção do pacote cifrado o Serial da Raspberry Pi 3B alvo, sendo que este serial é único para cada Raspberry Pi 3B. Para uma solução final, é recomendado aumentar a complexidade desta senha. Para obter o Serial da Raspberry Pi alvo, é necessário executar o seguinte comando no terminal da sua Raspberry Pi:

O resultado da execução deste comando é um Serial de 16 dígitos (no meu caso, este Serial é igual a 000000007a22bd7b), o qual será usado como senha de entrada para a geração da chave derivada que será usada para a cifra do pacote. Para a geração da chave derivada, é utilizada a técnica de se aplicar sucessivas operações de hash à senha, seguida da concatenação de 8 bits aleatórios denominado “salt”. Neste exemplo foi escolhido aplicar o número máximo de iterações da operação de hash (65.011.712), de forma a tornar o ataque à senha bastante custoso, pois esta técnica aumenta a entropia da chave criptográfica e diminui as chances de quebra de cifra por ataque de dicionário [1]. Esta técnica é denominada de Iterated and Salted S2K [2]. 

Para cifrar o tarball utilizando GPG e a chave citada, utilize o comando abaixo, informando como cifra o número serial recém-obtido.
Observação: serão pedidas, duas vezes, a cifra para ser usada no processo de cifração. É importante que não haja erro na digitação das mesmas, pois, caso contrário, não será possível decifrar o arquivo em um momento posterior.

O processo pode demorar alguns segundos, a depender do computador. Ao final do processo, será gerado o arquivo kernel_update.tar.gz.gpg. Será necessário copiá-lo para a raiz do pendrive, sem a utilização de sub-diretórios.

Esquemático da solução (Raspberry Pi)

O circuito esquemático (Raspberry Pi 3B + LED sinalizador de update de Kernel) pode ser visto na figura 5.

Figura 5 – circuito esquemático do projeto abordado neste artigo

Como fazer o update ser obtido e feito a partir de um pendrive autorizado para tal?

Um dos pilares para a segurança da solução aqui proposta é garantir que somente um pendrive autorizado / esperado será utilizado. Isso garante que o script de atualização de Kernel contido na Raspberry Pi 3B não vai aceitar dados de qualquer pendrive inserido. 

Para restringir o update de Kernel somente a um pendrive, a maneira mais eficaz é fazer este “filtro” a partir do UUID do pendrive. UUID é um identificador único, logo fazer essa amarração pelo UUID é uma garantia de que somente o pendrive esperado será considerado neste update. Para obter o UUID de um pendrive, execute o comando abaixo:

No artigo, o UUID do pendrive é 6FF1-48EF, conforme mostra a figura 6.

Figura 6 – UUID do pendrive (destacado em vermelho)

Ainda, na Raspberry Pi 3B (com distribuição Raspberry Pi OS), o pendrive não é montado automaticamente após inserção. Logo, na solução aqui proposta, o pendrive somente será montado se possuir o UUID autorizado ou esperado, de forma que outros pendrives não sejam nem montados, muito menos usados para inserção de programas ou scripts maliciosos. Isso é feito com auxílio do fstab, uma tabela de filesystem no Linux que descreve como cada drive / disco deve ser montado. Para colocar o pendrive no fstab, deverá ser seguido o procedimento abaixo:

  • Primeiramente é necessário criar um diretório para ser o ponto de montagem do pendrive. Neste artigo foi considerado o diretório /mnt/pendrive para isso.
  • Insira, no arquivo /etc/fstab da Raspberry Pi 3B, uma linha referente ao pendrive, vinculada ao UUID do mesmo. É importante utilizar os options defaults e nofail, de forma a permitir a montagem com todas as configurações-padrão de um disco/dispositivo de massa e, ainda, não sinalizar falha se o pendrive não estiver presente durante as montagens (no boot, por exemplo).
    Tal linha pode ser vista abaixo, considerando o UUID do pen drive utilizado no artigo, sendo necessária sua substituição para o que será configurado:


Lembre-se de abrir o arquivo /etc/fstab como root, conforme exemplo abaixo:

Ao fim do processo, o /etc/fstab ficará muito similar ao mostrado na figura 7?

Figura 7 – arquivo /etc/fstab completo

Configurando o fstab dessa maneira, para montar o pendrive com UUID desejado, é preciso, além de que ele esteja inserido, fazer a montagem de todos os discos descritos e ainda não montados, utilizando para isso o comando abaixo:

Neste ponto, é possível montar o pendrive somente quando este for o autorizado. Mas como fazer isso ser feito automaticamente, quando esse pendrive for inserido na Raspberry Pi 3B? Para isso, será utilizado o uDEV.

Conforme descrito no artigo “Utilizando o udev para criar automações com porta USB no Linux”  , o uDEV trata-se de um gerenciador de dispositivos para Linux. Na prática, o uDEV é responsável por comunicar ao sistema operacional sobre os dispositivos físicos conectados ao hardware, incluindo os USB, baseado nas informações recebidas do Kernel Linux.

Logo, será feita uma regra do uDEV responsável por, assim que for inserido o pendrive autorizado, fazer a montagem automática do mesmo e, em seguida, disparar automaticamente a execução do script de atualização de Kernel localizado em /home/pi/scripts (este script será detalhado mais à frente neste artigo). Dessa forma, basta o operador inserir o pendrive correto na Raspberry Pi para que a atualização do Kernel seja iniciada, sendo o mais prático possível.

Para criar a regra do uDEV, no terminal com a Raspberry Pi 3B, primeiramente crie o arquivo dela utilizando o comando abaixo:

E, no arquivo criado, insira a regra abaixo:

Feito isso, coloque a regra em vigor com o seguinte comando:

A partir de agora, a simples inserção do pendrive autorizado na Raspberry Pi 3B causará a atualização de Kernel.

Script de atualização de Kernel

É hora de construir o script que fará, de fato, a atualização do Kernel na Raspberry Pi 3B. Tal script é um shell script, logo é o mais “nativo” possível (não exige nada além das próprias ferramentas Linux, já instaladas por padrão na Raspberry Pi).

Quando executado, o script fará o seguinte:

  1. Sinaliza início da atualização ligando um LED (presente no GPIO 17 da Raspberry Pi 3B);
  2. Grava em tmpfs (/tmp) logs/arquivos indicando o status da atualização de Kernel
  3. Neste ponto, o pendrive já está montado em /mnt/pendrive. Logo, copia o tarball cifrado (kernel_update.tar.gz.gpg) para um diretorio no tmpfs (/tmp/kernel_update);
  4. Antes de prosseguir com a atualização, faz-se o backup do kernel, overlays e dtbs atuais (na prática, todo o conteúdo de /boot) em /home/pi/backup_kernel;
  5. Decifra e descompacta o tarball, e copia os arquivos (imagem do Kernel, DTBs, overlays e Kernel modules) para os diretórios corretos;
  6. Sinaliza fim da atualização apagando o LED;
  7. O tarball cifrado presente no pendrive é renomeado, para não ser usado pelo script novamente;
  8. O Linux reinicia, fazendo com que o próximo boot ocorra com o Kernel atualizado. Caso haja alguma falha durante a atualização, a versão antes do update é restaurada.

É necessário salvar o script como update_kernel.sh em /home/pi/scripts e, ainda, é preciso que este receba permissão de execução, utilizando para isso o comando abaixo:

O script é descrito a seguir.

Verificando o funcionamento

Nas figuras 8 e 9 estão, respectivamente, a versão de Kernel antes do update (5.15.32-v7+) e após a atualização (5.15.34DCE_CyberSecurity-v7), feito com a solução descrita neste artigo.

Figura 8 – antes do update de Kernel

Figura 9 – após update de Kernel (com a solução descrita neste artigo

Garantindo a origem e integridade do pacote de atualização

A base da segurança da informação é fundamentada em três pilares básicos: confidencialidade, integridade e disponibilidade. Mais recentemente foram incorporados também a autenticidade e irretratabilidade (não-repúdio).

A técnica de criptografia aplicada na etapa anterior garante a confidencialidade do pacote de atualização pois, caso o arquivo de atualização seja interceptado, as informações não serão facilmente decifradas sem ter a informação da senha utilizada. Também foi utilizado a boa prática de senhas únicas por dispositivo, em que a cifra de um mesmo pacote de atualização gera resultados totalmente diferentes.

A garantia de disponibilidade é facilmente obtida neste exemplo, dado que o pacote de atualização se encontra presente no pendrive e está disponível a todo momento para ser utilizado pelo sistema.

Então precisamos adicionar mecanismos de segurança de forma a garantir a integridade, autenticidade e não-repúdio do pacote de atualização. Para isso é recomendado o uso da assinatura digital. Com ela se garante que:

  • Arquivo não foi corrompido / adulterado: a verificação é feita com base no hash criptográfico da informação a ser validada, em que a mudança de um único bit faz com que o hash resultante seja totalmente diferente. 
  • Arquivo foi gerado pelo fabricante: apenas o detentor do par chave público-privada é capaz de gerar a assinatura do pacote. 

A Figura 9 mostra o fluxo de criação e a Figura 10 o fluxo de verificação da assinatura digital. Por “Arquivo”, entende-se qualquer conteúdo a ser protegido. Neste exemplo pode-se utilizar tanto o arquivo original de atualização (kernel_update.tar.gz) quanto a versão cifrada (kernel_update.tar.gz.gpg), dependendo da necessidade da aplicação em se manter as informações sigilosas ou não. Para os scripts do exemplo a seguir foi utilizado o arquivo não cifrado. 

Figura 9 – Criação da assinatura digital

Figura 10 – Verificação da assinatura digital

Script de geração da assinatura digital do arquivo de atualização

Primeiramente faça a instalação do pacote openSSL tanto no servidor de geração do pacote de atualização quanto no sistema alvo (Raspberry Pi 3B) com o comando:

Novamente, foi utilizado a linguagem shell script para a criação da assinatura digital. Ele consiste dos seguintes passos:

  1. Criação do par de chaves público-privada. Esta etapa é feita uma única vez. Atenção: Lembre-se de NUNCA compartilhar a chave privada! Ela é a base de segurança desse processo. Como saída do comando serão gerados dois arquivos:
    1. Chave privada
    2. Certificado digital. Ele contém a chave pública e várias outras informações. Para explicação detalhada, recomenda-se a leitura da referência [3].
  2. Criação da assinatura digital do arquivo de atualização.
    1. O arquivo referente à assinatura digital possui extensão .p7b.
  3. Criação do pacote de atualização, que agora contém o tarball original e o arquivo de assinatura digital associado. 

O script é descrito a seguir. Este deve ser usado no servidor de criação do pacote de atualização. Como sugestão, foi dado o nome de gen_signed_kernel_update.sh. É necessário dar permissão de execução ao script, utilizando para isso o comando abaixo:

Gen_signed_kernel_update.sh:

Script de verificação da assinatura digital do arquivo de atualização

Este script consiste dos seguintes passos:

  1. Descompacta o pacote de atualização, composto pelo arquivo de atualização e correspondente assinatura digital. 
  2. Verificação da assinatura digital do arquivo de atualização. 
    1. Caso a assinatura digital seja validada, o sistema deve usar o arquivo “kernel_update_verificado.tar.gz” como fonte para fazer a atualização do sistema (procedimento descrito em Script de atualização de Kernel)
    2. Caso a verificação falhe, o processo é abortado.

O script é descrito a seguir. Este deve ser usado no sistema a ser atualizado (Raspberry Pi 3B). Como sugestão, foi dado o nome de extract_verify_kernel_update.sh. Não esqueça de dar permissão de execução (chmod +x extract_verify_kernel_update.sh).

Extract_verify_kernel_update.sh?

Considerações finais

Este artigo teve por objetivo demonstrar uma técnica de atualização segura para sistemas com grande restrições de conectividade, e portanto, que dependem de uma interação física para se fazer a atualização. Dado que tais equipamentos ficam localizados nos mais diversos lugares, e passíveis de ataques com acesso físico ao equipamento, é essencial garantir a segurança do equipamento como um todo. 

Vale ressaltar que a técnica aqui descrita é necessária, porém não suficiente no processo de atualização segura de um equipamento. Além de garantir a segurança da informação utilizada na atualização, é necessário também garantir a robustez do processo. Dentre os desafios estão:

  • Atualização robusta a interrupção de energia: o sistema deve ter mecanismos de recuperação no caso de interrupção de energia durante a atualização. No exemplo do artigo, o que poderia acontecer caso o dispositivo fosse desligado durante a cópia dos arquivos referentes ao Kernel e drivers?
  • Tempo necessário para atualização: qual o tempo máximo aceitável para se aplicar a atualização? Deve-se ponderar que sistemas embarcados possuem as mais diversas restrições de processamento, memória e armazenamento. Além disso, quanto maior o tempo para a atualização, mais eventos indesejáveis podem ocorrer (queda energia, intempéries, instabilidade do sistema, etc). 
  • Recuperação do sistema em caso de falha na atualização: o sistema deve ter a capacidade de se recuperar caso a nova versão do software possua problemas durante a inicialização. Como exemplo imagine que a nova versão do Kernel causou problema na inicialização. Uma possível técnica contra isso é chamada de atualização A/B, em que são salvos localmente e de maneira independente a versão antiga e a atualizada do Kernel. Caso o uso deste atualizado falhar durante a inicialização, é feito o retorno para a versão antiga. 

A proteção com base em assinatura digital também pode ser utilizada para garantir a integridade do sistema, em que apenas imagens geradas pelo fabricante serão carregadas no dispositivo, tais como o preloader, bootloader e Kernel. Para isso, recomenda-se a leitura sobre boot seguro e o uso de TPM (Trusted Platform Module) [4] para o gerenciamento de chaves e verificação de integridade do sistema.

Referências

Saiba Mais

Utilizando watchdog no Linux embarcado

Extensão do Visual Studio Code para Kernel Linux Embarcado

Quando escolher Linux Embarcado?

JUNTE-SE HOJE À COMUNIDADE EMBARCADOS

Licença Creative Commons Esta obra está licenciada com uma Licença Creative Commons Atribuição-CompartilhaIgual 4.0 Internacional.
Home » Linux Embarcado » Atualização de Kernel segura via USB no Linux embarcado
Comentários:
Notificações
Notificar
guest
1 Comentário
recentes
antigos mais votados
Inline Feedbacks
View all comments
Yago Caetano
Membro
24/06/2022 20:10

Artigo excelente!
Parabéns aos autores.

Talvez você goste:
Nenhum resultado encontrado.