ORW
引入
在现今的CTF赛事中,越来越多的题目启动了沙箱,往往是禁用了execve函数,使我们没办法直接通过system(/bin/sh\x00)来getshell,这个时候就到了ORW大显身手的时刻。
ORW分别是open,read,write的首字母缩写,也正是要利用这三个函数来读出flag文件
原理
在禁用execve的情况下,我们需要经过以下操作来得到flag值
open开flag文件
read出flag的内容
write显示flag的值
攻击方式
在知晓大概的流程之后,就得设置寄存器的参数了,我们得知道各个函数对应的参数分别代表什么意思
open(file,oflag),read(fd,buf,n_bytes)和write(fd,buf,n_bytes)
open
file就是我们要读取的文件名,CTF中一般为flag,或者flag.txt。
而oflag则是我们以何种方式打开文件,如只读,只写,可读可写。一般来说我们都设置oflag=0,以默认方式打开文件,一般来说都是只读,我们并不需要对flag进行其它操作,所以只读的权限就够了
read和write
这两个是大同小异的。fd是文件描述符,通过设置它来决定函数的操作。在大多数时候,我们常常设置read的fd为0,代表标准输入,但在ORW中,我们需要设置read的fd为3,表示从文件中读取,buf就是我们读取出的flag值存放的地址,n_bytes就是能输入多少字节的数据。write的fd还是如常,依旧为1.
效果
源码如图

Linux下的执行效果




当然,我们构建源码是非常简单的,但是题目里就不一定了,根据题目的不同ORW里也有一些变种
栈上的ORW
ROP链的ORW
这是我们最常见的ORW了,通过ROPgadget在ELF文件、libc.so.6中寻找我们的gadget。在这种orw中,我们需要用的寄存器有rax,rdi,rdx,rsi。rax的作用不必多说,系统调用号。而rdi在这open中存储file的地址,在read和write中储存fd;rdx储存的是输入\输出的字节数大小;rsi在open中储存的是oflag,在read和write中储存的是buf
open(rdi-->file_addr,rsi-->oflag)
read/write(rdi-->fd,rsi-->buf,rdx-->s_nbytes)
嫌这样记太麻烦的话,就只需要记住它的参数传递符合x64的函数调用约定
例题 [HGAME 2023 week1]orw
根据题目一眼看出就是ORW,我们先看看函数




检验过后确实是ORW,禁用了execve,read函数里只够0x28个字节,明显是不够的,需要迁移。
流程
第一次我们泄露libc,用libc_base来求取open,read,write,为第二次的read的buf迁移做准备。
这道题的核心就是第二次的buf迁移,因为要覆盖到ret地址需要0x108个字节,我们能利用的只有0x28个字节,因此我们需要迁移到bss区。
这里的关键汇编指令是 lea rax, [rbp+buf],意为取[rbp+buf]的地址存储到rax中,而rax在下面又会赋值给rsi,相当于完成了迁移,就有足够的字节构造ORW了。

1 | payload_migration = b'a'*0x100 + p64(elf.bss() + 0x300 + 0x100) + p64(lea_rax) #0x100个a覆盖到rbp,将rbp覆盖为bss + 0x400处,至于为什么要0x400分开成0x300+0x100,是为了更好理解,因为buf相对rbp的距离是0x100字节,加0x100是为了抵消这部分偏移,便于后面计算。lea_rax就是ret到它所处的地址 |
这是执行过该payload后的效果

第三步就是纯粹的ORW链构造了,我直接放在完整payload里并解释好了
Payload
1 | from pwn import * |
shellcraft模块的ORW
众所周知,pwntools库里给我们提供了许多模块供我们进行脚本攻击,其中shellcraft也是我们常用的模块,常常被我们用于ret2shellcode攻击,但它也可以被用来构造ORW链,常见形式如下
shellcraft.open('/flag')
shellcraft.read(3,buf,n_bytes)
shellcraft.write(1,buf,n_bytes)
相比于ROP链的攻击,它更为简单,因为我们不需要再寻找寄存器了。不过它的条件也很苛刻,必须是可读可写可执行的区域,所以它的泛用性并不是那么广
例题 第二届贺春杯 shellcode
一如既往看看程序运行逻辑



