C ++: Astúcia e amor, ou o que poderia dar errado?





“C torna mais fácil atirar no próprio pé. É mais difícil fazer isso em C ++, mas vai custar muito. ”- Björn Stroustrup, C ++ Creator.


Neste artigo, mostraremos como escrever código estável, seguro e confiável e como é fácil quebrá-lo de forma totalmente não intencional. Para isso, tentamos coletar o material mais útil e estimulante.







Na SimbirSoft, trabalhamos em estreita colaboração com o projeto Secure Code Warrior para treinar outros desenvolvedores a criar soluções seguras. Especialmente para Habr, traduzimos um artigo escrito por nosso autor para o portal CodeProject.com.



Então, para o código!



Aqui está um pequeno código C ++ abstrato. Este código foi especialmente escrito para demonstrar todos os tipos de problemas e vulnerabilidades que podem ser encontrados em projetos muito reais. Como você pode ver, este é um código DLL do Windows (este é um ponto importante). Suponha que alguém vá usar esse código em alguma solução (segura, é claro).



Dê uma olhada no código. O que, em sua opinião, pode dar errado nisso?



O código
class Finalizer
{
    struct Data
    {
        int i = 0;
        char* c = nullptr;
        
        union U
        {
            long double d;
            
            int i[sizeof(d) / sizeof(int)];
            
            char c [sizeof(i)];
        } u = {};
        
        time_t time;
    };
    
    struct DataNew;
    DataNew* data2 = nullptr;
    
    typedef DataNew* (*SpawnDataNewFunc)();
    SpawnDataNewFunc spawnDataNewFunc = nullptr;
    
    typedef Data* (*Func)();
    Func func = nullptr;
    
    Finalizer()
    {
        func = GetProcAddress(OTHER_LIB, "func")
        
        auto data = func();
        
        auto str = data->c;
        
        memset(str, 0, sizeof(str));
        
        data->u.d = 123456.789;
        
        const int i0 = data->u.i[sizeof(long double) - 1U];
        
        spawnDataNewFunc = GetProcAddress(OTHER_LIB, "SpawnDataNewFunc")
        data2 = spawnDataNewFunc();
    }
    
    ~Finalizer()
    {
        auto data = func();
        
        delete[] data2;
    }
};

Finalizer FINALIZER;

HMODULE OTHER_LIB;
std::vector<int>* INTEGERS;

DWORD WINAPI Init(LPVOID lpParam)
{
    OleInitialize(nullptr);
    
    ExitThread(0U);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    static std::vector<std::thread::id> THREADS;
    
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            CoInitializeEx(nullptr, COINIT_MULTITHREADED);
            
            srand(time(nullptr));
            
            OTHER_LIB = LoadLibrary("B.dll");
            
            if (OTHER_LIB = nullptr)
                return FALSE;
            
            CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);
        break;
        
        case DLL_PROCESS_DETACH:
            CoUninitialize();
            
            OleUninitialize();
            {
                free(INTEGERS);
                
                const BOOL result = FreeLibrary(OTHER_LIB);
                
                if (!result)
                    throw new std::runtime_error("Required module was not loaded");
                
                return result;
            }
        break;
        
        case DLL_THREAD_ATTACH:
            THREADS.push_back(std::this_thread::get_id());
        break;
        
        case DLL_THREAD_DETACH:
            THREADS.pop_back();
        break;
    }
    return TRUE;
}

__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()
{
    for (int i : integers)
        i *= c;
    
    INTEGERS = new std::vector<int>(integers);
}

int Random()
{
    return rand() + rand();
}

__declspec(dllexport) long long int __cdecl _GetInt(int a)
{
    return 100 / a <= 0 ? a : a + 1 + Random();
}




Talvez você tenha achado este código simples, óbvio e seguro o suficiente? Ou talvez você tenha encontrado alguns problemas nele? Talvez até uma dúzia ou duas?



Bem, na verdade existem mais de 43 ameaças potenciais de vários graus de significância neste trecho !







O que você deve prestar atenção



1) sizeof (d) (onde d é um duplo longo) não é necessariamente um múltiplo de sizeof (int)



int i[sizeof(d) / sizeof(int)];


Esta situação não é testada ou tratada aqui. Por exemplo, um duplo longo pode ter 10 bytes em algumas plataformas (o que não é verdade para o compilador MS VS , mas é verdade para RAD Studio , anteriormente conhecido como C ++ Builder ).



