• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Gallopsled / pwntools / 1461b486abbb14c5fe4ad10e86ac2a921d486203

pending completion
1461b486abbb14c5fe4ad10e86ac2a921d486203

push

github-actions

GitHub
Fix documentation of tube.recvall (#2163)

3873 of 6370 branches covered (60.8%)

12198 of 16609 relevant lines covered (73.44%)

0.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.98
/pwnlib/encoders/i386/ascii_shellcode.py
1
""" Encoder to convert shellcode to shellcode that contains only ascii
2
characters """
3
# https://github.com/Gallopsled/pwntools/pull/1667
4

5
from __future__ import absolute_import
1✔
6

7
from itertools import product
1✔
8

9
import six
1✔
10

11
from pwnlib.context import LocalContext
1✔
12
from pwnlib.context import context
1✔
13
from pwnlib.encoders.encoder import Encoder
1✔
14
from pwnlib.encoders.encoder import all_chars
1✔
15
from pwnlib.util.iters import group
1✔
16
from pwnlib.util.packing import *
1✔
17

18

19
class AsciiShellcodeEncoder(Encoder):
1✔
20
    """ Pack shellcode into only ascii characters that unpacks itself and
21
    executes (on the stack)
22

23
    The original paper this encoder is based on:
24
    https://julianor.tripod.com/bc/bypass-msb.txt
25

26
    A more visual explanation as well as an implementation in C:
27
    https://vincentdary.github.io/blog-posts/polyasciishellgen-caezar-ascii-shellcode-generator/index.html#22-mechanism
28
    """
29

30
    def __init__(self, slop=20, max_subs=4):
1✔
31
        """ Init
32

33
        Args:
34
            slop (int, optional): The amount esp will be increased by in the
35
                allocation phase (In addition to the length of the packed
36
                shellcode) as well as defines the size of the NOP sled (you can
37
                increase/ decrease the size of the NOP sled by adding/removing
38
                b'P'-s to/ from the end of the packed shellcode).
39
                Defaults to 20.
40
            max_subs (int, optional): The maximum amount of subtractions
41
                allowed to be taken. This may be increased if you have a
42
                relatively  restrictive ``avoid`` set. The more subtractions
43
                there are, the bigger the packed shellcode will be.
44
                Defaults to 4.
45
        """
46
        if six.PY2:
1!
47
            super(AsciiShellcodeEncoder, self).__init__()
1✔
48
        elif six.PY3:
×
49
            super().__init__()
×
50
        self.slop = slop
1✔
51
        self.max_subs = max_subs
1✔
52

53
    @LocalContext
1✔
54
    def __call__(self, raw_bytes, avoid=None, pcreg=None):
1✔
55
        r""" Pack shellcode into only ascii characters that unpacks itself and
56
        executes (on the stack)
57

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

64
        Raises:
65
            RuntimeError: A required character is in ``avoid`` (required
66
                characters are characters which assemble into assembly
67
                instructions and are used to unpack the shellcode onto the
68
                stack, more details in the paper linked above ``\ - % T X P``).
69
            RuntimeError: Not supported architecture
70
            ArithmeticError: The allowed character set does not contain
71
                two characters that when they are bitwise-anded with eachother
72
                their result is 0
73
            ArithmeticError: Could not find a correct subtraction sequence
74
                to get to the the desired target value with the given ``avoid``
75
                parameter
76

77
        Returns:
78
            bytes: The packed shellcode
79

80
        Examples:
81

82
            >>> context.update(arch='i386', os='linux')
83
            >>> 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"
84
            >>> encoders.i386.ascii_shellcode.encode(sc)
85
            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'
86
            >>> avoid = {'\x00', '\x83', '\x04', '\x87', '\x08', '\x8b', '\x0c', '\x8f', '\x10', '\x93', '\x14', '\x97', '\x18', '\x9b', '\x1c', '\x9f', ' ', '\xa3', '\xa7', '\xab', '\xaf', '\xb3', '\xb7', '\xbb', '\xbf', '\xc3', '\xc7', '\xcb', '\xcf', '\xd3', '\xd7', '\xdb', '\xdf', '\xe3', '\xe7', '\xeb', '\xef', '\xf3', '\xf7', '\xfb', '\xff', '\x80', '\x03', '\x84', '\x07', '\x88', '\x0b', '\x8c', '\x0f', '\x90', '\x13', '\x94', '\x17', '\x98', '\x1b', '\x9c', '\x1f', '\xa0', '\xa4', '\xa8', '\xac', '\xb0', '\xb4', '\xb8', '\xbc', '\xc0', '\xc4', '\xc8', '\xcc', '\xd0', '\xd4', '\xd8', '\xdc', '\xe0', '\xe4', '\xe8', '\xec', '\xf0', '\xf4', '\xf8', '\xfc', '\x7f', '\x81', '\x02', '\x85', '\x06', '\x89', '\n', '\x8d', '\x0e', '\x91', '\x12', '\x95', '\x16', '\x99', '\x1a', '\x9d', '\x1e', '\xa1', '\xa5', '\xa9', '\xad', '\xb1', '\xb5', '\xb9', '\xbd', '\xc1', '\xc5', '\xc9', '\xcd', '\xd1', '\xd5', '\xd9', '\xdd', '\xe1', '\xe5', '\xe9', '\xed', '\xf1', '\xf5', '\xf9', '\xfd', '\x01', '\x82', '\x05', '\x86', '\t', '\x8a', '\r', '\x8e', '\x11', '\x92', '\x15', '\x96', '\x19', '\x9a', '\x1d', '\x9e', '\xa2', '\xa6', '\xaa', '\xae', '\xb2', '\xb6', '\xba', '\xbe', '\xc2', '\xc6', '\xca', '\xce', '\xd2', '\xd6', '\xda', '\xde', '\xe2', '\xe6', '\xea', '\xee', '\xf2', '\xf6', '\xfa', '\xfe'}
87
            >>> sc = shellcraft.echo("Hello world") + shellcraft.exit()
88
            >>> ascii = encoders.i386.ascii_shellcode.encode(asm(sc), avoid)
89
            >>> ascii += asm('jmp esp') # just for testing, the unpacker should also run on the stack
90
            >>> ELF.from_bytes(ascii).process().recvall()
91
            b'Hello world'
92
        """
93
        if not avoid:
1✔
94
            vocab = bytearray(
1✔
95
                b"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")
96
        else:
97
            required_chars = set('\\-%TXP')
1✔
98
            allowed = set(all_chars)
1✔
99
            if avoid.intersection(required_chars):
1!
100
                raise RuntimeError(
×
101
                    '''These characters ({}) are required because they assemble
102
                    into instructions used to unpack the shellcode'''.format(
103
                        str(required_chars, 'ascii')))
104
            allowed.difference_update(avoid)
1✔
105
            vocab = bytearray(map(ord, allowed))
1✔
106

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

110
        int_size = context.bytes
1✔
111

112
        # Prepend with NOPs for the NOP sled
113
        shellcode = bytearray(b'\x90'*int_size + raw_bytes)
1✔
114
        subtractions = self._get_subtractions(shellcode, vocab)
1✔
115
        allocator = self._get_allocator(len(subtractions) + self.slop, vocab)
1✔
116
        nop_sled = b'P' * self.slop  # push eax
1✔
117
        return bytes(allocator + subtractions + nop_sled)
1✔
118

119
    @LocalContext
1✔
120
    def _get_allocator(self, size, vocab):
121
        r""" Allocate enough space on the stack for the shellcode
122

123
        int_size is taken from the context
124

125
        Args:
126
            size (int): The allocation size
127
            vocab (bytearray): Allowed characters
128

129
        Returns:
130
            bytearray: The allocator shellcode
131

132
        Examples:
133

134
            >>> context.update(arch='i386', os='linux')
135
            >>> vocab = bytearray(b'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~')
136
            >>> encoders.i386.ascii_shellcode.encode._get_allocator(300, vocab)
137
            bytearray(b'TX-!!!!-!_``-t~~~P\\%!!!!%@@@@')
138
        """
139
        size += 0x1e  # add typical allocator size
1✔
140
        int_size = context.bytes
1✔
141
        # Use eax for subtractions because sub esp, X doesn't assemble to ascii
142
        result = bytearray(b'TX')  # push esp; pop eax
1✔
143
        # Set target to the `size` arg
144
        target = bytearray(pack(size))
1✔
145
        # All we are doing here is adding (subtracting) `size`
146
        # to esp (to allocate space on the stack), so we don't care
147
        # about esp's actual value. That's why the `last` parameter
148
        # for `calc_subtractions` can just be zero
149
        for subtraction in self._calc_subtractions(
1✔
150
                bytearray(int_size), target, vocab):
151
            # sub eax, subtraction
152
            result += b'-' + subtraction
1✔
153
        result += b'P\\'  # push eax, pop esp
1✔
154
        # Zero out eax for the unpacking part
155
        pos, neg = self._find_negatives(vocab)
1✔
156
        # and eax, pos; and eax, neg ; (0b00010101 & 0b00101010 = 0b0)
157
        result += flat((b'%', pos, b'%', neg))
1✔
158
        return result
1✔
159

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

165
        int_size is taken from the context
166

167
        Args:
168
            vocab (bytearray): Allowed characters
169

170
        Returns:
171
            Tuple[int, int]: value A, value B
172

173
        Raises:
174
            ArithmeticError: The allowed character set does not contain
175
                two characters that when they are bitwise-and-ed with eachother
176
                the result is 0
177

178
        Examples:
179

180
            >>> context.update(arch='i386', os='linux')
181
            >>> vocab = bytearray(b'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~')
182
            >>> a, b = encoders.i386.ascii_shellcode.encode._find_negatives(vocab)
183
            >>> a & b
184
            0
185
        """
186
        int_size = context.bytes
1✔
187
        for products in product(vocab, vocab):
1!
188
            if products[0] & products[1] == 0:
1✔
189
                return tuple(
1✔
190
                    # pylint: disable=undefined-variable
191
                    unpack(p8(x)*int_size)  # noqa: F405
192
                    for x in bytearray(products)
193
                )
194
        else:
195
            raise ArithmeticError(
×
196
                'Could not find two bitwise negatives in the provided vocab')
197

198
    @LocalContext
1✔
199
    def _get_subtractions(self, shellcode, vocab):
200
        r""" Covert the sellcode to sub eax and posh eax instructions
201

202
        int_size is taken from the context
203

204
        Args:
205
            shellcode (bytearray): The shellcode to pack
206
            vocab (bytearray): Allowed characters
207

208
        Returns:
209
            bytearray: packed shellcode
210

211
        Examples:
212

213
            >>> context.update(arch='i386', os='linux')
214
            >>> sc = bytearray(b'ABCDEFGHIGKLMNOPQRSTUVXYZ')
215
            >>> vocab = bytearray(b'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~')
216
            >>> encoders.i386.ascii_shellcode.encode._get_subtractions(sc, vocab)
217
            bytearray(b'-(!!!-~NNNP-!=;:-f~~~-~~~~P-!!!!-edee-~~~~P-!!!!-eddd-~~~~P-!!!!-egdd-~~~~P-!!!!-eadd-~~~~P-!!!!-eddd-~~~~P')
218
        """
219
        int_size = context.bytes
1✔
220
        result = bytearray()
1✔
221
        last = bytearray(int_size)
1✔
222
        # Group the shellcode into bytes of stack cell size, pad with NOPs
223
        # if the shellcode does not divide into stack cell size and reverse.
224
        # The shellcode will be reversed again back to it's original order once
225
        # it's pushed onto the stack
226
        sc = tuple(group(int_size, shellcode, 0x90))[::-1]
1✔
227
        # Pack the shellcode to a sub/push sequence
228
        for x in sc:
1✔
229
            for subtraction in self._calc_subtractions(last, x, vocab):
1✔
230
                result += b'-' + subtraction  # sub eax, ...
1✔
231
            last = x
1✔
232
            result += b'P'  # push eax
1✔
233
        return result
1✔
234

235
    @LocalContext
1✔
236
    def _calc_subtractions(self, last, target, vocab):
237
        r""" Given `target` and `last`, return a list of integers that when
238
         subtracted from `last` will equal `target` while only constructing
239
         integers from bytes in `vocab`
240

241
        int_size is taken from the context
242

243
        Args:
244
            last (bytearray): Original value
245
            target (bytearray): Desired value
246
            vocab (bytearray): Allowed characters
247

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

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

255
        Examples:
256

257
            >>> context.update(arch='i386', os='linux')
258
            >>> vocab = bytearray(b'!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~')
259
            >>> print(encoders.i386.ascii_shellcode.encode._calc_subtractions(bytearray(b'\x10'*4), bytearray(b'\x11'*4), vocab))
260
            [bytearray(b'!!!!'), bytearray(b'`___'), bytearray(b'~~~~')]
261
            >>> print(encoders.i386.ascii_shellcode.encode._calc_subtractions(bytearray(b'\x11\x12\x13\x14'), bytearray(b'\x15\x16\x17\x18'), vocab))
262
            [bytearray(b'~}}}'), bytearray(b'~~~~')]
263
        """
264
        int_size = context.bytes
1✔
265
        subtractions = [bytearray(int_size)]
1✔
266
        for sub in range(self.max_subs):
1!
267
            carry = success_count = 0
1✔
268
            for byte in range(int_size):
1✔
269
                # Try all combinations of all the characters in vocab of
270
                # `subtraction` characters in each combination. So if
271
                # `max_subs` is 4 and we're on the second subtraction attempt,
272
                # products will equal
273
                # [\, ", #, %, ...], [\, ", #, %, ...], (0,), (0,)
274
                for products in product(
1✔
275
                    *[x <= sub and vocab or (0,) for x in range(self.max_subs)]
276
                ):
277
                    # Sum up all the products, carry from last byte and
278
                    # the target
279
                    attempt = target[byte] + carry + sum(products)
1✔
280
                    # If the attempt equals last, we've found the combination
281
                    if last[byte] == attempt & 0xff:
1✔
282
                        carry = (attempt & 0xff00) >> 8
1✔
283
                        # Update the result with the current `products`
284
                        for p, i in zip(products, range(sub + 1)):
1✔
285
                            subtractions[i][byte] = p
1✔
286
                        success_count += 1
1✔
287
                        break
1✔
288
            if success_count == int_size:
1✔
289
                return subtractions
1✔
290
            else:
291
                subtractions.append(bytearray(int_size))
1✔
292
        else:
293
            raise ArithmeticError(
×
294
                str.format(
295
                    '''Could not find the correct subtraction sequence
296
                to get the the desired target ({}) from ({})''',
297
                    target[byte], last[byte]))
298

299

300
encode = AsciiShellcodeEncoder()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc