freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

afl-fuzz源码分析
2023-07-28 22:10:42
所属地 北京

afl-fuzz源码分析

GCC编译流程

image

  1. 预处理(Preprocessing):对源代码进行预处理,如宏替换、条件编译等,生成经过预处理的源代码。预处理可以通过gcc -E命令单独执行。

  2. 编译(Compilation):将预处理后的源代码翻译成汇编代码(Assembly code),生成.s文件。编译可以通过gcc -S命令单独执行。

  3. 汇编(Assembly):将汇编代码翻译成机器语言的二进制指令(Object code),生成.o文件。汇编可以通过as程序单独执行。

  4. 链接(Linking):将多个.o文件或库文件(如.a.so)链接成可执行程序或共享库文件,生成最终的二进制文件。链接可以通过ld程序单独执行,但通常由GCC自动调用。

1.afl-gcc

afl-gcc本身是gcc编译器的封装,通过afl的一些环境变量,设置一些gcc的编译选项,如asan,msan,编译器优化等,指定汇编器为afl-as,生成ob code.

全局变量

static u8*  as_path;                /* Path to the AFL 'as' wrapper      */
static u8** cc_params;              /* Parameters passed to the real CC  */
static u32  cc_par_cnt = 1;         /* Param count, including argv0      */
static u8   be_quiet,               /* Quiet mode                        */
            clang_mode;             /* Invoked as afl-clang*?            */

as_path:afl-as的路径

cc_params:调用gcc或者clang的参数

cc_par_cnt:gcc clang参数数量

be_quiet:静默模式

clang_mode:是否使用afl-clang

main

int main(int argc, char** argv) {

  if (isatty(2) && !getenv("AFL_QUIET")) {

    SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <lcamtuf@google.com>\n");

  } else be_quiet = 1;

  if (argc < 2) {

    SAYF("\n"
         "This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"
         "for gcc or clang, letting you recompile third-party code with the required\n"
         "runtime instrumentation. A common use pattern would be one of the following:\n\n"

         "  CC=%s/afl-gcc ./configure\n"
         "  CXX=%s/afl-g++ ./configure\n\n"

         "You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.\n"
         "Setting AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",
         BIN_PATH, BIN_PATH);

    exit(1);

  }

  find_as(argv[0]);

  edit_params(argc, argv);

  execvp(cc_params[0], (char**)cc_params);

  FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);

  return 0;

}

be_quiet为1时,不会打印程序输出信息

主要功能集中在find_as,edit_params函数,最后执行execvp

find_as

函数功能是寻找伪造的gcc-as(afl-as)汇编程序,实际上是通过环境变量"AFL_PATH"或者afl-gcc的当前执行目录寻找afl-as的路径.

edit_params

函数功能是,将argv的参数复制到cc_params,以及做一些参数的处理.

首先alloc给cc_params一个(argc+128)*8字节大小的内存

1.检查当前执行程序名是否为afl-clangxx,如果是

clang_mode=1,表示使用afl-clang模式

如果是clang++

尝试获取AFL_CXX环境变量.或者使用默认值"clang++",赋值给cc_params[0]

如果不是clang++

尝试获取AFL_CC环境变量.或者使用默认值"clang",赋值给cc_params[0]

如果不是afl-clangxx,会认为是apple平台并做一些处理,这里不做赘述.

2.while循环,遍历argv[1]和之后的参数,做一些处理.

参数:-B是否指定了汇编器afl-as的路径,如果是默认模式,直接跳过.

参数:-integrated-as和-pipe 直接跳过不做处理.

参数:-fsanitize=address和-fsanitize=memory,gcc的编译选项,LLVM的组件Asan,将asan_set=1,这两个参数是Asan用于检测内存访问越界,内存泄露问题的.如果编译时插入一些安全检查,需要记录和跟踪信息,可以加上.

参数:FORTIFY_SOURCE,将fortify_set = 1,gcc编译时会在一些容易出现漏洞的函数插入一些安全检查,如memcpy,strcpy...

3.while结束,对前面做的一些标记做参数处理.

-B as_path,find_as里面寻找到的afl-as路径.

clang_mode为1 设置-no-integrated-as

如果环境变量存在AFL_HARDEN.设置gcc -fstack-protector-all和-D_FORTIFY_SOURCE=2,这个afl的编译选项,会开启一些编译时的安全保护.
如果asan_set为1,设置了些编译时的内存错误检测,设置环境变量AFL_USE_ASAN为1

编译选项asan和msan相关,添加这些编译选项,利于内存错误的分析

获取环境变量AFL_USE_ASAN,AFL_USE_MSAN,AFL_HARDEN,设置了一些-U_FORTIFY_SOURCE和-fsanitize=memory参数,只是此处AFL_USE_ASAN和AFL_USE_MSAN不能同时设置,因为使用asan和msan编译同一源代码时,会对运行速度造成影响,afl-fuzz可能会觉得对效率有影响

编译器优化相关,添加这些编译选项,禁止编译优化,利于afl-fuzz更好的探测一些漏洞,但是程序会变慢,可能会对fuzz效率造成影响.

获取环境变量AFL_DONT_OPTIMIZE,存在

设置gcc编译选项-g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1

编译器内置函数优化相关的一些编译选项,内置函数可能不安全,禁用可能会导致程序变慢.

获取环境变量AFL_NO_BUILTIN,存在

一些函数不使用编译器内置的,如下:

cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";
 cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";
 cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";
 cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";
 cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";
 cc_params[cc_par_cnt++] = "-fno-builtin-strstr";
 cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr";

execvp(cc_params[0], (char**)cc_params);

afl-fuzz目录下会有一个as的连接,指向afl-as,gcc编译时,汇编器指定了afl-fuzz目录,会自动寻找as,从而执行afl-as,简而言之,就是代替了系统的汇编器as

执行gcc, 指定汇编器为afl-fuzz目录下的as(afl-as)进行汇编,,带上设置好的参数.

for (int i = 0; i < sizeof(cc_params); ++i) {
        SAYF("%s ", *(cc_params + i));
    }