int também pode ter tamanhos diferentes dependendo da plataforma (o código acima é para Windows , portanto, em relação a esta situação particular, o problema é um tanto artificial, mas para código portátil este problema é muito relevante).



Tudo isso pode se tornar um problema se quisermos usar o chamado trocadilho de digitação . A propósito, isso causa comportamento indefinidode acordo com o padrão de linguagem C ++. No entanto, é prática comum usar o trocadilho de digitação , já que os compiladores modernos geralmente definem o comportamento esperado correto para um determinado caso (como, por exemplo, o GCC faz ).







Fonte: Medium.com



A propósito, ao contrário de C ++, em C moderno, o trocadilho de digitação é perfeitamente aceitável (você sabe que C ++ e C são linguagens diferentes , e você não deve esperar saber C se souber C ++, e o contrário, certo?)



Solução: use static_assertpara controlar todas essas suposições em tempo de compilação. Ele irá avisá-lo se algo der errado com os tamanhos dos tipos:



static_assert(0U == (sizeof(d) % sizeof(int)), “Houston, we have a problem”);


2) time_t é uma macro, no Visual Studio pode se referir ao tipo inteiro de 32 bits (antigo) ou 64 bits (novo)



time_t time;


O acesso a uma variável deste tipo a partir de diferentes módulos executáveis ​​(por exemplo, o arquivo executável e a DLL que carrega) pode levar à leitura / gravação fora dos limites do objeto, se os dois binários forem compilados com uma representação física diferente desse tipo. O que, por sua vez, levará à corrupção da memória ou leituras incorretas.







Solução: certifique-se de que os mesmos tipos de tamanho estritamente definido sejam usados ​​para a troca de dados entre todos os módulos:



int64_t time;


3) B.dll (cujo identificador é armazenado pela variável OTHER_LIB ) ainda não foi carregado no momento em que acessamos a variável acima, portanto não podemos obter os endereços das funções desta biblioteca



4) o problema com a ordem de inicialização dos objetos estáticos ( SIOF ): (objeto OTHER_LIB usado no código antes de ser inicializado)



func = GetProcAddress(OTHER_LIB, "func");


FINALIZER é um objeto estático criado antes de chamar a função DllMain . Em seu construtor, estamos tentando usar uma biblioteca que ainda não foi carregada. O problema é agravado pelo fato de que o OTHER_LIB estático que é usado pelo FINALIZER estático é colocado na unidade de tradução a jusante. Isso significa que ele também será inicializado (zerado) posteriormente. Ou seja, no momento em que for acessado, conterá algum lixo pseudo-aleatório . WinAPIem geral, ele deve reagir normalmente a isso, porque com um alto grau de probabilidade simplesmente não haverá um módulo carregado com tal descritor. E mesmo que haja uma coincidência absolutamente incrível e ainda assim será - é improvável que tenha uma função chamada "Func" .



Solução: O conselho geral é evitar o uso de objetos globais, especialmente os complexos, especialmente se eles dependerem uns dos outros, especialmente em DLLs . No entanto, se você ainda precisar deles por algum motivo, seja extremamente cuidadoso e cuidadoso com a ordem em que são inicializados. Para controlar esta ordem , coloque todas as instâncias (definições) de objetos globais em uma unidade de traduçãona ordem correta para garantir que eles sejam inicializados corretamente.



5) o resultado retornado anteriormente não é verificado antes do uso



auto data = func();


func é um ponteiro de função . E deve apontar para uma função de B.dll . No entanto, como falhamos completamente em tudo na etapa anterior, será nullptr . Assim, tentando desreferenciá-lo, em vez da chamada de função esperada, obtemos uma violação de acesso ou uma falha de proteção geral ou algo parecido.



Solução: ao trabalhar com código externo (no nosso caso com WinAPI ), verifique sempre o resultado do retorno das funções chamadas. Para sistemas confiáveis ​​e tolerantes a falhas, essa regra se aplica até mesmo a funções para as quais há um contrato estrito [sobre o que devem ser devolvidos e quando].



6) leitura / gravação de lixo ao trocar dados entre módulos compilados com diferentes configurações de alinhamento / preenchimento



auto str = data->c;


