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.