奇奇怪怪的技巧
前言:
- 主播最近打了一些比赛,遇到了不少奇奇怪怪的手法,不过好在几乎都有惊无险地打过去了
- 主播还是没能学会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 * |
AE64使用细节
原理:
原型:
AE64().encode(shellcode,strategy,offset,rigester)
AE64是由杭电的师傅开发的一款纯字母shellcode生成器,可以将目标shellcode转化为纯字符
AE64提供两种策略(2参):
- small策略:生成尽可能短的shellcode,代价为运行速度较慢
- fast策略:生成运行速度快的shellcode,代价为长度很长
register代表存放shellcode地址(附近)的寄存器,offset代表第一句shellcode与寄存器的偏移
small策略下大概的结构为:先写入一段包含异或等运算的循环和一段无意义字符串,这段循环会将无意义字符串逐比特转化为我们的目标shellcode,所以即使我们的目标shellcode很短,转化出来的shellcode也会比较长,但是如果目标shellcode较长,因为*”转化器”(循环)*的存在通常也不会特别长
田师傅给我们整了个非常奇怪的沙箱
- 允许调用close(),open,read,write
- read的文件描述符必须是0
- 每次read和write都只能读入写出一个字符
对此我们采用的策略为:close(0)以对抗read.fd=0的限制,使用loop来循环读入flag,写出flag
在进入shellcode时,栈上存在shellcode_base地址:
先将汇编写好,然后反汇编,再使用AE64方法:
1 | sc=asm(""" |
全新的两个系统调用:shmget&shmat
当我们写shellcode时因为PIE/ALSR
,找不到放置”/bin/sh”,”./flag”等等重要字符串时我们可以使用这两个系统调用
shmget
- 原型–>
int shmid = shmget(key, size, shmflg);
- syscall=29
- 作用–>向内核申请了一块size大小的共享内存区域,并通过shmflg为其设置权限
- shmflg=IPC_CREAT | rwx
如果我希望申请一块rw的区域,shmflg=IPC_CREAT|0x666=0x1000 | 0666(八进制)=0x3B6
shmat
- 原型–>shmat(shmid, shmaddr, shmflg)
- syscall=30
- 作用–>将shmget申请的空间映射到虚拟内存中,并返回这片空间的地址
- shmid应该设置为shmget的返回值
- shmaddr为想让内核映射到进程地址空间的首选地址,如填NULL则自动选择地址(和mmap有点像)
- shmflg设置为0则不做地址对齐,默认以读写方式连接
例题:NPCCTF
允许我们执行任意系统调用,还会回显每个系统调用的返回值,但是开启了PIE和沙箱
沙箱kill了execve系列
我们继续orw首先要寻找一块可读可写的内存,此时就可以使用shmget&shmat
进行申请&映射
然后就是正常orw
1 | from pwn import * |