Se a estrutura de dados (que é usada para trocar informações entre módulos de comunicação) tiver esses mesmos módulos em apresentações físicas diferentes, isso resultará em todas as violações de acesso mencionadas anteriormente , uma proteção de memória de erro , segmentação de falha , corrupção de heap , etc. Ou vamos apenas ler o lixo. O resultado exato dependerá do cenário de uso real para esta memória. Tudo isso pode acontecer porque não há configurações de alinhamento / preenchimento explícitas para a própria estrutura . Portanto, se essas configurações globais no momento da compilação forem diferentes para os módulos de interação, teremos problemas.







Decisão:certifique-se de que todas as estruturas de dados compartilhadas tenham uma representação física forte, explicitamente definida e óbvia (usando tipos de tamanho fixo, alinhamento explicitamente especificado, etc.) e / ou binários interoperáveis ​​foram compilados com as mesmas configurações de alinhamento global / o preenchimento.



Veja também
Alignment (C++ Declarations)

Data structure alignment

Struct padding in C++



7) usar o tamanho de um ponteiro para uma matriz em vez do tamanho da própria matriz



memset(str, 0, sizeof(str));


Isso geralmente é o resultado de um erro de digitação trivial. Mas esse problema também pode surgir ao lidar com polimorfismo estático ou quando a palavra-chave auto é usada impensadamente ( especialmente quando está claramente sendo usada em excesso ). Gostaríamos de esperar, entretanto, que os compiladores modernos sejam inteligentes o suficiente para detectar esses problemas em tempo de compilação, usando os recursos de um analisador estático interno .



Decisão:



  • nunca confunda sizeof ( <tipo de objeto completo> ) e sizeof ( <tipo de ponteiro de objeto> );
  • não ignore os avisos do compilador ;

  • você também pode usar um pouco da mágica padrão do C ++ combinando typeid, constexpr e static_assert para garantir que os tipos estejam corretos em tempo de compilação ( características de tipo também podem ser úteis aqui , em particular std :: is_pointer ).


8) comportamento indefinido ao tentar ler um campo de união diferente do que foi usado anteriormente para definir o valor



9) uma tentativa de leitura fora dos limites é possível se o comprimento de um duplo longo difere entre os módulos binários



const int i0 = data->u.i[sizeof(long double) - 1U];


Isso já foi mencionado anteriormente, então aqui temos apenas outro ponto de presença do problema mencionado anteriormente.



Solução: Não se refira a um campo diferente daquele que você definiu anteriormente, a menos que tenha certeza de que seu compilador está manipulando-o corretamente. Certifique-se de que os tamanhos dos tipos de objetos compartilhados sejam os mesmos em todos os módulos de interação.



Veja também
Type-punning and strict-aliasing

What is the Strict Aliasing Rule and Why do we care?



10) mesmo se B.dll foi carregado corretamente e a função "func" foi exportada e importada corretamente, B.dll ainda é descarregado da memória neste momento (porque a função de sistema FreeLibrary foi anteriormente chamada na seção DLL_PROCESS_DETACH da função de retorno de chamada DllMain )



auto data = func();


Chamar uma função virtual em um objeto previamente destruído do tipo polimórfico, bem como chamar uma função em uma biblioteca dinâmica já descarregada, provavelmente resultará em um erro de chamada virtual puro .



Solução: implemente o procedimento de finalização correto no aplicativo para garantir que todas as bibliotecas dinâmicas sejam concluídas / descarregadas na ordem correta. Evite usar objetos estáticos com lógica complexa em DL L. Evite realizar quaisquer operações dentro da biblioteca após chamar DllMain / DLL_PROCESS_DETACH (quando a biblioteca entra em seu último estágio de seu ciclo de vida - a fase de destruição de seus objetos estáticos).



Você precisa entender qual é o ciclo de vida de uma DLL:



) LoadLibrary



  • ( , )
  • DllMain -> DLL_PROCESS_ATTACH ( , )
  • [] DllMain -> DLL_THREAD_ATTACH / DLL_THREAD_DETACH ( , . 30).
  • , , (, ),
  • ( / , , )
  • , ()
  • ( / , , )
  • - : ,


) FreeLibrary



  • DllMain -> DLL_PROCESS_DETACH ( , )
  • ( , )






