首页
论坛
课程
招聘
[原创]musl 1.2.2 总结+源码分析 One
2021-9-27 14:37 26008

[原创]musl 1.2.2 总结+源码分析 One

2021-9-27 14:37
26008

这里总结会从 musl 基本数据结构 ,到源码分析,Two为例题

启动musl 程序

提示:

 

这里不要用patchelf的形式来把musl 的libc.so patch到 bin 程序上

 

无论把libc.so 当作ld 或者libc 来patch 都会出现内存布局与实际远程内存布局不同的情况

1
patchelf --set-interpreter libc.so ./bin

or

1
patchelf --replace-needed libc.so ./libc.so ./bin

如果原本没patch, 先申请的chunk在libc 段

 

而patch 后 先申请的chunk 会被放在mmap 的地址上

add('1,0xc,'b'*0xc)

三种运行方式的差别

patchelf后:

image-20210919001411768

 

申请的0xc size的chunk 是被mmap 出来的一片区域

没patch

直接用如下方式启动

1
./libc.so ./bin

image-20210919001921888

 

可以看见同样是申请0xc 大小的chunk ,与patch后相比内存空间布局会出现不同

 

这样可能导致在打远程的时候失败。这种方式 mmap 出的地址 和 libc 基址的偏移和远程一样

源码调试

下载编译好了源码 在运行程序的时候会自动加载本地编译好的musl libc

 

这时候内存布局和远程类似 申请到的chunk也是在libc 地址后面

 

但是 这里mmap出的地址 和libc 基地址的偏移和远程是不一样的(所以还是差别)

 

image-20210921160914979

这里建议的启动方法:

方式1

  • 自己编译一份源码 可以直接运行程序(运行程序时会自动找到musl libc.so 不用patchelf)

  • 然后用本地有符号libc 调试最后换掉偏移(比较麻烦)

    自己编译源码具体可以参考下面这篇文档

    blog.fpliu.com/it/software/musl-libc

  • 可以看见下面我没patch 运行时自动搜索到musl libc.so 路径

image-20210921151145452

 

优点:调试时可以完整看源码

 

缺点:打远程要把偏移换一遍

方式2

(最好的方式 因为这样和远程环境大致一样 若要源码调试 在gdb中直接dir /源码路径即可)

 

直接利用题目给的libc 文件,利用如下方式启动

1
./libc.so ./bin

在脚本里就为

1
io = process(["./libc.so",'./bin'])

从小到大 源码分析

0x00 基本数据结构

先从musl 的基本数据结构 这里我选择从小到大来理解,因为在源码里它就是从小到大索引的

 

从chunk 一路索引到 meta。

 

首先介绍chunk

chunk:

1
2
3
4
5
6
struct chunk{
 char prev_user_data[];
    uint8_t idx;  //5bit为idx第几个chunk
    uint16_t offset; //与group的偏移
    char data[];
};

每个chunk 都有个4 字节的chunk头记录 idx 和 offset

 

(第一个chunk 比较特殊,因为它上面是 group结构 + chunk 头=0x10 )

 

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

 

image-20210919150053764

 

这里 offset 和 idx 比较重要

 

细节:和glibc 的chunk 类似 glibc chunk 可以占用下一个chunk 的prev_size 空间

 

而musl 可以使用 下一个chunk 头的低4B 来储存数据

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
};

image-20210919011425177

  • 在musl 中同一类大小的chunk 都是被分配到 同一个group 中进行管理
  • musl 是通过 chunk addr 和chunk 头对应的 offset 来索引到 group 地址的
  • 整体作为一个 group,其中开头的0x10我们当作group 头,这里的group头 涵盖了第一个chunk的头数据
  • 如这里的第一个chunk是0x7f242f97fd20开始
  • group开头的8个字节存的 meta 的地址,后面8个字节存了第一个chunk 的头数据 和 active_idx
  • 这里active_idx 代表能存下的多少个 可以用的同类型chunk
  • 如图这里可以存下的chunk [0,0x1d] 共 0x1e 个

