Writing Secure Code in C, You should know

Writing Secure Code in C

Writing secure code is very important. If you are c developer, then you should aware because in C there is no direct method to handle the exception (no inbuilt try and catch like another high-level language like C#). It is a responsibility of the developer to handle the all the exception manually. In this article, I will describe a few points that make your code cleaner and secure.

1. Understand the requirement first:

Before writing the code, it is very important to understand all the requirements. If you have clear visibility of the final product then it helps you to create test cases for the testing. It is very important to create proper test cases for the final product, it makes your product wonderful.

 

2. Create proper flow before writing a module:

Before writing the code it is a good idea to draw your thought which means creating a flow diagram for each scenario. Believe me, it will save many hours.

Previously I was working on a module where I needed to handle a lot of conditions for the different scenarios within a single call-back function. What mistake I had done that without creating the flow diagram I did the coding. Really it was a mistake and destroy my many hours in resolving the bugs. Finally, my all problem getting solved when I created the flow diagram.

So It is my advice to create the flow diagram before starting the coding and helps to make your code secure.

 

3. Initialize variables and pointer before use:

This is a good habit to initialize the variable and pointer at the time of declaration. It avoids strange behavior during accessing these objects. It is also necessary to initialize these objects with a defined state.

This rule does not only apply to predefined data types it is also applied to the user-defined data type (like structure). So you also have to make sure that your complex type functions, such as typedef structs, are initialized first.

Let’s take an example, suppose you have a complex type of structure of function pointers that are used in TCP/IP communication. So in that scenario at the time of object creation, you should initialize these function pointers and also need to take a flag that allows only single initialization.

typedef struct
{

    int Isinitialized;

    int (*OpenSocket)(void);

    int (*CloseSocket)(int);

    int (*ReadFromServer)(int,char*,short);

    int (*WriteToServer) (int,char*,short);

} sCommStructure;

 

Now at the time of object creation initialize the structure of function pointers.

static sCommStructure

g_sCommStructure =
{
    0,/* is initialized */
    NULL, /* open function */
    NULL,  /* close function */
    NULL, /* read function */
    NULL  /* write function */
};

 

Later on, during the construction of the object, you can check the flag for the initialization of function pointers, shown below

sCommStructure *CreateTcpComm(void)
{

    if (g_sCommStructure.Isinitialized == 0)
    {
        g_sCommStructure.OpenSocket = &TcpSocketCreate;
        g_sCommStructure.CloseSocket = &TcpSocketClose;
        g_sCommStructure.ReadFromServer = &TcpSocketReceive;
        g_sCommStructure.WriteToServer = &TcpSocketSend;
    }

    return (&g_sCommStructure);
}

 

If you want to learn more about the c language, here 10 Free days C video course for you.

C tutorial

4. Don’t ignore compiler warnings:

Nowadays compilers are very smart if they find any strange constructs then they throw a warning. So don’t avoid these warnings because they may be preventing you from future bugs.

 

5. Check return values:

There is a lot of developers that they avoid the return value of the function.  It could be dangerous and might be the cause of the application crash. You should check the return value of each function, it helps you to detect the bugs easily prevent application crashing.

This rule does not only apply to the user-created function, it also applies to the library function and standard library function. You must handle the return value properly and on error must display the proper error message it will save your time.

Let’s see an example code,

In the below code, everything is fine until the malloc function doesn’t return the null pointer. If malloc returns the NULL, the code will crash.

#include<stdio.h>
#include<stdlib.h>

int main(void)
{
    int *piBuffer = NULL;
    int n = 10, i = 0;

    //creating integer of size n.
    piBuffer = malloc(n * sizeof(int));

    //Assigned value to allocated memory
    for (i = 0; i < n; ++i)
    {
        piBuffer [i] = i * 3;
    }

    //Print the value
    for (i = 0; i < n; ++i)
    {
        printf("%d\n", piBuffer[i]);
    }
    //free up allocated memory

    free(piBuffer);
    return 0;
}

 

We can resolve the above problem to verify the return value of the malloc function. If malloc returns the null pointer, the code will display an error message and terminate the execution.

#include<stdio.h>
#include<stdlib.h>

int main(void)
{
    int *piBuffer = NULL;
    int n = 10, i = 0;

    //creating integer of size n.
    piBuffer = malloc(n * sizeof(int));

    //make sure pcBuffer is valid or not
    if (piBuffer == NULL)
    {
        // allocation failed, exit from the program
        fprintf(stderr, "Out of memory!\n");
        exit(1);
    }

    //Assigned value to allocated memory
    for (i = 0; i < n; ++i)
    {
        piBuffer[i] = i * 3;
    }

    //Print the value
    for (i = 0; i < n; ++i)
    {
        printf("%d\n", piBuffer[i]);
    }

    //free up allocated memory
    free(piBuffer);

    return 0;
}

 

6. Use enums as error types:

You should categorize the errors using the enum for each module. An enum is far better than a macro or numeric value. This categorization of errors for each module helps you to find the error at the time of debugging. This technique also helps the other developer who is assigned later on this module.

In the below example, I have created a list of some errors related to the file operation using the enum. The benefits of adding the last enum are that it provides the total number of enum entries.

typedef	enum
{
    FILE_OPEN_ERROR =0,

    FILE_CLOSE_ERROR,

    FILE_READ_ERROR,

    FILE_WRITE_ERROR,

    FILE_LAST_ERROR

} FILE_ERROR_LIST; //declaration of enum for file operation

 

7. Check input values:

If your module expects input value from another module then don’t believe in the incoming data. It is your responsibility to verify the incoming data, either might be you dereference the invalid pointer or access the array beyond its boundary that can be a cause of crashing or undefined behavior. This type of issue can be waste your many hours.

Let see an example,

Suppose you have a lookup table that contains the message for different scenarios and you need to create the module that is used to display the messages. To avoid any crash or undefined behavior you should check the incoming index for a lookup table. In this scenario enum is a good choice, you can map the enum element with a lookup table message.

 

8. Use string safe function:

Buffer overflow is a critical problem, it is also a point of entry for hackers and attackers. If you are working on the POS application, then you should learn how to play with string. There is a lot of string function in C but in which some functions are not secured, so you should be careful before working on string functions.

Let see an example,

A strcpy() is a well-known string function that is used to copy the data from the source to the destination buffer. This function has a lot of bugs, now the C committee introduces the new safe version of string function strcpy_s in C11. So it is my advice that use only string safe functions.

Syntax of strcpy_s(),

errno_t strcpy_s(char * restrict dst,rsize_t max, const char * restrict src);

The max parameter is used by strcpy_s() to check that the src is not bigger than the dst buffer. If there is any problem that occurs then it returns the error code.

 

9. Code readability:

You should always think that you are not writing the code for yourself. If anybody read your code then they should have clear visibility. It is a very good habit to write readable code, your code should be like a book that can be understood by any person easily.

There are the following points that make your code more readable

a) Braces:

You should always use a brace with conditional and branching statements like, if, else, switch, while, do while, and for keywords. It will increase the readability of your code and reduce the risk of bugs.

For example,

Don’t do this,

if(flag)
amount = 50.0;

 

you should do,

if(flag)
{
    amount = 50.0;
}

 

just like above also use braces for the loop even they have only a single or empty statement.

you should do,

while (!flag)
{
  // empty statement
}

 

b) Variable and function naming:

Don’t use i,j,k ..etc for the variable name. Use the proper name for the variable that explains the functionality. This rule is also applicable for function naming, you should write the function name in a way that explains the functionality of the function.

Let see an example,

Suppose you require to create two integer variables to store the value of month and day.

Don’t do,

int i;
int j;

You should do,

int day;
int month;

Suppose you have required to create a function to calculate salary,

Don’t do,

int test()
{
    /*

    Calculate salary

    */
    return 0;
}

 

You should do,

int calculateSallary()
{
    /*

    Calculate salary

    */
    return 0;
}

 

c) Comment related to the code implementation:

Good comments increase the readability of the code. Every module should have good commenting, it helps the developer who comes on the project after you and it also helps to maintain the codebase.

One thing you should remember is if you are commenting on the code which has multiple lines then you should use the preprocessors conditional compilation feature (for example, #if 0 … #endif), it increases the code clarity.

See the below example,

// Don't do this

/*

//bit reversal function
unsigned int ReverseTheBits(register unsigned int x)
{
 x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1));
 x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2));
 x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4));
 x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8));

return((x >> 16) | (x << 16));


}

 */

// Do this

#if 0
//bit reversal function
unsigned int ReverseTheBits(register unsigned int x)
{
    x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1));
    x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2));
    x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4));
    x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8));

    return((x >> 16) | (x << 16));


}
#endif

 

d) Don’t write complex code:

During the development, The code of a product will be changed and extended many times. You should not be thinking about the initial stage of development but you should think about all stage of development. At the time of coding you should remember one thing, there are a lot of people who will come on this project after you. So don’t write the code only for you, think about the other.

 

10. Use qualifiers properly:

You should know how to use qualifiers (const. volatile, restrict, …etc) properly either you will face a lot of problems. In C, one of the most popular qualifiers is const and volatile, we can also use this qualifier together. See this article for more detail, Application of const and volatile together.

Below find some important places where you should use const:

  • In the “call by reference”, if you don’t want to change the actual value which has passed in function.
    Eg.
    int PrintData ( const char *pcMessage);
  • In some places, const is better than macro because const handle by the compiler and has a type checking.
  • In the case of I/O and memory-mapped register, const is used with the volatile qualifier for efficient access.
    Eg.
    const volatile uint32_t *DEVICE_STATUS = (uint32_t *) 0x80102040;
  • When you don’t want to change the value of an initialized variable.

Below find some important places where you should use volatile:

  • Accessing the memory-mapped peripherals register or hardware status register.
#define COM_STATUS_BIT 0x00000006
uint32_t const volatile * const pStatusReg = (uint32_t*)0x00020000;
unit32_t GetRecvData()
{
    //Code to recv data
    while (((*pStatusReg) & COM_STATUS_BIT) == 0)
    {
       // Wait until flag does not set
    }
    return RecvData;
}
  • Sharing the global variables or buffers between the multiple threads.
  • Accessing the global variables in an interrupt routine or signal handler.
volatile int giFlag = 0;
ISR(void)
{
    giFlag = 1;
}
int main(void)
{
    while (!giFlag)
    {
       //do some work
    }
    return 0;
}

 

11. Mixing Signed and Unsigned Integers:

Don’t mix signed and unsigned integers together. If we mix signed and unsigned int in the program then it can create issues because as per the c standard if we perform the arithmetic operation on signed and unsigned numbers then the resultant value can be implementation dependent or undefined in some scenarios.

In C99, integer promotion is clearly defined that If an int can represent all values of the original type, the value is converted to an int, otherwise, it is converted to an unsigned int. All other types are unchanged by the integer promotions.

Note: My advice is that never mix the signed and unsigned and always enable the warning option in your IDE.

See the below program and think the output of,

#include <stdio.h>

int main(void)
{

    unsigned int uiData = 2;
    int iData = -20;

    if(iData + uiData > 6)
    {
        printf("%s\n", "a+b > 6");
    }
    else
    {
        printf("%s\n", "a+b < 6");
    }

    return 0;
}

If you are familiar with integer promotion then, of course, you know the answer either you need to read the integer promotion. So it is my recommendation when you performed an arithmetic operation where the operands are signed and unsigned then carefully perform the operation either you will get the undefined result.

You can read, Closer look at signed and unsigned integer.

 

12. Bit-wise Operators:

No doubt Bit-wise operators are a good choice but sometimes avoid to use of bit-wise operators. If you are working on negative integer numbers then it will be a good decision to avoid bitwise operators.

Let’s see the example code,

#include <stdio.h>

int main()
{
    int x = -30;
    x = x << 1;

    printf("%d\n", x);
    
    return 0;
}

If you will compile the above code, then you will get an undefined output.

 

13. Fixed-width Data Types:

You should use fixed length data type (uint8_t,uint16_t …etc) in place of implementation defined (int,long, …etc). In C99, C committee introduce <stdint.h> that define fixed length data types.

Writing Secure Code in C

14. Expose only what is needed:

In C, like other objective languages, there is no option to hide the information. If you are working on C, then you already know that every global variable and function that has no static keyword has global scope.

The global function and variable have the global scope that might be possible they access by another module and this module can change their functionality and value accidentally.

So we should use the static keyword with functions and variables that do not require outside the module in which they are declared.

 

15. Do typecasting carefully:

Some time typecasting creates a critical issue. So before performing any typecasting you should analyze the code very carefully. Mostly when you are going to convert int to short int or signed to unsigned or float to int.

 

16. Use code analyzer tool:

Every company has some coding guidelines but still, you should analyze your code with the code analyzer. Nowadays there is a lot of code analyzer available, you can check the below link to see the list of some code analyzer for C/C++.

Link for code analyzer

 

Recommended Post

Leave a Reply

Your email address will not be published. Required fields are marked *