level0
比较简单,直接溢出,覆盖返回地址为callsystem的地址就可以了。坑点,Ubuntu22需要再p64(callsystem_addr + 1),需要在后面加上1或2或4才能成功获取shell。


找到漏洞点。buf只有128bit,0x200会导致溢出。

显然,偏移的地址为0x80

因为是64位,0x80的偏移,poc如下:
1 | from pwn import * |

为什么要在callsys_addr那里+1?其实还可以+2、+4。
根本原因是在Ubuntu 18.04及更高版本(即glibc版本较高)的64位系统的GLIBC的栈对齐导致的。
x86-64 System V ABI(应用程序二进制接口)中的栈对齐(Stack Alignment)检查,具体表现为MOVAPS指令引发的Crash。
在64位Linux系统重,ABI规定:在调用call指令进入一个函数之前,栈顶指针RSP必须是16字节对齐的(即RSP的地址以0x0结尾)。
下面再详细的解释一下:
在Ubuntu22.04(以及其他高版本Linux)中,我的实验环境就是Ubuntu22,底层的C标准库(GLIBC)对system()函数的实现有一个严格的要求:
当程序执行到 system() 函数内部的某些 SSE 指令(如 movaps)时,栈顶指针(RSP)必须是 16 字节对齐的(即地址必须以 0 结尾,不能以 8 结尾)。
为什么会遇到Crash呢?直接崩掉呢?
下面就是做一个简单的数学加减法(以8字节为一个单位):
正常调用:
main调用call vulnerable_function:call指令压入返回地址,RSP - 8。- 进入函数执行
push rbp:RSP 再 - 8。 - 合计:-16(偶数),栈是对齐的,大家都开心。
你的 EXP 攻击:
- 你覆盖了返回地址,当
vulnerable_function结束时,执行ret。 ret指令相当于pop rip,RSP + 8。此时,RSP 回到了对齐状态(以 0 结尾)。- 问题来了:你直接跳转到了
callsystem的入口地址400596。 - 入口的第一条指令是
push rbp。执行它,RSP - 8。 - 此时 RSP 以 8 结尾(不对齐)。
- 带着这个“歪”的栈进入
system()-> 触发movaps异常 -> 程序崩溃 (SEGFAULT)。
解决的核心逻辑: 既然多执行一次 push 会导致不对齐,那我们跳过这个 push,RSP 就不会减 8,栈就保持了对齐状态,system() 就能跑通了。
为什么callsys_addr那里可以+1,+2,+4?我以实际的汇编代码给你解释一下。
执行命令objdump -d level0 | grep -A 5 callsystem,得到下面的结果:
1 | 0000000000400596 <callsystem>: |
1. 为什么 +0 (地址 400596) 不行?
- 指令:
55(push %rbp) - 分析:这是函数头。执行它会把 RBP 压栈,导致 RSP 减少 8 字节。
- 结果:栈不对齐 ->
system()崩溃。
2. 为什么 +1 (地址 400597) 可以?【推荐】
- 指令:
48 89 e5(mov %rsp, %rbp) - 原理:
- 你跳过了
55(push指令)。 - CPU 从
48开始读取,这正好是mov指令的完整头部。 mov指令只修改数据,不修改 RSP(栈指针)。
- 你跳过了
- 结果:RSP 保持原样(对齐状态)->
system()成功执行。这是最稳健的跳过 Prologue 的方法。
3. 为什么 +2 (地址 400598) 也可以?【纯属巧合】
这是一个很有趣的现象,叫做指令错位(Misaligned Instruction)。
- 原始机器码:
48 89 e5 - 你的跳转:跳过了
48,CPU 从89开始解释指令。 - CPU 看到的指令:
- 在 x86 架构中,
89 e5被翻译为mov %esp, %ebp(32位的寄存器传输)。
- 在 x86 架构中,
- 分析:
- 这是一条合法的汇编指令(运气很好,没有报非法指令错误)。
- 这条指令也不修改 RSP。
- 结果:RSP 没变 -> 栈对齐 ->
system()成功。- 注:虽然能通,但这是依赖机器码的巧合,不建议作为通用方法。
4. 为什么 +4 (地址 40059a) 也可以?【最干净】
- 指令:
bf 84 06 40 00(mov $0x400684, %edi) - 原理:
- 你跳过了
push和mov整个栈帧建立的过程(跳过了callsystem的废话部分)。 - 直接开始做正事:给
system函数传参(把字符串地址给 EDI)。
- 你跳过了
- 分析:这行代码显然不修改 RSP。
- 结果:RSP 保持对齐 -> 传参成功 ->
system()成功。
总结
- 根本原因:Ubuntu 22 的 GLIBC 中
system函数要求栈 16 字节对齐。 - 为何 +1/+4 有效:本质都是为了跳过
push rbp这一行代码,从而避免 RSP 发生 8 字节的偏移,维持栈的对齐状态。 - **学习到:**如果远程或高版本 Ubuntu 打不通,优先尝试 地址+1 或者在 Payload 里加一个
ret的地址(Gadget)来调整栈平衡。
level1
比较简单,32位,直接写入shellcode。栈可执行,在栈中,直接写入shellcode。

定位漏洞函数。

断点漏洞函数,发现偏移量0x88。

直接在栈中写入shellcode,并调用这个shellcode。
level1输出了buf的地址,再向buf中写入shellcode。利用栈溢出,覆盖返回地址,返回地址覆盖为buf的地址,也就是shellcode的地址,拿到shell。
poc如下:
1 | from pwn import * |
level2
这题之前做过[1],相同的思路。.got.plt中有system函数的地址,然后再找/bin/sh的地址,即可获得shell。
查看防护情况。

定位漏洞点,buf只有136字节,却可以输入0x100个字节。

查看偏移,0x88个字节的偏移。

程序是32位的,偏移0x88,136个字节,要再覆盖一个ebp指令,所以要覆盖136+4 = 140字节的垃圾数据,然后再填充system函数的返回地址。又因为system函数的汇编代码的第一个是push ebp,system函数要间隔两个字去找参数值/bin/sh,push ebp算一个字了,又因为32位,再填充4个字节,又算一个字,然后再去填充`/bin/sh字符串的地址。
所以完整的payload为:
1 | payload = flat([b'A' *(136 + 4),systemaddr, b'B' * 4, binshaddr]) |
完整的poc为:
1 | from pwn import * |
注意systemaddr、binshaddr地址要为整数。
level3
和上一题类似,这题没有system函数了。
防护检查。

漏洞点,这里无system函数了。