11) excluir um ponteiro opaco (o compilador precisa saber o tipo completo para chamar o destruidor, portanto, excluir um objeto usando um ponteiro opaco pode levar a vazamentos de memória e outros problemas)



12) se o destruidor DataNew for virtual, mesmo se a classe for exportada e importada corretamente e o completo informações sobre ele, de qualquer maneira chamar seu destruidor neste estágio é um problema - isso provavelmente levará a uma chamada de função puramente virtual (uma vez que o tipo DataNew é importado do arquivo B.dll já descarregado ). Esse problema é possível mesmo se o destruidor não for virtual.



13) se a classe DataNew for um tipo polimórfico abstrato, e sua classe base possui um destruidor virtual puro sem um corpo, em qualquer caso ocorrerá uma chamada de função virtual pura.



14) comportamento indefinido se a memória for alocada usando novo e excluído usando delete []



delete[] data2;


Em geral, você deve sempre ter cuidado ao liberar objetos obtidos de módulos externos.



Também é uma boa prática zerar ponteiros para objetos destruídos.



Decisão:



  • ao deletar um objeto, seu tipo completo deve ser conhecido
  • todos os destruidores devem ter um corpo
  • a biblioteca da qual o código é exportado não deve ser descarregada muito cedo
  • sempre use os diferentes formulários novos e exclua corretamente, não os confunda
  • o ponteiro para o objeto remoto deve ser zerado.






Observe também o seguinte:

- chamar delete em um ponteiro para void resultará em comportamento indefinido

funções puramente virtuais não devem ser chamadas do construtor

- chamar uma função virtual no construtor não será virtual

- tente evitar o gerenciamento manual de memória - use contêineres , mova semântica e dicas inteligentes



Veja também
Heap corruption: What could the cause be?



15) ExitThread é o método preferido para sair de um thread em C. Em C ++, chamar esta função encerrará o thread antes de chamar os destruidores de objetos locais (e qualquer outra limpeza automática), portanto, encerrar um thread em C ++ deve ser feito simplesmente retornando da função de thread



ExitThread(0U);


Solução: nunca use manualmente esta função em código C ++.



16) no corpo de DllMain, chamar qualquer função padrão que requeira DLLs de sistema diferentes de Kernel32.dll pode levar a vários problemas difíceis de diagnosticar



CoInitializeEx(nullptr, COINIT_MULTITHREADED);


Solução em DllMain:



  • evite qualquer (des) inicialização complicada
  • evite chamar funções de outras bibliotecas (ou pelo menos seja extremamente cuidadoso com isso)






17) inicialização incorreta do gerador de número pseudoaleatório em um ambiente multithread



18) uma vez que o tempo retornado pela função time tem uma resolução de 1 segundo, qualquer thread no programa que chame essa função durante este período de tempo receberá o mesmo valor na saída. Usar esse número para inicializar o PRNG pode levar a colisões (por exemplo, a geração dos mesmos nomes pseudo-aleatórios para arquivos temporários, os mesmos números de porta, etc.). Uma solução possível é misturar ( xor ) o resultado resultante com algum valor pseudo-aleatório , como o endereço de qualquer pilha ou objeto no heap, um tempo mais preciso, etc.



srand(time(nullptr));


Solução: MS VS requer inicialização PRNG para cada thread . Além disso, usar o tempo Unix como um inicializador fornece entropia insuficiente , uma geração de valor de inicialização mais avançada é preferida .



Veja também
Is there an alternative to using time to seed a random number generation?

C++ seeding surprises

Getting random numbers in a thread-safe way [C#]


19) pode travar ou travar (ou criar loops de dependência na ordem de carregamento de DLL )



OTHER_LIB = LoadLibrary("B.dll");


Solução: Não use LoadLibrary no ponto de entrada DllMain . Qualquer (des) inicialização complexa deve ser executada em certas funções exportadas do desenvolvedor DLL, como "Init" e "Deint" . A biblioteca fornece essas funções ao usuário, e o usuário deve chamá-las corretamente no momento certo. Ambas as partes devem cumprir estritamente este contrato.







20) erro de digitação (a condição é sempre falsa), lógica de programa errada e possível vazamento de recursos (porque OTHER_LIB nunca é descarregado no download bem-sucedido)



if (OTHER_LIB = nullptr)
    return FALSE;


