Repository: Gallopsled/pwntools

Issue:
ROP: Should have a ROP.jmp_esp / ROP.jmp_rsp gadget
This is an easy gadget for us to detect, and helps out a lot with older challenges that have NX disabled.


Discussion:
Is there some reason to disable JOP engine search on ROPGadget as occur at https://github.com/Gallopsled/pwntools/blob/f1628b80fe03b6af620aa396961f2e555259ec84/pwnlib/rop/rop.py#L1158? Maybe speed?

If pwntools should have a ROP.jmp_esp / ROP.jmp_rsp gadget, maybe JOP engine search should be enabled.
We disable JOP since we currently only auto-recognize ROP gadgets.  The ROP module is very limited in the gadgets it will accept (we had a GSOC project to do symbolic execution a few years ago, but it never landed).

JOP would just take extra time to scan the binary.  If all we care about is `jmp esp` we can find that much easier than filtering all JOP gadgets.
I see. I have not thought about only looking for its bytes in the executable sections while loading. It is a simpler and faster solution.

Resolving patch:
diff --git a/pwnlib/rop/rop.py b/pwnlib/rop/rop.py
--- a/pwnlib/rop/rop.py
+++ b/pwnlib/rop/rop.py
@@ -88,6 +88,36 @@
     0x7fffe038:       b'-c\x00$'
     0x7fffe03c:       b'ls\x00$'
 
+ROP also detects 'jmp $sp' gadget to help exploit binaries with NX disabled.
+You can get this gadget on 'i386':
+
+    >>> context.clear(arch='i386')
+    >>> elf = ELF.from_assembly('nop; jmp esp; ret')
+    >>> rop = ROP(elf)
+    >>> jmp_gadget = rop.jmp_esp
+    >>> elf.read(jmp_gadget.address, 2) == asm('jmp esp')
+    True
+
+You can also get this gadget on 'amd64':
+
+    >>> context.clear(arch='amd64')
+    >>> elf = ELF.from_assembly('nop; jmp rsp; ret')
+    >>> rop = ROP(elf)
+    >>> jmp_gadget = rop.jmp_rsp
+    >>> elf.read(jmp_gadget.address, 2) == asm('jmp rsp')
+    True
+
+Gadgets whose address has badchar are filtered out:
+
+    >>> context.clear(arch='i386')
+    >>> elf = ELF.from_assembly('nop; pop eax; jmp esp; int 0x80; jmp esp; ret')
+    >>> rop = ROP(elf, badchars=b'\x02')
+    >>> jmp_gadget = rop.jmp_esp    # It returns the second gadget
+    >>> elf.read(jmp_gadget.address, 2) == asm('jmp esp')
+    True
+    >>> rop = ROP(elf, badchars=b'\x02\x06')
+    >>> rop.jmp_esp == None         # The address of both gadgets has badchar
+    True
 
 ROP Example
 -------------------
@@ -1304,7 +1334,7 @@ def ret2dlresolve(self, dlresolve):
         self.raw(call)
 
     def __getattr__(self, attr):
-        """Helper to make finding ROP gadets easier.
+        """Helper to make finding ROP gadgets easier.
 
         Also provides a shorthand for ``.call()``:
             ``rop.function(args)`` is equivalent to ``rop.call(function, args)``
@@ -1341,6 +1371,25 @@ def __getattr__(self, attr):
                 count = int(attr.split('_')[1])
             return self.search(move=count)
 
+        #
+        # Check for 'jmp_esp'('i386') or 'jmp_rsp'('amd64')
+        #
+        if attr == 'jmp_esp' and context.arch == 'i386' \
+        or attr == 'jmp_rsp' and context.arch == 'amd64':
+            jmp_sp = {'i386': 'jmp esp',
+                      'amd64': 'jmp rsp'
+                     }[context.arch]
+
+            insn_asm = b'\xff\xe4'
+
+            for elf in self.elfs:
+                for addr in elf.search(insn_asm, executable = True):
+                    if set(pack(addr)) & self._badchars:
+                        continue
+
+                    return Gadget(addr, [jmp_sp], [], context.bytes)
+            return None
+
         if attr in ('int80', 'syscall', 'sysenter'):
             mapping = {'int80': 'int 0x80',
              'syscall': 'syscall',


Test patch: