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:
- Ter familiaridade com shell script
- Ter familiaridade com Linux, sobretudo com linhas de comando / terminal
- 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.
Como pendrive contendo o software seguro para update do Kernel, foi utilizado um pendrive SanDisk Cruzer Blade 16GB, conforme ilustrado na figura 2.
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: 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:
- Imagem do Kernel propriamente dita (zImage)
- Arquivos DTB (Device-Tree Blobs)
- Overlays de DTB
- 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:
sudo env PATH=$PATH make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=/mnt/hd_dados/kernel_modules_raspberrypi modules_install
- 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.
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:
tar -cvzf ~/kernel_update.tar.gz kernel_dtbs_modules/
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):
sudo apt-get install gpg
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:
cat /proc/cpuinfo | grep Serial | awk '{print $3}'
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.
gpg -c --force-mdc --s2k-mode 3 --s2k-count 65011712 kernel_update.tar.gz
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.
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:
sudo blkid
No artigo, o UUID do pendrive é 6FF1-48EF, conforme mostra a figura 6.
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:
UUID=6FF1-48EF /mnt/pendrive vfat defaults,nofail, 0 0
Lembre-se de abrir o arquivo /etc/fstab como root, conforme exemplo abaixo:
sudo nano /etc/fstab
Ao fim do processo, o /etc/fstab ficará muito similar ao mostrado na figura 7?
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:
sudo mount -a
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:
sudo nano /etc/udev/rules.d/pendrive.rules
E, no arquivo criado, insira a regra abaixo:
ACTION=="add", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", ENV{ID_FS_UUID}=="6FF1-48EF", RUN+="/usr/bin/mount -a 2> /tmp/erro_montagem.txt", RUN+="/home/pi/scripts/update_kernel.sh"
Feito isso, coloque a regra em vigor com o seguinte comando:
sudo udevadm control --reload-rules ; sudo udevadm trigger
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:
- Sinaliza início da atualização ligando um LED (presente no GPIO 17 da Raspberry Pi 3B);
- Grava em tmpfs (/tmp) logs/arquivos indicando o status da atualização de Kernel
- 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);
- 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;
- Decifra e descompacta o tarball, e copia os arquivos (imagem do Kernel, DTBs, overlays e Kernel modules) para os diretórios corretos;
- Sinaliza fim da atualização apagando o LED;
- O tarball cifrado presente no pendrive é renomeado, para não ser usado pelo script novamente;
- 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:
chmod +x /home/pi/scripts/update_kernel.sh
O script é descrito a seguir.
#!/bin/bash
# Script de atualizacao de Kernel
# Este script executa automaticamente ao se inserir o pendrive
# registrado / autorizado na regra do UDEV /etc/udev/rules.d/pendrive.rules.
# Esta regra do UDEV tambem eh responsavel por montar o pendrive em /mnt/pendrive.
#
# As suas açoes detalhadas sao:
# 1) Sinalizar inicio da atualizacao ligando um LED
# 2) Gravar em tmpfs (/tmp) logs/arquivos indicando os status das operacoes
# 3) Copiar o kernel, overlays e dtbs compactados e cifrados do pendrive
# para um diretorio no tmpfs (/tmp/kernel_update)
# 4) Fazer backup do kernel, overlays e dtbs ANTES da atualizacao em /home/pi/backup_kernel
# 5) Descifrar e descompactar o kernel, overlays e dtbs e copia-los para
# 6) seus respectivos locais em /boot
# 7) Sinalizar fim da atualizacao apagando um LED
# 8) O pendrive eh desmontado e pode ser removido
# 9) O Linux reinicia com o kernel atualizado
CMD_DATA_HORA="$(date)"
DIR_KERNEL_UPD_COPIADO="/tmp/kernel_update/"
DIR_KERNEL_UPD_DECIFRADO="/tmp/kernel_update/kernel_dtbs_modules/"
DIR_BACKUP_KERNEL_overlays_DTBS="/home/pi/backup_kernel/"
ARQ_SERIAL="/tmp/serial.txt"
GPIO_LED=17
# Funçao que volta o backup do Kernel (conteudo integral da pasta /boot)
volta_backup_kernel()
{
tar -xf "$DIR_BACKUP_KERNEL_overlays_DTBS"backup_kernel_overlays_dtbs.tar.gz --directory /boot
}
# Funçao que prepara o GPIO do LED para operar como saida
inicializa_led()
{
echo "$GPIO_LED" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio17/direction
}
# Funçao que libera o GPIO para uso
libera_gpio_led()
{
echo 17 > /sys/class/gpio/unexport
}
# Funçao para ligar o LED
liga_led()
{
echo 1 > /sys/class/gpio/gpio17/value
}
# Funçao para desligar o LED
desliga_led()
{
echo 0 > /sys/class/gpio/gpio17/value
}
##########
# Script #
##########
# Este script deve ser rodado como root. Caso contrario, nada eh feito
if [ "$EUID" -ne 0 ]; then
MSG="$CMD_DATA_HORA: [ERRO] O script precisa ser executado como root. Script terminado com falha."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
exit
fi
echo "Script sendo executado como root..."
# Prepara diretorios
if ! [ -d "$DIR_KERNEL_UPD_COPIADO" ]; then
mkdir "$DIR_KERNEL_UPD_COPIADO"
fi
# Inicializa script de update de Kernel, LED e liga o LED
touch /tmp/status_update_kernel.txt
MSG="$CMD_DATA_HORA: Script de update de Kernel iniciado"
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
inicializa_led
liga_led
# Copia kernel, overlays e dtbs para diretorio estipulado
cp /mnt/pendrive/kernel_update.tar.gz.gpg "$DIR_KERNEL_UPD_COPIADO"
if ! [ $? -eq 0 ]; then
MSG="$CMD_DATA_HORA: [ERRO] Falha ao copiar kernel, overlays e dtbs do pendrive. Script terminado com falha."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
desliga_led
libera_gpio_led
exit
fi
MSG="$CMD_DATA_HORA: Kernel, overlays e dtbs copiados do pendrive com sucesso."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
# Decifra e descompacta kernel, overlays e dtbs.
# A cifra eh o sha256 do serial da Raspberry Pi 3B
cat /proc/cpuinfo | grep Serial | awk '{print $3}' > "$ARQ_SERIAL"
CIFRA_CRIPTO="$(sha256sum "$ARQ_SERIAL" | awk '{print $1}')"
gpg --passphrase "$CIFRA_CRIPTO" --batch --yes --force-mdc --s2k-mode 3 --s2k-count 65011712 --output "$DIR_KERNEL_UPD_COPIADO"kernel_update.tar.gz --decrypt kernel_update.tar.gz.gpg
if ! [ $? -eq 0 ]; then
MSG="$CMD_DATA_HORA: [ERRO] Falha ao decifrar. Script terminado com falha."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
desliga_led
libera_gpio_led
exit
fi
MSG="$CMD_DATA_HORA: Kernel, overlays e dtbs descriptografados com sucesso"
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
tar -xf "$DIR_KERNEL_UPD_COPIADO"kernel_update.tar.gz --directory "$DIR_KERNEL_UPD_COPIADO"
if ! [ $? -eq 0 ]; then
MSG="$CMD_DATA_HORA: [ERRO] Falha ao descompactar. Script terminado com falha."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
desliga_led
libera_gpio_led
exit
fi
MSG="$CMD_DATA_HORA: Kernel, overlays e dtbs descompactados com sucesso"
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
# Faz backup do kernel, overlays e dtbs atuais (pasta boot inteira)
mkdir "$DIR_BACKUP_KERNEL_overlays_DTBS"
tar -czf "$DIR_BACKUP_KERNEL_overlays_DTBS"backup_kernel_overlays_dtbs.tar.gz /boot
if ! [ $? -eq 0 ]; then
MSG="$CMD_DATA_HORA: [ERRO] Falha ao fazer backup do kernel, overlays e dtbs atuais. Script terminado com falha."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
desliga_led
libera_gpio_led
exit
fi
MSG="$CMD_DATA_HORA: Backup do kernel, overlays e dtbs atuais feito com sucesso"
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
# Faz update de Kernel, overlays e dtbs
cp "$DIR_KERNEL_UPD_DECIFRADO"zImage /boot/kernel7.img
if ! [ $? -eq 0 ]; then
volta_backup_kernel
MSG="$CMD_DATA_HORA: [ERRO] Falha ao copiar imagem do Kernel. Backup restaurado. Script terminado com falha."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
desliga_led
libera_gpio_led
exit
fi
MSG="$CMD_DATA_HORA: Imagem do Kernel copiada com sucesso"
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
cp "$DIR_KERNEL_UPD_DECIFRADO"*.dtb /boot/
if ! [ $? -eq 0 ]; then
volta_backup_kernel
MSG="$CMD_DATA_HORA: [ERRO] Falha ao copiar arquivos .dtb. Backup restaurado. Script terminado com falha."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
desliga_led
libera_gpio_led
exit
fi
MSG="$CMD_DATA_HORA: Arquivos .dtb copiados com sucesso"
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
cp "$DIR_KERNEL_UPD_DECIFRADO"overlays/*.dtb* /boot/overlays/
if ! [ $? -eq 0 ]; then
volta_backup_kernel
MSG="$CMD_DATA_HORA: [ERRO] Falha ao copiar overlays. Backup restaurado. Script terminado com falha."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
desliga_led
libera_gpio_led
exit
fi
MSG="$CMD_DATA_HORA: Overlays copiados com sucesso"
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
# Finaliza script de update de Kernel, libera GPIO do LED e desliga LED
MSG="$CMD_DATA_HORA: Script de update de Kernel finalizado."
echo "$MSG" >> /tmp/status_update_kernel.txt
echo "$MSG"
desliga_led
libera_gpio_led
# reboot
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.
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.
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:
sudo apt-get install openssl
Novamente, foi utilizado a linguagem shell script para a criação da assinatura digital. Ele consiste dos seguintes passos:
- 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:
- Chave privada
- 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].
- Criação da assinatura digital do arquivo de atualização.
- O arquivo referente à assinatura digital possui extensão .p7b.
- 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:
chmod +x gen_signed_kernel_update.sh
Gen_signed_kernel_update.sh:
#!/bin/bash
#Esse script deve ser executado no servidor de geração do pacote de atualização
CERTIFICADO=./meu_certificado.crt
CHAVE_PRIVADA=./chave_privada.pem
KERNEL_UPDATE_FILE=./kernel_update.tar.gz
ASSINATURA_DIGITAL=./kernel_update.p7b
KERNEL_ASSINATURA_PACK=./kernel_update_pack.tar.gz
#Criação do certificado self-signed (sem envio para autoridade certificadora) - O par de chaves deve ser criado uma única vez!
if [ -f "$CERTIFICADO" ]; then
echo "Certificado identificado!"
else
echo "Gerando certificado"
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout chave_privada.pem -out meu_certificado.crt
#Verificação do certificado gerado
openssl x509 -in meu_certificado.crt -text -noout > info_certificado.txt
echo "Informações certificado em: info_certificado.txt"
fi
#Criar assinatura referente ao arquivo de atualização (arquivo p7b)
if [ -f "$KERNEL_UPDATE_FILE" ]; then
echo "Gerando assinatura digital do $KERNEL_UPDATE_FILE..."
openssl smime -sign -binary -in $KERNEL_UPDATE_FILE -signer $CERTIFICADO -inkey $CHAVE_PRIVADA -outform der -out $ASSINATURA_DIGITAL
echo "ok!"
else
echo "ERRO: Arquivo $KERNEL_UPDATE_FILE não encontrado!"
exit 1
fi
#Compactar o arquivo de atualização e a assinatura em um único pacote
echo "Compactando arquivo update e assinatura em $$KERNEL_ASSINATURA_PACK"
tar -cvzf $KERNEL_ASSINATURA_PACK $KERNEL_UPDATE_FILE $ASSINATURA_DIGITAL
echo "OK! Copiar o arquivo $KERNEL_ASSINATURA_PACK para o pendrive"
Script de verificação da assinatura digital do arquivo de atualização
Este script consiste dos seguintes passos:
- Descompacta o pacote de atualização, composto pelo arquivo de atualização e correspondente assinatura digital.
- Verificação da assinatura digital do arquivo de atualização.
- 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)
- 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?
#!/bin/bash
KERNEL_UPDATE_FILE=/tmp/kernel_update.tar.gz
ASSINATURA_DIGITAL=/tmp/kernel_update.p7b
KERNEL_ASSINATURA_PACK=/tmp/kernel_update_pack.tar.gz
KERNEL_UPDATE_FILE_VERIFICADO=/tmp/kernel_update_verificado.tar.gz
ASSINATURA_PKCS7=/tmp/kernel_update.pkcs7
#Esse script deve ser executado no dispositivo a ser atualizado (Raspberry Py)
cd /tmp
#descompacta Kernel pack
tar -xvf $KERNEL_ASSINATURA_PACK ./
#Verifica a assinatura digital do arquivo
echo "Verificando assinatura digital do $KERNEL_UPDATE_FILE"
openssl smime -verify -binary -inform der -in $ASSINATURA_DIGITAL -content $KERNEL_UPDATE_FILE -noverify > $KERNEL_UPDATE_FILE_VERIFICADO 2> error.txt
VERIF_OUTPUT=`grep -i "fail" error.txt`
#Remove o pacote gerado na etapa anterior se a verificação de assinatura falhar
if [ -n "${VERIF_OUTPUT}" ]; then
echo "Erro na verificação da assinatura digital!"
rm $KERNEL_UPDATE_FILE_VERIFICADO
exit 1
fi
echo "Sucesso!!"
echo "Usar o arquivo $KERNEL_UPDATE_FILE_VERIFICADO para aplicar as atualizacoes. "
#opcional (Verificar o gerador da assinatura)
echo
echo "Informações sobre o gerador do pacote"
openssl pkcs7 -inform der -in $ASSINATURA_DIGITAL -out $ASSINATURA_PKCS7
openssl pkcs7 -print_certs -in $ASSINATURA_PKCS7 |& grep -i "issuer"
cd -
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
- [1]: Dictionary-based Password Attack – https://capec.mitre.org/data/definitions/16.html
- [2]: Iterated and Salted S2K – https://www.rfc-editor.org/rfc/rfc4880#section-3.7.1.3
- [3]: How to use OpenSSL: Hashes, digital signatures, and more –
- https://opensource.com/article/19/6/cryptography-basics-openssl-part-2
- [4]: Introduction to the Secure Boot Chain – https://edk2-docs.gitbook.io/understanding-the-uefi-secure-boot-chain/overview/introduction-to-the-secure-boot-chain
Saiba Mais
Utilizando watchdog no Linux embarcado






Artigo excelente!
Parabéns aos autores.