古典密码


古典密码

移位密码

简单移位密码

密码和编码最大的区别是多了一个密钥k,一般用m代表明文,c代表密文。

移位密码是将明文根据密钥进行了位置的变换而得到的密文,举个例子:

m = "flag{easy_easy_crypto}"
k = "3124"

首先以k的长度切分m如下:

flag {eas y_ea sy_c rypt o}

然后按照密钥3124的顺序对每一部分密钥进行变化:

lafg ea{s _eya y_sc yprt }o

所以密文为lafgea{s_eyay_scyprt}o

再说攻击策略,一般有两种:爆破和语义分析,爆破首先爆破字段长度然后爆破顺序,具体此处不细叙。

Python加密解密代码如下

def shift_encrypt(m, k):
    l = len(k)
    c = ""
    for i in range(0, len(m), l):
        tmp_c = [""] * l  # 初始化列表
        if i + l > len(m):
            tmp_m = m[i:]
        else:
            tmp_m = m[i:i + l]
        for kindex in range(len(tmp_m)):
            tmp_c[int(k[kindex]) - 1] = tmp_m[kindex]
        c += "".join(tmp_c)
    return c
def shift_decrypt(c, k):
    l = len(k)
    m = ""
    for i in range(0, len(c), l):
        tmp_m = [""] * l
        if i + l > len(c):
            tmp_c = c[i:]
            use = []
            for kindex in range(len(tmp_c)):
                use.append(int(k[kindex]) - 1)
            use.sort()
            for kindex in range(len(tmp_c)):
                tmp_m[kindex] = tmp_c[use.index(int(k[kindex]) - 1)]  
                # index返回第一个匹配的索引位置
        else:
            tmp_c = c[i:i + l]
            for kindex in range(len(tmp_c)):
                tmp_m[kindex] = tmp_c[int(k[kindex]) - 1]
        m += "".join(tmp_m)
    return m
m = "flag{easy_easy_crypto}"
k = "3124"
c = shift_encrypt(m, k)
print(c)
print(shift_decrypt(c, k))
# lafgea{s_eyay_scyprt}o
# flag{easy_easy_crypto}

云影密码

云影密码仅包含01248五个数字,其中0用于分割,其余数字用于做加和操作之后转换为明文,Python解码如下

def c01248_decode(c):
    l=c.split("0")
    origin="abcdefghijklmnopqrstuvwxyz"
    r=""
    for i in l:
        tmp=0
        for num in i:
            tmp+=int(num)
        r+=origin[tmp-1]
    return r
print(c01248_decode("8842101220480224404014224202480122"))
# welldone

栅栏密码

栅栏密码密钥只有一个数字k,表示栅栏长度,将加密的明文分成k个一组,然后取每组的一个字符依次连接,拼接而成就是密文。样例如下:

m = "flag{zhalan_mima_hahaha}"
k = 4

首先每4个分一组

flag {zha lan_ mima _hah aha}

然后依次取出每组的第一个字符组成一组,再依次取出第2个、第3个、第4个:

f{lm_alzaihhahnmaaga_ah}

而栅栏密码的解密方法就是加密的逆过程,Python加解密代码如下

def zhalan_encrypt(m,k):
    chip=[]
    for i in range(0,len(m),k):
        if i+k>=len(m):
            tmp_m=m[i:]
        else:
            tmp_m=m[i:i+k]
        chip.append(tmp_m)
    c=""
    for i in range(k):
        for tmp_m in chip:
            if i<len(tmp_m):
                c+=tmp_m[i]
    return c
def zhalan_decrypt(c,k):
    l=len(c)
    partnum=l//k
    if l%k!=0:
        partnum+=1
    m=[""]*l
    for i in range(0,l,partnum):
        if i + partnum>=len(c):
            tmp_c=c[i:]
        else:
            tmp_c=c[i:i+partnum]
        for j in range(len(tmp_c)):
            m[j*k+i//partnum]=tmp_c[j]
    return "".join(m)
m="flag{zhalan_mima_hahaha}"
k=4
c=zhalan_encrypt(m,k)
print(c)
print(zhalan_decrypt(c,k))
# f{lm_alzaihhahnmaaga_ah}
# flag{zhalan_mima_hahaha}

替代密码

单表替代密码

凯撒密码

凯撒密码通过将字母移动一定的位数来实现加密和解密,明文中所有的字母都在字母表上向后(或向前)按照一个固定的数目移动偏移后被替换成密文。因此,位数就是凯撒密码加密和解密的密钥因为只考虑可见字符,并且都是ASCⅡ码,所以128是模数。而只对凯撒密码的爆破就是爆破密钥K。具体见以下代码:

def caesar_encrypt(m, k):
    r = ""
    for i in m:
        r += chr((ord(i) + k) % 128)
    return r
def caesar_decrypt(c, k):
    r = ""
    for i in c:
        r += chr((ord(i) - k) % 128)
    return r
def caesar_brute(c, match_str):
    result = []
    for k in range(128):
        tmp = caesar_decrypt(c, k)
        if match_str in tmp:
            result.append(tmp)
    return result
m = "flag{kaisamima}"
k = 3
c = caesar_encrypt(m, k)
print(c)
print(caesar_decrypt(c, k))
c1 = "39.4H/?BA2,0.2@.?J"
print(caesar_brute(c, "flag{"))
# iodj~ndlvdplpd 
# flag{kaisamima}
# ['flag{kaisamima}']

事实上,以上并不是一般的凯撒密码,一般的凯撒密码只对字母作变换。具体实现大同小异,不赘述。

ROT13

在凯撒密码中有一种特例,当k=13,并且只用于大小写英文字母的时候,称之为ROT13.而且值得注意的是ROT13的加密和解密函数是一样的(因为英文字母只有26个,+13和-13效果一样)。

另外ROT13通常会作用在MD5,flag等字符串上,MD5中的字符只有“ABCDEF”,对应的ROT13为”NOPQRS”,flag对应的是”SYNT”,如果看到这些就可以识别出是ROT13,样例如下

def rot13(m):
    r = ""
    for i in m:
        if ord(i) in range(ord('A'), ord('Z') + 1):
            r += chr((ord(i) + 13 - ord('A')) % 26 + ord('A'))
        elif ord(i) in range(ord('a'), ord('z') + 1):
            r += chr((ord(i) + 13 - ord('a')) % 26 + ord('a'))
        else:
            r += i
    return r
c = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
print(rot13(c))
print(rot13(rot13(c)))
print(codecs.decode(c,"rot13"))
# iodj~ndlvdplpd 
# flag{kaisamima}
# ['flag{kaisamima}']

还有其他ROT,有在线网站https://www.qqxiuzi.cn/bianma/ROT5-13-18-47.php。

埃特巴什码

埃特巴什码的替代表不是通过移位得到的,而是通过对称获得的。其通过将字母表的位置完全镜面对称后获得字母的替代表,然后进行加密。Python代码如下

def atabsh_encode(m):
    alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    Origin=alphabet+alphabet.lower()
    TH_A=alphabet[::-1]
    TH_a=alphabet.lower()[::-1]
    TH=TH_A+TH_a
    r=""
    for i in m:
        tmp=Origin.find(i)
        if tmp!=-1:
            r+=TH[tmp]
        else:
            r+=i
    return r
m="flag{ok_atabsh_flag}"
c=atabsh_encode(m)
print(atabsh_encode(c))
print(c)
# uozt{lp_zgzyhs_uozt}
# flag{ok_atabsh_flag}

经典单表替代密码

经典单表替代密码就是用一个替代表对每一个位置的字符进行查表替换。因为是单表替换,所以没有替代表时,爆破的难度会比较高,一般来说会给出一段具有足够语言意义的密文,然后使用词频统计的方法进行攻击。在有替代表的情况下的加解密代码如下

def substitution_encode(m, k, origin="abcdefghijklmnopqrstuvwxyz"):
    r = ""
    for i in m:
        if origin.find(i) != -1:
            r += k[origin.find(i)]
        else:
            r += i
    return r
def substitution_decode(c, k, origin="abcdefghijklmnopqrstuvwxyz"):
    r = ""
    for i in c:
        if k.find(i) != -1:
            r += origin[k.find(i)]
        else:
            r += i
    return r
m = "flag{good_good_study}"
k = "zugxjitlrkywdhfbnvosepmacq"
c = substitution_encode(m, k)
print(c)
print(substitution_decode(c, k))
# iwzt{tffx_tffx_osexc}
# flag{good_good_study}

培根密码

培根密码一般使用两种不同的字体代表密文,密文的内容不是关键所在,关键是字体。使用AB代表两种字体,五个一组,表示密文,明密文对应表如下所示

a AAAAA g AABBA n ABBAA t BAABA
b AAAAB h AABBB o ABBAB u-v BAABB
c AAABA i-j ABAAA p ABBBA w BABAA
d AAABB j ABAAB q ABBBB x BABAB
e AABAA k ABABA r BAAAA y BABBA
f AABAB l ABABB s BAAAB z BABBB

也可以使用在线工具解密:http://rumkin.com/tools/cipher/baconian.php

图形密码

猪圈密码和跳舞的小人都是典型的图形替代密码,图形替代是通过将明文用图形替代以实现加密。猪圈密码使用不同的格子来表示不同的字母;跳舞的小人源自《福尔摩斯探案集》,是使用小人图案来表示不同的字母,同时用举旗子来表示单词结束。具体此处不细说,遇到可在网上查找对应表。

以下给图形密码做一些简单列举,遇到了直接对应搜索即可。

猪圈密码
猪圈密码变种圣堂武士密码
跳舞的小人
我的世界附魔台
标准银河字母

仿射密码

仿射密码替代表的生成方式依据:$c=am+b\ mod\ n$,其中m为明文对应字母得到的数字,n为字符数量,c为密文,a和b为密钥。在拥有密钥的情况下,解密只需要求出a关于n的逆元即可,即$m=invert(a)\times(c-b)\ mod\ n$,关于逆元会在后面叙述,此处略过。

因为明密文空间一样,所以n很容易得知。那么在没有密钥的情况下,一般有以下几种思路:第一种是爆破,在密钥空间小的情况下可以这么做;第二种因为仿射密码也是单表替代密码的特例,字母也是一一对应的,所以也可以使用词频统计进行攻击;第三种是已知明文攻击,如果我们知道了任意两个字符的明密文对,那么我们可以推理出密钥ab。具体实现如下

from gmpy2 import invert
def affine_encode(m, a, b, origin="abcdefghijklmnopqrstuvwxyz"):
    r = ""
    for i in m:
        if origin.find(i) != -1:
            r += origin[(a * origin.index(i) + b) % len(origin)]
        else:
            r += i
    return r
def affine_decode(c, a, b, origin="abcdefghijklmnopqrstuvwxyz"):
    r = ""
    n = len(origin)
    ai = invert(a, n)
    for i in c:
        if origin.find(i) != -1:
            r += origin[(ai * (origin.index(i) - b)) % len(origin)]
        else:
            r += i
    return r
def affine_guessab(m1, c1, m2, c2, origin="abcdefghijklmnopqrstuvwxyz"):
    x1 = origin.index(m1)
    x2 = origin.index(m2)
    y1 = origin.index(c1)
    y2 = origin.index(c2)
    n = len(origin)
    dxi = invert(abs(x1 - x2), n)
    if x1 - x2 < 0:
        dxi *= -1
    a = dxi * (y1 - y2) % n
    b = (y1 - a * x1) % n
    return (a, b)
m = "affinecipher"
a = 5
b = 8
c = affine_encode(m, a, b)
print(c)
print(affine_decode(c, a, b))
print(affine_guessab('a', 'i', 'f', 'h'))
# ihhwvcswfrcp
# affinecipher
# (mpz(5), mpz(8))

多表替代密码

棋盘密码

Playfair、Polybius和Nihilist均属于棋盘类密码。此类密码的密钥为1个5×5的棋盘。棋盘的生成符合如下条件:顺序随意;不得出现重复字母;i和j可视为同一字(也有讲q去除的,以保证总数为25个)。生成棋盘后,不同的加密方式使用了不同的转换方式。生成棋盘代码如下

def gen_cheese_map(k, use_Q=True, upper=True):
    k = k.upper()
    k0 = ""
    origin = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    for i in k:
        if i not in k0:
            k0 += i
    for i in origin:
        if i not in k0:
            k0 += i
    if use_Q == True:
        k0 = k0[0:k0.index("J")] + k0[k0.index("J") + 1:]
    else:
        k0 = k0[0:k0.index("Q")] + k0[k0.index("Q") + 1:]
    if upper == False:
        k0 = k0.lower()
    assert len(k0) == 25
    r = []
    for i in range(5):
        r.append(k0[i * 5:i * 5 + 5])
    return r
print(gen_cheese_map("helloword"))
# ['HELOW', 'RDABC', 'FGIKM', 'NPQST', 'UVXYZ']

Playfair根据明文的位置去寻找新的字母。首先将明文字母两两一组进行切分,并按照如下规则进行加密。

1)若两个字母不同行也不同列,则需要在矩阵中找出另外两个字母(第一个字母对应行优先),使这四个字母成为一个长方形的四个角。

2)若两个字母同行,则取这两个字母右方的字母(若字母在最右方则取最左方的字母)。

3)若两个字母同列,则取这两个字母下方的字母(若字母在最下方则取最上方的字母)。

变换、加密和解密代码如下

def _playfair_2char(tmp, map):
    for i in range(5):
        for j in range(5):
            if map[i][j] == tmp[0]:
                ai = i
                aj = j
            if map[i][j] == tmp[1]:
                bi = i
                bj = j
    if ai == bi:
        axi = ai
        bxi = bi
        axj = (aj + 1) % 5
        bxj = (bj + 1) % 5
    elif aj == bj:
        axj = aj
        bxj = bj
        axi = (ai + 1) % 5
        bxi = (bi + 1) % 5
    else:
        axi = ai
        axj = bj
        bxi = bi
        bxj = aj
    return map[axi][axj] + map[bxi][bxj]
def playfair_encode(m, k="", cheese_map=[]):
    m = m.upper()
    origin = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    tmp = ""
    for i in m:
        if i in origin:
            tmp += i
    m = tmp
    assert k != "" or cheese_map != []
    if cheese_map == []:
        map = gen_cheese_map(k)
    else:
        map = cheese_map
    m0 = []
    idx = 0
    while idx < len(m):
        tmp = m[idx:idx + 2]
        if tmp[0] != tmp[1]:
            m0.append(tmp)
            idx += 2
        elif tmp[0] != "X":
            m0.append(tmp[0] + "X")
            idx += 1
        else:
            m0.append(tmp[0] + "Q")
            idx += 1
        if idx == len(m) - 1:
            if tmp[0] != "X":
                m0.append(tmp[0] + "X")
                idx += 1
            else:
                m0.append(tmp[0] + "Q")
    r = []
    for i in m0:
        r.append(_playfair_2char(i, map))
    return r