嗯,半shellcode半ORW,题目映射了一段可读可写可执行的地址,那么就很简单了,先往addr里塞用shellcraft编写的ORW链,然后从buf跳到addr即可
Payload
1 | from pwn import * |
禁用ORW的部分函数写ORW
sendfile
普通的ORW太简单,一般都会给你ban掉一部分跟ORW有关的函数,open被ban了可以用openat代替,write没了可以用puts代替,read可以用readv等代替。这时候就是考验攻击者对函数的熟练度了,有时候还需要汇编功底
例题 XYCTF orw
看着是一道很简单的ORW题,但是它的水很深,一不留神就掉坑里了



把ORW函数几乎给你ban了个遍,open还好说,但这read和write都ban的不成样了
坑
想找两个函数来代替这俩,然后用ROP给执行了,但其实这也是这题的一个坑,因为…

得了,咱寄存器别找了,手搓汇编吧,但要找函数代替品也不简单…
来看看它的sendfile代码
1 | ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); |
sendfile是一个用于在文件描述符之间高效传输数据的系统调用,它在两个文件描述符之间传输数据而不需要在用户空间进行数据缓冲,从而提高性能
out_fd表示的是目标文件描述符。数据将被写入到这个文件描述符,一般来是stdout(做堆的师傅们应该常见这玩意),用于输出到屏幕,可以粗略理解为write的fd
in_fd是源文件描述符,数据将从这个文件描述符读取,可以粗略理解为read的fd为3的情况
offset不必多言,就是从文件内容offset字节处开始读取
count也不必多言,n_bytes
了解完了这些就可以手搓汇编了
Payload
1 | from pwn import * |
效果如下



禁用open和openat
例题 2024强网杯初赛 baby_heap
因为没保存草稿又懒得再重新写一遍,我就直接发链接,也是我写的
堆上的ORW
不仅栈上有ORW,堆上当然也有ORW,不过堆上的ORW调用情况比栈上的更麻烦,要考虑的变量多了
纯ROP链的ORW
这个“纯”的意思是没有配合其它函数,而是通过单纯控制堆块来在程序返回地址处来ORW。
原理
这种ORW通常都是通过environ函数(环境变量)泄露出当前函数的返回地址,通过修改bins中堆的fd指针为ret,申请到该地址后,在后面布置ORW的ROP链来获得flag
例题 [CISCN 2022 华东北]bigduck
来看看程序是什么样的
经典menu题(所有的函数名和变量名我自己已经改过了)

仅申请堆而没有操作

经典UAF漏洞

普通show

edit函数可改变大小并输入内容

有计时,开了沙箱,要ORW


流程
通过程序可以看出,这是一道经典的UAF的menu题,虽然这题libc版本较高,为2.33,但hook函数还没被扬,但是很难用setcontext+orw,因为2.29及之后setcontext有些改变,使这样的ORW比较难以利用,gadget不好找,所以我们只能采用这种ORW。利用edit和show函数,在申请进入environ函数体内后,就可以通过show计算出edit_ret,从而布置ROP链,这就是大概思路了
leak_libc和heap_base
先放这一阶段的Payload
1 | def add(): |
给几个关键步骤的截图,方便更好理解
show(0)时泄露heap_base,如果不移位,会造成heap_base泄露错误
记住这个environ和stack_ptr


在经过edit之后,看它的fd指针和实际的内容

这就是它对堆内容进行了移位和异或的操作,实习内容和fd指针指向的位置是不一样的,是高版本的保护机制
ORW
泄露出栈地址就好说了,先把两个堆块给free出来,用来继续伪造chunk。往edit_ret里编辑ORW即可
1 | pop_rdi = libc_base + 0x28a55 |
最终效果如下


