前言

上一篇了解了一下差分分析,这次我们结合一道CTF题目聊一聊线性分析

同属于选择明文的差分分析不同,线性分析属于已知明文攻击方法,它通过寻找明文和密文之间的一个“有效”的线性逼近表达式,将分组密码与随机置换区分开来,并再此基础上进行密钥恢复攻击。

在正式介绍线性分析之前,我们还是要介绍一下相关的基础概念,参考《分组密码的攻击方法与实例分析》一书

基础概念

首先还是给出和差分分析一样的一个迭代分组密码的加密流程

迭代分组密码的加密流程

内积

线性掩码

线性逼近表达式

线性壳

迭代分组密码的一条 i 轮线性壳是指一对掩码(β0,βi),其中β0 是输入掩码,βi 是输出掩码。

线性特征

线性壳的线性概率

线性特征的线性概率

线性区分器

先给出一个命题,对{0,1}^n 上的随机置换R,任意给定掩码α,β,α ≠ 0,β ≠ 0, 则 LP(α,β ) = 0,​ 即 偏差 ε​(α,β) = 0

如果我们找到了一条 r-1 轮线性逼近表达式 (α,β),其线性概率 LP(α,β) ≠ 0,即偏差 ε(α,β) ≠ 0。则利用该线性逼近表达式可以将 r-1 轮的加密算法与随即置换区分开来,利用该线性区分器就可以对分组密码进行密钥恢复攻击。假设攻击 r 轮加密算法,为获得第 r 轮的轮密钥的攻击步骤如下。
步骤1

寻找一个 r – 1轮的线性逼近表达式 (α,β) ,设其偏差为ε(α,β),使得 |ε(α,β)|较大。

步骤2

根据区分器的输出,攻击者确定要恢复的第 r 轮轮密钥 k_r(或者其部分比特):设攻击的密钥比特长度为 l,对每个可能的候选密钥gk_i, 0 ≤ i ≤ 2^l -1,设置相应的 2^l 个计数器λ_i,并初始化。

步骤3

均匀随机地选取明文 X,在同一个未知密钥 k 下加密,(一般是让拥有密钥地服务端帮你加密)获得相应地密文 Z, 这里选择明文地数目为 m ≈ c ·(1/ε ^2),c 为某个常数。

步骤4

对一个密文Z,我们用自己猜测地第 r 轮轮密钥 gk_i(或其部分比特)对其进行第 r 轮解密得到Y_{r-1},然后我们计算线性逼近表达式 α x · X ⊕ β · Y_{r-1} 是否为0,若成立,则给相应计数器λ_i 加 1

步骤5

将 2^l 个计数器中|λ/m – 1/2| 最大地指所对应地密钥 gk_i(或其部分比特)作为攻击获得地正确密钥值。

Remark 针对步骤1中,我们如何去寻找一个高概率地 r -1 轮线性逼近表达式呢?例如针对一个S盒,我们可以选择穷举所有的输入、并获得相应的输出,然后穷举输入掩码、输出掩码,去获取这个S盒的相关线性特性。

下面就根据一道CTF中出现的赛题来解释分析上述过程。

实例-[NPUCTF2020]EzSPN

task.py

import os
from binascii import hexlify, unhexlify
import Crypto.Random.random as random
from secret import flagSZ = 8
coef = [239, 163, 147, 71, 163, 75, 219, 73]
sbox = list(range(256))
random.shuffle(sbox)
sboxi = []
for i in range(256):sboxi.append(sbox.index(i))def doxor(l1,l2):return [x[0]^x[1] for x in zip(l1,l2)]def trans(blk):res = []for k in range(0, SZ, 8):bits = [bin(x)[2:].rjust(8,'0') for x in blk[k:k+8]]for i in range(8):res.append(int(''.join([x[(i+1) % 8] for x in bits]),2))res[k:k+8] = [(coef[i] * res[k+i]) % 256 for i in range(8)]return resdef encrypt_block(pt, ks):cur = doxor(pt, ks[:SZ])cur = [sbox[x] for x in cur]cur = trans(cur)cur = [sboxi[x] for x in cur]cur = doxor(cur, ks[SZ:])return curdef encrypt(pt, k):x = 0 if len(pt)%SZ==0 else (SZ-len(pt)%SZ)pt += [x]*xct = ''for i in range(0, len(pt), SZ):res = encrypt_block([x for x in pt[i:i+SZ]], k)ct += ''.join(["{:02x}".format(xx) for xx in res])return ctdef doout(x):if len(x) % 16:x = (16 - len(x) % 16) * "0" + xreturn xdef doin(x):return list(unhexlify(x))def genkeys():return list(os.urandom(2*SZ))if __name__ == "__main__":print(sbox)key = genkeys()ct = encrypt(flag, key)print(ct)while True:pt = doin(input())print(doout(encrypt(pt, key)))

题目提供一个交互,能够加密你的输入并返回密文。所以这里是否可以采用差分分析的攻击方法呢,但这里我们讨论的线性分析,所以我们就用线性分析来做这道题目里。

那么看到这里所用加密系统的流程。

关键函数是encrypt_block(pt, ks),

首先是一个明文和一半的密钥异或,然后是进入S盒(题目每次随机生成并提供S盒具体内容),然后trans一下,再进入一个逆S盒,最后再和另一半的密钥异或。

看到这个trans函数,这里有一个int(‘’.join([x[(i+1) % 8] for x in bits]),2),这个(i+1%8)有点类似位移的效果,然后是乘以了一个系数,但这个系数已经定死了,所以没有什么关系。

首先测一个这个trans函数的一个位移效果,大概类似这样


然后整个流程图(画的丑了点)

那么针对这道题,我们利用线性分析攻击手法,

**步骤1,**我们去分析S盒的线性特性以找到使得线性表达式偏差大的掩码对(α,β)。我们穷举所有的S盒的可能的输入并计算出相应的输出,然后穷举所有的输入、输出掩码的组合,然后根据其是否符合满足线性逼近表达式 α · X ⊕ β · F(X,K) = 0 来更新计数器,最后我们计算相应的偏差表offset

def linearSbox():global linearInput    for i in range(256):si = sbox[i]for j in range(256):for k in range(256):a = bitxor(i, j) # 线性估计输入b = bitxor(si, k) # 线性估计输出 if a == b:offset[j][k] += 1for i in range(256):offset[i] = [abs(x - 128) / 256 for x in offset[i]]for linearOutput in range(256):cur = [x[linearOutput] for x in offset]linearInput.append(cur.index(max(cur)))

其中j是输入掩码,k是输出掩码。

这里的offset以输入掩码为行,输出掩码为列,所以这里的cur是获取同一输出掩码下不同输入掩码的偏差

并且在linearInput中记录下同一输入掩码下使得线性表达式偏差最大的输出掩码。

这里我们简单看一下输出的offset表

可以看到列表的第一行很特殊,除了第一个是0.5这个极大的值以外,其他都是0。因为在输入掩码为0、输出掩码也为0的话,肯定是能够满足的情况下,针对所有的输入、输出肯定是都能满足线性逼近表达式的,所以偏差达到了最大,0.5。但是换做其他输出掩码的话,针对所有的输出,其最后的表达式的结果分布得很均匀,所以偏差为0。

再举第二行第一列的0.03131…这个值的含义:就是针对所有的256个输入,当输入掩码为1,输出掩码为1时的线性逼近表达式的偏差。即有 256 * (0.03+0.5) ≈ 135 个输入满足(或者不满足)这个线性逼近表达式。

有了S盒的线性特性之后,步骤2是设置一个计数器counter,然后我们开始进入步骤3搜集明文密文对。就随机选择明文,发送给服务端,然后接收服务端返回的密文即可。

接着开始步骤4,由于这里有两轮加密,然后我们在步骤1已经生成了关于单个S盒的一个offset表,这个就相当于是 1 轮线性特征了,那么我们接下来就按照线性分析的攻击思路,

这里我们按字节猜测密钥,首先拿一个密文的第一个字节,异或第一个字节的密钥(枚举)后从S逆盒出去,然后把系数coef除掉,再根据P盒,把这个值换到相应的位置,然后这里我们根据S盒的线性特征选取合适的输出、输入掩码对我们的这对输入进行测试。若满足线性逼近表达式,则True计数加一,否则False计数加一。

最后步骤5,针对每个字节我们都测一万对,取结果偏差最大的key值。这样就完成了密钥每个字节的猜测。

结合代码再细细讲一遍

def calcOffset(pt, ct, j, guessed_key):  # 猜测第j段子密钥pt = list(unhexlify(pt))ct = list(unhexlify(ct))ct[j] ^= guessed_keyct[j] = sbox[ct[j]] # sbox即为sboxi的逆ct[j] = (ct[j] * coef[j]) % 256u1 = bitxor(pt[0], linearInput[1 << ((6 - j) % 8)])u2 = bitxor(ct[j], 0b10000000)if u1 == u2:return Trueelse:return Falsedef linearAttack():key2 = []for i in range(8): # 第二轮子密钥的第i段count = [0 for _ in range(256)]for guessed_key in range(256):print("[+] Cracking key...({}-{})".format(i, guessed_key))for j in range(10000):if calcOffset(plain[j], cipher[j], i, guessed_key) == True:count[guessed_key] += 1bias = [abs(x - 5000) / 10000 for x in count]key2.append(bias.index(max(bias)))return key2

linearAttack()中我们主要关注那个循环。这里调用了calcOffset函数,传入了一对明文密文对(plain[j], cipher[j])、索引(i)、以及猜测的密钥(guessed_key)。

在calcOffset中,首先用密文异或了guessed_key,然后根据值获取从Sbox的输入,然后乘以了coef(这里已经是原来coef的逆了),接着是u2 = bitxor(ct[j], 0b10000000),用了0b10000000作为掩码,即选取了密文的第一个bit。然后选取 linearInput[1 << ((6 – j) % 8)] 做为明文第一个字节的掩码。

为什么只选明文的第一个字节,以及其掩码的选取这里可能看的不是很直观,因为这里其实包含了两部分。

我们需要回去注意到P盒。当这里j=0,即爆破第一个字节的密钥时,我们选取的是密文的第1个字节,掩码选的是第1个bit,那么这是从P盒输入的哪个bit传来的呢,从P盒我们可以看到这是P盒输入的第一个字节的第2bit传来的,那么P盒输入的第2bit,实际上就是S盒输出的第2bit,也就是S盒的输出掩码为0b01000000,也就是1 << 6。而这个 linearInput 存的就是输出掩码为某个值的使得线性逼近表达式偏差最大的输入掩码。
linearAttack()中我们主要关注那个循环。这里调用了calcOffset函数,传入了一对明文密文对(plain[j], cipher[j])、索引(i)、以及猜测的密钥(guessed_key)。

在calcOffset中,首先用密文异或了guessed_key,然后根据值获取从Sbox的输入,然后乘以了coef(这里已经是原来coef的逆了),接着是u2 = bitxor(ct[j], 0b10000000),用了0b10000000作为掩码,即选取了密文的第一个bit。然后选取 linearInput[1 << ((6 – j) % 8)] 做为明文第一个字节的掩码。

为什么只选明文的第一个字节,以及其掩码的选取这里可能看的不是很直观,因为这里其实包含了两部分。

