SEED-Labs SHELLCODE
什么是Shellcode
Shellcode是一段用于利用软件漏洞的有效负载,通常是16进制的机器码。它被称为“shellcode”,因为它通常会启动一个命令行解释器,从而使攻击者可以控制受攻击计算机。但是,任何执行类似任务的代码片段都可以称为shellcode。
Shellcode可以通过多种方式获取,例如使用编译器生成、手动编写或使用专门的工具生成。Shell-Storm提供了一个shellcode数据库,其中包含各种平台和架构的shellcode。
1. 编写shellcode
查看示例shellcode
1 | section .text |
这段汇编代码是一个Linux下的shellcode,它的作用是启动一个shell。下面是对代码的详细解释:
xor eax, eax:将eax寄存器清零。
push eax:将0压入栈中,作为字符串的结尾标志。
push “//sh”:将字符串”//sh”压入栈中。
push “/bin”:将字符串”/bin”压入栈中。
mov ebx, esp:将esp寄存器的值赋给ebx寄存器,ebx指向字符串”/bin//sh”。
push eax:将0压入栈中,作为argv1的值。
push ebx:将ebx寄存器的值压入栈中,作为argv[0]的值。
mov ecx, esp:将esp寄存器的值赋给ecx寄存器,ecx指向argv[]数组。
xor edx, edx:将edx寄存器清零,表示没有环境变量。
xor eax, eax:将eax寄存器清零。
mov al, 0x0b:将0x0b赋给al寄存器,表示要执行execve()系统调用。
int 0x80:触发系统调用。
这段代码的作用是启动一个shell。它首先在栈上构造了一个字符串数组,然后调用execve()系统调用来执行/bin//sh程序。这个程序是一个shell程序,因此执行完这段代码后就会进入到shell环境中。请注意,这段代码是一个shellcode,它并不是一个完整的程序,也不能直接编译运行。它需要被插入到其他程序中才能发挥作用。
为什么int 0x80是触发系统调用?
在Linux中,系统调用是通过中断来实现的。每个系统调用都有一个对应的系统调用号,这个号码存储在eax寄存器中。当程序执行int 0x80指令时,CPU会切换到内核态,并根据eax寄存器中的值找到对应的系统调用函数并执行。因此,int 0x80指令是触发系统调用的一种方式
为什么将edx清零代表没有环境变量?
在Linux中,系统调用execve()需要三个参数:文件名、命令行参数和环境变量。这些参数都是以指针的形式传递给execve()函数的。其中,第三个参数是一个指向环境变量数组的指针。如果程序不需要使用环境变量,那么这个指针就应该是NULL。在这段汇编代码中,xor edx, edx 的作用就是将edx寄存器清零,表示没有环境变量。
编译shellcode
用nasm编译代码。
We compile the assembly code above (mysh.s) using nasm, which is an assembler and disassembler for the Intel x86 and x64 architectures. The -f elf32 option indicates that we want to compile the code to 2-bit ELF binary format. The Executable and Linkable Format (ELF) is a common standard file format for executable file, object code, shared libraries. For 64-bit assembly code, elf64 should be used.
这个命令是用来将汇编代码文件 mysh.s 编译成目标文件 mysh.o 的。其中,nasm 是一种汇编语言编译器,用于在 Linux 操作系统上编写程序。-f elf32 选项表示生成 32 位的 ELF 格式目标文件。-o mysh.o 选项表示将生成的目标文件保存为 mysh.o。这个目标文件可以被链接器 ld 使用,生成可执行文件。
1 | nasm -f elf32 mysh.s -o mysh.o |
Once we get the object code mysh.o, if we want to generate the executable binary, we can run the linker program ld, which is the last step in compilation. The -m elf i386 option means generating the 32-bit ELF binary. After this step, we get the final executable code mysh. If we run it, we can get a shell. Before and after running mysh, we print out the current shell’s process IDs using echo $$, so we can clearly see that mysh indeed starts a new shell.
接下来,使用 ld 命令将目标文件链接成可执行文件。其中,-m elf_i386 选项表示生成 32 位的 ELF 格式可执行文件。-o mysh 选项表示将生成的可执行文件保存为 mysh。这个可执行文件可以直接在 Linux 系统上运行。
在Linux中,$$ 是一个特殊的变量,它代表当前进程的进程号(PID)。当您在终端中输入 echo $$ 时,Shell会将 $$ 替换为当前进程的PID,并将结果输出到终端。例如,如果当前进程的PID是1234,那么 echo $$ 的输出结果就是1234。
1 | ld -m elf_i386 mysh.o -o mysh |
1 | echo $$ |
25751为当前shell的进程ID。
1 | mysh |
运行一下刚才编译完的shellcode,进程ID变了。
获取机器码
需要的是机器码。可以用objdump进行“反汇编”(disassemble)。
objdump 是一个命令行工具,用于显示 Linux 系统上的目标文件和可执行文件的信息。它可以用来查看二进制文件的汇编代码、符号表、重定位表、段表等信息。objdump 可以帮助程序员了解二进制文件的内部结构,从而更好地理解程序的运行机制。例如,程序员可以使用 objdump 来查看一个可执行文件的汇编代码,以便了解程序的执行流程和算法实现。
objdump 支持多种格式的目标文件和可执行文件,包括 ELF、COFF、PE 和 Mach-O 等格式。它还支持多种架构的处理器,包括 x86、ARM、MIPS 和 PowerPC 等处理器。
There are two different common syntax modes for assembly code, one is the AT&T syntax mode, and the other is Intel syntax mode. By default, objdump uses the AT&T mode. In the following, we use the -Mintel option to produce the assembly code in the Intel mode.
1 | mysh.o: file format elf32-i386 |
可以用xxd工具来获得机器码。
xxd 是一个 Linux 命令行工具,用于将二进制文件转换为十六进制表示,并以可读的形式显示。它还可以将十六进制输出转换为原始的二进制格式,即将任意文件转换为十六进制或二进制形式。xxd 可以为给定的标准输入或者文件做一次十六进制的输出,并且可以将十六进制输出转换为原来的二进制格式。如果没有给定输入文件,标准输入就作为输入文件。如果没有给定输出文件,结果将输出至标准输出。
1 | xxd -p -c 20 mysh.o |
-p参数是连续显示;-c参数是一行显示多少个。这里是一行显示20个。
把机器码放入实验给的convert.py,进行转化。convert.py如下。
1 | ori_sh =""" |
可见,其第一步删除了回车,接下来用循环两个两个插入十六进制数,然后每16个十六进制数字对换个行。
shellcode 删除0
shellcode是不能有0x00的。
为什么shellcode不能有0x00?
Shellcode是一段二进制代码,用于利用漏洞执行恶意代码。当Shellcode中包含0x00时,字符串处理函数(如strlen、strcpy、strcat、sprintf等)会将Shellcode截断,导致Shellcode无法完成其预期的功能。因此,0x00被称为坏字符。为了避免坏字符的出现,可以使用指令优化或Shellcode编码等技术。指令优化是通过选择一些特殊的指令避免在Shellcode中直接生成坏字符。例如,可以使用slti指令(set less than immediate)来设置寄存器a2为0或1。Shellcode编码是通过对Shellcode进行编码来避免坏字符的出现。例如,可以使用base64编码或十六进制编码等技术来对Shellcode进行编码。
文档里写了一些技巧。
If we want to assign zero to eax, we can use “mov eax, 0”, but doing so, we will get a zero in the machine code. A typical way to solve this problem is to use “xor eax, eax”.
异或寄存器本身,经典的寄存器归零方法。
If we want to store 0x00000099 to eax. We cannot just use mov eax, 0x99, because the second operand is actually 0x00000099, which contains three zeros. To solve this problem, we can first set eax to zero, and then assign a one-byte number 0x99 to the al register, which is the least significant 8 bits of the eax register.
需要赋值高位0的数字的时候,可以直接放低位寄存器。
Another way is to use shift. In the following code, first 0x237A7978 is assigned to ebx. The ASCII values for x, y, z, and # are 0x78, 0x79, 0x7a, 0x23, respectively. Because most Intel CPUs use the small-Endian byte order, the least significant byte is the one stored at the lower address (i.e., the character x), so the number presented by xyz# is actually 0x237A7978. You can see this when you dissemble the code using objdump.
还能通过移位进行赋值。注意:大多数Intel的CPU用小端顺序。
Another way is to use shift. In the following code, first 0x237A7978 is assigned to ebx. The ASCII values for x, y, z, and # are 0x78, 0x79, 0x7a, 0x23, respectively. Because most Intel CPUs use the small-Endian byte order, the least significant byte is the one stored at the lower address (i.e., the character x), so the number presented by xyz# is actually 0x237A7978. You can see this when you dissemble the code using objdump.
After assigning the number to ebx, we shift this register to the left for 8 bits, so the most significant byte 0x23 will be pushed out and discarded. We then shift the register to the right for 8 bits, o the most significant byte will be filled with 0x00. After that, ebx will contain 0x007A7978, which is equivalent to “xyzn0”, i.e., the last byte of this string becomes zero.
1 | mov ebx, "xyz#" |
shellcode 删除0
采用左右移的方式唤起/bin/bash。
1 | section .text |
首先,将“hxxx”压入ebx,然后再左移24个,就是000h;然后再右移24个,就是h000。然后压入栈。然后压入/bas,再压入/bin,就是/bin/bash。
shellcode 压入变量
1 | section .text |
先压入“/bin/bash”,再压入-c。这里也是用左移右移做的;然后压入la,这里也是用左移右移做的;然后直接压入”ls -“,最后就是/bin/bash,参数-c以及ls -la。
shellcode 压入环境变量
根据这个博客,execve()系统调用的第三个参数是一个指向环境变量数组的指针,它允许我们将环境变量传递给程序。在我们的示例程序中(xor edx,edx),我们向execve()传递了一个null指针,因此没有向程序传递环境变量。
其提供的代码是通过写入/usr/bin/env文件做到的。
1 |
|
原理与之前一致。
2. 运用代码段
在Task1中,解决获取数据地址问题的方式是每次构造完数据结构后,动态获取当前的栈顶地址,这样就能获取目标数据的地址。
还有另一种方法可以解决同样的问题,即获取所有必要数据结构的地址。在这种方法中,数据存储在代码区中,其地址通过函数调用机制获取。让我们看看下面的代码。
1 | section .text |
section .text定义代码段
global _start定义程序入口点
_start:程序入口点
BITS 32指定程序为32位
在程序运行时,首先跳转到two。然后,two会call one。此时,运用了call指令。它在跳到目的地址前,会将下一条指令地址保存为返回地址。故,当函数return时,会直接返回到call指令后的一条指令。
在这个记录中,call后面是一个字符串,而不是一条指令。call会将这个字符串的地址压栈。(压到返回地址的栈地址里头)当“进入函数”时,或者说,call 地址one的时候,栈顶就是储存的返回地址。所以,pop ebx指令就是获取db ‘/bin/sh*AAAABBBB’这个字符串的地址,然后存到ebx里头。这样就获取了这个字符串的地址。
然后需要构造这个字符串。首先,xor eax, eax挪出我们需要的0。mov [ebx+7], al这句话,替换了字符串里的,把变为了0。然后,把命令”/bin/sh00”的地址给了占位符AAAA。然后,mov [ebx+12], eax将ebx+12,即把BBBB赋值为0。lea ecx, [ebx+8],即为把命令”/bin/sh00”的地址给ecx。然后,xor edx, edx置空edx,不设环境变量,最后mov al, 0x0b调用execve,int 0x80调用中断。
改造它,让它可以执行/usr/bin/env a=11 b=12
1 | section .text |
原理如上。首先,
1 | mov [ebx+12], al |
之后,字符串变为’/usr/bin/env0BBBBCCCC0a=110b=220AAAA’
然后,
1 | mov [ebx+17], eax |
之后,字符串变为’/usr/bin/env0000000000a=110b=2200000’
然后,设置命令。
1 | mov [ebx+13], ebx |
之后,ebx+13的位置变为’/usr/bin/env’。
然后,设置环境变量(edx)
1 | lea edx, [ebx+32] ; env[2] |
这就是压三个环境变量。分别压了a=11,b=22和0。
然后,设置参数。
1 | xor ecx, ecx |
首先,execve的argv[1]设为0。然后,argv[0]设为’/usr/env/bin’。这句是通过把ebx+13的指向的内容地址(/usr/env/bin)传给ecx,然后把ecx压入栈实现的。
最后调用execve和中断。
1 | xor eax, eax |
3. 64位shellcode
1 | section .text |
左右移去0
1 | mov rax,'h*******' |
编译运行即可
1 | nasm -f elf64 mysh_64.s -o mysh_64.o |
在x86-64 CPU上的Linux操作系统中,要使用系统调用,需要使用汇编语言代码。在64位x86_64 Linux系统中,可用的系统调用定义在 /usr/include/asm/unistd_64.h 头文件中。每个系统调用都对应一个编号以及若干个参数。如果想使用汇编语言调用系统调用,那么在调用之前,需要将系统调用编号存到 %rax ,将参数依次存到 %rdi, %rsi, %rdx, %r10, %r8, %r9 中,然后再执行 syscall 指令即可。
int 0x80 是一种旧的系统调用方式,它是通过中断/异常实现的。在执行 int 0x80 指令时,会发生 trap。硬件找到在中断描述符表中的表项,在自动切换到内核栈 (tss.ss0 : tss.esp0) 后根据中断描述符的 segment selector 在 GDT 中找到相应的段描述符,并将其加载到 CS 寄存器中。然后执行相应的中断处理程序。int 0x80 的缺点是它需要进行两次模式切换:从用户态切换到内核态,再从内核态切换回用户态。这种模式切换会带来一定的开销。
syscall 指令是一种新的系统调用方式,它是通过陷阱门实现的。syscall 指令会将当前进程从用户态切换到内核态,并跳转到内核中指定的地址处执行相应的系统调用处理程序。与 int 0x80 相比,syscall 指令只需要进行一次模式切换,因此效率更高。