Engenharia reversa de um código QR para prova de vacinação

imagem


Quando Quebec anunciou que enviaria e-mails de confirmação de vacinação para todos que foram vacinados usando o código QR em anexo, meus joelhos dobraram um pouco. Eu estava ansioso para desmontá-lo e sacudir minha cabeça com a quantidade de informações privadas de saúde que sem dúvida serão reveladas no processo.



Minha confirmação de vacinação finalmente chegou, e o resultado é ... nada mal. No entanto, sempre há diversão em hacks de conhecimento zero, então decidi fazer um blog sobre minha experiência de qualquer maneira.



Minha primeira impressão foi: "Meu Deus, este é um código QR desnecessariamente grande." Não há muitas informações listadas no código QR, então eles provavelmente criptografam todos os tipos de informações pessoais sem meu conhecimento. Você sabe, como aquele código de barras na parte de trás da sua carteira de motorista .



Naturalmente, a primeira coisa que fiz foi escanear o código usando o aplicativo QRcode.



resultado
shc:/567629000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413774



Interessante. Achei que haveria o bom e velho JSON em formato binário, mas era diferente. Parece que codificar um monte de dígitos em base64 é ineficiente, mas eles conseguiram amontoar tudo em um código QR.



Infelizmente, é aqui que termina a parte de conhecimento zero do processo, porque tenho um indicador bastante claro de para onde ir a seguir: o esquema de URI. É claro que se trata de comunicação com algum aplicativo no dispositivo da pessoa que verifica o código que irá se cadastrar para processar este esquema shc :



. Mas qual é esse esquema?



Uma pequena pesquisa me levou ao Big Book O 'URI Schemes da IANA, onde shc



listado como pré-registrado sob o nome SMART Health Cards Framework. Portanto, não é apenas algo que o governo de Quebec inventou em movimento, é realmente parte de um projeto real! Isso é encorajador e inesperado.



Acontece que esse formato tem uma documentação extensa e objetivos de design muito sensatos , o que considero um alívio para o detentor desse código e um pouco frustrante quando alguém está prestes a analisá-lo por completo. Mas não importa! Tenho um código e um documento a seguir, então vamos remover a tampa e dar uma olhada dentro.



De acordo com o doc, usar o modo numérico para codificar dados de código QR fornece densidade de dados um pouco maior do que usar o modo binário, o que explica o URI de número gigante em vez da string codificada em base64 mais sensível. O primeiro enigma está resolvido.



A longa sequência de números parece ser codificada a partir de uma sequência ASCII, onde cada par de dígitos é um número decimal que é o código do caractere. Para tornar as coisas ainda mais confusas, a saída é calculada usando Ord © -45 . É hora de escrever um script para reverter esse processo.



php -r '$o = ""; foreach (str_split(preg_replace("/[^0-9]/", "", file_get_contents("php://stdin")), 2) as $c) $o .= chr($c + 45); echo $o;' <input.txt | xxd

00000000: 6579 4a72 6157 5169 4f69 4a73 4d33 6c79  eyJraWQiOiJsM3ly
00000010: 5254 4632 526a 646d 6157 5270 6257 5649  RTF2RjdmaWRpbWVI
...
000003b0: 3561 6876 5265 336d 6368 7335 7836 4e49  5ahvRe3mchs5x6NI
000003c0: 4669 3556 5277                           Fi5VRw
      
      





Várias coisas podem ser aprendidas com isso. Primeiro, é óbvio que o PHP ainda é minha linguagem de programação rápida. Infelizmente, colocaremos essa revelação pessoal de lado para uma introspecção posterior.



Do ponto de vista técnico, tudo agora parece strings codificadas em base64. E, claro, o documento me diz que devo procurar o JWS, ou seja, um token da web assinado por JSON.



Vou fazer uma pausa e dizer que este é, na verdade, um ótimo caso de uso para o JWT. Basicamente, em vez de algum token sem sentido ou bloco gigante de dados confidenciais, o conceito JWT implica que devo esperar uma lista de permissões a que tenho direito, embrulhada em um blob que é assinado criptograficamente pelo emissor (neste caso, Quebec Santé et Services sociaux).



O bom desse modelo é que ele pode ser verificado por qualquer pessoa com a chave pública correspondente, mesmo sem uma conexão com a Internet. Além disso, a resposta à pergunta "essa pessoa tem o direito de embarcar em uma aeronave / assistir a um show / visitar uma residência para idosos?" deve responder diretamente inline, não implicitamente implícito por meio da API proprietária ou um monte de campos secretos relacionados aos números de lote da vacina, etc.



Agora não tenho uma cópia da chave pública correspondente, mas o corpo deve ser assinado, não criptografado, então eu ainda posso ler.



Talvez, no espírito da engenharia reversa, eu deva desmontar manualmente o JWS, mas esta é uma especificação bastante bem documentada (e, mais importante, bem implementada). Vou usar o pacote web-token / jwt-framework Composer para isso .



$ composer require web-token/jwt-framework
      
      







<?php
require_once(__DIR__.'/vendor/autoload.php');

use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Jose\Component\Signature\Serializer\CompactSerializer;

$serializerManager = new JWSSerializerManager([
    new CompactSerializer(),
]);

$input_raw = file_get_contents('php://stdin');
$input_token = implode(
    array_map(
        function ($ord) { return chr($ord + 45); },
        str_split(preg_replace('/[^0-9]+/', '', $input_raw), 2)
    )
);

$jws = $serializerManager->unserialize($input_token);
var_dump($jws);
      
      





$ cat input.txt | php parse.php
object(Jose\Component\Signature\JWS)#5 (4) {
  ["isPayloadDetached":"Jose\Component\Signature\JWS":private]=>
  bool(false)
  ["encodedPayload":"Jose\Component\Signature\JWS":private]=>
  string(772) "hVNhb9..."
  ["signatures":"Jose\Component\Signature\JWS":private]=>
  array(1) {
    [0]=>
    object(Jose\Component\Signature\Signature)#6 (4) {
      ["encodedProtectedHeader":"Jose\Component\Signature\Signature":private]=>
      string(106) "eyJraW..."
      ["protectedHeader":"Jose\Component\Signature\Signature":private]=>
      array(3) {
        ["kid"]=>
        string(43) "l3yrE1..."
        ["zip"]=>
        string(3) "DEF"
        ["alg"]=>
        string(5) "ES256"
      }
      ["header":"Jose\Component\Signature\Signature":private]=>
      array(0) {
      }
      ["signature":"Jose\Component\Signature\Signature":private]=>
      string(64) "�Q�..."
    }
  }
  ["payload":"Jose\Component\Signature\JWS":private]=>
  string(579) "�Sao..."
}
      
      





Então, nós decodificamos com sucesso o cabeçalho, mas nenhum corpo chega. A dica aqui é "zip": "DEF" no cabeçalho, como também indicado nas especificações.



a carga útil é compactada usando o algoritmo DEFLATE (consulte RFC1951) antes de assinar (observe, isso deve ser compactação DEFLATE bruta, sem nenhum cabeçalho zlib ou gz




Vamos tentar:



echo json_encode(json_decode(gzinflate($jws->getPayload())), JSON_PRETTY_PRINT);
      
      





NB: decodificamos e, em seguida, recodificamos o objeto JSON para adicionar espaço em branco para facilitar a leitura, especificando a constante JSON_PRETTY_PRINT



{
    "iss": "https:\/\/covid19.quebec.ca\/PreuveVaccinaleApi\/issuer",
    "iat": 1621476457,
    "vc": {
        "@context": [
            "https:\/\/www.w3.org\/2018\/credentials\/v1"
        ],
        "type": [
            "VerifiableCredential",
            "https:\/\/smarthealth.cards#health-card",
            "https:\/\/smarthealth.cards#immunization",
            "https:\/\/smarthealth.cards#covid19"
        ],
        "credentialSubject": {
            "fhirVersion": "1.0.2",
            "fhirBundle": {
                "resourceType": "Bundle",
                "type": "Collection",
                "entry": [
                    {
                        "resource": {
                            "resourceType": "Patient",
                            "name": [
                                {
                                    "family": [
                                        "Paulson"
                                    ],
                                    "given": [
                                        "Mikkel"
                                    ]
                                }
                            ],
                            "birthDate": "1987-xx-xx",
                            "gender": "Male"
                        }
                    },
                    {
                        "resource": {
                            "resourceType": "Immunization",
                            "vaccineCode": {
                                "coding": [
                                    {
                                        "system": "http:\/\/hl7.org\/fhir\/sid\/cvx",
                                        "code": "208"
                                    }
                                ]
                            },
                            "patient": {
                                "reference": "resource:0"
                            },
                            "lotNumber": "xxxxxx",
                            "status": "Completed",
                            "occurrenceDateTime": "2021-xx-xxT04:00:00+00:00",
                            "location": {
                                "reference": "resource:0",
                                "display": "xxxxxxxxxxxxxxxxxx"
                            },
                            "protocolApplied": {
                                "doseNumber": 1,
                                "targetDisease": {
                                    "coding": [
                                        {
                                            "system": "http:\/\/browser.ihtsdotools.org\/?perspective=full&conceptId1=840536004",
                                            "code": "840536004"
                                        }
                                    ]
                                }
                            },
                            "note": [
                                {
                                    "text": "PB COVID-19"
                                }
                            ]
                        }
                    }
                ]
            }
        }
    }
}
      
      





Há um pouco mais de informações pessoais lá do que o estritamente necessário, embora eu acredite que combinar nome e data de nascimento com identidade com foto seja um processo sensato. Eles também fornecem informações específicas sobre vacinas, em vez de aprovações específicas, como eu esperava. Novamente, isso o torna ainda mais utilizável em todas as jurisdições e elimina a necessidade de relançar o JWS toda vez que a política muda, o que no caso de Quebec acontece cerca de duas vezes por semana.



Ao longo desta análise, perguntei-me o que pode impedir alguém de simplesmente apresentar uma prova perfeitamente válida da vacinação de outra pessoa. Como todo o corpo está criptograficamente assinado, você não pode alterar o comprovante de vacinação de outra pessoa para adicionar seu nome, o que significa que combinar o comprovante de vacinação com uma identidade com foto é um plano perfeitamente razoável. Esse certamente será o caso em aeroportos, mas eu duvido muito que em instalações esportivas, etc. E. Solicitará um segundo ID. Eles irão simplesmente escanear o código QR, ver uma marca de seleção em seu dispositivo e passar para o próximo.



Um pensamento de despedida: embora meu processo fosse voltado para descobrir quais dos meus dados pessoais estão codificados em um código QR, o modelo JWT é conhecido por ser fácil de bagunçar, seja por esquecer de validar antes de analisar os dados ou por permitir dados não assinados tokens ... Se as implementações não respeitarem uma lista branca central de signatários autorizados, seria trivialmente fácil criar um token perfeitamente válido que você assina com sua própria chave. Como sempre, a segurança do modelo realmente depende de quão rigorosamente a parte confiável aplica o padrão.



No entanto, verifica-se que as únicas informações pessoais são exatamente as informações contidas no documento PDF completo sobre vacinas: nome, data de nascimento, sexo (por algum motivo), bem como informações sobre a data e as doses específicas que o proprietário recebeu nos dias de hoje. Quando estiver confortável com as implicações de privacidade de apresentar sua carteira de motorista em um bar, você não precisa mais se preocupar em ser solicitado a mostrar o comprovante de vacinação.



O código é um monte de lixo, mas se você quiser ver o que está em seu próprio código QR, pode verificar o repositório GitHub para esta postagem.



All Articles