Secure programming/fr

From Lazarus wiki
Jump to navigationJump to search

English (en) français (fr) polski (pl)

Avant-propos

Cette page de wiki tente d'enseigner une approche différente dans la façon créer un logiciel. La page utilise des exemples très simples pour montrer que beaucoup de problèmes peuvent être profitables pour prendre l'avantage dans le but de créer une attaque de sécurité sur un ordinateur, un programme ou un système en entier.

Veuillez noter que le document est seulement un début sur la technologie sur comment écrire un meilleur et un code un peu plus sécurisé code, mais il n'essaye pas d'être un guide complet sur la façon de le faire. En fait c'est seulement un résumé de la façon dont nous devons voir notre code et programme , et comment éviter beaucoup de problèmes communs.

Veuillez vous rappeler que ce document vise un meilleur codage et n'est pas là pour montrer comment hacker(bidouiller) ou craquer (casser la protection) de programmes.

Ne faites pas confiance aux entrées

En développant un programme, il est probable qu'il interagira avec un utilisateur de différentes façons, même s'il se limite à lire des fichiers dans le système et présenter les données.

Généralement à l'école et à l'université, quand quelqu'un commence à écrire des programmes, cette personne apprend comment recevoir des données, les professeurs disent généralement à cette personne "supposez que les données que vous recevez sont valide".

Mais Nous ne pouvons pas faire confiance à toute entrée que nous ne pouvons pas contrôler car son contenu est inconnu et pourrait exploiter une vulnérabilité dans notre logiciel.

Lire depuis un fichier est lire depuis une source non sûre, de même que les entrées d'un utilisateur ou les données provenant d'un réseau par exemple.

Pourquoi ne dois-je pas croire aux entrées ?

Pour comprendre pourquoi une entrée est dangereuse, nous devons d'abord comprendre ce qu'est une entrée.

Une entrée peut être lue depuis un clavier, le mouvement de la souris ou des clics de souris, ou depuis la lecture ou l'acceptation d'information de différente façons telles que les flux de données ou même les fonctions système. En fait, tout ce qu'obtient votre programme de l'extérieur est une entrée.

Peu importe le type d'entrée : par exemple, un utilisateur ou un autre système peut nous donner une mauvaise entrée, et les raisons peuvent être intentionnelles ou une erreur. Vous ne pouvez pas contrôler cette entrée, et la raison principale est que vous ne pouvez pas deviner ce que l'entrée sera.

Les résultats peuvent être des "données" vides (NULL) que l'utilisateur nous fournit, un nombre qui est hors de l'étendue attendue, ou une plus grande quantité de caractères que nous attendions, ou même une tentative de changer l'adresse de la variable qui accepte L'entrée de l'utilisateur. Nous ne pouvons tout simplement pas savoir ce que l'utilisateur va fournir.

Tout manipulation "non-sûre" de l'entrée peut aboutir à

  • la récupération d'information critique que l'utilisateur n'est pas autorisé à voir,
  • la modification non permise de donnée,
  • la corruption de donnée,
  • l'interruption (crash, plantage) du programme lui-même.

A quel type de problèmes pouvons-nous nous attendre ?

Pour chaque type de bug, vous pouvez sans doute trouver un type d'attaque, mais je vais vous donner une petite liste de types d'attaque communs, au lieu d'écrire beaucoup de types d'attaque.

Le types d'attaque les plus communs sont :

Débordement de tampon

C'est quand une donnée dépasse la quantité de mémoire qui lui est allouée :

