从0到1:CTFer成长之路
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.3 密码学和逆向知识

加密算法与伪随机数算法是开发中经常用到的东西,在CTF比赛中也是。除了CRYPTO分类下对密码算法纯粹的考虑,Web题目也会涉及密码学的使用,最常见的有对敏感信息如密码或用户凭据进行加密保存对重要信息的校验等。这些过程中可能隐藏着一些对密码学的错误使用,比赛过程中在成功伪造出我们需要的信息后,配合其他漏洞,最终可获取flag。

除了密码学,对源码进行混淆加密也是正常操作。利用Python、PHP、JavaScript等语言的特性,比赛题目将代码变成不那么直观的样子,加大了分析难度,但是只要掌握了方法,参赛者也能很快分析出藏在混淆背后的秘密。

本节就CTF中常见的Web+CRYPTO或Web逆向题目进行探讨。

3.3.1 密码学知识

在密码学与Web结合的题目中往往包含明显的提示,如题目的关键词中有ENCRYPT、DECRYPT等,甚至直接给出相关代码,让参赛者进行分析。

3.3.1.1 分组加密

分组加密就是将一个很长的字符串分成若干固定长度(分组长度)的字符串,每块明文再通过一个与分组长度等长的密钥进行加密,将加密的结果进行拼接,最终得到加密后的结果。当然,在分组过程中,块的长度不一定是分组长度的整数倍,所以需要进行填充,让明文变为分组长度的整数倍。这个填充的过程刚好可以帮助我们识别分组加密。

3.3.1.2 加密方式的识别

在分组加密方式中,加密过程中明文会被分为若干等长的块。随着明文长度的增加,密文的长度可能不变,或一次增加固定的长度,而这个增加的长度就是在分组加密中使用到的密钥的长度。这类题目中常见的加密算法有AESDES两种。其中,DES的分组长度固定为64比特,而AES有AES-128、AES-192、AES-256三种。由此可以初步判定加密时使用的算法是哪一种,再根据题目提供的信息对密钥进行爆破,或者配合其他攻击方式伪造密文。

在分组加密中有ECB、CBC、CFB、PCBC、OFB、CTR六种模式,对每种模式的加密的区别可以参考密码学部分,下面将通过对其中三种加密方式和相应例题来介绍这类题目中常包含的套路。

3.3.1.3 ECB模式

ECB(Electronic CodeBook,电子密码本)模式的工作流程图见图3-3-1和图3-3-2。

在加密过程中,需要加密的消息,按照分组大小被分为数个块,再使用密钥对若干块明文分别加密,将加密结果拼接后得到密文。解密过程类似。这种加密方式最大的问题就是对所有分块使用同样的密钥进行加密,若明文相同,则产生的密文也相同。所以,针对ECB这种加密方式,我们只需关注某一组已知可控的明文及对应的加密结果,即可对其余加密块进行攻击。

这里以HITCON 2018 Oh My Reddit为例进行讲解。题目代码请参考:https://github.com/orangetw/My-CTF-Web-Challenges/tree/master/hitcon-ctf-2018/oh-my-raddit/src。

图3-3-1

图3-3-2

根据提示,flag is hitcon{ENCRYPTION_KEY},我们可以得知这是一道密码与Web相结合的题目;再查看hint界面,提示

密钥都是小写字符。

查看题目中涉及的链接和对应的明文不难发现,随着title长度的变化,每次密文在产生长度变化时都变化了16个字符,由此可以推断出密钥的长度为64bit,加密方式可能为DES。

我们发现了网页中两条很有趣的链接,这两条链接中title开头的字符串都是“Bypassing W”,而密文中也存在相同的“1d8feb029243ed633882b1034e878984”:

可以猜想加密使用的模式为ECB模式(因为开头及结尾均不同的情况下,对相同字符串的加密结果相同)。那么,根据我们现在的已知信息:

密钥的长度为64bit,8字符,可能为DES加密方式。

密钥中的字符均为小写字符。

对“Bypassing W”中某8个字符的加密结果可能为“3882b1034e878984”。

