重要结构

1
typedef struct { int lock; int cnt; void *owner; } _IO_lock_t;

源码分析

gets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
char *
_IO_gets (char *buf)
{
size_t count;
int ch;
char *retval;

_IO_acquire_lock (stdin);
ch = _IO_getc_unlocked (stdin);
if (ch == EOF)
{
retval = NULL;
goto unlock_return;
}
if (ch == '\n')
count = 0;
else
{
/* This is very tricky since a file descriptor may be in the
non-blocking mode. The error flag doesn't mean much in this
case. We return an error only when there is a new error. */
int old_error = stdin->_flags & _IO_ERR_SEEN;
stdin->_flags &= ~_IO_ERR_SEEN;
buf[0] = (char) ch;
count = _IO_getline (stdin, buf + 1, INT_MAX, '\n', 0) + 1;
if (stdin->_flags & _IO_ERR_SEEN)
{
retval = NULL;
goto unlock_return;
}
else
stdin->_flags |= old_error;
}
buf[count] = 0;
retval = buf;
unlock_return:
_IO_release_lock (stdin);
return retval;
}

_IO_gets 是 gets 的本体

这里重点关注 _IO_acquire_lock 和 _IO_release_lock

_IO_acquire_lock 和 _IO_release_lock

1
2
3
4
5
6
7
8
#  define _IO_acquire_lock(_fp) \
do { \
FILE *_IO_acquire_lock_file \
__attribute__((cleanup (_IO_acquire_lock_fct))) \
= (_fp); \
_IO_flockfile (_IO_acquire_lock_file);

# define _IO_release_lock(_fp) ; } while (0)

注意 cleanup 属性

_IO_flockfile 和 _IO_funlockfile

1
2
3
4
# define _IO_flockfile(_fp) \
if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_lock (*(_fp)->_lock)
# define _IO_funlockfile(_fp) \
if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_unlock (*(_fp)->_lock)

此处 _lock 为 _IO_lock_t 的结构体指针

_IO_lock_lock 和 _IO_lock_unlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define _IO_lock_lock(_name) \
do { \
void *__self = THREAD_SELF; \
if (SINGLE_THREAD_P && (_name).owner == NULL) \
{ \
(_name).lock = LLL_LOCK_INITIALIZER_LOCKED; \
(_name).owner = __self; \
} \
else if ((_name).owner != __self) \
{ \
lll_lock ((_name).lock, LLL_PRIVATE); \
(_name).owner = __self; \
} \
else \
++(_name).cnt; \
} while (0)

#define _IO_lock_unlock(_name) \
do { \
if (SINGLE_THREAD_P && (_name).cnt == 0) \
{ \
(_name).owner = NULL; \
(_name).lock = 0; \
} \
else if ((_name).cnt == 0) \
{ \
(_name).owner = NULL; \
lll_unlock ((_name).lock, LLL_PRIVATE); \
} \
else \
--(_name).cnt; \
} while (0)

_IO_acquire_lock_fct

1
2
3
4
5
6
7
8
static inline void
__attribute__ ((__always_inline__))
_IO_acquire_lock_fct (FILE **p)
{
FILE *fp = *p;
if ((fp->_flags & _IO_USER_LOCK) == 0)
_IO_funlockfile (fp);
}

由于 cleanup 属性, gets 执行完成后会调用这个

_IO_stdfile_0_lock

1
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer;
1
#define _IO_lock_initializer { LLL_LOCK_INITIALIZER, 0, NULL }

gets 执行完成后, rdi 为 _IO_stdfile_0_lock 的地址,指向一个 _IO_lock_t 结构体

THREAD_SELF

1
2
3
4
5
#  define THREAD_SELF \
({ struct pthread *__self; \
asm ("mov %%fs:%c1,%0" : "=r" (__self) \
: "i" (offsetof (struct pthread, header.self))); \
__self;})

极其高效地获取“当前线程”的线程控制块(TCB,即 struct pthread 结构体)的内存基地址

利用思路

目标是通过 gets 后 rdi 的残留值传入 puts 以 leak tls

在 _IO_gets 中,获取输入之前会先用 _IO_lock_lock 处理 _IO_stdfile_0_lock ,这使得 (_name).owner = __self = THREAD_SELF

所以只要我们能覆盖前 _IO_stdfile_0_lock 的前 8 字节就可以通过 (_name).owner 去 leak tls

但是要注意 _IO_acquire_lock_fct 即 _IO_funlockfile 即 _IO_lock_unlock 会在 gets 结束时执行,这意味着 (_name).cnt 会被减去 1 ,然后若此时 (_name).cnt 为 0 ,那么 (_name).owner 会被清空,无法 leak tls

由于 puts 的输出截断于 \x00 ,而 gets 会将末尾设置为 \x00 ,我们需要利用上面 cnt 被减去 1 的机制绕过输出截断

若构造 'AAAA\x00\x00\x00' 的 payload ,由于 cnt 只有不为 0 时才会被减去 1 ,而且 cnt 为 0 owner 会被清空,该构造无效

因此我们考虑分两次布置:第一次布置 b'A' * 8 + b'\x00' * 6 ,目的是触发 (_name).owner == NULL ,第二次布置 b'B' * 4 后即可绕过截断

但是在不同 linux 版本的情况下 leak 出的地址与 libc 的偏移会不同,甚至可能 leak 出的是与 ld 相关的部分,这就导致可能需要尝试利用 ld 中的 gadget 再去 leak libc