C ++: Astucia y amor, o ¿qué podría salir mal?





“C hace que sea fácil dispararse en el pie. Es más difícil hacer esto en C ++, pero tomará un paso adelante ”. - Björn Stroustrup, creador de C ++.


En este artículo, le mostraremos cómo escribir código estable, seguro y confiable, y lo fácil que es romperlo de manera completamente involuntaria. Para ello hemos intentado recopilar el material más útil y fascinante.







En SimbirSoft, trabajamos en estrecha colaboración con el proyecto Secure Code Warrior para capacitar a otros desarrolladores para crear soluciones seguras. Especialmente para Habr, tradujimos un artículo escrito por nuestro autor para el portal CodeProject.com.



¡Así que al código!



Aquí hay un pequeño fragmento de código C ++ abstracto. Este código fue escrito especialmente para demostrar todo tipo de problemas y vulnerabilidades que potencialmente se pueden encontrar en proyectos muy reales. Como puede ver, este es un código DLL de Windows (este es un punto importante). Supongamos que alguien va a usar este código en alguna solución (segura, por supuesto).



Eche un vistazo más de cerca al código. ¿Qué, en su opinión, podría salir mal?



El 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();
}




¿Quizás encontró este código simple, obvio y lo suficientemente seguro? ¿O tal vez encontró algunos problemas en él? ¿Quizás incluso una docena o dos?



Bueno, en realidad hay más de 43 amenazas potenciales de diversos grados de importancia en este fragmento .







A qué debes prestar atención



1) sizeof (d) (donde d es un doble largo) no es necesariamente un múltiplo de sizeof (int)



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


Esta situación no se prueba ni se maneja aquí. Por ejemplo, un doble largo puede tener 10 bytes en algunas plataformas (lo cual no es cierto para el compilador MS VS , pero sí para RAD Studio , anteriormente conocido como C ++ Builder ).



int también puede ser de diferentes tamaños dependiendo de la plataforma (el código anterior es para Windows , por lo tanto, en relación con esta situación particular, el problema es algo artificial, pero para el código portátil este problema es muy relevante).



Todo esto puede convertirse en un problema si queremos usar el llamado juego de palabras con mecanografía . Por cierto, provoca un comportamiento indefinido.según el estándar del lenguaje C ++. Sin embargo, es una práctica común usar el juego de palabras de mecanografía , ya que los compiladores modernos generalmente definen el comportamiento correcto y esperado para un caso dado (como, por ejemplo, GCC lo hace ).







Fuente: Medium.com



Por cierto, a diferencia de C ++, en C moderna del juego de palabras escribir es perfectamente válido (ya se sabe que C ++ y C son diferentes lenguas , y usted debe no esperar saber si sabes C C ++ y al revés, ¿verdad?)



Solución: use static_assertpara controlar todos esos supuestos en tiempo de compilación. Le advertirá si algo sale mal con los tamaños de letra:



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


2) time_t es una macro, en Visual Studio puede referirse al tipo entero de 32 bits (antiguo) o de 64 bits (nuevo)



time_t time;


El acceso a una variable de este tipo desde diferentes módulos ejecutables (por ejemplo, el archivo ejecutable y la DLL que carga) puede llevar a lectura / escritura fuera de los límites del objeto, en caso de que los dos binarios estén compilados con una representación física diferente de este tipo. Lo que, a su vez, provocará daños en la memoria o lecturas basura.







Solución: asegúrese de que se utilicen los mismos tipos de un tamaño estrictamente definido para el intercambio de datos entre todos los módulos:



int64_t time;


3) B.dll ( cuyo identificador está almacenado por la variable OTHER_LIB ) aún no se ha cargado en el momento en que accedemos a la variable anterior, por lo que no podemos obtener las direcciones de las funciones de esta biblioteca



4) el problema con el orden de inicialización de los objetos estáticos ( SIOF ): (objeto OTHER_LIB utilizado en el código antes de que se inicializara)



func = GetProcAddress(OTHER_LIB, "func");


