How does msfvenom shellcode actually work to execve sh -c ls

79 views Asked by At
MOV    RAX, 0x68732f6e69622f
CDQ
PUSH   RAX
PUSH   RSP
POP    RDI
PUSH   RDX
PUSH   0x632d
PUSH   RSP
POP    RSI
PUSH   RDX
CALL   FUN_0000001e
INSB   RDI, DX
JNC    FUN_0000001e

FUN_0000001e:
PUSH   RSI
PUSH   RDI
PUSH   RSP
POP    RSI
PUSH   0x3b
POP    RAX
SYSCALL

These are the assembly instructions for the msfvenom payload:

msfvenom -p linux/x64/exec CMD='ls'

I understand how the execve systemcall is prepared, and /bin/sh -c passed as an argument. But how is this code executing the ls command ?

I tried executing this shellcode using c function pointer, and it executed ls as expected.

1

There are 1 answers

3
Peter Cordes On

Single-step it in a debugger and watch the values on the stack change.

0x0068732f6e69622f is '/bin/sh' with a terminating 0 byte.

push rsp/pop rdi is a 2-byte way to do mov rdi, rsp.

insb / jnc don't actually run, those are just data. insb's machine code is just 0x6c; it's a "string" instruction that implicitly uses [rdi] post-incremented. IDK what asm syntax this is for, but it's not NASM. To get NASM to assemble it, I just removed the operands so it was plain insb.

If you assemble this and look at the machine code, you can see the bytes:

...
  401017:       e8 03 00 00 00          call   40101f <FUN_0000001e>
  40101c:       6c                      ins    BYTE PTR es:[rdi],dx
  40101d:       73 00                   jae    40101f <FUN_0000001e>

000000000040101f <FUN_0000001e>:
  40101f:       56                      push   rsi
 ...

ASCII 6C 73 00 is "ls" zero-terminated. That's the string whose address was pushed by call.

A normal person would write that as db 0x6c, 0x73, 0 or db "ls", 0 instead of "disassembling" those bytes into instruction mnemonics.


Note that this "shellcode" is not free from 0 bytes: a forward call will have zeros in the high 3 bytes of the rel32 (the "return" address that is pushed, is the address of the ASCII bytes that follow it). Also, push 0x632d is a qword push of a sign-extended imm32 whose high 2 bytes are 0, since there's no push word size override.

And jae with rel8=0 of course has a zero byte.

Two ways to make this work in NUL-free shellcode involve putting the call + 0-terminated string at the end of the payload, so it's calling backwards. jmp rel8 to the call, then jump backward to near where you came from with the string address on the stack. The call/pop trick is widely used in shellcode examples. Or in x86-64 code, lea rdi, [mystring + 0x11111111] / sub rdi, 0x11111111, but that takes more bytes of machine code.

Other options include creating it from immediates like mov eax, 'xxls' ; shr eax, 16 / push rax, or mov eax, 'ls' ^ 0x11111111 / xor eax, 0x11111111 / push rax. (The same technique can work for the -c part, or you might be able to cram parts of them into one immediate.) Or have the string bytes there in the payload with a non-zero placeholder, and mov byte [reg+whatever], al to store zero bytes where they need to go, from some register that you zeroed with xor eax, eax or whatever. Or AH would already be zero if you materialized an 8-bit constant with push 1 / pop rax (3 bytes).

msfvenom can make shellcode that voids zero bytes if you ask it to, so you can see what strategy it picks.