def _playfair_2char_decode(tmp, map):
    for i in range(5):
        for j in range(5):
            if map[i][j] == tmp[0]:
                ai = i
                aj = j
            if map[i][j] == tmp[1]:
                bi = i
                bj = j
    if ai == bi:
        axi = ai
        bxi = bi
        axj = (aj - 1) % 5
        bxj = (bj - 1) % 5
    elif aj == bj:
        axj = aj
        bxj = bj
        axi = (ai - 1) % 5
        bxi = (bi - 1) % 5
    else:
        axi = ai
        axj = bj
        bxi = bi
        bxj = aj
    return map[axi][axj] + map[bxi][bxj]
def playfair_decode(c, k="", cheese_map=[]):
    assert k != "" or cheese_map != []
    if cheese_map == []:
        map = gen_cheese_map(k)
    else:
        map = cheese_map
    r = []
    for i in c:
        r.append(_playfair_2char_decode(i, map))
    return "".join(r)
m = "Hide the gold in the tree stump"
k = "playfairexample"
c = playfair_encode(m, k)
print(c)
print(playfair_decode(c, k))
# ['BM', 'OD', 'ZB', 'XD', 'NA', 'BE', 'KU', 'DM', 'UI', 'XM', 'MO', 'UV', 'IF']
# HIDETHEGOLDINTHETREXESTUMP

playfair也有在线网站http://rumkin.com/tools/cipher/playfair.php

Polybius密码相对简单一点(Nihilist原理相同),只用棋盘的坐标作为密文,可能不一定是数字,坐标可以用字母表示,解密也是参照map去寻找对应的明文,代码如下

def polybius_encode(m, k="", name="ADFGX", cheese_map=[]):
    m = m.upper()
    assert k != "" or cheese_map != []
    if cheese_map == []:
        map = gen_cheese_map(k)
    else:
        map = cheese_map
    r = []
    for x in m:
        for i in range(5):
            for j in range(5):
                if map[i][j] == x:
                    r.append(name[i] + name[j])
    return r
def polybius_decode(c, k="", name="ADFGX", cheese_map=[]):
    assert k != "" or cheese_map != []
    if cheese_map == []:
        map = gen_cheese_map(k)
    else:
        map = cheese_map
    r = ""
    for x in c:
        i = name.index(x[0])
        j = name.index(x[1])
        r += map[i][j]
    return r
m = "helloworld"
k = "abcd"
c = polybius_encode(m, k)
print(c)
print(polybius_decode(c, k))
# ['DF', 'AX', 'FA', 'FA', 'FG', 'XD', 'FG', 'GD', 'FA', 'AG']
# HELLOWORLD

维吉尼亚密码

凯撒密码是单表替代密码,其只使用了一个替代表,维吉尼亚密码则是标准的多表替代密码。

\normalsize 首先,多表替代密码的密钥不再是固定不变的,而是随着位置发生改变的。在维吉尼亚密码中,根据密钥的字母来选择。比如密钥是LOVE,那么明文会没四组一个循环,使用的密钥如表所示

L-LMNOPQRSTUVWXYZABCDEFGHIJK
O-OPQRSTUVWXYZABCDEFGHIJKLMN
V-VWXYZABCDEFGHIJKLMNOPQRSTU
E-EFGHIJKLMNOPQRSTUVWXYZABCD

明文的第一个位置用L加密,二三四用OVE,到第五个位置回到L,一般情况下,维吉尼亚密码的破解必须依靠爆破+词频统计的方法来进行,这里有一个破解网站:http://www.quipqiup.com/index.php(具体使用参见网站说明)

对于自己破解维吉尼亚密码来说,其实也是词频统计,因为间隔密钥长度地字符使用的替代表是相同的。所以,如果密文长度足够长,并且知道密钥长度,是可以通过词频统计的方法进行破解的。

关于密钥的长度,可以使用卡西斯基试验和弗里德曼试验来获取,卡西斯基试验是类似于the这样的常用单词,如果使用了重复的密钥加密,那么两个相同的连续串的间隔将是密钥长度的倍数。获取这样的值并计算最大公约数,就可以得到密钥长度。

其它破解网站

https://www.guballa.de/vigenere-solver

希尔密码

希尔密码是运用基本矩阵原理的替换密码,由Lester S.Hill在1929年发明。将每个字母当作二十六进制数字:A=0,B=1,C=2,依次类推。将一串字母当成你、维向量,与一个n×n的矩阵相乘,再将得出的结果模26。注意,用作加密的矩阵(即密钥)在$Z_{26}^n$​中必须是可逆的,否则就不可能译码。只有矩阵的行列式与26互质才是可逆的。例如明文act:
$$
\begin{bmatrix}
0 \newline
2 \newline
19
\end{bmatrix}
$$
密钥:
$$
\begin{bmatrix}
6 & 24 & 1 \newline
13 & 16 & 10 \newline
20 & 17 & 15
\end{bmatrix}
$$
加密过程为:
$$
\begin{bmatrix}
6 & 24 & 1 \newline
13 & 16 & 10 \newline
20 & 17 & 15
\end{bmatrix}
$$
加密过程为:
$$
\begin{bmatrix}
6 & 24 & 1 \newline
13 & 16 & 10 \newline
20 & 17 & 15
\end{bmatrix}
\begin{bmatrix}
0 \newline
2 \newline
19
\end{bmatrix}
\equiv
\begin{bmatrix}
67 \newline
222 \newline
319
\end{bmatrix}
\equiv
\begin{bmatrix}
15 \newline
14 \newline
7
\end{bmatrix}
$$
所以密文为POH


文章作者: Phoenix
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Phoenix !
  目录