O SecureString é sempre prático em um aplicativo C #?

224

Fique à vontade para me corrigir se minhas suposições estiverem erradas aqui, mas deixe-me explicar por que estou perguntando.

Retirado do MSDN, a SecureString:

Representa o texto que deve ser mantido em sigilo. O texto é criptografado para privacidade ao ser usado e excluído da memória do computador quando não é mais necessário.

Eu entendo isso, faz todo o sentido armazenar uma senha ou outras informações particulares em um SecureStringover a System.String, porque você pode controlar como e quando ele é realmente armazenado na memória, porque System.String:

é imutável e, quando não é mais necessário, não pode ser programado de forma programática para a coleta de lixo; isto é, a instância é somente leitura depois de criada e não é possível prever quando a instância será excluída da memória do computador. Conseqüentemente, se um objeto String contiver informações confidenciais, como senha, número do cartão de crédito ou dados pessoais, existe o risco de as informações serem reveladas depois de usadas, porque o aplicativo não pode excluir os dados da memória do computador.

No entanto, no caso de um aplicativo GUI (por exemplo, um cliente ssh), SecureString ele deve ser construído a partir de a System.String . Todos os controles de texto usam uma sequência como seu tipo de dados subjacente .

Portanto, isso significa que toda vez que o usuário pressiona uma tecla, a string antiga que estava lá é descartada e uma nova string é criada para representar qual é o valor dentro da caixa de texto, mesmo se estiver usando uma máscara de senha. E não podemos controlar quando ou se algum desses valores é descartado da memória .

Agora é hora de fazer login no servidor. Adivinha? Você precisa passar uma string pela conexão para autenticação . Então, vamos converter nosso SecureStringem um System.String.... e agora temos uma string no heap sem nenhuma maneira de forçá-lo a passar pela coleta de lixo (ou escrever 0 no seu buffer).

Meu ponto é : não importa o que você faz, em algum lugar ao longo da linha, que SecureStringestá indo para ser convertido em um System.String, o que significa que vai pelo menos existem na pilha em algum ponto (sem qualquer garantia de coleta de lixo).

O que eu quero dizer não é : se existem maneiras de contornar o envio de uma string para uma conexão ssh ou contornar o fato de um controle armazenar uma string (faça um controle personalizado). Para esta pergunta, você pode substituir "conexão ssh" por "formulário de login", "formulário de registro", "formulário de pagamento", "alimentos que você alimentaria seu filhote de cachorro, mas não seus filhos", etc.

  • Então, em que momento o uso de um SecureStringrealmente se torna prático?
  • Vale a pena o tempo extra de desenvolvimento para erradicar completamente o uso de um System.Stringobjeto?
  • O SecureStringobjetivo é simplesmente reduzir a quantidade de tempo System.Stringem que está na pilha (reduzindo o risco de mudar para um arquivo de troca físico)?
  • Se um invasor já tem os meios para uma inspeção de pilha, então provavelmente (A) já tem meios para ler as teclas digitadas, ou (B) já possui fisicamente a máquina ... Então, usando um SecureStringimpedimento ele chegaria ao dados de qualquer maneira?
  • Isso é apenas "segurança através da obscuridade"?

Desculpe se eu estou colocando as perguntas muito grossas, a curiosidade acabou de me superar. Sinta-se à vontade para responder a uma ou todas as minhas perguntas (ou diga que minhas suposições estão completamente erradas). :)