var
  iNums : array [0..9] of integer;
  ...
  FillChar (iNums[-1], 100, #0);
  ...
  for i := -10 to 10 do
  readln (iNums[i]);
  ...

Dans cet exemple, nous pouvons voir que le tableau statique iNums n'accepte que 10 nombres, alors que nous entrons dans cette variable 21 nombres.

Veuillez noter que le compilateur sait signaler de tels cas, qui ne sont que des formes peu élaborées.

Si l'utilisateur peut entrer de la donnée qui est envoyée dans un tampon, il peut entrer certaines valeurs pouvant être interprétées par les instructions en code machine, qui seront écrites en dehors de notre tampon. L'ordinateur pourra alors exécuter ce code au lieu du code qui aurait dû être là.

C'est le débordement de tampon.

Attaque par déni de service (DoS)

Le déni de service n'est pas seulement un problème de réseau, il peut exister dans d'autres formes :

procedure Recurse;
begin
  while (True) do
    begin
      Recurse;
    end;
end;

Cette procédure s'exécutera jusqu'à ce que le système soit à court de ressources car il alloue plus de mémoire de pile à chaque récursion, cela provoquera l'arrêt de réponse du système, ou même le crash. Bien que certains systèmes, comme GNU/Linux, tentera de vous donner la possibilité d'arrêter le programme, cela prendra un certain temps pour se faire.

Veuillez remarquer que cela n'est qu'un exemple statique, mais nous pouvons faire une attaque DoS sur un système en exécutant ce code.

Une autre attaque DoS connue est le manque de libération de ressources système telle que la mémoire, les sockets, les descripteurs de fichiers, ...

Par exemple :

...
  begin
    while True do
      begin
        Getmem(OurPtr, 10);
        OurPtr := Something;
      end;
  end.

Cet exemple montre une allocation de mémoire (Getmem est comme malloc en langage C ; il réserve de la mémoire pour utilsiation), mais nous sortons de l'exécution sans avoir libérer la mémoire après emploi.

Injections

Quand l'utilisateur nous donne une entrée avec laquelle nous travaillons sans l'avoir assainie, l'utilisateur peut mettre dedans des marque SQL ou du code (comme du script, ou du code machine) par exemple, qui amènera notre programme à réaliser quelque action indésirable (p.ex. supprimer des enregistrements/tables, retourner des données protégées telles que des structures de bases de données/tables, utilisateur et mot de passe, contenu de dossier ou de fichier ou même exécuter un programme sur l'ordinateur).

Un exemple d'injection SQL :

Entrée d'utilisateur :

 SVP, entrez votre nom : a' OR 1=1

Dans le code :

...
Write('SVP, entrez votre nom : ');
ReadLn(sName);
Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name='#32 + sName + #32);
...

