前置知识:
在2.23下检测逻辑如下:
LABEL(form_number) : if (s->_flags2 & _IO_FLAGS2_FORTIFY) {
if (!readonly_format) {
extern int __readonly_area(const void *, size_t) attribute_hidden;
readonly_format = __readonly_area(format, ((STR_LEN(format) + 1) * sizeof(CHAR_T)));
}
if (readonly_format < 0)
__libc_fatal("*** %n in writable segment detected ***n");
}
可以看到如果readonly_format小于0就会出错,而readonly_format是 __readonly_area这个函数的返回值,这个函数字面意思就是读取只读区域,就是判断格式化字符串的地址是不是只读位置。我们再来看看这个函数源代码:
__readonly_area:
int __readonly_area(const char *ptr, size_t size) {
const void *ptr_end = ptr + size;
FILE *fp = fopen("/proc/self/maps", "rce");
if (fp == NULL) {
/* It is the system administrator's choice to not have /proc
available to this process (e.g., because it runs in a chroot
environment. Don't fail in this case. */
if (errno == ENOENT
/* The kernel has a bug in that a process is denied access
to the /proc filesystem if it is set[ug]id. There has
been no willingness to change this in the kernel so
far. */
|| errno == EACCES)
return 1;
return -1;
}
/* We need no locking. */
__fsetlocking(fp, FSETLOCKING_BYCALLER);
char *line = NULL;
size_t linelen = 0;
while (!feof_unlocked(fp)) {
if (_IO_getdelim(&line, &linelen, 'n', fp) <= 0)
break;
char *p;
uintptr_t from = strtoul(line, &p, 16);
if (p == line || *p++ != '-')
break;
char *q;
uintptr_t to = strtoul(p, &q, 16);
if (q == p || *q++ != ' ')
break;
if (from < (uintptr_t) ptr_end && to > (uintptr_t) ptr) {
/* Found an entry that at least partially covers the area. */
if (*q++ != 'r' || *q++ != '-')
break;
if (from <= (uintptr_t) ptr && to >= (uintptr_t) ptr_end) {
size = 0;
break;
} else if (from <= (uintptr_t) ptr)
size -= to - (uintptr_t) ptr;
else if (to >= (uintptr_t) ptr_end)
size -= (uintptr_t) ptr_end - from;
else
size -= to - from;
if (!size)
break;
}
}
fclose(fp);
free(line);
/* If the whole area between ptr and ptr_end is covered by read-only
VMAs, return 1. Otherwise return -1. */
return size == 0 ? 1 : -1;
}
首先FILE *fp = fopen("/proc/self/maps", "rce");,通过fopen读取maps文件,后续通过文件来和格式化字符串的地址做判断,是否在只读区域,也就是说在可写区域的段上是不能写入%n的,否则会返回-1,导致readonly_format小于0。
那么怎么去绕过呢?
fopen的源代码可以发现,也是调用了open,而open返回的文件描述符,如果我们把文件描述符改为0,就成了标准输入。看下__IO_FILE结构体源码:
_IO_FILE * _IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot, int read_write, int is32not64) {
int fdesc;
#ifdef _LIBC
if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
fdesc = open_not_cancel (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
else
fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
#else
fdesc = open (filename, posix_mode, prot);
#endif
if (fdesc < 0)
return NULL;
fp->_fileno = fdesc;
_IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS)) == (_IO_IS_APPENDING | _IO_NO_READS)) {
_IO_off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
if (new_pos == _IO_pos_BAD && errno != ESPIPE) {
close_not_cancel (fdesc);
return NULL;
}
}
_IO_link_in ((struct _IO_FILE_plus *) fp);
return fp;
}
libc_hidden_def (_IO_file_open)
首先open将文件描述符给了fdesc,然后赋值给了file结构体的_fileno( fp->_fileno = fdesc;),也就是说让 open 函数返回 0 就会使 __readonly_area 程序从标准输入中读取数据,这时候我们只需要从标准输入中传入 000000000000-7fffffffffff r-xp 00000000 00:00 0 /bin/vm 即可绕过 %n 检测。
原文始发于微信公众号(由由学习吧):fprintf_chk 绕过
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论