Steven Jeffries
fonte
25
Lembre-se de que SecureStringnão é realmente uma string segura. É apenas uma maneira de diminuir a janela de tempo em que alguém pode inspecionar sua memória e obter com êxito os dados confidenciais. Isso não é à prova de balas e não era para ser. Mas os pontos que você está levantando são muito válidos. Relacionados: stackoverflow.com/questions/14449579/...
Theodoros Chatzigiannakis
1
@TheodorosChatzigiannakis, sim, foi o que eu imaginei. Passei o dia todo hoje pensando em uma maneira segura de armazenar uma senha para a vida útil do aplicativo, e isso me fez pensar: vale a pena?
Steven Jeffries
1
Provavelmente não vale a pena. Mas, novamente, você pode estar se defendendo contra um cenário extremo. Extremo não no sentido em que é improvável que alguém obtenha esse tipo de acesso ao seu computador, mas no sentido de que se alguém obtém esse tipo de acesso, o computador é considerado (para todos os efeitos) comprometido e eu não ' Não pense que exista qualquer linguagem ou técnica que você possa usar para se defender completamente disso.
Theodoros Chatzigiannakis
8
Ele protege contra uma senha visível por anos , muito tempo depois que todo mundo parou de pensar que poderia haver um problema. Em um disco rígido descartado, armazenado no arquivo de paginação. Esfregar a memória para que as chances de isso sejam baixas é importante, não é possível fazer isso com o System.String, pois é imutável. Requer infraestrutura que poucos programas hoje em dia têm acesso, interoperabilidade com código nativo, para que os métodos Marshal.SecureStringXxx () sejam úteis esteja ficando raro. Ou uma maneira decente para solicitar ao usuário que :)
Hans Passant
1
SecureString é uma técnica detalhada de segurança. A troca entre custo de desenvolvimento e ganho de segurança não é favorável na maioria das situações. Existem muito poucos casos de uso bons para isso.
usr

Respostas:

237

Na verdade, existem usos muito práticos do SecureString.

Você sabe quantas vezes eu já vi esses cenários? (a resposta é: muitos!):

  • Uma senha aparece em um arquivo de log acidentalmente.
  • Uma senha está sendo mostrada em algum lugar - uma vez que uma GUI mostrava uma linha de comando do aplicativo que estava sendo executada, e a linha de comando consistia em senha. Oops .
  • Usando o perfil de memória para criar um perfil do software com seu colega. O colega vê sua senha na memória. Parece irreal? De modo nenhum.
  • Uma vez eu usei RedGatesoftware que poderia capturar o "valor" de variáveis ​​locais em caso de exceções, incrivelmente útil. No entanto, posso imaginar que ele registrará "senhas de string" acidentalmente.
  • Um despejo de memória que inclui senha da string.

Você sabe como evitar todos esses problemas? SecureString. Geralmente, garante que você não cometa erros tolos como tal. Como isso evita? Garantindo que a senha seja criptografada na memória não gerenciada e que o valor real só possa ser acessado quando você tiver 90% de certeza do que está fazendo.

No sentido, SecureStringfunciona com bastante facilidade:

1) Tudo está criptografado

2) Chamadas de usuário AppendChar

3) Descriptografe tudo em MEMÓRIA NÃO GERENCIADA e adicione o personagem

4) Criptografe tudo novamente em MEMÓRIA NÃO GERENCIADA.

E se o usuário tiver acesso ao seu computador? Um vírus seria capaz de obter acesso a tudo isso SecureStrings? Sim. Tudo o que você precisa fazer é conectar-se RtlEncryptMemoryquando a memória estiver sendo descriptografada, você obterá a localização do endereço de memória não criptografado e a lerá. Voila! De fato, você pode criar um vírus que constantemente procura o uso SecureStringe registra todas as atividades nele. Não estou dizendo que será uma tarefa fácil, mas pode ser feita. Como você pode ver, a "força" de SecureStringdesaparece completamente quando há um usuário / vírus em seu sistema.

Você tem alguns pontos em sua postagem. Claro, se você usar alguns dos controles da interface do usuário que contêm internamente uma "senha de string", usar o real SecureStringnão é tão útil. Embora, ainda assim, possa proteger contra alguma estupidez listada acima.

Além disso, como outros observaram, o WPF suporta o PasswordBox, que é usado SecureStringinternamente por meio da propriedade SecurePassword .

A linha inferior é; se você tiver dados confidenciais (senhas, cartões de crédito, SecureStringetc. ), use . É isso que o C # Framework está seguindo. Por exemplo, a NetworkCredentialclasse armazena a senha como SecureString. Se você olhar para isso , poderá ver mais de 80 usos diferentes no .NET framework de SecureString.

Há muitos casos em que você precisa converter SecureStringpara string, porque algumas API esperam isso.

O problema usual é:

  1. A API é GENERIC. Ele não sabe que há dados confidenciais.
  2. A API sabe que está lidando com dados confidenciais e usa "string" - isso é apenas um design ruim.

