注:本文中所有题面数据均经过处理,并未复现现场数据,仅作辅助理解过程之用。
0x00 前言
一次偶然,我经历了一场奇异的CTF解题过程,尽管上下折腾许久,但终有所收获。现将解题过程与思路分享。
0x01 开场
开题 github.com/xxxx8888/AaBbcCdD 打开后只有一个README
5KB才6行?Raw加载原始文件
一串神秘字符和一段熟悉的base64编码,由编码中的等号发现,是由两段base64组成的。
先看第一段神秘字符串,乍一看摸不着头脑。仔细看看,有见 : / 等字符,大胆猜测这是url的变形。
对半拆分后看到熟悉的字眼,发现是字符的移位变换。
hts/x_lj3w33lgic.eiwn_e tp:/xsfhta25.lthm/_atky
按上到下,左到右的规则重新拼合得到链接
https://xx_slfjh3twa3235l.glitch.me/i_want_key
打开后得到一组RSA公钥,且先保存。
-----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANE6WKCgLluv3Fn5kqg9HR+XwJ6iattQ CmqV/xEOxJU9HVUl30hoVvRspz1a5rQ8xiZhueWRhNMkdkmyO18itPkCAwEAAQ== -----END PUBLIC KEY-----
再解析base64字符串,第一组得
经过了解,这是一种叫 jjencode 的混淆算法,通过在线工具解密得到又一组base64,解密得乱码。想到可能与RSA解密相关。暂存
看方才第二组编码 继续解码得到两个链接
dive.giggle.cow/file/d/26TTOq3zCg-2nrBdeTWtV7tHgg4z-xxxXx/view?usp=sharing s1.ax1x.com/2000/01/01/bCZFqK.png
分别是一张gif → (文件名:This_is_d.gif) 与一张二维码图片。等等,二维码好像不对劲?
通过查阅资料了解到,二维码(QR Code)的的关键部位就在三个基准点和黑白相间的基准线上
其中黑白相间的基准线立刻引起了我的注意。在对畸形的二维码进行分析后,发现左半侧的图像经过了旋转变换
我通过python脚本来修复二维码(Photoshop,GIMP等工具亦可)
1 from PIL import Image 2 3 img_origin = Image.open('qr.png') 4 img = img_origin.copy() 5 img_origin.close() 6 width = img.width 7 height = img.height 8 left_img = img.crop((6,6,width//2-10,height-6)) 9 left_img = left_img.rotate(180) 10 img.paste(left_img,(6,6)) 11 img.show()
得到原图 通过在线工具得出其中内容,还是base64
******************************************************************* U2FsdGVkX18I1TdtQoyW2bd02ewoSz+5Zo4c3Tit1r3mA0Z+jwMw9uhwIR6o+kN/ nK2pbsHjs8U5CqN9oKC6wg== *******************************************************************
解析 得到Salted__开头的加密字符串,根据经验,判断这是aes加密的字符串,保存。
顺利解决的部分就告一段落。总结整理,得到一张闪动的gif,一组RSA公钥以及对应的密文,一组aes密文。
0x02 困境
前半段告结。剩下的数据令人有些摸不着头脑。黑白闪动的图片?难道是摩尔斯电码?
使用identify工具查看各帧的显示时间,剔除重复项后发现间隔长短不一,从0.1-1.1s不等。
而查阅资料得摩尔斯电码的时间点最多只有1,3,5,7四种。此路不通。只能换个任务思考。
RSA密文怎么解密?我可只有公钥,而RSA在应用场景中只有私钥才能解密(理论中公钥与私钥是可以交换的)。
我先想到了针对RSA的攻击。先通过openssl查看公钥的$n$,$e$。
唔,512bit不好暴力拆解n,抱着试试看的心理,上传到factordb,出现意想不到的一幕
???一个合法的$n$竟然无法分解?再仔细看,发现是机(jiǎo)智(huá)的出题人故意上传了无解的结果来欺骗我们。。
一条能走的路都没有了,我只好一边用yafu跑着暴力分解,一边苦苦思索解题思路。
0x03 突破
终于,发现提示就近在眼前!RSA中的私钥$d$,莫非就是This_is_d.gif所指?
黑白闪动,不就是01信号嘛!帧长度就是行程!立刻动手,使用python解析隐藏的二进制数据。
1 from PIL import Image 2 3 img = Image.open('This_is_d.gif') 4 frame = 1 5 img.seek(img.tell()) 6 7 try: 8 while True: 9 rgb_img = img.convert('RGB') 10 color = rgb_img.getpixel((5, 5)) 11 duration = img.info['duration'] 12 if color == (255, 255, 255): 13 state = '1' 14 else: 15 state = '0' 16 print(state * (duration//100),end='') 17 img.seek(img.tell() + 1) 18 frame += 1 19 except EOFError: 20 pass
二进制数据转16进制得
得到了$d$,又有$n$,根据RSA解密公式 $c^{d}\equiv m\pmod N$,编写python脚本
1 import base64 2 import binascii 3 4 n = 0xd13a58a0a02e5bafdc59f992a83d1d1f97c09ea26adb500a6a95ff110ec4953d1d5525df486856f46ca73d5ae6b43cc62661b9e59184d3247649b23b5f22b4f9 5 d = 0x63cd1e0b787cc4756d7969a7c0226eaaec3b10304f224fdab81ed66d2f8b2bd7ff4010a5f498faf01a32139ac371c4a32fddc3bcac47ad53914eee70ea4bb651 6 with open('task2_base64.txt') as f: 7 str_base64 = f.read() 8 encrypted = int(binascii.b2a_hex(base64.b64decode(str_base64)),16) 9 plain = hex(pow(encrypted,d,n))[2:] 10 padding_end = plain.find('00')+2 #--!-!--注意 RSA PKCS#1 padding--!-!-- 11 str_plain = binascii.a2b_hex(plain[padding_end:]) 12 print(str_plain)
Succeed ! 解密得到字符串 839$nc*!9vx_ ,很有可能就是aes密文的密码。使用openssl工具进行解密成功。得到一组字符串。
大写字母,2个以上的=号,莫不是base编码家族中的base32?通过在线工具解析, 最终拿到flag。
0x04 总结
总的来看,这道题难度一般,综合了Crypto,Misc等知识点,又通过“此地无银三百两”的手法设置障碍。本次解题过程前半部分一切顺利,失误就在于缺乏对提示的敏感性(其实是太菜),导致进入了错误的解题方向,需要我整理思考。
0x05 后记
本次解题过程中使用的几种工具与python模块:
factordb [http://www.factordb.com/index.php]
Pillow [https://pypi.org/project/Pillow/]
identify [https://imagemagick.org/script/identify.php]
openssl [https://github.com/openssl/openssl]
我再推荐几种常用的、本文中未使用的工具与python模块:
rsatool [https://github.com/ius/rsatool]
gmpy2 [https://pypi.org/project/gmpy2/]
yafu [https://sourceforge.net/projects/yafu/]
pycrypto [https://pypi.org/project/pycrypto/]
具体安装及用法则不再赘述。
0xFF 题外话
在本题RSA解密中,由于加密前明文采用PKCS#1标准进行padding,我在编写脚本解密时未考虑到,导致解密不出明文。
通过查阅规范文本【RFC 8017】了解该标准下的解密方式后解密成功,下附规范文本中的解密步骤:
7.2.2. Decryption Operation RSAES-PKCS1-V1_5-DECRYPT (K, C) Input: K recipient's RSA private key C ciphertext to be decrypted, an octet string of length k, where k is the length in octets of the RSA modulus n Moriarty, et al. Informational [Page 29] RFC 8017 PKCS #1 v2.2 November 2016 Output: M message, an octet string of length at most k - 11 Error: "decryption error" Steps: 1. Length checking: If the length of the ciphertext C is not k octets (or if k < 11), output "decryption error" and stop. 2. RSA decryption: a. Convert the ciphertext C to an integer ciphertext representative c (see Section 4.2): c = OS2IP (C). b. Apply the RSADP decryption primitive (Section 5.1.2) to the RSA private key (n, d) and the ciphertext representative c to produce an integer message representative m: m = RSADP ((n, d), c). If RSADP outputs "ciphertext representative out of range" (meaning that c >= n), output "decryption error" and stop. c. Convert the message representative m to an encoded message EM of length k octets (see Section 4.1): EM = I2OSP (m, k). 3. EME-PKCS1-v1_5 decoding: Separate the encoded message EM into an octet string PS consisting of nonzero octets and a message M as EM = 0x00 || 0x02 || PS || 0x00 || M. If the first octet of EM does not have hexadecimal value 0x00, if the second octet of EM does not have hexadecimal value 0x02, if there is no octet with hexadecimal value 0x00 to separate PS from M, or if the length of PS is less than 8 octets, output "decryption error" and stop. (See the note below.) 4. Output M.
另外,我们只要具有一定密码学基础,在已经得到$d$的情况下,就可以展开$d$泄漏攻击,还原出完整的pem私钥。
首先当$d$泄露之后,我们自然可以解密所有加密的消息。我们甚至还可以对模数$N$进行分解。其基本原理如下
我们知道$ed \equiv 1 \bmod \varphi(n)$,那么存在一个$k$使得
$ed-1=k\varphi(n)$
又$\forall a\in {Z}_n^*$,满足$a^{ed-1}\equiv1(\bmod n)$。令
$ed-1=2^st$
其中,$t$是一个奇数。然后可以证明对于至少一半的$a\in {Z}_n^*$,存在一个$i\in[1,s]$,使得
$a^{2^{i-1}t}\not\equiv\pm1(\bmod n),a^{2^{i}t}\equiv1(\bmod n)$
成立。如果$a$,$i$满足上述条件,$gcd(a^{2^{i-1}t}-1,n)$是$n$的一个非平凡因子,所以可以对$n$进行暴力分解。
下面使用rsatool得到私钥,并用openssl直接解密密文。