En soumettant cette instruction SQL avec l'entrée de l'utilisateur, il résultera de l'ajout de OR 1=1 dans la requête passée à la base de donnée : dans ce cas, la condition sera toujours vraie et l'utilisateur aura obtenu un éccès au programme et pourra connaître la liste des mots de passe (NdT : on fait l'hypothèse que ces derniers sont enregistrés en clair, mauvaise idée).

Accès à vos données et modifications

Ce n'est pas uniquement votre programme qui aura accès aux données qu'il utilise. Si vous stockez de la donnée dans des fichiers ou des bases de données (distantes), un attaquant peut obtenir l'accès à travers le système d'exploitation (et/ou la base de données et/ou la couche réseau/protocole de base de données).

Chiffrement : est-ce suffisant ?

Pour contrer la menace décrite ci-dessus, les programmeurs utilise souvent le chiffrement, il peut être utilisé pour fournir :

  • la protection de la confidentialité des données
  • la non-répudiation (est-ce que la donnée a été créée par la personne qui le prétend) et l'intégrité (la donnée est-elle inchangée), en utilisant des mécanismes supplémentaires de signature numérique/hachage).

ensemble pour

  • la communication des données
  • stockage/récupération des données

Si vous utilisez ces méthodes, vos données ne sont toutefois pas automatiquement sûres.

Il y a de multiples attaques possibles sur les données chiffrées :

  • attaque sur des algorithmes mal sécurisés ou leur implémentation (p.ex. en utilisant une attaque avec du texte brut connu)
  • attaque sur les clés de chiffrement (p.ex. en faisant une rétro-analyse de votre programme, si vous stocker des clés dans un fichier ou en dur dans le programme, ou en corrigeant le programme pour intercepter les clés/mots de passe entrés par l'utilisateur).

Si vous ne savez pas exactement ce que vous allez faire (et vous ne saurez pas, à moins d'avoir une formation en cryptographie), svp utilisez (par ordre décroissant de préférence) :

  • des bibliothèques réputées qui sont maintenues/corrigées (p.ex. les bibliothèque internes de FPC, et des bibliothèques telles que DCPCrypt ou des bibliothèques externes comme OpenSSL ou cryptlib) qui utilise des protocoles réputés et largement utilisés qui utilisent des algorithmes réputés et largement utilisés.
    • Utilisez p.ex. Trusted Authentication/SSPI pour la connectivité SQL Server/Firebird pour éviter l'envoi de mots de passe a travers le câble (dans Firebird >= 2.0 : en texte clair !) et s'appuie sur la sécurité du système d'exploitation pour l'authentification.
    • Utilisez Cryptlib ou OpenSSL avedc Synapse pour implémenter SSL/TSL au lieu d'une solution personnelle.
  • des protocols et APIs réputées et largement utilisés au lieu de solutions personnelles. Ces protocoles doivent utiliser des algorithmes de cryptographie/hachage bien compris et largement employés. Exemples :
    • GPG/PGP
    • TLS (SSL) avec PKI/CAs (de préférence ne faites pas confiance au CAs dont vous avez besoin, et réalisez des certificats d'authenthification client si votre analyse de la menace l'exige.
    • SSH (e.g. avec clé d'authentification publique/privée, si nécessaire avec renforcement par des périphrases pour les clés)
    • Dans Windows, utilisez l'API pour obtenir l'utilisateur actuellement authentifié. Si vous mettez en application la sécurité adaptée du système (longueur de mot de passe, modification, accès physiques etc), vous n'aurez pas besoin de gérer votre propore mécanisme de nom de connexion/mot de passe au niveau de l'application.
  • des algorithmes bien connus de crypto/hachage (tels que AES/Rijndael, 3DES et SHA512). Soyez conservateur dans les algorithmes que vous acceptez (p.ex. MD5 n'est pas sûr dans la signature de message).

Le chiffrement est une partie d'un ensemble possible de mesures de sécurité ; l'effort/l'argent dépensé doit être évalué comme faisant partie de l'analyse de la sécurité (voir plus bas).

Mythes et suppositions

Many of the security issues exist because of ignoring important warnings and information that was given by the compiler, and by thinking that your program does not contain any exploitable problem.

Here are some examples for this type of problem:

Mythes:

  • Security by Obscurity - When no one knows about a problem no one can take advantage of it; e.g. use an obscure column name for storing passwords in your database.
  • Secure programming language - There are languages such as Perl that many people think are secure from buffer overflows and other vulnerabilities while that is not true.
  • Hash password is secure - A file that has a hashed password is not secure. Hash can only passed one and you can not retrieve the original data. I don't get this. Does the author mean that a hashed password can be retrieved by a brute force or rainbow table attack and that it therefore needs a salt, or multiple hash rounds? --BigChimp 16:19, 24 July 2011 (CEST)
  • Nothing can break my program - Believing you're the only programmer in the world who writes faultless code is probably a bit optimistic. Maybe you're lucky and you just write code that doesn't work right without exploitable security vulnerabilies...

Suppositions:

  • The QA team will find and fix my security bugs.
  • The user (or somebody else) will not attack my program and its data.
  • My program will be used only for its original use.
  • All exceptions can remain unhandled.

Analyse pour comprendre les menaces et la sécurité

In general, a programmer (or his employers/business owners) should perform an analysis of all threats (from physical access/attack through logical and social engineering attacks) should be done for the entire system (including the infrastructure - OS, database, network as well as physical machines/cabling/buildings/external connections) to analyse whether you're not leaving open a security hole that is unacceptable (from a risk/benefit perspective).

The extent of the analysis should depend on the value of the data/processes that the system protects. It obviously makes no sense to go crazy trying to analyse your house's physical security when developing a hobby program to keep track of your bridge scores.

Benefits of these kind of analyses:

  • risks remaining after security measures are made known or explicit. Often, there is some discussion on the chances of this risk occurring, or the impact associated with it, but the fact that there is a remaining attack vector is at least clear and decisions can be made based on that information
  • you can fairly easily see what security measures are overengineered ("too secure", waste of money) or underengineered ("not secure enough"). If you can't use this information now, you can at least learn from it for other projects, including future maintenance/modification of your program

Solutions spécifiques

Now we know some problems we can encounter when developing programs, we should learn how to fix these problems. All of the problems we saw above manifest into two types: assumptions and lack of careful programming. And for learning how to fix them, we first need to learn to think in a different way than we have up to now.

Débordement

For fixing overflow of data, like buffers and other type of input, we first of all need to identify the type of data we need to work with.

Débordement de tampon

If we return to our example of:

var
  iNums: array [0..9] of Integer;
  ...
  FillChar(iNums[-1], 100, #0);
  ...
  for i := -10 to 10 do
    ReadLn(iNums[i]);
  ...


We see here a range that was overflowed by our values, without even checking if the index number is correct.

In dynamic/open arrays in Pascal we can know the limits of the allocated memory. So all we need to do is check if the size is too small or too big for our buffer, and limit the accepting for the size we wish it to be.

So the example should be changed into:

var
  iNums: array [0..9] of Integer;
 
  ...
  FillChar (iNums[Low(iNums)], High(iNums), #0);
  ...
 
  for i := Low(iNums) to High(iNums) do
    ReadLn(iNums[i]);
  ...

But wait, something is not right yet!

The readln will accept an unlimited amount of chars, and no one promises us that it will be an integer or even in the range we can handle.

Débordement numérique

While a string in Pascal is a pure array (hrmm hrmm.. not really, at least not in FPC, but let's pretend it is for a second, OK ?) so readln will try to find and see what are its limits and will not try to overflow the range we gave that type, but numbers are not the same.

Numbers have limits, a computer/compiler has limits of many kinds regarding memory and numbers. It can give only a "small" amount of memory for numbers (floating point and integer numbers). And many times we do not need a large range of numbers to use (like boolean variable that needs only two numbers usually).

In the above example we may have a buffer overflow that will cause a range check error that will give us the wrong number (Carry Flag reminder issues... I'm not going to explain them in here), and we also have a DoS effect, because our program will halt from that point.

So what can we do to fix that?

First of all we may wish to work with a string variable that will be the length of the largest number +1 (for minus sign), or we can create our own readln procedure/function that will specialize with the integer type.

For the first option we can do the following (copied from the FPC documentation):

Program Example74;
 
{ Program to demonstrate the Val function. }
Var 
  I, Code: Integer; 
begin
  Val(ParamStr(1), I, Code);
  If Code <> 0 then
    Writeln('Error at position ', code, ' : ', Paramstr(1)[Code])
  else
    Writeln('Value : ', I);  
end.

Here we see how to convert a string into an integer with some very easy error handling. The function StrToInt may also do the trick but it then we need to capture an exception in any error dealing.


Here is a small example for a small readln like procedure for integer numbers.

program MyReadln;
uses 
  CRT;
 
procedure MyIntReadLn(var Param: Integer; ParamLength: Integer);
var
  Line: string; 
  ch: char;
  Error: Integer;
   
begin
  Line  := '';
 
  repeat
    ch := ReadKey;
    if (Length (Line) <> ParamLength) then
      begin
       if (ch in ['0'..'9']) then
        begin
          Line := Line + ch;
          write (ch);
        end
       else
       if (ch = '-') and (Length(Line) = 0) then
        begin
          Line := '-';
          write (ch);
        end;
       end;
       
     if (ch = #8) and (Length(Line) <> 0) then // backspace
      begin
       Line := Copy(Line, 1, Length(Line) - 1);
       gotoxy(WhereX - 1, WhereY);
       write(' ');
       gotoxy(WhereX - 1, WhereY);
      end;
   until (ch = #13);
 
   val(Line, Param, Error);
 
   if (Error <> 0) then
     Param := 0;
 
  writeln;
end;
 
var
  Num : Integer;
 
begin
  Write('Number: ');
  MyIntReadLn(Num, 2);
  WriteLn('The number is: ', Num);
end.

Please note that you can make it even better, and more efficient if you wish. This is only a very small example to show how to do it.

Quels sont les risques de sécurité dans les débordement ?

Overflow of memory can allow arbitrary CPU code to be executed and users may run whatever type of code they wish, and nothing can stop them.

Dénis de service

Denial of Service (DoS) is one of the hardest types of attacks to prevent. The reasons are:

  • The denial of service can even be executed without any exploitable bug, like using the "ping" program on a lot of machines to DoS a machine connected to the internet.
  • Every system resource can be a possible denial of service, like opening sockets, reading files or allocating memory.
  • Removal of files like a kernel module can cause a big problem. I don't get this line --BigChimp 19:32, 24 July 2011 (CEST)
  • Lack of configuration or wrong configuration can cause a denial of service as well if it allows vulnerable resources to be misused.
  • Too much permissions or lack of them I don't get that not enough permissions can be a problem. Neither too many, either? Is a security risk, obviously but not a DoS problem. --BigChimp 19:32, 24 July 2011 (CEST) --BigChimp 19:32, 24 July 2011 (CEST).
  • Almost any type of exploit can result in a denial of service.

So as you can see, a denial of service can be almost anything that can stop the system from working as it should, because of exploitation or buggy code or just a program that captures system resources..

In the above denial of service example:

procedure Recurse;
begin
  while (True) do
    begin
      Recurse;
    end;
end;

I created also a stack overflow (another type of buffer overflow), that caused the computer to need more memory resources to continue executing the code.

Any system resource that is available to the program can be abused by not returning it back to the system when the program "does not need it anymore". The keeping of system resources like memory, or sockets remove from other programs the ability to perform some of their actions. That way most programs will stop their execution and report an error, and some will hang and keep on looking for the system resources.

Please note that some of the abuse of system resources exists because of a bug in the programming, like waiting for a 150k buffer, while the actual buffer is only 2 bytes, and when the program is still looking for the 150k buffer a new request for a 150k buffer is made etc.. until the system is not able to answer any of the requests anymore (this is a known type of attack).

A good workaround for this bug is to limit how many non full buffers can be allocated at one time. If the buffer is not full after a timeout, it should be free. However, this solution will also cause a Denial of Service, because the communication will stop anyway at some point, or a slow connection can cause data loss.

Injection

There are many ways to inject some type of code into our programs. As we saw at the above example:

User Input:

 Please enter your name: a' OR 1=1

Inside the code:

... 
write('Please enter your name: '); 
readln(sName); 
Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name='#32 + sName + #32); 
...

The injection occurred when we do not filter our code (sanitize is the more professional word :)): this means checking that we receive only the exact type of input that we are looking for, and nothing else.

For example, we could check if sName has spaces. If so, do not continue checking for the rest of the variable. This helps if the username is only allowed to be one word consisting of letters, maybe the tick sign (') and maybe even underscore (_) and then it's over. If we enter a number, this should be illegal (unless we wish to use "hacker like language" (leetspeak), or allow the use of numbers.

There are many ways to sanitize your data. The less effective one (but often used) is the following:

Désinfection inefficace

function ValidVar (const S: AnsiString; AllowChars: TCharset): Boolean;
var
  i: Word;
begin
  i := 0;
  Result := True;
  
  while (Result) and (i <= Length(S)) do
  begin
    Inc(i);
    Result := S[i] in AllowChars;
  end;
end;

The function return true if we have a valid structure of content given by the AllowChars in the S variable. Please note that this function is only a proof of concept and may need more work in order to be fully used.

Another way to do the same is to use regular expression as the following (this is a Proof of concept only in the Perl language. FPC does not have a fully supported regular expression engine that allows to modify strings):

$sName =~ s/[^a-z0-9\_\']//gi;

The regular expression removes any non valid chars from the string and returns to us the purged string. Please note that as far as I know, this regular expression will work also in ereg engines, but with minimal adjustments (g flag instructs Perl to replace all the matching patterns found. i is for case insensitivity).

Now when we know that our input is valid, we need to see what is the use of the variable content. If the variable content is going into a database, or a cgi script, or anything else that has its own syntax, we must escape the content according to the non-allowed or control characters of the relevant language (e.g. SQL for databases).

There are many ways to escape this type of content. Let's assume for now that this content is going into a query of a database. Now first of all we must make sure that our escaping will not increase our data size above the length limits of our database fields. Because if they will, then we can change from an injection to a data loss/denial of server/buffer overflow problems (a respected database usually will truncate the data and sometimes not in a good location).

Usually the only escaping we need to do for using a string in a database is to escape only the ticks (') char (although some databases may have problems with more chars then ticks). So all we should do is to represent ticks in a way that will not effect the database engine, like backslash tick (\') or double every single tick to two ticks (''), or maybe even use another char that will be replace the ticks in the query and replace again when we will show it to the user.

Restriction des entrées avec les paramètres de requêtes SQL

Obligatory visual illustration.

After we made sure that we respect the limits, we can continue in our attempts. To escape the code we can use several approaches. A less debugging friendly way, but a sure way of correct escaping is to use the parameters technique:

Query1.SQL.Add('SELECT Password FROM tblUsers WHERE Name=?');
Query1.Parameters.Add(sName);
if (Query1.Execute) then
...

This technique allows the database engine to escape the parameter in a way that we could use the content without any problems of illegal characters. Also, some databases have increased performance for repeated calls to this code as it can prepare an internal statement with parameters for this. The down side is that we can never debug the outcome of the query. That is, we can not see how the content of sName embedded in the SQL statement, and we can never see if our query was correct because of that.

However, once you have tested the query without parameters, adding the parameters is quite easy, so in practice, this problem is not as big as it seems.

Code efficace

The security measures mentioned above complicate code if you used the most efficient code to get needed functionality. Fortunately, you can sometimes pass off code complications to a framework/library that deals with it. For example: in the SQL DoS example you can let the database engine deal with escaping data using parametrised queries at the small added cost of having to use parameters in your code.

However, writing performance efficient but security-vulnerable code doesn't help anybody except liability lawyers, and forensic experts.

Au-delà du document

While in this document I gave some short (yeah, I know it's an understatement ;)) examples and information on how to create better code, there are many issues that I did not touch in this document. Part of them are user privileges for execution of the programs, system root kits and other problems that our code needs to take in consideration (environment variable is only one example).

SVP, veuillez lire plus de ressources techniques telles que :

Débordement de tampon :

Déni de service:

Injection SQL :