尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

CTF逆向工程中Z3求解器的应用:从数学约束到自动求解

CTF逆向工程中Z3求解器的应用:从数学约束到自动求解 1. 项目概述当CTF逆向遇上Z3一种降维打击的解题思路在CTFCapture The Flag的逆向工程赛题里我们常常会遇到一种让人又爱又恨的题目程序逻辑清晰但核心验证部分是一堆复杂的数学约束或位运算。手动推算费时费力还容易出错爆破当变量空间稍微大一点就变成了不可能完成的任务。这时候如果你还在一行行读汇编或者试图在脑子里模拟寄存器状态那可能就有点“用蛮力对抗数学”了。我干了十多年安全研究从早期的纯手动逆向到后来引入各种自动化工具最深的一个体会就是解题效率的跃升往往来自于思维工具的升级。而Python的Z3求解器就是这样一个能让你在逆向题上实现“降维打击”的神器。简单来说Z3是一个由微软研究院开发的高性能定理证明器或者说SMT可满足性模理论求解器。你可以把它理解为一个“超级数学引擎”。你不需要知道方程怎么解你只需要告诉它变量之间的关系比如x y 100且x * y 2000它就能自动帮你算出x和y可能是多少。在CTF逆向中程序的验证逻辑本质上就是一系列对输入flag的约束条件。我们的目标就是把汇编或代码中的这些约束“翻译”成Z3能理解的数学语言然后让它替我们找出满足所有条件的那个唯一解——也就是正确的flag。这篇文章我就以一个老逆向手的视角带你彻底搞懂如何用Z3来“优雅”地解决那些令人头疼的逆向题。我们会从Z3的核心概念讲起然后通过几个由浅入深的实战案例手把手教你如何将逆向代码“建模”成Z3约束并最终自动求解。无论你是刚接触CTF的新手还是苦于某些复杂约束题的老手相信这套方法都能极大拓宽你的解题工具箱。2. Z3求解器核心概念与逆向思维转换在深入实战前我们有必要统一一下“语言”。Z3的思想和传统的调试、爆破截然不同理解这种思维转换是成功运用的关键。2.1 什么是SMT为什么它适合逆向SMT全称Satisfiability Modulo Theories中文叫“可满足性模理论”。这个听起来很学术的词拆开理解就简单了可满足性给定一系列逻辑命题判断是否存在一种赋值让每个变量取一个值能使所有命题同时为真。模理论这里的“理论”指的是一套预定义的规则和对象比如整数算术理论支持加减乘除、比较、位向量理论支持与或非、移位、数组理论等。Z3内置了这些理论所以我们不用从最基本的逻辑门开始描述问题。在逆向题中程序对输入通常是一个字符串或一串字节的检查无非就是一系列操作取某些位进行加减、异或、比较或者进行复杂的多项式运算。这些操作完美对应了Z3的位向量BitVec和整数算术理论。我们的核心任务就是从程序的执行流中提取出这些关于输入变量的等式或不等式约束。举个例子你看到一段代码if (input[0] input[1] 0x88 input[0] ^ input[1] 0x42)。传统思路是动态调试输入各种值去碰。而Z3思路是定义两个符号变量a BitVec(‘a‘, 8),b BitVec(‘b‘, 8)然后添加约束a b 0x88和a ^ b 0x42最后让Z3求解a和b。思维从“试错”变成了“描述问题并求解”。2.2 Z3-Python基础符号变量、求解与模型在Python中使用Z3首先得安装pip install z3-solver。它的核心对象就几个符号变量这是我们要求解的未知数。在逆向中它们通常代表flag的每一个字节。from z3 import * # 定义一个8位一个字节的位向量符号变量名为‘byte0‘ byte0 BitVec(‘byte0‘, 8) # 也可以一次定义多个比如flag长度为20 flag [BitVec(f‘f{i}‘, 8) for i in range(20)]这里用BitVec而不是Int是因为程序处理单字节时是模256的运算溢出后回绕BitVec(8)能精确模拟这种特性。Int是无限精度的整数不适合模拟单字节运算。求解器与约束Solver()对象就像一个容器我们不断向里面添加约束条件。s Solver() # 添加约束第一个字节必须是字符 ‘c‘ (ASCII 99) s.add(flag[0] ord(‘c‘)) # 添加约束第一个字节和第二个字节之和为200 s.add(flag[0] flag[1] 200) # 添加约束所有字节必须是可打印字符ASCII 32-126 for f in flag: s.add(f 32, f 126)求解与获取模型添加完所有约束后让求解器工作。if s.check() sat: # 如果可满足sat m s.model() # 获取一个满足条件的模型解 # 从模型中提取每个符号变量的具体值 result [] for f in flag: result.append(m[f].as_long()) # 将解转换为整数 flag_str bytes(result).decode(‘ascii‘, errors‘ignore‘) print(f“Found flag: {flag_str}“) else: print(“No solution found.“)s.check()返回sat可满足、unsat不可满足或unknown无法判定。s.model()返回一个具体的解。如果有多个解可以通过循环s.check()并在每次找到解后添加排除条件s.add(Or([f ! m[f] for f in flag]))来寻找其他解但在CTF中flag通常是唯一的。实操心得一变量定义的粒度刚开始用Z3最容易犯的错误就是变量定义得太粗。比如一个32位的整数参与了一系列位操作你是把它定义成一个BitVec(32)的变量还是定义成4个BitVec(8)的变量这取决于题目的操作。如果题目是像eax (eax 13) | (eax 19)这样的循环移位那必须用32位变量来模拟。如果题目是mem[0] al; mem[1] ah这样按字节存取那就需要拆成8位变量。核心原则看程序处理数据的最小单位是什么就按什么粒度来定义符号变量。通常对于字符串flag按字节定义是最稳妥的。3. 实战案例一线性方程组的秒解——从“猜”到“算”的飞跃我们来看第一类也是最简单的Z3应用场景逆向题的核心验证逻辑是一个线性方程组。3.1 案例背景与约束提取假设我们逆向一个程序它的关键函数反编译后看起来像这样伪代码void check(char* input) { if (input[0] input[1] input[2] ! 0xAB) return; if (input[1] - input[0] ! 0x12) return; if (input[2] ^ input[0] ^ input[1] ! 0x39) return; if (input[0] * 3 input[1] * 7 - input[2] * 2 ! 0x1FF) return; printf(“Correct!“); }题目要求输入3个字符的flag。手动解这个四元一次方程组当然可以但已经有点麻烦了。用Z3我们几乎可以“无脑”翻译。3.2 Z3建模与求解脚本编写我们的脚本就是“翻译官”把C代码的约束变成Python-Z3的约束。from z3 import * # 1. 定义符号变量3个8位位向量代表3个字符 x, y, z BitVecs(‘x y z‘, 8) # 2. 创建求解器 s Solver() # 3. 添加约束直接翻译自伪代码 s.add(x y z 0xAB) s.add(y - x 0x12) s.add(z ^ x ^ y 0x39) s.add(x * 3 y * 7 - z * 2 0x1FF) # 4. 添加额外合理约束输入通常是可打印字符 s.add(x 32, x 126) s.add(y 32, y 126) s.add(z 32, z 126) # 5. 求解并输出 if s.check() sat: m s.model() # 注意直接m[x]得到的是Z3内部对象需转换 val_x m[x].as_long() val_y m[y].as_long() val_z m[z].as_long() flag bytes([val_x, val_y, val_z]).decode() print(f“Solved! Flag part: {flag}“) # 输出具体值用于验证 print(f“x{val_x}({chr(val_x)}), y{val_y}({chr(val_y)}), z{val_z}({chr(val_z)})“) else: print(“No solution.“)运行这个脚本几乎在瞬间就能得到解。这就是Z3最基础的用法将条件“陈述”出来而非“执行”出来。程序逻辑变成了静态的约束声明。3.3 常见陷阱整数溢出与位向量运算在上面的例子中我们用了BitVec(8)。这里有一个关键点在x86汇编中add al, bl这样的指令操作的是8位寄存器如果结果超过255高位会被截断并且会设置标志位但结果本身是取模256的。Z3的BitVec类型完美模拟了这一点。如果我们错误地使用了Int类型那么x y z 0xAB这个约束在数学整数域里可能成立但在8位模运算下可能不成立因为实际程序运行中0xAB是模256后的结果。注意事项算术运算与位运算的优先级在编写约束时要注意Python和Z3运算符的优先级。Z3的重载运算符,-,,|,^优先级可能与Python内置的不同。最稳妥的做法是多用括号来明确指定计算顺序。例如(a 0xF0) | (b 4)清晰的括号能避免意想不到的错误。4. 实战案例二处理循环与分支——将执行流“拍平”为约束逆向题很少是简单的静态方程更多是包含循环和分支的动态逻辑。Z3同样能处理核心思想是用符号变量模拟程序状态将动态的循环展开为静态的多个约束用逻辑“或”来处理分支。4.1 案例背景一个简单的变换循环考虑以下验证逻辑伪代码char enc[] {0x48, 0x5F, 0x36, 0x35, 0x35, 0x25, 0x14, 0x2C, 0x1D, 0x01, 0x03, 0x2D, 0x0C, 0x6F}; void check(char* input) { for (int i 0; i 14; i) { input[i] ((input[i] ^ 0x55) 0x10) 0xFF; if (input[i] ! enc[i]) { fail(); } } success(); }这是一个典型的逐字节变换每个输入字节先与0x55异或再加0x10最后结果要与密文数组enc匹配。4.2 循环的展开与约束建模对于这种确定次数的循环我们在Z3建模时直接将其“展开”。循环体中的操作就是对第i个符号变量施加一个变换并要求变换结果等于enc[i]。from z3 import * enc [0x48, 0x5F, 0x36, 0x35, 0x35, 0x25, 0x14, 0x2C, 0x1D, 0x01, 0x03, 0x2D, 0x0C, 0x6F] flag_len len(enc) # 定义flag的符号变量数组 flag [BitVec(f‘f{i}‘, 8) for i in range(flag_len)] s Solver() # “展开”循环为每个字节添加约束 for i in range(flag_len): # 模拟变换: ((input[i] ^ 0x55) 0x10) 0xFF enc[i] # 0xFF 对于8位BitVec是自动的所以可以省略 transformed (flag[i] ^ 0x55) 0x10 # 注意虽然transformed是BitVec(8)但加法可能产生数学上大于255的值 # Z3的BitVec会自动处理模运算。这里直接比较即可。 s.add(transformed enc[i]) # 添加可打印字符约束可选但能加速求解并确保结果合理 s.add(flag[i] 32, flag[i] 126) if s.check() sat: m s.model() result [] for f in flag: result.append(m[f].as_long()) print(“Flag:“, bytes(result).decode()) else: print(“No solution.“)这个脚本成功的关键在于我们用符号运算(flag[i] ^ 0x55) 0x10代表了程序运行时对内存中值的计算过程。Z3不会去“执行”这个计算而是将其作为一个关于flag[i]的等式约束来推理。4.3 处理条件分支使用逻辑运算符如果循环体内有分支呢比如for (int i 0; i len; i) { if (i % 2 0) { input[i] input[i] ^ 0xAA; } else { input[i] input[i] 0x11; } if (input[i] ! enc[i]) fail(); }在Z3中我们需要用If函数或者逻辑“或”来模拟这个分支。更直观的方法是根据i的奇偶性直接为每个字节添加不同的约束因为i在循环中是常数。for i in range(flag_len): if i % 2 0: s.add((flag[i] ^ 0xAA) enc[i]) else: s.add((flag[i] 0x11) enc[i])如果分支条件依赖于输入值本身符号变量那就必须使用Z3的逻辑表达式。例如if (input[i] 0x40): ... else: ...则需要写成from z3 import If for i in range(flag_len): # 条件表达式本身也是符号化的 condition flag[i] 0x40 # If(条件, 真值, 假值) transformed If(condition, flag[i] ^ 0xAA, flag[i] 0x11) s.add(transformed enc[i])Z3的强大之处就在于它能处理这种符号化的条件在求解过程中会去探索两种可能性并找到满足所有约束的那条路径。实操心得二约束的简化与优化向Z3添加约束不是越多越好不必要的约束有时会拖慢求解速度。例如上面的例子中如果我们知道flag的格式是flag{...}那么前5个字节的约束就是固定的直接加上s.add(flag[0]ord(‘f‘), flag[1]ord(‘l‘), ...)能极大缩小搜索空间。反之如果盲目添加“可能是可打印字符”这样的弱约束对于长flag求解器可能需要探索的空间依然巨大。原则是优先添加从程序逻辑中直接推导出的强约束再辅以根据题目上下文如常见flag格式得出的确定约束。5. 实战案例三复杂运算与中间变量的引入有些逆向题的运算非常复杂可能涉及多层嵌套的位操作和算术运算。直接写成一个巨大的Z3表达式可能难以阅读和调试。这时引入中间符号变量是关键。5.1 案例背景多层混合运算假设我们遇到如下验证代码片段// 假设input是int型数组4字节每个 for (int i 0; i 4; i) { int v input[i]; v (v 16) ^ (v 0xFFFF); // 高16位与低16位异或 v v * 0xABCD1234; v (v 0xF0F0F0F0) | ((v 0x0F0F0F0F) 4); // 半字节交换 if (v ! target[i]) return 0; }这里每个input[i]是32位整数经过多步变换后与目标值比较。5.2 使用中间变量分解复杂约束对于这种多步运算我们可以像写程序一样为每一步的结果创建临时的符号变量。这并不会增加求解难度反而让约束集更清晰也便于调试。from z3 import * target [0x12345678, 0x9ABCDEF0, 0x11223344, 0x55667788] # 示例目标值 s Solver() flag_ints [BitVec(f‘int{i}‘, 32) for i in range(4)] # 定义4个32位输入 for i in range(4): v flag_ints[i] # 第一步高16位与低16位异或 # Z3中提取位可以使用 Extract(high, low, bitvector) low16 Extract(15, 0, v) # 位[15:0] high16 Extract(31, 16, v) # 位[31:16] # 注意Extract返回的是位向量需要零扩展或拼接来保持位数 # 这里 (high16 ^ low16) 结果可能是任意宽度我们将其视为32位低16位有效高16位为0不原代码是int运算结果还是32位。 # 更准确的模拟 ((v 16) 0xFFFF) ^ (v 0xFFFF) step1 ((v 16) 0xFFFF) ^ (v 0xFFFF) # 注意v16后是32位需要0xFFFF取低16位 # 但step1现在是32位其高16位是0。原C代码中两个16位数异或结果提升为int32位高16位为0。 # 所以这个模拟是准确的。 # 第二步乘以常数 step2 step1 * 0xABCD1234 # 第三步半字节交换 # (v 0xF0F0F0F0) | ((v 0x0F0F0F0F) 4) mask_high BitVecVal(0xF0F0F0F0, 32) mask_low BitVecVal(0x0F0F0F0F, 32) step3 (step2 mask_high) | ((step2 mask_low) 4) # 最终约束 s.add(step3 target[i]) # 我们可能还知道这些int是由flag字符串的字节转换而来例如小端序 # flag_bytes [BitVec(f‘b{i}‘, 8) for i in range(16)] # 然后添加约束将4个32位变量与16个字节变量关联起来 # 例如flag_ints[0] Concat(flag_bytes[3], flag_bytes[2], flag_bytes[1], flag_bytes[0]) # 这里为了简化假设我们只求整数解。 if s.check() sat: m s.model() for i in range(4): print(f“input[{i}] {hex(m[flag_ints[i]].as_long())}“) else: print(“No solution.“)通过引入step1,step2,step3这些中间变量约束的逻辑变得非常清晰几乎是对源代码的一对一翻译。调试时如果求解失败你也可以检查中间步骤的约束是否设置正确。5.3 内存布局与字节序的建模在逆向中经常遇到将字符串或字节数组按特定格式如小端序解释为整数的情况。Z3提供了Concat和Extract函数来处理位向量的拼接与切片这对于精确建模内存布局至关重要。假设flag是16个字节的字符串flag_bytes而程序将其视为4个小端序的32位整数进行处理flag_bytes [BitVec(f‘byte_{i}‘, 8) for i in range(16)] flag_ints [] for i in range(0, 16, 4): # 小端序低地址存低位字节 # Concat 的参数是从高到低 one_int Concat(flag_bytes[i3], flag_bytes[i2], flag_bytes[i1], flag_bytes[i]) flag_ints.append(one_int)现在flag_ints[0]就对应了内存中前4个字节组成的小端序整数。之后的所有约束都施加在flag_ints上。求解出flag_ints后再通过模型反解出每个flag_bytes的值。注意事项Concat与Extract的位序Concat(a, b)将a放在高位b放在低位。这与我们通常书写数字时高位在左的习惯一致但在模拟小端序内存时要注意顺序反转。Extract(high, low, bv)提取位[high:low]包含两端其中high和low是索引high low。例如提取一个32位数的低8位Extract(7, 0, x)。6. 实战案例四综合挑战——逆向一个完整的CrackMe让我们综合运用以上技巧处理一个更接近真实比赛的题目。假设有一个CrackMe其核心验证函数如下经过简化和反编译// 假设输入是长度为19的字符串格式为 flag{xxxxxxxxxxxxxxxx} int verify(char* s) { if (strlen(s) ! 19) return 0; if (memcmp(s, “flag{“, 5) ! 0) return 0; if (s[18] ! ‘}‘) return 0; int sum 0; int prod 1; for (int i 5; i 18; i) { // 只处理花括号内的13个字符 sum s[i]; prod * s[i]; // 一个非线性变换 s[i] ((s[i] ^ (s[i] 4)) 0x23) 0xFF; } if (sum ! 0x4A5) return 0; // 和必须为某个值 if (prod ! 0x12345678) return 0; // 积必须为某个值注意溢出实际是模2^32 // 变换后的字符串与一个硬编码数组比较 unsigned char enc[] {0x89, 0xC3, 0xFA, 0x1E, 0x5D, 0x7A, 0xB4, 0xCC, 0x2F, 0x91, 0x0A, 0xE5, 0x77}; for (int i 0; i 13; i) { if (s[i5] ! enc[i]) return 0; } return 1; }这个题目结合了多种元素固定格式、循环、算术约束和与积、非线性变换、最终比较。6.1 分步建模与约束构建我们的Z3脚本需要一步步地建立所有这些约束。from z3 import * # 1. 定义变量19个字节的flag flag [BitVec(f‘f{i}‘, 8) for i in range(19)] s Solver() # 2. 固定格式约束 prefix b“flag{“ for i in range(5): s.add(flag[i] prefix[i]) s.add(flag[18] ord(‘}‘)) # 3. 处理花括号内的13个字节 (索引5到17) inner_bytes flag[5:18] # 注意Python切片是左闭右开5:18取索引5到17 # 定义用于计算和与积的符号变量32位因为int是32位 sum_bv BitVecVal(0, 32) # 位向量形式的0 prod_bv BitVecVal(1, 32) for b in inner_bytes: # 将8位字节零扩展为32位再进行算术运算模拟C语言中的整数提升 b_32 ZeroExt(24, b) # 在b的高位添加24个0扩展为32位 sum_bv sum_bv b_32 prod_bv prod_bv * b_32 # 注意这里乘法可能溢出用BitVec自动模拟模2^32 # 添加和与积的约束 s.add(sum_bv 0x4A5) s.add(prod_bv 0x12345678) # 4. 非线性变换约束并连接到最终比较 enc [0x89, 0xC3, 0xFA, 0x1E, 0x5D, 0x7A, 0xB4, 0xCC, 0x2F, 0x91, 0x0A, 0xE5, 0x77] for i, b in enumerate(inner_bytes): # 变换: ((b ^ (b 4)) 0x23) 0xFF # b 4: 位向量右移高位补0 shifted LShR(b, 4) # 使用逻辑右移高位补0 xored b ^ shifted transformed (xored 0x23) # BitVec(8)加法自动模256 # 要求变换后等于密文 s.add(transformed enc[i]) # 5. 可打印字符约束可选但推荐 for i in range(5, 18): s.add(flag[i] ord(‘!‘), flag[i] ord(‘~‘)) # 可打印ASCII范围 # 6. 求解 if s.check() sat: m s.model() result [] for f in flag: result.append(m[f].as_long()) flag_str bytes(result).decode(‘ascii‘) print(f“Found flag: {flag_str}“) # 验证一下和与积 inner_vals result[5:18] calc_sum sum(inner_vals) calc_prod 1 for val in inner_vals: calc_prod (calc_prod * val) 0xFFFFFFFF # 模拟32位溢出 print(f“Verification - Sum: {hex(calc_sum)}, Product: {hex(calc_prod)}“) else: print(“No solution found.“) # 如果无解可以尝试放松一些约束比如可打印字符看看是不是约束过强 # s.push() 和 s.pop() 可以用于临时修改约束进行调试6.2 调试技巧当Z3返回unsat时怎么办在实际操作中最令人沮丧的不是脚本运行慢而是s.check()返回unsat约束不可满足。这意味着我们的约束条件存在矛盾。如何调试逐步添加约束不要一次性添加所有约束。可以先把最“硬”的、直接从反编译代码得来的约束加上比如最后的变换比较transformed enc[i]然后check()一次。如果sat再逐步加上和、积、格式等约束看是哪一步导致了unsat。Z3的s.push()和s.pop()可以创建作用域来方便测试。检查变量定义和运算确保你使用的变量位数BitVec(8)vsBitVec(32)与程序匹配。确保运算模拟正确特别是移位逻辑右移LShRvs 算术右移和溢出处理。检查约束的“强度”有时我们添加的额外约束如可打印字符可能过强。可以暂时注释掉这些约束看是否能得到解。如果能得到解但解中包含不可打印字符说明题目本身可能允许非打印字符比如flag包含下划线、数字、花括号等但有时也可能包含不可见字符。使用unsat_core对于复杂约束Z3可以尝试找出导致不可满足的核心约束集。s.set(unsat_coreTrue) # ... 添加约束 ... if s.check() unsat: core s.unsat_core() print(“Unsat core:“, core)这能帮你定位是哪些约束互相冲突。实操心得三从动态调试中提取约束最可靠的约束来源是反汇编/反编译代码。但有时代码混淆严重手动分析困难。一个高级技巧是结合动态符号执行虽然Z3本身不是动态的或“污点分析”的思想。你可以写一个Python脚本模拟程序的执行流程但不对输入进行具体赋值而是记录输入符号变量经过的每一个操作从而自动生成Z3约束。对于简单的程序可以手动进行这种“符号化模拟”对于复杂的可以使用像angr这样的框架它能自动执行符号化探索并调用Z3求解。这属于更进阶的用法但思路一脉相承将程序执行路径转化为符号约束。7. 性能调优与高级技巧当题目非常复杂约束变量很多比如上百个字节时直接求解可能会很慢甚至内存不足。这时需要一些调优技巧。7.1 利用已知结构减少搜索空间这是最有效的优化。如果flag有固定前缀如flag{、后缀如}或者中间有固定分隔符如-一定要作为约束首先加上。这能极大剪枝搜索树。7.2 分阶段求解如果约束可以自然地分成相对独立的几组可以先求解一部分变量再将解代入求解剩余变量。例如题目先对flag进行一个可逆的加密变换再检查结果。你可以先定义加密后的中间变量添加加密约束和最终比较约束先求解中间变量。因为加密是可逆的求出中间变量后再单独求解原始flag就很简单了。7.3 选择正确的求解策略Z3的Solver()对象可以设置一些参数来影响求解策略虽然大多数情况下默认设置已经很好。对于位向量逻辑Z3通常非常高效。7.4 注意整数与位向量的混用尽量避免在同一个约束中混用Int和BitVec类型。如果需要比较使用BitVec并注意位数。Z3支持不同类型间的转换如BV2Int但引入转换可能会增加复杂度。8. 常见问题与排查技巧实录在实际使用Z3解逆向题的过程中我踩过不少坑这里总结一份速查表问题现象可能原因解决方案s.check()返回unsat1. 约束条件存在矛盾。2. 变量位数定义错误如该用8位用了32位。3. 运算模拟错误如该用逻辑右移用了算术右移。4. 额外约束如可打印字符过强。1. 使用unsat_core定位矛盾约束。2. 检查反编译代码确认数据操作的最小单位。3. 核对运算语义特别是移位和溢出。4. 暂时移除额外约束看是否可解。求解时间过长或内存耗尽1. 约束过多或过于复杂。2. 搜索空间太大变量多约束弱。1. 尝试添加更多强约束如固定字符。2. 考虑分阶段求解。3. 检查是否有不必要的约束。得到的解看起来是乱码1. 约束不充分存在多解Z3返回了其中一个。2. 字符编码假设错误如非ASCII。1. 添加更多约束如格式、可打印字符。2. 遍历所有解在找到解后添加排除条件再次求解。3. 检查题目描述flag可能包含数字、下划线等。Concat/Extract结果不符合预期位序理解错误。Concat(a,b)中a在高位。Extract(high,low,bv)提取[high:low]。编写简单的测试用例验证例如用具体值测试Concat和Extract的行为。模拟乘除法时行为怪异BitVec的乘除法是模2^n的并且除法是整数除法。这与C语言中整数溢出的行为一致。但如果题目使用了特殊的数学库或浮点数则需要用其他理论如实数算术模拟。确认程序使用的是标准整数运算。对于有符号除法Z3的BitVec除法UDiv/URem是无符号的有符号的需要用SDiv/SRem。最后一点体会Z3不是万能的它最适合解决那些约束清晰、但手动求解繁琐的问题。对于高度混淆、控制流复杂、或者大量使用反调试、自修改代码的题目静态提取约束可能非常困难需要结合动态分析。然而一旦你掌握了将程序逻辑“翻译”成Z3约束的思维你会发现一大类逆向题的难度骤然降低。这种从“动态跟踪”到“静态建模”的思维转变是逆向工程师能力进阶的重要一环。下次再遇到满是数学运算的CrackMe不妨先停下来想想能不能用Z3来优雅地解决它
返回列表