Posts in Category: Cryptography

密码故事(下)

本系列为《密码故事》的书摘,共有上、中、下三篇,各篇链接及包含内容如下:

  • 上篇:单字母替换密码,多字母替换密码。
  • 中篇:加密的机械化,恩格玛机及其破解,象形文字和楔形文字的破解。
  • 下篇:公开密钥,PGP,量子密钥分发。

公开密钥

除了恩格玛机之外,德国还有一套更强大的加密法,称为洛伦兹密码(Lorenz cipher),用来加密希特勒和他的军官之间的通讯。布莱切利的两位破解员,约翰・蒂尔特曼(John Tiltman)和比尔・塔特(Bill Tutte)发现了洛伦兹密码的一个破绽,需要综合寻找、匹配、数据分析和谨慎的判断,而「炸弹」很难变通地应付洛伦兹密码的精妙之处。而布莱切利的数学家马克斯・纽曼(Max Newman)基于图灵的「万能机器」的概念,发明了可以针对不同问题进行自我调节的机器,现在我们称之为「可编程计算机」。1943 年,工程师汤米・弗劳尔斯(Tommy Flowers)无视布莱切利高级官员的怀疑,根据纽曼的蓝图,花了 10 个月时间,制造了科洛希斯(Colossus computer,其中总共有 1500 个电子管。事实上,科洛希斯正是现代数字计算机的前身。

然而,在二战结束后,汤米・弗劳尔斯根据命令将科洛希斯及其蓝图销毁,世界上第一台计算机的设计图就此消失。1945 年,宾夕法尼亚大学的约翰・墨西里(John Mauchly)和 J・蒲利斯普・埃克特(J. Presper Eckert)完成了 ENIAC,它含有 18000 个电子管,每秒能计算 5000 次。几十年来,ENIAC 而非科洛希斯被认为是计算机之母。

用计算机加密和机器加密只有三个重要的区别:

  • 密码机受限于实际建造中的困难,而计算机可以模拟出任何无限复杂的机器。
  • 速度上的区别:电子设备比机器的扰频器运转起来要快得多。
  • 最重要的区别:计算机加密的是数字而非字母。

即便如此,加密过程仍然是按照古老的原则进行的,这些原则就是移位和替换。每次加密,无论多么复杂,都可以还原成一些简单操作的组合。

随着越来越多的公司采用计算机,公司之间的通讯加密也越来越频繁,就出现了加密标准化的问题。1973 年,美国国立标准局正式征求一套标准的加密方案,能够使企业之间进行秘密通讯。IBM 的托马斯・J・沃森实验室(Thomas J Watson)的霍斯特・菲斯特尔(Horst Feistel)发明卢斯福密码机(Lucifer cipher)。1976 年,56 位版本的卢斯福密码机被国家安全局正式采用,称之为数据加密标准(Data Encryption Standard,即 DES)。选用 56 位版本(密钥数目 1018)是因为国家安全局相信这样的密钥对民间通讯来说是安全的,因为没有一个民间组织拥有如此强大的计算机能够在一个合理的时间内检查每个密钥,但国家安全局拥有世界上最快的计算机资源,刚好能破解卢斯福的密钥。

卢斯福系统的加密步骤如下所述:首先,电文翻译成一长段二进制数字串。然后,以每 64 个数字为一个单位,分解这段数字串。每个单位数字串分别独立加密。第三步,我们把注意力集中在每个单位数字串上。把 64 个数字像洗牌那样分成两组,每组 32 个数字,分别把它们叫做左 0 和右 0。右 0 中的数字加入「切碎机函数」中进行复杂的替换。然后,把「切碎」后的右 0 加到左 0 上,形成新的一组 32 个数字的序列,称作右 1。把最开始的右 0 标记为左 1。这一系列的操作称为一个「回合」。然后整个操作又重复做一次,不同的只是这一次以右 1 和左 1 开始,得到的数字序列称为右 2 和左 2。加密过程总共要进行 16 个「回合」。这个过程就像是和面一样:试想在一大块面上写着信息,首先,将这块面分成 64 厘米长的小块;然后,挑出其中的一半碾压后折叠起来,再加到另一半上拉长,从而形成一个新的面块。这个过程不断地重复,直到消息文字彻底地被面块混合起来。经过 16 个回合的「揉面」后,密文发送出去。另一端的接收者收到密文后,将加密过程反过来进行,从而解码出明文。「切碎」的具体实施方法是可以变化的,它取决于发送者和接收者达成的密钥。换言之,只要密钥不同,同样的电文可以用成千上万种不同的方式加密。

除了加密标准化问题之外,密码通讯中存在的另一个问题是「密钥分发」。20 世纪 70 年代,银行尝试雇用专职的密钥分发员,他们带着一个锁着的箱子(padlocked briefcase)走遍全世界,亲手将密钥交给客户。二战时,德国高级指挥部每个月都需要分发《每日密钥》月刊给所有的恩格玛机操作员。美国政府的密钥是 COMSEC(通讯安全局)掌管和分发的。20 世纪 70 年代,COMSEC 每天分发的密钥数以吨计,当装载着 COMSEC 密钥的船靠港时,密码分发员会到甲板上收集各种卡片、纸带以及软盘和其他一切贮存密钥的介质,再把它们分发给客户。计算机改变了密码的实施方式,但 20 世纪密码学最伟大的革命是发明了密钥分发的技巧,这被认为是两千年来自单字母替换密码发明以后最伟大的成就。

密码故事(中)

本系列为《密码故事》的书摘,共有上、中、下三篇,各篇链接及包含内容如下:

  • 上篇:单字母替换密码,多字母替换密码。
  • 中篇:加密的机械化,恩格玛机及其破解,象形文字和楔形文字的破解。
  • 下篇:公开密钥,PGP,量子密钥分发。

加密的机械化

20 世纪初,意大利物理学家古列尔莫・马可尼(Guglielmo Marconi)发明了无线电。无线电具有双重特性:易通信性和易拦截性,这在一战爆发后表现得尤为突出。所有成员都想充分发掘无线电的资源,却苦于不知如何保障安全。在 1914 年到 1918 年间,密码编码者提出了几个新密码,但一个个均被破解了。

ADFGVX 密码(ADFGVX cipher:是一战时德军使用的一种密码,在 1918 年 3 月 5 日首次使用。德国的密码编码师组成的一个小组从各种密码中选择了 ADFGVX 密码,他们相信它是不可破解的。这个密码的特点在于它错综复杂,综合了替换和移位两种处理方法。法国密码破译学家乔治斯・佩因芬(Georges Painvin)中尉在 6 月 2 日的晚上,破解了一段 ADFGVX 信息。佩因芬的突破导致了一系列其他密码依次被破解。ADFGVX 密码的破解成了一战期间密码学的典型。尽管产生了无数的新密码,但它们无非是 19 世纪已经破解了的密码的变形或组合。对于密码破译师来说,最大的问题是如何应付信息数量的急剧增长。一战期间,无线电的使用使得截获的信息激增。据估计法国在一战期间共截获的德国通讯信息达 1000 万个单词。

公元前 400 年,中国的孙武在他的著作《孙子兵法》中提到:「三军之事,莫亲于间,赏莫厚于间,事莫密于间。」大意为:在军队中,没有比间谍更为亲近的人,给予奖赏时,没有比间谍更为优厚的,没有什么事情比间谍更为秘密。法国人坚信孙武的话,在磨炼破译密码技能的同时,还发展了几个收集无线电情报的辅助技术。

法国监听员学会了如何辨别一个无线电操作员的「手迹」,一旦一条加密的信息以摩斯电码的形式发送出来,它就变成了一系列的点和短横,而我们可以通过分析每个无线电操作员的传送速度,他的停顿以及点和短横的相对长度来确定他们的身份。此外,法国人还建立了六个方向的搜索站,它们能够检测每个信息发自哪里。每个站点不断移动它的天线直到收到的信号最强,这就确定了信息源的一个方向。通过组合两个或两个以上站点的信息方向,就可能定位出敌方传送线的确切源头。再加上第一条信息即操作员的手迹,就可能确立某特定军营的身份和地点。法国的情报员可以在几天内跟踪它的走向,一般能推出某敌军部队的目的地和军事目的,这种形式的情报搜集被称作信道分析,它在一个新的密码出现后,前期显得非常有用。对于每个新密码来说,密码分析师可能暂时无法破解它,但即使一个信息无法破译,信道分析仍会提供一些有用的信息。

齐默尔曼的电报(Zimmermann Telegram:1917 年 1 月 17 日,英国截获了一个德军电报,由英国海军部的密码局「40 号房」的蒙哥马利教士(Reverend Montgomery)和曾为出版商的内格尔(Nigel de Grey)破解。这封电报由德国新上任的外交大臣阿瑟・齐默尔曼(Arthur Zimmermann)发出,齐默尔曼的策略是从 2 月 1 日起发动无限制潜艇战争,同时为了使美国保持中立,将和墨西哥联盟,说明墨西哥总统进攻美国,收回诸如得克萨斯、新墨西哥以及亚历桑那的领土,同时希望墨西哥总统说服日本也进攻美国,以此牵制美国使其无法派遣部队到欧洲。他将加密电报发给德国驻华盛顿的大使,再转交给德国驻墨西哥的大使,最后转交给墨西哥总统。

密码故事(上)

西蒙・辛格的《密码故事》生动有趣、引入入胜,但中文译本存在多处人名前后翻译不一致的现象,且原著中关于针孔加密法的示例也完全丢失,建议有条件的同学尽量阅读英文原著《The Code Book》。

本文对于专用词汇的翻译不仅做了统一,索性和通用的翻译对齐,所以和中文译本里的译名可能有出入。共有上、中、下三篇,各篇链接及包含内容如下:

  • 上篇:单字母替换密码,多字母替换密码。
  • 中篇:加密的机械化,恩格玛机及其破解,象形文字和楔形文字的破解。
  • 下篇:公开密钥,PGP,量子密钥分发。

单字母替换密码

隐文术:通过把信息隐藏起来的这种秘密通信称为 Steganography(隐文术),由希腊词 Steganos(意为「覆盖」)和 Graphein(意为「写」)派生而来。

希罗多德(Herodotus)以编年史的形式记载了公元前 5 世纪希腊和波斯之间的冲突,其中提到住在波斯的希腊人德马拉图斯(Demaratus)利用已上蜡的一副可折叠的刻写板,先将蜡刮去,再将波斯的阴谋刻写在木板的背面,然后再涂上蜡盖住消息,从而将信息秘密地传递给希腊人。得到警告的希腊人开始武装自己,并最终在波斯人进攻时佯装被包围,并诱使波斯船队进入海湾,运用策略打败了波斯。

希罗多德还讲述了希斯塔亚乌斯(Histaiaeus)的故事:希斯塔亚乌斯想鼓励米勒图斯(Miletus)的阿里斯塔哥拉斯(Aristagoras)反叛波斯国王,为了秘密地传达他的指示,希斯塔亚乌斯剃光了他的一个信使的头发,将信息写在其头皮上,再等信使的头发重新长起来,很明显这段历史时期发生的还不是什么紧急事件。这个信使显然不用携带任何异物,能够自由穿行,不会有麻烦。一旦到达目的地,他就剃光头发,指给联络人看。

中国古代将信息写在小块丝绸上,塞进一个小球里,再用蜡封上,然后让信使吞下这个蜡球。

16 世纪,意大利科学家乔瓦尼・波塔(Giovanni Porta)描述了如何将信息隐藏在一个煮熟的鸡蛋里:把少许明矾和一点醋混在一起制成一种墨水,再用这种墨水将信息写在鸡蛋壳表面。墨水溶液就会经蛋壳上的微孔渗透进去,在已凝固的鸡蛋白表面留下印迹,这样只能剥去蛋壳后才能读取。

早在公元 1 世纪普林尼(Pliny the Elder)就解释了体液如何用作隐形墨水。用体液写的字晾干后即变得透明,但轻轻地加热就能把液体烤焦,从而字迹就以棕色显现出来。许多有机液体都有这样的性质,因为它们富含碳因而很容易被烤焦。事实上,即使是现代间谍也很少知道在标准配备的隐形墨水用完之后还可以用自己的尿液来临时代替。

密码术:在隐文术发展的同时,还有另一种方法也在演化,那就是 Cryptography(密码术),从希腊词 Kryptos(意为「隐藏」)派生而来。密码术的目的不是隐藏信息本身,而是要隐藏它的意思,也就是一种加密的过程。

隐文术和密码术常常混合使用。例如,二战期间流行的微粒照片是一种隐文术。德军在拉丁美洲的间谍将一页文件缩小在直径不到 1 毫米的微型照片上,看上去就是一个点,再将这个点状照片贴在看似无关紧要的一封信的某个句号上面。这种微型照片在 1941 年第一次被美国联邦调查局发现后,德军间谍开始有所防范,在缩小之前,将照片中的信息打乱。

密码术分为两种,即移位和替换。在移位中,字母不变,位置改变;在替换中,字母改变,位置不变。

S. 中使用的加密法

S.》中使用到了四种经典的加密法。本文介绍这四种加密法的加解密方法、在书中的应用和解密过程。

波雷费加密法

波雷费加密法(Playfair cipher)由 Charles Wheatstone 于 1854 年发明,由 Lord Playfair 大力推广并以后者的名字命名。此加密法在第二次波尔战争和两次世界大战中都有应用,主要用于短时间内保护重要但非致命的信息。现代电子计算机的出现使得波雷费加密法在几秒钟内就会被破解,而不再被军方使用。

波雷费加密法首先需要生成一个 5×5 的矩阵,使用关键字或短语(去掉重复的字母)及剩下的英文字母按顺序填充这个矩阵。填充顺序可以是从左到右、从上到下填充,也可以是从最左上角开始、顺时针方向螺旋向内填充。由于英文字母有 26 个,而矩阵只有 25 个位置,因此可以去掉字母 J 或 Q,或者字母 I 和字母 J 使用同一格等。

加密时,将明文每两个字母作为一个二元组,对照上面生成的矩阵,对每个二元组依以下四条规则处理:

  • 如果二元组中两个字母相同,则将第二个字母替换为 X 再继续加密;如果只剩一个字母(即明文长度为奇数),则加上字母 X 再继续加密。也可以使用字母 Q 或其它任意不常用的字母。
  • 如果二元组中的两个字母在矩阵中位于同一行,则分别使用右边的字母替换(矩阵中最右边的字母则翻转到最左边的字母替换)。
  • 如果二元组中的两个字母在矩阵中位于同一列,则分别使用下面的字母替换(矩阵中最下面的字母则翻转到最上面的字母替换)。
  • 如果二元组中的两个字母在矩阵中既不在同一行,也不在同一列,则分别使用各自同一行且与对方同一列的字母替换(也就是对角上的两个字母,注意替换后二元组中两个字母的顺序)。

解密时,对密文两两组成的二元组使用上述后三条规则相反的操作处理(同行左移,同列上移,其余对角),并根据第一条规则去掉末尾不合理的字母 X 或 Q,或将字母 X 或 Q 替换为其前面的字母,即可得到明文。

双棘轮算法

PGP 自我扫盲

前言

久闻 PGP 大名,印象中是使用非对称加密算法,且经常能在邮件列表的签名档中看到一长串的 PGP Public Key,却一直不知其原理及用法。最近把这块知识盲区扫除并记录成文。

PGP、OpenPGP 和 GPG

PGP 是 Pretty Good Privacy 的缩写,由 Philip R. Zimmermann 于 1991 年编写并上传到 USENET 而在全球流行,被广泛用于文本、电子邮件、文件甚至整个磁盘分区的签名、加密和解密。上传行为因涉嫌违反美国的加密软件出口限制而使 Zimmermann 陷入长达三年的犯罪调查。政府的调查终止之后,Zimmermann 创立了 PGP 公司,PGP 由此成为商业软件(PGP 公司在辗转多次之后最终被 Symantec 收购)。

OpenPGP 是由 PGP 衍生出的开源规范,而 GnuPG(简称 GPG)就是遵循 OpenPGP 规范的 GNU 实现。在不需要特别说明差异的情况下,三者统称为 PGP。

OpenSSL AES 算法中 Key 和 IV 是如何生成的?

书接上回。在《LDAP 密码加密方式初探》一文中,使用 OpenSSL 命令 AES 算法加密解密时,都用到了 Key 和 IV 参数,那么这两个参数是如何生成的呢?

仍然以 AES-256-CBC 开始探索。先准备好生成 Key 和 IV 的 passphrase:

$ echo -n “drjom(&)(&)MOJRD” > passphrase

上述回文形式的 passphrase 来自一个神秘的组织:)

将此 passphrase 传入 openssl 命令生成对应的 Key 和 IV:

$ openssl enc -aes-256-cbc -kfile passphrase -md md5 -P -salt
salt=51D9C4B24C759179
key=BBF4EA0E7A0EBD7C60CCE2024E218A53BBB69CCA65B4D0B705E37080676E5F5D
iv =8E5EC1AC2191167DF9B753BA93A1E7B8

其中的 salt 是随机生成的,因此每次执行的结果并不相同。而 Key 和 IV 的生成方法参考 SuperUser 的一个回答补充及验证如下(注意 md5sum 输出的结果与上述 openssl 命令的输出结果比较):

$ perl -e ‘print pack “H“, “51D9C4B24C759179″‘ > salt
$ cat passphrase salt > hash1_128.tmp
$ md5sum hash1_128.tmp
bbf4ea0e7a0ebd7c60cce2024e218a53 hash1_128.tmp
$ perl -e ‘print pack “H
“, “bbf4ea0e7a0ebd7c60cce2024e218a53″‘ > hash1_128
$ cat hash1_128 passphrase salt > hash2_128.tmp
$ md5sum hash2_128.tmp
bbb69cca65b4d0b705e37080676e5f5d hash2_128.tmp
$ perl -e ‘print pack “H*”, “bbb69cca65b4d0b705e37080676e5f5d”‘ > hash2_128
$ cat hash2_128 passphrase salt > hash3_128.tmp
$ md5sum hash3_128.tmp
8e5ec1ac2191167df9b753ba93a1e7b8 hash3_128.tmp

可以看出,对于 AES-256-CBC 来说:

hash1_128 = MD5(Passphrase + Salt)
hash2_128 = MD5(hash1_128 + Passphrase + Salt)
hash3_128 = MD5(hash2_128 + Passphrase + Salt)
Key = hash1_128 + hash2_128
IV = hash3_128

Key 和 IV 分别就是 AES-256-CBC 的 Key 和 IV。

当没有 salt 时,上述过程仍然成立。先使用 openssl 命令带 -nosalt 选项生成 Key 和 IV:

$ openssl enc -aes-256-cbc -kfile passphrase -md md5 -P -nosalt
key=D5E483D8B90C02BD4D470BA8049E1FA61D64EB2BFA444CBF9853CDFB8B24DA7A
iv =304E9E87DB9C1C8101F605ED4DD0B9EB

分步验证如下(注意 md5sum 输出的结果与上述 openssl 命令的输出结果比较):

$ md5sum passphrase
d5e483d8b90c02bd4d470ba8049e1fa6 passphrase
$ perl -e ‘print pack “H“, “d5e483d8b90c02bd4d470ba8049e1fa6″‘ > hash1_128
$ cat hash1_128 passphrase > hash2_128.tmp
$ md5sum hash2_128.tmp
1d64eb2bfa444cbf9853cdfb8b24da7a hash2_128.tmp
$ perl -e ‘print pack “H
“, “1d64eb2bfa444cbf9853cdfb8b24da7a”‘ > hash2_128
$ cat hash2_128 passphrase > hash3_128.tmp
$ md5sum hash3_128.tmp
304e9e87db9c1c8101f605ed4dd0b9eb hash3_128.tmp

也就是说:

hash1_128 = MD5(Passphrase)
hash2_128 = MD5(hash1_128 + Passphrase)
hash3_128 = MD5(hash2_128 + Passphrase)
Key = hash1_128 + hash2_128
IV = hash3_128

在此基础上,看看 AES-128-CBC 生成的 Key 和 IV 是什么样子的:

$ openssl enc -aes-128-cbc -kfile passphrase -md md5 -P -nosalt
key=D5E483D8B90C02BD4D470BA8049E1FA6
iv =1D64EB2BFA444CBF9853CDFB8B24DA7A

对比 AES-256-CBC 可以看出,AES-128-CBC 的 Key 和 IV 生成方法进一步简化(以下为没有 salt 时的情况):

hash1_128 = MD5(Passphrase)
hash2_128 = MD5(hash1_128 + Passphrase)
Key = hash1_128
IV = hash2_128

在上述验证过程中使用到 openssl 命令时,都用 -md 选项将生成 Key 和 IV 的 hash 函数指定为 md5。那么假如不指定的话,默认的 hash 函数是什么呢?

此问答可知,从 1.1 版本开始,默认的 hash 函数由 MD5 变为 SHA256(可使用 openssl version 命令查看当前版本号),另外也可以通过修改 /etc/ssl/openssl.cnf 配置文件中的 default_md 字段指定默认的 hash 函数

要知道,MD5 生成的 hash 是 128bit 的,而 SHA256 生成的 hash 是 256bit 的,上述 256bit Key 生成时的拼接操作是否有必要呢?

继续验证,仍然回到 AES-256-CBC 并使用 SHA256 作为 hash 函数:

$ openssl enc -aes-256-cbc -kfile passphrase -md sha256 -P -nosalt
key=53A8968B0F53CAA2D21F2694B19EDD0676AF034D4D570651B3689C7827EC84C2
iv =ED889267E14BA02167ED96E226153158

分步看看:

$ sha256sum passphrase
53a8968b0f53caa2d21f2694b19edd0676af034d4d570651b3689c7827ec84c2 passphrase
$ perl -e ‘print pack “H*”, “53a8968b0f53caa2d21f2694b19edd0676af034d4d570651b3689c7827ec84c2″‘ > hash1_256
$ cat hash1_256 passphrase > hash2_256.tmp
$ sha256sum hash2_256.tmp
ed889267e14ba02167ed96e226153158373dbeff2b1177c12906ab786dd1ebd8 hash2_256.tmp

可以看到,对 passphrase 做一次 SHA256 运算就已经是 256bit Key 了,对 Key 和 passphrase 拼接后再次做 SHA256 运算,截取前 128bit 作为 IV 的值。也就是说:

hash1_256 = SHA256(Passphrase)
hash2_256 = SHA256(hash1_256 + Passphrase)
Key = hash1_256
IV = First128bit(hash2_256)

再看看 AES-128-CBC 的情况:

$ openssl enc -aes-128-cbc -kfile passphrase -md sha256 -P -nosalt
key=53A8968B0F53CAA2D21F2694B19EDD06
iv =76AF034D4D570651B3689C7827EC84C2

对 passphrase 做一次 SHA256 运算之后,前 128bit 作为 AES-128-CBC 的 Key 值,后 128bit 作为其 IV 值。写成等式是:

hash1_256 = SHA256(Passphrase)
Key = First128bit(hash1_256)
IV = Second128bit(hash1_256)

至此,可以看出 AES 算法 Key 和 IV 的生成规律了:将 hash 结果(第一次 hash 运算时为空)、passphrase 和 salt(nosalt 时为空)拼接后循环做 hash 运算,再根据 AES 所需的 Key 和 IV 的 bit 数取值。

更进一步的,从上述生成过程可见,只要生成了足够 bit 位的值,hash 运算就停止了,这称为一个迭代,这正是 OpenSSL 为人所诟病的不足。而 GnuPG 使用了多次迭代。

小结

  • OpenSSL AES 算法使用的 Key 和 IV 生成规律:将 hash 结果(第一次 hash 运算时为空)、passphrase 和 salt(nosalt 时为空)拼接后循环做 hash 运算,再根据 AES 所需的 Key 和 IV 的 bit 数取值。
  • 默认的 hash 函数,从 OpenSSL 1.1 开始由 MD5 变为 SHA256。
  • 可以通过 /etc/ssl/openssl.cnfdefault_md 字段修改默认的 hash 函数。
  • OpenSSL AES 生成 Key 和 IV 时只做一次迭代,GnuPG 使用多次迭代。

以上。

LDAP 密码加密方式初探

最近在登录公司内部的一个管理系统时,总是弹出对话框提示证书过期、是否继续云云,今天突发奇想,这个登录过程该不会没有加密传输吧?

先用 Wireshark 抓包看看,果然是未加密的 HTTP 连接,在其中一个访问 /LogicService.asmx 的 POST 请求中,可以看到如下的 XML 内容:

<?xml version="1.0" encoding="utf-8" ?>
<soap:Envelope
xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"
<soap:Body>
<EncryptLogin xmlns=\"http://tempuri.org/\">
<UserID>philip_ye</UserID>
<password>4r4g8OovaDRiiijWlEC5AQ==</password>
<ip>192.1.1.110.11.151.164</ip>
</EncryptLogin>
</soap:Body>
</soap:Envelope>

其中的 password 字段是我用「admin」作为密码输入登录界面再抓包得到的值,密码的密文 4r4g8OovaDRiiijWlEC5AQ== 最后有两个等号,应该是 base64 编码后的结果,尝试着解码看看:

$ printf 4r4g8OovaDRiiijWlEC5AQ== | base64 -d | hexdump -C
00000000 e2 be 20 f0 ea 2f 68 34 62 8a 28 d6 94 40 b9 01 |.. ../h4b.(..@..|

得到一个 128bit 的值:e2be20f0ea2f6834628a28d69440b901,这个值看着又像是 MD5 的结果,而 MD5 运算在通常的应用中是不可逆的,因此对「admin」做 MD5 运算来验证:

$ printf admin | md5sum
21232f297a57a5a743894a0e4a801fc3 –

得到的结果是 21232f297a57a5a743894a0e4a801fc3,与 e2be20f0ea2f6834628a28d69440b901 不相等,显然没有这么简单。

根据公司的这个管理系统客户端的默认图标来看,应该是 .NET Framework 写的。根据 StackOverflow 上这个问答找来 .NET Reflector 对 EXE 文件反编译,整个管理系统客户端的源代码赫然眼前。当年通过反编译学习某些功能实现原理的日子再次上演了。

找到「登录」按钮的点击事件处理函数,其中有这么一条语句:

bool flag = manage.EncryptLogin(userID, SingleEncode.Encode(text), ip);

其中的 text 就是从密码框中获取到的明文密码,也就是我之前输入的「admin」,而 SingleEncode.Encode() 就是对密码加密的接口了。

继续查看 SingleEncode.Encode() 的实现:

public static string Encode(string s) =>
Convert.ToBase64String(GetBytesFromStream(GetEncodeStream(Encoding.ASCII.GetBytes(s))));

可以看出,整个加密过程确实是先加密之后再使用 base64 编码的。继续看 GetEncodeStream() 的实现(Encoding.ASCII.GetBytes()、GetBytesFromStream() 都是系统接口):

private static MemoryStream GetEncodeStream(byte[] bytes)
{
MemoryStream stream = new MemoryStream();
CryptoStream stream2 = new CryptoStream(stream, GetEncoder(), CryptoStreamMode.Write);
stream2.Write(bytes, 0, bytes.Length);
stream2.FlushFinalBlock();
return stream;
}

重点在 GetEncoder():

private static ICryptoTransform GetEncoder()
{
RijndaelManaged managed = new RijndaelManaged();
return managed.CreateEncryptor(Key, IV);
}

至此可以看出,采用的是 Rijndael 加密算法,也就是大名鼎鼎的 AES 加密算法。Rijndael 与 AES 的区别可参考 The Differences Between Rijndael and AES

RijndaelManaged 是系统类,入参 Key 和 IV 可在 MSDN 上 RijndaelManaged 类的说明文档中找到:

  • IV: Gets or sets the initialization vector (IV) for the symmetric algorithm.(Inherited from SymmetricAlgorithm.)
  • Key: Gets or sets the secret key for the symmetric algorithm.(Inherited from SymmetricAlgorithm.)

在 GetEncoder() 所在的 SingleEncode 类的构造函数中,对 Key 和 IV 进行初始化赋值:

static SingleEncode()
{
Key = new byte[] {
0xd8, 0xd5, 0x19, 0x48, 0xea, 0xae, 0x74, 0x84,
0xe1, 0x15, 0x5e, 0xd0, 0x54, 0xf7, 0x69, 0x9b,
0x77, 0x00, 0xc4, 0x35, 0x78, 0xbb, 0x99, 0x62,
0x20, 0xb9, 0xed, 0x7f, 0xf9, 0x76, 0x6d, 0x16
};
IV = new byte[] {
0x46, 0x3e, 0xa9, 0x70, 0x1f, 0xb5, 0xaa, 0x7a,
0x0f, 0xa6, 0x59, 0xc1, 0x62, 0xef, 0x0a, 0xf6
};
}

为避免我司信息安全事故,上述 Key 和 IV 已经过修改,并非实际使用的值。

可以看到 Key 为 32 字节(256-bit),IV 为 16 字节。由 mcrypt(3) 得知,需要 IV 的只有 CBC 模式和 CFB 模式,而从明文「admin」长度 5 字节、密文长度 32 字节、Key 长度 32 字节这点来判断,使用的应该是 CBC 模式(CBC 模式的密文长度与 Key 长度相等,而 CFB 模式的密文长度与明文长度相等)。也就是说,具体加密算法应该是 AES-256-CBC。

将上述的 Key 和 IV 拼接后传入 openssl 命令验证一下(openssl 命令行使用说明详见 OpenSSL Wiki 页面):

$ printf admin | openssl enc -aes-256-cbc -K d8d51948eaae7484e1155ed054f7699b7700c43578bb996220b9ed7ff9766d16 -iv 463ea9701fb5aa7a0fa659c162ef0af6 | hexdump -C
00000000 e2 be 20 f0 ea 2f 68 34 62 8a 28 d6 94 40 b9 01 |.. ../h4b.(..@..|

Bingo!对「admin」使用 AES-256-CBC 加密得到的值正是对抓包看到的 4r4g8OovaDRiiijWlEC5AQ== 进行 base64 解码之后的值: e2be20f0ea2f6834628a28d69440b901。

把整个加密过程串起来:

$ printf admin | openssl enc -aes-256-cbc -K d8d51948eaae7484e1155ed054f7699b7700c43578bb996220b9ed7ff9766d16 -iv 463ea9701fb5aa7a0fa659c162ef0af6 | base64
4r4g8OovaDRiiijWlEC5AQ==

要知道,AES 是对称加密算法,也就是说,知道了 Key 和 IV,是可以将密文解密成明文的:

$ printf 4r4g8OovaDRiiijWlEC5AQ== | base64 -d | openssl enc -d -aes-256-cbc -K d8d51948eaae7484e1155ed054f7699b7700c43578bb996220b9ed7ff9766d16 -iv 463ea9701fb5aa7a0fa659c162ef0af6
admin

最后,把加密和解密的过程写成一个简单的脚本,命名为 ldap-password.sh:

#!/bin/sh

KEY=d8d51948eaae7484e1155ed054f7699b7700c43578bb996220b9ed7ff9766d16
IV=463ea9701fb5aa7a0fa659c162ef0af6

help() {
echo “Encryption: $0 enc [PASSWORD_IN_PLAIN_TEXT]”
echo “Decryption: $0 dec [PASSWORD_IN_CIPHER]”
}

if [ $# -ne “2” ]
then
help
elif [ “$1” == “enc” ]
then
printf $2 | openssl enc -aes-256-cbc -K $KEY -iv $IV | base64
elif [ “$1” == “dec” ]
then
printf $2 | base64 -d | openssl enc -d -aes-256-cbc -K $KEY -iv $IV
printf “\n”
else
help
fi

这样加解密就方便多了:

$ ./ldap-password.sh enc admin
4r4g8OovaDRiiijWlEC5AQ==
$ ./ldap-password.sh dec 4r4g8OovaDRiiijWlEC5AQ==
admin

小结

  • 登录时与服务器的传输是使用 HTTP 明文传输的。
  • 密码字段是加密的,先对密码的明文使用 AES-256-CBC 加密,再使用 base64 编码生成密文。
  • 使用 AES 加密算法时的初始化向量(IV)和密钥(Key)是硬编码的值,安全性大大折扣。

以上。