我们需要回去注意到P盒。当这里j=0,即爆破第一个字节的密钥时,我们选取的是密文的第1个字节,掩码选的是第1个bit,那么这是从P盒输入的哪个bit传来的呢,从P盒我们可以看到这是P盒输入的第一个字节的第2bit传来的,那么P盒输入的第2bit,实际上就是S盒输出的第2bit,也就是S盒的输出掩码为0b01000000,也就是1 << 6。而这个 linearInput 存的就是输出掩码为某个值的使得线性逼近表达式偏差最大的输入掩码。

当我们选取密文第二个字节的时候,根据P盒,第二个字节第1个bit来自于明文第一个字节第3个bit,也就是0b00100000,也就是1 << 5,同理可推

所以这里只用选明文的第一个字节,密文不同字节的第一个比特,且输入掩码为 linearInput[1 << ((6 – j) % 8)]。

另外可能还有一个问题,S盒的输入不是明文啊,应该是明文异或了第一个段密钥。确实,但是试想一下,在线性逼近表达式的这个式子中,密钥部分相互异或后,最后无非就是0或者1,而且由于密钥固定,所以针对固定的输入掩码,这个值是固定的。对于返回结果没有影响或者只是一个取反的效果。而我们是根据偏移来判断的,所以正并不影响结果,key的具体的值在这里也就没什么影响。

举个例子,如果输入掩码是0b00110011,输入掩码是0b00001111,那么由于密钥的存在,线性逼近表达式就是这么写:X_2 ⊕ X_3 ⊕ X_6⊕ X_7 ⊕ K_2 ⊕ K_3⊕ K_6⊕ K_7 = Y_4⊕ Y_5 ⊕ Y_6 ⊕ Y_7

其中K_2 ⊕ K_3⊕ K_6⊕ K_7 这部分无非就是等于 0 或者 1,对于结果没有影响或者只是一个取反的效果,对偏差则没有影响。

爆破出第二部分key的每一个字节之后,我们就可以逆算法流程。由于S盒是双射的,用明文、密文、第二部分key就可以去恢复第一部分的key,然后再写一个解密函数就可以解决这道问题了,

来自出题人的exp:

import os, sys
from binascii import hexlify, unhexlify
from pwn import *SZ = 8
offset = [[0 for i in range(256)] for i in range(256)]  #Sbox线性估计offset
linearInput = []
sbox, sboxi, plain, cipher = [], [], [], []
enc_flag = None
coef = [15, 11, 155, 119, 11, 99, 83, 249]def getData(ip, port):global enc_flag, sbox, sboxiio = remote(ip, port)sbox_str = io.recvline()sbox = eval(sbox_str)for i in range(256):sboxi.append(sbox.index(i))enc_flag = io.recvline().strip().decode("utf8")for i in range(10000):print("[+] Getting data...({}/10000)".format(i))pt = hexlify(os.urandom(8)).decode("utf8")plain.append(pt)io.sendline(pt)ct = io.recvline().strip().decode("utf8")print(ct,pt)cipher.append(ct)io.close()def doxor(l1, l2):return [x[0] ^ x[1] for x in zip(l1, l2)]# 线性层
def trans(blk):res = []for k in range(0, SZ, 8):cur = blk[k:k+8]cur = [(cur[i] * coef[i]) % 256 for i in range(8)]bits = [bin(x)[2:].rjust(8, '0') for x in cur]bits = bits[-1:] + bits[:-1]for i in range(8):res.append(int(''.join([x[i] for x in bits]), 2))return resdef bitxor(n, mask):bitlist = [int(x) for x in bin(n & mask)[2:]]return bitlist.count(1) % 2# Sbox线性估计
def linearSbox():global linearInput    for i in range(256):si = sbox[i]for j in range(256):for k in range(256):a = bitxor(i, j) # 线性估计输入b = bitxor(si, k) # 线性估计输出 if a == b:offset[j][k] += 1for i in range(256):offset[i] = [abs(x - 128) / 256 for x in offset[i]]for linearOutput in range(256):cur = [x[linearOutput] for x in offset]linearInput.append(cur.index(max(cur)))def calcOffset(pt, ct, j, guessed_key):  # 猜测第j段子密钥pt = list(unhexlify(pt))ct = list(unhexlify(ct))ct[j] ^= guessed_keyct[j] = sbox[ct[j]] # sbox即为sboxi的逆ct[j] = (ct[j] * coef[j]) % 256u1 = bitxor(pt[0], linearInput[1 << ((6 - j) % 8)])u2 = bitxor(ct[j], 0b10000000)if u1 == u2:return Trueelse:return Falsedef linearAttack():key2 = []for i in range(8): # 第二轮子密钥的第i段count = [0 for _ in range(256)]for guessed_key in range(256):print("[+] Cracking key...({}-{})".format(i, guessed_key))for j in range(10000):if calcOffset(plain[j], cipher[j], i, guessed_key) == True:count[guessed_key] += 1bias = [abs(x - 5000) / 10000 for x in count]key2.append(bias.index(max(bias)))return key2def getkey(key2):ct = list(unhexlify(cipher[0]))pt = list(unhexlify(plain[0]))cur = doxor(ct, key2)cur = [sbox[x] for x in cur]cur = trans(cur)cur = [sboxi[x] for x in cur]key = doxor(cur, pt) + key2return keydef decrypt_block(ct, key):cur = doxor(ct, key[SZ:])cur = [sbox[x] for x in cur]cur = trans(cur)cur = [sboxi[x] for x in cur]cur = doxor(cur, key[:SZ])return curdef decrypt(ct, key):pt = b''for i in range(0, len(ct), SZ * 2):block_ct = list(unhexlify(ct[i : i + SZ * 2]))block_pt = decrypt_block(block_ct, key)pt += bytes(block_pt)return ptif __name__ == "__main__":getData('node4.buuoj.cn', 27125)linearSbox()key2 = linearAttack()key = getkey(key2)print(key)flag = decrypt(enc_flag, key)print(flag)

那么对于这道简单的两轮分组密码的线性分析就到此告一段落了,但对于分组密码的学习不会到此为止,因为,还有高阶差分、截断差分、不可能差分……

最后

我有收集整理了一些学习的资料与工具,有想要得朋友,可以给个关注私我,发给你

【资料了解】