从chunk 索引到 group:

 

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#musl-1.2.2\src\malloc\mallocng\meta.h line129
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);// base 指向的就是group 地址
  ............
}

根据源码我们可以知道 从chunk 索引到group 起始地址的计算式子为

 

group_addr = chunk_addr - 0x10 * offset - 0x10

 

补充

 

offset = p[-2] (这里的p 就是代指chunk)

 

index 从 get_slot_index(p)中得到

1
2
3
4
static inline int get_slot_index(const unsigned char *p)
{
    return p[-3] & 31;
}

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;
    uintptr_t last_idx:5;
    uintptr_t freeable:1;
    uintptr_t sizeclass:6;
    uintptr_t maplen:8*sizeof(uintptr_t)-12;
};

其中如果这个meta 前后都没有,那么它的prev next 就指向它自己

 

avail_mask,free_mask 是bitmap 的形式体现 chunk 的状态

 

这里例子是我申请了3个 0x30的chunk1、2、3, 然后free 掉chunk2

 

image-20210919020006738

 

image-20210919015741708

 

avail_mask == 120 ==b"01111000" (最前面那个0 不算只是为了对齐)

 

在 avail_mask 中 2 进制的 0 表示不可分配 1表示可分配,顺序是从后到前

 

如01111000 中最后的 3个0 , 表示第1、2、3个 chunk 是不可分配的 前面4个chunk 是可以分配的

 

free_mask == 2 =0010 中的 1 表示第二个chunk2已经被释放

 

last_idx 可以表示最多可用堆块的数量 最多数量=last_idx+1(因为是从0 - last_idx)

 

freeable=1根据源码 代表meta否可以被回收 freeable=0 代表不可以 =1 代表可以

1
2
3
4
5
6
7
8
#musl-1.2.2\src\malloc\mallocng\free.c line 38
static int okay_to_free(struct meta *g)
{
    int sc = g->sizeclass;
 
    if (!g->freeable) return 0;
    ...........
}

sizeclass=3 表示由0x3这个group进行管理这一类的大小的chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
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

 

maplen >= 1表示这个meta里的group 是新mmap出来的,长度为多少

meta->maplen = (needed+4095)/4096;

 

并且这个group 不在size_classes里

 

maplen =0 表示group 不是新mmap 出来的在size_classes里

 

细节:

  • meta 一般申请的是堆空间brk 分配的,有可能是mmap 映射的,而group 都是使用的mmap 的空间

  • 由于bitmap的限制, 因此一个group中最多只能有32个chunk

meta_area

1
2
3
4
5
6
struct meta_area {
    uint64_t check;
    struct meta_area *next;
    int nslots;
    struct meta slots[];
};

meta_area 是管理meta的合集 meta_area 以页为单位分配 所以计算地址如下

 

meta_area_addr = meta & ( -4096 )

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

 

check:是个校验数字 保护meta_area 里的meta,防止meta被 伪造

 

meta_area *next 指向下一个meta_area 如果没有 就默认为0

 

nslots: meta 槽的数量

 

细节:在这个meta_area 页被使用的时候 上一个临近的页 会被设置为不可写

 

是为了防止 使用者覆盖check 校验值

 

image-20210919151955253

__malloc_context

是musl libc 记录结构状态的表,记录各个meta 和 secret 队列信息等

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;
};

image-20210919154755122

 

小总结一下

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

image-20210920011323565

0x01 释放与分配

(如果不想看源码 可以跳下面看总结)

malloc

源码路径

/src/malloc/mallocng/malloc.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
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 && 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); 、
   //alloc_slot 从group 中取出对应大小chunk 的idx
  // 这里先从对应sc 的group 中找没找到 再从队列中其他的group 中找
 // 利用其他group 中被free 的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
}

malloc 的流程:

 

一、

  • 先检查 申请的chunk的 needed size 是否超过最大申请限制
  • 检查申请的needed 是否超过需要mmap 的分配的阈值 超过就用mmap 分配一个group 来给chunk使用
  • 若是mmap 则设置各种标记

