Section outline

  • Shellcode 的主要目的是运行一个 shell 程序,例如 /bin/sh。在 Ubuntu 操作系统中,这可以通过调用 execve() 系统调用来实现。
    execve("/bin/sh", argv[], 0)

    我们需要为该系统调用传递三个参数:在 amd64 架构中,它们通过 rdi, rsi, rdx 寄存器传递。在 arm64 架构中,它们通过 x0, x1, x2 寄存器传递。伪代码如下所示:
    // 针对 amd64 架构
     Let rdi = address of the "/bin/sh" string
     Let rsi = address of the argv[] array
     Let rdx = 0
    
     Let rax = 59    // 59 是 execve 的系统调用号
     syscall         // 调用 execve()
    
    // 针对 arm64 架构
     Let x0  = address of the "/bin/sh" string
     Let x1  = address of the argv[] array
     Let x2  = 0
    
     Let x8 =  221   // 221 是 execve 的系统调用号
     svc 0x1337      // 调用 execve()

    编写 shellcode 的主要挑战是如何获取 "/bin/sh" 字符串的地址以及 argv[] 数组的地址。通常有两种典型的方法:
    • 方法 1:将字符串和数组存储在代码段中,然后使用指向代码段的 PC 寄存器获取它们的地址。本任务中我们关注这种方法。
    • 方法 2:在栈上动态构造字符串和数组,然后使用栈指针寄存器获取它们的地址。我们将在下一任务中关注这种方法。
     
      • 我们提供了一个示例 shellcode (mysh64.s)。以下代码针对 amd64 架构。代码也可以在 Labsetup 文件夹中找到。如果你在 Apple 机器上完成本实验,可以在 arm 子文件夹中找到针对 arm64 的示例代码。

        section .text
          global _start
            _start:
               BITS 64
               jmp short two
            one:
               pop rbx            
               
               mov [rbx+8],  rbx  ; 将 rbx 存储到内存地址 rbx + 8
               mov rax, 0x00      ; rax = 0
               mov [rbx+16], rax  ; 将 rax 存储到内存地址 rbx + 16
               
               mov rdi, rbx       ; rdi = rbx        🅰
               lea rsi, [rbx+8]   ; rsi = rbx +8     🅱  
               mov rdx, 0x00      ; rdx = 0
               mov rax, 59        ; rax = 59
               syscall
            two:
               call one                                                                  
               db '/bin/sh', 0 ; 命令字符串(以零终止) 🅲
               db 'AAAAAAAA'   ; argv[0] 的占位符
               db 'BBBBBBBB'   ; argv[1] 的占位符

        上述代码首先跳转到 two 位置的指令,而该指令又跳转到 one 位置,但这次使用了 call 指令。此指令用于函数调用,即在跳转到目标位置之前,它将下一条指令的地址(即返回地址)保存到栈顶,这样函数返回时可以返回到 call 指令之后的指令。

        在这个例子中,call 指令之后的“指令”实际上不是一条指令,而是存储了一个字符串。call 指令会将其地址(即字符串的地址)压入栈中,作为函数的返回地址。当我们跳转到 one 位置的函数后,栈顶存储的是返回地址。因此,pop rbx 指令实际上获取了位置 🅲 的字符串地址,并将其保存到 rbx 寄存器中。这就是如何获取字符串地址的方法。

        请完成以下任务:

        •  编译并运行代码,验证是否可以获得一个 shell。使用 -g 选项启用调试信息,因为我们将对代码进行调试。
          // 针对 amd64
          $ nasm -g -f elf64 -o mysh64.o mysh64.s
          $ ld --omagic -o mysh64 mysh64.o
          
          // 针对 arm64
          $ as  -g -o mysh64.o mysh64.s
          $ ld --omagic -o mysh64 mysh64.o
          注意,在运行链接器程序 ld 时,我们需要使用 --omagic 选项。默认情况下,代码段是不可写的。当此程序运行时,它需要修改存储在代码区域的数据,如果代码段不可写,程序将崩溃。 这在实际攻击中不是问题,因为实际攻击中,代码通常被注入到可写的数据段(例如栈或堆)中。通常我们不会将 shellcode 作为独立程序运行。
        • 使用 gdb 调试程序,展示程序如何获取 shell 字符串 /bin/sh 的地址
        • 解释程序如何构造 argv[] 数组,并展示哪些代码行设置了 argv[0] 和 argv[1] 的值
        • 解释第 🅰 行和 🅱 行的真正含义。
         
        以下是一些可能对本实验有用的 gdb 命令。如果需要了解其他 gdb 命令,可以在 gdb 内部键入 help 获取命令类别名称列表。键入 help 加上类别名称,可以获取该类别中的命令列表。
        $ gdb mysh64
        
        help          -- 打印帮助信息
        break one     -- 在 "one" 处设置断点
        run           -- 启动被调试程序。
        step          -- 单步执行程序直到到达不同的源代码行。
        print  $rbx   -- 打印 rbx 寄存器的值
        x/40bx <addr> -- 打印内存地址 <addr> 的内容(40 字节)
        x/40bx $rsp   -- 打印栈顶 40 字节的内容
        x/5gx  $rsp   -- 打印栈顶 5 个双字(8 字节)的内容
        quit          -- 退出 gdb

      • Shellcode 广泛用于缓冲区溢出攻击。在许多情况下,漏洞是由字符串复制函数(例如 strcpy())引起的。对于这些字符串复制函数,零被视为字符串的结束。因此,如果 shellcode 中间包含零,字符串复制函数将无法复制零之后的内容,攻击也无法成功。虽然不是所有漏洞都受零的影响,但零会限制 shellcode 的应用范围。

        前一节提供的示例代码并不是真正的 shellcode,因为它包含几个零。请使用 objdump 命令获取 shellcode 的机器代码,并标记所有包含零的指令。

        要消除这些零,你需要重写 mysh64.s,替换所有有问题的指令。指南部分提供了一些消除零的方法。请展示修改后的 mysh64.s 并解释你是如何消除每个零的。
      • 在 mysh64.s 中,我们构造了用于 execve() 系统调用的 argv[] 数组。由于我们的命令是 /bin/sh,没有任何命令行参数,因此 argv 数组只包含两个元素:第一个是指向命令字符串的指针,第二个是零。

        在本任务中,我们需要运行以下命令,也就是说,我们希望使用 execve() 来执行以下命令,该命令使用 /bin/bash 来执行 "echo hello; ls -la" 指令。

        /bin/bash -c "echo hello; ls -la"

        在此新命令中,argv 数组应包含以下四个元素,所有这些都需要在 shellcode 里构造。请修改 mysh64.s 并演示执行结果。与往常一样,您的 shellcode 中不能有任何零。

        argv[0] = "/bin/bash" 字符串的地址
        argv[1] = "-c" 字符串的地址
        argv[2] = "echo hello; ls -la" 命令字符串的地址
        argv[3] = 0
      • execve() 系统调用的第三个参数是一个指向环境变量数组的指针,它允许我们向程序传递环境变量。在我们的示例程序中,我们向 execve() 传递了一个空指针,因此没有环境变量被传递给程序。

        在本任务中,如果我们将 shellcode(mysh64.s)中的命令 "/bin/sh" 替换为 "/usr/bin/env",该命令用于打印出环境变量。您会发现,当我们运行我们的 shellcode 时,没有任何输出,因为我们的进程没有任何环境变量。

        在本任务中,我们将编写一个名为 myenv64.s 的 shellcode。当此程序被执行时,它会执行 "/usr/bin/env" 命令,并打印出以下环境变量:
        $ ./myenv64
        aaa=hello
        bbb=world
        ccc=hello world

        要编写这样的 shellcode,我们需要构造一个环境变量数组,并在调用 execve() 之前,将该数组的地址存储到 rdx 寄存器中。构造此数组的方法与构造 argv[] 数组的方法完全相同。请参见以下内容:
        env[0] = "aaa=hello"        字符串的地址
        env[1] = "bbb=world"        字符串的地址
        env[2] = "ccc=hello world"  字符串的地址
        env[3] = 0                  0 标志数组的结束