setcontext+ORW
这个攻击方式主要利用setcontext函数,通过rdi寄存器控制其它寄存器,主要是利用mov rsp, qword ptr [rdi + 0xa0],实现栈的迁移
setcontext

我框红的就是关键的地方,要提前设置好[rdi+0xa0]的内容,使我们的rsp迁过去,要在迁过去之前设置好ORW链。
例题 [CISCN 2021 初赛]silverwolf
一如既往看程序
嗯,还是menu

add函数限制了我们的size大小,只能是tcache,并且不允许设置index

UAF,不能选择index

show,只show最近创造的chunk的内容

平平无奇edit,但是也只能edit最新创造的chunk

流程
这道题限制了我们的寻址,使我们只能操作最近申请的堆块,看似走投无路,其实还有一条小路可走,那就是劫持tcache_perthread_struct
tcache_perthread_struct
它是在libc-2.27之后加入的,用于管理tcache_bins,大小一般为0x250或0x290
源码
1 |
|
我们可以看到,该结构体有两个数组,一个是counts,另一个是entry
counts的数组记录的是tcache上各个bin上的堆个数,总大小为64字节
entry储存的是各个指针,存储的是各个bin链表上的首chunk的fd指针
而entry就储存在counts之下
entry数组和counts数组都是我们的攻击目标,通过修改counts和entry,我们就能操控tcache_bins
劫持tcache_perthread_struct
1 | def add(size): |
因为只能操作最近申请的chunk,所以我们若是想在堆块里布置ORW链,我们就需要能一次性对多个堆块进行操作,而tcache_perthread_struct恰好能满足我们想要的这个条件,所以首先就是要劫持它。
泄露libc
这里提供两种方案,一种是直接填满tcache_bins使tcache_perthread_struct进入unsorted_bins;另一种是伪造counts数组使其误以为0x250的bins已经满了,从而再free一次就可以进入unsorted_bins。
先第一种
1 | for i in range(7): |
第二种
1 | edit(p64(0) * 4 + p64(0x0000000007000000)) #伪造counts数组,使其以为0x250的tcache_bins已被填满,下一次free的时候会把tcache_perthread_struct弄进unsorted_bins |
这两种选哪种都可以,我个人习惯第一种。第二种比较麻烦的一点是要测准counts数组分别对应哪个大小的tcache_bins,不如第一种简单
布置堆块
有了libc_base和heap_base,我们就需要布置堆块了。以往的堆块都能根据索引来进行操作,但这道题不行,这时就需要发挥tcache_perthread_struct的作用了。entry管理各个tache_bins链表上的首chunk的fd指针,那么我们只需要对entry进行修改,就能布置任意堆块,达成我们想要的攻击效果,先看代码
1 | edit(b'\x00' * 0x78) #修改counts,并把entry的一些指针清空 |
至于为什么不直接用ORW链而伪造两个fake_orw,有两个原因。
其一,我们不能直接修改链表为ORW链,会出现未知的错误。
其二,是为了我们的setcontext,用setcontext来迁rsp,达到一种迁栈的效果,ret到这个地址上,就可以完成ORW
完成ORW
在前面,我们已经完成好了堆块的布局,我们只需根据对应的堆块名称执行对应的操作即可,先看堆块的布局


与我们需要用到的地址别无二致,那么此时我们只需要完成如下操作
1 | 0x20 free_hook-->setcontext |
就可以成功getshell了!
1 | #orw |

不过这种攻击目前限于libc-2.29之前,2.29的setcontext控制的寄存器就变成rdx,需要找gadget实行rdi和rdx之间的转换,比之前更麻烦,需要合适地址,条件比较苛刻。而2.31要用rdx控制各种寄存器的地址变成了setcontext+0x61。
总结
ORW的形式多种多样,多做做题应该就能认识个七七八八了,还是得靠练