Você levantou um ponto positivo: o que acontece quando SecureStringé convertido string? Isso só pode acontecer por causa do primeiro ponto. Por exemplo, a API não sabe que são dados confidenciais. Eu pessoalmente não vi isso acontecer. Obter string do SecureString não é assim tão simples.

Não é simples por uma razão simples ; ele nunca teve a intenção de permitir que o usuário convertesse o SecureString em string, como você afirmou: o GC entrará em ação. Se você se vê fazendo isso, precisa se afastar e se perguntar: por que estou fazendo isso ou preciso mesmo? isso porque?

Há um caso interessante que eu vi. Ou seja, a função WinApi LogonUser usa LPTSTR como uma senha, o que significa que você precisa ligar SecureStringToGlobalAllocUnicode. Isso basicamente fornece uma senha não criptografada que fica na memória não gerenciada. Você precisa se livrar disso assim que terminar:

// Marshal the SecureString to unmanaged memory.
IntPtr rawPassword = Marshal.SecureStringToGlobalAllocUnicode(password);
try
{
   //...snip...
}
finally 
{
   // Zero-out and free the unmanaged string reference.
   Marshal.ZeroFreeGlobalAllocUnicode(rawPassword);
}

Você sempre pode estender a SecureStringclasse com um método de extensão, como ToEncryptedString(__SERVER__PUBLIC_KEY), que fornece uma stringinstância SecureStringcriptografada usando a chave pública do servidor. Somente o servidor pode descriptografá-lo. Problema resolvido: a Coleta de Lixo nunca verá a sequência "original", pois você nunca a expõe na memória gerenciada. É exatamente isso que está sendo feito em PSRemotingCryptoHelper( EncryptSecureStringCore(SecureString secureString)).

E como algo muito relacionado: o Mono SecureString não criptografa . A implementação foi comentada porque .. aguarde .. "De alguma forma, causa quebra de teste da unidade" , o que traz ao meu último ponto:

SecureStringnão é suportado em qualquer lugar. Se a plataforma / arquitetura não suportar SecureString, você receberá uma exceção. Há uma lista de plataformas suportadas na documentação.

Erti-Chris Eelmaa
fonte
Eu pensaria que SecureStringseria muito mais prático se houvesse um mecanismo para acessar caracteres individuais dos mesmos. A eficiência desse mecanismo pode ser aprimorada com um tipo de estrutura SecureStringTempBuffersem nenhum membro público, que pode ser passado refpara o SecureStringmétodo "ler um caractere" [se a criptografia / descriptografia for processada em blocos de 8 caracteres, essa estrutura poderá conter dados para 8 caracteres e a localização do primeiro desses caracteres dentro de SecureString].
Supercat
2
Eu sinalizo isso em todas as auditorias de código fonte que realizo. Também deve-se repetir que, se você usá- SecureStringlo, precisa ser usado por toda a pilha.
Casey
1
Eu implementei a extensão .ToEncryptedString (), mas usando o certificado. Você se importaria de dar uma olhada e me avise se estou fazendo tudo errado? Espero que ele seja seguro. Gist.github.com/sturlath/99cfbfff26e0ebd12ea73316d0843330
Sturla
@Sturla - O que há EngineSettingsno seu método de extensão?
InteXX 28/05
15

Existem poucas questões em suas suposições.

Primeiro de tudo, a classe SecureString não possui um construtor String. Para criar um, você aloca um objeto e depois acrescenta os caracteres.

No caso de uma GUI ou console, você pode facilmente passar cada tecla pressionada para uma string segura.

A classe é projetada de uma maneira que você não pode, por engano, acessar o valor que está armazenado. Isso significa que você não pode obter a stringsenha diretamente dela.

Portanto, para usá-lo, por exemplo, para autenticar através da Web, você precisará usar classes apropriadas que também são seguras.

Na estrutura .NET, você tem algumas classes que podem usar o SecureString

  • O controle PasswordBox do WPF mantém a senha como um SecureString internamente.
  • A propriedade Senha do System.Diagnostics.ProcessInfo é uma SecureString.
  • O construtor para X509Certificate2 usa um SecureString para a senha.

(Mais)

Para concluir, a classe SecureString pode ser útil, mas requer mais atenção do desenvolvedor.

Tudo isso, com exemplos, está bem descrito na documentação do SecureString do MSDN

Damian Leszczyński - Vash
fonte
10
Não entendo bem o terceiro ao último parágrafo da sua resposta. Como você transfere um SecureStringpela rede sem serializá-lo em um string? Eu acho que o ponto da pergunta da OP é que chega um momento em que você deseja realmente usar o valor mantido com segurança em a SecureString, o que significa que você precisa tirá-lo de lá e não é mais seguro. Em toda a biblioteca de classes do Framework, praticamente não existem métodos que aceitem a SecureStringentrada como diretamente (tanto quanto eu posso ver), então qual é o sentido de manter um valor dentro de uma SecureStringem primeiro lugar?
stakx - não está mais contribuindo com o
@ DamianLeszczyński-Vash, eu sei que o SecureString não tem um construtor de string, mas o que quero dizer é que você (A) cria um SecureString e acrescenta cada caractere de uma string a ela, ou (B) em algum momento precisa usar o valor armazenado no SecureString como uma sequência para transmiti-lo a alguma API. Dito isto, você traz alguns pontos positivos, e posso ver como, em alguns casos muito específicos, pode ser útil.
Steven Jeffries
1
@stakx Você poderia enviá-lo pela rede obtendo char[]e enviando cada charum em um soquete - não criando objetos colecionáveis ​​no processo.
precisa saber é o seguinte
Char [] é colecionável e mutável, portanto, pode ser substituído.
Peter Ritchie
Claro, também é fácil esquecer de substituir esse array. De qualquer forma, qualquer API para a qual você passa os caracteres também pode fazer uma cópia.
Chao
10

Um SecureString é útil se:

  • Você o constrói caractere por caractere (por exemplo, da entrada do console) ou obtém-o de uma API não gerenciada

  • Você o usa passando-o para uma API não gerenciada (SecureStringToBSTR).

Se você alguma vez convertê-lo em uma sequência gerenciada, será derrotado.

UPDATE em resposta ao comentário

... ou BSTR como você mencionou, o que não parece mais seguro

Após a conversão para um BSTR, o componente não gerenciado que consome o BSTR pode zerar a memória. A memória não gerenciada é mais segura no sentido de que pode ser redefinida dessa maneira.

No entanto, existem muito poucas APIs no .NET Framework que oferecem suporte ao SecureString, portanto, você está certo ao dizer que é de valor muito limitado hoje.

O principal caso de uso que eu veria é em um aplicativo cliente que requer que o usuário insira um código ou senha altamente confidencial. A entrada do usuário pode ser usada caractere por caractere para criar um SecureString; depois, isso pode ser passado para uma API não gerenciada, que zera o BSTR recebido após o uso. Qualquer despejo de memória subsequente não conterá a seqüência sensível.

Em um aplicativo para servidor, é difícil ver onde isso seria útil.

ATUALIZAÇÃO 2

Um exemplo de uma API .NET que aceita um SecureString é esse construtor para a classe X509Certificate . Se você digitar com ILSpy ou similar, verá que o SecureString é convertido internamente em um buffer não gerenciado ( Marshal.SecureStringToGlobalAllocUnicode), que é zerado ao terminar com ( Marshal.ZeroFreeGlobalAllocUnicode).

Joe
fonte
1
+1, mas você não acaba sempre "derrotando seu objetivo"? Dado que quase nenhum método na Biblioteca de Classes do Framework aceita a SecureStringcomo entrada, qual seria o uso deles? Você sempre precisaria converter novamente para stringmais cedo ou mais tarde (ou, BSTRcomo você mencionou, o que não parece mais seguro). Então, por que se preocupar SecureString?
stakx - não está mais contribuindo com o
5

A Microsoft não recomenda o uso SecureStringde códigos mais recentes.

Na documentação da classe SecureString :

Importante

Não recomendamos que você use a SecureStringclasse para novos desenvolvimentos. Para obter mais informações, consulte SecureStringnão deve ser usado.

O que recomenda:

Não use SecureStringpara novo código. Ao portar código para o .NET Core, considere que o conteúdo da matriz não está criptografado na memória.

A abordagem geral de lidar com credenciais é evitá-las e, em vez disso, contar com outros meios de autenticação, como certificados ou autenticação do Windows. no GitHub.

