海信“智能工厂”内部视频,原来你家的电视是这样造出来的!
1970年8月,青岛无线电二厂研制出了一台14英寸的电子管黑白电视机。
这是整个山东省第一台自己生产的电视,
它的名字是——青岛牌。
这台电视的外壳是木质,组合零件手工打造,
第一年加班加点只生产了82台。
后来,电视从小屏到大屏,从黑白到彩色再到4K、8k超高清;
从笨拙厚重到轻薄智能,从往日的奢侈品到如今走进千家万户;
青岛无线电二厂变成了青岛电视机厂,后来又成了海信集团。
150英寸的世界最大电视也已经在测试中,
海信电视更已经连续13年全国市场占有率第一,全球出货量排到了前三。
从青岛牌到海信牌;
从14英寸到150英寸;
从82台到数千万台;
这背后,有时代和社会变迁的印迹,更是企业成长壮大的注脚。
走进位于黄岛的海信电视智能工厂,
看到那几百台不停忙碌的机器人,
或许更能了解海信这几十年来的巨大变化。
在智能工厂里面,
AGV自动导引小车来回穿梭于库房与线体间,
自动将物料运至相匹配的下一个工位,
机器人、机械手和自动化专机有条不紊地实现产品的自动装配、检测、包装。
整叠的液晶面板通过六轴机器人抓取,通过提升机+空中输送线运送至卡合机,
卡合机承接空中自动输送到的液晶面板,并通过视觉定位将液晶面板与背光半成品进行撕膜并精确组装。
组装完成后,通过产品软件硬件设计变更以及自动化检测设备,完成多项产品关键参数的自动调整。
检验合格之后,成品经过自动套袋、自动封箱、自动封包、并箱、缠膜、贴标、分拣、码垛……
成品到达下线口,系统自动呼叫AGV小车,并自动分配路线,由AGV自动运送到指定的最优货位……
一台一台的海信电视,就这样下线,
并从这里发往全国、发往世界各地,
出现在无数家庭温暖的客厅里。
48年来,
变的是电视的生产速度、尺寸和颜色,
不变的是海信一以贯之的温暖和信赖!
Padding Oracle Attack¶
Padding Oracle Attack 攻击一般需要满足以下几个条件
- 加密算法采用 PKCS5 Padding 的加密算法。 当然,非对称加密中 OAEP 的填充方式也有可能会受到影响。分组模式为 CBC 模式。
- 攻击者能力攻击者可以拦截上述加密算法加密的消息。攻击者可以和 padding oracle(即服务器) 进行交互:客户端向服务器端发送密文,服务器端会以某种返回信息告知客户端 padding 是否正常。
Padding Oracle Attack 攻击可以达到的效果如下
- 在不清楚 key 和 IV 的前提下解密任意给定的密文。
Padding Oracle Attack 攻击的基本原理如下
- 对于很长的消息一块一块解密。
- 对于每一块消息,先解密消息的最后一个字节,然后解密倒数第二个字节,依次类推。
这里我们回顾一下 CBC 的
- 加密
Ci=EK(Pi⊕Ci−1)C0=IVCi=EK(Pi⊕Ci−1)C0=IV
- 解密
Pi=DK(Ci)⊕Ci−1C0=IVPi=DK(Ci)⊕Ci−1C0=IV
我们主要关注于解密,这里我们并不知道 IV 和 key。这里我们假设密文块的长度为 n 个字节。
假设我们截获了密文最后两个密文块 FF 与 YY ,以获取密文块 YY 的对应明文的最后一个字节为例子进行分析。为了获取 YY 解密后的内容,我们首先需要伪造一块密文块 F′F′ 以便于可以修改 YY 对应解密明文的最后一个字节。这是因为若我们构造密文 F\’|Y ,那么解密 YY 时具体为 P′=DK(Y)⊕F′P′=DK(Y)⊕F′ ,所以修改密文块 F′F′ 的最后一个字节 F′nFn′ 可以修改 Y 对应解密明文 P′P′ 的最后一个字节 P′nPn′ ,进而反推出原先的明文 PP 的最后一个字节。下面给出获取 PP 最后一个字节的过程:
- i=0,设置 F′F′ 的每个字节为随机字节。
- 设置 F′n=i⊕0x01Fn′=i⊕0x01 。
- 将 F\’|Y 发送给服务器,如果服务器端没有报错,那有很大概率 P′P′ 的最后一个字节是 0x01。否则,只有 P′P′ 的最后 P′n⊕i⊕0x01Pn′⊕i⊕0x01 字节都是 P′n⊕i⊕0x01Pn′⊕i⊕0x01 才不会报错。而且,需要注意的是 padding 的字节只能是 1 到 n。 因此,若想要使得在 F\’ 随机地情况下,并且满足 padding 字节大小的约束情况下还不报错概率很小。所以在服务器端不报错的情况下,我们可以认为我们确实获取了正确的字节。这时可知 Dk(Y)Dk(Y) 的最后一个字节 Dk(Y)nDk(Y)n 为 P′n⊕F′n=0x01⊕i⊕0x01=iPn′⊕Fn′=0x01⊕i⊕0x01=i ,即可知道原先的明文 PP 的最后一个字节 Pn=Dk(Y)n⊕Fn=i⊕FnPn=Dk(Y)n⊕Fn=i⊕Fn 。
- 在出现错误的情况下,i=i+1,跳转到 2.。
当获取了 PP 的最后一个字节后,我们可以继续获取 PP 的倒数第二个字节,此时需要设置 F′n=Dk(Y)n⊕0x02Fn′=Dk(Y)n⊕0x02 ,同时设置 Fn−1=i⊕0x02Fn−1=i⊕0x02 去枚举 i。以此类推,我们可以获取 Y 所对应的明文 PP 的所有字节。
所以,综上所示,Padding Oracle Attack 其实在一定程度上是一种具有很大概率成功的攻击方法。
然而,需要注意的是,往往遇到的一些现实问题并不是标准的 Padding Oracle Attack 模式,我们往往需要进行一些变形。
程序中采用的加密是 AES CBC,其中采用的 padding 与 PKCS5 类似
但是,在每次 unpad 时并没有进行检测,而是直接进行 unpad。
其中,需要注意的是,每次和用户交互的函数是
- send_msg ,接受用户的明文,使用固定的 2jpmLoSsOlQrqyqE 作为 IV,进行加密,并将加密结果输出。
- recv_msg ,接受用户的 IV 和密文,对密文进行解密,并返回。根据返回的结果会有不同的操作
这里我们再简单总结一下我们已有的部分
- 加密
- 加密时的 IV 是固定的而且已知。
- \’Welcome!!\’ 加密后的结果。
- 解密
- 我们可以控制 IV。
首先,既然我们知道 Welcome!! 加密后的结果,还可以控制 recv_msg 中的 IV,那么根据解密过程
Pi=DK(Ci)⊕Ci−1C0=IVPi=DK(Ci)⊕Ci−1C0=IV
如果我们将 Welcome!! 加密后的结果输入给 recv_msg,那么直接解密后的结果便是 (Welcome!!+\’\\x07\’*7) xor iv,如果我们恰当的控制解密过程中传递的 iv,那么我们就可以控制解密后的结果。也就是说我们可以执行上述所说的任意命令。从而,我们也就可以知道 flag 解密后的结果。
其次,在上面的基础之上,如果我们在任何密文 C 后面添加自定义的 IV 和 Welcome 加密后的结果,作为输入传递给 recv_msg,那么我们便可以控制解密之后的消息的最后一个字节,那么由于 unpad 操作,我们便可以控制解密后的消息的长度减小 0 到 255。
基本利用思路如下
- 绕过 proof of work
- 根据执行任意命令的方式获取加密后的 flag。
- 由于 flag 的开头是 hitcon{,一共有 7 个字节,所以我们任然可以通过控制 iv 来使得解密后的前 7 个字节为指定字节。这使得我们可以对于解密后的消息执行 get-md5 命令。而根据 unpad 操作,我们可以控制解密后的消息恰好在消息的第几个字节处。所以我们可以开始时将控制解密后的消息为 hitcon{x,即只保留hitcon{ 后的一个字节。这样便可以获得带一个字节哈希后的加密结果。类似地,我们也可以获得带制定个字节哈希后的加密结果。
- 这样的话,我们可以在本地逐字节爆破,计算对应 md5,然后再次利用任意命令执行的方式,控制解密后的明文为任意指定命令,如果控制不成功,那说明该字节不对,需要再次爆破;如果正确,那么就可以直接执行对应的命令。
具体代码如下
最后结果如下
这个程序时接着上面的程序继续搞的,不过这次进行的简单的修改
- 加密算法的 iv 未知,不过可以根据 Welcome 加密后的消息推算出来。
- 程序多了一个 56 字节的 token。
- 程序最多能进行 340 操作,因此上述的爆破自然不可行
程序的大概流程如下
- 经过 proof of work
- 发送 “Welcome!!” 加密后的消息
- 在 340 次操作中,需要猜中 token 的值,然后会自动将 flag 输出。
当然,在上个题目中存在的漏洞,在这个题目中仍然存在,即
- 任意执行给定命令
- 长度截断
由于 340 的次数限制,虽然我们仍然可以获得 md5(token[:i]) 加密后的值(这里需要注意的是这部分加密后恰好是 32 个字节,前 16 个字节是 md5 后加密的值,后面的 16 个字节完全是填充的加密后的字节。这里md5(token[:i]) 特指前 16 个字节。)。但是,我们不能再次为了获得一个字符去爆破 256 次了。
既然不能够爆破,那么我们有没有可能一次获取一个字节的大小呢?这里,我们再来梳理一下该程序可能可以泄漏的信息
- 某些消息的 md5 值加密后的值,这里我们可以获取 md5(token[:i]) 加密后的值。
- unpad 每次会对解密后的消息进行 unpad,这个字节是根据解密后的消息的最后一个字节来决定的。如果我们可以计算出这个字节的大小,那么我们就可能可以知道一个字节的值。
这里我们深入分析一下 unpad 的信息泄漏。如果我们将加密 IV 和 encrypt(md5(token[:i])) 放在某个密文 C 的后面,构成 C|IV|encrypt(md5(token[:i])),那么解密出来的消息的最后一个明文块就是 md5(token[:i])。进而,在 unpad 的时候就是利用 md5(token[:i]) 的最后一个字节( 0-255)进行 unpad,之后对 unpad 后的字符串执行指定的命令(比如 md5)。那么,如果我们事先构造一些消息哈希后加密的样本,然后将上述执行后的结果与样本比较,如果相同,那么我们基本可以确定 md5(token[:i]) 的最后一个字节。然而,如果 md5(token[:i]) 的最后一个字节小于 16,那么在 unpad 时就会利用一些 md5 中的值,而这部分值,由于对于不同长度的 token[:i] 几乎都不会相同。所以可能需要特殊处理。
我们已经知道了这个问题的关键,即生成与 unpad 字节大小对应的加密结果样本,以便于查表。
具体利用思路如下
- 绕过 proof of work。
- 获取 token 加密后的结果 token_enc ,这里会在 token 前面添加 7 个字节 \”token: \” 。 因此加密后的长度为 64。
- 依次获取 encrypt(md5(token[:i])) 的结果,一共是 57 个,包括最后一个 token 的 padding。
- 构造与 unpad 大小对应的样本。这里我们构造密文 token_enc|padding|IV_indexi|welcome_enc。由于 IV_indexi 是为了修改最后一个明文块的最后一个字节,所以该字节处于变化之中。我们若想获取一些固定字节的哈希值,这部分自然不能添加。因此这里产生样本时 unpad 的大小范围为 17 ~ 255。如果最后测试时 md5(token[:i]) 的最后一个字节小于 17 的话,基本就会出现一些未知的样本。很自然的一个想法是我们直接获取 255-17+1 个这么多个样本,然而,如果这样做的话,根据上面 340 的次数(255-17+1+57+56>340)限制,我们显然不能获取到 token 的所有字节。所以这里我们需要想办法复用一些内容,这里我们选择复用 encrypt(md5(token[:i])) 的结果。那么我们在补充 padding 时需要确保一方面次数够用,另一方面可以复用之前的结果。这里我们设置 unpad 的循环为 17 到 208,并使得 unpad 大于 208 时恰好 unpad 到我们可以复用的地方。这里需要注意的是,当 md5(token[:i]) 的最后一个字节为 0 时,会将所有解密后的明文 unpad 掉,因此会出现 command not found 的密文。
- 再次构造密文 token_enc|padding|IV|encrypt(md5(token[:i])) ,那么,解密时即使用 md5(token[:i]) 的最后一个字节进行 unpad。如果这个字节不小于 17 或者为 0,则可以处理。如果这个字节小于 17,那么显然,最后返回给用户的 md5 的结果并不在样本范围内,那么我们修改其最后一个字节的最高比特位,使其 unpad 后可以落在样本范围内。这样,我们就可以猜出 md5(token[:i]) 的最后一个字节。
- 在猜出 md5(token[:i]) 的最后一个字节后,我们可以在本地暴力破解 256 次,找出所有哈希值末尾为 md5(token[:i]) 的最后一个字节的字符。
- 但是,在第六步中,对于一个 md5(token[:i]) 可能会找出多个备选字符,因为我们只需要使得其末尾字节是给定字节即可。
- 那么,问题来了,如何删除一些多余的备选字符串呢?这里我就选择了一个小 trick,即在逐字节枚举时,同时枚举出 token 的 padding。由于 padding 是 0x01 是固定的,所以我们只需要过滤出所有结尾不是 0x01 的 token 即可。
这里,在测试时,将代码中 sleep 注释掉了。以便于加快交互速度。利用代码如下
效果如下
这个题目还是蛮有意思的,题目描述如下
附件以及最后的 exp 自行到 ctf-challenge 仓库下寻找。
题目的基本流程为
- 不断接收 a 和 b 两个字符串,其中 a 为明文,b 为密文,注意
- b 在解密后需要满足尾部恰好等于 iv。
- 如果 a 和 b 相等,那么根据
- a 为 gimme_flag ,输出加密后的 flag。
- 否则,输出一串随机加密的字符串。
- 否则输出一串明文的字符串。
此外,我们还可以发现题目中的 unpad 存在问题,可以截断指定长度。
一开始,很直接的思路是 a 和 b 的长度都输入 0 ,那么可以直接绕过 a==b 检查,获取一串随机密文加密的字符串。然而似乎并没有什么作用,我们来分析一下加密的流程
不妨假设 P0=iv,C0=ivP0=iv,C0=iv,则
Ci=Ci−1⊕E(Pi−1⊕Pi)Ci=Ci−1⊕E(Pi−1⊕Pi)
那么,假设消息长度为 16,与我们想要得到的gimme_flag padding 后长度类似,则
C1=IV⊕E(IV⊕P1)C1=IV⊕E(IV⊕P1)
C2=C1⊕E(P1⊕IV)C2=C1⊕E(P1⊕IV)
可以很容易的发现 C2=IVC2=IV。
(盗图,下面的图片更加清晰
反过来想,如果我们向服务器发送 iv+c+iv,那么总能绕过 tsb_decrypt 的 mac 检查
那么此时,服务器解密后的消息则是
unpad(IV⊕D(C1⊕IV))unpad(IV⊕D(C1⊕IV))
我们可以考虑控制 D 解密的消息为常数值,比如全零,即C1=IV,那么我们就可以从 0 到 255 枚举 IV 的最后一个字节,得到 IV⊕D(C1⊕IV)IV⊕D(C1⊕IV) 的最后一个字节也是 0~255。而只有是 1~15 的时候,unpad 操作过后,消息长度不为 0。因此,我们可以在枚举时统计究竟哪些数字导致了长度不为零,并标记为 1,其余标记为 0。
此外,我们可以在最初的时候打表获取最后一个字节所有的可能情况,记录在 xor_byte_map 中。
通过与这个表进行对比,我们就可以知道最后一个字节可能的情况。
在获取了明文最后一个字节后,我们就可以利用 unpad 的漏洞,从长度 1 枚举到长度 15 来获得对应的明文内容。
这一点比较简单,我们希望利用这一点来获取 gimme_flag 的密文
其中 plain0 和 cipher0 是我们获取的 AES 加密的明密文对,不包括之前和之后的两个异或。
这一点,其实就是利用解密任意加密块的功能实现的,如下
可以思考一下为什么第二块之后的密文操作会有所不同。
完整的代码参考 ctf-challenge 仓库。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。