4、Z3求解器入门:Z3的安装、基本数据类型(Int, BitVec, Bool)、约束求解与模型获取

好,咱们正式开始玩Z3。

说实话,我第一次接触Z3的时候,心里想的是:「这玩意儿不就是个高级版的解方程工具吗?」后来真拿它去逆向一个密码学算法校验,才发现自己太天真了。Z3的强大之处在于——它不光能解方程,还能解带约束的逻辑谜题。你给它一堆条件,它告诉你「行」还是「不行」,如果行,还能给你一个具体的答案。

这一章,我们就从零开始,把Z3装好,把它的三个核心数据类型搞明白,再跑几个实际的约束求解例子。我保证,学完这一章,你就能用它去破解一些简单的密码校验了。

4.1 安装Z3:别踩我踩过的坑

安装其实很简单,但有几个细节我得提醒你。

推荐方式:pip安装

pip install z3-solver

注意,包名是 z3-solver,不是 z3。我见过有人装错了包,折腾了半天。

如果你在Windows上遇到问题,大概率是Python环境变量没配好。我个人习惯用虚拟环境,干净利落。

验证安装

打开Python交互环境,输入:

from z3 import *
print(Int('x'))

如果能正常输出 x,说明安装成功。

我曾经踩过的坑: 在Linux上用系统自带的Python3,结果权限不够,装不上。后来用 pip install --user z3-solver 搞定的。如果你也遇到权限问题,试试这个。

4.2 三大基本数据类型:Int、BitVec、Bool

Z3里最常用的就是这三种。说白了,它们对应着现实世界里的整数、二进制位串和真假判断。

4.2.1 Int(整数)

这个最简单,就是数学里的整数。Z3里的整数没有位数限制,可以无限大(当然,实际求解时会受内存限制)。

from z3 import *

x = Int('x')
y = Int('y')

s = Solver()
s.add(x + y == 10)
s.add(x - y == 2)

if s.check() == sat:
    print(s.model())  # 输出:[x = 6, y = 4]

你看,这就是一个简单的二元一次方程组。Z3一秒就解出来了。

4.2.2 BitVec(位向量)

这个才是逆向分析里的主角。为什么?因为计算机里的整数都是有位宽的——比如8位、16位、32位、64位。BitVec就是用来模拟这种固定位宽的整数。

举个例子,一个8位的BitVec,取值范围是0到255。如果你让它加1,255加1会变成0,因为溢出了。这跟真实CPU的行为一模一样。

from z3 import *

a = BitVec('a', 8)  # 8位变量
b = BitVec('b', 8)

s = Solver()
s.add(a + b == 0x100)  # 注意,0x100 = 256,8位装不下

if s.check() == sat:
    print(s.model())  # 输出可能让你意外

为什么会这样?因为8位加法,结果只保留低8位。0x100的低8位是0x00,所以实际上Z3在求解 a + b == 0。这就是溢出效应,在密码学算法里经常被利用。

核心区别:

  • Int:数学整数,无溢出,适合纯数学问题
  • BitVec:固定位宽整数,有溢出,适合逆向工程和密码学

4.2.3 Bool(布尔值)

这个就是True/False。在Z3里,它通常用来做条件判断,或者组合成更复杂的逻辑表达式。

from z3 import *

p = Bool('p')
q = Bool('q')

s = Solver()
s.add(Or(p, q))      # p 或 q 为真
s.add(Not(And(p, q))) # p 和 q 不能同时为真

if s.check() == sat:
    print(s.model())  # 输出:p = True, q = False 或 p = False, q = True

嗯,这里要注意:Bool变量本身不是约束,你得用逻辑运算符把它们组合起来,才能形成有意义的条件。

4.3 约束求解与模型获取

好,数据类型讲完了,咱们来聊聊核心流程。Z3求解一个问题的步骤,其实就三步:

  1. 声明变量:告诉Z3你要用哪些变量,什么类型
  2. 添加约束:把条件告诉Z3
  3. 求解并获取模型:让Z3干活,然后读出结果

我画了一张图,帮你理清这个流程:

1. 声明变量 2. 添加约束 3. 求解 sat? 获取模型 无解(unsat) Z3约束求解核心流程

你看,流程很清晰。关键就是 s.check() 返回 sat 还是 unsat。如果是 sat,就用 s.model() 把解拿出来。

4.4 实战:破解一个简单的密码校验

光说不练假把式。咱们来点实际的。

假设你逆向了一个程序,发现它有一个密码校验函数,逻辑是这样的:

def check_password(pwd):
    # pwd 是一个4字节的整数
    # 校验条件:
    # 1. pwd 的每个字节都不为0
    # 2. pwd 的低16位 + 高16位 == 0xABCD
    # 3. pwd 的字节0 XOR 字节3 == 0x42
    # 4. pwd 的字节1 XOR 字节2 == 0x13
    return True  # 如果满足所有条件

用Z3来破解它:

from z3 import *

# 声明一个32位的变量
pwd = BitVec('pwd', 32)

# 提取4个字节
byte0 = Extract(7, 0, pwd)    # 最低字节
byte1 = Extract(15, 8, pwd)
byte2 = Extract(23, 16, pwd)
byte3 = Extract(31, 24, pwd)  # 最高字节

# 低16位和高16位
low16 = Extract(15, 0, pwd)
high16 = Extract(31, 16, pwd)

s = Solver()

# 约束1:每个字节都不为0
s.add(byte0 != 0)
s.add(byte1 != 0)
s.add(byte2 != 0)
s.add(byte3 != 0)

# 约束2:低16位 + 高16位 == 0xABCD
s.add(low16 + high16 == 0xABCD)

# 约束3:字节0 XOR 字节3 == 0x42
s.add(byte0 ^ byte3 == 0x42)

# 约束4:字节1 XOR 字节2 == 0x13
s.add(byte1 ^ byte2 == 0x13)

# 求解
if s.check() == sat:
    model = s.model()
    result = model[pwd].as_long()
    print(f"找到密码: 0x{result:08X}")
    # 输出每个字节
    print(f"字节0: 0x{(result & 0xFF):02X}")
    print(f"字节1: 0x{((result >> 8) & 0xFF):02X}")
    print(f"字节2: 0x{((result >> 16) & 0xFF):02X}")
    print(f"字节3: 0x{((result >> 24) & 0xFF):02X}")
else:
    print("无解,密码校验逻辑可能有误")

个人经验: 我在逆向一个嵌入式设备的固件时,遇到过类似的校验。当时用Z3几秒钟就解出来了,而手动逆向推算至少得半小时。Z3在处理这种位运算约束时,效率极高。

4.5 常见陷阱与避坑指南

用Z3的时候,有几个坑我帮你提前踩了:

陷阱1:BitVec的符号问题

BitVec默认是无符号的。如果你需要处理负数,得用 BitVecVal 或者显式转换。我曾经在解一个带符号比较的约束时,折腾了半天才发现是符号问题。

陷阱2:模型中的值怎么取出来

model[var] 返回的是Z3的内部对象,不是Python整数。你得用 .as_long() 转换。如果是Bool类型,用 .is_true() 判断。

陷阱3:约束过多导致求解变慢

Z3虽然强大,但也不是万能的。如果你加了上百个复杂的非线性约束,求解时间可能会暴涨。我的建议是:先加简单约束,逐步增加复杂度,看看Z3的反应。

4.6 小结

这一章我们搞定了三件事:

  • 装好了Z3,避开了常见的安装坑
  • 搞懂了Int、BitVec、Bool三种数据类型,特别是BitVec在逆向中的核心地位
  • 跑通了约束求解的完整流程,并用一个实战例子演示了如何破解密码校验

说实话,Z3入门并不难,难的是怎么把实际问题转化成Z3能理解的约束。这个能力需要多练。下一章我们会深入BitVec的高级操作,比如移位、循环、符号扩展——这些都是密码学算法里经常出现的操作。

好,今天就到这里。你自己动手试试上面的例子,把密码改一改,看看Z3能不能解出来。遇到问题随时回来翻翻这一章。


公众号:蓝海资料掘金营,微信deep3321