原理知识
异或运算想必大家刚接触编程语言的时候就有学习到,它是一个二元运算符,符号常用 $\oplus$,在编程语言中常为 ^
,运算性质也是非常的简单,我们设两个输入分别为 $A,B$ 输出为 $C$,于是可以得到下表。
A | B | C |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
可以看见异或运算与加法类似,唯一不同的是最后一行。于是异或运算也称为 “模二加“,即$$A\oplus B = C \iff A+B \equiv C \pmod 2$$异或运算还有如下性质:
交换律:$A ⊕ B = B ⊕ A$
结合律:$A ⊕ (B ⊕ C) = (A ⊕ B) ⊕ C$
恒等律:$A ⊕ 0 = A$
自 逆:$A ⊕ A = 0$
这几条性质结合起来使得异或运算具有对合性,即 $f(f(x))=x$,
举个例子,设 $f(x) = x \oplus B$,那么$$f(A) =A \oplus B = C$$
$$f(f(A)) = (A \oplus B) \oplus B = A$$
正是因为异或运算的对合性,再加上其在计算机中可以高效运行,于是异或运算成为了现代密码学中对称密码的基石。
我们先来看一个简单的例子,来自 cryptohack
1234 |
KEY1 = a6c8b6733c9b22de7bc0253266a3867df55acde8635e19c73313KEY2 ^ KEY1 = 37dcb292030faa90d07eec17e3b1c6d8daf94c35d4c9191a5e1eKEY2 ^ KEY3 = c1545756687e7573db23aa1c3452a098b71a7fbf0fddddde5fc1FLAG ^ KEY1 ^ KEY3 ^ KEY2 = 04ee9855208a2cd59091d04767ae47963170d1660df7f56f5faf |
解题:
我们已知 KYE1 和 KEY2 ^ KEY1,我们对这两个数进行异或操作得到: KYE1 ^ KEY2 ^ KEY1
使用交换律,得到: KYE1 ^ KEY1 ^ KEY2
根据自逆,我们有:0 ^ KEY2
根据恒等律,我们得到了:KEY2
以此类推,我们可以使用 KEY2 和 KEY2 ^ KEY3 得到 KYE3
使用KEY1,KEY2,KEY3 和 FLAG ^ KEY1 ^ KEY3 ^ KEY2 得到 FLAG
当然,比较简单的方法,就是一次性使用 KEY1, KEY2 ^ KEY3 和 FLAG ^ KEY1 ^ KEY3 ^ KEY2 ,
将他们异或在一起就可以直接得到 FLAG。
一次一密(One Time Pad)
然后我们来看看宇宙无敌的具有无条件安全的一次一密。
Vernam 的一次一密于1917年获得专利,是一种极其简单、完全保密的密码系统,尽管效率很低。密钥 $k$ 由一串二进制数字 $k_0,k_1,…,k_N$ 组成 。它用于加密二进制明文字符串 $m_0,m_1,…,m_N$ ,通过将两个字符串逐位异或运算在一起。然后密文 $c=c_0,c_1,…,c_N$ 由下式给出$$c_i=k_i\oplus m_i \forall i\in{0,1,\dots,N}$$每个密钥只使用一次,然后被丢弃,系统的名称也由此产生。由于每个密钥的使用概率相等,并且只有一个密钥将给定的 $m$ 加密到给定的 $c$,即密钥 $m⊕ c$。可以证明的是 Vernam 的一次一密具有无条件安全性。
举一个简单的例子就是加密者可以使用密钥 $1$ 加密 $1$ 得到密文 $1\oplus 1=0$,但是破解者在没有密钥的情况下无法判断密文 $0$ 是由 $1\oplus 1$ 计算得来的,还是由 $0 \oplus 0$ 得来的。一个比特有 $2$ 种可能性,那么 $n$ 个比特就有 $2^n$ 种可能性。
不过也正是因为效率原因,并且也存在密钥分发问题,一次一密在现实中的应用并没有推广开来。(属于是胎死腹中)
多次一密(Many Times Pad)
既然一次一密效率太低了,那么如果多次重复使用一个较短的密钥可行么?我们先来看下面这个问题
2019 De1CTF XORZ
12345678910111213 |
from itertools import *from data import flag,plainkey=flag.strip("de1ctf{").strip("}")assert(len(key<38))salt="WeAreDe1taTeam"ki=cycle(key)si=cycle(salt)cipher = ''.join([hex(ord(p) ^ ord(next(ki)) ^ ord(next(si)))[2:].zfill(2) for p in plain])print cipher# output:# 49380d773440222d1b421b3060380c3f403c3844791b202651306721135b6229294a3c3222357e766b2f15561b35305e3c3b670e49382c295c6c170553577d3a2b791470406318315d753f03637f2b614a4f2e1c4f21027e227a4122757b446037786a7b0e37635024246d60136f7802543e4d36265c3e035a725c6322700d626b345d1d6464283a016f35714d434124281b607d315f66212d671428026a4f4f79657e34153f3467097e4e135f187a21767f02125b375563517a3742597b6c394e78742c4a725069606576777c314429264f6e330d7530453f22537f5e3034560d22146831456b1b72725f30676d0d5c71617d48753e26667e2f7a334c731c22630a242c7140457a42324629064441036c7e646208630e745531436b7c51743a36674c4f352a5575407b767a5c747176016c0676386e403a2b42356a727a04662b4446375f36265f3f124b724c6e346544706277641025063420016629225b43432428036f29341a2338627c47650b264c477c653a67043e6766152a485c7f33617264780656537e5468143f305f4537722352303c3d4379043d69797e6f3922527b24536e310d653d4c33696c635474637d0326516f745e610d773340306621105a7361654e3e392970687c2e335f3015677d4b3a724a4659767c2f5b7c16055a126820306c14315d6b59224a27311f747f336f4d5974321a22507b22705a226c6d446a37375761423a2b5c29247163046d7e47032244377508300751727126326f117f7a38670c2b23203d4f27046a5c5e1532601126292f577776606f0c6d0126474b2a73737a41316362146e581d7c1228717664091c |
根据题面我们有 $c_i = p_i \oplus k_i \oplus s_i$
其中由于 $s_i$ 我们是知道的,因此可以得到 $p_i \oplus k_i = c_i \oplus s_i$
于是我们先爆破密钥 $k$ 的长度,想法很简单,我们认定明文 $plain$ 都是由可见字符组成,并且我们的 $k$ 是 flag,显然肯定也都是可见字符(我们假设只有字母数字和下划线)。因此我们先将密文按照某一长度进行分组,假设长度为 3,那么分组结果为
123 |
[0,3,6,9...][1,4,7,10...][2,5,8,11...] |
然后对组内每一个元素异或所有的可见字符,将结果中的可见字符放入集合最终取交集。结果不为空的说明长度正确。
12345678910111213141516171819202122232425262728293031323334353637383940 |
from Crypto.Util.number import *from Crypto.Util.strxor import strxorfrom string import printablekeytable = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"ci = "..."s = b"WeAreDe1taTeam"ci = bytes.fromhex(ci)# c = p^k = ci^sc = strxor(ci,(s*((len(ci)//len(s))+1))[:len(ci)])def bruce_printable(s):TMP = set()for each in printable:tmp = chr(ord(each)^s)if tmp in keytable:TMP.add(tmp)return TMPfor k_len in range(1,38):candidates = []for j in range(k_len):block = [c[k_len*i+j] for i in range(len(c)//k_len)]candidate=set()init = Truefor eachbyte in block:tmp_candidate=bruce_printable(eachbyte)if init:candidate = tmp_candidateinit = Falseelse:candidate &= tmp_candidateif candidate:candidates.append(candidate)#print(candidates)if k_len == len(candidates):print("[+]",k_len) |
运行脚本我们得到 key 的长度为 30,(如果将 key 的范围变大,会得到更多的可能长度)
随后我们对 key 的每一个字节进行爆破,规则与上述方法一样,先分组,然后组内的所有字节与单个密钥异或,如果解密均为可见字符,说明该密钥字符可能正确。
12345678910111213141516 |
k_len = 30key_candidates=[]for j in range(k_len):block = [c[k_len*i+j] for i in range(len(c)//k_len)]keyi_candidate = []for keyi in keytable:for eachbyte in block:if chr(ord(keyi)^eachbyte) not in plaintable:breakelse:keyi_candidate.append(keyi)if not keyi_candidate:print("[-] error")exit()key_candidates.append(keyi_candidate)print(key_candidates) |
结果得到非常多的候选集和,于是我们将plain的字符集缩小
1
|
plaintable="1234567890qwertyuiopasdfghjklxcvbnmQWERTYUIOPSDFGHJKLZXCVBNM\'` ;,."
|
得到的候选集合就大大缩小了:
1
|
[['W'], ['3', '4'], ['k', 'l'], ['c'], ['0', '7'], ['j', 'm'], ['3'], ['t'], ['H', 'O'], ['j'], ['h', 'o'], ['1'], ['n'], ['u'], ['5', '9'], ['2', '5'], ['r', 'u'], ['n'], ['1'], ['o'], ['j'], ['O'], ['t'], ['3'], ['m'], ['0', '7'], ['c', 'd'], ['k', 'l'], ['3', '4'], ['W']]
|
最后根据解密明文再一一尝试我们最终能够得到 flag:W3lc0m3tOjo1nu55un1ojOt3m0cl3W
在上述解题过程中我们发现,如果密文数据越多,明文和密钥的字符集越小,我们就越能够准确的恢复出密钥,并解密得到明文。于是我们思考,如果这里被加密的明文是完全随机的呢?每个字节都有 256 种可能。如此,重复使用密钥还有关系么?
2023 SEECTF-non-neutrality
出题人题解:https://demo.hedgedoc.org/8qpnrgxeTiSKCl4WueGIHA?view
123456789101112131415161718 |
from secrets import randbitsfrom Crypto.Util.number import bytes_to_longimport osflag = os.environ.get('FLAG', 'SEE{not_the_real_flag}').encode()# get a one-time-pad in which not exactly half the bits are setdef get_xorpad(bitlength): xorpad = randbits(bitlength) return xorpad if bin(xorpad).count('1') != bitlength // 2 else get_xorpad(bitlength)def leak_encrypted_flag(): return bytes_to_long(flag) ^ get_xorpad(len(flag) * 8)# I managed to leak the encrypted flag a lot of timesif __name__ == '__main__': for _ in range(2**16): print(leak_encrypted_flag()) |
题目随机生成一个比特串,与固定的flag字符串进行异或然后输出,总共给了 65536 组数据。乍一看觉得和上面的题目差不多,但这里被加密的是完全随机的比特串,我们无法再以可见字符为标准以进行区分。
但这里的比特串还是有一点限制,在于随机比特串的 1 比特和 0 比特的数量必然是不相等的。我们记比特串 1 比特的数量为其汉明重量,然后测试密文得到比特串长度是 272,因此随机比特串的汉明重量 不为 136。于是我们将所有生成的比特串的汉明重量进行分类:
- 汉明重量为奇数:$50%$
- 汉明重量等于136(被舍弃):${272 \choose 136}/2^{272}\approx 4.8%$
- 汉明重量为偶数但不等于136:$45.2%$
于是理论上用于加密 flag 的比特串的汉明重量奇偶比为:10:9,
我们检测一下密文的汉明重量奇偶比
1234 |
from collections import Counterfor x in open('nn_out.txt').readlines()] arr = [int(x) '1')%2 for x in arr)) print(Counter(bin(x).count(Counter({1: 34422, 0: 31114}) |
奇偶比为 10:9,并且我们还能够知道 flag 的汉明重量为偶数。
随后我们定义一个神器的函数 $f(x,y)$,其表示对于 $x$ 比特长的比特串,汉明重量奇偶性与 $y$ 相同但是不等于 $y$ 的可能取值数量。例如我们有 $f(5,3)=6$,因为 5 比特长,且汉明重量为奇数但不为 3,于是汉明重量只能是 1 和 5,于是所有可能为$$00001,00010,00100,01000,10000,11111$$不难推出的是 $f(x,y)=2^{x-1}-{x \choose y}$ 【奇偶性去掉一半,再去掉汉明重量等于 $y$ 的所有组合】
我们注意到这六个比特串的每一个比特位置,都是 4 个 0 比特和 2 个 1 比特。
这表示每个位置的取值可能性比为 “0”:”1” = 4 : 2。比较关键的点是这里可以用 $f$ 表示为 $f(4,3):f(4,2)$ 。可以利用组合数的性质(${n\choose m}={n-1\choose m}+{n-1\choose m-1}$)推一下有 $f(x,y)=f(x-1,y)+f(x-1,y-1)$。
于是我们准备解题:根据我们定义的 $f$ 函数,我们需要先把所有汉明重量为奇数的抛掉(这样剩余用于加密的随机比特串的汉明重量均为偶数)
12 |
arr = [x for x in arr if bin(x).count('1')%2 == 0]arrbits = np.array([[int(a) for a in f'{x:0272b}'] for x in arr]) |
然后根据我们已知的 flag 的格式 SEE{******************}
,将随机比特串的对应位置的比特确定下来。然后我们计算每个位置的比特可能性。
例如第一个符合要求的密文串为
3253110189075583532909324934537273169857320522865536346363834070130933364482802356
flag 能确定的位置分别是 SEE{}
所对应的位置和每一个字节的第一个比特(可见字符第一个比特都是0)总共 69 个比特,即
1234567891011 |
flag = 'SEE{?????????????????????????????}'flagbits = np.array(bits(flag.encode()))known = np.array([False] * 272)for i, c in enumerate(flag): if c == '?': known[8*i] = True else: known[8*i:8*i+8] = Trueprint(known) |
我们将密文串和已知部分的 flag 串异或,得到的比特串将有效位置记录下来,
12 |
0] != flagbits), -1) np.count_nonzero(known * (arrbits[35 |
能够得到其中 35 个 1 比特。于是这个随机比特串剩余部分就不可能有 $136-35=101$ 个 1 比特。总共有 $f(272-69,101)$ 种可能【剩余比特串汉明重量不为 101,但是需要是奇数,因为偶数(比特串总汉明重量)- 奇数(35:比特串总已知汉明重量)是奇数】。于是未知位置的比特为 “1” 或 “0” 对应的可能比为 $f(202,101):f(202,100)$。
这样一组数据就测完了,我们也能够得到 flag 在对应位置的比特值的可能比。
对于每一组我们都这样测下来,然后将 flag 对应位置的比特值可能性 相加*,根据最终可能概率来确定该处为 0 或者 1。
*相加:这里具体如何进行加权并没有一个明确的说法,但又是解题的关键。出题人在这里使用了对数,即每个位置加上 $log(f(x-1,y))-log(f(x-1,y-1))$。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546 |
from collections import Counterfrom pwn import bits, unbitsfrom scipy.special import binomimport numpy as np# determine the parity of our flagarr = [int(x) for x in open('nn_out.txt').readlines()]print(Counter(bin(x).count('1')%2 for x in arr))# Counter({1: 34422, 0: 31114})# This means the flag has an even number of set bits# set up array with our encrypted bitsarr = [x for x in arr if bin(x).count('1')%2 == 0]LEN = max(arr).bit_length()assert LEN == 272arrbits = np.array([[int(a) for a in f'{x:0272b}'] for x in arr])# set up array with our guesses for the flagflag = 'SEE{?????????????????????????????}'flagbits = np.array(bits(flag.encode()))# set up array of which positions are known. it's ASCII so every MSBit is setknown = np.array([False] * 272)for i, c in enumerate(flag): if c == '?': known[8*i] = True else: known[8*i:8*i+8] = True# this is the same function defined above, but in the log spacedef f(n,x): return np.log(2.0**(n-1) - binom(n,x))while n := sum(~known): # calculate our probabilities! this has a scale of -inf to inf rather than the usual 0 to 1 k = 136 - np.count_nonzero(known * (arrbits != flagbits), -1) probs = (f(n-1, k) - f(n-1, k-1)) @ (2 * arrbits - 1) # out of all the unknown bits, get the 10 with the best probability inds = np.nonzero(~known)[0] inds = inds[np.argsort(-abs(probs[inds]))[:10]] # set these flag bits to the corresponding sign flagbits[inds] = probs[inds] > 0 known[inds] = True # print our best estimate of the flag print(unbits(np.where(known, flagbits, probs > 0))) |
经过测试,发现每次确定10个比特我们能得到一个最接近正确答案的值:b'SEE{__litevally_any_bias_is_bad__}'
而正确的 flag 为:SEE{__literally_any_bias_is_bad__}
,仅差了一个比特。
可见,尽管只是对加密原文(随机比特串)有了一点点的限制(汉明重量不为某一个值),在数据量足够大(也就是密钥重复使用的次数足够多)的情况下,我们也能够最大程度的对密钥进行恢复。不过也因为这里的密钥是可见字符,这给我们提供了不小的帮助。
总结
异或真简单。
异或有点东西。
我不懂异或,懂不了一点呜呜呜呜呜。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可联系QQ 643713081,也可以邮件至 [email protected] - source:Van1sh的小屋
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论