LibFuzzer入门:虚拟机搭建环境,手把手跑通模糊测试

发布时间:2026/10/11 2:50:43
LibFuzzer入门:虚拟机搭建环境,手把手跑通模糊测试 前阵子一个朋友给我看了段解析数据的代码说自己写了七八个单元测试边界值也都照顾到了可每次改动还是心慌。我说你别在用例上死磕了把它丢给LibFuzzer跑一晚上试试。第二天早上他发来消息凌晨三点崩了一个很隐蔽的数组越界被揪了出来。这就是我写这篇入门文的由来——Fuzzing模糊测试听起来很专业实际门槛没有想象中高尤其在LibFuzzer这种工具的帮助下新手完全可以在一个下午跑通全流程甚至复现出“自动找Bug”的效果。这篇文章我会用大白话讲清楚Fuzzing的核心原理从零开始带你在虚拟机里搭好实验环境标题承诺的虚拟机教程绝不敷衍装好编译链写出第一个fuzz target再故意埋一个Bug把“发现崩溃、定位根因、修复验证”整条链路走一遍。最后还会聊聊语料库、字典、并行控制这些能直接提升效率的进阶手法。全文不依赖图形界面点来点去但尽量照顾到纯小白所有命令都有逐行解释——你只要跟着敲基本都能跑通。1. 为什么入门Fuzzing我首推LibFuzzer1.1 Fuzzing到底在做什么用海量“刁钻输入”逼程序现形我们平时写测试用例本质上是在替程序“提问”这个输入合理吗那个输入越界了吗但人的想象力有限尤其面对复杂的格式解析、网络协议、文件读取这类代码边界情况多到根本枚举不完。Fuzzing的思路恰恰相反——它不靠人想而是靠程序自动生成海量输入喂给目标函数观察程序会不会崩溃、断言失败、内存越界或者超时。LibFuzzer的战略可以用一句话概括不按套路出牌用机器最擅长的方式去试错。它会随机生成大量输入也会在已有输入上做变异比如翻转几个字节、插入一段数据、拼接已知的“危险片段”。这些看起来毫无逻辑的输入恰恰是手工测试最容易漏掉的“暗角”。有个很贴切的类比普通测试像导游带游客走固定路线Fuzzing则像放一群精力旺盛的野马在草原上乱跑它们踩到陷阱的概率自然高得多。尤其在解析类代码里一个字节的错位就可能让后续逻辑彻底失控这种问题靠眼睛很难看出来但机器一晚上能试几百万次。1.2 主流Fuzzing引擎的三条技术路线圈子里流行的Fuzzing引擎不止一个常见的还有AFL及其后继AFL和Honggfuzz。它们的目标一致但实现思路差别很大。引擎运行模型插桩方式上手门槛典型场景LibFuzzer进程内in-process编译期LLVM插桩低一条命令编译库函数、内存型解析代码AFL进程外out-of-process编译期汇编级别插桩中需要处理stdin/文件文件解析、外部可执行程序Honggfuzz进程外为主也支持进程内动态插桩硬件反馈中高配置项多需要更细粒度反馈的场景简单解释一下“进程内”和“进程外”的区别。AFL会在外部反复启动你编译好的程序每次崩溃都会另起一个进程隔离性很好但进程启动开销很大。LibFuzzer则直接把fuzz逻辑和被测代码链接进同一个进程在内存里反复调用目标函数吞吐量高一个量级代价是如果被测代码本身有严重问题可能把整个进程带垮——所以官方一直建议配合ASANAddressSanitizer一起用这正好也是新手最好的安全网。1.3 对新手最友好的几个设计我推荐LibFuzzer作为入门首选不是因为AFL不好而是因为它把你从一堆琐事里解放了出来不用管输入通道。AFL需要程序从stdin或者文件读取输入你得先为被测代码写一层“文件转内部逻辑”的胶水代码LibFuzzer直接提供一个入口函数给你输入以内存指针的方式传进来少了很多弯路。集成度极高。只要Clang版本足够新-fsanitizefuzzer这一个编译参数就能激活全部核心功能不需要单独安装任何工具。与ASAN天然联动。内存越界、释放后使用、空指针这类Bug在LibFuzzer环境下几乎都会被ASAN准确拦下来并输出带堆栈回溯的崩溃现场定位效率非常高。我的建议很直接如果你将来想深入理解覆盖率引导、变异策略这些底层机制LibFuzzer的源码和LLVM是一体的学习路径非常顺如果你需要测试一个已经有完整可执行文件的外部程序再去研究AFL也不迟。2. 虚拟机实验环境搭建从镜像选择到共享剪贴板全流程2.1 为什么非要先装虚拟机而不是直接在宿主机上跑有读者可能会问我就想试试Fuzzing在本地装个Clang直接编译不就行了行但我不推荐。第一Fuzzing过程中被测代码随时可能崩溃。虽然ASAN能拦截大部分内存错误但被测试的代码如果出现死锁、极端内存膨胀甚至直接触发内核级的问题在虚拟机里发生只影响虚拟环境重启一个快照就能恢复在宿主机上则可能把你正在编辑的资料全部带走。第二环境是脏的。很多开发机里装了旧版GCC、多种Python版本、各种环境变量和Clang的插桩工具链混在一起容易出幺蛾子。虚拟机能给你一个干净、可重复的实验环境将来哪一步搞坏了删除重来就行。第三方便快照回归。我在调试崩溃样本时经常需要对比“修改前”和“修改后”的行为。虚拟机支持多份快照一个快照放Bug版本另一个放修复版本切换起来非常方便。对真要拿Fuzzing干活的团队来说CI上跑Fuzzing也通常使用隔离的容器或虚拟机现在先在本地把这套思维养起来后面迁移到自动化流程会平滑很多。2.2 虚拟机软件选型与关键参数设置虚拟机软件我推荐VirtualBox原因是开源免费、跨平台、资料多。如果你公司有VMware的商业授权VMware也没有问题操作思路基本一样。创建虚拟机的时候有几个参数直接决定后面的体验操作系统类型Linux / Ubuntu (64-bit)。内存建议至少分配4GB最好8GB。LibFuzzer跑起来后ASAN需要额外内存开销默认上限约2GB如果机器内存太小容易直接被杀。CPU分配2到4个核。后面如果想体验-jobs4并行Fuzzing至少要给4个核。磁盘建议固定或动态分配40GB以上。Ubuntu系统本身占用不大但Fuzzing会产生大量语料库和崩溃样本空间充裕一点心里不慌。系统镜像我建议直接用Ubuntu LTS版本比如Ubuntu 22.04的桌面版。有人喜欢Server版省资源但新手在纯命令行里处理网络、挂载、权限问题会多一重心理负担桌面版能极大降低挫败感。真机跑Fuzzing一般用Server版但那是后话入门阶段没必要和自己过不去。2.3 Ubuntu安装与增强功能配置少走几个冤枉路虚拟机创建好后挂载下载好的Ubuntu ISO镜像启动进入安装流程。语言、键盘、时区这些按默认走即可注意在“安装类型”里选择“清除整个磁盘”并安装——这里的“磁盘”是虚拟磁盘不会影响宿主机。Ubuntu安装完成后第一件事是更新软件源并安装基础工具。打开终端执行sudo apt update sudo apt upgrade -y sudo apt install -y build-essential git curl这里提前把build-essential装好因为后面编译代码和编译VirtualBox增强功能都要用到。接下来装VirtualBox增强功能Guest Additions它主要解决两个问题让鼠标可以不按特殊键自由进出虚拟机窗口让虚拟机分辨率跟随窗口自动调整。在虚拟机窗口菜单里点击“设备” - “安装增强功能”Ubuntu会挂载一张光盘镜像然后执行sudo mount /dev/cdrom /mnt cd /mnt sudo ./VBoxLinuxAdditions.run --nox11 sudo reboot我实测中最常见的失败原因是没有提前安装linux-headers-$(uname -r)增强功能的编译依赖它。如果上述命令报错先执行sudo apt install linux-headers-$(uname -r) dkms再重试。增强功能装好后建议再配置共享文件夹方便宿主机和虚拟机传代码。在VirtualBox的“设置 - 共享文件夹”里添加一个目录比如把宿主机上的~/fuzzshare共享为fuzzshare在Ubuntu里挂载mkdir -p ~/share sudo mount -t vboxsf fuzzshare ~/share这样你可以在宿主机上下载工具包、写代码在虚拟机里直接读取省去来回复制。2.4 快照规划与性能提醒虚拟机装好后趁环境还干净建议立刻打一个快照。拿VirtualBox来说在“快照”面板里给当前状态起个名字就行比如install-done。**快照是廉价保险别舍不得打。**后面每完成一个阶段装好Clang、跑通第一个target、复现崩溃等都可以补一个快照。万一后续操作把环境搞乱了几秒钟回到可用状态。性能上有一个大家容易忽略的点给虚拟机分配的内存不要超过宿主机物理内存的一半不然宿主机会开始用交换空间整机变慢后虚拟机反而跑得更差。LibFuzzer是计算密集型任务如果发现虚拟机风扇狂转、编译明显变慢先检查是不是内存分多了再检查是不是后台同时跑着太多编译任务。3. 工具链安装与第一个fuzz target编译命令逐行拆解3.1 安装Clang版本对了问题就少一半LibFuzzer是LLVM项目的一部分直接集成在Clang编译器里所以核心依赖只有Clang一个。Ubuntu 22.04默认软件源的clang版本是14这个版本对LibFuzzer的日常使用完全够用。安装命令sudo apt install -y clang clang --version看到类似clang version 14.0.0的输出即安装成功。如果你的发行版较老apt默认给的是clang 6.0甚至更旧那LibFuzzer可能没有被集成需要手动安装新版本比如clang-14然后用clang-14命令替代clang。这一点后面避坑章节还会展开说。顺便把llvm-symbolizer装上它对ASAN崩溃堆栈的符号化很有帮助sudo apt install -y llvm3.2 一个标准fuzz target长什么样LibFuzzer要求你提供一个入口函数名字固定叫LLVMFuzzerTestOneInput接收const uint8_t *data和size_t size两个参数输入数据就是Fuzzer不断变异出来的字节流。下面我用一个简化的“按逗号切分字段”的解析函数做演示它接收一段字节流统计逗号数量。代码很简单但骨架能直接用到真实项目里#include stdint.h #include stddef.h // 被测函数统计输入数据里逗号的数量 static int CountCommas(const uint8_t *data, size_t size) { int count 0; for (size_t i 0; i size; i) { if (data[i] ,) { count; } } return count; } // LibFuzzer统一入口 int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { CountCommas(data, size); return 0; }注意几个要点入口文件不要写main函数。-fsanitizefuzzer编译时LibFuzzer会自动生成main你写了反而报重复定义。被测函数里不要用printf刷屏。Fuzzer每秒调用成千上万次任何打印都会拖垮速度还会污染日志。不要依赖全局状态做逻辑判断。同一个进程会被反复调用全局变量会在多次调用间残留容易引入“每次都不同”的假Bug后面避坑章节我会用专门篇幅讲。3.3 编译命令逐项解释把上面的代码保存为fuzz_csv.c然后执行clang -fsanitizefuzzer,address -g -O1 fuzz_csv.c -o fuzz_csv这一条命令信息量很大拆开看-fsanitizefuzzer这是核心Clang会自动链接LibFuzzer runtime并生成一个main函数这个main负责初始化、循环生成输入、调用LLVMFuzzerTestOneInput、处理崩溃转储。-fsanitizeaddress开启ASAN。一旦被测代码发生堆越界、栈越界、释放后使用、空指针解引用等问题ASAN会立即拦截并打印详尽的错误报告。它和fuzzer可以共存实际项目里几乎必开。-g生成调试信息。没有它崩溃堆栈里只有函数地址符号化和定位会麻烦很多。-O1优化级别。官方文档明确建议用-O1或-O2优化级别太低会导致很多问题模式发生变化太高则可能让某些错误被编译器优化掉-O1是覆盖率反馈和错误保真度之间的平衡点。编译完成后当前目录会出现一个名为fuzz_csv的可执行文件。3.4 第一次运行看懂启动日志运行它./fuzz_csv你会看到类似这样的输出INFO: Running with entropic power schedule (default) INFO: Seed: 3347809551 INFO: Loaded 1 modules INFO: Loaded 0 corpora filesSeed本次运行的随机种子。每次运行都会不同也可以手动指定-seed12345复现某次运行过程。Loaded 0 corpora files表示还没有提供初始语料库LibFuzzer会从零开始纯随机生成输入。随后日志会持续滚动#2 NEW cov: 12 ft: 18 corp: 1/1b exec/s: 1200 rss: 28MB #4 NEW cov: 14 ft: 25 corp: 2/3b exec/s: 1500 rss: 29MB这里cov表示当前代码覆盖率ft是边数corp是语料库大小exec/s是每秒执行次数。这个CountCommas函数足够简单覆盖很快会饱和。正常情况下让它跑几分钟都不会崩溃因为代码里没有会导致内存错误的问题——但这就是我们想要的基线行为。提示如果你看到ERROR: AddressSanitizer failed to allocate ...类似报错说明虚拟机的内存或ASAN的内存映射受限后面避坑章节会专门讲。4. 故意埋一个Bug把崩溃定位与修复流程完整走一遍4.1 制造一个必现的数组越界场景为了让读者真正看到“发现崩溃”到“修复验证”的全过程我们现在故意往代码里埋一个经典Bug用输入数据的第一个字节作为数组索引并且不做边界检查。这是很多C/C越界问题的真实写照——开发者想当然地以为输入值一定在某个范围内。#include stdint.h #include stddef.h static int ParseByte(const uint8_t *data, size_t size) { if (size 4) return -1; int table[8] {0}; // 故意不检查 data[0] 的范围 int idx data[0]; table[idx] 1; return size; } int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { ParseByte(data, size); return 0; }保存为fuzz_demo.c编译clang -fsanitizefuzzer,address -g -O1 fuzz_demo.c -o fuzz_demo运行./fuzz_demoFuzzer很快会生成一个data[0]大于等于8的输入。因为uint8_t的取值范围是0到255越界概率接近97%用不了多久就会崩。4.2 崩溃输出的每个字段意味着什么当我实际跑这个例子时几秒钟之内就看到类似下面的输出ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd... at pc ... READ of size 4 at 0x7ffd... thread T0 #0 0x... in ParseByte fuzz_demo.c:11:9 #1 0x... in LLVMFuzzerTestOneInput fuzz_demo.c:16 #2 0x... in LLVMFuzzerOutput... ... Test unit written to ./crash-2fd6... Base64: AQAAAAAAAAA逐段解读AddressSanitizer: stack-buffer-overflowASAN判定这是栈缓冲区越界。如果是堆越界会显示heap-buffer-overflow释放后访问则显示use-after-free这个描述基本能让你第一时间知道内存错误类型。READ of size 4程序试图读取4个字节因为table[idx] 1读的是int而目标地址超出了栈缓冲区范围。ASAN精确到读写大小对判断越界跨度很有帮助。#0 ... in ParseByte fuzz_demo.c:11:9堆栈第一帧直接指向代码文件fuzz_demo.c的第11行第9列。这就是-g调试信息带来的好处没有它你只能看到一个裸地址。Test unit written to ./crash-2fd6...LibFuzzer把触发崩溃的最小输入样本保存到了当前目录文件名以crash-开头。这个文件是宝贝后面复现和回归都靠它。有一点要强调崩溃样本的内容只有8个字节Base64解码后是AQAAAAAAAAA也就是第一个字节为1、后面7个字节为0它短到让人觉得“就这也能崩”。但这就是Fuzzing的价值——它能用极小的输入精确命中你代码里的弱点。4.3 手动复现不看日志也能让程序崩给你看拿到崩溃样本后我们可以脱离Fuzzer直接用可执行文件喂入这个文件来复现./fuzz_demo crash-2fd6*注意LibFuzzer编译出的程序有一个特殊行为如果运行时传入文件路径作为参数它不会进入Fuzzing循环而是直接把文件内容交给LLVMFuzzerTestOneInput。这相当于一个“单测模式”对复现和回归非常有用。执行后你会看到同样的ASAN报错。到这一步问题已经可以稳定复现说明崩溃根因不依赖于Fuzzer的随机性是真实存在的bug。4.4 修复与回归验证修复越界很简单加一个范围检查static int ParseByte(const uint8_t *data, size_t size) { if (size 4) return -1; int table[8] {0}; int idx data[0]; if (idx 0 || idx (int)(sizeof(table) / sizeof(table[0]))) { return -1; } table[idx] 1; return size; }重新编译并直接传入原来的崩溃样本验证clang -fsanitizefuzzer,address -g -O1 fuzz_demo.c -o fuzz_demo_fixed ./fuzz_demo_fixed crash-2fd6*此时程序应正常返回无任何ASAN输出。再把它拿回Fuzzer里运行确认不再产生崩溃mkdir corpus cp crash-2fd6* corpus/ ./fuzz_demo_fixed corpus/把崩溃样本放进语料库是一个极其重要的习惯。一方面验证修复是否真的覆盖了该样本另一方面让Fuzzer在这些“历史上崩溃过的输入”基础上继续变异防止同类问题换个马甲卷土重来。5. 让Fuzzing真正高效的三个进阶动作种子、字典与并行控制5.1 语料库种子决定覆盖率起点的第一块拼图我们前面跑的例子都是“零种子”启动LibFuzzer只能从头随机生成输入就像让一个从没见过逗号的机器猜“这是什么”。真实项目里你手里往往已经有正常流量、合法样本、历史Bug输入这些都应该做成种子目录让Fuzzer站在一个更高的起点上。在项目目录建一个corpus/把你认为“正常且能走到较深逻辑”的输入放进去。对于CSV解析器可以放几行正常CSV、带引号的字段、换行符乱飞的边缘样本。LibFuzzer会先读取所有种子然后从中变异出新样本。有个经验值种子文件不要太大几百字节到几十KB都合适。种子动辄几MB会拖慢变异效率LibFuzzer还提供了压缩命令./fuzz_csv -merge1 corpus corpus_min-merge1会把corpus里所有能增加覆盖率的样本合并进corpus_min那些冗余的重复样本会被自动剔除。定期做一次语料库最小化是保持Fuzzer长期高效运行的好习惯。5.2 字典文件让变异不再“盲人摸象”纯随机变异生成的输入大部分是垃圾字符。对文本格式、协议关键字这类场景直接告诉Fuzzer“你应该多尝试这些token”能大大提升效率。LibFuzzer的字典文件格式很简单就是一系列token字符串, \ \r \n 0x true false NULL保存为csv.dict运行时用-dictcsv.dict指定./fuzz_csv -dictcsv.dict corpus/字典的价值在于把变异方向从“逐字节瞎改”变成“结构相关词条的组合”。比如HTTP头解析器字典里可以放Content-Type、Content-Length、Transfer-Encoding这些tokenJSON解析器可以放{、}、[]、:这些结构符号。用不用字典对格式类代码的覆盖率差距可能是成倍的。5.3 并行与资源控制跑得久也要跑得稳Fuzzing默认是单进程但现代CPU多核不用白不用。LibFuzzer支持-jobs和-workers参数配合工作比如./fuzz_csv -jobs4 -workers4 corpus/这样会并行启动4个Fuzzing worker各自独立变异主进程负责任务调度。实测在4核虚拟机上吞吐量大约能翻三到四倍。不过并行模式对内存的要求也更高4个ASAN进程同时跑内存占用轻松超过8GB虚拟机的内存分配要提前规划好。资源控制参数我几乎每次都会带上./fuzz_csv -timeout25 -rss_limit_mb2048 -max_len4096 corpus/-timeout25单个输入执行超过25秒就视为超时直接终止该输入并记录。-rss_limit_mb2048进程内存超过2GB就重启防止内存无限膨胀拖垮系统。-max_len4096限制单个输入的最大长度避免Fuzzer生成几十MB的巨型样本导致解析耗时激增。这些参数表面是“限制”实际是“保护环”——它们保证Fuzzer长时间运行时即使被测代码出现极端资源消耗也不会让整个虚拟机卡死。另外一个实用小参数是-print_final_stats1运行结束后会输出完整的统计信息包括覆盖率、语料库大小、崩溃和超时的数量。跑完一阶段后看一眼这个数据比盯着滚动日志更能判断“这次Fuzz值不值”。6. 实际动手之后才发现的几个坑与我的最终建议6.1 工具链相关的三个经典版本坑第一个坑是最容易被忽略的编译器版本过低。我见过有朋友在旧系统上执行apt install clang装的是4.9或者6.0的老版本。那个年代的Clang对LibFuzzer的支持要么相当别扭、要么根本编译不出可执行文件。建议装新一点的系统或者显式安装clang-14。判断标准很简单clang --version如果低于7就不要纠结升级系统或者用官方源安装新版。第二个坑是遗漏-g导致堆栈不可读。没有调试信息时ASAN的堆栈全是地址加偏移量你需要额外跑addr2line才能定位到源码行非常影响效率。我在实际排查时习惯把-g -O1当作铁律不写全不要开始Fuzz。第三个坑是ASAN内存映射问题。在一些比较老的内核或容器环境里ASAN初始化会报failed to allocate。解决办法是给虚拟机的内存映射放宽限制或者在编译命令中尝试-fsanitize-address-use-odr-indicator等兼容参数。这个坑最头疼的地方在于它和环境强相关搜到同样的报错也不一定同一个解法所以我的建议是先把Ubuntu 22.04这种主流版本环境跑通再去折腾其他发行版。6.2 fuzz target写法相关三条我踩过的红线第一入口函数里不要用stdin读输入。LibFuzzer的输入通道直接走内存指针你写了从标准输入读数据的逻辑Fuzzer根本不会喂给那个通道反而会卡在读入步骤造成“看起来像死循环”的假象。需要读文件类外部程序的逻辑时提前做一层适配把文件内容读到内存再传给LLVMFuzzerTestOneInput。第二小心全局状态污染。被测代码如果依赖static变量或者全局缓存第一次调用可能一切正常第二次、第三次就会因为状态残留走出和在单测里完全不同的路径。某些时候它甚至能掩盖Bug——第一次调用没事后面调用才开始崩你很难判断根因。推荐的姿势是被测函数尽量无状态真有不可避免的全局状态也要在每次调用开始处重置。第三不要写复杂的main函数来包装。LibFuzzer自动生成的main已经处理好参数解析、语料加载、崩溃保存这些逻辑你只要一心写好LLVMFuzzerTestOneInput即可。我见过有人为了“定制启动流程”手写main结果把LibFuzzer内部机制绕得乱七八糟反而要花几个小时调试。6.3 工作流建议不是所有代码都值得砸时间Fuzz最后说点个人体会。Fuzzing不是万能药它最适合处理的是“输入直接决定控制流”的代码比如文件格式解析、网络报文解析、压缩解压、正则引擎、序列化反序列化。而业务逻辑复杂、大量外部依赖、需要复杂前置状态的代码让Fuzzer从头猜状态非常费劲——那种场景更适合配合结构化语料和自定义mutator去玩那是进阶话题。如果你想把LibFuzzer接到真实项目里我的建议是先挑一个足够小的解析模块下手比如项目内某个JSON配置解析器或一个自定义协议的decode函数写好target跑一个晚上看清覆盖率曲线再决定要不要扩大范围。拿我自己的经验来说第一次看到“崩了一个我自己完全没想到的输入”时那种感觉和看别人演示Demo完全不同——这不是环境配置成功了而是这套方法论真正开始替你工作了。另外提醒一句任何Fuzzing产出都应纳入回归体系。把崩溃样本放进项目的测试用例库里让每次CI都能自动重放这些历史崩溃输入这才是Fuzzing价值能持续保持的原因。我在实际项目中就是这么做的一次Fuzz跑出的10个样本比一部分手写的单元测试更“值钱”因为它们每个都代表了一条曾经真实击穿代码边界的路径。最后一个真诚的建议入门阶段千万别追求“跑得久”先追求“看得懂”。把一次只跑30秒的小Fuzz跑通把崩溃样本手动复现一次把ASAN日志从头读到尾这比挂着Fuzzer跑一整晚但完全不知道日志在说什么有用得多。等你真的看懂了那些日志LibFuzzer就不再是个黑盒子了——那时候你已经可以把它当工具使而不是当魔术看。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询