Crypto:
blurred_memory:
题目:
1 | from random import sample |
分析:
- 输入一串字符串,先进行分割处理,如下:
输入:x_1x_2x_3…x_n\ 得到:[x_1,x_2,x_2,x_3,x_3,x_3,…,x_n,x_n]
- 接下来将处理数组的每个元素在有限域GF(257)下做n次幂乘,然后求和添入out中。
大致形式如下:
1 | out[0] = x0 + 2*x1 + 3*x2 + ... + n*x_{n-1}(modp) |
我们的目标是从输出的和中,求出每一个元(x_n)。
先进行一些未知数、可用式子的估计,看看从哪里下手:
通过简单的求和判断可以知道,我们的未知数一共是22个,可用的式子大致55个(没记错的话),而且这个式子的形式化成矩阵之后可以发现:很像范德蒙德矩阵,但是由于方程组是非线性的,我们也没办法直接通过常规的高斯消元之类的办法解出来。
那么现在就想到有两种办法去解这种问题:
- 通过多项式小根求解的办法(Grobner基)去求多元的根。
- 通过化简多项式的办法,找出根之间的关系,进行消元求解。
所以一开始的思路就自然而然的想要用Gröbner基,去求每一个根。如下:
1 | p = 257 |
求六七个根还好,到后面直接爆了。
队内的师傅告诉我说这个是不行的,因为groebner_basis()的时间复杂度是O(n),添加的元越多,指数越高,到后面就越难搞,至于题目中的22元更加不用说了,所以思路一自然而然地Fail了。
考虑从化简的地方下手:
先别把变量合并。那这是对称多项式方程组,找了一下symmetric poly system求解方法,牛顿对称多项式能转成初等对称多项式(两者关系叫牛顿恒等式)。而且这些多项式恰对应一个单变量多项式方程的解。从而变元成了一个,自然能求。——密码爷爷
化简的办法如下(化简核心原理):文章在这
所以求解大致步骤:
- 用牛顿恒等式,求初等对称多项式的值
- 找到与之相关的253次首一多项式,其他系数是交错的初等对称多项式的值
- 直接在Fp上解253次方程。甚至直接代数进去验证就行
那么exp如下:
exp:
1 | #SageMath |
sort-teaser:
题目:
1 | import re |
分析:
题目很长,得先做源审,看看能输入什么和处理什么:
- 我们可以定义一个functionA,通过输入一些二元数学运算符和定义一些参数,对这个functionA进行100轮运算。(注意只能是顺序结构,而且不能注入其他指令)
- 对于每一轮的结果,都会把flag_array作为参数A代入计算,将参数B作为结果保存。
- 经过100轮计算后,如果发现第一轮的计算结果与flag相同,那么将给出flag。
简单地摆弄了一下题目之后,当真无奈了(赛后复盘发现确实是自己的想象力不够充分)。
那么通过一定的复盘,发现这道题其实和sql注入这种东西有种同工异曲之处,利用异常判断,去注入一些“搜索”命令,使得程序给出的反馈形成"Oracle"。
以下将给出队内师傅的解法:
exp:
这里注意到两种不同情况。如果100次测试输出不是完全相同,那么会抛出一个assert异常;而如果全都相同,但不等于要求的结果,则会给出另一种提示。从这里入手作区分,可以利用类似侧信道的思想去泄露出一些东西出来。
首先可以测出flag的长度,利用右移,不断调整移位长度即可:
1 | 'B=A>>232' |
接着泄露字符。由于flag是静态的,而每次nc限制30行指令,因此需要爆破+分段来处理。指令大概就是按字符去比较,每次比较6个字符刚好极限30行(或许有更优的方法)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 from pwn import *
from Crypto.Util.number import *
import re,string,itertools
from hashlib import sha256
#context.log_level = 'debug'
#sh=remote('202.112.238.82','13371')
#sh=process(['python3','sort-teaser.py'])
global sh
sla = lambda a,b :sh.sendlineafter(str(a),str(b))
sa = lambda a,b :sh.sendafter(str(a),str(b))
lg = lambda name,data : sh.success(name + ": 0x%x" % data)
se = lambda payload: sh.send(payload)
rl = lambda : sh.recvline()
rv = lambda n : sh.recv(n)
sl = lambda payload: sh.sendline(payload)
ru = lambda a :sh.recvuntil(str(a))
rud = lambda a :sh.recvuntil(str(a),drop=True)
#泄露字符种类
def test(o,i):
global sh
#sh=process(['python3','sort-teaser.py'])
sh=remote('202.112.238.82','13371')
rl()
sl(f'C={o}')
sl('T=A&255')
sl('N=0')
sl(f'A=A>>{i*6*8}')
for _ in range(6):
sl('F=A&255')
sl('G=C==F')
sl('N=N+G')
sl('A=A>>8')
sl('B=T&N')
sl('EOF')
res=b'Traceback' in rl()
sh.close()
return res
chars=[]
for o in range(32,126):
tmp=0
for i in range(5):
if test(o,i):
tmp+=1
if tmp==5:
chars.append(o)
print(chars)
s=''.join(map(chr,chars))
print(s,len(s))
print(bytes_to_long(s.encode()))
sh.interactive()
'''
[49, 65, 67, 70, 80, 84, 95, 97, 99, 100, 101, 103, 104, 108, 110, 114, 115, 116, 123, 125]
1ACFPT_acdeghlnrst{} 20
'''
flag字符种类20个,长度为29。看来确实需要统计次数,然而这个不咋好弄,想了个办法先把2次以上的统计出来,具体就是在位运算上做了点trick。严格意义上不是精确统计,但概率还是比较大(100次里至少1次发生2个以上相同字符出现在6个字符里)。
1 | #统计出现2次以上的字符 |
后面差3个字符,懒得改trick了,连蒙带猜出来的。
1 | res='1AACFPTT___acdeeeghlnnnrsst{}' |
确实蛮震撼的。
复现不动另外几个密码了,感觉有点诡异:
这里贴一下部分的wp:
sort三连:WP
Reverse:
apple:
题目:
1 | flag = "QWERTYUIOPASDFGHJKLZXCVBNM01234567" |
队内大手子贴心地帮我们逆完了程序,直接给出测试样例和Python源码,那么这时候的工作就和干密码分析、逆向差不多了。
分析:
- 首先这个源码因为是直接硬扒下来做源审的,通过测试样例之后可以确定有效性,那么就确定可以当做密码题来写了。接下来从上到下去分析这个加密的逻辑、以及逆向的思路
- Encrypt1:将输入的flag进行一定的乱序排序(不完全无规则,后文会给出原因)。
- Encrypt2:将输入的flag进行类似于DES中S盒、P盒一样的变换,将源输入拓展成num1,num2,并异或后再与掩码异或输出。
- Encrypt3:类似于背包加密一样,在bit级别程度上,将输入的比特每一位分散到final_result每一位的上面。
像这种纯对称的,照着逻辑一点点拼回去就行,难度没什么大的,但是为什么要在这里写这篇分析?(因为我解出来了)
因为这里可以注意到输出结果的时候是丢失了四位密文的(就在最后一步输出那里),简单地想一下,如果我照着完全密文去逆,然后求出Encrypt1那部分的输出(因为还原第一部分还要处理一个乱序,这个后面再说)。时间复杂度会不会太高了?我们需要爆破4位字符!(大小写字母、数字、特殊字符),加上一堆运算,这个能跑是能跑,但是得跑半天都有可能。
那么这里给出写这篇分析的理由:我们可以用一定的办法去缩减这种情况的复杂度!
总之,先把正常情况下,无信息缺失的解密逆出来,然后再考虑信息缺失的情况:
part1:逆向操作
1 | from Crypto.Util.number import * |
除了encrypt1都弄好了。正常的输出(无缺失的情况下),也是可以逆的(程序解密无问题)。
在爆破之前,想一个问题,这个缺失的四字符是会对每一个密文块都有影响吗?好像确实不是,虽然这个程序仿造了DES的一些处理逻辑,但是这个缺失的四字符并不是构成逆解密的唯一解!我们是有机会通过其他情况(解密有效字符较多的时候),得到部分密文前缀的。
例如:假设我现在的字典只有大写字母,这个爆破的时间复杂度完全是可以接受的,所有的解密里面,出现了那么几组有效字符位置相同,内容相同的情况,我们是否可以认为这种情况下撞对了部分字符?只要撞出一个字符,后面三个的爆破就是轻而易举了!
照着这个思路去碰,碰第一组得到Z,X,z,x开头前缀情况下得到的有效信息很多,那么根据ascii码判断可以知道,前缀肯定是在这个范围区间里面,第一位大概率是X,Z——或是旁边的相关特殊字符等。
如下尝试:
1 | valid_char = [ord(x) for x in string.digits + |
我们可以利用上述的思路很快的碰出前缀:[ecGT,然后得到乱序的密文:
1 | s5vrn6__3C{_R_PTPTACC}-43LrhelWerr4e0ruwBv4oF3 |
part2:
再去分析第一段加密的逻辑:
- 可以发现排序的id是按照首字母大小排的(很大概率)
- 取字符的时候是取该字母的前一位
那么我们有如下的映射关系:
![1}P1AVR)RIGF
CPRADL@R.png)
下面的flag2是正常排序,flag的乱序后输出的,上下位大概会是一一对应的映射关系,我们可以得到两字母的顺序,通过组合起来,极大概率能得到flag:
1 | flag2 = 's5vrn6__3C{_R_PTPTACC}-43LrhelWerr4e0ruwBv4oF3' |
2万种可能,看起来很迷惑,再丢进去加密看结果是否和原密文一样就好:
1 | def encode(flag): |
可惜拿的是四血,没有播报,sad。
虽然是逆向,但是着实是密码手魅力时刻了。
最后总结一下这次比赛的收获:
- 大比赛很多方向的考点的内容都是和其他方向联动互通的,单学密码学的话这把确实输得挺惨。有一些内容可能会在WEB的注入里面出现过,有些逆向分析的技巧和手法确实会对写脚本和分析挺有用的。
- 关于多项式的根、多项式的性质运算技巧之类的(又学一轮grobner basis),期间还翻了一堆small_root的解法,学到了蛮多办法的(除了Coppersmith)。
- 逆向,逆个痛快了。
当然还有一些密码题解,例如nanoOTP那个,之后大概率还会再弄弄,去年的revenge,虽然去年全场都是非预期的(
比赛时候发现本地的话去年的办法还是可以通的,远程就不行了,这几天听别队师傅透露了一些解法,大概率会再学学,学好了再发(