FINALIZER es un objeto estático que se crea antes de llamar a la función DllMain . En su constructor, estamos intentando utilizar una biblioteca que aún no se ha cargado. El problema se agrava por el hecho de que el OTHER_LIB estático que utiliza el FINALIZER estático se coloca en la unidad de traducción aguas abajo. Esto significa que también se inicializará (pondrá a cero) más adelante. Es decir, en el momento en que se acceda, contendrá algo de basura pseudoaleatoria . WinAPIen general, debería reaccionar normalmente a esto, porque con un alto grado de probabilidad simplemente no habrá un módulo cargado con dicho descriptor en absoluto. E incluso si ocurre una coincidencia absolutamente increíble y todavía sucede, es poco probable que contenga una función llamada "Func" .



Solución: El consejo general es evitar el uso de objetos globales, especialmente los complejos, especialmente si dependen unos de otros, especialmente en DLL . Sin embargo, si aún los necesita por algún motivo, tenga mucho cuidado y cuidado con el orden en el que se inicializan. Para controlar este orden , coloque todas las instancias (definiciones) de objetos globales en una unidad de traducciónen el orden correcto para asegurarse de que se inicialicen correctamente.



5) el resultado devuelto anteriormente no se comprueba antes de su uso



auto data = func();


func es un puntero de función . Y debería apuntar a una función de B.dll . Sin embargo, dado que fallamos completamente en todo en el paso anterior, esto será nullptr . Por lo tanto, al tratar de eliminar la referencia, en lugar de la llamada a la función esperada, obtenemos una violación de acceso o una falla de protección general o algo así.



Solución: cuando trabaje con código externo (en nuestro caso con WinAPI ), siempre verifique el resultado de retorno de las funciones llamadas. Para sistemas confiables y tolerantes a fallas, esta regla se aplica incluso a funciones para las cuales existe un contrato estricto [sobre qué deben devolver y cuándo].



6) leer / escribir basura al intercambiar datos entre módulos compilados con diferentes configuraciones de alineación / relleno



auto str = data->c;


Si la estructura de datos (que se utiliza para intercambiar información entre módulos de comunicación) tiene estos mismos módulos en una presentación física diferente, resultará en todas las violaciones de acceso mencionadas anteriormente , una protección de memoria de errores , segmentación de fallas , corrupción de pila , etc. O simplemente leeremos la basura. El resultado exacto dependerá del escenario de uso real de esta memoria. Todo esto puede suceder porque no hay configuraciones explícitas de alineación / relleno para la estructura en sí . Por lo tanto, si estas configuraciones globales en el momento de la compilación fueran diferentes para los módulos interactivos, tendremos problemas.







Decisión:asegúrese de que todas las estructuras de datos compartidas tengan una representación física fuerte, definida explícitamente y obvia (usando tipos de tamaño fijo, alineación explícitamente especificada, etc.) y / o se hayan compilado binarios interoperables con la misma configuración de alineación global / relleno.



ver también
Alignment (C++ Declarations)

Data structure alignment

Struct padding in C++



7) usar el tamaño de un puntero a una matriz en lugar del tamaño de la matriz en sí



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


Esto suele ser el resultado de un error tipográfico trivial. Pero este problema también puede surgir potencialmente cuando se trata de polimorfismo estático o cuando la palabra clave auto se usa sin pensar ( especialmente cuando claramente se usa en exceso ). Sin embargo, uno quisiera esperar que los compiladores modernos sean lo suficientemente inteligentes como para detectar tales problemas en tiempo de compilación, usando las capacidades de un analizador estático interno .



Decisión:



  • nunca confunda sizeof ( <tipo de objeto completo> ) y sizeof ( <tipo de puntero de objeto> );
  • no ignore las advertencias del compilador ;

  • También puede usar un poco de magia repetitiva de C ++ combinando typeid, constexpr y static_assert para asegurarse de que los tipos sean correctos en el momento de la compilación (los rasgos de tipo también pueden ser útiles aquí , en particular std :: is_pointer ).


8) comportamiento indefinido al intentar leer un campo de unión diferente al que se usó anteriormente para establecer el valor



9) un intento de leer fuera de límites es posible si la longitud de un doble largo difiere entre módulos binarios



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


