CTF-reverse中有一类题目是通过约束方程求解变量的值,然后转化为对应的ASCII码,最终获得flag,约束方程以及要求解的未知数往往非常多,因此手算十分不现实,借助python中的z3模块可以很快完成求解!
下面是某道CTF-reverse原题的反汇编代码,可以看到约束方程非常多,要求解的未知数也是多达16个,因此本篇用于记录z3求解器脚本方便使用
一,例题放送
int __cdecl main(int argc, const char **argv, const char **envp) { char flag[32]; // [rsp+20h] [rbp-60h] BYREF int v5; // [rsp+40h] [rbp-40h] int v6; // [rsp+44h] [rbp-3Ch] int v7; // [rsp+48h] [rbp-38h] int v8; // [rsp+4Ch] [rbp-34h] int v9; // [rsp+50h] [rbp-30h] int v10; // [rsp+54h] [rbp-2Ch] int v11; // [rsp+58h] [rbp-28h] int v12; // [rsp+5Ch] [rbp-24h] int v13; // [rsp+60h] [rbp-20h] int v14; // [rsp+64h] [rbp-1Ch] int v15; // [rsp+68h] [rbp-18h] int v16; // [rsp+6Ch] [rbp-14h] int v17; // [rsp+70h] [rbp-10h] int v18; // [rsp+74h] [rbp-Ch] int v19; // [rsp+78h] [rbp-8h] int v20; // [rsp+7Ch] [rbp-4h] _main(argc, argv, envp); printf("Please input your flag: "); scanf("%s", flag); v20 = flag[0]; v19 = flag[1]; v18 = flag[2]; v17 = flag[3]; v16 = flag[4]; v15 = flag[5]; v14 = flag[6]; v13 = flag[7]; v12 = flag[8]; v11 = flag[9]; v10 = flag[10]; v9 = flag[11]; v8 = flag[12]; v7 = flag[13]; v6 = flag[14]; v5 = flag[15]; if ( 7 * flag[0] == 546 // flag[0]=78 && 2 * v19 == 166 // v19=83 && 6 * v18 + v17 + 7 * v15 == 1055 && 2 * v7 + v12 + 7 * v15 + v17 + 4 * v19 + 4 * v16 + 6 * v13 + 8 * v5 == 3107 && 4 * v16 == 336 // v16=84 && 2 * v19 + 7 * v15 == 656 // v15=70 && 2 * v7 + 3 * v9 + 3 * v14 + 6 * v13 + v12 + 5 * v11 + 16 * v10 + 6 * v8 + 8 * v5 == 5749 && 6 * v13 == 606 // v13=101 && 5 * v6 + v12 == 652 // v12=52 && 5 * v11 + 16 * v10 + 6 * v8 == 3213 && 2 * v7 + 3 * v9 + 24 * v10 + 5 * v11 + 3 * v14 + 6 * v13 + v12 + 6 * v8 + 8 * v5 == 6717 && 3 * v9 == 285 // v9=95 && 2 * v12 + 3 * v14 + 6 * v13 + 8 * v10 + 6 * v8 + 2 * v7 + 5 * v6 + 8 * v5 == 4573 && 5 * v6 == 600 // v6=120 && v17 + 6 * v18 + 4 * v16 + 7 * v15 + 2 * v7 == 1615 && v12 + 7 * v15 + 2 * v19 + 6 * v13 + 8 * v5 == 2314 )// v5=125 { puts("Success"); system("pause"); } else { puts("Wrong"); system("pause"); } return 0; }
二,解决本题的完整脚本
下面是完整脚本,想节省时间不看解释的可以直接取走用
from z3 import *
#创建未知数变量
v = [Int(f'v{i}') for i in range(0, 16)]
#创建解释器对象
solver = Solver()#创建一个求解器对象
#添加约束方程
solver.add(v[0] * 7 == 546)
solver.add(v[1] * 2 == 166)
solver.add(v[2] * 6 + v[3] * 1 + v[5] * 7 == 1055)
solver.add(v[1] * 4 + v[3] + v[4] * 4 + v[5] * 7 + v[7] * 6 + v[8] * 1 + v[13] * 2 + v[15] * 8 == 3107)
solver.add(v[4] * 4 == 336)
solver.add(v[1] * 2 + v[5] * 7 == 656)
solver.add(v[6] * 3 + v[7] * 6 + v[8] + v[9] * 5 + v[10] * 16 + v[11] * 3 + v[12] * 6 + v[13] * 2 + v[15] * 8 == 5749)
solver.add(v[7] * 6 == 606)
solver.add(v[8] + v[14] * 5 == 652)
solver.add(v[9] * 5 + v[10] * 16 + v[12] * 6 == 3213)
solver.add(v[6] * 3 + v[7] * 6 + v[8] + v[9] * 5 + v[10] * 24 + v[11] * 3 + v[12] * 6 + v[13] * 2 + v[15] * 8 == 6717)
solver.add(v[11] * 3 == 285)
solver.add(v[6] * 3 + v[7] * 6 + v[8] * 2 + v[10] * 8 + v[12] * 6 + v[13] * 2 + v[14] * 5 + v[15] * 8 == 4573)
solver.add(v[14] * 5 == 600)
solver.add(v[2] * 6 + v[3] * 1 + v[4] * 4 + v[5] * 7 + v[13] * 2 == 1615)
solver.add(v[1] * 2 + v[5] * 7 + v[7] * 6 + v[8] * 1 + v[15] * 8 == 2314)
#求解并转化为字符输出,得到flag
if solver.check() == sat: #check()方法用来判断是否有解,sat(即satisify)表示满足有解
ans = solver.model() #model()方法得到解
for i in v:
print(chr(ans[i].as_long()), end='')
#一般不会无解,如果无解八成是未知数变量的类型不符合,或约束方程添加错误
else:
print("no ans!")
三,脚本详解
(一)导入z3库
这步看似就是简简单单的一个倒库操作,但也能硬控一大片萌新很长时间,比如蒟蒻博主/(ㄒoㄒ)/~~
因为第一次用z3,所以这个库理所当然是不存在的要先下载,但是用【pip install z3】命令下载的库是不对的(能下载,但下载的不对),而要用【pip install z3-solver】,具体见本篇->Windows下安装z3(python3_z3-py3-whl csdn-CSDN博客
而且下载好后也不能用【import z3】这种导入方式,而要用下面代码块的这种,具体原因未知
from z3 import *
(二) 创建未知数变量
未知数变量即方程中的未知数,必须主动提供给z3求解器,如果是比较简单的、只有几个参数的方程,可以直接手动列出,如下图所示(注意逐个列出时使用Int(),一次全部列出使用Ints())
如果参数较多,则建议使用循环
当然,博主建议都用循环,直接硬套下面这行代码来创建变量
其中
【for i in range(0, 16)】的含义是产生值为0到15的i
【f'v{i}'】表示格式化字符串,结果显然为['v0', 'v1', 'v2' ... 'v15']
这里最后还有一步操作很关键,【Int()】是z3中用于将变量转换为特定类型的一个函数,转换后的结果显然为[v0, v1, v2 ... v15],这步转换是不可或缺的,只有转为z3中的特定整数类型变量后,它们才能作为之后的约束方程中的变量来使用
v = [Int(f'v{i}') for i in range(0, 16)]
(三)创建解释器对象
这一步是最好理解的,就是实例化了z3中的一个Solver类对象以方便后续调用其成员函数,当然如果你没有面向对象程序的经验,可能查阅资料需要稍作理解
solver = Solver()
(四)添加约束方程
这一步也很好理解,直接调用Solver类的成员函数add()将题目给出的所有约束方程添加即可,每个方程都要添加注意不要遗漏!
solver.add(v[0] * 7 == 546) solver.add(v[1] * 2 == 166) solver.add(v[2] * 6 + v[3] * 1 + v[5] * 7 == 1055) solver.add(v[1] * 4 + v[3] + v[4] * 4 + v[5] * 7 + v[7] * 6 + v[8] * 1 + v[13] * 2 + v[15] * 8 == 3107) solver.add(v[4] * 4 == 336) solver.add(v[1] * 2 + v[5] * 7 == 656) solver.add(v[6] * 3 + v[7] * 6 + v[8] + v[9] * 5 + v[10] * 16 + v[11] * 3 + v[12] * 6 + v[13] * 2 + v[15] * 8 == 5749) solver.add(v[7] * 6 == 606) solver.add(v[8] + v[14] * 5 == 652) solver.add(v[9] * 5 + v[10] * 16 + v[12] * 6 == 3213) solver.add(v[6] * 3 + v[7] * 6 + v[8] + v[9] * 5 + v[10] * 24 + v[11] * 3 + v[12] * 6 + v[13] * 2 + v[15] * 8 == 6717) solver.add(v[11] * 3 == 285) solver.add(v[6] * 3 + v[7] * 6 + v[8] * 2 + v[10] * 8 + v[12] * 6 + v[13] * 2 + v[14] * 5 + v[15] * 8 == 4573) solver.add(v[14] * 5 == 600) solver.add(v[2] * 6 + v[3] * 1 + v[4] * 4 + v[5] * 7 + v[13] * 2 == 1615) solver.add(v[1] * 2 + v[5] * 7 + v[7] * 6 + v[8] * 1 + v[15] * 8 == 2314)
(五)求解并转化为字符输出,得到flag
使用成员函数check()获取返回值,判断是否等于sat(sat是z3模块中的一个常量,代表方程组有解)
有解情况下使用成员函数model()获取解,该函数会返回一个列表,列表中的解以键值对的形式存储,例如本题的解形式如下:
接下来要做的是将解转化为字符,但由于该列表中的元素是z3中的特殊类型,需要先转换为python中的整数类型才能使用chr()函数转为对应字符
另外需要注意的是该列表求出的解不是按未知参数的名字大小排序的,如果直接按列表中的解的顺序转为字符输出,flag显然不对
解决办法是循环访问v列表中的[v0, v1, v2 ... v15],将其作为索引去访问ans列表,即此处的【ans[i]】,然后使用as_long()函数将解的类型转为python中的int类型,最后使用chr()函数转为对应字符
if solver.check() == sat: #check()方法用来判断是否有解,sat(即satisify)表示满足有解 ans = solver.model() #model()方法得到解 for i in v: print(chr(ans[i].as_long()), end='') #一般不会无解,如果无解八成是未知数变量的类型不符合,或约束方程添加错误 else: print("no ans!")