我们可以尝试爆破密钥。因为388...984串出现在后面,也应该以8位为窗口,倒着将“Bypassing W”作为明文进行爆破,即按照"assing+W"、"passing+"(“+”是因为将title进行了URL编码)的顺序进行尝试。

我们使用hashcat工具:

命令中的“617373696e672b57”是将"assing+W"转化为HEX编码后的结果。运行结束后,我们得到了一个可能的密钥“ldgonaro”。使用这个密钥对密文进行解密:

解密成功且内容有意义,可以认为密钥正确。但是我们按照格式提交flag后,提示错误。再观察题目,其中有文件下载相关链接,通过分析该链接,我们可以实现任意文件下载。下载app.py进行分析,最终得到密钥。

3.3.1.4 CBC模式

CBC(Cipher Block Chaining,密码分组链接)中,每块明文都需要与前一块密文进行异或,再进行加密,最后将得到的加密后的密文块进行拼接,得到最终的密文串。这就使得在CBC加密过程中,对每块明文进行加密时要依赖前面的所有明文块,同时通过IV(Initialization Vector,初始向量)保证每条消息的唯一性,其工作流程见图3-3-3和图3-3-4。

图3-3-3(来自维基百科)

图3-3-4(来自维基百科)

异或运算有如下性质:

由于IV和密文块直接参与了异或解密的过程,就导致了在出题过程中常见的两种攻击方式:通过IV,影响第一个明文分组;通过第n个密文分组影响第n+1个明文分组。

根据解密流程,假如修改第n个分组解密后的结果,设p_n代表第n组明文,c_n代表第n组密文,dec(key,c)为解密算法,key为密钥。代码如下:

如果想修改某一组的解密结果,只需知道原来的明文是什么、想修改的明文内容、上一组向后传递的密文即可(若为第一组,则需要IV)。

这里以PicoCTF 2018的Secure Logon题目为例,题目中提供了服务器端的代码:https://github.com/shiltemann/CTF-writeups-public/blob/master/PicoCTF_2018/writeupfiles/server_noflag.py。在/flag路由下,只有获取Cookie中保存的由AES加密后的JSON串,且admin字段为1的情况下,才会将flag显示到页面中。

/login路由中则给出了Cookie的生成算法

其中使用的加密算法为:

通过对login函数及AESCipher的分析,我们可以知道:使用的AES-128-CBC加密算法;Cookie中的内容为base64(iv,data);data为json.dumps(cookie)的结果;Cookie中包含{"admin":0,"username":"something","password":"something"},并按key字母序进行了排序。

为了达成admin为1,我们需要进行CBC比特翻转攻击。

根据json.dumps的结果,可知需要修改的字符位于整个加密字符串的第11位,将其从0变为1。

根据分组长度为16,我们可以得知要翻转的字符位于第一组第11位。

根据公式,我们开始进行翻转攻击。所需要的IV已经保存在Cookie中base64解密结果的前16位中。那么,我们需要的所有信息都已经满足,开始写程序翻转:

将翻转后的Cookie进行替换,即可成功拿到flag。

3.3.1.5 Padding Oracle Attack

Padding Oracle是Padding根据服务器对信息解密时的表征来对应用进行攻击,针对的同样是CBC加密模式,其中的关键是Padding的使用。在分组加密中,需要先将所有的明文串分成若干固定长度的分组,为了满足这样的需求,要求我们对明文进行填充,将其补充为完整的数据块。

在填充时有多种规则,其中最常见的是PKCS#5标准中定义的规则,即当明文中最后一个数据块包含N个内容为N的填充数据(N取决于明文块最后一部分的数据长度)。每个字符串都应该包含至少一个填充块,也就是说:需要补充1个数据块时,补充01;需要补充2个数据块时,补充02……当字符串长度正好为分组长度的整数倍数时,额外添加一个块,内容为Padding,见图3-3-5。

图3-3-5

在解密时,服务器将数据解密后,在判断最后一个数据块末尾的Padding是否合法时,可能因为Padding出现的错误而抛出填充异常,就是给攻击者对加密进行攻击时的Oracle(提示)。一般的Web应用会将IV和加密后的字符串一同交还给客户端作为凭据,用于以后对客户身份的验证时使用。这里以P.W.N.CTF 2018:Converter为例(见图3-3-6),题目地址:http://converter.uni.hctf.fun/,主要功能是为用户输入一个字符串,通过服务器的转换器将该格式的文档转换为其他格式。注意,转换Markdown时使用的为pandoc,可能存在命令注入的漏洞。在完成输入后,服务器返回一串Cookie:

对这段cookie进行修改,发现:在修改字符串的最后一位时,提示“ValueError:Invalid padding bytes.”;在修改字符串的最开始一位时,提示“JSONDecodeError:Expecting value:line 1 column 1(char 0)”;不进行修改时,页面返回正常,见图3-3-7。

由于输入相同的内容时,返回的vals的值不同,我们可以推测用于加密的算法采用的加密模式为CBC模式。在逐步增加传入的内容的长度时,我们发现,返回的vals的长度在发生变化时,变化的长度为32,所以可以确定加密方式是128-CBC方式。根据这些内容,我们可以尝试Padding Oracle攻击,恢复明文。因为在CBC模式进行解密时需要一个IV,且服务器只返回了我们一个vals,所以我们可以先假设第一个分组为IV,后续信息为加密结果。

在题目的场景中,根据应用程序的提示,我们可以判断出一个加密字符串的填充是否正确,同时可以对该应用进行Padding Oracle攻击。

那么,在本题中,我们可以认为服务器返回的信息与明文间有对应关系,见图3-3-8。

由于我们不知道明文的内容,图中明文都用'?'进行代替。但是不难推测,最后一个block中一定包含一个合法的Padding。

图3-3-6

图3-3-7

图3-3-8

在CBC模式的解密过程中,对最后一个分组加密、解密的流程见图3-3-9和图3-3-10,符号⊕代表异或。

在了解了CBC方式对字符串如何进行解密及Padding的规则后,我们可以利用Padding Oracle对这道题被加密的明文进行恢复。至于原理,我们以其中某个加密块为例进行讲解。选取第一块,注意第一块在进行异或时操作数为IV,之后的块在进行异或时,操作数为前一个密文块。为了操作方便,只对一个加密块进行破解。在破解时,先将IV设为全0。

图3-3-9

图3-3-10

通过将Cookie设置为

进行访问,服务器返回ValueError:Invalid padding bytes。因为在使用0作为IV进行解密后,解密的结果包含的Padding出现了错误,而导致解密过程中出现填充异常,见图3-3-11。

图3-3-11

通过变化IV,使最终得到的解密结果的字节进行变化,当IV+1即Cookie为

时,虽然依旧返回500错误,但服务器解密出的明文的结果已经发生了变化,见图3-3-12。

图3-3-12

由于IV的变化,服务器完成解密时最终字符串的内容变化为0x3C。如此重复,直到解密出的明文的最后1字节为0x01,Cookie的内容如下

则服务器返回“JSONDecodeError:Expecting value:line 1 column 1(char 0)”,而不是由发生填充错误导致的“ValueError:Invalid padding bytes.”。此时可以推测最后一个字符为0x01,满足了Padding的要求,见图3-3-13。根据异或的计算过程和CBC解密的流程,我们可以知道

也就是把对第一个密文块进行解密后的中间值的内容为0x73

在正常解密流程中,该字符与原IV中同样位置的字符进行异或运算,运算后的值为最终的解密结果。所以0x73 xor 0x51=0x22(hex解码后为'"'),即原来的明文字符串中的值。

图3-3-13

现在我们已经知道了最后1字节在解密后的中间结果。通过修改IV,我们可以使最后1字节在异或后的最终结果为0x02,那么此时由于倒数第二个字符解密结果不满足Padding规则(见图3-3-14),服务器会再次返回500错误。

图3-3-14

那么,依旧逐步修改IV,使最终解密的结果为0x02,正确填充时(见图3-3-15),Cookie如下所示

图3-3-15此时,根据类似的计算过程,可以还原倒数第二位的内容:

如此重复,直到填充字符串的长度为整个块的长度,此时我们可以还原第一个块的全部内容,见图3-3-16。

图3-3-16