O operador de atribuição, copiando, retorna um link do tipo esquerdo, ou seja, if irá verificar o valor OTHER_LIB (que será nullptr) e nullptr será interpretado como falso.



Solução: sempre use a forma reversa para evitar erros de digitação como este:



if/while (<constant> == <variable/expression>)


21) é recomendado usar a função do sistema _beginthread para criar um novo thread no aplicativo (especialmente se o aplicativo foi vinculado a uma versão estática da biblioteca de tempo de execução C), caso contrário, podem ocorrer vazamentos de memória ao chamar ExitThread, DisableThreadLibraryCalls



22) todas as chamadas externas para DllMain são serializadas, portanto, no corpo Esta função não deve tentar criar threads / processos ou interagir com eles, caso contrário, podem ocorrer deadlocks.



CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);


23) chamar funções COM durante o encerramento de DLL pode levar ao acesso incorreto à memória, uma vez que o componente correspondente pode já estar descarregado



CoUninitialize();


24) não há como controlar a ordem de carregamento e descarregamento de serviços COM / OLE em processo, portanto, não chame OleInitialize ou OleUninitialize a partir da função DllMain



OleUninitialize();


Veja também
COM Clients and Servers

In-process, Out-of-process, and Remote Servers



25) chamada gratuita para um bloco de memória alocado com novo



26) se o processo do aplicativo estiver em processo de encerrar seu trabalho (conforme indicado por um valor diferente de zero do parâmetro lpvReserved), todos os threads no processo, exceto o atual, já foram encerrados ou foram interrompidos forçadamente quando chamar a função ExitProcess, que pode deixar alguns dos recursos do processo, como o heap, em um estado inconsistente. Como resultado, não é seguro para DLL limpar recursos . Em vez disso, a DLL deve permitir que o sistema operacional recupere memória.



free(INTEGERS);


Solução: certifique-se de que o antigo estilo C de alocação manual de memória não esteja misturado com o “novo” estilo C ++. Seja extremamente cuidadoso ao gerenciar recursos na função DllMain .



27) pode fazer com que a DLL seja usada mesmo após o sistema ter executado seu código de saída



const BOOL result = FreeLibrary(OTHER_LIB);


Solução: Não chame FreeLibrary no ponto de entrada DllMain.



28) o thread atual (possivelmente principal) irá travar



throw new std::runtime_error("    ");


Solução: evite lançar exceções na função DllMain. Se a DLL não puder ser carregada corretamente por algum motivo, a função deve simplesmente retornar FALSE. Você também não deve lançar exceções da seção DLL_PROCESS_DETACH.



Sempre tome cuidado ao lançar exceções fora da DLL. Quaisquer objetos complexos (por exemplo, classes da biblioteca padrão ) podem ter diferentes representações físicas (e até mesmo lógica de trabalho) em diferentes módulos executáveis ​​se forem compilados com diferentes versões (incompatíveis) das bibliotecas de tempo de execução .







Tente trocar apenas tipos de dados simples entre os módulos(com tamanho fixo e representação binária bem definida).



Lembre-se de que encerrar o encadeamento principal encerrará automaticamente todos os outros encadeamentos (que não serão encerrados corretamente e podem, portanto, danificar a memória, deixando os primitivos de sincronização e outros objetos em um estado imprevisível e incorreto. Além disso, esses encadeamentos já deixarão de existir no momento em que os objetos estáticos iniciarão sua própria desconstrução, então não tente esperar que nenhum segmento termine nos destruidores de objetos estáticos).



Veja também
Top 20 C++ multithreading mistakes and how to avoid them



29) você pode lançar uma exceção (por exemplo, std :: bad_alloc), que não é detectada aqui



THREADS.push_back(std::this_thread::get_id());


Como a seção DLL_THREAD_ATTACH é chamada a partir de algum código externo desconhecido, não espere ver o comportamento correto aqui.



Solução: use try / catch para incluir instruções que podem lançar exceções que provavelmente não podem ser tratadas corretamente (especialmente se saírem da DLL ).



Veja também
How can I handle a destructor that fails?



30) UB se streams foram apresentados antes de carregar esta DLL



THREADS.pop_back();


Threads que já existem no momento em que a DLL é carregada (incluindo aquele que carrega diretamente a DLL ) não chamam a função de ponto de entrada da DLL carregada (é por isso que eles não são registrados com o vetor THREADS durante o evento DLL_THREAD_ATTACH), enquanto eles ainda a chamam com o evento DLL_THREAD_DETACH após a conclusão.