Esto ya se mencionó anteriormente, por lo que aquí solo tenemos otro punto de presencia del problema mencionado anteriormente.



Solución: No consulte un campo que no sea el que estableció anteriormente a menos que esté seguro de que su compilador lo está manejando correctamente. Asegúrese de que los tamaños de los tipos de objetos compartidos sean los mismos en todos los módulos que interactúan.



ver también
Type-punning and strict-aliasing

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



10) incluso si B.dll se cargó correctamente y la función "func" se exportó e importó correctamente, B.dll todavía se descarga de la memoria en este momento (porque la función del sistema FreeLibrary se llamó anteriormente en la sección DLL_PROCESS_DETACH de la función de devolución de llamada DllMain )



auto data = func();


Llamar a una función virtual en un objeto de tipo polimórfico previamente destruido, así como llamar a una función en una biblioteca dinámica ya descargada, probablemente resultará en un error de llamada virtual puro .



Solución: implemente el procedimiento de finalización correcto en la aplicación para asegurarse de que todos los archivos DLL estén terminados / descargados en el orden correcto. Evite el uso de objetos estáticos con lógica compleja en DL L. Evite realizar operaciones dentro de la biblioteca después de llamar a DllMain / DLL_PROCESS_DETACH (cuando la biblioteca entra en la última etapa de su ciclo de vida, la fase de destrucción de sus objetos estáticos).



Necesita comprender cuál es el ciclo de vida de una DLL:



) LoadLibrary



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


) FreeLibrary



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






11) eliminar un puntero opaco (el compilador necesita saber el tipo completo para llamar al destructor, por lo que eliminar un objeto con un puntero opaco puede provocar pérdidas de memoria y otros problemas)



12) si el destructor DataNew es virtual, incluso si la clase se exporta e importa correctamente y la totalidad información al respecto, de todos modos llamar a su destructor en esta etapa es un problema; esto probablemente conducirá a una llamada de función puramente virtual (ya que el tipo DataNew se importa del archivo B.dll ya descargado ). Este problema es posible incluso si el destructor no es virtual.



13) si la clase DataNew es un tipo polimórfico abstracto, y su clase base tiene un destructor virtual puro sin cuerpo, en cualquier caso ocurrirá una llamada de función virtual pura.



14) comportamiento indefinido si la memoria se asigna con new y se elimina con delete []



delete[] data2;


En general, siempre debe tener cuidado al liberar objetos obtenidos de módulos externos.



También es una buena práctica poner a cero los indicadores de los objetos destruidos.



Decisión:



  • al eliminar un objeto, se debe conocer su tipo completo
  • todos los destructores deben tener un cuerpo
  • la biblioteca desde la que se exporta el código no debe descargarse demasiado pronto
  • utilice siempre los diferentes formularios nuevos y elimínelos correctamente, no los confunda
  • el puntero al objeto remoto debe ponerse a cero.






También tenga en cuenta lo siguiente:

- llamar a delete en un puntero a void dará como resultado un comportamiento indefinido Las

funciones puramente virtuales no deben llamarse desde el constructor

- llamar a una función virtual en el constructor no será virtual

- intente evitar la gestión de memoria manual - use contenedores , mueva semántica y punteros inteligentes



ver también
Heap corruption: What could the cause be?



15) ExitThread es el método preferido para salir de un subproceso en C. En C ++, cuando se llama a esta función, el subproceso terminará antes de llamar a los destructores de objetos locales (y cualquier otra limpieza automática), por lo que la terminación de un subproceso en C ++ debe hacerse simplemente regresando de la función de subproceso



ExitThread(0U);


Solución: nunca utilice esta función manualmente en código C ++.



16) en el cuerpo de DllMain, llamar a cualquier función estándar que requiera archivos DLL del sistema que no sean Kernel32.dll puede provocar varios problemas difíciles de diagnosticar



CoInitializeEx(nullptr, COINIT_MULTITHREADED);


Solución en DllMain:



  • evitar cualquier (des) inicialización complicada
  • evite llamar a funciones de otras bibliotecas (o al menos tenga mucho cuidado con esto)






17) Inicialización incorrecta del generador de números pseudoaleatorios en un entorno multiproceso



18) dado que el tiempo devuelto por la función de tiempo tiene una resolución de 1 seg., Cualquier hilo del programa que llame a esta función durante este período de tiempo recibirá el mismo valor en la salida. El uso de este número para inicializar PRNG puede provocar colisiones (por ejemplo, la generación de los mismos nombres pseudoaleatorios para archivos temporales, los mismos números de puerto, etc.). Una posible solución es mezclar ( xor ) el resultado resultante con algún valor pseudoaleatorio , como la dirección de cualquier pila u objeto en el montón, una hora más precisa, etc.



srand(time(nullptr));


Solución: MS VS requiere la inicialización de PRNG para cada hilo . Además, el uso de tiempo Unix como inicializador proporciona una entropía insuficiente , por lo que se prefiere una generación de valor de inicialización más avanzada .



ver también
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) pueden bloquearse o bloquearse (o crear bucles de dependencia en el orden de carga de DLL )



OTHER_LIB = LoadLibrary("B.dll");


Solución: no utilice LoadLibrary en el punto de entrada DllMain . Cualquier (des) inicialización compleja debe realizarse en determinadas funciones exportadas por el desarrollador de DLL, como "Init" y "Deint" . La biblioteca proporciona estas funciones al usuario, y el usuario debe llamarlas correctamente en el momento adecuado. Ambas partes deben cumplir estrictamente con este contrato.







20) error tipográfico (la condición siempre es falsa), lógica de programa incorrecta y posible fuga de recursos (porque OTHER_LIB nunca se descarga en una descarga exitosa)



if (OTHER_LIB = nullptr)
    return FALSE;


El operador de asignación al copiar devuelve un enlace del tipo de la izquierda, es decir if comprobará el valor de OTHER_LIB (que será nullptr) y nullptr se interpretará como falso.



Solución: utilice siempre la forma inversa para evitar errores tipográficos como este:



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


21) se recomienda usar la función del sistema _beginthread para crear un nuevo hilo en la aplicación (especialmente si la aplicación estaba vinculada con una versión estática de la biblioteca de tiempo de ejecución de C) de lo contrario, pueden ocurrir pérdidas de memoria al llamar a ExitThread, DisableThreadLibraryCalls



22) todas las llamadas externas a DllMain están serializadas, por lo que en el cuerpo Esta función no debe intentar crear subprocesos / procesos o interactuar con ellos, de lo contrario pueden producirse puntos muertos



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


23) llamar a funciones COM durante la terminación de DLL puede conducir a un acceso incorrecto a la memoria, ya que el componente correspondiente ya puede estar descargado



CoUninitialize();


24) no hay forma de controlar el orden de carga y descarga de los servicios COM / OLE en proceso, así que no llame a OleInitialize u OleUninitialize desde la función DllMain



OleUninitialize();


ver también
COM Clients and Servers

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



25) llamar gratis para un bloque de memoria asignado con nuevo



26) si el proceso de aplicación está en proceso de terminar su trabajo (como lo indica un valor distinto de cero del parámetro lpvReserved), todos los subprocesos en el proceso, excepto el actual, ya han terminado o se han detenido forzosamente cuando llamando a la función ExitProcess, que puede dejar algunos de los recursos del proceso, como el montón, en un estado inconsistente. Como resultado, no es seguro para DLL limpiar recursos . En cambio, la DLL debería permitir que el sistema operativo recupere memoria.



free(INTEGERS);


Solución: asegúrese de que el antiguo estilo C de asignación de memoria manual no se mezcle con el "nuevo" estilo C ++. Tenga mucho cuidado al administrar recursos en la función DllMain .



27) puede hacer que la DLL se use incluso después de que el sistema haya ejecutado su código de salida



const BOOL result = FreeLibrary(OTHER_LIB);


Solución: no llame a FreeLibrary en el punto de entrada de DllMain.



28) el hilo actual (posiblemente principal) se bloqueará



throw new std::runtime_error("    ");


