网上各种文章都说emma是kwwi的延伸,讲的都有点模糊,但我没学过kwwi,好不容易才学会,所以今天来讲一下emma。
达到的效果
执行任意函数或gadget
实现的条件
可以控制或伪造一个file结构体,重点是能控制它的vtable指针
如何进行攻击
由于在2.24之后,新增了对vtable的检查机制(即调用vtable的函数之前,要先检查vtable指针是否在__start___libc_IO_vtables和__stop___libc_IO_vtables之间,也就是只能调用者二者之间所存放的函数)所以在2.23版本中的FSOP(在堆中伪造_IO_file_jumps,并让vtable指向此处)利用就失效了,所以emma是一种伪造vtable指向一个合法地址,再利用这个合法地址的一些危险函数实现攻击的一种方法
首先罗列一下emma会用到的结构体和函数
static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = {
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_cookie_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_file_setbuf),
JUMP_INIT(sync, _IO_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_cookie_read),
JUMP_INIT(write, _IO_cookie_write),
JUMP_INIT(seek, _IO_cookie_seek),
JUMP_INIT(close, _IO_cookie_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue),
};
/* Special file type for fopencookie function. */
struct _IO_cookie_file
{
struct _IO_FILE_plus __fp;
void *__cookie;
cookie_io_functions_t __io_functions;
};
typedef struct _IO_cookie_io_functions_t
{
cookie_read_function_t *read; /* Read bytes. */
cookie_write_function_t *write; /* Write bytes. */
cookie_seek_function_t *seek; /* Seek/tell file position. */
cookie_close_function_t *close; /* Close file. */
} cookie_io_functions_t;
危险的函数
static ssize_t
_IO_cookie_read (FILE *fp, void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_read_function_t *read_cb = cfile->__io_functions.read;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (read_cb);
#endif
if (read_cb == NULL)
return -1;
return read_cb (cfile->__cookie, buf, size);
}
static ssize_t
_IO_cookie_write (FILE *fp, const void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_write_function_t *write_cb = cfile->__io_functions.write;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (write_cb);
#endif
if (write_cb == NULL)
{
fp->_flags |= _IO_ERR_SEEN;
return 0;
}
ssize_t n = write_cb (cfile->__cookie, buf, size);
if (n < size)
fp->_flags |= _IO_ERR_SEEN;
return n;
}
static off64_t
_IO_cookie_seek (FILE *fp, off64_t offset, int dir)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (seek_cb);
#endif
return ((seek_cb == NULL
|| (seek_cb (cfile->__cookie, &offset, dir)
== -1)
|| offset == (off64_t) -1)
? _IO_pos_BAD : offset);
}
static int
_IO_cookie_close (FILE *fp)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_close_function_t *close_cb = cfile->__io_functions.close;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (close_cb);
#endif
if (close_cb == NULL)
return 0;
return close_cb (cfile->__cookie);
}
那么emma的利用流程是这样的:
在程序return 0/exit/调用malloc assert时,程序会调用fflush(stderr),这个函数是在程序结束时将所有文件结构体刷新的函数,它会根据vtable找到overflow函数去调用overflow(fp)进行刷新,在vtable正常指向_IO_file_jumps时程序会正常调用overflow(fp),如果把vtable伪造成_IO_cookie_jumps,那么程序就会调用_IO_cookie_jumps里的overflow(fp),那么如果把vtable伪造成_IO_cookie_jumps+0x58呢?这时候我们要去看一眼_IO_cookie_jumps结构体了

原本调用overflow时,应该是找这个表偏移为3的位置,现在把vtable伪造成_IO_cookie_jumps+0x58,那应该在此基础之上再往下找3个偏移,也就是在原来的基础之上找11(0x58对应的偏移)+3的偏移,数一数发现刚好就是_IO_cookie_read,这不正好就是我们提到的危险函数吗?所以现在你应该清楚到底该如何调用这个危险函数了。
那么这些函数为什么是危险函数呢?这里以read为例。说到这里,我们不得不去分析一下他的源码了

在这里我们重点关注这三个箭头指向的位置,方便起见,三个箭头从上到下我们依次记为1,2,3.首先看箭头1,他会把fp强制类型转换成_IO_cookie_file类型,并赋值给cfile变量,_IO_cookie_file长啥样?回到上面的结构体我们看一下

它是由一个_IO_file_plus指针加一个cookie指针加一个函数表组成,简单画一下大概长这样

接着看到箭头2指向的代码,找到cfile->__io_functions.read并赋值给read_cb,而cfile->__io_functions.read不就是箭头指向的位置吗?也就是说,如果我能够伪造一个_IO_cookie_file,在__io_functions.read里放上我自己想要跳转的函数或者gadget,不就可以实现调用了吗?
那么箭头三又是什么意思呢?我们已经知道,此时的read_cb已经是我们自己的函数了,那么这句话的作用说白了就是设置rdi,rsi和rdx,其中,cookie的值就是rdi的值,所以经过适当的伪造,我们也可以控制rdi为我们想要的值,如果题目没开沙箱,那么这里把__io_functions.read未造成systemaddr,把cookie伪造成binshaddr,此时就可以getshell,如果是orw,那么可以参考我house of apple里的apple1打法
然而,这都是理想世界,现实终归是残酷的。
在_IO_cookie_read处,有这样一段代码
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (write_cb);
#endif
这段代码其实是一个小加密,在tls段有一个point_chk_guard,它相当于一个加密的密钥,所有__io_functions里的函数指针,要经过一定的加密,这个加密就是point_chk_guard异或目标函数指针在右移0x11(即target^guard>>0x11),所以我在填写我的函数地址时,也要进行加密的伪造,然而我并不知道point_chk_guard是多少,这里就有两种绕过方式
1.泄露出point_chk_guard
2.改写point_chk_guard为已知量
方法1不必多说,方法2怎么去找到point_chk_guard在哪呢?如果程序开了canary,我们可以先查看canary的值

接下来利用search -p命令搜索canary对应的值

箭头指向的位置+8就是point_chk_guard的存放地址啦

所以利用任意一种任意写的手段把这里修改成已知量即可