ÍNDICE DE CONTEÚDO
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:
1 |
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:
1 |
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):
1 |
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:
1 |
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.
1 |
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:
1 |
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:
1 |
UUID=6FF1-48EF /mnt/pendrive vfat defaults,nofail, 0 0 |
Lembre-se de abrir o arquivo /etc/fstab como root, conforme exemplo abaixo:
1 |
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:
1 |
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:
1 |
sudo nano /etc/udev/rules.d/pendrive.rules |
E, no arquivo criado, insira a regra abaixo:
1 |
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:
1 |
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:
1 |
chmod +x /home/pi/scripts/update_kernel.sh |
O script é descrito a seguir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
#!/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:
1 |
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:
1 |
chmod +x gen_signed_kernel_update.sh |
Gen_signed_kernel_update.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#!/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?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#!/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.