Tags: python, slae-64, shellcode
This post is the seventh of seven that will comprise my attempt at the SecurityTube Linux Assembly Expert (SLAE-64) certification. Each post will correspond to seven assignments of varying difficulty. I decided to take SLAE-64 to shore up my knowledge of assembly and shellcoding before diving in to OSCE.
For this assignment, I chose to use RSA as my encryption schema. I did this not because it’s practical, but because it was the most interesting to me. According to wikipedia, “RSA (Rivest–Shamir–Adleman) is one of the first public-key cryptosystems and is widely used for secure data transmission. In such a cryptosystem, the encryption key is public and it is different from the decryption key which is kept secret (private). … In an asymmetric key encryption scheme, anyone can encrypt messages using the public key, but only the holder of the paired private key can decrypt. Security depends on the secrecy of the private key.”
As stated above, RSA uses two separate keys, one public and one private.
After the keys are created, a sender can encrypt their message with the receiver’s public key and the receiver then decrypts the message with their own private key.
That’s enough information to go on as far as this post is concerned.
First up, I knew I was going to use a cryptography library. Up until this project, i had always used pycrypto. I only realized while completing this assignment is that pycrypto is no longer maintained. I googled around for a replacement and found pycryptodomex. It is a fork of pycrypto that is actively maintained and can provide a drop-in replacement for pycrypto in existing code. Below is the install process to get pycryptodomex on a debian-based system. I also make use of pipenv to setup a python virtual environment. If you haven’t used pipenv and you write python, do yourself a favor and check it out. It’s written by Kenneth Reitz (creator of the requests library) so you can be it’s exceptional.
Installation of pycrptodomex
sudo apt-get install build-essential libgmp3-dev python3-dev
pipenv --python 3.6 install pycryptodomex
pipenv shell
I broke my crypter up into a few classes, which we’ll take a look at below. The code is well documented so I don’t plan to do much more than a brief synopsis of the code. The full file can be found on slae-64 gitlab repo.
This section is just for your information, in case you’re wondering where the libraries in the code snippets below came from.
import os
import sys
import argparse
import textwrap
import subprocess
from pathlib import Path
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import AES, PKCS1_OAEP
from Cryptodome.Random import get_random_bytes
This class is responsible for creating an RSA key-pair or loading one of the keys for the Crypter class to utilize when performing en|decryption.
class KeyManager:
def __init__(self, passphrase: str = None) -> None:
""" Creates, saves, and loads both private and public RSA keys used for asymmetric encryption.
If a passphrase is specified, KeyManager will encrypt private key file with said passphrase. The scrypt key
derivation function is used to guard against dictionary attacks.
:param passphrase: Optional - passphrase used to encrypt private key file
"""
self.privkey: RSA.RsaKey
self.pubkey: RSA.RsaKey
self.passphrase = passphrase
def create_keys(self, key_size: int) -> None:
""" Create private and public RSA keys to be used in asymmetric encryption.
:param key_size: size of the key used in bits
"""
key: RSA.RsaKey = RSA.generate(key_size)
protection = 'scryptAndAES128-CBC' if self.passphrase else None
self.privkey = key.export_key(passphrase=self.passphrase, pkcs=8, protection=protection)
self.pubkey = key.publickey().export_key()
def save_keys(self, privkey_outfile: str, pubkey_outfile: str) -> None:
""" Write private and public RSA keys to disk.
:param privkey_outfile: path to private key file (default: private.pem)
:param pubkey_outfile: path to public key file (default: public.pem)
"""
privkey_outfile = Path(privkey_outfile)
privkey_outfile.write_bytes(data=self.privkey)
pubkey_outfile = Path(pubkey_outfile)
pubkey_outfile.write_bytes(data=self.pubkey)
def load_key(self, key_type: str, infile: str) -> None:
""" Load either a private or public RSA key from disk.
:param key_type: Required - Expects string of either "public" or "private"
:param infile: Required - path to key file
"""
if key_type == 'private':
self.privkey = RSA.import_key(Path(infile).read_bytes())
elif key_type == 'public':
self.pubkey = RSA.import_key(Path(infile).read_bytes())
This class does a few things, but at its core, it loads and executes shellcode. It really just makes a bunch of calls to things like ld
, nasm
, objdump
, and gcc
. I spent some time trying to get python to execute the shellcode directly from memory, but gave it up after a few failed attempts and just went with a call to execv.
class Shellcode:
def __init__(self, shellcode_src: str = None) -> None:
""" Links, assembles, and loads shellcode from various sources.
OS Executable Dependencies:
from .asm/.nasm
ld
nasm
objdump
from linked/assembled binary
objdump
from msfvenom ... -f raw output
None
from encrypted shellcode generated with this tool
None
:param shellcode_src: source file used to generate/load shellcode
"""
self.shellcode: bytes = None
self.shellcode_src: str = shellcode_src
def _run_cmd(self, cmd: str) -> subprocess.CompletedProcess:
""" Run shell command, capture stdout, and return the results.
:param cmd: shell command to be run by subprocess.run
:return: CompletedProcess returned by subprocess.run(cmd)
"""
completed_proc: subprocess.CompletedProcess
try:
completed_proc = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
except OSError as e:
if e.errno == os.errno.ENOENT:
print(f'{cmd.split()[0]} not installed', file=sys.stderr)
else:
print(e.with_traceback(), file=sys.stderr)
raise SystemExit
return completed_proc
def load(self, src_type: str = None) -> None:
""" Depending on src_type; links, assembles, dumps, and loads shellcode from various sources.
Valid source types are
bin
previously linked/assembled binary
asm
.asm/.nasm source code
raw
shellcode previously encrypted by this tool
msfvenom ... -f raw output file
:param src_type: type of source file (bin, asm, raw)
"""
objfile: str
_shellcode: bytes
proc: subprocess.CompletedProcess
_shellcode_src: Path = Path(self.shellcode_src)
# link/assemble .nasm/.asm
if src_type == 'asm':
objfile = f"{_shellcode_src.stem}.o"
self._run_cmd(f'nasm -f elf64 {_shellcode_src} -o {objfile}')
self._run_cmd(f"ld {objfile} -o {_shellcode_src.stem}")
# dump shellcode from binary (includes types bin/asm)
if src_type != 'raw':
proc = self._run_cmd(f"objdump -d {_shellcode_src.stem} | grep '^ ' | cut -f 2")
_shellcode = br"\x" + br"\x".join(proc.stdout.split())
self.shellcode = _shellcode
# do nothing special for '-f raw' and encrypted shellcode
if src_type == 'raw':
self.shellcode = _shellcode_src.read_bytes()
def print(self) -> None:
""" Simple helper to display loaded shellcode """
print(self.shellcode.decode())
def execute(self) -> None:
""" Exectutes the loaded shellcode.
Dependencies:
gcc
Uses gcc to compile a shellcode skeleton and then exec's the compiled program over the running instance of
this tool.
"""
template: str
skeleton: Path
template = f"""\
#include <stdio.h>
#include <string.h>
unsigned char code[] = \\
"{self.shellcode.decode()}";
int main() {{
printf("Shellcode length: %zu\\n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}}
"""
skeleton = Path("shellcode-skeleton.c")
skeleton.write_text(textwrap.dedent(template))
self._run_cmd(f'gcc -o {skeleton.stem} {skeleton} -fno-stack-protector -z execstack')
os.execv(skeleton.stem, (skeleton.stem,))
The Crypter class has a reference to an instance of a Shellcode class and an instance of a KeyManager class. Through those, it encrypts or decrypts the shellcode with the appropriate key.
class Crypter:
def __init__(self, keymgr: KeyManager, shellcode: Shellcode) -> None:
""" Encrypts and decrypts shellcode
If the crypter class is being instantiated, it's assumed that shellcode to either decrypt or encrypt is loaded
into a Shellcode isntance and will be available via self.shellcode. Similarly, either a public or private key
is expected to be loaded into a KeyManager instance and will be available via self.keymgr.
:param keymgr: KeyManager instance
:param shellcode: Shellcode instance
"""
self.keymgr = keymgr
self.shellcode = shellcode
self.tag: bytes
self.nonce: bytes
self.ciphertext: bytes
self.encrypted_session_key: bytes
def encrypt(self) -> None:
""" Encrypts piece of shellcode using recipient's public RSA key.
Since we want to be able to encrypt an arbitrary amount of data, we use a hybrid encryption scheme.
We use RSA with PKCS#1 OAEP for asymmetric encryption of an AES session key. The session key can then be
used to encrypt all the actual data.
We use the EAX mode to allow detection of unauthorized modifications.
- https://pycryptodome.org/en/latest/src/examples.html
"""
session_key: bytes = get_random_bytes(16)
rsa_cipher: RSA.RsaKey = PKCS1_OAEP.new(self.keymgr.pubkey)
self.encrypted_session_key = rsa_cipher.encrypt(session_key)
aes_cipher = AES.new(session_key, AES.MODE_EAX)
self.nonce = aes_cipher.nonce
self.ciphertext, self.tag = aes_cipher.encrypt_and_digest(self.shellcode.shellcode)
def save(self, outfile='shellcode.enc') -> None:
""" Simple helper to write shellcode to disk """
with open(outfile, 'wb') as f:
for byte in (self.encrypted_session_key, self.nonce, self.tag, self.ciphertext):
f.write(byte)
def decrypt(self):
""" Decrypts piece of shellcode using private RSA key.
At this point, it's assumed that both the private key and the encrypted shellcode have been loaded by their
respective classes.
"""
key_sib = self.keymgr.privkey.size_in_bytes()
self.encrypted_session_key = self.shellcode.shellcode[:key_sib]
self.nonce = self.shellcode.shellcode[key_sib:key_sib + 16]
self.tag = self.shellcode.shellcode[key_sib + 16:key_sib + 32]
self.ciphertext = self.shellcode.shellcode[key_sib + 32:]
# use private key to decrypt session key
cipher_rsa = PKCS1_OAEP.new(self.keymgr.privkey)
session_key = cipher_rsa.decrypt(self.encrypted_session_key)
# use decrypted session key to decrypt the shellcode
cipher_aes = AES.new(session_key, AES.MODE_EAX, self.nonce)
self.shellcode.shellcode = cipher_aes.decrypt_and_verify(self.ciphertext, self.tag)
The ArgumentParser is really what ties all of the classes together. The command line options coupled with the logic below the parser make the three disparate parts a cohesive unit. I tried wrapping this all up in an executable zip using the zipapp module, but coudln’t because of the C bindings in pycryptodomex. There are other avenues to make this a single executable, but I hadn’t had an opportunity to explore the zipapp module until now. Unfortunately, C bindings are a limitation of the executable zip format.
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Asymmetric Shellcode Encrypter/Decrypter')
key_opts = parser.add_argument_group('key options', description='creates, saves, and loads both private and public RSA keys used for asymmetric encryption')
key_opts.add_argument('--key-size', help='size of the key used in bits (default: 2048)', default=2048, choices=[2048, 3072])
key_opts.add_argument('--privkey-out', help='path to private key file (default: private.pem)', default='private.pem')
key_opts.add_argument('--pubkey-out', help='path to public key file (default: public.pem)', default='public.pem')
key_opts.add_argument('--key-type', help='type of key to load', choices=['public', 'private'], default='public')
key_opts.add_argument('--passphrase', help='passphrase used to en|decrypt private key file')
in_or_out = key_opts.add_mutually_exclusive_group()
in_or_out.add_argument('--load-key', dest='infile', help='load either a private or public RSA key from disk')
in_or_out.add_argument('--create', action='store_true', help='create private and public RSA keys and save them')
sc_opts = parser.add_argument_group('shellcode options', description='links, assembles, and loads shellcode from various sources')
sc_opts.add_argument('--shellcode-src', help='source from which to generate/load shellcode')
sc_opts.add_argument('--shellcode-type', choices=['asm', 'raw', 'bin'], help='type of source for shellcode')
sc_opts.add_argument('--print', action="store_true", help='print the loaded shellcode (requires --shellcode-src and --shellcode-type)', default=False)
sc_opts.add_argument('--execute', action="store_true", help='execute the loaded shellcode (requires --shellcode-src and --shellcode-type)', default=False)
crypt_opts = parser.add_argument_group('crypter options', description='encrypts and decrypts shellcode')
crypt_opts.add_argument('--encrypt', action='store_true', help='encrypt shellcode loaded via --shellcode-src && --shellcode-type (expects public key)', default=False)
crypt_opts.add_argument('--encfile-out', help='path to encrypted shellcode file (default: shellcode.enc)', default='shellcode.enc')
crypt_opts.add_argument('--decrypt', action='store_true', help='decrypt shellcode loaded via --shellcode-src && --shellcode-type (expects private key)', default=False)
args = parser.parse_args()
shellcode: Shellcode
keymanager: KeyManager = KeyManager(passphrase=args.passphrase)
# keymanager options
if args.create:
keymanager.create_keys(key_size=args.key_size)
keymanager.save_keys(privkey_outfile=args.privkey_out, pubkey_outfile=args.pubkey_out)
elif args.infile:
keymanager.load_key(key_type=args.key_type, infile=args.infile)
# shellcode options
if (args.shellcode_src and not args.shellcode_type) or (not args.shellcode_src and args.shellcode_type):
parser.error(f"shellcode-src and shellcode-type must both be specified if either is used")
elif args.shellcode_src and args.shellcode_type:
shellcode = Shellcode(shellcode_src=args.shellcode_src)
shellcode.load(src_type=args.shellcode_type)
if args.print:
shellcode.print()
# crypter options
if args.shellcode_src and args.shellcode_type and args.infile:
crypter = Crypter(keymanager, shellcode)
if args.encrypt:
if args.key_type == 'private':
parser.error(f"if encrypting, you must load a public key (--load-key PUBLIC_KEY && --key-type public)")
crypter.encrypt()
crypter.save(outfile=args.encfile_out)
elif args.decrypt:
if args.key_type == 'public':
parser.error(f"if decrypting, you must load a private key (--load-key PRIVATE_KEY && --key-type private)")
crypter.decrypt()
crypter.save(outfile=f"{args.encfile_out}.decrypted")
if args.execute:
crypter.shellcode.execute()
If you can’t tell, I like writing Python! It feels like it may have been overkill, but I enjoyed writing it.
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: E64-1584
My SLAE-64 Assignments Repository