Github link: SLAE Assignment #7
For this final assignment, I decided that I wanted to add to the functionality of my custom encoder/egghunter combo by modifying my shconfig.py program to encrypt the decoder/egghunter stub.
For the AES implementation, I decided to utilize the CBC method using the implementation written by kokke.
The usage instructions below will produce the final shellcode.c output which you then compile with GCC and run.
- Python 3.5+
- ./compile.sh bencoder
This uses NASM to compile the assembly source for the encoder.
This should produce three files: enc.hex, key.hex, and raw.hex.
enc.hex contains the encoded shellcode bytes
key.hex contains the key used by the decoder stub for the encoded shellcode bytes
raw.hex contains the original shellcode that was encoded by ./bencoder
- ./shconfig.py -e <egg value> -f asm –encrypt_with_key <key for AES encryption>
<egg value>: This should be 4 valid hexadecimal characters only
<key for AES encryption>: This is the key you are using to encrypt the bytes which comprise the decoder stub & egghunter
- gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
Explanation of files
bencoder.nasm (The custom encoder):
This assembly program encodes the execve-stack shellcode by XOR’ing each byte with a new pseudorandom byte obtained from the Intel ‘rdrand’ CPU instruction. This creates a key to decode the execve-stack shellcode.
It writes the pseudorandom bytes obtained by the ‘rdrand’ instruction into the ‘key.hex’ file, the encoded bytes into the ‘enc.hex’ file, and the original execve-stack shellcode bytes into the ‘raw.hex’ file.
(Creates, compiles and executes C source file for encryption, captures encryption program output and writes C source file for decryption):
Reads all of the binary .hex files created by the compiled bencoder.nasm program to obtain the hex values.
It then places the values into a dictionary which is keyed by the filename. The key and egg value are then combined and formatted appropriately to be placed in the C source file. The egghunter/decoder shellcode bytes are then pieced together with the ‘enc.hex’ bytes from the dictionary as well as the bytes from the egg value entered in step 2 above. The opcodes from the egghunter/decoder combo program were obtained by compiling the decoder.nasm program, and then using objdump to display the opcodes. The create_decoder_shellcode() function re-creates these opcodes based on whatever egg value was specified as well as whatever value happens to be associated with the ‘enc.hex’ key of the dictionary (named ‘byte_string_dict’) created earlier. A C source file is then written (encrypt.c) and GCC is called to compile it. The encrypt program is then executed, which outputs the variable declaration for the encrypted shellcode bytes to be used by the decryption program.
The C source file for decryption/shellcode execution is then written (shellcode.c).
This is the C source file written and compiled by the shconfig.py program which returns a string which is the variable declaration for the encrypted decoder/egghunter shellcode that contains the encoded execve() shellcode.
Compiling and then running this program will print the encrypted version of the bytes entered into the in uint8_t array, in the form of the variable declaration which shconfig.py places into the shellcode.c source file.
This is the C source file used to decrypt and execute the encrypted decoder/egghunter shellcode stub which will decode and then execute the execve() shellcode.
compile.sh (compilation shell script):
Uses NASM to compile the supplied assembly program.
enc.hex (encoded shellcode):
These bytes are the encoded shellcode produced by the bencoder.nasm program.
key.hex (key bytes):
These bytes serve as the key to decode the encoded shellcode.
raw.hex (raw shellcode):
This is the exeve-stack shellcode prior to encoding. Provided only as an example for examination, and not currently used by any program here.
Most of the heavy lifting here is done by shconfig.py. Three modules are used: argparse, subprocess, and random. Argparse is used to handle the options sent to the program during execution, subprocess is used to execute GCC with the appropriate options for the intermediate encryption step, and random is used to get pseudorandom values for the initialization vector in the AES implementation.
shconfig starts by parsing out the arguments passed to it using argparse. The egg that is specified is stored in a variable called “egg”, and the encryption key specified is stored in a variable called “args.encryption_key”.
The program then starts collecting information from the 3 files that were produced by the bencoder program that was written in assembly (bencoder.nasm). Using a for loop, the actual hex bytes are stored into a dictionary (byte_string_dict), as is the count of bytes (file_byte_counts).
Next, the byte values of the egg are inserted in front of the “key” that is used to XOR against the encoded shellcode for the decoder stub. The result is a dictionary called “eggified_key”. An integer value called “buf_size” is set to the value which equals the number of bytes in the encoded shellcode plus a fixed value of 68, which represents the total size of the egg and decoder stub.
At this point shconfig.py now has enough information to produce the intermediate encryption files (encrypt.c & compiled ./encrypt program). The “create_source_files” function is called with arguments: eggified_key, buf_size, args.encryption_key.
Because the aes.c program performs encryption on 16 byte chunks using the CBC method, the “create_source_files” function needs to determine how many 16 byte chunks are going to be encrypted. It does that by checking if the “buf_size” argument passed to it is evenly divisible by 16. If not, it performs floor division of the total number of bytes to obtain the number of 16 byte chunks, and then adds one more chunk.
String values for use in the encrypt.c program are then saved, and the initialization vector array values (uint8_t iv) are determined by utilizing the “randint” method from the “random” library.
More string representations of values for the variables in the encrypt.c program are then computed, and the encrypt.c file is written out to disk with all of the appropriate C declarations.
“subprocess.check_output” is used to run GCC with the following: “gcc encrypt.c -o encrypt”.
Next, “subprocess.check_output” is used to run the now-compiled ./encrypt program and store the output from the program into the “encrypted_shellcode_var” variable which will be used in the “shellcode.c” final output file.
Finally, the “create_source_files” function calls it’s own function to piece together the strings needed for the rest of the C declarations that will go into the “shellcode.c” final output file, and then it writes that file to disk, and the program terminates.
As an example, the shellcode.c file that is produced will look similar to the following:
The preprocessor directives for the files to include are self-explanatory. The defines specify that we are using CBC mode only, and not using ECB mode.
key : The key used for decryption.
iv : The initialization vector value that was produced with the help of the “randint” method in the “random” library.
buffer: This is the buffer used for decryption. The fixed size was dynamically generated by the aforementioned “buf_size” integer variable, representing the total number of bytes for the encoded shellcode, egg, and decoder stub.
the_key : The key used for decoding after decryption, prepended with the egg value that the decoder stub searches for.
code: This is the array holding the actual shellcode bytes to be executed after decryption. The fixed size was dynamically generated based on the total size of the decoder/egghunter stub (in bytes).
in: This is the array that holds the encrypted shellcode bytes that are to be decrypted, which in this case is the decoder/egghunter stub shellcode bytes.
Please note that shellcode.c serves as a demonstration only, and the values will of course be different if you run your own example (as they should be!). Naturally, if you intend on making use of this program in a more real-world use case, it would be wise to find a way to stage the encryption key somewhere in memory that makes it difficult to spot what it is without significant dynamic analysis of your code.
When shellcode.c is compiled and ran, the decoder stub/egghunter shellcode is decrypted, execution passes to those shellcode bytes, which decode the execve-stack shellcode bytes and pass execution to them. Bam! We have our root shell:
That’s it for this assignment! This one was by far my favorite after #4, as it allowed for some creativity.
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
Student ID: SLAE-860