SᴇM
fonte
4

Como você identificou corretamente, SecureStringoferece uma vantagem específica sobre string: apagamento determinístico. Há dois problemas com esse fato:

  1. Como outros já mencionaram e como você notou por si mesmo, isso não basta por si só. Você deve garantir que todas as etapas do processo (incluindo recuperação de entrada, construção da string, uso, exclusão, transporte etc.) ocorram sem prejudicar o objetivo do uso SecureString. Isso significa que você deve ter cuidado para nunca criar um imutável gerenciado pelo GC stringou qualquer outro buffer que armazene as informações confidenciais (ou será necessário acompanhar isso também). Na prática, isso nem sempre é fácil de conseguir, porque muitas APIs oferecem apenas uma maneira de trabalhar string, não SecureString. E mesmo se você conseguir tudo certo ...
  2. SecureStringprotege contra tipos muito específicos de ataque (e para alguns deles, nem é tão confiável). Por exemplo, SecureStringpermite reduzir a janela de tempo em que um invasor pode despejar a memória do seu processo e extrair com êxito as informações confidenciais (novamente, como você apontou corretamente), mas esperando que a janela seja muito pequena para o invasor tirar um instantâneo da sua memória não é considerado segurança.

Então, quando você deve usá-lo? Somente quando você estiver trabalhando com algo que possa permitir que você trabalhe SecureStringpara todas as suas necessidades e, mesmo assim, você deve estar ciente de que isso é seguro apenas em circunstâncias específicas.

Theodoros Chatzigiannakis
fonte
2
Você está assumindo que um invasor pode capturar instantaneamente a memória de um processo a qualquer momento. Esse não é necessariamente o caso. Considere um despejo de memória. Se a falha ocorreu depois que o aplicativo foi concluído com os dados confidenciais, o despejo de memória não deve conter os dados confidenciais.
4

O texto abaixo é copiado do analisador de código estático HP Fortify

Resumo: O método PassString () no PassGenerator.cs armazena dados confidenciais de maneira insegura (por exemplo, em string), possibilitando extrair os dados inspecionando a pilha.

Explicação: Dados confidenciais (como senhas, números de previdência social, números de cartão de crédito etc.) armazenados na memória podem vazar se forem armazenados em um objeto String gerenciado. Os objetos de sequência não são fixados; portanto, o coletor de lixo pode realocar esses objetos à vontade e deixar várias cópias na memória. Como esses objetos não são criptografados por padrão, qualquer pessoa que possa ler a memória do processo poderá ver o conteúdo. Além disso, se a memória do processo for trocada para o disco, o conteúdo não criptografado da sequência será gravado em um arquivo de troca. Por fim, como os objetos String são imutáveis, a remoção do valor de uma String da memória pode ser feita apenas pelo coletor de lixo CLR. O coletor de lixo não precisa ser executado, a menos que o CLR esteja com pouca memória, portanto não há garantia de quando a coleta de lixo ocorrerá.

Recomendações: em vez de armazenar dados confidenciais em objetos como Strings, armazene-os em um objeto SecureString. Cada objeto armazena seu conteúdo em um formato criptografado na memória o tempo todo.

vikrantx
fonte
3

Eu gostaria de abordar este ponto:

Se um invasor já tem os meios para uma inspeção de pilha, é provável que (A) já tenha meios de ler as teclas digitadas, ou (B) já possua fisicamente a máquina ... Então, usando um SecureStringimpediria que eles chegassem ao dados de qualquer maneira?

Um invasor pode não ter acesso total ao computador e ao aplicativo, mas pode ter meios para acessar algumas partes da memória do processo. Geralmente, é causada por erros, como saturação de buffer, quando entradas especialmente construídas podem fazer com que o aplicativo exponha ou substitua parte da memória.

Coração vazando memória

Tome Heartbleed, por exemplo. Solicitações especialmente construídas podem fazer com que o código exponha partes aleatórias da memória do processo ao invasor. Um invasor pode extrair certificados SSL da memória, mas a única coisa de que precisa é apenas usar uma solicitação malformada.

No mundo do código gerenciado, as saturações de buffer se tornam um problema com muito menos frequência. E no caso do WinForms, os dados já estão armazenados de maneira insegura e você não pode fazer nada sobre isso. Isso torna a proteção SecureStringpraticamente inútil.