Solución: evite lanzar excepciones en la función DllMain. Si la DLL no se puede cargar correctamente por algún motivo, la función simplemente debería devolver FALSE. Tampoco debe lanzar excepciones de la sección DLL_PROCESS_DETACH.



Siempre tenga cuidado al lanzar excepciones fuera de la DLL. Cualquier objeto complejo (por ejemplo, clases de la biblioteca estándar ) puede tener una representación física diferente (e incluso lógica de trabajo) en diferentes módulos ejecutables si se compilan con versiones diferentes (incompatibles) de las bibliotecas en tiempo de ejecución .







Intente intercambiar solo tipos de datos simples entre módulos(con un tamaño fijo y una representación binaria bien definida).



Recuerde que terminar el hilo principal terminará automáticamente todos los demás hilos (que no terminan correctamente y por lo tanto pueden dañar la memoria, dejando primitivas de sincronización y otros objetos en un estado impredecible e incorrecto. Además, estos hilos ya dejarán de existir en el momento en que Los objetos estáticos comenzarán su propia deconstrucción, así que no intente esperar a que termine ningún hilo en los destructores de objetos estáticos).



ver también
Top 20 C++ multithreading mistakes and how to avoid them



29) puede lanzar una excepción (por ejemplo, std :: bad_alloc), que no se detecta aquí



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


Dado que la sección DLL_THREAD_ATTACH se llama desde un código externo desconocido, no espere ver el comportamiento correcto aquí.



Solución: use try / catch para incluir declaraciones que puedan generar excepciones que probablemente no se puedan manejar correctamente (especialmente si salen de la DLL ).



ver también
How can I handle a destructor that fails?



30) UB si se presentaron transmisiones antes de cargar esta DLL



THREADS.pop_back();


Los subprocesos que ya existen en el momento en que se carga la DLL (incluido el que carga directamente la DLL ) no llaman a la función de punto de entrada de la DLL que se está cargando (por eso no se registran con el vector THREADS durante el evento DLL_THREAD_ATTACH), mientras que aún lo llaman con el evento DLL_THREAD_DETACH al finalizar.

Esto significa que el número de llamadas a las secciones DLL_THREAD_ATTACH y DLL_THREAD_DETACH de la función DllMain será diferente.



31) es mejor usar tipos enteros de tamaño fijo



32) pasar un objeto complejo entre módulos puede fallar si se compila con diferentes configuraciones de enlace y compilación y banderas (diferentes versiones de la biblioteca en tiempo de ejecución, etc.)



33) acceder al objeto c por su dirección virtual (que es compartida por los módulos) puede causar problemas si los punteros se manejan de manera diferente en estos módulos (por ejemplo, si los módulos están asociados con diferentes parámetros LARGEADDRESSAWARE )



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


ver también
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]



Y...
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



La lista anterior apenas está completa, por lo que probablemente pueda agregar algo importante en los comentarios.



Trabajar con punteros es en realidad mucho más complejo de lo que la gente suele pensar. Sin lugar a dudas, los desarrolladores experimentados podrán recordar otros matices y sutilezas existentes (por ejemplo, algo sobre la diferencia entre punteros a un objeto y punteros a una función , por lo que, tal vez, no se puedan usar todos los bits del puntero , etc. .).







34) se puede lanzar una excepción dentro de una función :



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


la especificación throw () de esta función está vacía:



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


El tiempo de ejecución de C ++ llama a std :: inesperado cuando se viola una especificación de excepción: se lanza una excepción desde una función cuya especificación de excepción no permite excepciones de este tipo.



Solución: use try / catch (especialmente al asignar recursos, especialmente en archivos DLL ) o la forma nothrow del nuevo operador. En cualquier caso, nunca partas de la suposición ingenua de que todos los intentos de asignar varios tipos de recursos siempre terminarán con éxito .



ver también
RAII

We do not use C++ exceptions

Memory Limits for Windows and Windows Server Releases









Problema 1: la formación de tal valor "más aleatorio" es incorrecta. Según el teorema del límite central , la suma de las variables aleatorias independientes tiende a una distribución normal y no a una uniforme (incluso si los valores iniciales en sí mismos se distribuyen uniformemente).