Isso significa que o número de chamadas para as seções DLL_THREAD_ATTACH e DLL_THREAD_DETACH da função DllMain será diferente.



31) é melhor usar tipos inteiros de tamanho fixo



32) passar um objeto complexo entre os módulos pode falhar se compilado com links e configurações de compilação diferentes e sinalizadores (versões diferentes da biblioteca de tempo de execução, etc.)



33) acessar o objeto c por seu endereço virtual (que é compartilhado por módulos) pode causar problemas se os ponteiros forem tratados de forma diferente nesses módulos (por exemplo, se os módulos estiverem associados a parâmetros LARGEADDRESSAWARE diferentes )



__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()


Veja também
Is it possible to use more than 2 Gbytes of memory in a 32-bit program launched in the 64-bit Windows?

Application with LARGEADDRESSAWARE flag set getting less virtual memory

Drawbacks of using /LARGEADDRESSAWARE for 32 bit Windows executables?

how to check if exe is set as LARGEADDRESSAWARE [C#]

/LARGEADDRESSAWARE [Ru]

ASLR (Address Space Layout Randomization) [Ru]



E...
Virtual memory

Physical Address Extension

Tagged pointer

std::ptrdiff_t

What is uintptr_t data type

Pointer arithmetic

Pointer aliasing

What is the strict aliasing rule?

reinterpret_cast conversion

restrict type qualifier



A lista acima dificilmente está completa, então você provavelmente pode adicionar algo importante nos comentários.



Trabalhar com ponteiros é, na verdade, muito mais complexo do que as pessoas geralmente pensam deles. Sem dúvida, desenvolvedores experientes serão capazes de se lembrar de outras nuances e sutilezas existentes (por exemplo, algo sobre a diferença entre ponteiros para um objeto e ponteiros para uma função , por causa do qual, talvez, nem todos os bits do ponteiro podem ser usados , etc. .).







34) uma exceção pode ser lançada dentro de uma função :



INTEGERS = new std::vector<int>(integers);


a especificação throw () desta função está vazia:



__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()


std :: inesperado é chamado pelo tempo de execução C ++ quando uma especificação de exceção é violada: uma exceção é lançada de uma função cuja especificação de exceção não permite exceções desse tipo.



Solução: use try / catch (especialmente ao alocar recursos, principalmente em DLLs ) ou nothrow forma do novo operador. Em qualquer caso, nunca faça a suposição ingênua de que todas as tentativas de alocar vários tipos de recursos sempre terminarão com sucesso .



Veja também
RAII

We do not use C++ exceptions

Memory Limits for Windows and Windows Server Releases









Problema 1: a formação de tal valor "mais aleatório" está incorreta. De acordo com o teorema do limite central , a soma das variáveis ​​aleatórias independentes tende a uma distribuição normal , e não a uma distribuição uniforme (mesmo que os próprios valores originais sejam distribuídos uniformemente).



Problema 2: possível estouro de tipo inteiro (que é um comportamento indefinido para tipos inteiros com sinal )



return rand() + rand();


Ao trabalhar com geradores de números pseudo-aleatórios, criptografia e similares, sempre tome cuidado ao usar "soluções" caseiras. A menos que você tenha formação especializada e experiência nessas áreas altamente específicas, as chances são muito altas de que você simplesmente se supere e piore a situação.



35) o nome da função exportada será decorado (alterado) para evitar esse uso de extern "C"



36) nomes começando com '_' são implicitamente proibidos para C ++, pois este estilo de nomenclatura é reservado para o STL



__declspec(dllexport) long long int __cdecl _GetInt(int a)


Vários problemas (e suas possíveis soluções):



37) rand não é seguro para threads, use rand_r / rand_s ao invés



38) rand está obsoleto, é melhor usar moderno
C++11 <random>


39) não é um fato que a função rand foi inicializada especificamente para o segmento atual (MS VS requer a inicialização desta função para cada segmento onde será chamado)



40) existem geradores especiais de números pseudo-aleatórios , e é melhor usá-los em soluções resistentes a hack (eles são adequados soluções portáteis como Libsodium / randombytes_buf , OpenSSL / RAND_bytes , etc.)



