Lukas' Notes

security memory

Definition

Shellcode

Shellcode is the payload an attacker places in memory — historically the overflowed buffer itself — and jumps to by hijacking the saved return address. Its name comes from the classic goal, execve("/bin/sh"): spawn a shell on the victim machine so the attacker has an interactive process with the program’s privileges. It is the injected-code variant of instruction pointer hijacking, the form the technique took before NX made the stack non-executable.

NOP Sled

Aiming at an unpredictable stack address

Stack addresses are not always predictable — they depend on the environment, on ASLR, and on how the program reached the vulnerable call. The attacker does not know exactly where their payload will land, only roughly. A NOP sled absorbs that uncertainty: a stretch of nop instructions (0x90) placed before the real payload so that landing anywhere inside the sled slides execution forward harmlessly until it reaches the payload.

  • guess an address somewhere in the sled;
  • overwrite the saved rip with the guess;
  • if the guess lands inside the sled, the CPU runs nops until it reaches execve("/bin/sh") at the end.

The [[Knowledge/x86-64 Instruction|nop]] is one byte on x86-64, so a wide sled tolerates a wide guess. The sled is a statistical trick, not a defence — it widens the attacker’s target.

Worked Example

A 23-byte execve("/bin/sh") payload

 0:  48 31 f6                xor    rsi,rsi          ; rsi = 0
 3:  56                      push   rsi              ; null terminator for the string
 4:  48 bf 2f 62 69 6e 2f
     2f 73 68                movabs rdi,0x68732f2f6e69622f  ; "/bin//sh"
 e:  57                      push   rdi              ; push "/bin//sh\0"
 f:  54                      push   rsp              ; rsp now points at the string
10:  5f                      pop    rdi              ; rdi = address of "/bin//sh"
11: b0 3b                   mov    al, 0x3b         ; rax = 59 (execve syscall number)
13: 58                      pop    rax              ; (alternative: pop the constant)
14: 99                      cltd                    ; rdx = 0 (sign-extend eax into edx)
15: 0f 05                   syscall                 ; execve("/bin//sh", NULL, NULL)

The shell "/bin//sh" is built on the stack as the constant 0x68732f2f6e69622f — the byte-reversed ASCII of //bin/sh — pushed through rdi, then the syscall fires with rdi pointing at the string, rsi/rdx null as the argv/envp.

No Null Bytes

Why the payload reads through gets / strcpy cleanly

The overflowed input often reaches the buffer through a string function — gets, strcpy, scanf("%s") — which stops at the null terminator. A single \0 inside the payload would truncate it, so a usable shellcode must contain no null bytes. The chosen assembly reflects this constraint:

  • xor rsi,rsi instead of mov rsi,0 (which would encode 0x00 literals);
  • mov al,0x3b writes one byte, leaving the rest of rax untouched;
  • the string constant /bin//sh is byte-reversed into a non-zero movabs.

A payload that reads through a string function is a payload that survives the string semantics; a payload with a single null is one that gets cut off before it reaches the stack.