Problema 2: posible desbordamiento del tipo de entero (que es un comportamiento indefinido para los tipos de entero con signo )



return rand() + rand();


Cuando trabaje con generadores de números pseudoaleatorios, cifrado y similares, siempre tenga cuidado con el uso de "soluciones" caseras. A menos que tenga una educación especializada y experiencia en estas áreas tan específicas, es muy probable que simplemente se engañe a sí mismo y empeore la situación.



35) el nombre de la función exportada se decorará (cambiará) para evitar este uso de "C" externa



36) los nombres que comiencen con '_' están implícitamente prohibidos para C ++, ya que este estilo de nomenclatura está reservado para el STL



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


Varios problemas (y sus posibles soluciones):



37) rand no es seguro para subprocesos, use rand_r / rand_s en lugar de



38) rand está en desuso, mejor use moderno
C++11 <random>


39) no es un hecho que la función rand se haya inicializado específicamente para el subproceso actual (MS VS requiere la inicialización de esta función para cada subproceso donde se llamará)



40) existen generadores especiales de números pseudoaleatorios , y es mejor usarlos en soluciones resistentes a hackeo (son adecuadas soluciones portátiles como Libsodium / randombytes_buf , OpenSSL / RAND_bytes , etc.)



41) división potencial por cero: puede hacer que el hilo actual se bloquee



42) operadores con diferente precedencia se usan en la misma fila , lo que introduce caos en el orden de cálculo - use paréntesis y / o puntos de secuenciapara especificar la secuencia obvia de cálculo



43) potencial desbordamiento de enteros



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




ver también
Do not use std::rand() for generating pseudorandom numbers





Y...
ExitThread function

ExitProcess function

TerminateThread function

TerminateProcess function





¡Y eso no es todo!



Imagine que tiene algún contenido importante en la memoria (por ejemplo, la contraseña de un usuario). Por supuesto, no desea conservarlo en la memoria por más tiempo de lo realmente necesario, aumentando así la probabilidad de que alguien pueda leerlo desde allí .



Un enfoque ingenuo para resolver este problema se vería así:



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);
}


Y ciertamente no funcionará de la manera que nos gustaría. Entonces, ¿qué se debe hacer? :( "Solución"



incorrecta # 1: si Memset no funciona, ¡hagámoslo 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 qué esto tampoco nos conviene? El hecho es que no hay restricciones en este código que no permitirían a un compilador moderno optimizarlo (por cierto, la función memset , si todavía se usa, probablemente estará incorporada ).



ver también
The as-if rule

Are there situations where this rule does not apply?

Copy elision

Atomics and optimization



"Solución" incorrecta n.º 2: intente "mejorar" la "solución" anterior jugando con la palabra clave 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
}


esto funcionara? Tal vez. Por ejemplo, este enfoque se utiliza en RtlSecureZeroMemory (que puede ver por sí mismo si observa la implementación real de esta función en las fuentes del SDK de Windows ). Sin embargo, esta técnica no funcionará como se esperaba con todos los compiladores .







ver también
volatile member functions



"Solución" incorrecta n. ° 3: utilice una función API de SO inadecuada (por ejemplo, RtlZeroMemory ) o STL (por ejemplo, std :: fill, std :: for_each)



RtlZeroMemory(memBuf, memBufSize);


Más ejemplos de intentos de resolver este problema aquí .



¿Y cómo está bien?



  • utilizar la función de API del sistema operativo correcta , por ejemplo, RtlSecureZeroMemory para Windows
  • use la función C11 memset_s :


Además, podemos evitar que el compilador optimice el código imprimiendo (en un archivo, consola u otro flujo) el valor de la variable, pero esto obviamente no es muy útil.



ver también
Safe clearing of private Data



Resumiendo



Esto, por supuesto, no es una lista completa de todos los posibles problemas, matices y sutilezas que puede encontrar al escribir aplicaciones en C / C ++ .



También hay cosas geniales como:





Y mucho más.







¿Algo que agregar? ¡Comparte tu interesante experiencia en los comentarios!



PD: ¿Quieres saber más?
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