«C vous permet de vous tirer une balle dans le pied facilement. C'est plus difficile de faire cela en C ++, mais cela prendra toute une longueur d'avance. »- Björn Stroustrup, créateur C ++.
Dans cet article, nous allons vous montrer comment écrire du code stable, sécurisé et fiable, et à quel point il est facile de le casser complètement sans le vouloir. Pour cela, nous avons essayé de collecter le matériel le plus utile et le plus fascinant.
Chez SimbirSoft, nous travaillons en étroite collaboration avec le projet Secure Code Warrior pour former d'autres développeurs à créer des solutions sécurisées. Surtout pour Habr, nous avons traduit un article écrit par notre auteur pour le portail CodeProject.com.
Alors au code!
Voici un petit morceau de code C ++ abstrait. Ce code a été spécialement écrit afin de démontrer toutes sortes de problèmes et vulnérabilités qui peuvent potentiellement être trouvés sur des projets très réels. Comme vous pouvez le voir, il s'agit du code d'une DLL Windows (c'est un point important). Supposons que quelqu'un utilise ce code dans une solution (sûre, bien sûr).
Regardez de plus près le code. À votre avis, qu'est-ce qui pourrait mal tourner?
Le code
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();
}
Peut-être avez-vous trouvé ce code simple, évident et suffisamment sûr? Ou peut-être y avez-vous trouvé des problèmes? Ou peut-être même une douzaine ou deux?
Eh bien, il y a en fait plus de 43 menaces potentielles de divers degrés d'importance dans cet extrait !
À quoi vous devez faire attention
1) sizeof (d) (où d est un long double) n'est pas nécessairement un multiple de sizeof (int)
int i[sizeof(d) / sizeof(int)];
Cette situation n'est ni testée ni traitée ici. Par exemple, un long double peut être de 10 octets sur certaines plates-formes (ce qui n'est pas vrai pour le compilateur MS VS , mais vrai pour RAD Studio , anciennement connu sous le nom de C ++ Builder ).
int peut également être de tailles différentes selon la plate-forme (le code ci-dessus est pour Windows , par conséquent, par rapport à cette situation particulière, le problème est quelque peu artificiel, mais pour le code portable, ce problème est très pertinent).
Tout cela peut devenir un problème si nous voulons utiliser le soi-disant jeu de mots de frappe . Au fait, cela provoque un comportement indéfiniselon le standard du langage C ++. Cependant, il est courant d' utiliser le jeu de mots de frappe , car les compilateurs modernes définissent généralement le comportement attendu correct pour un cas donné (comme le fait, par exemple, GCC ).
Source: Medium.com
Au fait, contrairement au C ++, en C moderne, le jeu de mots de frappe est parfaitement valide (vous savez que C ++ et C sont des langages différents , et vous ne devriez pas vous attendre à connaître C si vous connaissez C ++, et l'inverse, non?)
Solution: utilisez static_assertpour contrôler toutes ces hypothèses au moment de la compilation. Il vous avertira si quelque chose ne va pas avec les tailles de caractères:
static_assert(0U == (sizeof(d) % sizeof(int)), “Houston, we have a problem”);
2) time_t est une macro, dans Visual Studio, il peut faire référence à un type entier 32 bits (ancien) ou 64 bits (nouveau)
time_t time;
L'accès à une variable de ce type à partir de différents modules exécutables (par exemple, le fichier exécutable et la DLL qu'il charge) peut conduire à une lecture / écriture en dehors des limites de l'objet, au cas où les deux binaires seraient compilés avec une représentation physique différente de ce type. Ce qui, à son tour, entraînera une corruption de la mémoire ou des lectures inutiles.
Solution: assurez-vous que les mêmes types d'une taille strictement définie sont utilisés pour l'échange de données entre tous les modules:
int64_t time;
3) B.dll (dont le handle est stocké par la variable OTHER_LIB ) n'a pas encore été chargé au moment où nous accédons à la variable ci-dessus, donc nous ne pouvons pas obtenir les adresses des fonctions de cette bibliothèque
4) le problème avec l'ordre d'initialisation des objets statiques ( SIOF ): (objet OTHER_LIB utilisé dans le code avant son initialisation)
func = GetProcAddress(OTHER_LIB, "func");
FINALIZER est un objet statique qui est créé avant d'appeler la fonction DllMain . Dans son constructeur, nous essayons d'utiliser une bibliothèque qui n'a pas encore été chargée. Le problème est aggravé par le fait que la AUTRE_LIB statique utilisée par le FINALIZER statique est placée dans l'unité de traduction en aval. Cela signifie qu'il sera également initialisé (remis à zéro) plus tard. Autrement dit, au moment où il sera consulté, il contiendra des déchets pseudo-aléatoires . WinAPIen général, il devrait réagir normalement à cela, car avec un degré de probabilité élevé, il n'y aura tout simplement pas de module chargé avec un tel descripteur. Et même s'il y a une coïncidence absolument incroyable et qu'elle le sera toujours - il est peu probable qu'elle ait une fonction nommée "Func" .
Solution: un conseil général est d'éviter d'utiliser des objets globaux, en particulier des objets complexes, surtout s'ils dépendent les uns des autres, en particulier dans les DLL . Cependant, si vous en avez encore besoin pour une raison quelconque, soyez extrêmement prudent et prudent avec l'ordre dans lequel ils sont initialisés. Pour contrôler cet ordre , placez toutes les instances (définitions) des objets globaux dans une unité de traductiondans le bon ordre pour s'assurer qu'ils sont correctement initialisés.
5) le résultat précédemment renvoyé n'est pas vérifié avant utilisation
auto data = func();
func est un pointeur de fonction . Et il doit pointer vers une fonction de B.dll . Cependant, comme nous avons complètement échoué à tout à l'étape précédente, ce sera nullptr . Ainsi, en essayant de le déréférencer, au lieu de l'appel de fonction attendu, nous recevrons une violation d'accès ou une erreur de protection générale ou quelque chose comme ça.
Solution: lorsque vous travaillez avec du code externe (dans notre cas avec WinAPI ), vérifiez toujours le résultat de retour des fonctions appelées. Pour les systèmes fiables et tolérants aux pannes, cette règle s'applique même aux fonctions pour lesquelles il existe un contrat strict [sur ce qu'elles doivent renvoyer et quand].
6) lecture / écriture de déchets lors de l'échange de données entre des modules compilés avec différents paramètres d'alignement / de remplissage
auto str = data->c;
Si la structure de données (qui est utilisée pour échanger des informations entre les modules de communication) a ces mêmes modules dans une présentation physique différente, cela entraînera toutes les violations d'accès mentionnées précédemment , une protection de mémoire d'erreur , une segmentation des pannes , une corruption de tas , etc. Ou nous allons simplement lire les ordures. Le résultat exact dépendra du scénario d'utilisation réel de cette mémoire. Tout cela peut se produire car il n'y a pas de paramètres d' alignement / de remplissage explicites pour la structure elle-même . Par conséquent, si ces paramètres globaux au moment de la compilation étaient différents pour les modules en interaction, nous aurons des problèmes.
Décision:s'assurer que toutes les structures de données partagées ont une représentation physique forte, explicitement définie et évidente (en utilisant des types de taille fixe, un alignement explicitement spécifié, etc.) et / ou que les binaires interopérables ont été compilés avec les mêmes paramètres d'alignement global / remplissage.
7) en utilisant la taille d'un pointeur vers un tableau au lieu de la taille du tableau lui-même
memset(str, 0, sizeof(str));
C'est généralement le résultat d'une faute de frappe triviale. Mais ce problème peut également survenir lors du traitement du polymorphisme statique ou lorsque le mot-clé auto est utilisé sans réfléchir (en particulier lorsqu'il est clairement surutilisé ). On aimerait cependant espérer que les compilateurs modernes sont suffisamment intelligents pour détecter de tels problèmes au moment de la compilation, en utilisant les capacités de l' analyseur statique interne .
Décision:
- ne confondez jamais sizeof ( <type d'objet complet> ) et sizeof ( <type de pointeur d'objet> );
- n'ignorez pas les avertissements du compilateur ;
- vous pouvez également utiliser un peu de magie standard C ++ en combinant typeid, constexpr et static_assert pour vous assurer que les types sont corrects au moment de la compilation ( les traits de type peuvent également être utiles ici , en particulier std :: is_pointer ).
8) comportement indéfini lors de la tentative de lecture d'un champ union différent de ce qui était précédemment utilisé pour définir la valeur
9) il est possible d'essayer de lire dans la zone de mémoire valide si la taille d'un long double diffère entre les modules binaires
const int i0 = data->u.i[sizeof(long double) - 1U];
Cela a déjà été mentionné plus tôt, nous venons donc ici d'avoir un autre point de présence du problème mentionné précédemment.
Solution: ne faites pas référence à un champ autre que celui que vous avez défini précédemment, sauf si vous êtes sûr que votre compilateur le gère correctement. Assurez-vous que les tailles des types d'objets partagés sont les mêmes dans tous les modules en interaction.
voir également
10) même si B.dll a été chargé correctement et que la fonction "func" a été correctement exportée et importée, B.dll est toujours déchargé de la mémoire à ce moment (car la fonction système FreeLibrary a été précédemment appelée dans la section DLL_PROCESS_DETACH de la fonction de rappel DllMain )
auto data = func();
L'appel d'une fonction virtuelle sur un objet de type polymorphe précédemment détruit, ainsi que l'appel d'une fonction dans une bibliothèque dynamique déjà déchargée, entraînera probablement une pure erreur d'appel virtuel .
Solution: implémentez la procédure de finalisation correcte dans l'application pour vous assurer que toutes les DLL sont terminées / déchargées dans le bon ordre. Évitez d'utiliser des objets statiques avec une logique complexe dans DL L. Évitez d'effectuer des opérations à l'intérieur de la bibliothèque après avoir appelé DllMain / DLL_PROCESS_DETACH (lorsque la bibliothèque entre dans sa dernière étape de son cycle de vie - la phase de destruction de ses objets statiques).
Vous devez comprendre quel est le cycle de vie d'une DLL:
) LoadLibrary
- ( , )
- DllMain -> DLL_PROCESS_ATTACH ( , )
- [] DllMain -> DLL_THREAD_ATTACH / DLL_THREAD_DETACH ( , . 30).
- , , (, ),
- ( / , , )
- , ()
- ( / , , )
- - : ,
) FreeLibrary
- DllMain -> DLL_PROCESS_DETACH ( , )
- ( , )
11) suppression d'un pointeur opaque (le compilateur a besoin de connaître le type complet pour appeler le destructeur, donc la suppression d'un objet à l'aide d'un pointeur opaque peut entraîner des fuites de mémoire et d'autres problèmes)
12) si le destructeur DataNew est virtuel, même si la classe est exportée et importée correctement et le complet des informations à ce sujet, de toute façon appeler son destructeur à ce stade est un problème - cela conduira probablement à un appel de fonction purement virtuel (puisque le type DataNew est importé du fichier B.dll déjà déchargé ). Ce problème est possible même si le destructeur n'est pas virtuel.
13) si la classe DataNew est de type polymorphe abstrait, et sa classe de base a un destructeur virtuel pur sans corps, dans tous les cas un appel de fonction virtuelle pure se produira .
14) comportement indéfini si la mémoire est allouée via new et supprimée à l'aide de delete []
delete[] data2;
En général, vous devez toujours faire attention lorsque vous libérez des objets reçus de modules externes.
Il est également recommandé de mettre à zéro les pointeurs vers les objets détruits.
Décision:
- lors de la suppression d'un objet, son type complet doit être connu
- tous les destructeurs doivent avoir un corps
- la bibliothèque à partir de laquelle le code est exporté ne doit pas être déchargée trop tôt
- utilisez toujours les différents formulaires nouveaux et supprimés correctement, ne les confondez pas
- le pointeur vers l'objet distant doit être mis à zéro.
Notez également ce qui suit:
- l'appel de delete sur un pointeur vers void entraînera un comportement indéfini.Les
fonctions purement virtuelles ne doivent pas être appelées depuis le constructeur
- l'appel d'une fonction virtuelle dans le constructeur ne sera pas virtuel
- essayez d'éviter la gestion manuelle de la mémoire - utilisez des conteneurs , déplacez la sémantique et pointeurs intelligents
voir également
15) ExitThread est la méthode préférée pour quitter un thread en C. En C ++, l'appel de cette fonction terminera le thread avant d'appeler les destructeurs d'objets locaux (et tout autre nettoyage automatique), donc terminer un thread en C ++ doit être fait simplement en retournant de la fonction thread
ExitThread(0U);
Solution: n'utilisez jamais manuellement cette fonction dans le code C ++.
16) dans le corps de DllMain, l'appel de toutes les fonctions standard qui nécessitent des DLL système autres que Kernel32.dll peut entraîner divers problèmes difficiles à diagnostiquer
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
Solution dans DllMain:
- éviter toute (dé) initialisation compliquée
- évitez d'appeler des fonctions à partir d'autres bibliothèques (ou du moins soyez extrêmement prudent avec cela)
17) initialisation incorrecte du générateur de nombres pseudo-aléatoires dans un environnement multithread
18) puisque l'heure retournée par la fonction time a une résolution de 1 seconde, tout thread du programme qui appelle cette fonction pendant cette période recevra la même valeur en sortie. L'utilisation de ce numéro pour initialiser PRNG peut conduire à des collisions (par exemple, génération des mêmes noms pseudo-aléatoires pour les fichiers temporaires, des mêmes numéros de port, etc.). Une solution possible consiste à mélanger ( xor ) le résultat obtenu avec une valeur pseudo-aléatoire , telle que l'adresse de n'importe quelle pile ou objet dans le tas, une heure plus précise, etc.
srand(time(nullptr));
Solution: MS VS nécessite l'initialisation PRNG pour chaque thread . De plus, l' utilisation de l'heure Unix comme initialiseur fournit une entropie insuffisante , une génération de valeur d'initialisation plus avancée est préférable .
voir également
19) peut bloquer ou planter (ou créer des boucles de dépendance dans l'ordre de chargement des DLL )
OTHER_LIB = LoadLibrary("B.dll");
Solution: n'utilisez pas LoadLibrary au point d'entrée DllMain . Toute (dé) initialisation complexe doit être effectuée dans certaines fonctions exportées par le développeur DLL telles que "Init" et "Deint" . La bibliothèque fournit ces fonctions à l'utilisateur, et l'utilisateur doit les appeler correctement au bon moment. Les deux parties doivent respecter strictement ce contrat.
20) faute de frappe (la condition est toujours fausse), logique de programme incorrecte et fuite de ressources possible (car OTHER_LIB n'est jamais déchargé en cas de téléchargement réussi)
if (OTHER_LIB = nullptr)
return FALSE;
L'opérateur d'affectation par copie renvoie un lien de type gauche, c'est-à-dire if vérifiera la valeur OTHER_LIB (qui sera nullptr) et nullptr sera interprété comme faux.
Solution: utilisez toujours le formulaire inversé pour éviter les fautes de frappe comme celle-ci:
if/while (<constant> == <variable/expression>)
21) il est recommandé d'utiliser la fonction système _beginthread pour créer un nouveau thread dans l'application (surtout si l'application était liée à une version statique de la bibliothèque d'exécution C), sinon des fuites de mémoire peuvent se produire lors de l'appel ExitThread, DisableThreadLibraryCalls
22) tous les appels externes à DllMain sont sérialisés, donc dans le corps Cette fonction ne doit pas tenter de créer des threads / processus ou d'interagir avec eux, sinon des blocages peuvent se produire.
CreateThread(nullptr, 0U, &Init, nullptr, 0U, nullptr);
23) L'appel de fonctions COM lors de l'arrêt de la DLL peut entraîner un accès mémoire incorrect, car le composant correspondant peut déjà être déchargé
CoUninitialize();
24) il n'y a aucun moyen de contrôler l'ordre de chargement et de déchargement des services COM / OLE en cours, alors n'appelez pas OleInitialize ou OleUninitialize à partir de la fonction DllMain
OleUninitialize();
25) appel gratuit pour un bloc de mémoire alloué avec un nouveau
26) si le processus d'application est en train de terminer son travail (comme indiqué par une valeur différente de zéro du paramètre lpvReserved), tous les threads du processus, à l'exception de l'actuel, se sont déjà terminés ou ont été arrêtés de force lorsque appelant la fonction ExitProcess, qui peut laisser certaines des ressources de processus, telles que le tas, dans un état incohérent. Par conséquent, il n'est pas sûr pour DLL de nettoyer les ressources . Au lieu de cela, la DLL doit permettre au système d'exploitation de récupérer de la mémoire.
free(INTEGERS);
Solution: assurez-vous que l'ancien style C d'allocation de mémoire manuelle n'est pas mélangé avec le «nouveau» style C ++. Soyez extrêmement prudent lors de la gestion des ressources dans la fonction DllMain .
27) peut entraîner l'utilisation de la DLL même après que le système a exécuté son code de sortie
const BOOL result = FreeLibrary(OTHER_LIB);
Solution: n'appelez pas FreeLibrary au point d'entrée DllMain.
28) le thread actuel (éventuellement principal) plantera
throw new std::runtime_error(" ");
Solution: évitez de lancer des exceptions dans la fonction DllMain. Si la DLL ne peut pas être chargée correctement pour une raison quelconque, la fonction doit simplement renvoyer FALSE. Vous ne devez pas non plus lever d'exceptions de la section DLL_PROCESS_DETACH.
Soyez toujours prudent lorsque vous lancez des exceptions en dehors de la DLL. Tous les objets complexes (par exemple, les classes de la bibliothèque standard ) peuvent avoir une représentation physique différente (et même une logique de travail) dans différents modules exécutables s'ils sont compilés avec différentes versions (incompatibles) des bibliothèques d'exécution .
Essayez d'échanger uniquement des types de données simples entre les modules(avec une taille fixe et une représentation binaire bien définie).
N'oubliez pas que la fin du thread principal mettra automatiquement fin à tous les autres threads (qui ne se terminent pas correctement et peuvent donc endommager la mémoire, laissant les primitives de synchronisation et d'autres objets dans un état imprévisible et incorrect. De plus, ces threads cesseront déjà d'exister au moment où les objets statiques commenceront leur propre déconstruction, n'essayez donc pas d'attendre que les threads se terminent dans les destructeurs d'objets statiques).
voir également
29) vous pouvez lancer une exception (par exemple, std :: bad_alloc), qui n'est pas interceptée ici
THREADS.push_back(std::this_thread::get_id());
Puisque la section DLL_THREAD_ATTACH est appelée à partir d'un code externe inconnu, ne vous attendez pas à voir un comportement correct ici.
Solution: utilisez une commande try / catch pour attacher des instructions susceptibles de lever des exceptions qui ne peuvent probablement pas être gérées correctement (surtout si elles sortent de la DLL ).
voir également
30) UB si des flux ont été présentés avant le chargement de cette DLL
THREADS.pop_back();
Les threads qui existent déjà au moment du chargement de la DLL (y compris celui qui charge directement la DLL ) n'appellent pas la fonction de point d'entrée de la DLL chargée (c'est pourquoi ils ne sont pas enregistrés dans le vecteur THREADS lors de l'événement DLL_THREAD_ATTACH), alors qu'ils l'appellent toujours avec l'événement DLL_THREAD_DETACH à la fin.
Cela signifie que le nombre d'appels aux sections DLL_THREAD_ATTACH et DLL_THREAD_DETACH de la fonction DllMain sera différent.
31) il est préférable d'utiliser des types entiers de taille fixe
32) le passage d'un objet complexe entre les modules peut planter s'il est compilé avec différents paramètres et indicateurs de lien et de compilation (différentes versions de la bibliothèque d'exécution, etc.)
33) l'accès à l'objet c par son adresse virtuelle (qui est partagée par les modules) peut poser des problèmes si les pointeurs sont traités différemment dans ces modules (par exemple, si les modules sont associés à différents paramètres LARGEADDRESSAWARE )
__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()
voir également
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]
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]
Et...
La liste ci-dessus est à peine complète, vous pouvez donc probablement ajouter quelque chose d'important dans les commentaires.
Travailler avec des pointeurs est en fait beaucoup plus complexe que ce que les gens pensent habituellement. Sans aucun doute, les développeurs chevronnés pourront se souvenir d'autres nuances et subtilités existantes (par exemple, quelque chose sur la différence entre les pointeurs vers un objet et les pointeurs vers une fonction , à cause de laquelle, peut-être, tous les bits du pointeur ne peuvent pas être utilisés , etc. .).
34) une exception peut être levée à l' intérieur d'une fonction :
INTEGERS = new std::vector<int>(integers);
la spécification throw () de cette fonction est vide:
__declspec(dllexport) int Initialize(std::vector<int> integers, int& c) throw()
std ::xpected est appelé par le runtime C ++ lorsqu'une spécification d'exception est violée: une exception est levée à partir d'une fonction dont la spécification d'exception interdit les exceptions de ce type.
Solution: utilisez try / catch (en particulier lors de l'allocation de ressources, en particulier dans les DLL ) ou la forme nothrow du nouvel opérateur. Dans tous les cas, ne faites jamais l'hypothèse naïve que toutes les tentatives d'allocation de divers types de ressources se termineront toujours avec succès .
voir également
Problème 1: la formation d'une telle valeur "plus aléatoire" est incorrecte. Selon le théorème central de la limite , la somme des variables aléatoires indépendantes tend vers une distribution normale , et non vers une distribution uniforme (même si les valeurs initiales elles-mêmes sont distribuées uniformément).
Problème 2: débordement possible de type entier (qui est un comportement indéfini pour les types entiers signés )
return rand() + rand();
Lorsque vous travaillez avec des générateurs de nombres pseudo-aléatoires, le chiffrement, etc., méfiez-vous toujours des «solutions» maison. À moins que vous n'ayez une formation et une expérience spécialisées dans ces domaines très spécifiques, il y a de fortes chances que vous vous déjouiez tout simplement et d'aggraver la situation.
35) le nom de la fonction exportée sera décoré (changé) pour empêcher cette utilisation d'extern "C"
36) les noms commençant par '_' sont implicitement interdits en C ++, car ce style de nommage est réservé à la STL
__declspec(dllexport) long long int __cdecl _GetInt(int a)
Plusieurs problèmes (et leurs solutions possibles):
37) rand n'est pas thread-safe, utilisez rand_r / rand_s à la place
38) rand est obsolète, mieux utiliser modern
C++11 <random>
39) ce n'est pas un fait que la fonction rand ait été initialisée spécifiquement pour le thread courant (MS VS nécessite l'initialisation de cette fonction pour chaque thread où elle sera appelée)
40) il existe des générateurs spéciaux de nombres pseudo-aléatoires , et il vaut mieux les utiliser dans des solutions résistantes au piratage (ils conviennent solutions portables comme Libsodium / randombytes_buf , OpenSSL / RAND_bytes , etc.)
41) division potentielle par zéro: peut provoquer le blocage du thread actuel
42) des opérateurs avec une priorité différente sont utilisés dans la même ligne , ce qui introduit le chaos dans l'ordre de calcul - utilisez des parenthèses et / ou points de séquencepour spécifier la séquence évidente du calcul
43) débordement d'entier potentiel
return 100 / a <= 0 ? a : a + 1 + Random();
voir également
Et ce n'est pas tout!
Imaginez que vous avez un contenu important en mémoire (par exemple, le mot de passe d'un utilisateur). Bien sûr, vous ne voulez pas le garder en mémoire plus longtemps que nécessaire, augmentant ainsi la probabilité que quelqu'un puisse le lire à partir de là .
Une approche naïve pour résoudre ce problème ressemblerait à ceci:
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);
}
Et cela ne fonctionnera certainement pas comme nous le souhaiterions. Que faire alors? :( "Solution"
incorrecte # 1: si memset ne fonctionne pas, faisons-le manuellement!
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';
}
Pourquoi cela ne nous convient-il pas non plus? Le fait est qu'il n'y a aucune restriction dans ce code qui ne permettrait pas à un compilateur moderne de l' optimiser (en passant, la fonction memset , si elle est toujours utilisée, sera très probablement intégrée ).
voir également
"Solution" incorrecte # 2: essayez "d'améliorer" la "solution" précédente en jouant avec le mot-clé volatile
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
}
Est-ce que ça va marcher? Peut être. Par exemple, cette approche est utilisée dans RtlSecureZeroMemory (que vous pouvez voir par vous-même en regardant l'implémentation réelle de cette fonction dans les sources du SDK Windows ). Cependant, cette technique ne fonctionnera pas comme prévu avec tous les compilateurs .
voir également
Mauvaise «solution» # 3: utilisez une fonction API du système d'exploitation inappropriée (par exemple RtlZeroMemory ) ou STL (par exemple std :: fill, std :: for_each)
RtlZeroMemory(memBuf, memBufSize);
Plus d'exemples de tentatives pour résoudre ce problème ici .
Et comment est-ce vrai?
- utiliser la fonction API du système d'exploitation appropriée , par exemple, RtlSecureZeroMemory pour Windows
- utilisez la fonction memset_s C11 :
De plus, on peut empêcher le compilateur d'optimiser le code en imprimant (dans un fichier, une console ou un autre flux) la valeur de la variable, mais ce n'est évidemment pas très utile.
voir également
Résumer
Ceci, bien sûr, n'est pas une liste complète de tous les problèmes, nuances et subtilités possibles que vous pouvez rencontrer lors de l'écriture d'applications en C / C ++ .
Il y a aussi de bonnes choses comme:
- Verrous en direct ;
- (, , ABA, , );
- ;
- (- , , );
- GDI ;
- , volatile atomic ;
- (, 0603 603);
- problème de désynchronisation de contrôle / accès ( heure de contrôle à heure d'utilisation );
- expressions lambda qui vivent plus longtemps que leurs objets référencés;
- utilisation de spécifications de format incorrectes dans les fonctions de la famille printf ;
- échange incorrect de données entre deux appareils avec un ordre d'octet différent (par exemple, sur un réseau), etc., etc.
Et beaucoup plus.
Quelque chose à ajouter? Partagez vos expériences intéressantes dans les commentaires!
PS Vous voulez en savoir plus?
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
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