二、

  1. 若申请的chunk 没超过阈值 就从active 队列找管理对应size 大小的meta
  2. 没找到 就使用对应更大size 的meta 来满足
  3. 从对应的group 通过 alloc_slot(sc, n)得到idx 索引到 对应要分配的chunk
  4. enframe(g, idx, n, ctr) 取出 对应meta 中对应idx 的chunk

free

源码路径

/src/malloc/mallocng/malloc.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
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);
}

free

 

​ 通过get_meta(p)得到meta (get_meta 是通过chunk 对应的offset 索引到对应的grop 再索引到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 定位

 

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

关于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
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)) {//okay_to_free 检测是否可以被释放
        // 如果group里的chunk都被释放或可以使用 并且可以释放 就回收meta
        if (g->next) {// 如果队列中 有下一个meta
            assert(sc < 48);// 检测 sc 是不是mmap 分配的
            int activate_new = (ctx.active[sc]==g);// 检测当前meta g 和 队列里的active[sc] meta 是否一样
            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 };
}
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 触发条件

 

image-20210920171255661

self = 1 << idx

 

下面是几种简单的触发情况

 

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 表示只有 1个 chunk 没被 释放,这时释放的chunk 就应该是那最后一个chunk

 

如下面情况 avail_mask ==0 free_mask=63=00111111 last_idx = 6

 

已经释放6 个chunk 还有最后一个chunk没被释放 在释放最后一个chunk 时会触发dequeue使得对应meta出队

 

image-20210920171918336

 

3.如果发现这个group中所有的chunk要么被free, 要么是可用的, 那么就会回收掉这个group,调用dequeue从队列中出队

0x02 利用

一般有如下几种利用方法,核心原理都是构造假的chunk 索引到假的group 从而所引导假的meta

 

或覆盖group 中指向meta 的指针 覆盖为假的meta ,然后使得假的meta dequeue 最终实现unlink

 

(构造fake_meta 需要先泄露 secret 校验值)

dequeue 的两种流程

一、

 

通过构造假的meta 满足各种条件 通过以下流程

 

free()->nontrivial_free()->dequeue

 

image-20210920175246340

 

这里通过free 到 dequeue

 

二、

 

通过realloc 里也带有free

 

realloc()->free(old)->nontrivial_free()->dequeue

伪造meta后控制程序流的方法

注意: musl 是没有malloc_hook和 free_hook 这种一般的hook 位

 

且musl 程序的IO_FILE 结构体格式和libc 不一样 没有IO_jump_t的vtable

 

但是存在read,write,seek,close 四个函数指针

 

image-20210920180511605

 

在下一篇文章musl 大总结+源码分析 Two中会结合最近几场大型比赛的题 进行总结

 

下面粗略讲一下思路

  1. 伪造meta 后满足各种条件 使得其进入dequeue 通过unlink,构造prev next 实现 任意地址指针互写

通过任意地址互写指针,向stdout_used 写入我们伪造的fake_stdout地址, 通过IO_FILE 劫持程序执行流

 

到我们布置好的fake_stdout 上,可以找IO_FILE 里的一些函数 如最近学习exit puts

 

这种方式可以先 在fake_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 stdin 和stderr 结构体 劫持程序流


【公告】欢迎大家踊跃尝试高研班11月试题,挑战自己的极限!

最后于 2021-9-27 19:24 被0xRGz编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (3)
雪    币: 6926
活跃值: 活跃值 (12804)
能力值: (RANK:690 )
在线值:
发帖
回帖
粉丝
ScUpax0s 活跃值 12 2021-9-30 12:47
2
0
目前musl源码解析还是比较少的,支持!
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
韩王 活跃值 2021-10-13 16:30
3
0
师傅,函数调用图使用什么软件生成的呀
雪    币: 1444
活跃值: 活跃值 (537)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
0xRGz 活跃值 1 2021-10-13 17:58
4
0
函数调用图是 understand 吾爱上的破解
游客
登录 | 注册 方可回帖
返回