No post anterior, sobre guardar segredos em variáveis de ambiente, mostrei como é fácil roubar dados sensíveis de variáveis de ambiente, e apresentei algumas ideias sobre como resolver o problema.
Nesse post vamos assumir que esse problema está resolvido, e estamos carregando os segredos pra aplicação de forma segura. O que acontece então? O valor está seguro? Vamos analisar uma aplicação .NET e entender.
Strings no .NET
Strings no .NET vão para a memória em texto simples, sem qualquer criptografia. Imagine o impacto de desempenho se toda string fosse criptografada, não faz sentido. E pra piorar, no .NET strings ficam na heap, a área da memória que é gerenciada por um Garbage Collector, ou seja, não temos como saber, de forma determinística, quando o valor será removido da memória. E pior, quando acontece uma coleta de lixo (um GC), a memória é comprimida, ou seja, ela é copiada de um lugar para o outro, e o local antigo não necessariamente foi sobrescrito. E strings são imutáveis, ou seja, sempre que você manipula uma string, é feita uma cópia da mesma em memória.
Veja então a seguinte situação: você carregou, de forma segura, uma senha para conectar ao banco de dados, e em seguida a concatenou com o restante da string de conexão e armazenou em uma variável estática. Fazendo isso, você está garantindo que há pelo menos duas cópias dessa senha na memória, sendo que uma nunca vai ser coletada pelo GC, por ser estática.
Uma aplicação alvo
Vamos avaliar o quão segura é a string do cenário anterior. Vamos supor que esta seja uma aplicação ASP.NET Core. Criei um projeto mvc do zero utilizando dotnet new mvc
e carreguei uma senha a partir do arquivo de configuração. Os pontos que importam são os seguintes:
A classe SecretManager
que vai guardar o valor:
public class SecretManager { private readonly string secret; public SecretManager(string secret) => this.secret = secret; }
No método ConfigureServices
eu guardo o valor utilizando a classe SecretManager
. O valor é lido a partir de uma fonte segura da configuração:
services.AddSingleton(new SecretManager(Configuration.GetValue("Secret")));
Esse é um padrão muito comum, um valor é lido da configuração e armazenado em um objeto singleton, que no fim das contas está amarrado a uma variável estática em algum lugar.
Roubando a senha
Rodei a aplicação e tirei um dump com o Process Explorer. Lembrando, como falei no último post, que o dump pode ser tirado de várias formas, e precisamos somente de um terminal, nem uma janela é preciso.
Abri o dump no WinDbg Preview, o que está na Windows Store. A partir de agora o negócio fica sério.
Lembre que quem está atacando a aplicação não sabe nada sobre seu código. Até o momento, tudo que sabemos é que é uma aplicação .NET Core, porque o dump veio do processo dotnet.exe
.
Primeiro carregamos o sos, que é a extensão de debug para .NET, a partir do local do Core CLR:
.loadby sos coreclr
Será que a app tem poucas strings? De repente eu consigo olhar todas e já achar o que procuro. O comando é o dumpheap, mas queremos somente as estatísticas, e não um relatório por objeto:
!dumpheap -type System.String -stat
O resultado é o que segue. Quando for muito grande, como agora, vou colocar ...
pra deixar claro que uma parte foi suprimida:
Statistics: MT Count TotalSize Class Name 00007ffc1b6754e0 1 24 System.Collections.Generic.GenericEqualityComparer`1[[System.String, System.Private.CoreLib]] ... 00007ffc1b602c90 1688 106072 System.String[] 00007ffc1b637f90 15356 1367550 System.String Total 19621 objects
Quinze mil strings, não vai dar pra analisar uma a uma.
Vamos então analisar a classe de início da aplicação, a Startup
. Pra fazer isso é simples, usamos o comando name2ee
, que mostra os detalhes de uma classe:
!name2ee SuperSegura.dll SuperSegura.Startup
(para entender melhor cada comando do SOS veja a referência dos SOS.)
Se você encontrar a classe, vai ver na saída a EEClass
, que contém a estrutura que procuramos, assim:
Module: 00007ffbd7644d18 Assembly: SuperSegura.dll Token: 0000000002000004 MethodTable: 00007ffbd7648980 EEClass: 00007ffbd7797ac8 Name: SuperSegura.Startup
Queremos ver quais métodos ela tem, vamos usar o comando DumpMT
pra listar isso, passando o endereço da MethodTable
.
!DumpMT -md 00007ffbd7648980
A saída é essa:
EEClass: 00007ffbd7797ac8 Module: 00007ffbd7644d18 Name: SuperSegura.Startup mdToken: 0000000002000004 File: C:\p\rabiscos\SuperSegura\bin\Debug\netcoreapp2.0\publish\SuperSegura.dll BaseSize: 0x18 ComponentSize: 0x0 Slots in VTable: 8 Number of IFaces in IFaceMap: 0 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Name ... 00007ffbd77b72a0 00007ffbd7648928 JIT SuperSegura.Startup..ctor(Microsoft.Extensions.Configuration.IConfiguration) 00007ffbd77c79c0 00007ffbd7648938 JIT SuperSegura.Startup.get_Configuration() 00007ffbd77b7710 00007ffbd7648948 JIT SuperSegura.Startup.ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection) 00007ffbd7b737a0 00007ffbd7648958 JIT SuperSegura.Startup.Configure(Microsoft.AspNetCore.Builder.IApplicationBuilder, Microsoft.AspNetCore.Hosting.IHostingEnvironment)
Conhecendo .NET Core, a configuração vai acontecer no método ConfigureServices
. Vamos dar uma olhada nele. Pra isso, vamos ler sua IL com o comando dumpil
. Vamos passar o endereço do method descriptor do ConfigureServices
.
!dumpil 00007ffbd7648948
O resultado é conforme a seguir:
ilAddr = 000001ea623820a3 IL_0000: nop IL_0001: ldarg.1 IL_0002: call Microsoft.Extensions.DependencyInjection.MvcServi::AddMvc IL_0007: pop IL_0008: ldarg.1 IL_0009: ldarg.0 IL_000a: call SuperSegura.Startup::get_Configuration IL_000f: ldstr "Secret" IL_0014: call IL_0019: newobj SuperSegura.SecretManager::.ctor IL_001e: call IL_0023: pop IL_0024: ret
Note que alguns tipos de token não foram identificados pelo SOS. Provavelmente é alguma construção nova da IL, e o SOS não foi ainda atualizado. Mas já dá pra ver que foi criado um SecretManager
e que ele provavelmente foi passado pra um método que não estamos conseguindo identificar. Sem problemas, vamos olhar o assembly. Basta passar o mesmo endereço, dessa vez pro comando u
.
!u 00007ffbd7648948
O Windbg anota os símbolos que ele acha, então fica fácil de olhar os métodos e classes utilizados. Vou deixar aqui no output somente as linhas que mostram alguns desses símbolos. O que estamos procurando é o método para o qual o SecretManager
foi passado:
Normal JIT generated code SuperSegura.Startup.ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection) Begin 00007ffbd77b7710, size cf 00007ffb`d77b7710 55 push rbp ... 00007ffb`d77b77ae e8fdfbffff call 00007ffb`d77b73b0 (SuperSegura.SecretManager..ctor(System.String), mdToken: 0000000006000004) ... 00007ffb`d77b77bb 48b990c990d7fb7f0000 mov rcx,7FFBD790C990h (MD: Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton[[SuperSegura.SecretManager, SuperSegura]](Microsoft.Extensions.DependencyInjection.IServiceCollection, SuperSegura.SecretManager)) ...
Tá bem claro que um objeto SecretManager
está sendo passado pro método AddSingleton
. Vamos procurar esse objeto. Não devem ter muitos desses, então vamos usar o dumpheap
sem o -stat
dessa vez e listar todas as instâncias dele:
dumpheap -type SuperSegura.SecretManager
O resultado mostra de cara que temos um único objeto:
Address MT Size 000001ec627c9a50 00007ffbd790c8d0 24 Statistics: MT Count TotalSize Class Name 00007ffbd790c8d0 1 24 SuperSegura.SecretManager Total 1 objects
Vamos ver o que ele possui, fazendo o dump dele com DumpObj
(também conhecido como do
):
!DumpObj /d 000001ec627c9a50
O resultado é conforme a seguir. Ele mostra todos os campos e endereços do objeto:
Name: SuperSegura.SecretManager MethodTable: 00007ffbd790c8d0 EEClass: 00007ffbd791ff48 Size: 24(0x18) bytes File: C:\p\rabiscos\SuperSegura\bin\Debug\netcoreapp2.0\publish\SuperSegura.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffc1b637f90 4000001 8 System.String 0 instance 000001ea627af040 secret
Essa string com o nome secret
parece interessante, vamos olhar ela com do
:
!do 000001ea627af040
O resultado é o seguinte:
Name: System.String MethodTable: 00007ffc1b637f90 EEClass: 00007ffc1adca250 Size: 40(0x28) bytes File: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.0.6\System.Private.CoreLib.dll String: Lambda3 Fields: MT Field Offset Type VT Attr Value Name 00007ffc1b656648 4000218 8 System.Int32 1 instance 7 m_stringLength 00007ffc1b6381d8 4000219 c System.Char 1 instance 4c m_firstChar 00007ffc1b637f90 400021a 78 System.String 0 shared static Empty >> Domain:Value 000001ea622124b0:NotInit <<
Note o valor Lambda3
. Esse é o segredo que eu procurava. Encontramos a senha!
Eu procurei de outra forma também, analisando as configurações. Fui capaz de chegar nas fontes de configuração, havia uma fonte de Json, que é o que eu estava usando no teste. Não deu outra, consegui ler o valor a partir de lá também. Ou seja, nossa string sensível estava em pelo menos dois locais diferentes.
E agora?
Pois é, strings não são seguras. Vamos resolver isso no próximo post. Até lá, não se desespere. Existe solução.
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.