再根据CBC模式的解密规则,在解密过程中,解密时产生的中间结果不受到IV的影响。此时将第二个密文块直接拼接至置0的IV序列后,按照类似的步骤,但在异或得到明文时,需要与前一个密文块对应位置的值进行异或,这样可以完成对第二个密文块进行破解。如此反复,最终恢复整个明文。

根据第二部分CBC模式加密解密的原理,在已知明文、密文、目标明文、IV时,我们可以构造任意字符串,此时可以将需要的命令注入cookie,即将

修改为

在修改的过程中,需要从最后一个密文块开始伪造。在伪造时,它的前一个密文块解密后的内容也会改变。由于Padding Oracle的存在,我们可以获得修改后的密文块解密时的中间结果,从而依次向前推进,完成对任意字符串的伪造。

原理介绍完毕,但是为了方便解题,可以使用https://github.com/pspaul/padding-oracle中提供的工具,仅需修改小部分代码,即可实现全部功能。

针对本题目所编写的代码如下:

3.3.1.6 Hash Length Extension

在Web中,密码学的应用除了加密还有签名。当服务器端生成一个需要保存在客户端的凭据时,正确使用哈希函数可以保证用户伪造的敏感信息不会通过服务器的校验而对系统的正常运行造成影响。哈希函数很多采用的是Merkle-Damgård结构,如MD5、SHA1、SHA256等,在错误使用的情况下,这些哈希算法会受到哈希长度扩展攻击(Hash Length Extension,HLE)的影响。

首先,HLE适用的加密情况为Hash(secret+message),这时虽然我们不知道secret的内容,但是依旧可以在message后拼接构造好的payload,发给服务器,并通过服务器的校验。要理解这种攻击手法,我们需要对Hash算法有了解。这里以SHA1为例,在加密时,我们需要关注的有三个步骤(具体算法请见密码学部分):

(1)对信息进行处理

在SHA1算法中,算法将输入的信息以512bit为一组进行处理,这时可能出现不足512bit的情况,就需要我们对原信息进行填充。填充时,在该数组的最后填上一个1,再持续填入0,直到整个消息的长度满足length(message+padding)%512=448。这里之所以是448,因为我们需要在消息的最后补充上该信息的长度信息,而这部分的长度64bit加上之前的448bit刚好能作为一个512bit的分组。

(2)补长度

MD算法中,最后一个分组用来填写长度,这也正是SHA1算法能够处理的信息的长度不能超过2^64bit长度的原因。

(3)计算hash

在计算信息摘要时,在补位完成后的信息中取出512bit进行哈希运算。在运算时,有5个初始的链接变量A=0x67452301,B=0xEFCDAB89,C=0x98BADCFE,D=0x10325476,E=0xC3D2E1F0,用来参与第一轮的运算。在第一轮的运算后,A、B、C、D、E会按照一定规则,更新为经过当前轮的计算后,哈希函数得出的结果。也就是说,在每轮运算后,得出的结果都会作为下一轮的初始值而继续运算。重复该过程,直到对全部信息分组完成运算,输出Hash计算的结果,也就是SHA1值。

对于题目中可能出现的Hash(secret+message)方式,服务器一般会将Hash运算的结果即Hash(secret+message+原填充+原长度)的结果发给客户端。那么,现在只需要猜对secret的长度,完成填充及补长度的过程,就可以在不知道secret的情况下得到Hash函数某一轮计算的中间结果,即对Hash(secret+message+原填充+原长度+payload)运算时,刚好处理完payload之前一个分组的中间结果。由于中间结果在之后的运算中不会受到之前分组中信息的影响,这就导致了可以在保证Hash运算结果正确的情况下,将任意payload添加至原信息的尾部。

