奇奇怪怪的技巧
前言:
- 主播最近打了一些比赛,遇到了不少奇奇怪怪的手法,不过好在几乎都有惊无险地打过去了
- 主播还是没能学会ret2dlresolve,不知道ret2dlresolve的博客什么时候才能发出来 :(
- 部分题目因为供题人的要求不能发出附件 :( 不好意思
1.execv调用
此处可以布置一个ROP,没有沙箱可以直接获取shell
我在之前进行了多次栈迁移(迁移到bss上),导致无法调用system(“/bin/sh”)
1 | system("/bin/sh")会使用大量push操作(俗称抬栈),如果我们的"栈"处于一个比较小的可写区域,会导致抬栈失败,system调用失败 |
此时我想到可以使用ret2syscall,此时四个寄存器的初始值(rax,rdi,rsi,rdx)都不符合预期,我们需要同时控制这四个寄存器,且要syscall需要8x9=0x48字节的空间,但是我们只有0x40的read :(
此时我误打误撞学会了一个技巧:execve这个函数可以将 pop rax,ret;syscall 这步操作合成为 call execve
这样我们的空间就充足,顺利调用execve
2.侧信道的使用🤔
当沙箱ban了write类调用时,我们除了可以使用万能的io_uring,我们还有一个利器:测信道爆破flag!
2.1 shellcode写法
原理:
- 我们使用open+read将flag读入内存后,可以使用cmp这个汇编指令将可见字符串逐个与flag的每一位比较
- 当尝试到正确的char时,cmp会返回0,配合汇编指令jz $-4可以将执行流跳回cmp指令,此时程序会陷入死循环:cmp->jz->cmp->jz
- 我们可以在jz指令的后面放置一个被沙箱ban掉的系统调用
- 匹配失败就会触发该系统调用导致进程被kill,匹配成功就会陷入死循环
核心指令:
1 | #匹配模块,使用格式化字符挪动每次需要匹配的字符 |
1 | #时间(测信道)管理模块,指定每次可接受管道信息的时间限制为2s,如果超过2s说明陷入死循环,匹配成功 |
这种打法的缺点是时间复杂度为O(n),如果使用二分法匹配字符则时间复杂度会降低到O(logn)
以后一定把二分法的shellcode写出来
2.2 rop的写法:
原理:
- shellcode写法中的cmp指令被我们替换为strncmp函数,逐位检测flag
- 这个手法巧妙在如果利用strcmp的返回值:当匹配成功时会返回0,失败时非0,此时syscall,因为read位于白名单中,不会被沙箱抛出,所以会卡住
- 最后也用运行时间侧写是否匹配
1 | #比较猜测的flag和真实flag,虽然仅仅是这道题比较特殊,每次运行时读入的flag长度和猜测的flag长度一样 |
3.gdb使用&sendfile
got表可写,没开pie,很明显想让我们ret2dlresolve,但是既然csu和magic(详见上一篇文章magic_gadget)都有,那就有逃课打法了 :)
3.1 gdb调试:
每次使用magic_gadget前我都习惯性地先用gdb看一眼,以便放心使用
啊?怎么ret指令没了
- 原因很简单,并不是magic_gadget被故意摘除了,而是我们使用telescope查看汇编存在问题
- 很明显汇编指令不可能按八字节对齐,但是telescope是以八字节为对齐查看汇编的,这就会导致汇编出现错位,显示出一堆奇奇怪怪的指令 😵
- 正确使用方法为:
pwngdb> x/5i address
x/5i是指显示从address开始的五条指令,这样我们就能看到add,nop,ret回来了 :)
3.2 sendfile:
很巧妙的一个 系统调用 ,可以实现read+write的结合
- sendfile的系统调用号为187,在沙箱中有可能被漏掉🤔
- 原形为:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
- 一参为目标文件描述符(一般设置为1,标准输出),二参为源文件描述符(一般为3,flag文件),三参为0(从flag文件的头开始读取),四参为读取的长度,因为sendfile会根据文件的结尾标志 EOF 完成读取,所以如果rcx是个很大的数也不用担心 :)
4.随机数表
1 | fd=open("/dev/urandom") |
此时我们无法预测随机数种子:urandom用于生成随机数,但它不依赖系统中断,而是使用伪随机数生成算法来生成随机数。它会利用熵池中的数据来初始化一个内部状态,然后根据这个状态生成随机数序列
但是rand函数拥有一个性质:当种子一定时,随机生成的数字会遵守一个特定的算法
这个算法有一定几率为o[n] = o[n - 31] + o[n - 3]
- 当可以得到足够多随机数时(31个),我们可以尝试用这个算法预测下一个随机数
- 成功的概率大概50%,可以采用多次爆破
- 不断对随机数表进行修正
1 | num[33 + i - 31] + num[33 + i -3])%0x80000000 //0x8000000000用于防止溢出 |
5.c++异常处理机制
题目链接: nss
这里有个整形溢出,但是由于前面有三个相似的函数(都没有整形溢出),主播没看到这个导致做了一个下午做红温了
题目可以得到pie,elf中存在足够的gadget和open,read,write函数
关键的难点在于栈溢出绕过canary:
- cpp异常处理大致为throw_catch结构
- 当进入异常处理时,会进行栈退回(call _Unwind_RaiseException@plt)
- 栈退回会恢复rbp后返回,重点是这个过程会绕过canary的检查,直接返回
- 如果想栈迁移也可以构造特定的rbp
于是我们的payload结构可以构造为:
padding | fake_rbp | ret(vuln函数的调用入口) | padding | 覆盖调用vuln函数的返回地址为ROP
按照这个格式注入payload后,触发cpp的异常处理,便可以绕过canary的检查
1 | from pwn import * |
2.39下的house of apple2(orw)
自从secontext中设置rsp的寄存器从rdi变成了rdx,利用难度就上了一个level /(QwQ)/~~
- 参考文章: 基于house-of-apple2的GLIBC2.39利用
- 可使用例题: xyctf_2025_heap2
可使用的magic_gadget与函数:
1 | svcudp_reply |
其中svcudp_reply不是导出符号,可以通过ROPgadget的opcode在libc中定位
ROPgadget --binary ./libc.so --opcode 4c8b6748
swap
- 可以通过rdi控制rax,r12
- rax可以控制执行流到swapcontext,r12控制栈地址(rsp),rcx控制执行流到”栈”(迁移后的位置)上的ROP
- 其实
fldenv bytes ptr [rcx]
要求rcx不能为null,如果刚刚好与ROP中的p64(0)撞到的话,在ROP中垫几个ret滑块指令就能绕过 - 后半段swapcontext会push rcx….ret,所以我们要第二次在rcx里设置好对应的ROP地址使得跳转成功
exp.py:
1 | from pwn import * |