No entanto, a GUI pode ser programada para uso SecureStringe, nesse caso, a redução da janela de disponibilidade de senha na memória pode valer a pena. Por exemplo, PasswordBox.SecurePassword do WPF é do tipo SecureString.

Athari
fonte
Hmm, definitivamente interessante de saber. Com toda a honestidade, não estou realmente preocupado com o tipo de ataque necessário para obter um valor System.String da minha memória, estou apenas perguntando por pura curiosidade. No entanto, obrigado pela informação! :)
Steven Jeffries
2

Há algum tempo, tive que criar uma interface AC # contra um gateway de pagamento com cartão de crédito java e era necessário criptografar chaves de comunicações seguras compatíveis. Como a implementação do Java era bastante específica e eu tive que trabalhar com os dados protegidos de uma determinada maneira.

Achei esse design bastante fácil de usar e mais fácil do que trabalhar com o SecureString… para quem gosta de usar… fique à vontade, sem restrições legais :-). Observe que essas classes são internas, pode ser necessário torná-las públicas.

namespace Cardinity.Infrastructure
{
    using System.Security.Cryptography;
    using System;
    enum EncryptionMethods
    {
        None=0,
        HMACSHA1,
        HMACSHA256,
        HMACSHA384,
        HMACSHA512,
        HMACMD5
    }


internal class Protected
{
    private  Byte[] salt = Guid.NewGuid().ToByteArray();

    protected byte[] Protect(byte[] data)
    {
        try
        {
            return ProtectedData.Protect(data, salt, DataProtectionScope.CurrentUser);
        }
        catch (CryptographicException)//no reason for hackers to know it failed
        {
#if DEBUG
            throw;
#else
            return null;
#endif
        }
    }

    protected byte[] Unprotect(byte[] data)
    {
        try
        {
            return ProtectedData.Unprotect(data, salt, DataProtectionScope.CurrentUser);
        }
        catch (CryptographicException)//no reason for hackers to know it failed
        {
#if DEBUG
            throw;
#else
            return null;
#endif
        }
    }
}


    internal class SecretKeySpec:Protected,IDisposable
    {
        readonly EncryptionMethods _method;

        private byte[] _secretKey;
        public SecretKeySpec(byte[] secretKey, EncryptionMethods encryptionMethod)
        {
            _secretKey = Protect(secretKey);
            _method = encryptionMethod;
        }

        public EncryptionMethods Method => _method;
        public byte[] SecretKey => Unprotect( _secretKey);

        public void Dispose()
        {
            if (_secretKey == null)
                return;
            //overwrite array memory
            for (int i = 0; i < _secretKey.Length; i++)
            {
                _secretKey[i] = 0;
            }

            //set-null
            _secretKey = null;
        }
        ~SecretKeySpec()
        {
            Dispose();
        }
    }

    internal class Mac : Protected,IDisposable
    {
        byte[] rawHmac;
        HMAC mac;
        public Mac(SecretKeySpec key, string data)
        {

            switch (key.Method)
            {
                case EncryptionMethods.HMACMD5:
                    mac = new HMACMD5(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA512:
                    mac = new HMACSHA512(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA384:
                    mac = new HMACSHA384(key.SecretKey);
                    break;
                case EncryptionMethods.HMACSHA256:
                    mac = new HMACSHA256(key.SecretKey);

                break;
                case EncryptionMethods.HMACSHA1:
                    mac = new HMACSHA1(key.SecretKey);
                    break;

                default:                    
                    throw new NotSupportedException("not supported HMAC");
            }
            rawHmac = Protect( mac.ComputeHash(Cardinity.ENCODING.GetBytes(data)));            

        }

        public string AsBase64()
        {
            return System.Convert.ToBase64String(Unprotect(rawHmac));
        }

        public void Dispose()
        {
            if (rawHmac != null)
            {
                //overwrite memory address
                for (int i = 0; i < rawHmac.Length; i++)
                {
                    rawHmac[i] = 0;
                }

                //release memory now
                rawHmac = null;

            }
            mac?.Dispose();
            mac = null;

        }
        ~Mac()
        {
            Dispose();
        }
    }
}
Walter Vehoeven
fonte