我们以Backdoor CTF 2017中的Extends Me为例,题目中提供了相应的源码(https://github.com/jbzteam/CTF/tree/master/BackdoorCTF2017):

在login函数中,通过post方式传入username,在Cookie中传入data和user的值。其中,data是SLHA1(key|username|user)的结果。在这个签名过程中,key为未知参数,username可控,user可控。只有在data中的内容与SLHA1签名后的结果相同时,才会返回flag。

再观察SLHA1函数,可以发现,它是一种类似SHA1算法的Hash算法,但修改了其中的填充和链接变量,所以SLHA1算法也会受到HLE的威胁。

那么,此时的解题的思路是将admin这一字符串填充至user的末尾。我们可以按照修改后的思路对程序进行修改,完成哈希长度扩展。

这里爆破的长度是一个范围,是因为我们不知道key的长度是多少,所以需要填充的内容的长度无法确定。在算法正确的情况下,通过遍历方式,在key的长度正确时,服务器会将flag返回。

3.3.1.7 伪随机数

在密码学中,伪随机数也是一个重要的概念。但是软件并不能生成真随机数。用不安全的库生成的伪随机数不够随机,也是CTF比赛中的一个考点。

伪随机数的生成实现一般是“算法+种子”。PHP中有mt_rand和rand两种生成伪随机数的函数,它们对应的播种函数为mt_srand和srand。在seed相同时,不论生成多少次,它们产生的随机数总是相同的,以下程序输出的随机数见图3-3-17。

假如以某种方式我们得到了服务器所使用的种子,不管是固定值还是时间戳,我们都可以对之后生成的伪随机数进行预测。

在rand函数中,如果没有调用srand,那么产生的随机数则有规律可循,即:

图3-3-17

state[i]=state[i-3]+state[i-31]

此外,在每次调用mt_rand时,PHP都会检查是否已经播种。如果已经播种,就直接产生随机数,否则自动播种。自动播种时,使用的种子范围为0~232,而且在每个PHP处理的进程中,只要进行了自动播种,就会一直使用这个种子,直到该进程被回收。所以,我们可以在保持连接keep-alive时,根据前几次随机数生成的结果,使用php_mt_seed工具对种子进行爆破,从而达到预测随机数的目的。

虽然我们只对PHP的伪随机数进行了说明,但是实际上,其他语言中也存在伪随机数的强弱的问题,如Python中,见图3-3-18。

在应对此类题目的时候可以查阅相关的官方文档中相关函数的介绍,如果生成的伪随机数可以被预测,则会有相关该伪随机函数不适合加密之类的提示,见图3-3-19和图3-3-20。

图3-3-18

图3-3-19

图3-3-20

3.3.1.8 密码学小结

上文介绍的几种密码学的攻击方式和例子只是少部分Web与Crypto结合的产物,但是密码学重点不止这些,如分组加密模式中依然有可以被重放攻击的CFB模式,可以被位反转攻击影响的CTR模式,甚至其他流加密算法。虽然没有与Web相结合的例子,但是依然可以成为以后出题人的关注点,出现在题目中。所以,Web参赛者也要懂得一些密码学的知识,识别一个加密算法是否易受到攻击,并将题目中获取的数据和需要构造的字符串即时交给队内的密码学大佬,最终达到题目中的要求。

3.3.2 Web中的逆向工程

3.3.2.1 Python

在CTF比赛时,一些目标可能存在任意文件下载漏洞但对可以下载的文件类型进行了限制,如Python中禁止下载.py文件。Python在运行时为了加速程序运行,因此会将.py文件编译为.pyc或.pyo文件,通过恢复这些文件中的字节码信息,同样可以获得原程序的代码。

比如,在LCTF 2018的L playground2中,关键代码见图3-3-21,文件下载的接口限制了不能直接下载.py文件,但可以下载相应的.pyc文件进行反编译,获得源代码,见图3-3-22。

图3-3-21

图3-3-22

3.3.2.2 PHP

CTF Web比赛中很可能碰到对代码进行加密的情况。为了理解PHP加密,我们需知道PHP在运行时不会被直接执行,而是经过一次编译,执行编译后的Opcode,其中有三个重要的函数,分别是zend_compile_file、zend_compile_string、zend_execute。常见的加密方法有对问文件进行加密、对代码进行加密、实现虚拟机等方式,由于加密方式的不同解密时也会根据不同算法,调用解密插件修改后的编译或执行函数。

传统的PHP加密方案只是在PHP代码的基础上,通过代码混淆的方式破坏其可读性,通过壳对最终执行代码进行解密,再通过eval将解密的结果执行。对于这类题目,既然我们知道它最终通过eval将代码进行解密,那么直接通过hook eval执行过程。在PHP的扩展中,在初始化时将zend_compile_file替换为我们自行编写的函数,在每次执行的时候输出其参数,就能将解密的结果输出。

例如,phpjiami就采取了这种方法。在PWNHUB中,“傻fufu的工作日”一题就采用了这种加密方式。题目源代码网址为https://github.com/CTFTraining/pwnhub_2017_open_weekday。题目提供了由phpjiami处理后的备份文件,可以直接下载加密后的代码,见图3-3-23。

图3-3-23

网络上有很多编写好的hook eval插件源代码,如https://github.com/bizonix/evalhook,只需编译并加载到PHP中,再运行我们的源码,就可以得到真正的源代码,见图3-3-24。

除了使用这种方式进行代码混淆,使用插件对代码进行加密也是一种方式。这种加密方式通过对PHP底层的zend_compile_*进行hook,在hook后的函数中进行解密操作,再将解密后的源代码传给PHP的相关执行函数。对于这种类型的加密,我们仍然可以使用hook eval类似的方式进行解密。

比如,在SCTF 2018的Simple PHP Web中,源代码地址如下:https://github.com/CTFTraining/sctf_2018_babysyc.git。通过文件包含漏洞直接读取index.php源代码发现是乱码,怀疑代码进行过加密。通过对phpinfo.php的观察,我们发现服务器启动了encrypt_php插件,那么在指定插件目录下下载该插件。分析该加密插件,该加密对zend_compile_file进行了hook,见图3-3-25。

再观察encrypt_compile_file中的逻辑。在函数执行的最后,加密程序直接将解密后的结果传回了最开始的zend_compile_file,见图3-3-26,此时只需调整hook插件与解密插件的位置,让hook函数在解密函数后被调用,就可以输出解密后的代码,见图3-3-27。

图3-3-24

图3-3-25

图3-3-26

图3-3-27

另一种加密方式是对已经编译后的Opcode进行处理,此时监控zend_compile_*并不会有任何效果,因为加密根本没有使用PHP进行编译,而是直接解密得到Opcode并执行。由于编译的过程没有起作用,因此只能hook函数zend_execute,甚至其中真正执行代码的zend_execute_ex,从中得到Opcode后再进行分析。PHP的vld扩展提供了对Opcode进行分析的工具,需要修改vld的源代码,将dump OpCode的代码加到vld_execute_ex中,然后人工分析opcode,就能逐步分析出加密的结果。

例如,RCTF 2019中的sourceguardian一题,我们看到sg_load函数和题目名字的提示可知,代码使用sourceguardian加密,见图3-3-28,使用修改后的vld,可以将Opcode导出。

图3-3-28

图3-3-28(续)

对Opcode进行分析后,即可逐步恢复源代码,见图3-3-29。

图3-3-29

还有一种最复杂的加密方式,即重新实现一个VM,将PHP的源代码编译生成的opcode加密,混淆为仅能被自定义的VM理解的样式,交给自定义的VM进行解析。其中典型的例子如VMP,由于需要完成对虚拟机、代码的共同分析,工作量巨大,故很难实现解密。

3.3.2.3 JavaScript

不管怎样,JavaScript加密最终会将解密后的结果交给JavaScript引擎来执行,由此我们只需像解密PHP一样,为其中的关键函数加入hook,就可以完成解密了。

如在大多数情况下,加密的代码在进行解密后,如果想再次被执行,只能通过调用eval等函数,那么我们可以将eval函数修改为打印的函数,不让其执行,而是输出,就可以得到其中的关键代码。

一些代码可能对开发者工具进行检测,对于这种反调试方式,我们可以通过BurpSuite的代理功能,删除其中反调试部分的代码。JavaScript代码加密实现的难度太大,所以很多时候只是采用混淆的方式进行处理。而混淆仅仅对变量名和代码结构进行了调整,可以通过代码美化工具,将其结构进行优化,甚至通过Partial Evaluation技术解决。现在网络上有很多开源的工具能对代码进行优化,如Google的Closure Compiler、FaceBook的Prepack、JStillery。虽然大多数应用是对代码进行优化,但是在优化的过程中会在编译期重构AST、计算函数、初始化对象等,最终呈现可读的代码。