base64stego (来源:攻防世界)
1.关卡描述
2.解题步骤
分析:
首先题目就告诉了我们base64,stego不知道是什么可能是某个工具,描述又提到十三,那么存在ROT13解密的可能性。(笔记:stego是Steganography的缩写,即隐写)
下载附件,发现是一个加密的zip文件,没有什么密码提示,估计就是伪加密了
利用010editor找到并修改09改成00
解密之后,看到文本内容:
base64解码一下:
又去解码了一小段:
发现结果是一样的,利用脚本试试,发现也不行
那么,我们先ROT13一下:
base64之后:
又试了一下先base64再rot13,还是不行,完全不知道怎么做了。
=================================
参考资料:
看题目来源是olympicCTF,这是俄罗斯2014 年有道 misc 题是关于 Base64 的隐写题,那我们直接写解码,这里的思路是先循环解密base64字符串,提取出可以隐写的最后2-4位,再拼接最后转回ascii码flag就出来了
import base64
base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
base64=''
def deBase64(base64_):
data=''
Bytes=""
num=0
for i in range(len(base64_)):
if base64_[i] == '=':
data = "00"
num -= 2
#一个“=”可以隐藏2bit数据
else:
data = bin(base64chars.find(base64_[i]))[2:]
#base64chars共64个数,二进制表示0111111,所以舍弃八位二进制的最高位
Bytes += data.zfill(6)
#不够六位二进制的前面补0,不断往后加成字符串
return Bytes[num:]
#返回隐藏的数据内容
with open('stego.txt','rb') as f:
#rb:以二进制格式打开一个文件用于只读,文件指针将会放在文件的开头。这是默认模式
flag = ''
bin_str = ''
for line in f .readlines():
#readlines()方法用于读取所有行(直到结束符 EOF)并返回列表
base64 = str(line,"utf-8").strip("\n")
#去掉每行头尾空白
if(not base64.count('=')):
continue
#该行无“=”则下一行
bin_str += deBase64(base64)
#将隐藏的内容拼接成字符串
for j in range(0,len(bin_str),8):
flag += chr(int(bin_str[j:j+8],2))
#以字节为单位,将二进制转化为对应ASCII码字符
print("flag{{{}}}".format(flag.strip(b'\x00'.decode())))
#去除不可见字符显示
===========
接下来来学习一下base64隐写的原理
复习一下 Base64 吧:
https://www.tr0y.wang/2017/06/14/Base64steg/#%E5%A4%8D%E4%B9%A0%E4%B8%80%E4%B8%8B-Base64-%E5%90%A7
BASE64 是一种编码方式, 是一种可逆的编码方式.
编码后的数据是一个字符串, 包含的字符为: A-Za-z0-9+/
共 64 个字符:26 + 26 + 10 + 1 + 1 = 64
其实是 65 个字符, = 是填充字符.
64 个字符需要 6 位二进制来表示, 表示成数值为 0~63.
这样, 长度为 3 个字节的数据经过 Base64 编码后就变为 4 个字节
编码
比如, 字符串”Tr0”经过 Base64 编码后变为”VHIw”
上面说的字符串长度为 3 个字节的数据位数是 8x3=24, 可以精确地分成 6x4.
如果字节数不是 3 的倍数, 则位数就不是 6 的倍数, 那么就不能精确地划分成 6 位的块.
此时, 需在原数据二进制值后面添加零, 使其字节数是 6 的倍数.
然后, 在编码后的字符串后面添加 1 个或 2 个等号”=”, 表示所添加的零值字节数.
比如, 字符串”Tr0y”经过 Base64 编码后变为”VHIweQ==”
橙色底纹就是添加的 0.
这是 Base64 编码的方式.
解码
解码就是编码的逆过程.
- 把 Base64 字符串去掉等号, 转为二进制数(VHIweQ== -> VHIweQ -> 010101000111001000110000011110010000).
- 从左到右, 8 个位一组, 多余位的扔掉, 转为对应的 ASCII 码(01010100 01110010 00110000 01111001 0000 -> 扔掉最后 4 位 -> 01010100 01110010 00110000 01111001 -> Tr0y)
隐写原理
注意红色的 0, 我们在解码的时候将其丢弃了, 所以这里的值不会影响解码. 所以我们可以在这进行隐写.
为什么等号的那部分 0 不能用于隐写? 因为修改那里的二进制值会导致等号数量变化, 解码的第 1 步会受影响. 自然也就破坏了源字符串.
而红色部分的 0 是作为最后一个字符二进制的组成部分, 还原时只用到了最后一个字符二进制的前部分, 后面的部分就不会影响还原.
唯一的影响就是最后一个字符会变化. 如下图
如果你直接解密’VHIweQ==’与’VHIweR==’, 得到的结果都是’Tr0y’.
当然, 一行 base64 顶多能有 2 个等号, 也就是有 2*2 位的可隐写位. 所以我们得弄很多行, 才能隐藏一个字符串, 这也是为什么题目给了一大段 base64 的原因.
接下来, 把要隐藏的 flag 转为 8 位二进制, 塞进去就行了.
加密
# -*- coding: cp936 -*-
import base64
flag = 'Tr0y{Base64isF4n}' #flag
bin_str = ''.join([bin(ord(c)).replace('0b', '').zfill(8) for c in flag])
base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
with open('0.txt', 'rb') as f0, open('1.txt', 'wb') as f1: #'0.txt'是明文, '1.txt'用于存放隐写后的 base64
for line in f0.readlines():
rowstr = base64.b64encode(line.replace('\n', ''))
equalnum = rowstr.count('=')
if equalnum and len(bin_str):
offset = int('0b'+bin_str[:equalnum * 2], 2)
char = rowstr[len(rowstr) - equalnum - 1]
rowstr = rowstr.replace(char, base64chars[base64chars.index(char) + offset])
bin_str = bin_str[equalnum*2:]
f1.write(rowstr + '\n')
解密
# -*- coding: cp936 -*-
b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
with open('1.txt', 'rb') as f:
bin_str = ''
for line in f.readlines():
stegb64 = ''.join(line.split())
rowb64 = ''.join(stegb64.decode('base64').encode('base64').split())
offset = abs(b64chars.index(stegb64.replace('=','')[-1])-b64chars.index(rowb64.replace('=','')[-1]))
equalnum = stegb64.count('=') #no equalnum no offset
if equalnum:
bin_str += bin(offset)[2:].zfill(equalnum * 2)
print ''.join([chr(int(bin_str[i:i + 8], 2)) for i in xrange(0, len(bin_str), 8)]) #8 位一组
很多国外的 CTF 出题思路异常开阔, 值得好好学学.