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
这些是 msfvenom 有效负载的汇编指令:
msfvenom -p linux/x64/exec CMD='ls'
我了解 execve 系统调用是如何准备的,以及 /bin/sh -c 作为参数传递的。但是这段代码是如何执行 ls 命令的呢?
我尝试使用 c 函数指针执行此 shellcode,它按预期执行了 ls。
在调试器中单步执行并观察堆栈上的值变化。
0x68732f6e69622f
是 /bin/sh
push rsp
/pop rdi
是一种 2 字节的方式来实现 mov rdi, rsp
。
insb
/ jnc
实际上并没有运行,这些只是数据。 insb
的机器码就是0x6c
;这是一个“字符串”指令,隐式使用 [rdi]
后递增。我不知道这是什么 asm 语法,但它不是 NASM。为了让 NASM 组装它,我只是删除了操作数,所以它很简单 insb
。
如果你组装它并查看机器代码,你可以看到字节:
...
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
63 73 00
是 "ls"
以零结尾。这就是 call
推送地址的字符串。
普通人会将其写为
db 0x6c, 0x73, 0
或 db "ls", 0
,而不是将这些字节“分解”为指令助记符。
请注意,此“shellcode”并非不受
0
字节的影响:前向 call
将在 rel32
的高 3 个字节中包含零(推送的“返回”地址是 ASCII 字节的地址)跟着它)。此外,push 0x632d
是符号扩展的 imm32 的 qword 推送,其高 2 个字节为 0,因为没有 push word
大小覆盖。
并且 rel8=0 的
jae
当然有一个零字节。
在无 NUL 的 shellcode 中实现此功能的两种方法包括将
call
+ 0 结尾的字符串放在有效负载的末尾,因此它是向后调用的。 jmp rel8
到 call
,然后向后跳转到堆栈上字符串地址所在的位置附近。 call/pop 技巧在 shellcode 示例中广泛使用。或者在 x86-64 代码中,lea rdi, [mystring + 0x11111111]
/ sub rdi, 0x11111111
,但这需要更多字节的机器代码。
其他选项包括从立即数创建它,例如
mov eax, 'xxls'
; shr eax, 16
/ push rax
,或 mov eax, 'ls' ^ 0x11111111
/ xor eax, 0x11111111
/ push rax
。 (同样的技术也适用于 -c
部分,或者您可以将它们的一部分塞进一个立即数中。)或者将字符串字节放在有效负载中,并使用非零占位符,并使用 mov byte [reg+whatever], al
来存储它们需要去的地方为零字节,来自您用 xor eax, eax
或其他方式清零的某个寄存器。或者,如果您用 push 1
/ pop rax
(3 个字节)具体化一个 8 位常量,AH 已经为零。
msfvenom 可以根据您的要求制作无效零字节的 shellcode,这样您就可以看到它选择了什么策略。