Nas últimas duas semanas, vimos uma série de artigos falando sobre o que foi descrito como um "crack de senha mestra" no popular gerenciador de senhas de código aberto KeePass.
O bug foi considerado importante o suficiente para obter um identificador oficial do governo dos EUA (é conhecido como CVE-2023-32784, se você quiser procurá-lo), e dado que a senha mestra para o seu gerenciador de senhas é praticamente a chave para todo o seu castelo digital, você pode entender porque a história provocou tanto entusiasmo.
A boa notícia é que um invasor que quisesse explorar esse bug quase certamente precisaria já ter infectado seu computador com malware e, portanto, seria capaz de espionar as teclas digitadas e os programas em execução de qualquer maneira.
Em outras palavras, o bug pode ser considerado um risco facilmente gerenciado até que o criador do KeePass saia com uma atualização, que deve aparecer em breve (no início de junho de 2023, aparentemente).
Como o divulgador do bug tem o cuidado de fazem notar, neste artigo:
Se você usar criptografia de disco completo com uma senha forte e seu sistema estiver [livre de malware], você deve ficar bem. Ninguém pode roubar suas senhas remotamente pela internet apenas com essa descoberta.
Os riscos explicados
Fortemente resumido, o bug se resume à dificuldade de garantir que todos os vestígios de dados confidenciais sejam eliminados da memória assim que você terminar com eles.
Ignoraremos aqui os problemas de como evitar ter dados secretos na memória, mesmo que brevemente.
Neste artigo, queremos apenas lembrar aos programadores em todos os lugares que o código aprovado por um revisor preocupado com a segurança com um comentário como “parece limpar corretamente depois de si mesmo”…
…pode, de fato, não ser completamente limpo, e o possível vazamento de dados pode não ser óbvio a partir de um estudo direto do próprio código.
Simplificando, a vulnerabilidade CVE-2023-32784 significa que uma senha mestra do KeePass pode ser recuperável dos dados do sistema mesmo após o encerramento do programa KeyPass, porque informações suficientes sobre sua senha (embora não sejam realmente a própria senha bruta, na qual focaremos em um momento) pode ficar para trás na troca do sistema ou arquivos de suspensão, onde a memória do sistema alocada pode acabar salva para mais tarde.
Em um computador Windows onde o BitLocker não é usado para criptografar o disco rígido quando o sistema está desligado, isso daria a um bandido que roubou seu laptop uma chance de inicializar a partir de uma unidade USB ou CD e recuperar sua senha mestra mesmo embora o próprio programa KeyPass cuide para nunca salvá-lo permanentemente em disco.
Um vazamento de senha de longo prazo na memória também significa que a senha poderia, em teoria, ser recuperada de um despejo de memória do programa KeyPass, mesmo que esse despejo tenha sido obtido muito depois de você digitar a senha e muito depois do KeePass em si não tinha mais necessidade de mantê-lo por perto.
Claramente, você deve assumir que o malware já instalado em seu sistema pode recuperar quase qualquer senha digitada por meio de uma variedade de técnicas de espionagem em tempo real, desde que estejam ativas no momento em que você digitou. Mas você pode razoavelmente esperar que seu tempo exposto ao perigo seja limitado ao breve período de digitação, não estendido a muitos minutos, horas ou dias depois, ou talvez mais, inclusive depois que você desligar o computador.
O que fica para trás?
Portanto, pensamos em dar uma olhada de alto nível em como os dados secretos podem ser deixados para trás na memória de maneiras que não são diretamente óbvias no código.
Não se preocupe se você não for um programador - vamos simplificar e explicar conforme avançamos.
Começaremos examinando o uso e a limpeza da memória em um programa C simples que simula a entrada e o armazenamento temporário de uma senha fazendo o seguinte:
- Alocando um pedaço dedicado de memória especialmente para armazenar a senha.
- Inserindo uma string de texto conhecida para que possamos encontrá-lo facilmente na memória, se necessário.
- Acrescentando 16 caracteres ASCII pseudo-aleatórios de 8 bits da faixa AP.
- Imprimindo o buffer de senha simulado.
- Liberando a memória na esperança de eliminar o buffer de senha.
- Saindo o programa.
Bastante simplificado, o código C pode se parecer com isso, sem verificação de erros, usando números pseudo-aleatórios de baixa qualidade da função de tempo de execução C rand()
, e ignorando qualquer verificação de estouro de buffer (nunca faça nada disso em código real!):
// Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);
Na verdade, o código que finalmente usamos em nossos testes inclui alguns bits e peças adicionais mostrados abaixo, para que possamos despejar todo o conteúdo de nosso buffer de senha temporário conforme o usamos, para procurar por conteúdo indesejado ou restante.
Observe que despejamos deliberadamente o buffer depois de chamar free()
, que é tecnicamente um bug de uso após liberação, mas estamos fazendo isso aqui como uma maneira sorrateira de ver se algo crítico é deixado para trás depois de devolver nosso buffer, o que pode levar a um vazamento de dados perigoso na vida real.
Também inserimos dois Waiting for [Enter]
solicita no código para nos dar a chance de criar despejos de memória em pontos-chave do programa, fornecendo dados brutos para pesquisar mais tarde, a fim de ver o que foi deixado para trás enquanto o programa era executado.
Para fazer despejos de memória, usaremos o Microsoft ferramenta sysinternals procdump
com o -ma
opção (despejar toda a memória), o que evita a necessidade de escrever nosso próprio código para usar o Windows DbgHelp
sistema e sua complexidade MiniDumpXxxx()
funções.
Para compilar o código C, usamos nossa própria versão pequena e simples do código aberto e gratuito de Fabrice Bellard Minúsculo Compilador C, disponível para Windows de 64 bits em fonte e forma binária diretamente da nossa página do GitHub.
O texto copiado e colado de todo o código-fonte retratado no artigo aparece na parte inferior da página.
Isto é o que aconteceu quando compilamos e executamos o programa de teste:
C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard Despojado por Paul Ducklin para uso como uma ferramenta de aprendizado Versão petcc64-0.9.27 [0006] - Gera 64 bits Somente PEs -> unl1.c -> c:/users/duck/tcc/petccinc/stdio.h [. . . .] -> c:/users/duck/tcc/petcclib/libpetcc1_64.a -> C:/Windows/system32/msvcrt.dll -> C:/Windows/system32/kernel32.dll -------- ----------------------- virt file size section 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 bytes) C:UsersduckKEYPASS> unl1.exe Despejando 'novo' buffer no início 00F51390: 90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P....... 00F513A0: 73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 haste32cmd. exe.D 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F513 0F42: 52 4 57F 53 45 52 5 41F 50 50 5 50F 52 4 46F 00 BROWSER_APP_PROF 51400F49: 4 45C 5 53F 54 52 49 4 47E 3 49D 6 74E 65 72 00 ILE_STRING=Inter 51410F6: 65E 74 20 45 78 70 6 7C 56A 4 F3 4C AC 00B 00 00 líquido ExplzV.< .K.. A string completa era: improváveltextoJHKNEJJCPOMDJHAN 51390F75: 6 6E 69C 6 65B 6 79C 74 65 78 74 4 48A 4 4B 00E improváveltextoJHKN 513F0A45: 4 4A 43A 50 4 4F 44D 4 48 A 41 4 00E 65 00 44 00 EJJCPOMDJHAN.eD 513F0B72 : 69 76 65 72 44 61 74 61 3 43D 3 5A 57C 69 6 00E riverData=C:Win 513F0C64: 6 77F 73 5 53C 79 73 74 65 6 33D 32 5 44C 72 32 dowsSystem00Dr 513F0D69: 76 65 72 73 5 44C 72 69 76 65 72 44 61 74 61 00 iversDriverData 513F0E00: 45 46 43 5 34F 33 37 32 3 31D 00 46 50 53 5 4372F .EFC_1=00.FPS_ 513F0F42: 52 4 57F 53 45 52 5 41F 50 50 5 50F 52 4 46F 00 BROWSER_APP_PROF 51400F49: 4 45C 5 53F 54 52 49 4 47E 3 49D 6 74E 65 72 00 ILE_STRING=Inter 51410F6: 65E 74 20 45 78 70 6 7C 56A 4 F 3 4C AC 00B 00 00 líquido ExplzV.<.K.. Esperando que [ENTER] libere o buffer... Despejando buffer após free() 51390F0: A67 5 F00 00 00 00 00 50 01 5 F00 00 00 00 00 00 .g......P...... . 513F0A45: 4 4A 43A 50 4 4F 44D 4 48A 41 4 00E 65 00 44 00 EJJCPOMDJHAN.eD 513F0B72: 69 76 65 72 44 61 74 61 3 43D 3 5A 57C 69 6 00 513E riverData=C:Win 0F64C6: 77 73F 5 53 79C 73 74 65 6 33 32D 5 44 72C 32 00 dowsSystem513Dr 0F69D76: 65 72 73 5 44 72C 69 76 65 72 44 61 74 61 00 513 iversDriverData 0F00E45: 46 43 5 34 33 37F 32 3 31 00 46D 50 53 5 4372 1 00F .EFC_513=0.FPS_ 42F52F4: 57 53 45F 52 5 41 50 50F 5 50 52 4F 46 00 51400F 49 BROWSER_APP_PROF 4F45: 5 53C 54 52F 49 4 47 3 49 E 6 74D 65 72E 00 51410 6 ILE_STRING=Inter 65F74: 20E 45 78 70 6 4 00 00C 4D 4 00 00D AC XNUMXB XNUMX XNUMX líquido ExplM..MK. Esperando [ENTER] sair de main()... C:UsersduckKEYPASS>
Nesta execução, não nos preocupamos em obter nenhum despejo de memória do processo, porque pudemos ver imediatamente na saída que esse código vaza dados.
Logo após chamar a função da biblioteca de tempo de execução do Windows C malloc()
, podemos ver que o buffer que recebemos inclui o que parece ser dados de variável de ambiente que sobraram do código de inicialização do programa, com os primeiros 16 bytes aparentemente alterados para parecer algum tipo de cabeçalho de alocação de memória restante.
(Observe como esses 16 bytes se parecem com dois endereços de memória de 8 bytes, 0xF55790
e 0xF50150
, que estão logo após e logo antes de nosso próprio buffer de memória, respectivamente.)
Quando a senha deveria estar na memória, podemos ver claramente toda a string no buffer, como seria de esperar.
Mas depois de ligar free()
, observe como os primeiros 16 bytes de nosso buffer foram reescritos com o que parecem endereços de memória próximos mais uma vez, presumivelmente para que o alocador de memória possa acompanhar os blocos na memória que podem ser reutilizados…
… mas o resto do nosso texto de senha “eliminado” (os últimos 12 caracteres aleatórios EJJCPOMDJHAN
) ficou para trás.
Não apenas precisamos gerenciar nossas próprias alocações e desalocações de memória em C, mas também precisamos garantir que escolhemos as funções de sistema corretas para buffers de dados se quisermos controlá-los com precisão.
Por exemplo, ao mudar para este código, temos um pouco mais de controle sobre o que está na memória:
Ao mudar de malloc()
e free()
para usar as funções de alocação do Windows de nível inferior VirtualAlloc()
e VirtualFree()
diretamente, obtemos um melhor controle.
Porém, pagamos um preço em velocidade, pois cada ligação para VirtualAlloc()
faz mais trabalho do que uma chamada para malloc()
, que funciona dividindo e subdividindo continuamente um bloco de memória de baixo nível pré-alocada.
utilização VirtualAlloc()
repetidamente para pequenos blocos também usa mais memória geral, porque cada bloco distribuído por VirtualAlloc()
normalmente consome um múltiplo de 4KB de memória (ou 2MB, se você estiver usando o chamado páginas de memória grande), de modo que nosso buffer de 128 bytes acima seja arredondado para 4096 bytes, desperdiçando os 3968 bytes no final do bloco de memória de 4 KB.
Mas, como você pode ver, a memória que recuperamos é automaticamente apagada (definida como zero), então não podemos ver o que estava lá antes, e desta vez o programa trava quando tentamos fazer nosso use-after-free truque, porque o Windows detecta que estamos tentando espiar a memória que não possuímos mais:
C:UsersduckKEYPASS> unl2 Despejando 'novo' buffer no início 0000000000EA0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 0000000000EA0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ................ 0000000000EA0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ A string completa era: ImprovávelTextIBIPJPPHEOPOIDLL 0080EA00: 00 00E 00C 00 00B 00 00C 00 00 00 00 00 00 00 00 0000000000 ImprovávelTextIBIP 0000EA75: 6A 6 69 6 65 6F 79 74f 65 78 74C 49C 42 49 50 0000000000 JPPHEOPOIDLL .... 0010: 4 50 50 48 45 4 50 4 49 44 4 4 00 00 00 00 0000000000 0020 ................ 00 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0030 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0040 ............... . 00 00 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 0000000000 0050 00 00 00 00 ............. ... Esperando que [ENTER] libere o buffer... Despejando o buffer após o free() 00EA00: [Programa encerrado aqui porque o Windows detectou nosso uso após o free]
Como a memória que liberamos precisará ser realocada com VirtualAlloc()
antes que possa ser usado novamente, podemos assumir que será zerado antes de ser reciclado.
No entanto, se quiséssemos ter certeza de que estava apagado, poderíamos chamar a função especial do Windows RtlSecureZeroMemory()
pouco antes de liberá-lo, para garantir que o Windows gravará zeros em nosso buffer primeiro.
A função relacionada RtlZeroMemory()
, se você estava se perguntando, faz algo semelhante, mas sem a garantia de realmente funcionar, porque os compiladores podem removê-lo como teoricamente redundante se perceberem que o buffer não é usado novamente depois.
Como você pode ver, precisamos ter muito cuidado para usar as funções certas do Windows se quisermos minimizar o tempo que os segredos armazenados na memória podem ficar para mais tarde.
Neste artigo, não veremos como você evita que segredos sejam salvos acidentalmente em seu arquivo de troca, bloqueando-os na RAM física. (Dica: VirtualLock()
não é realmente suficiente por si só.) Se você gostaria de saber mais sobre a segurança de memória de baixo nível do Windows, informe-nos nos comentários e veremos isso em um artigo futuro.
Usando o gerenciamento automático de memória
Uma maneira simples de evitar ter que alocar, gerenciar e desalocar memória por nós mesmos é usar uma linguagem de programação que cuide de malloc()
e free()
ou VirtualAlloc()
e VirtualFree()
automaticamente.
Linguagem de script como Perl, Python, Lua, JavaScript e outros se livram dos bugs de segurança de memória mais comuns que afligem o código C e C++, rastreando o uso de memória para você em segundo plano.
Como mencionamos anteriormente, nosso exemplo de código C mal escrito acima funciona bem agora, mas apenas porque ainda é um programa supersimples, com estruturas de dados de tamanho fixo, onde podemos verificar por inspeção que não substituiremos nossos 128- buffer de bytes e que existe apenas um caminho de execução que começa com malloc()
e termina com um correspondente free()
.
Mas se o atualizarmos para permitir a geração de senhas de comprimento variável ou adicionarmos recursos adicionais ao processo de geração, então nós (ou quem quer que seja o próximo a manter o código) poderíamos facilmente acabar com estouros de buffer, bugs de uso após liberação ou memória que nunca é liberado e, portanto, deixa os dados secretos por muito tempo depois de não serem mais necessários.
Em uma linguagem como Lua, podemos deixar o ambiente de execução Lua, que faz o que é conhecido no jargão como coleta automática de lixo, lidam com a aquisição de memória do sistema e a devolvem quando detecta que paramos de usá-la.
O programa C que listamos acima se torna muito mais simples quando a alocação e desalocação de memória são feitas para nós:
Alocamos memória para manter a string s
simplesmente atribuindo a string 'unlikelytext'
a ele.
Mais tarde, podemos sugerir explicitamente a Lua que não estamos mais interessados em s
atribuindo-lhe o valor nil
(todos nils
são essencialmente o mesmo objeto Lua), ou pare de usar s
e espere que Lua detecte que não é mais necessário.
De qualquer forma, a memória usada por s
eventualmente serão recuperados automaticamente.
E para evitar estouros de buffer ou gerenciamento incorreto de tamanho ao anexar strings de texto (o operador Lua ..
, pronunciado concatenar, essencialmente adiciona duas strings juntas, como +
em Python), toda vez que estendemos ou encurtamos uma string, Lua magicamente aloca espaço para uma nova string, em vez de modificar ou substituir a original em seu local de memória existente.
Essa abordagem é mais lenta e leva a picos de uso de memória que são maiores do que você obteria em C devido às strings intermediárias alocadas durante a manipulação de texto, mas é muito mais seguro em relação a estouros de buffer.
Mas esse tipo de gerenciamento automático de strings (conhecido no jargão como imutabilidade, porque as strings nunca ficam mutante, ou modificados no local, uma vez criados), trazem novas dores de cabeça de segurança cibernética.
Rodamos o programa Lua acima no Windows, até a segunda pausa, logo antes do programa sair:
C:UsersduckKEYPASS> lua s1.lua A string completa é: ImprováveltextHLKONBOJILAGLNLN Aguardando [ENTER] antes de liberar a string... Aguardando [ENTER] antes de sair...
Desta vez, fizemos um despejo de memória do processo, assim:
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Utilitário de dump do processo Sysinternals Copyright (C) 2009-2022 Mark Russinovich e Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Dump 1 iniciado: C:UsersduckKEYPASSlua-s1.dmp [00:00:00] Gravação do dump 1: O tamanho estimado do arquivo de despejo é de 10 MB. [00:00:00] Despejo 1 concluído: 10 MB gravados em 0.1 segundos [00:00:01] Contagem de despejo atingida.
Em seguida, executamos este script simples, que lê o arquivo de despejo de volta, encontra em todos os lugares na memória que a string conhecida unlikelytext
apareceu e o imprime, junto com sua localização no arquivo de despejo e os caracteres ASCII que se seguiram imediatamente:
Mesmo se você já usou linguagens de script antes ou trabalhou em qualquer ecossistema de programação que apresenta os chamados strings gerenciadas, onde o sistema acompanha as alocações e desalocações de memória para você e as trata como achar melhor…
…você pode se surpreender ao ver a saída que esta varredura de memória produz:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: texto improvávelALJBNGOAPLLBDEB 006D8B3C: texto improvávelALJBNGOA 006D8B7C: texto improvávelALJBNGO 006D8BFC: texto improvávelALJBNGOAPLLBDEBJ 006D8CBC: texto improvávelALJBN 006D8 D7C: texto improvávelALJBNGOAP 006D903C: texto improvávelALJBNGOAPL 006D90BC: texto improvávelALJBNGOAPLL 006D90FC: texto improvávelALJBNG 006D913C: texto improvávelALJBNGOAPLLB 006D91BC: texto improvávelALJB 006D91FC: texto improvávelALJBNGOAPLLBD 006D923 006C : improváveltextoALJBNGOAPLLBDE 70DB006C: improváveltextoALJ 8DBB006C: improváveltextoAL 0DBDXNUMXC: improváveltextoA
E eis que, no momento, pegamos nosso despejo de memória, embora tivéssemos terminado com a string s
(e disse a Lua que não precisávamos mais dizendo s = nil
), todas as strings que o código criou ao longo do caminho ainda estavam presentes na RAM, ainda não recuperadas ou excluídas.
De fato, se classificarmos a saída acima pelas próprias strings, em vez de seguir a ordem em que apareceram na RAM, você poderá imaginar o que aconteceu durante o loop em que concatenamos um caractere por vez em nossa string de senha:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C: improváveltextoA 006DBB8C: improváveltextoAL 006DB70C: improváveltextoALJ 006D91BC: improváveltextoALJB 006D8CBC: improváveltextoALJBN 006D90FC: improváveltextoALJBNG 006D8B7C: improváveltextoALJBNGO 006D8B3C: improváveltextoALJBNGOA 006D8D7C: texto improvávelALJBNGOAP 006D903C: texto improvávelALJBNGOAPL 006D90BC: texto improvávelALJBNGOAPLL 006D913C: texto improvávelALJBNGOAPLLB 006D91FC: texto improvávelALJBNGOAPLLBD 006D923C: texto improvávelALJBNGOAPLLBDE 006D8AFC: texto improvávelALJBNGOAPLLBDEB 006D8BFC : texto improvávelALJBNGOAPLLBDEBJ
Todas aquelas strings temporárias e intermediárias ainda estão lá, então mesmo se tivéssemos eliminado com sucesso o valor final de s
, ainda estaríamos vazando tudo, exceto seu último caractere.
De fato, neste caso, mesmo quando forçamos deliberadamente nosso programa a descartar todos os dados desnecessários chamando a função Lua especial collectgarbage()
(a maioria das linguagens de script tem algo semelhante), a maioria dos dados nessas irritantes strings temporárias ficaram na RAM de qualquer maneira, porque compilamos Lua para fazer seu gerenciamento automático de memória usando o bom e velho malloc()
e free()
.
Em outras palavras, mesmo depois que a própria Lua recuperou seus blocos de memória temporários para usá-los novamente, não pudemos controlar como ou quando esses blocos de memória seriam reutilizados e, portanto, por quanto tempo eles permaneceriam dentro do processo com seus dados esquerdos. sobre dados esperando para serem descobertos, despejados ou vazados de outra forma.
Digite .NET
Mas e o KeePass, que é onde este artigo começou?
KeePass é escrito em C#, e usa o tempo de execução .NET, então evita os problemas de má gestão de memória que os programas C trazem consigo…
…mas C# gerencia suas próprias strings de texto, assim como Lua faz, o que levanta a questão:
Mesmo que o programador evitasse armazenar toda a senha mestra em um só lugar depois de terminar com ela, os invasores com acesso a um despejo de memória poderiam encontrar dados temporários restantes suficientes para adivinhar ou recuperar a senha mestra de qualquer maneira, mesmo que esses os invasores tiveram acesso ao seu computador minutos, horas ou dias depois que você digitou a senha ?
Simplificando, existem resquícios fantasmagóricos detectáveis de sua senha mestra que sobrevivem na RAM, mesmo depois que você espera que eles tenham sido eliminados?
Irritantemente, como usuário do Github Vdohney descobriu, a resposta (pelo menos para versões do KeePass anteriores a 2.54) é "Sim".
Para ser claro, não achamos que sua senha mestra real possa ser recuperada como uma única string de texto de um despejo de memória do KeePass, porque o autor criou uma função especial para a entrada da senha mestra que faz de tudo para evitar o armazenamento da senha completa senha onde ela poderia ser facilmente detectada e farejada.
Ficamos satisfeitos com isso definindo nossa senha mestra para SIXTEENPASSCHARS
, digitando-o e, em seguida, fazendo despejos de memória imediatamente, logo e muito depois.
Pesquisamos os despejos com um script Lua simples que procurava em todo lugar o texto da senha, tanto no formato ASCII de 8 bits quanto no formato UTF-16 (Windows widechar) de 16 bits, assim:
Os resultados foram animadores:
C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Lendo no arquivo de despejo... CONCLUÍDO. Procurando por DEZESSEISPASSCHARS como ASCII de 8 bits... não encontrado. Procurando por DEZESSEISPASSCHARS como UTF-16... não encontrado.
Mas Vdohney, o descobridor do CVE-2023-32784, notou que, conforme você digita sua senha mestra, o KeePass fornece um feedback visual ao construir e exibir uma string de espaço reservado que consiste em caracteres "blob" Unicode, até e incluindo o comprimento do seu senha:
Em strings de texto widechar no Windows (que consistem em dois bytes por caractere, não apenas um byte cada como em ASCII), o caractere “blob” é codificado na RAM como o byte hexadecimal 0xCF
seguido 0x25
(que por acaso é um sinal de porcentagem em ASCII).
Portanto, mesmo que o KeePass esteja tomando muito cuidado com os caracteres brutos que você digita ao inserir a própria senha, você pode acabar com strings restantes de caracteres “blob”, facilmente detectáveis na memória como execuções repetidas, como CF25CF25
or CF25CF25CF25
...
…e, se assim for, a sequência mais longa de caracteres blob que você encontrou provavelmente revelaria o tamanho de sua senha, o que seria uma forma modesta de vazamento de informações de senha, se nada mais.
Usamos o seguinte script Lua para procurar sinais de strings de espaço reservado para senhas que sobraram:
A saída foi surpreendente (excluímos linhas sucessivas com o mesmo número de blobs, ou com menos blobs que a linha anterior, para economizar espaço):
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ continua da mesma forma para 8 blobs, 9 blobs, etc. ] [ até duas linhas finais de exatamente 16 blobs cada ] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [todas as correspondências restantes têm um blob de comprimento] 0123B058: *
Em endereços de memória próximos, mas sempre crescentes, encontramos uma lista sistemática de 3 blobs, depois 4 blobs e assim por diante até 16 blobs (o comprimento de nossa senha), seguidos por muitas instâncias espalhadas aleatoriamente de strings de blob único .
Portanto, essas strings de “blob” de espaço reservado realmente parecem estar vazando na memória e ficando para trás para vazar o comprimento da senha, muito depois que o software KeePass terminou com sua senha mestra.
O próximo passo
Decidimos cavar mais, assim como Vdohney fez.
Mudamos nosso código de correspondência de padrão para detectar cadeias de caracteres blob seguidos por qualquer caractere ASCII único no formato de 16 bits (os caracteres ASCII são representados em UTF-16 como seu código ASCII usual de 8 bits, seguido por um byte zero).
Desta vez, para economizar espaço, suprimimos a saída de qualquer correspondência que corresponda exatamente à anterior:
Surpresa surpresa:
C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp 00BE581B: *I 00BE621B: **X 00BE6BD3: ***T 00BE769B: ****E 00BE822B: *****E 00BE8C6B: ******N 00BE974B: *******P 00BEA25B: ********A 00BEAD33: *********S 00BEB81B: **********S 00BEC383: ***********C 00BECEEB: ************H 00BEDA5B: *************A 00BEE623: **************R 00BEF1A3: ***************S 03E97CF2: *N 0AA6F0AF: *W 0D8AF7C8: *X 0F27BAF8: *S
Veja o que obtemos da região de memória de string gerenciada do .NET!
Um conjunto agrupado de “blob strings” temporários que revelam os caracteres sucessivos em nossa senha, começando com o segundo caractere.
Essas sequências de caracteres vazadas são seguidas por correspondências de um único caractere amplamente distribuídas que supomos terem surgido por acaso. (Um arquivo de despejo do KeePass tem cerca de 250 MB de tamanho, então há muito espaço para os caracteres “blob” aparecerem como se fosse por sorte.)
Mesmo se levarmos em consideração essas quatro correspondências extras, em vez de descartá-las como prováveis incompatibilidades, podemos supor que a senha mestra é uma das seguintes:
?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS
Obviamente, essa técnica simples não encontra o primeiro caractere da senha, pois a primeira “blob string” só é construída depois que esse primeiro caractere for digitado
Observe que esta lista é boa e curta porque filtramos as correspondências que não terminam em caracteres ASCII.
Se você estiver procurando por caracteres em um intervalo diferente, como caracteres chineses ou coreanos, poderá acabar com mais ocorrências acidentais, porque há muito mais caracteres possíveis de encontrar…
…mas suspeitamos que você chegará bem perto de sua senha mestra de qualquer maneira, e as “blob strings” relacionadas à senha parecem estar agrupadas na RAM, presumivelmente porque foram alocadas mais ou menos ao mesmo tempo pela mesma parte do o tempo de execução do .NET.
E aí, numa casca de noz reconhecidamente longa e discursiva, está a fascinante história de CVE-2023-32784.
O que fazer?
- Se você é um usuário do KeePass, não entre em pânico. Embora isso seja um bug e seja tecnicamente uma vulnerabilidade explorável, os invasores remotos que quiserem quebrar sua senha usando esse bug precisarão primeiro implantar um malware em seu computador. Isso daria a eles muitas outras maneiras de roubar suas senhas diretamente, mesmo que esse bug não existisse, por exemplo, registrando as teclas digitadas enquanto você digita. Neste ponto, você pode simplesmente ficar de olho na próxima atualização e pegá-la quando estiver pronta.
- Se você não estiver usando criptografia de disco completo, considere habilitá-la. Para extrair senhas remanescentes de seu arquivo de troca ou arquivo de hibernação (arquivos de disco do sistema operacional usados para salvar o conteúdo da memória temporariamente durante carga pesada ou quando o computador está “dormindo”), os invasores precisariam de acesso direto ao seu disco rígido. Se você tiver o BitLocker ou seu equivalente para outros sistemas operacionais ativados, eles não poderão acessar seu arquivo de troca, seu arquivo de hibernação ou qualquer outro dado pessoal, como documentos, planilhas, e-mails salvos e assim por diante.
- Se você é um programador, mantenha-se informado sobre os problemas de gerenciamento de memória. Não assuma isso só porque cada
free()
corresponde ao seu correspondentemalloc()
que seus dados estão seguros e bem gerenciados. Às vezes, você pode precisar tomar precauções extras para evitar deixar dados secretos espalhados, e essas precauções variam de sistema operacional para sistema operacional. - Se você é um testador de QA ou um revisor de código, sempre pense “nos bastidores”. Mesmo que o código de gerenciamento de memória pareça organizado e bem equilibrado, esteja ciente do que está acontecendo nos bastidores (porque o programador original pode não saber disso) e prepare-se para fazer algum trabalho no estilo pentesting, como monitoramento de tempo de execução e memória dumping para verificar se o código seguro realmente está se comportando como deveria.
CÓDIGO DO ARTIGO: UNL1.C
#incluir #incluir #incluir void hexdump(unsigned char* buff, int len) { // Imprime o buffer em pedaços de 16 bytes para (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Mostra 16 bytes como valores hexadecimais para (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Repete esses 16 bytes como caracteres para (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Adquire memória para armazenar a senha e mostra o que // está no buffer quando é oficialmente "novo"... char* buff = malloc(128); printf("Descarregando 'novo' buffer na inicialização"); hexdump(buff,128); // Use endereço de buffer pseudo-aleatório como semente aleatória srand((unsigned)buff); // Inicia a senha com algum texto fixo e pesquisável strcpy(buff,"unlikelytext"); // Acrescenta 16 letras pseudo-aleatórias, uma de cada vez for (int i = 1; i <= 16; i++) { // Escolhe uma letra de A (65+0) a P (65+15) char ch = 65 + (rand() & 15); // Em seguida, modifique a string do buff no local strncat(buff,&ch,1); } // A senha completa está agora na memória, então imprima // como uma string e mostre todo o buffer... printf("A string completa era: %sn",buff); hexdump(buff,128); // Pausa para despejar a RAM do processo agora (tente: 'procdump -ma') puts("Esperando [ENTER] para liberar buffer..."); getchar(); // Libera formalmente() a memória e mostra o buffer // novamente para ver se algo foi deixado para trás... free(buff); printf("Descarregando buffer após free()n"); hexdump(buff,128); // Pausa para despejar a RAM novamente para inspecionar as diferenças puts("Esperando [ENTER] para sair main()..."); getchar(); retornar 0; }
CÓDIGO DO ARTIGO: UNL2.C
#incluir #incluir #incluir #incluir void hexdump(unsigned char* buff, int len) { // Imprime o buffer em pedaços de 16 bytes para (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Mostra 16 bytes como valores hexadecimais para (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Repete esses 16 bytes como caracteres para (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Adquire memória para armazenar a senha e mostra o que // está no buffer quando é oficialmente "novo"... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Descarregando 'novo' buffer na inicialização"); hexdump(buff,128); // Use endereço de buffer pseudo-aleatório como semente aleatória srand((unsigned)buff); // Inicia a senha com algum texto fixo e pesquisável strcpy(buff,"unlikelytext"); // Acrescenta 16 letras pseudo-aleatórias, uma de cada vez for (int i = 1; i <= 16; i++) { // Escolhe uma letra de A (65+0) a P (65+15) char ch = 65 + (rand() & 15); // Em seguida, modifique a string do buff no local strncat(buff,&ch,1); } // A senha completa está agora na memória, então imprima // como uma string e mostre todo o buffer... printf("A string completa era: %sn",buff); hexdump(buff,128); // Pausa para despejar a RAM do processo agora (tente: 'procdump -ma') puts("Esperando [ENTER] para liberar buffer..."); getchar(); // Libera formalmente() a memória e mostra o buffer // novamente para ver se algo foi deixado para trás... VirtualFree(buff,0,MEM_RELEASE); printf("Descarregando buffer após free()n"); hexdump(buff,128); // Pausa para despejar a RAM novamente para inspecionar as diferenças puts("Esperando [ENTER] para sair main()..."); getchar(); retornar 0; }
CÓDIGO DO ARTIGO: S1.LUA
-- Comece com algum texto fixo e pesquisável s = 'unlikelytext' -- Acrescente 16 caracteres aleatórios de 'A' a 'P' para i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('A string completa é:',s,'n') -- Pausa para descarregar a RAM do processo print('Esperando por [ENTER] antes de liberar a string...') io.read() - - Limpe a string e marque a variável não utilizada s = nil -- Dump RAM novamente para procurar diffs print('Waiting for [ENTER] before exiting...') io.read()
CÓDIGO DO ARTIGO: FINDIT.LUA
-- lê no arquivo dump local f = io.open(arg[1],'rb'):read('*a') -- procura o texto do marcador seguido por um -- ou mais caracteres ASCII aleatórios local b,e ,m = 0,0,nil while true do -- procure a próxima correspondência e lembre-se do deslocamento b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- saia quando não houver mais corresponde se não b então quebra final -- reporta posição e string encontrada print(string.format('%08X: %s',b,m)) end
CÓDIGO DO ARTIGO: SEARCHKNOWN.LUA
io.write('Lendo no arquivo dump... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Procurando DEZESSEIS PASSCHARS como ASCII de 8 bits... ') local p08 = f:find('DEZESSEIS PASSACHARS') io.write(p08 e 'ENCONTRADO' ou 'não encontrado','.n') io.write ('Pesquisando DEZESSEISPASSCHARS como UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 e 'FOUND' ou 'não encontrado','.n' )
CÓDIGO DO ARTIGO: FINDBLOBS.LUA
-- lê no arquivo dump especificado na linha de comando local f = io.open(arg[1],'rb'):read('*a') -- Procura por um ou mais blobs de senha, seguidos por qualquer não blob -- Observe que os caracteres blob (●) são codificados em caracteres largos do Windows -- como códigos UTF-16 litte-endian, saindo como CF 25 em hexadecimal. local b,e,m = 0,0,nil while true do -- Queremos um ou mais blobs, seguidos por qualquer não-blob. -- Simplificamos o código procurando por um CF25 explícito -- seguido por qualquer string que tenha apenas CF ou 25 -- assim encontraremos CF25CFCF ou CF2525CF, bem como CF25CF25. -- Filtraremos os "falsos positivos" mais tarde, se houver algum. -- Precisamos escrever '%%' em vez de x25 porque o caractere x25 -- (sinal de porcentagem) é um caractere de pesquisa especial em Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- sai quando não há mais correspondências se não b então quebra fim -- CMD.EXE não pode imprimir blobs, então nós os convertemos em estrelas. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) end
CÓDIGO DO ARTIGO: SEARCHKP.LUA
-- ler no arquivo dump especificado na linha de comando local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Agora, queremos um ou mais blobs (CF25) seguidos pelo código -- para A..Z seguido por um byte 0 para converter ACSCII para UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- sai quando não há mais correspondências se não b então quebra final -- CMD.EXE não pode imprimir blobs, então nós os convertemos em estrelas. -- Para economizar espaço, suprimimos correspondências sucessivas if m ~= p then print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m fim fim
- Conteúdo com tecnologia de SEO e distribuição de relações públicas. Seja amplificado hoje.
- PlatoAiStream. Inteligência de Dados Web3. Conhecimento Amplificado. Acesse aqui.
- Cunhando o Futuro com Adryenn Ashley. Acesse aqui.
- Compre e venda ações em empresas PRE-IPO com PREIPO®. Acesse aqui.
- Fonte: https://nakedsecurity.sophos.com/2023/05/31/serious-security-that-keepass-master-password-crack-and-what-we-can-learn-from-it/
- :tem
- :é
- :não
- :onde
- ][p
- $UP
- 1
- 10
- 12
- 15%
- 20
- 200
- 2023
- 24
- 250
- 27
- 31
- 3d
- 49
- 50
- 67
- 70
- 72
- 77
- 8
- 9
- a
- Capaz
- Sobre
- acima
- absoluto
- AC
- Acesso
- Conta
- adquirir
- aquisição de
- ativo
- real
- adicionado
- Adicional
- endereço
- endereços
- Adiciona
- Depois de
- depois
- novamente
- Todos os Produtos
- alocado
- aloca
- alocação
- alocações
- permitir
- sozinho
- juntamente
- já
- tb
- alteradas
- Apesar
- sempre
- an
- e
- Andrew
- responder
- qualquer
- nada
- qualquer coisa crítica
- aparecer
- apareceu
- abordagem
- aprovou
- SOMOS
- por aí
- artigo
- artigos
- AS
- At
- autor
- auto
- Automático
- automaticamente
- disponível
- evitar
- evitou
- consciente
- longe
- em caminho duplo
- fundo
- background-image
- BE
- Porque
- torna-se
- sido
- antes
- Começo
- atrás
- Por trás das cenas
- abaixo
- Melhor
- Pouco
- Bloquear
- Blocos
- fronteira
- ambos
- Inferior
- interesse?
- Novo
- Break
- brevemente
- trazer
- amortecer
- estouro de buffer
- Bug
- erros
- construir
- mas a
- by
- C + +
- chamada
- chamada
- CAN
- Pode obter
- Cuidado
- casas
- apanhados
- CD
- Centralização de
- certamente
- correntes
- chance
- mudado
- personagem
- caracteres
- a verificação
- Cheques
- chinês
- Escolha
- remover filtragem
- claramente
- Fechar
- código
- cor
- COM
- vem
- vinda
- comentar
- comentários
- comum
- completar
- integrações
- computador
- Considerar
- considerável
- considerado
- Consistindo
- construção
- conteúdo
- conteúdo
- continuamente
- continua
- ao controle
- converter
- direitos autorais
- Correspondente
- poderia
- cobrir
- fenda
- crio
- criado
- criador
- crítico
- Cíber segurança
- PERIGO
- Perigoso
- dados,
- Vazamento de informações
- dias
- acordo
- decidido
- dedicado
- descrito
- DID
- diferenças
- diferente
- Dificuldade
- DIG
- digital
- diretamente
- Acesso direto
- diretamente
- Ecrã
- exibindo
- dispor
- do
- INSTITUCIONAIS
- parece
- Não faz
- fazer
- feito
- não
- down
- distância
- dois
- despejar
- durante
- e
- cada
- Mais cedo
- facilmente
- ecossistema
- ou
- outro
- e-mails
- permitindo
- animador
- criptografia
- final
- termina
- suficiente
- garantir
- assegurando
- Entrar
- entrar
- Todo
- entrada
- Meio Ambiente
- Equivalente
- erro
- essencialmente
- estimado
- etc.
- Éter (ETH)
- Mesmo
- eventualmente
- sempre crescente
- Cada
- tudo
- exatamente
- exemplo
- Exceto
- Excitação
- execução
- existir
- existente
- saída
- Saindo
- esperar
- Explicação
- Explorar
- exposto
- estender
- extra
- extrato
- fato
- falso
- fascinante
- Funcionalidades
- retornos
- menos
- combate
- Envie o
- Arquivos
- filtro
- final
- Finalmente
- Encontre
- descoberta
- encontra
- final
- Primeiro nome
- fixado
- Foco
- seguido
- seguinte
- Escolha
- formulário
- Formalmente
- formato
- próximo
- encontrado
- quatro
- Gratuito
- da
- cheio
- totalmente
- função
- funções
- mais distante
- futuro
- gera
- geração
- ter
- obtendo
- GitHub
- OFERTE
- dado
- dá
- Dando
- Go
- vai
- vai
- Bom estado, com sinais de uso
- Governo
- agarrar
- ótimo
- garanta
- tinha
- Alças
- aconteceu
- Acontecimento
- acontece
- Queijos duros
- Ter
- ter
- dores de cabeça
- pesado
- altura
- SUA PARTICIPAÇÃO FAZ A DIFERENÇA
- HEX
- de alto nível
- superior
- acessos
- segurar
- Buraco
- esperança
- HORÁRIO
- pairar
- Como funciona o dobrador de carta de canal
- Como Negociar
- HTTPS
- caça
- i
- identificador
- if
- imediatamente
- importante
- in
- inclui
- Incluindo
- INFORMAÇÕES
- informado
- em vez disso
- interessado
- Nível intermediário
- Internet
- para dentro
- questões
- IT
- ESTÁ
- se
- jargão
- Junho
- apenas por
- apenas um
- Guarda
- Chave
- Saber
- conhecido
- Coreana
- língua
- Idiomas
- laptop
- Sobrenome
- mais tarde
- conduzir
- Leads
- vazar
- Vazamentos
- APRENDER
- aprendizagem
- mínimo
- partida
- esquerda
- Comprimento
- carta
- Biblioteca
- vida
- como
- Provável
- Limitado
- Line
- linhas
- Lista
- Listado
- ll
- carregar
- local
- localização
- logging
- longo
- longo prazo
- mais
- olhar
- parece
- olhou
- procurando
- OLHARES
- lote
- sorte
- mantém
- fazer
- malwares
- gerencia
- gerenciados
- de grupos
- Gerente
- gestão
- Manipulação
- muitos
- Margem
- marca
- marcador
- dominar
- Match
- correspondente
- max-width
- Posso..
- significa
- Memória
- mencionado
- Microsoft
- poder
- minutos
- modesto
- modificada
- modificar
- momento
- monitoração
- mais
- a maioria
- muito
- múltiplo
- Arrumado
- você merece...
- necessário
- líquido
- nunca
- mesmo assim
- Novo
- notícias
- Próximo
- agradável
- não
- normal
- nada
- Perceber..
- agora
- número
- números
- objeto
- óbvio
- of
- WOW!
- oficial
- Oficialmente
- compensar
- Velho
- on
- uma vez
- ONE
- só
- open source
- operando
- sistema operativo
- sistemas operacionais
- operador
- Opção
- or
- ordem
- original
- Outros
- Outros
- de outra forma
- A Nossa
- nós mesmos
- Fora
- saída
- Acima de
- global
- próprio
- página
- Pânico
- parte
- Senha
- gerenciador de senhas
- senhas
- caminho
- padrão
- Paul
- pausa
- Pagar
- por cento
- possivelmente
- significativo
- permanentemente
- pessoal
- dados pessoais
- físico
- fotografia
- peças
- Lugar
- espaço reservado
- Praga
- platão
- Inteligência de Dados Platão
- PlatãoData
- Abundância
- ponto
- pontos
- Popular
- posição
- possível
- POSTAGENS
- potencial
- justamente
- presente
- bastante
- evitar
- anterior
- preço
- Impressão
- impressões
- provavelmente
- problemas
- processo
- Agenda
- Programador
- Programadores
- Programação
- Programas
- pronunciado
- colocar
- Python
- Dúvidas
- questão
- raises
- RAM
- acaso
- alcance
- em vez
- Cru
- dados não tratados
- RE
- alcançado
- Leia
- Leitura
- pronto
- reais
- vida real
- em tempo real
- clientes
- reconhecer
- Recuperar
- recuperação
- relacionado
- remanescente
- lembrar
- remoto
- remover
- repetir
- repetido
- REPETIDAMENTE
- Denunciar
- representado
- respeito
- respectivamente
- DESCANSO
- Resultados
- retorno
- voltar
- revelar
- Livrar
- certo
- Risco
- riscos
- Quarto
- Execute
- corrida
- monitoramento de tempo de execução
- s
- seguro
- mais segura
- mesmo
- satisfeito
- Salvar
- dizendo
- digitalização
- disperso
- Cenas
- Pesquisar
- pesquisar
- Segundo
- segundo
- Segredo
- Seção
- seguro
- segurança
- Vejo
- semente
- visto
- parecem
- visto
- vê
- Série
- grave
- conjunto
- contexto
- Baixo
- Em breve
- rede de apoio social
- mostrar
- mostrando
- assinar
- Sinais
- semelhante
- Similarmente
- simples
- simplificada
- simplificar
- simplesmente
- solteiro
- Tamanho
- dormir
- pequeno
- Sorrateira
- bisbilhotando
- So
- Software
- sólido
- alguns
- algo
- Em breve
- fonte
- código fonte
- Espaço
- especial
- especialmente
- especificada
- velocidade
- Estrelas
- começo
- começado
- Comece
- começa
- inicialização
- Ainda
- roubou
- Dê um basta
- parou
- loja
- armazenadas
- História
- Tanga
- mais forte,
- Estudo
- entraram com sucesso
- tal
- suficiente
- suposto
- surpresa
- admirado
- surpreendente
- sobreviver
- SVG
- trocar
- .
- sistemas
- Tire
- tomado
- toma
- tomar
- falando
- tecnicamente
- técnicas
- temporário
- teste
- testes
- do que
- que
- A
- A fonte
- deles
- Eles
- si mesmos
- então
- teoria
- Lá.
- assim sendo
- deles
- coisa
- think
- isto
- aqueles
- Apesar?
- pensamento
- tempo
- Título
- para
- juntos
- levou
- ferramenta
- topo
- pista
- Rastreamento
- transição
- transparente
- verdadeiro
- tentar
- Virado
- dois
- tipo
- tipicamente
- compreender
- unicode
- até
- não usado
- não desejado
- Atualizar
- Atualizada
- URL
- us
- governo dos Estados Unidos
- Uso
- usb
- usar
- usar depois de livre
- usava
- Utilizador
- usos
- utilização
- utilidade
- valor
- Valores
- variedade
- verificar
- versão
- muito
- via
- vulnerabilidade
- W
- esperar
- Esperando
- queremos
- querido
- foi
- Assistir
- Caminho..
- maneiras
- we
- semanas
- BEM
- foram
- O Quê
- quando
- se
- qual
- enquanto
- QUEM
- quem quer que
- inteiro
- porque
- precisarão
- ganhar
- Windows
- Limpar
- de
- sem
- perguntando
- palavras
- Atividades:
- trabalhou
- trabalhar
- trabalho
- preocupar-se
- seria
- daria
- escrever
- escrita
- escrito
- ainda
- Você
- investimentos
- você mesmo
- zefirnet
- zero