非专业的密码算法通常都很脆弱。如果密码学专家出手,这些算法将不堪一击。不过,即使密码学专业人士的帮忙,逆向工程分析人员同样可破解密码。
我曾经遇到过一个把64位数据转换为另一种数据的单向函数[1]。那时,我们要把hash值还原为原始数据。
在IDA中,程序的具体指令如下:
sub_401510 proc near
; ECX = input
mov rdx, 5D7E0D1F2E0F1F84h
mov rax, rcx ; input
imul rax, rdx
mov rdx, 388D76AEE8CB1500h
mov ecx, eax
and ecx, 0Fh
ror rax, cl
xor rax, rdx
mov rdx, 0D2E9EE7E83C4285Bh
mov ecx, eax
and ecx, 0Fh
rol rax, cl
lea r8, [rax+rdx]
mov rdx, 8888888888888889h
mov rax, r8
mul rdx
shr rdx, 5
mov rax, rdx
lea rcx, [r8+rdx*4]
shl rax, 6
sub rcx, rax
mov rax, r8
rol rax, cl
; EAX = output
retn
sub_401510 endp
ECX寄存器传递函数的第一个参数,由此可判断这是GCC编译的程序。
如果您手头没有Hex-Rays一类的反编译程序,或者您根本信不过这些自动化工具,那么您可以自己进行反编译。在进行反编译时,可以把CPU寄存器当作C语言的变量,然后把汇编语言直接翻译为等效的C指令。例如,上述程序可反编译为:
uint64_t f(uint64_t input)
{
uint64_t rax, rbx, rcx, rdx, r8;
ecx=input;
rdx=0x5D7E0D1F2E0F1F84;
rax=rcx;
rax*=rdx;
rdx=0x388D76AEE8CB1500;
rax=_lrotr(rax, rax&0xF); // rotate right
rax^=rdx;
rdx=0xD2E9EE7E83C4285B;
rax=_lrotl(rax, rax&0xF); // rotate left
r8=rax+rdx;
rdx=0x8888888888888889;
rax=r8;
rax*=rdx;
rdx=rdx>>5;
rax=rdx;
rcx=r8+rdx*4;
rax=rax<<6;
rcx=rcx-rax;
rax=r8
rax=_lrotl (rax, rcx&0xFF); // rotate left
return rax;
};
力图谨慎的人,可以把上述代码再次编译为可执行程序。在工作方式上,它应当与最初的程序完全一致。
然后,我们根据寄存器的使用方法,整理刚才写出的C代码。这时就需要加倍小心、高度集中注意力,任何细小的纰漏都可能让我们前功尽弃。
首先添加上注释、进行段落划分:
uint64_t f(uint64_t input)
{
uint64_t rax, rbx, rcx, rdx, r8;
ecx=input;
rdx=0x5D7E0D1F2E0F1F84;
rax=rcx;
rax*=rdx;
rdx=0x388D76AEE8CB1500;
rax=_lrotr(rax, rax&0xF); // rotate right
rax^=rdx;
rdx=0xD2E9EE7E83C4285B;
rax=_lrotl(rax, rax&0xF); // rotate left
r8=rax+rdx;
rdx=0x8888888888888889;
rax=r8;
rax*=rdx;
// RDX here is a high part of multiplication result
rdx=rdx>>5;
// RDX here is division result!
rax=rdx;
rcx=r8+rdx*4;
rax=rax<<6;
rcx=rcx-rax;
rax=r8
rax=_lrotl (rax, rcx&0xFF); // rotate left
return rax;
};
接下来整理程序尾部的数学计算指令:
uint64_t f(uint64_t input)
{
uint64_t rax, rbx, rcx, rdx, r8;
ecx=input;
rdx=0x5D7E0D1F2E0F1F84;
rax=rcx;
rax*=rdx;
rdx=0x388D76AEE8CB1500;
rax=_lrotr(rax, rax&0xF); // rotate right
rax^=rdx;
rdx=0xD2E9EE7E83C4285B;
rax=_lrotl(rax, rax&0xF); // rotate left
r8=rax+rdx;
rdx=0x8888888888888889;
rax=r8;
rax*=rdx;
// RDX here is a high part of multiplication result
rdx=rdx>>5;
// RDX here is division result!
rax=rdx;
rcx=(r8+rdx*4)-(rax<<6);
rax=r8
rax=_lrotl (rax, rcx&0xFF); // rotate left
return rax;
};
根据乘法因子的特点,我们应能判断出程序是通过乘法指令等效实现的除法运算。[2]因此,我们可以使用Wolfram Mathematica来计算除数。
指令清单77.1 Wolfram Mathematica
In[1]:=N[2^(64 + 5)/16^^8888888888888889]
Out[1]:=60.
那么,原始的运算指令应当是:
uint64_t f(uint64_t input)
{
uint64_t rax, rbx, rcx, rdx, r8;
ecx=input;
rdx=0x5D7E0D1F2E0F1F84;
rax=rcx;
rax*=rdx;
rdx=0x388D76AEE8CB1500;
rax=_lrotr(rax, rax&0xF); // rotate right
rax^=rdx;
rdx=0xD2E9EE7E83C4285B;
rax=_lrotl(rax, rax&0xF); // rotate left
r8=rax+rdx;
rax=rdx=r8/60;
rcx=(r8+rax*4)-(rax*64);
rax=r8
rax=_lrotl (rax, rcx&0xFF); // rotate left
return rax;
};
整理计算指令得:
uint64_t f(uint64_t input)
{
uint64_t rax, rbx, rcx, rdx, r8;
rax=input;
rax*=0x5D7E0D1F2E0F1F84;
rax=_lrotr(rax, rax&0xF); // rotate right
rax^=0x388D76AEE8CB1500;
rax=_lrotl(rax, rax&0xF); // rotate left
r8=rax+0xD2E9EE7E83C4285B;
rcx=r8-(r8/60)*60;
rax=r8
rax=_lrotl (rax, rcx&0xFF); // rotate left
return rax;
};
继续简化代码可发现,程序计算的是余数、而非商:
uint64_t f(uint64_t input)
{
uint64_t rax, rbx, rcx, rdx, r8;
rax=input;
rax*=0x5D7E0D1F2E0F1F84;
rax=_lrotr(rax, rax&0xF); // rotate right
rax^=0x388D76AEE8CB1500;
rax=_lrotl(rax, rax&0xF); // rotate left
r8=rax+0xD2E9EE7E83C4285B;
return _lrotl (r8, r8 % 60); // rotate left
};
最终,我们把程序转化成华丽的C语言代码:
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <intrin.h>
#define C1 0x5D7E0D1F2E0F1F84
#define C2 0x388D76AEE8CB1500
#define C3 0xD2E9EE7E83C4285B
uint64_t hash(uint64_t v)
{
v*=C1;
v=_lrotr(v, v&0xF); // rotate right
v^=C2;
v=_lrotl(v, v&0xF); // rotate left
v+=C3;
v=_lrotl(v, v % 60); // rotate left
return v;
};
int main()
{
printf ("%llu\n", hash(...));
};
除了密码专家之外,没什么人能够根据hash值逆推原始数据。旋转位左/右移指令足以令人望而却步——它能够保证映射函数不是单满射函数,而且还保留了碰撞的可能性;说得直白一些就是“多个输入可能产生同一个输出”。
由于这个函数采用了64位因子,所以暴力破解也不太现实。
在加密学知识不足的情况下,我们可以使用微软研究团队发布的Z3工具[3]尝试破解。虽然它只是个形式验证工具,但是我们将用它来作SMT求解。也就是说,我们要用Z3来求解巨型方程式。
我们使用的Python源代码如下:
1 from z3 import *
2
3 C1=0x5D7E0D1F2E0F1F84
4 C2=0x388D76AEE8CB1500
5 C3=0xD2E9EE7E83C4285B
6
7 inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
8
9 s = Solver()
10 s.add(i1==inp*C1)
11 s.add(i2==RotateRight (i1, i1 & 0xF))
12 s.add(i3==i2 ^ C2)
13 s.add(i4==RotateLeft(i3, i3 & 0xF))
14 s.add(i5==i4 + C3)
15 s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17 s.add(outp==10816636949158156260)
18
19 print s.check()
20 m=s.model()
21 print m
22 print (" inp=0x%X" % m[inp].as_long())
23 print ("outp=0x%X" % m[outp].as_long())
程序的第7行声明了各个变量。这些都是64位变量。其中,i1~i6都是中间变量(形参),在各个指令之间传递寄存器的值。
第10~15行之间是我们添加的约束条件。这些条件之中,第17行限定的约束条件最为重要:在使用这个函数时,我们要查找输出值为10816636949158156260的输入值。
本质上说,基于SMT的求解方法可以搜索满足全部限定条件的所有输入值。
上述程序中的RotateRight、RotateLeft和URem都是Z3提供的Python API。它们都不是Python语言提供的标准指令。
然后运行上述程序:
...>python.exe 1.py
sat
[i1 = 3959740824832824396,
i3 = 8957124831728646493,
i5 = 10816636949158156260,
inp = 1364123924608584563,
outp = 10816636949158156260,
i4 = 14065440378185297801,
i2 = 4954926323707358301]
inp=0x12EE577B63E80B73
outp=0x961C69FF0AEFD7E4
程序输出中的“sat”是“satisfiable(满足条件的值)”的缩写。这就是说,我们的求解方法至少可以找到一个解。程序用方括号把最终解标注了出来。屏幕输出的最后两行是用16进制显示的输入、输出值。如果把0x12EE577B63E80B73代入原函数的输入变量,那么它的输出值与我们指定的值相符。
另外需要注意的是,因为原函数不是单满射函数,所以可能存在多个符合条件的输入值。不过,网上公开的Z3 SMT求解程序只会计算出一组解。为此,我们对上面的程序稍加修改,添加了第19行,让程序“探寻其他的解”:
1 from z3 import *
2
3 C1=0x5D7E0D1F2E0F1F84
4 C2=0x388D76AEE8CB1500
5 C3=0xD2E9EE7E83C4285B
6
7 inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
8
9 s = Solver()
10 s.add(i1==inp*C1)
11 s.add(i2==RotateRight (i1, i1 & 0xF))
12 s.add(i3==i2 ^ C2)
13 s.add(i4==RotateLeft(i3, i3 & 0xF))
14 s.add(i5==i4 + C3)
15 s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17 s.add(outp==10816636949158156260)
18
19 s.add(inp!=0x12EE577B63E80B73)
20
21 print s.check()
22 m=s.model()
23 print m
24 print (" inp=0x%X" % m[inp].as_long())
25 print ("outp=0x%X" % m[outp].as_long())
这样一来,它就可以求得另一组解:
...>python.exe 2.py
sat
[i1 = 3959740824832824396,
i3 = 8957124831728646493,
i5 = 10816636949158156260,
inp = 10587495961463360371,
outp = 10816636949158156260,
i4 = 14065440378185297801,
i2 = 4954926323707358301]
inp=0x92EE577B63E80B73
outp=0x961C69FF0AEFD7E4
人工排除已知解的方法不太先进。其实程序可以自动地修改约束条件,并且自行排除已知解,以便自动化地求得所有解。自行求得全部解的程序十分精巧:
1 from z3 import *
2
3 C1=0x5D7E0D1F2E0F1F84
4 C2=0x388D76AEE8CB1500
5 C3=0xD2E9EE7E83C4285B
6
7 inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
8
9 s = Solver()
10 s.add(i1==inp*C1)
11 s.add(i2==RotateRight (i1, i1 & 0xF))
12 s.add(i3==i2 ^ C2)
13 s.add(i4==RotateLeft(i3, i3 & 0xF))
14 s.add(i5==i4 + C3)
15 s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17 s.add(outp==10816636949158156260)
18
19 # copypasted from http://stackoverflow.com/questions/11867611/z3py-checking-all-solutions-for-equation
20 result=[]
21 while True:
22 if s.check() == sat:
23 m = s.model()
24 print m[inp]
25 result.append(m)
26 # Create a new constraint the blocks the current model
27 block = []
28 for d in m:
29 # d is a declaration
30 if d.arity() > 0:
31 raise Z3Exception("uninterpreted functions are not supported")
32 # create a constant from declaration
33 c=d()
34 if is_array(c) or c.sort().kind() == Z3_UNINTERPRETED_SORT:
35 raise Z3Exception("arrays and uninterpreted sorts are not supported")
36 block.append(c != m[d])
37 s.add(Or(block))
38 else:
39 print "results total=",len(result)
40 break
运行上述程序,可得:
1364123924608584563
1234567890
9223372038089343698
4611686019661955794
13835058056516731602
3096040143925676201
12319412180780452009
7707726162353064105
16931098199207839913
1906652839273745429
11130024876128521237
15741710894555909141
6518338857701133333
5975809943035972467
15199181979890748275
10587495961463360371
results total= 16
可见,总共有16个输入值满足条件“输出值为0x92EE577B63E80B73”。
第二个解是1234567890。在编写本文时,笔者使用的输入值正是这个数。
接下来,我们要更深入地讨论程序的算法。在这些解里面,有没有低32位与输出值的低32位相等的解?
为此,我们修改本章的第一个脚本程序,对第17行的限定条件进行修改:
1 from z3 import *
2
3 C1=0x5D7E0D1F2E0F1F84
4 C2=0x388D76AEE8CB1500
5 C3=0xD2E9EE7E83C4285B
6
7 inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
8
9 s = Solver()
10 s.add(i1==inp*C1)
11 s.add(i2==RotateRight (i1, i1 & 0xF))
12 s.add(i3==i2 ^ C2)
13 s.add(i4==RotateLeft(i3, i3 & 0xF))
14 s.add(i5==i4 + C3)
15 s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17 s.add(outp & 0xFFFFFFFF == inp & 0xFFFFFFFF)
18
19 print s.check()
20 m=s.model()
21 print m
22 print (" inp=0x%X" % m[inp].as_long())
23 print ("outp=0x%X" % m[outp].as_long())
上述程序证明,确实存在这种解:
sat
[i1 = 14869545517796235860,
i3 = 8388171335828825253,
i5 = 6918262285561543945,
inp = 1370377541658871093,
outp = 14543180351754208565,
i4 = 10167065714588685486,
i2 = 5541032613289652645]
inp=0x13048F1D12C00535
outp=0xC9D3C17A12C00535
在此基础上,我们再添加一个约束条件——验证是否存在“最后16位是0x1234”的解:
1 from z3 import *
2
3 C1=0x5D7E0D1F2E0F1F84
4 C2=0x388D76AEE8CB1500
5 C3=0xD2E9EE7E83C4285B
6
7 inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
8
9 s = Solver()
10 s.add(i1==inp*C1)
11 s.add(i2==RotateRight (i1, i1 & 0xF))
12 s.add(i3==i2 ^ C2)
13 s.add(i4==RotateLeft(i3, i3 & 0xF))
14 s.add(i5==i4 + C3)
15 s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17 s.add(outp & 0xFFFFFFFF == inp & 0xFFFFFFFF)
18 s.add(outp & 0xFFFF == 0x1234)
19
20 print s.check()
21 m=s.model()
22 print m
23 print (" inp=0x%X" % m[inp].as_long())
24 print ("outp=0x%X" % m[outp].as_long())
即使有如此苛刻的约束条件,程序仍然算出了一个解:
sat
[i1 = 2834222860503985872,
i3 = 2294680776671411152,
i5 = 17492621421353821227,
inp = 461881484695179828,
outp = 419247225543463476,
i4 = 2294680776671411152,
i2 = 2834222860503985872]
inp=0x668EEC35F961234
outp=0x5D177215F961234
Z3函数的破解速度非常快。这说明原始算法十分脆弱,根本算不上加密算法。业余人员自制的算法多数都是这样。
这种方法是否可以破解那些真正专业的加密算法呢?虽然像AES、RSA这样的加密算法都同样属于大规模方程组,但是其计算规模非常之大,以至于未来几年的计算机系统都不可能对其进行破解。当然,加密学专家非常清楚这件事。
总而言之,SMT/SAT求解程序(例如Z3)可以处理非专业的加密算法。
另外,我还写过一篇与Z3有关的博客。有兴趣的读者可查阅参考文献[Yur12]。
[1] 单向函数的有关概念,可参见本书第34章的介绍。
[2] 可参见本书第41章。
[3] http://z3.codeplex.com/。