【入门建议收藏】密码学学习笔记之线性分析入门篇——EzSPN相关推荐

  1. [学习笔记]多元线性回归分析——理解篇

    回归分析是数据分析中最基础最重要的分析工具,绝大多数的数据分析问题,都可以使用回归的思想来解决.回归分析的任务就是,通过研究自变量x和因变量y的相关关系,尝试去解释y的形成机制,进而达到通过x去预测y ...

  2. 学习笔记之Qt从入门到精通(三)

    整理日期: 2010年4月9日 本文是学习笔记之Qt从入门到精通(二)的接续 Part 3: 进阶学习 Qt4 学习笔记 Qt 可以运行在不同的平台,像是Unix/X11.Windows.Mac OS ...

  3. 《Angular4从入门到实战》学习笔记

    <Angular4从入门到实战>学习笔记 腾讯课堂:米斯特吴 视频讲座 二〇一九年二月十三日星期三14时14分 What Is Angular?(简介) 前端最流行的主流JavaScrip ...

  4. Bootstrap学习笔记01【快速入门、栅格布局】

    Java后端 学习路线 笔记汇总表[黑马程序员] Bootstrap学习笔记01[快速入门.栅格布局][day01] Bootstrap学习笔记02[全局CSS样式.组件和插件.案例_黑马旅游网][d ...

  5. Flowable学习笔记(一、入门)

    转载自  Flowable学习笔记(一.入门) 一.Flowable简介 1.Flowable是什么 Flowable是一个使用Java编写的轻量级业务流程引擎.Flowable流程引擎可用于部署BP ...

  6. 前端学习笔记:Bootstrap框架入门

    前端学习笔记:Bootstrap框架入门 一.Bootstrap概述 1.基本信息 ​Bootstrap,来自 Twitter,是目前很受欢迎的前端框架.Bootstrap 是基于 HTML.CSS. ...

  7. 《Java Web开发入门很简单》学习笔记

    <Java Web开发入门很简单>学习笔记 1123 第1章 了解Java Web开发领域 Java Web主要涉及技术包括:HTML.JavaScript.CSS.JSP.Servlet ...

  8. unity3D-游戏/AR/VR在线就业班 C#入门访问修饰符学习笔记

    unity3D-游戏/AR/VR在线就业班 C#入门访问修饰符学习笔记 点击观看视频学习:http://edu.csdn.NET/lecturer/107 访问修饰符 public --公共的,在哪里 ...

  9. 【台大郭彦甫】Matlab入门教程超详细学习笔记二:基本操作与矩阵运算(附PPT链接)

    Matlab入门教程超详细学习笔记二:基本操作与矩阵运算 前言 一.基本操作 1.把matlab当作计算器使用 2.变量 3.控制格式输出 二.矩阵运算 1.矩阵 2.矩阵索引 3.使用:创建向量 4 ...

最新文章

  1. R语言glmnet交叉验证选择(alpha、lambda)拟合最优elastic回归模型:弹性网络(elasticNet)模型选择最优的alpha值、模型最优的lambda值,最终模型的拟合与评估
  2. mybatis-plus 多列映射成数组_JavaScript 为什么需要类数组
  3. ASP网站精品源码集合(免积分下载)
  4. vb6 combo根据index显示_彻底搞懂CSS层叠上下文、层叠等级、层叠顺序、z-index
  5. ON DUPLICATE KEY UPDATE
  6. 简单几行代码申请权限
  7. 一个bootstrap.css的使用案例
  8. 【通信】基于matlab多径衰落信道仿真【含Matlab源码 338期】
  9. 什么?你的私钥泄漏了?
  10. 又是整数划分(poj1032)
  11. 价值1680元的python实战全套教学视频
  12. 7-76 打印选课学生名单 (25分)
  13. 输入一英文字符串(字符串长度限制在200个字符以内),单词间仅用一个或多个空格间隔(即没有标点符号),编写程序将此字符串中第1个最长的单词输出。 输入输出样例: memory has no re
  14. 蜻蜓飞过,从此智能硬件厂商有了儿童梦工厂
  15. VS中的debug和releasse版本的区别
  16. 【数据挖掘实操】用文本挖掘剖析近5万首《全唐诗》
  17. js数组方法及其返回值(简单用法)
  18. K8S集群应用市场安装部署:第一篇
  19. 如何使用群晖nas快速收集多份文件?
  20. 投资大师巴菲特长期投资理念

热门文章

  1. Database之SQLSever:SQL命令实现查询之多表查询、嵌套查询、分页复杂查询,删除表内重复记录数据、连接(join、left join和right join简介及其区别)等案例之详细攻略
  2. AI:周志华老师文章《关于强人工智能》的阅读笔记以及感悟
  3. Keras之MLPR:利用MLPR算法(3to1【窗口法】+【Input(3)→(12+8)(relu)→O(mse)】)实现根据历史航空旅客数量数据集(时间序列数据)预测下月乘客数量问题
  4. DL之DNN优化技术:神经网络算法简介之数据训练优化【mini-batch技术+etc】
  5. Py之SnowNLP:SnowNLP中文处理包的简介、安装、使用方法、代码实现之详细攻略
  6. 发现你的身形——OpenCV图像轮廓
  7. 深入浅出统计学 第四五章 离散概率的计算与分布
  8. 【数据结构复习】(1)绪论
  9. Katalon Studio之swagger中的API导入
  10. 桐花万里python路-高级篇-并发编程-03-线程