Repository: Gallopsled/pwntools

Issue:
cyclic_find() does not work when given an integer needle larger than 2**(8n)-1
I understand that a decision was made to not take the `n` value (for `cyclic()` and `cyclic_find()`) from the `context` in favour of backwards compatibility. See #484

The workaround is that, while `cyclic()` and `cyclic_find()` have a default `n` of 4, `cyclic_find()` will allow you to specify more than `n` bytes as a needle, and will loudly truncate to the first four bytes before doing a lookup:

```
% git -C /opt/pwntools rev-parse HEAD
bd12d1874f17e1fd6a9b26411ccc7ccd6c31f4cb

% python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.

>>> import pwn, pwnlib

>>> pwnlib.__version__
'4.8.0dev'

>>> pwn.cyclic_find(pwn.cyclic()[1000:1004])
1000

>>> pwn.cyclic_find(pwn.cyclic()[1000:1008])
[!] cyclic_find() expects 4-byte subsequences by default, you gave b'zaakbaak'
    Unless you specified cyclic(..., n=8), you probably just want the first 4 bytes.
    Truncating the data at 4 bytes.  Specify cyclic_find(..., n=8) to override this.
1000
```

However, there is no such accommodation when giving an integer-type needle that is larger than `2**(8n)-1` (i.e. larger than `0xffffffff`)

This works:

```
>>> pwn.cyclic_find(pwn.u32(pwn.cyclic()[1000:1004]))
1000
```

This doesn't:

```
>>> pwn.cyclic_find(pwn.u64(pwn.cyclic()[1000:1008]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/pwntools/pwnlib/context/__init__.py", line 1547, in setter
    return function(*a, **kw)
  File "/opt/pwntools/pwnlib/util/cyclic.py", line 211, in cyclic_find
    subseq = packing.pack(subseq, bytes=n)
  File "/opt/pwntools/pwnlib/util/packing.py", line 149, in pack
    raise ValueError("pack(): number does not fit within word_size [%i, %r, %r]" % (0, number, limit))
ValueError: pack(): number does not fit within word_size [0, 7737572710135456122, 4294967296]
```

See also:

```
>>> pwn.cyclic_find(2**32-1)
-1

>>> pwn.cyclic_find(2**32)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/pwntools/pwnlib/context/__init__.py", line 1547, in setter
    return function(*a, **kw)
  File "/opt/pwntools/pwnlib/util/cyclic.py", line 211, in cyclic_find
    subseq = packing.pack(subseq, bytes=n)
  File "/opt/pwntools/pwnlib/util/packing.py", line 149, in pack
    raise ValueError("pack(): number does not fit within word_size [%i, %r, %r]" % (0, number, limit))
ValueError: pack(): number does not fit within word_size [0, 4294967296, 4294967296]
```

Getting `-1` for the first test there makes sense, since `b'\xff\xff\xff\xff'` isn't in a normal `cyclic()` pattern. The second test in an ideal world would also return `-1` because nothing that looks like `b'\x00\x00\x00\x00\x01\x00\x00\x00'`, no matter how it's truncated, appears in the normal `cyclic()` pattern.

Off the top of my head, perhaps if the user gives an int to `cyclic_find()` that is `>= 2**(8n)` then the value should be loudly truncated similarly to the case where more than `n` bytes are given.

I normally work with x86 systems, I don't know if `cyclic_find()` has any affordances for endianness when given an integer needle, but if so they should be taken into account when truncating.


Discussion:

Resolving patch:
diff --git a/pwnlib/util/cyclic.py b/pwnlib/util/cyclic.py
--- a/pwnlib/util/cyclic.py
+++ b/pwnlib/util/cyclic.py
@@ -197,6 +197,16 @@ def cyclic_find(subseq, alphabet = None, n = None):
         >>> cyclic_find(0x61616162, endian='big')
         1
 
+        Similarly to the case where you can provide a bytes value that is longer
+        than four bytes, you can provided an integer value that is larger than
+        what can be held in four bytes. If such a large value is given, it is
+        automatically truncated.
+
+        >>> cyclic_find(0x6161616361616162)
+        4
+        >>> cyclic_find(0x6261616163616161, endian='big')
+        4
+
         You can use anything for the cyclic pattern, including non-printable
         characters.
 
@@ -208,14 +218,32 @@ def cyclic_find(subseq, alphabet = None, n = None):
         n = context.cyclic_size
 
     if isinstance(subseq, six.integer_types):
+        if subseq >= 2**(8*n):
+            # Assumption: The user has given an integer that is more than 2**(8n) bits, but would otherwise fit within
+            #  a register of size 2**(8m) where m is a multiple of four
+            notice = ("cyclic_find() expected an integer argument <= {cap:#x}, you gave {gave:#x}\n"
+                      "Unless you specified cyclic(..., n={fits}), you probably just want the first {n} bytes.\n"
+                      "Truncating the data at {n} bytes.  Specify cyclic_find(..., n={fits}) to override this.").format(
+                cap=2**(8*n)-1,
+                gave=subseq,
+                # The number of bytes needed to represent subseq, rounded to the next 4
+                fits=int(round(float(subseq.bit_length()) / 32 + 0.5) * 32) // 8,
+                n=n,
+            )
+            log.warn_once(notice)
+            if context.endian == 'little':
+                subseq &= 2**(8*n) - 1
+            else:
+                while subseq >= 2**(8*n):
+                    subseq >>= 8*n
         subseq = packing.pack(subseq, bytes=n)
     subseq = packing._need_bytes(subseq, 2, 0x80)
 
     if len(subseq) != n:
-        log.warn_once("cyclic_find() expects %i-byte subsequences by default, you gave %r\n"
-            "Unless you specified cyclic(..., n=%i), you probably just want the first 4 bytes.\n"
-            "Truncating the data at 4 bytes.  Specify cyclic_find(..., n=%i) to override this.",
-            n, subseq, len(subseq), len(subseq))
+        log.warn_once("cyclic_find() expected a %i-byte subsequence, you gave %r\n"
+            "Unless you specified cyclic(..., n=%i), you probably just want the first %d bytes.\n"
+            "Truncating the data at %d bytes.  Specify cyclic_find(..., n=%i) to override this.",
+            n, subseq, len(subseq), n, n, len(subseq))
         subseq = subseq[:n]
 
     if alphabet is None:


Test patch: