QSYM 内存模型深度剖析符号数据如何从输入流转到分支条件【免费下载链接】qsymQSYM: A Practical Concolic Execution Engine Tailored for Hybrid Fuzzing项目地址: https://gitcode.com/gh_mirrors/qs/qsymQSYM 是一款为混合模糊测试Hybrid Fuzzing量身定制的实用化符号执行引擎Concolic Execution Engine它的核心秘密就藏在内存模型里。本文从零开始剖析 QSYM 内存模型带你完整看懂一条符号数据从输入文件流入内存、随指令传播、最终生成分支条件的全过程适合想理解符号执行引擎工作原理的新手与安全研究者。先认识 QSYM混合模糊测试背后的符号引擎传统模糊测试Fuzzing靠随机变异输入来碰运气而符号执行能把分支条件翻译成数学约束直接算出能走到新路径的输入。QSYM 的定位是实用化它不像经典符号执行那样追求 100% 的符号化而是刻意把大量不划算的操作直接具体化只对值得求解的分支投入算力从而在真实程序上跑出远超传统方案的速度。QSYM 的整体链路可以概括为四步输入符号化——通过系统调用挂钩把输入字节变成符号变量指令级传播——基于 Intel PIN 动态插桩让符号随机器指令流动分支约束生成——在条件跳转处构造表达式并交给求解器反路径求解——否定当前路径条件生成能探索新代码的输入。接下来我们沿着这四步逐一拆解 QSYM 内存模型。QSYM 内存模型一张按页组织的符号表QSYM 内存模型的核心类Memory定义在 memory.h 中实现位于 memory.cpp。它的设计非常巧妙为进程的每一页内存维护一张表达式指针表。std::unordered_mapADDRINT, ExprRef* page_table_; ExprRef* stack_page_; // 栈页 ExprRef* unmapped_page_; // 未映射页统一哨兵 ExprRef* zero_page_; // 零页vdso 等只读区 ExprRef* brk_page_; // 堆brk区域地址按kPageShift 12右移得到页索引页内偏移用addr kPageMask得到每个地址对应一个ExprRef也就是一个表达式指针——NULL 表示该内存是具体值Concrete非 NULL 表示它承载着符号表达式未映射、栈、堆、vdso 等区域各有专门的页对象isUnmappedAddress通过比对页指针即可 O(1) 判断地址是否可读。这套设计把符号内存和真实内存松耦合真实内存由进程自己管理QSYM 只维护一张平行的符号影子表。程序怎么读写内存影子表就怎么记录符号互不干扰。第一步输入数据如何变成符号变量QSYM 通过 libdft 拦截read、pread64、readlink等系统调用见 syscall_desc.cpp 中__NR_read对应的postReadHook。当一个输入文件被读入缓冲区后QSYM 会在该缓冲区上调用makeExpr(addr, size)inline void makeExpr(ADDRINT addr) { ExprRef e g_expr_builder-createRead(off_); // 取一个递增的输入偏移 setExprToMem(addr, e); }这里出现了 QSYM 内存模型的第一个关键角色——Read表达式。ReadExpr定义在 expr.h就是输入文件的第 i 个字节的化身它内部只保存一个index_对应输入文件偏移并且把isConcrete_置为 false表示这是一个真正的符号变量。每个输入字节对应一个唯一的 Read 表达式这一一映射正是符号数据的源头。第二步符号如何沿指令传播有了符号源头接下来就是 QSYM 的核心工作——指令级符号传播。QSYM 用 XED 解码每条指令analysis.cpp 中的analyzeInstruction把机器指令翻译成语义操作例如ADD→Add表达式同时更新 EFLAGS 标志位CMP→Sub表达式用于后续条件跳转MOV→ 直接搬运寄存器/内存的表达式。传播过程由两个模块协同完成插桩回调instrument.cpp负责在每条指令前/后提取表达式并计算结果表达式构建器expr_builder.cpp负责构造新的表达式节点。多字节读写时内存模型会逐字节取出表达式再拼接。getExprFromMem(addr, size)的实现逻辑是对每个字节分别取表达式NULL 则用真实字节值构造常量再通过createConcat按大端序拼接成一个完整的位向量表达式。反之setExprToMem(addr, size, e)会把一个宽表达式用createExtract切分成逐字节的子表达式写回影子表。表达式节点本身构成一棵有向无环图DAG例如x 1 8会表示成Ult( Add( Read(3), Constant(1) ), Constant(8) )每个节点记录bits()位宽、子节点、哈希值和依赖集。所有非叶子表达式都维护uses_反向引用一旦某个Read表达式被具体化concretize()依赖它的上层表达式会沿 DAG 自底向上级联具体化这是 QSYM 高效化简的关键机制之一。第三步分支条件如何从内存长出来当程序执行到条件跳转Jcc指令时QSYM 的插桩回调instrumentJccinstrument.cpp会被触发。它读取 EFLAGS 寄存器中对应的标志位表达式CF/ZF/SF/OF 等依据跳转类型组合出完整的分支条件表达式然后调用g_solver-addJcc(e, taken, pc);solver.cpp中的addJcc是决策核心若表达式已具体化直接忽略不浪费求解器通过isInterestingJcc判断该分支是否值得求解——QSYM 会结合 AFL 的覆盖率位图bitmap来判断这条路径是否探索过只有有趣的分支才进入求解流程对有趣分支调用negatePath把当前路径约束取反交给 Z3 求解新输入把当前分支约束加入路径条件栈addConstraint供后续分支继续累积。negatePath会借助依赖森林dependency.cpp中的DependencyForest只重放与该分支相关的约束而不是全部路径约束这是 QSYM 相对早期符号执行工具的重大性能优化——把重放整条路径降级为重放相关子路径。第四步求解与静默具体化策略求解阶段由 Z3 的QF_BV无量词位向量求解器完成超时 10 秒见 solver.cpp 的kSolverTimeout。求解成功就把新输入写入输出目录供下一次模糊测试迭代使用。但 QSYM 内存模型真正的精髓是实用主义它并不强行让所有数据都保持符号化。遇到求解代价过高的运算如复杂除法、非平凡非线性运算时concretizeMem/concretizeReg会执行静默具体化——把符号表达式替换成当前真实值并把Equal(expr, value)作为约束记录下来g_solver-addValue从而既不丢精度又大幅降低求解压力。此外还有三层轻量优化支撑性能范围约束Range Setexpr.h中每个表达式维护有符号/无符号两个区间集合简单比较可以直接用区间判定绕开 Z3表达式缓存Cacheexpr_cache.cpp用哈希去重相同表达式避免 DAG 无限膨胀交换律归一化构建器责任链Commutative → Symbolic → Common → ConstantFolding → Cache → Base见 expr_builder.cpp保证x1与1x归并为同一表达式。一张图看懂符号数据流把上面四步串起来QSYM 内存模型中的数据流就是一条清晰的流水线阶段关键模块数据形态输入注入syscall hook makeExpr输入字节 →Read表达式内存建模Memory::setExprToMem影子页表中存下符号指令传播instrument expr_builder表达式 DAG 生长分支提取instrumentJcc EFLAGS条件表达式约束求解Z3 negatePath新输入文件小结QSYM 内存模型的三条设计哲学回顾整条链路QSYM 内存模型的设计哲学可以浓缩为三点影子内存按页并行用页表结构给真实内存照镜子符号与具体值互不干扰懒符号化 主动具体化只在系统调用处生成符号遇到昂贵运算果断具体化并保留约束求解最小化依赖森林、区间传播、表达式缓存三管齐下把 Z3 调用次数压到最低。对于想深入学习符号执行或混合模糊测试的读者建议顺着 memory.h → expr.h → instrument.cpp → solver.cpp 的顺序精读源码你会发现 QSYM 内存模型的每一处设计都在回答同一个问题如何用最小的求解代价让符号数据走完从输入到分支的最后一公里。【免费下载链接】qsymQSYM: A Practical Concolic Execution Engine Tailored for Hybrid Fuzzing项目地址: https://gitcode.com/gh_mirrors/qs/qsym创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考