41) divisão potencial por zero: pode causar o travamento da thread atual



42) operadores com precedência diferente são usados ​​na mesma linha , o que introduz o caos na ordem de cálculo - use parênteses e / ou pontos de sequênciapara especificar a sequência óbvia de computação



43) potencial estouro de inteiro



return 100 / a <= 0 ? a : a + 1 + Random();




Veja também
Do not use std::rand() for generating pseudorandom numbers





E...
ExitThread function

ExitProcess function

TerminateThread function

TerminateProcess function





E isso não é tudo!



Imagine que você tenha algum conteúdo importante na memória (por exemplo, a senha de um usuário). Claro, você não quer mantê-lo na memória por mais tempo do que o realmente necessário, aumentando assim a probabilidade de que alguém possa lê-lo de lá .



Uma abordagem ingênua para resolver esse problema seria mais ou menos assim:



bool login(char* const userNameBuf, const size_t userNameBufSize,
           char* const pwdBuf, const size_t pwdBufSize) throw()
{
    if (nullptr == userNameBuf || '\0' == *userNameBuf || nullptr == pwdBuf)
        return false;
    
    // Here some actual implementation, which does not checks params
    //  nor does it care of the 'userNameBuf' or 'pwdBuf' lifetime,
    //   while both of them obviously contains private information 
    const bool result = doLoginInternall(userNameBuf, pwdBuf);
    
    // We want to minimize the time this private information is stored within the memory
    memset(userNameBuf, 0, userNameBufSize);
    memset(pwdBuf, 0, pwdBufSize);
}


E certamente não funcionará da maneira que gostaríamos. O que precisa ser feito? :( "Solução"



incorreta nº 1: se o memset não funcionar, vamos fazer manualmente!



void clearMemory(char* const memBuf, const size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
}


Por que isso também não nos convém? O fato é que não há restrições neste código que não permitiriam que um compilador moderno o otimizasse (aliás, a função memset , se ainda for usada, provavelmente estará embutida ).



Veja também
The as-if rule

Are there situations where this rule does not apply?

Copy elision

Atomics and optimization



"Solução" incorreta nº 2: tente "melhorar" a "solução" anterior brincando com a palavra-chave volátil



void clearMemory(volatile char* const volatile memBuf, const volatile size_t memBufSize) throw()
{
    if (!memBuf || memBufSize < 1U)
        return;
    
    for (volatile size_t idx = 0U; idx < memBufSize; ++idx)
        memBuf[idx] = '\0';
    
    *(volatile char*)memBuf = *(volatile char*)memBuf;
    // There is also possibility for someone to remove this "useless" code in the future
}


Isso vai funcionar? Talvez. Por exemplo, essa abordagem é usada em RtlSecureZeroMemory (que você pode ver por si mesmo observando a implementação real dessa função nas fontes do SDK do Windows ). No entanto, essa técnica não funcionará como esperado com todos os compiladores .







Veja também
volatile member functions



"Solução" errada # 3: use a função API do sistema operacional inadequada (por exemplo, RtlZeroMemory ) ou STL (por exemplo, std :: fill, std :: for_each)



RtlZeroMemory(memBuf, memBufSize);


Mais exemplos de tentativas de resolver este problema aqui .



E como isso está certo?



  • use a função API do sistema operacional correta , por exemplo, RtlSecureZeroMemory para Windows
  • use a função C11 memset_s :


Além disso, podemos evitar que o compilador otimize o código imprimindo (em um arquivo, console ou outro fluxo) o valor da variável, mas isso obviamente não é muito útil.



Veja também
Safe clearing of private Data



Resumindo



Obviamente, esta não é uma lista completa de todos os possíveis problemas, nuances e sutilezas que você pode encontrar ao escrever aplicativos em C / C ++ .



Existem também coisas excelentes como:





E muito mais.







Algo a acrescentar? Compartilhe sua experiência interessante nos comentários!



PS Quer saber mais?
Software security errors

Common weakness enumeration

Common types of software vulnerabilities



Vulnerability database

Vulnerability notes database

National vulnerability database



Coding standards

Application security verification standard

Guidelines for the use of the C++ language in critical systems



Secure programming HOWTO

32 OpenMP Traps For C++ Developers

A Collection of Examples of 64-bit Errors in Real Programs




All Articles