再过一段时间就是西湖论剑了,复现一下去年的题目看看能不能学到知识。
Vpwn:


2.35 版本的 glibc,开启了 PIE、Canary 和 Full RELEO,看 IDA 发现是 C++类型的菜单题。
1 | case 1: |
菜单的主要部分就是这些了,然后去一个个分析其作用:
case1:
Edit an element in the vector\n

最开始让输入 v6(value) 和 v7(idx),然后把 v6 赋值给 v3 ,调用*(_DWORD *)sub_185C(v8, v7) = v3;
v8 是一个数组,在 main 函数上定义过:_BYTE v8[40]; // [rsp+30h] [rbp-40h] BYREF,初始化为 0。
std::out_of_range::out_of_range 是 C++ 标准库中的异常类构造函数,代表数组或容器访问越界,因此可以确定 if 当中的内容我们不用多看,感觉主要是负责报错的。
最后返回 4 * a2 + a1 = v3,因此猜测 a2 实际上是 len,加上初始 idx 可以变成新的 idx v3
其中依据 a2 >= *(_QWORD *)(a1 + 0x18) 这个判断条件我们或许可以逆向出来一个结构体:
1 | struct a1{ |
case2:
Push a new element\n

一开始会让我们输入 value
1 | __int64 __fastcall sub_18F4(__int64 a1, int *a2) |
这里我们可以 push 一个值进入某 v8[a1 + 4 * result],对应数组长度 len 会+1:result + 1,这里就存在一个栈溢出漏洞,倘若我们一直进行 push 操作,对于每次四字节的填充,0x18 只需要 6 次即可填满,第七次就会溢出到 len 当中:
1 | struct a1{ |
case3:
Pop the last element\n

1 | --*(_QWORD *)(a1 + 24); |
这里就存在防止数组越界的内容,主要是最后两行:每次 pop 会对数组下标 -1,不存在越界
case4:
Print vector\n

很明显了,*(_QWORD *)(a1 + 24)代表 len,也就是打印所有 array[result] 的内容,假设我们通过 push 当中的溢出把 len 修改的很大,那就可以轻易造成越界读。
EXP 思路:
越界写篡改 len:
1 | from pwn import* |
最开始依旧是越界修改 len,我们先接收看一看是什么内容,发现全是数字

泄露 libc_base:
1 | printf() |
和之前做的 l3akctf2026 有一点像,都是 4 个字节的数据要拆分拼凑成 8 个字节的数据然后转化十六进制,因此我们直接将其拼凑起来,然后读取下表为偶数的值即可:

因此可以看到 array[18] 泄露了 libc 附近的地址,因此:
1 | libc_base = (array[18] | array[19] << 32) - 0x29d90 |

写入 system(‘/bin.sh’);
由于我们修改了 len,且 edit 的内容在栈上,我们可以在栈上布置 rop 链执行 system('/bin.sh'),最终通过 case5 进行 exit 触发栈上 rop 链:
1 | libc.address = libc_base |
这里由于我们每次只能写四个字节的内容,因此一个地址要分两次 edit,并且地址由低到高。
总 EXP:
1 | from pwn import * |
注意事项:
在一开始的时候,edit 函数并没有添加对于 value 的判断
1 | def edit(idx, value): |
不过我们在最后传参的时候是拆成两个 32 为数据传的,数据是 value,属于 int 类型
例如 ret 地址是:0x7f27fec29139拆分开就是 high32:0x00007f27;low32:0xfec29139
1 | edit(18, ret & 0xffffffff) # 0xfec29139 |
对于 uint 类型的低 32 位数据会超出 int 类型的 0x7fffffff的最大值从而造成报错
value -= 0x100000000 会将显示给程序读的十进制数变成负数,方便正常解析,但写进内存的 4 字节不变。
依旧以 ret 为例:ret_low32 = 0xfec29139, 0xfec29139 - 0x100000000 = -20803271
由于因为有符号整数用补码存储。 因此发送-20803271实际就是写入0xfec29139,并不会影响后续地址。
Heaven’s door


执行 fork,父进程可以进入到 if 中利用 mmap 在 0x10000 的地址开辟 0x1000 大小的空间,权限是 7 代表可读可写可执行,遇到 mmap 我们通常想到的方法是往里面编 shellcode。子进程就是打印一些字符串:

之后我们想要看一下沙箱内部调用:

发现并不能完整输出沙箱内容,这道题让我想起来了 nepnep2026 的一道题,查看沙箱的时候本身就是运行程序,由于某些条件未满足导致无法查看沙箱:
1 | mmap((void *)0x10000, 0x1000u, 7, 50, -1, 0); |
如果我们只是单纯运行程序,在 read 的时候没有接收到字符串就会卡住,因此我们要输入一些内容:
1 | printf '\xc3' | seccomp-tools dump ./heaven |

这里 printf 会把内容输出到 stdout,通过管道符 “|”会将内容送入管道符右侧的 stdin,然后进行标准输入执行 read,成功后就可以运行到沙箱。
我们并不清楚 MEMORY[0x10000] 是什么,只好去看一看汇编:

本身就是 call rdx,也就是 call 0x10000 调用 mmap 内容,mmap 内部会被我们写入 shellcode
if ( (int)count_syscall_instructions(0x10000, 4096) > 2 )这里要求 shellcode 内容不能调用两次以上的 syscall,大概就是这些要点了。
EXP 思路:
就是直接往 mmap 写 shellcode 拿 flag 就可以了,没什么特别的东西,少用点 syscall,使用 sendfile:
1 | from pwn import * |
1 |
|
简单解释一下吧,长时间不写栈也有点忘了。
- 执行完 open 操作会把 fd 存入 rax,sendfile 第一个参数是 1 代表 stdout,e 是 stderr
- 那就是打开 ./flag,然后进行标准输出,这道题目就出来了。

但是自己搓一个 shellcode 也可以:
1 | payload = asm( |
说些什么吧!