gcc ../testc.c -o ../testc -B /home/ash/code/afl-fuzz -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1

2.afl-as

afl-as是gas汇编器的封装.

全局变量

static u8** as_params;          /* Parameters passed to the real 'as'   */

static u8*  input_file;         /* Originally specified input file      */
static u8*  modified_file;      /* Instrumented file for the real 'as'  */

static u8   be_quiet,           /* Quiet mode (no stderr output)        */
            clang_mode,         /* Running in clang mode?               */
            pass_thru,          /* Just pass data through?              */
            just_version,       /* Just show version?                   */
            sanitizer;          /* Using ASAN / MSAN                    */

static u32  inst_ratio = 100,   /* Instrumentation probability (%)      */
            as_par_cnt = 1;     /* Number of params to 'as'             */

as_params:gcc传递给as的参数

input_file:指定输入文件

modified_file:afl-as输出的插桩后的汇编文件

be_quiet:静默模式

clang_mode:clang编译模式

pass_thru:是否在汇编时不做插桩.

just_version:只显示版本

sanitizer:使用asan/msan

inst_ratio:插桩代码的比例.

as_par_cnt:指定线程运行afl-as

main

gcc传递过来的参数

/home/ash/code/afl-fuzz/as --gdwarf-5 --64 -o /tmp/ccQwLzYy.o /tmp/ccw6hnjr.s

首先从环境变量中获取AFL_INST_RATIO,赋值给inst_ratio_str,这个变量代表插桩频率,如果为100,则在每个块中都会插入插桩代码,如果为0,则只插桩函数入口的块.

获取当前时间,pid,计算成一个rand_seed,生成随机数.

edit_params函数对参数进行处理

获取环境变量AS_LOOP_ENV_VAR,这个环境变量是afl-as重复汇编的次数,默认设置为1.为了解决插桩可能会导致的执行异常情况,多次通过随机值插桩,避免程序出现异常.

通过检查环境变量AFL_USE_ASAN,AFL_USE_MSAN,判断是否存在这些编译选项,将sanitizer设置为1,并将inst_ratio/3,这里做了处理原因如果开启了asan或者msan会导致程序的分支增多,在插桩到这些分支,然后执行,是没有意义的.所以这里直接做了除3处理,将比例减小.

执行add_instrumentation函数,开始插桩

fork一个子进程执行gas进行汇编,参数

as --gdwarf-5 --64 -o /tmp/ccQwLzYy.o /tmp/.afl-1040-1683019150.s

gas执行结束后,检测环境变量AFL_KEEP_ASSEMBLY,如果没有就删除掉插桩的汇编文件.

edit_params

主要是做as的参数处理,构造汇编文件名参数,架构,赋值input_file=汇编文件.

add_instrumentation

插桩函数

首先获取需要插桩的汇编文件赋值于inputfile,而后在本地创建插桩文件modified_file,并打开赋值于outf.

逐行读取至line数组中,然后做一些条件判断,跳过标签,宏,注释这些不需要插桩的地方,这里是通过一些标志位判断的.

输入代码到modified_file.

对当前读取到的区段进行判断,如果为.text相关的区段,将标志位instr_ok修改为1,表示将对代码分支进行插桩.

如果为bss,data这些数据段,标志为0,不会进行插桩.

如果为p2align宏,将标志位skip_next_label更改为1.

if (line[0] == '\t' && line[1] == '.') {

            /* OpenBSD puts jump tables directly inline with the code, which is
               a bit annoying. They use a specific format of p2align directives
               around them, so we use that as a signal. */

            if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
                isdigit(line[10]) && line[11] == '\n')
                skip_next_label = 1;

            if (!strncmp(line + 2, "text\n", 5) ||
                !strncmp(line + 2, "section\t.text", 13) ||
                !strncmp(line + 2, "section\t__TEXT,__text", 21) ||
                !strncmp(line + 2, "section __TEXT,__text", 21)) {
                instr_ok = 1;
                continue;
            }

            if (!strncmp(line + 2, "section\t", 8) ||
                !strncmp(line + 2, "section ", 8) ||
                !strncmp(line + 2, "bss\n", 4) ||
                !strncmp(line + 2, "data\n", 5)) {
                instr_ok = 0;
                continue;
            }

        }

检测是否为.code段,如果为.code32,需要跳过,.code64不跳过.

如果为.intel_syntax,需要跳过,.att_syntax不跳过.

检测和跳过ad-hoc ___asm___代码块,#APP表示,进入代码块,需要跳过,#NO_APP表示结束,需要插桩.

这里做的处理是跳过一些不需要插桩的部分,确保程序不会出错.

if (strstr(line, ".code")) {

            if (strstr(line, ".code32")) skip_csect = use_64bit;
            if (strstr(line, ".code64")) skip_csect = !use_64bit;

        }

        /* Detect syntax changes, as could happen with hand-written assembly.
           Skip Intel blocks, resume instrumentation when back to AT&T. */

        if (strstr(line, ".intel_syntax")) skip_intel = 1;
        if (strstr(line, ".att_syntax")) skip_intel = 0;

        /* Detect and skip ad-hoc __asm__ blocks, likewise skipping them. */

        if (line[0] == '#' || line[1] == '#') {

            if (strstr(line, "#APP")) skip_app = 1;
            if (strstr(line, "#NO_APP")) skip_app = 0;

        }

afl-fuzz插桩的范围

^main: - 函数入口点
^.L0: - GCC分支标签
^.LBB0_0: - clang分支标签(但只在clang模式下)。
^tjnz foo - 条件跳转分支

总之,是主要的main函数,和条件分支指令前进行插桩,以此实现fuzz的路径覆盖率检测,其中又包含了对插桩比例的计算,随机数小于inst_ratio才会进行插桩.

而后将插桩指令写入至outfd,这里通过对架构进行了判断,以插入不同的桩代码,并生成了一个随机数写入R(MAP_SIZE),作为桩的编号,然后将ins_lines,插桩计数+1.

/* If we're in the right mood for instrumenting, check for function
           names or conditional labels. This is a bit messy, but in essence,
           we want to catch:

             ^main:      - function entry point (always instrumented)
             ^.L0:       - GCC branch label
             ^.LBB0_0:   - clang branch label (but only in clang mode)
             ^\tjnz foo  - conditional branches

           ...but not:

             ^# BB#0:    - clang comments
             ^ # BB#0:   - ditto
             ^.Ltmp0:    - clang non-branch labels
             ^.LC0       - GCC non-branch labels
             ^.LBB0_0:   - ditto (when in GCC mode)
             ^\tjmp foo  - non-conditional jumps

           Additionally, clang and GCC on MacOS X follow a different convention
           with no leading dots on labels, hence the weird maze of #ifdefs
           later on.

         */

        if (skip_intel || skip_app || skip_csect || !instr_ok ||
            line[0] == '#' || line[0] == ' ')
            continue;

        /* Conditional branch instruction (jnz, etc). We append the instrumentation
           right after the branch (to instrument the not-taken path) and at the
           branch destination label (handled later on). */

        if (line[0] == '\t') {

            if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) {

                fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
                        R(MAP_SIZE));

                ins_lines++;

            }

            continue;

        }

而后判断是否为:的判断,下个字符是否为.,如果不是,afl-as默认当作一个function.,将instrument_next更改为1.

如果是继续判断,是否为.L.LBB这种需要插桩的跳转目标标签.

gnu编译器,通常以.L0开头,clang编译下,以.LBB开头,计算inst_ratio通过后,将instrument_next更改为1.

#ifdef __APPLE__

        /* Apple: L<whatever><digit>: */

        if ((colon_pos = strstr(line, ":"))) {

          if (line[0] == 'L' && isdigit(*(colon_pos - 1))) {

#else

        /* Everybody else: .L<whatever>: */

        if (strstr(line, ":")) {

            if (line[0] == '.') {

#endif /* __APPLE__ */

                /* .L0: or LBB0_0: style jump destination */

#ifdef __APPLE__

                /* Apple: L<num> / LBB<num> */

                if ((isdigit(line[1]) || (clang_mode && !strncmp(line, "LBB", 3)))
                    && R(100) < inst_ratio) {

#else

                /* Apple: .L<num> / .LBB<num> */

                if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3)))
                    && R(100) < inst_ratio) {

#endif /* __APPLE__ */

                    /* An optimization is possible here by adding the code only if the
                       label is mentioned in the code in contexts other than call / jmp.
                       That said, this complicates the code by requiring two-pass
                       processing (messy with stdin), and results in a speed gain
                       typically under 10%, because compilers are generally pretty good
                       about not generating spurious intra-function jumps.

                       We use deferred output chiefly to avoid disrupting
                       .Lfunc_begin0-style exception handling calculations (a problem on
                       MacOS X). */

                    if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;

                }

            } else {

                /* Function label (always instrumented, deferred mode). */

                instrument_next = 1;

            }

        }

    }

最后,结束while循环.

判断ins_lines插桩数量不为0,则根据架构插入main_payload64或main_payload_32

而后释放资源.

afl-as桩代码分析

lea     rsp, [rsp-98h]
mov     [rsp+0A0h+var_A0], rdx
mov     [rsp+0A0h+var_98], rcx
mov     [rsp+0A0h+var_90], rax
mov     rcx, 0D8FEh
call    __afl_maybe_log
mov     rax, [rsp+0A0h+var_90]
mov     rcx, [rsp+0A0h+var_98]
mov     rdx, [rsp+0A0h+var_A0]
lea     rsp, [rsp+98h]

保存rdc,rcx,rax寄存器状态

传入一个随机数至rcx,这个随机数是之前R(MAP_SIZE)产生的,调用__afl_maybe_log

恢复寄存器

.lcomm   __afl_area_ptr, 8
.lcomm   __afl_prev_loc, 8
.lcomm   __afl_fork_pid, 4
.lcomm   __afl_temp, 4
.lcomm   __afl_setup_failure, 1
.comm    __afl_global_area_ptr, 8, 8

通过.comm,.lcomm在bss段初始化的一些变量.

__afl_area_ptr:一块内存的指针,记录当前代码块的执行信息

__afl_prev_loc:记录上一个插桩的路径信息.

_afl_maybe_log首次执行流程:

.text:00005555555554F0 lahf
.text:00005555555554F1 seto    al
.text:00005555555554F4 mov     rdx, cs:__afl_area_ptr
.text:00005555555554FB test    rdx, rdx
.text:00005555555554FE jz      short __afl_setup

首先执行lahf和 seto al将eflags寄存器的低8位(SF,ZF,AF,PF,CF)和OF保存到ax中.

判断__afl_area_ptr是否为0,为0执行__afl_setup

.text:0000555555555528 __afl_setup: 
.text:0000555555555528 cmp     cs:__afl_setup_failure, 0
.text:000055555555552F jnz     short __afl_return
.text:000055555555552F
.text:0000555555555531 lea     rdx, __afl_global_area_ptr
.text:0000555555555538 mov     rdx, [rdx]
.text:000055555555553B test    rdx, rdx
.text:000055555555553E jz      short __afl_setup_first

继续判断__afl_setup_failure是否为0,不为0说明afl初始化时发生了错误,直接调用__afl_return还原,结束.

继续判断afl_global_area_ptr中指针指向的内存是否为0,如果为0,说明afl是首次执行,执行__afl_setup_first

.text:0000555555555549 __afl_setup_first:                      ; CODE XREF: __afl_maybe_log+4E↑j
.text:0000555555555549 lea     rsp, [rsp-160h]
.text:0000555555555551 mov     [rsp+160h+var_160], rax
.text:0000555555555555 mov     [rsp+160h+var_158], rcx
.text:000055555555555A mov     [rsp+160h+var_150], rdi
.text:000055555555555F mov     [rsp+160h+var_140], rsi
.text:0000555555555564 mov     [rsp+160h+var_138], r8
.text:0000555555555569 mov     [rsp+160h+var_130], r9
.text:000055555555556E mov     [rsp+160h+var_128], r10
.text:0000555555555573 mov     [rsp+160h+var_120], r11
.text:0000555555555578 movq    [rsp+160h+var_100], xmm0
.text:000055555555557E movq    [rsp+160h+var_F0], xmm1
.text:0000555555555584 movq    [rsp+160h+var_E0], xmm2
.text:000055555555558D movq    [rsp+160h+var_D0], xmm3
.text:0000555555555596 movq    [rsp+160h+var_C0], xmm4
.text:000055555555559F movq    [rsp+160h+var_B0], xmm5
.text:00005555555555A8 movq    [rsp+160h+var_A0], xmm6
.text:00005555555555B1 movq    [rsp+160h+var_90], xmm7
.text:00005555555555BA movq    [rsp+160h+var_80], xmm8
.text:00005555555555C4 movq    [rsp+160h+var_70], xmm9
.text:00005555555555CE movq    [rsp+160h+var_60], xmm10
.text:00005555555555D8 movq    [rsp+160h+var_50], xmm11
.text:00005555555555E2 movq    [rsp+160h+var_40], xmm12
.text:00005555555555EC movq    [rsp+160h+var_30], xmm13
.text:00005555555555F6 movq    [rsp+160h+var_20], xmm14
.text:0000555555555600 movq    [rsp+160h+var_10], xmm15
.text:000055555555560A push    r12
.text:000055555555560C mov     r12, rsp
.text:000055555555560F sub     rsp, 10h
.text:0000555555555613 and     rsp, 0FFFFFFFFFFFFFFF0h
.text:0000555555555617 lea     rdi, _AFL_SHM_ENV               ; "__AFL_SHM_ID"
.text:000055555555561E call    _getenv
.text:000055555555561E
.text:0000555555555623 test    rax, rax
.text:0000555555555626 jz      __afl_setup_abort
.text:0000555555555626
.text:000055555555562C mov     rdi, rax                        ; nptr
.text:000055555555562F call    _atoi
.text:000055555555562F
.text:0000555555555634 xor     rdx, rdx                        ; shmflg
.text:0000555555555637 xor     rsi, rsi                        ; shmaddr
.text:000055555555563A mov     rdi, rax                        ; shmid
.text:000055555555563D call    _shmat
.text:000055555555563D
.text:0000555555555642 cmp     rax, 0FFFFFFFFFFFFFFFFh
.text:0000555555555646 jz      __afl_setup_abort
.text:0000555555555646
.text:000055555555564C mov     byte ptr [rax], 1
.text:000055555555564F mov     rdx, rax
.text:0000555555555652 mov     cs:__afl_area_ptr, rax
.text:0000555555555659 lea     rdx, __afl_global_area_ptr
.text:0000555555555660 mov     [rdx], rax
.text:0000555555555663 mov     rdx, rax

__afl_setup_first中开辟了160字节的栈帧,保存当前寄存器的状态.

获取环境变量__AFL_SHM_ID的值.该值是一个共享内存id,用于fuzz时不同进程之间的通信.

如果失败执行__afl_setup_abort:__afl_setup_failure自增,还原寄存器状态,释放栈,调用__afl_return还原,结束.

执行shmat获取__AFL_SHM_ID对应的共享内存,附加后,获取在本进程空间可以访问的地址.如果失败,则执行__afl_setup_abort

在共享内存中写入1.将地址写入_afl_area_ptr_afl_global_area_ptr

而后开始执行__afl_forkserver

pushq %rdx
pushq %rdx
movq $4, %rdx               					/* length    */
leaq __afl_temp(%rip), %rsi 					/* data      */
movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi       /* file desc */
CALL_L64("write")
cmpq $4, %rax
jne  __afl_fork_resume
/* Designated file descriptors for forkserver commands (the application will
   use FORKSRV_FD and FORKSRV_FD + 1): */

#define FORKSRV_FD          198

__afl_temp的值写入到指定的通信描述符中.__afl_temp实质上存储的是后面fork server的状态.这里如果写入失败将会跳转到__afl_fork_resume->__afl_store->__afl_return

__afl_fork_resume:释放资源,恢复寄存器状态

.text:0000555555555531 48 8D 15 00 2B 00 00          lea     rdx, __afl_global_area_ptr
.text:0000555555555538 48 8B 12                      mov     rdx, [rdx]
.text:000055555555553B 48 85 D2                      test    rdx, rdx
.text:000055555555553E 74 09                         jz      short __afl_setup_first
.text:000055555555553E
.text:0000555555555540 48 89 15 D1 2A 00 00          mov     cs:__afl_area_ptr, rdx
.text:0000555555555547 EB B7                         jmp     short __afl_store
.text:0000555555555500                               __afl_store:                            ; CODE XREF: __afl_maybe_log+57↓j
.text:0000555555555500                                                                       ; __afl_maybe_log+314↓j
.text:0000555555555500 48 33 0D 19 2B 00 00          xor     rcx, cs:__afl_prev_loc
.text:0000555555555507 48 31 0D 12 2B 00 00          xor     cs:__afl_prev_loc, rcx
.text:000055555555550E 48 D1 2D 0B 2B 00 00          shr     cs:__afl_prev_loc, 1
.text:0000555555555515 80 04 0A 01                   add     byte ptr [rdx+rcx], 1
.text:0000555555555519 80 14 0A 00                   adc     byte ptr [rdx+rcx], 0

__afl_store:

首先将当前桩的值(R(MAPSIZE))异或先前桩的值,然后右移1位.再将__afl_prev_loc异或这个值,然后将rcx中的值作为偏移__afl_global_area_ptr中指向的内存作为偏移,将此位置+1.

实际上在内存中形成了一个map结构,记录了分支执行次数,右移的目的是为了如果当前分支和上一个分支是一样的,xor的结果是0的情况.

还有一种情况时2个分支如果相互都存在执行关系的话,xor结果是一样的,右移后就可以区分了.

__afl_return:恢复eflags寄存器状态

.text:000055555555568C                               __afl_fork_wait_loop:                   ; CODE XREF: __afl_maybe_log+22F↓j
.text:000055555555568C 48 C7 C2 04 00 00 00          mov     rdx, 4                          ; nbytes
.text:0000555555555693 48 8D 35 92 29 00 00          lea     rsi, __afl_temp                 ; buf
.text:000055555555569A 48 C7 C7 C6 00 00 00          mov     rdi, 0C6h                       ; status
.text:00005555555556A1 E8 CA FA FF FF                call    _read
.text:00005555555556A1
.text:00005555555556A6 48 83 F8 04                   cmp     rax, 4
.text:00005555555556AA 0F 85 59 01 00 00             jnz     __afl_die
.text:00005555555556AA
.text:00005555555556B0 E8 2B FB FF FF                call    _fork
.text:00005555555556B0
.text:00005555555556B5 48 83 F8 00                   cmp     rax, 0
.text:00005555555556B9 0F 8C 4A 01 00 00             jl      __afl_die
.text:00005555555556B9
.text:00005555555556BF 74 63                         jz      short __afl_fork_resume
.text:00005555555556BF
.text:00005555555556C1 89 05 61 29 00 00             mov     cs:__afl_fork_pid, eax
.text:00005555555556C7 48 C7 C2 04 00 00 00          mov     rdx, 4                          ; n
.text:00005555555556CE 48 8D 35 53 29 00 00          lea     rsi, __afl_fork_pid             ; buf
.text:00005555555556D5 48 C7 C7 C7 00 00 00          mov     rdi, 0C7h                       ; fd
.text:00005555555556DC E8 6F FA FF FF                call    _write
.text:00005555555556DC
.text:00005555555556E1 48 C7 C2 00 00 00 00          mov     rdx, 0                          ; options
.text:00005555555556E8 48 8D 35 3D 29 00 00          lea     rsi, __afl_temp                 ; stat_loc
.text:00005555555556EF 48 8B 3D 32 29 00 00          mov     rdi, qword ptr cs:__afl_fork_pid ; pid
.text:00005555555556F6 E8 B5 FA FF FF                call    _waitpid
.text:00005555555556F6
.text:00005555555556FB 48 83 F8 00                   cmp     rax, 0
.text:00005555555556FF 0F 8E 04 01 00 00             jle     __afl_die
.text:00005555555556FF
.text:0000555555555705 48 C7 C2 04 00 00 00          mov     rdx, 4                          ; n
.text:000055555555570C 48 8D 35 19 29 00 00          lea     rsi, __afl_temp                 ; buf
.text:0000555555555713 48 C7 C7 C7 00 00 00          mov     rdi, 0C7h                       ; fd
.text:000055555555571A E8 31 FA FF FF                call    _write
.text:000055555555571A
.text:000055555555571F E9 68 FF FF FF                jmp     __afl_fork_wait_loop

接着会执行__afl_fork_wait_loop的流程,中间如果遇到系统调用失败的问题,会跳转至__afl_die直接退出进程.

首先会阻塞读取指定的通信描述符FORKSRV_FD的值,,读取失败会结束进程,然后__fork一个子进程,它的作用在afl-fuzz.c中会体现,用于控制fork server.

子进程执行__afl_fork_resume

当前进程:

当前进程将子进程id记录到__afl_fork_pid,将子进程id写入共享内存当中.

waitpid等待子进程执行完毕,并将status存储至__afl_temp

然后将status信息写入共享内存.而后继续循环__afl_fork_wait_loop的流程.

子进程:

释放资源,恢复状态,继续向下执行代码

4.afl-clang-fast

afl-fast-clang与之前的afl-gcc实现思路是一样的,它是clang的一层封装

与afl-gcc不同的是,afl-fast-clang使用了llvm pass进行了插桩.

这也是afl-fuzz推荐的一种插桩和编译方式,因为llvm是模块化的,使用llvm pass可扩展性比较强.

之前分析过afl-gcc,afl-fast-clang与它功能一致,这里一些基本的函数就简单概述下,主要是分析下llvm pass的代码

全局变量

static u8*  obj_path;               //LLVM PASS程序路径
static u8** cc_params;              //clang参数
static u32  cc_par_cnt = 1;         //clang参数数量

find_obj

从环境变量AFL_PATH寻找afl-llvm-rt.o,或者从当前目录找,最后从AFL_PATH宏去找,找到后赋值obj_path.找不到报错.

edit_params

主要是设置clang的参数

通过当前执行afl-clang-fast/afl-clang-fast++设置参数clang/clang++,并设置环境变量AFL_CXX/AFL_CC

而后load llvm pass插件afl-llvm-pass.so编译目标,传入插桩参数

根据传入参数,设置bit_mode,x_set,asan_set的值.

如果x_set的值为1,则设置参数为-x none

根据bit_mode的值,设置参数obj_path/afl-llvm-rt-32.o或obj_path/afl-llvm-rt-64.o,

默认为obj_path/afl-llvm-rt.o

然后设置AFL运行时库afl-llvm-rt.o的一些宏.

main

main函数在执行find_obj设置obj_path和调用edit_params设置参数后,执行execvp,参数如下

clang -Xclang -load -Xclang /home/ash/code/afl-fuzz/afl-llvm-pass.so -Qunused-arguments ../testc.c -o ../testc.o -g -O3 -funroll-loops -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 -D__AFL_LOOP(_A)=({ static volatile char *_B __attribute__((used));  _B = (char*)"##SIG_AFL_PERSISTENT##"; __attribute__((visibility("default"))) int _L(unsigned int) __asm__("__afl_persistent_loop"); _L(_A); }) -D__AFL_INIT()=do { static volatile char *_A __attribute__((used));  _A = (char*)"##SIG_AFL_DEFER_FORKSRV##"; __attribute__((visibility("default"))) void _I(void) __asm__("__afl_manual_init"); _I(); } while (0) /home/ash/code/afl-fuzz/afl-llvm-rt.o

llvm-pass代码分析

namespace {

  class AFLCoverage : public ModulePass {

    public:

      static char ID;
      AFLCoverage() : ModulePass(ID) { }

      bool runOnModule(Module &M) override;

      // StringRef getPassName() const override {
      //  return "American Fuzzy Lop Instrumentation";
      // }

  };

}

afl-fuzz编写的pass名为AFLCoverage,以模块为单位对IR进行处理.重点看下runOnModule,看看是怎么对模块进行处理的.

bool AFLCoverage::runOnModule(Module &M) {

  LLVMContext &C = M.getContext();

  IntegerType *Int8Ty  = IntegerType::getInt8Ty(C);
  IntegerType *Int32Ty = IntegerType::getInt32Ty(C);

  /* Show a banner */

  char be_quiet = 0;

  if (isatty(2) && !getenv("AFL_QUIET")) {

    SAYF(cCYA "afl-llvm-pass " cBRI VERSION cRST " by <lszekeres@google.com>\n");

  } else be_quiet = 1;

  /* Decide instrumentation ratio */

  char* inst_ratio_str = getenv("AFL_INST_RATIO");
  unsigned int inst_ratio = 100;

  if (inst_ratio_str) {

    if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || !inst_ratio ||
        inst_ratio > 100)
      FATAL("Bad value of AFL_INST_RATIO (must be between 1 and 100)");

  }

  /* Get globals for the SHM region and the previous location. Note that
     __afl_prev_loc is thread-local. */

  GlobalVariable *AFLMapPtr =
      new GlobalVariable(M, PointerType::get(Int8Ty, 0), false,
                         GlobalValue::ExternalLinkage, 0, "__afl_area_ptr");

  GlobalVariable *AFLPrevLoc = new GlobalVariable(
      M, Int32Ty, false, GlobalValue::ExternalLinkage, 0, "__afl_prev_loc",
      0, GlobalVariable::GeneralDynamicTLSModel, 0, false);

  /* Instrument all the things! */

首先获取LLVM上下文对象,这个上下文对象LLVMContext时llvm用于管理一些信息和状态的,比如常量,函数,类型定义等.

然后通过上下文对象获取了2个整数类型的对象,分别是8为和32位的.

而后通过环境变量AFL_QUIET设置是否启用静默模式be_quiet

读取环境变量AFL_INST_RATIO,设置插桩概率inst_ratio的值,默认为100.

而后通过LLVM的GlobalVariable类创建了2个全局变量

__afl_area_ptr:8位,共享内存,记录桩执行信息

__afl_prev_loc:32位,记录先前代码块

int inst_blocks = 0;

  for (auto &F : M)
    for (auto &BB : F) {

      BasicBlock::iterator IP = BB.getFirstInsertionPt();
      IRBuilder<> IRB(&(*IP));

      if (AFL_R(100) >= inst_ratio) continue;

通过inst_blocks变量记录插桩的基本块数量.

然后遍历每个基本块进行处理.

而后通过BB.getFirstInsertionPt();从基本块头部开始获取可插入点位置,而后作为构造参数创建了IRBuilder对象,用于后面创建和插入指令.

首先生成100内的随机数是否大于inst_ratio,大于则跳过不做处理.

/* Make up cur_loc */

      unsigned int cur_loc = AFL_R(MAP_SIZE);

      ConstantInt *CurLoc = ConstantInt::get(Int32Ty, cur_loc);

      /* Load prev_loc */

      LoadInst *PrevLoc = IRB.CreateLoad(AFLPrevLoc);
      PrevLoc->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
      Value *PrevLocCasted = IRB.CreateZExt(PrevLoc, IRB.getInt32Ty());

而后获取了一个随机数,并通过ConstantInt::get将这个随机数创建为常量.

然后创建了读取指令用于读取AFLPrevLoc的值,获取前一个基本块的编号,并将这个这个指令设置了一个nosanitize的属性,并且后面的一些读取指令都加了这个属性,作用应该是编译时,跳过对该指令的一些安全检查,提高fuzz速度.然后创建零扩展指令,对结果进行零扩展.

/* Load SHM pointer */

      LoadInst *MapPtr = IRB.CreateLoad(AFLMapPtr);
      MapPtr->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
      Value *MapPtrIdx =
          IRB.CreateGEP(MapPtr, IRB.CreateXor(PrevLocCasted, CurLoc));

而后创建如下获取共享内存中指定内存地址的操作指令:

创建读取指令读取AFLMapPtr,也就是共享内存,然后通过当前基本块id xor前一个基本块id的结果作为偏移,以AFLMapPtr作为基址,获取对应的内存地址.

/* Update bitmap */
      LoadInst *Counter = IRB.CreateLoad(MapPtrIdx);
      Counter->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));
      Value *Incr = IRB.CreateAdd(Counter, ConstantInt::get(Int8Ty, 1));
      IRB.CreateStore(Incr, MapPtrIdx)
          ->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));

而后创建向上一步获取的内存地址中的数据进行自增的操作指令:

获取内存里的数据,自增之后,再写入.

/* Set prev_loc to cur_loc >> 1 */

      StoreInst *Store =
          IRB.CreateStore(ConstantInt::get(Int32Ty, cur_loc >> 1), AFLPrevLoc);
      Store->setMetadata(M.getMDKindID("nosanitize"), MDNode::get(C, None));

      inst_blocks++;

然后将当前的基本块id右移1位,写入至AFLPrevLoc,插桩的基本块计数+1.

右移的目的是为了解决出现以下两种情况存在的问题:

1.如果当前基本块和上一个基本块是一样的,xor的结果是0的情况.

2.如果2个基本块相互存在控制流关系的话,无论从哪个基本块执行到目标基本块,xor结果是一样的,右移后就可以进行区分了.

与前面的afl-as中插桩的代码一样,都是对插桩的基本块执行次数进行了记录,实现覆盖率的反馈.

afl-llvm-rt.o.c 分析

clang -Xclang -load -Xclang /home/ash/code/afl-fuzz/afl-llvm-pass.so -Qunused-arguments ../testc.c -o ../testc.o -g -O3 -funroll-loops -D__AFL_HAVE_MANUAL_CONTROL=1 -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1 -D__AFL_LOOP(_A)=({ static volatile char *_B __attribute__((used));  _B = (char*)"##SIG_AFL_PERSISTENT##"; __attribute__((visibility("default"))) int _L(unsigned int) __asm__("__afl_persistent_loop"); _L(_A); }) -D__AFL_INIT()=do { static volatile char *_A __attribute__((used));  _A = (char*)"##SIG_AFL_DEFER_FORKSRV##"; __attribute__((visibility("default"))) void _I(void) __asm__("__afl_manual_init"); _I(); } while (0) /home/ash/code/afl-fuzz/afl-llvm-rt.o

这是afl-clang-fast最终传至clang的参数,包含AFL的运行时库,和一些宏,可以使用llvm mode下的一些额外功能,这部分代码在afl-llvm-rt.o.c中.

首先是初始化的代码

__attribute__((constructor(CONST_PRIO))) void __afl_auto_init(void) {

  is_persistent = !!getenv(PERSIST_ENV_VAR);

  if (getenv(DEFER_ENV_VAR)) return;

  __afl_manual_init();

}

__attribute__((constructor(CONST_PRIO)))所修饰的函数会在程序启动时优先调用,可以确定的是它是在main函数之前调用,可以定义多个这种类型的函数,其中CONST_PRIO表示执行的优先级,

首先获取环境变量PERSIST_ENV_VAR,赋值给is_persistent,用于标识persistent mode模式,然后获取环境变量DEFER_ENV_VAR,如果为true,直接return,这个标志的作用是标识是否延迟fork server

否则执行__afl_manual_init();函数,这个函数内部会初始化共享内存和fork server,下面会详细分析.

这样设计的目的是因为在deferred instrumentation模式下,需要延迟fork server,所以会自己定义初始化的位置.不需要自动初始化.

所以这段代码是为了兼容额外模式做的处理.

afl的文档中介绍了3种额外功能模式

1.deferred instrumentation

afl会只执行一次目标二进制文件,在执行某个位置之前停止,然后克隆进程进行持续而稳定的fuzz,这种fuzz的方式减少了大部分操作系统的链接和libc成本,据官方所说,某些情况下可以将性能提高10倍以上.

使用方法:

在代码中找到需要进行暂停,然后克隆进程的位置,需要注意的是,这个位置尽量避免访问和创建一些资源,比如创建线程进程,定时器,临时文件,网络套接字等.

在选好合适的位置后,添加如下代码,然后再使用afl-clang-fast重新编译,它是不支持afl-gcc和afl-clang的.

#ifdef __AFL_HAVE_MANUAL_CONTROL
  __AFL_INIT();
#endif
cc_params[cc_par_cnt++] = "-D__AFL_INIT()="
                              "do { static volatile char *_A __attribute__((used)); "
                              " _A = (char*)\"" DEFER_SIG "\"; "
                              #ifdef __APPLE__
                              "__attribute__((visibility(\"default\"))) "
    "void _I(void) __asm__(\"___afl_manual_init\"); "
                              #else
                              "__attribute__((visibility(\"default\"))) "
                              "void _I(void) __asm__(\"__afl_manual_init\"); "
                              #endif /* ^__APPLE__ */
                              "_I(); } while (0)";

在之前的afl-clang-fast.c中可以看到__AFL_INIT();实际执行的函数应是__afl_manual_init

void __afl_manual_init(void) {

  static u8 init_done;

  if (!init_done) {

    __afl_map_shm();
    __afl_start_forkserver();
    init_done = 1;

  }

}

如果没有进行初始化就会执行__afl_map_shm();,__afl_start_forkserver();进行初始化.

__afl_map_shm

static void __afl_map_shm(void) {

  u8 *id_str = getenv(SHM_ENV_VAR);

  /* If we're running under AFL, attach to the appropriate region, replacing the
     early-stage __afl_area_initial region that is needed to allow some really
     hacky .init code to work correctly in projects such as OpenSSL. */

  if (id_str) {

    u32 shm_id = atoi(id_str);

    __afl_area_ptr = shmat(shm_id, NULL, 0);

    /* Whooooops. */

    if (__afl_area_ptr == (void *)-1) _exit(1);

    /* Write something into the bitmap so that even with low AFL_INST_RATIO,
       our parent doesn't give up on us. */

    __afl_area_ptr[0] = 1;

  }

}

这个函数是获取共享内存的,前面afl-as中桩代码中也有这部分逻辑,就不会赘述了.

__afl_start_forkserver();

static void __afl_start_forkserver(void) {

  static u8 tmp[4];
  s32 child_pid;

  u8  child_stopped = 0;

  /* Phone home and tell the parent that we're OK. If parent isn't there,
     assume we're not running in forkserver mode and just execute program. */

  if (write(FORKSRV_FD + 1, tmp, 4) != 4) return;

  while (1) {

    u32 was_killed;
    int status;

    /* Wait for parent by reading from the pipe. Abort if read fails. */

    if (read(FORKSRV_FD, &was_killed, 4) != 4) _exit(1);

    /* If we stopped the child in persistent mode, but there was a race
       condition and afl-fuzz already issued SIGKILL, write off the old
       process. */

    if (child_stopped && was_killed) {
      child_stopped = 0;
      if (waitpid(child_pid, &status, 0) < 0) _exit(1);
    }

    if (!child_stopped) {

      /* Once woken up, create a clone of our process. */

      child_pid = fork();
      if (child_pid < 0) _exit(1);

      /* In child process: close fds, resume execution. */

      if (!child_pid) {

        close(FORKSRV_FD);
        close(FORKSRV_FD + 1);
        return;

      }

    } else {

      /* Special handling for persistent mode: if the child is alive but
         currently stopped, simply restart it with SIGCONT. */

      kill(child_pid, SIGCONT);
      child_stopped = 0;

    }

    /* In parent process: write PID to pipe, then wait for child. */

    if (write(FORKSRV_FD + 1, &child_pid, 4) != 4) _exit(1);

    if (waitpid(child_pid, &status, is_persistent ? WUNTRACED : 0) < 0)
      _exit(1);

    /* In persistent mode, the child stops itself with SIGSTOP to indicate
       a successful run. In this case, we want to wake it up without forking
       again. */

    if (WIFSTOPPED(status)) child_stopped = 1;

    /* Relay wait status to pipe, then loop back. */

    if (write(FORKSRV_FD + 1, &status, 4) != 4) _exit(1);

  }

}

首先设置child_stopped为0.

向通信描述符FORKSRV_FD + 1中写入数据,实质上这里存储的是后面fork server的状态.

然后开始循环

FORKSRV_FD中阻塞读取,读取到数据继续向下执行.

这里的FORKSRV_FDFORKSRV_FD + 1在afl-fuzz.c的init_forkserver函数进行了初始化,将st_pipe和ctl_pipe两个管道分别复制给了FORKSRV_FDFORKSRV_FD + 1.

FORKSRV_FD是用于控制fork server的通信管道.

FORKSRV_FD + 1内写入了一些fork server的状态信息,afl-fuzz.c内会进行读取.

这两个管道的作用会在afl-fuzz.c中体现.

判断child_stoppedwas_killed不为0,这两个值表示子进程是否停止和子进程是否被杀死.

如果为true,则表示子进程已经停止,并且之前被杀死,然后将child_stopped复位为0,并等待子进程退出状态.

继续判断child_stopped状态.

如果为0,则fork一个子进程,进行fuzz,释放当前管道资源,return.

如果为1,这是persistent mode的特殊处理,此时子进程处于暂停状态,通过kill(child_pid, SIGCONT);函数对 子进程进行重启.然后将child_stopped复位为0.

FORKSRV_FD + 1中写入子进程id,然后等待子进程结束.

persistent mode下进行特殊处理,该模式下,子进程会通过sigstop自行停止指示运行成功,这种情况下需要唤醒它进行fuzz,所以将child_stopped状态更改为1,会执行到之前的步骤,继续运行.

将状态写入至FORKSRV_FD + 1至,继续循环.

2.persistent mode

persistent mode模式在单个进程中通过测试用例进行fuzz,通过使用一些不影响上下文状态的api,和处理输入文件进行fuzz时,进行状态的重置,可以重用一个进程进行持续的fuzz,不需要fork新的进程,节省了资源的开销,提高了效率.

使用方式是通过下面的宏.

需要读取测试用例,调用目标模块,然后恢复一些状态,这些代码都要自己实现.

while (__AFL_LOOP(1000)) {
  /* Read input data. */
  /* Call library code to be fuzzed. */
  /* Reset state. */
}
/* Exit normally */

这种模式之前在一篇adobe漏洞挖掘的文章里见到过,使用该模式挖掘Jp2k类型图片的解析模块,挖掘者逆向了adobe解析jp2k图片的一个最底层的函数,逆向了该函数的参数,然后进行模拟构造,再进行调用,最大限度的提高效率,挖出了大量漏洞.

在afl-fuzz的文档说明循环次数最好为1000,目的是减少内存泄漏和一些其他问题带来的影响.

cc_params[cc_par_cnt++] = "-D__AFL_LOOP(_A)="
                              "({ static volatile char *_B __attribute__((used)); "
                              " _B = (char*)\"" PERSIST_SIG "\"; "
                              #ifdef __APPLE__
                              "__attribute__((visibility(\"default\"))) "
    "int _L(unsigned int) __asm__(\"___afl_persistent_loop\"); "
                              #else
                              "__attribute__((visibility(\"default\"))) "
                              "int _L(unsigned int) __asm__(\"__afl_persistent_loop\"); "
                              #endif /* ^__APPLE__ */
                              "_L(_A); })";

这里__AFL_LOOP(_A)对应的函数是__afl_persistent_loop

int __afl_persistent_loop(unsigned int max_cnt) {

  static u8  first_pass = 1;
  static u32 cycle_cnt;

  if (first_pass) {

    /* Make sure that every iteration of __AFL_LOOP() starts with a clean slate.
       On subsequent calls, the parent will take care of that, but on the first
       iteration, it's our job to erase any trace of whatever happened
       before the loop. */

    if (is_persistent) {

      memset(__afl_area_ptr, 0, MAP_SIZE);
      __afl_area_ptr[0] = 1;
      __afl_prev_loc = 0;
    }

    cycle_cnt  = max_cnt;
    first_pass = 0;
    return 1;

  }

  if (is_persistent) {

    if (--cycle_cnt) {

      raise(SIGSTOP);

      __afl_area_ptr[0] = 1;
      __afl_prev_loc = 0;

      return 1;

    } else {

      /* When exiting __AFL_LOOP(), make sure that the subsequent code that
         follows the loop is not traced. We do that by pivoting back to the
         dummy output region. */

      __afl_area_ptr = __afl_area_initial;

    }

  }

  return 0;

}

这块代码需要结合之前的代码整体看下,

执行顺序是afl_auto_init->__afl_manual_init->__afl_start_forkserver->__afl_persistent_loop

初始化共享内存,然后进行fork server

然后执行到这个函数__afl_persistent_loop:

函数参数是max_cnt,最大循环次数.

首先定义了两个变量

first_pass初始化为1,表示是否为第一次执行.

cycle_cnt表示剩余循环次数.

如果是第一次执行会,清空__afl_area_ptr,__afl_area_ptr[0]设置为1,然后将__afl_prev_loc设置为0.

然后将初始化cycle_cnt设置为最大循环次数.first_pass修改为0,return 1.

如果不是第一次执行.

判断is_persistent当前是否为persistent mode,不是return 0.

然后cycle_cnt减1,判断剩余次数是否为0.

如果不为0,则通过raise(SIGSTOP),让当前进程暂停,__afl_area_ptr[0]设置为1,然后将__afl_prev_loc设置为0.return 1.此时在fork server会设置child_stopped标志位,然后再下次时,会恢复之前的子进程执行.

如果为0,则将__afl_area_ptr指向__afl_area_initial,它是空的.

5.af-fuzz

# fuzzing # fuzz
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录