Al x Cybersecurity Challenge香港人工智能网络安全挑战赛wp(Miscellaneous全) Miscellaneousdatabase题目描述某公司的RAID 5业务服务器备份只保留下部分数据。请检查残留证据尽可能还原有用内容并找出隐藏的 flag。题目说的很明确了这个服务器采用的是RAID 5而且是只保留下部分数据看起来是一道RAID 5恢复的题目应该是先恢复RAID 5阵列再进入文件系统找flag那在找之前我们自然是先要搞明白什么是RAID 5阵列RAIDRedundant Array of Independent Disks即独立磁盘冗余阵列我们一般称之为磁盘阵列其实本质上就是用多个独立的磁盘组成在一起形成一个大的磁盘系统从而实现比单块磁盘更好的存储性能和更高的可靠性而RAID5正是其中一种排列方式实际上这是目前用的最多的一种磁盘阵列排列方式因为其兼顾了存储性能、数据安全和存储成本其排布大致如下RAID 5至少需要3块硬盘数据会被分成若干数据块分别写入不同硬盘同时系统会计算一份用于恢复数据的校验信息 Parity并将校验信息分散存储在各块硬盘中。例如有 3 块硬盘校验块不会固定放在某一块硬盘上而是轮流分布这样可以避免某一块专门负责校验的硬盘成为性能瓶颈那如何恢复数据呢RAID 5一般采用的是XOR技术例如 A XOR B P 存进去的那么如果A的数据坏了只要 P XOR B A 就能把A恢复出来所以只要同时坏的不超过1块RAID 5通常仍然可以继续读取数据好来看题来可以看到题目给了两块镜像file disk1.img file disk2.img检查文件类型可以直接发现是三块盘搭的RAID 5但是只给了两块尝试降级恢复看看具体的RAID元数据我们一般在Linux直接用mdadm来处理RAID先把这两个镜像变成loop设备因为RAID的成员盘一般不能直接挂载我们先建立loop设备其实就是把.img文件给模拟成一块磁盘 给它分配一个/dev/loopX设备sudo losetup -fP disk1.img sudo losetup -fP disk2.img查看 loop 设备对应关系losetup -a建立好loop设备后就可以用mdadm来看元数据了sudo mdadm --examine /dev/loop0 sudo mdadm --examine /dev/loop1可以看到关键信息这两块分别是role 0和role 1即第一块成员盘和第二块成员盘我们缺失了role 2第三块成员盘注意这一步是必须的因为我们得知道这俩分别是哪一块成员盘才能按条带规则来重组RAID由于缺失了一块盘所以我们需要降级模式来组装RAIDsudo mdadm --assemble --readonly --run /dev/md0 /dev/loop0 /dev/loop1各参数大概是下边这些意思--assemble 组装已有 RAID 阵列 --readonly 只读方式组装避免搞坏了 --run 即使阵列不完整也尝试启动 /dev/md0 生成的 RAID 设备名 /dev/loop0 第一个 RAID 成员 /dev/loop1 第二个 RAID 成员cat /proc/mdstat即可看见RAID的状态这边的[UU_]中U表示正常存在的成员盘_表示缺失的成员盘通过 RAID 规则把成员盘重新组合起来后我们终于得到了完整的 ext4 文件系统开始挂载就好了sudo mkdir -p /mnt/raid_recover sudo mount -o ro /dev/md0 /mnt/raid_recover创建一下挂载目录然后直接只读挂载cd过去发现成功恢复我们已经进入了原文件系统之后就是找东西环节了题目说是数据我们可以搜一下sudo find /mnt/raid_recover -type f | grep -iE db|sqlite|database这个 /var/lib/companydb/important.db的数据库明显很可疑打开看看sqlite3 /mnt/raid_recover/var/lib/companydb/important.db.tables看看表名就发现个此地无银三百两的表我们直接全部拉出来看看SELECT * FROM flag_not_here;给了四个flag不过仔细看看就能发现只有最后一个是对的所以本题答案为flag{raid5_recovery_1s_1mportant}ntfs_dump题目描述某员工将数据隐藏到bin文件中请尽可能还原数据并找出隐藏的flagflag格式为FLAG{xxx}。’和题目描述一模一样啊只给了一个bin文件说是把数据隐藏进去了让我们还原先file看看文件格式发现被识别为了NTFS的启动扇区看起来像是个NTFS文件系统镜像但是扔到Xways报错了似乎不是一个正常的NTFS镜像不过BPB似乎没有损坏我们还是能看到一些基础参数可以看到这边每扇区字节数是00 02小端序存储所以应该是0x0200即512字节每簇是8个扇区所以是每簇8*5124096字节扇区总数是0x4000即16384小端序$MFT起始簇号为4小端序不过其实Xways的技术报告可以直接分析出就是了$MFT 偏移 $MFT 起始簇号 × 簇大小所以$MFT 偏移应该是16384即0x4000正常情况下准备按应该能看到FILE但是现在这里塞了一大堆的伪造文件头这会导致binwalk一类的工具无法检测显然我们必须手动搜索些有用的才行那一般这种数据恢复的题目都会放到压缩包里作为载体所以我们可以优先对于ZIP的几个主要结构十六进制进行搜索主要是下边这三个PK\x03\x04 Local File Header本地文件头 PK\x01\x02 Central Directory中央目录 PK\x05\x06 End of Central Directory中央目录结束记录当然其中中央目录当然是最主要的里边会记录很多东西from pathlib import Path data Path(ntfs_dump.bin).read_bytes() for sig in [ bPK\x03\x04, bPK\x01\x02, bPK\x05\x06, btext.txt, ]: pos [] start 0 while True: i data.find(sig, start) if i -1: break pos.append(i) start i 1 print(sig, len(pos), [hex(x) for x in pos[:10]])发现了虽然PK\x03\x04有很多个但是PK\x01\x02和PK\x05\x06都只出现了一次这说明有一个0x5b735附近的zip很有可能是真的探查一下基本信息from pathlib import Path import struct data Path(ntfs_dump.bin).read_bytes() cd_off 0x5b735 fields struct.unpack_from(4s6H3I5H2I, data, cd_off) ( sig, ver_made, ver_need, flag, method, mtime, mdate, crc, csize, usize, name_len, extra_len, comment_len, disk, int_attr, ext_attr, local_header_off, ) fields name data[cd_off 46 : cd_off 46 name_len] print(filename:, name) print(crc:, hex(crc)) print(compressed size:, csize) print(uncompressed size:, usize) print(method:, method) print(local header offset:, hex(local_header_off))根据各种偏移解析一下中央目录得到了一些信息重点可以看到压缩方法是0即stored不压缩直接存的所以压缩数据长度和原始数据长度相同搜索一下text.txt发现有两个地方都有我们知道text.txt是8字节所以它后面的数据起点应该是0x5910ae 8 0x5910b6也就是说第一段疑似文件内容从这里开始0x5910b6继续往后看会发现数据到某个位置突然变成大量假文件头模式假文件头开始位置是0x591911所以第一段真实数据范围是0x5910b6 ~ 0x591911计算长度0x591911 - 0x5910b6 0x85b 2139但是中央目录告诉我们text.txt应该有 4240 字节。还差2101字节即0x835字节呢不在这边会不会是和中央目录连在一起呢我们回去继续观察 ZIP 中央目录中央目录从0x5b735开始往前取2101字节看看就是0x5af00 ~ 0x5b735这个范围我们只要验证这两段拼起来的 CRC32 是否等于中央目录里的0x2bcce130即可确认正确与否其实from pathlib import Path import zlib data Path(ntfs_dump.bin).read_bytes() part1 data[0x5910b6:0x591911] part2 data[0x5af00:0x5b735] payload part1 part2 print(part1 length: , len(part1)) print(part2 length: , len(part2)) print(total length: , len(payload)) print(crc32: , hex(zlib.crc32(payload) 0xffffffff))发现合起来还真的crc32一样确认了就是这个我们开始尝试自己创建一个zip缺一个文件头我们尝试按中央目录重建本地文件头from pathlib import Path import struct import zlib import zipfile dump Path(ntfs_dump.bin).read_bytes() # ------------------------------------------------------------ # 1. ZIP 中央目录和 EOCD 的位置 # ------------------------------------------------------------ cd_off 0x5b735 eocd_off 0x5b76b # ------------------------------------------------------------ # 2. 解析中央目录 # ------------------------------------------------------------ fields struct.unpack_from(4s6H3I5H2I, dump, cd_off) ( sig, ver_made, ver_need, flag, method, mtime, mdate, crc, csize, usize, name_len, extra_len, comment_len, disk, int_attr, ext_attr, local_header_off, ) fields filename dump[cd_off 46 : cd_off 46 name_len] print([] filename: , filename.decode()) print([] crc: , hex(crc)) print([] compressed size: , csize) print([] uncompressed size: , usize) print([] method: , method) # ------------------------------------------------------------ # 3. 恢复 text.txt 的两段真实数据 # ------------------------------------------------------------ part1 dump[0x5910b6:0x591911] part2 dump[0x5af00:0x5b735] payload part1 part2 assert len(payload) csize assert (zlib.crc32(payload) 0xFFFFFFFF) crc print([] payload recovered) print([] payload length: , len(payload)) print([] payload crc32: , hex(zlib.crc32(payload) 0xFFFFFFFF)) # ------------------------------------------------------------ # 4. 构造 Local File Header # ------------------------------------------------------------ local_header ( struct.pack( 4s5H3I2H, bPK\x03\x04, ver_need, flag, method, mtime, mdate, crc, csize, usize, name_len, 0, ) filename ) # ------------------------------------------------------------ # 5. 拷贝原始中央目录和 EOCD # ------------------------------------------------------------ central_directory dump[cd_off : cd_off 0x36] eocd dump[eocd_off : eocd_off 22] recovered_zip local_header payload central_directory eocd Path(recovered.zip).write_bytes(recovered_zip) print([] recovered.zip written) # ------------------------------------------------------------ # 6. 测试 ZIP 是否可以正常读取 # ------------------------------------------------------------ with zipfile.ZipFile(recovered.zip, r) as z: print([] zip files:, z.namelist()) text z.read(text.txt) Path(text.txt).write_bytes(text) print([] text.txt extracted:, len(text), bytes)得到了恢复的压缩包和text.txt但是发现不是明文还需要提取才行注意到这个数据的大小是4240字节4240 16 * 265猜测可能有固定步长隐写可能是每 16 字节藏 1 字节真实数据但是步长和偏移不知道我们可以爆破看看from pathlib import Path import re data Path(text.txt).read_bytes() for step in range(2, 65): for offset in range(step): stream data[offset::step] m re.search(rbFLAG\{[^}]\}, stream) if m: print([] found) print(step , step) print(offset , offset) print(flag , m.group().decode())发现确实是每 16 字节藏 1 字节真实数据得到了本题的flagFLAG{DE6DDD7A-5D57-415A-99D7-6C0CE3F688BC}Riddler题目描述A misplaced note is full of strange phrases and misleading remarks. The real message is hidden among relationships that look accidental at first glance. Can you separate the signal from the noise?一份放错地方的笔记中充满了奇怪的短语和误导性的描述。真正的信息藏在乍看偶然的关系之中你能从噪声里找出线索吗给的附件很直接啊一个有flag的压缩包和一个秘密的txt这txt应该就是题目里那个充满了奇怪的短语和误导性描述的笔记了打开可以看到的确和题目说的一样充满了噪声而线索正藏在这里边不难定位到关键是中间那部分بعدكـ is to رباعيه, ? is to love-buzz الْحي is to toyota, ? is to الغيـرة feriass is to dham, ? is to much ميسيnn is to دعواتكمn, ? is to were يصيير is to newt, ? is to ta لأمانة is to roan, ? is to viral elain is to omán, ? is to dy一眼看过去的话像是什么类比题A is to B, ? is to C当然我们也可以写作A : B ? : C笔记里已经帮我们排除了很多可能了不是频率分析不是进制转化更不是翻译隐藏的是需要我们找到其中的关系忽略那些重复的意思找到这些词的关系这题需要注意题目和文件里给的那些关键词unfinished language experiment strange scripts not direct translation file appears too short这些词指向了词向量尤其是专门强调了文件太短too short专门说了这也是解密的一部分其实也是一种提示提示我们是短维度的模型所以这题的考点其实是词向量类比 word embedding analogy词向量是什么呢可以理解为是某些语义关系可以通过向量加减体现出来有点难以理解吧我们可以举个例子先比如king - man woman ≈ queen为什么词可以变成向量因为一个词的含义可以由其上下文决定比如king和queen经常在一个上下文computer就离这俩比较远这就必须引入GloVe这个词向量模型的概念了它的作用就是把一个词变成一串数字也就是一个向量注意这些数字不是乱来的而是这个模型通过大量文本训练出来的那本题中的格式是A : B ? : C我们可以理解为如果关系是从A到B那么同样的关系也应该从?到C也就是说? B - A C所以每一行都要计算这个式子才行vector(?) vector(B) - vector(A) vector(C)最后我们还要在词向量模型中找最接近这个向量的词那题目已经提示了这一个词向量模型应该是一个短维度的模型这个可能比较难想吧或许也需要尝试多亏了我们队伍的Jerry刚刚做过类似的题目一试就出来了是 glove-twitter-25 模型即Twitter语料上的 25 维 GloVe 词向量模型最后直接写代码就好了import gensim.downloader as api model api.load(glove-twitter-25) rows [ (بعدكـ, رباعيه, love-buzz), (الْحي, toyota, الغيـرة), (feriass, dham, much), (ميسيnn, دعواتكمn, were), (يصيير, newt, ta), (لأمانة, roan, viral), (elain, omán, dy), ] result [] for a, b, c in rows: word, score model.most_similar( positive[b, c], negative[a], topn1 )[0] print(f{b} - {a} {c} {word} score{score}) result.append(word) password .join(result) print(password , password)这边要进行词向量的运算自然少不了Gensim库这主要是拿来处理词向量、加载词向量模型的在这边我们就可以让其自动下载并加载我们要用的这个短维的词向量模型得到结果行运算最近词1رباعيه - بعدكـ love-buzzctf2toyota - الْحي الغيـرةmaybe3dham - feriass muchyou4دعواتكمn - ميسيnn wereare5newt - يصيير taai6roan - لأمانة viralmaster7omán - elain dyado拼接就能得到密码了ctfmaybeyouareaimasterado最后直接拿密码打开压缩包即可得到flagflag{39fc0ee0f9da4fa397631f0dcba31555}Mysterious Code题目描述一次 IDS 告警留下了BASE58的可疑流量。请分析这些痕迹还原隐藏内容并找出其中的秘密。只给了一份流量包题目给的描述明显是先分析流量然后可能涉及BASE58编码最后要我们还原隐藏内容才行wireshark打开一看发现是大量的DNS流量而且DNS查询的域名格式非常类似0-GRSXGZ2UK5YGQSSSORMTCVDHLJWXKNCQ.hackg.fun 1-NBTHIQTHKZTXKRL2IZRUE4DVHFLDSRDT.hackg.fun 2-OVATQZDRPB2VKNDEKM3E6TSEIZSWUWKX.hackg.fun ... EOF-f1b662512c1fcbfc.hackg.fun通过观察不难发现这些域名的格式都是统一的序号-数据.hackg.fun的格式可以看到最后还有EOF这说明DNS查询很可能被用来传输隐藏数据攻击者利用域名字段携带文本来传输由于单个域名长度有限所以拆成了很多片前边的序号就是排序的所以我们要按序号提取这些分片的内容才行from scapy.all import rdpcap, DNSQR from collections import defaultdict, Counter from pathlib import Path import re pcap Path(Mysterious_Code.pcapng) pkts rdpcap(str(pcap)) pat re.compile(rb^(\d)-([A-Za-z0-9])\.hackg\.fun\.?$, re.I) chunks defaultdict(list) for pkt in pkts: if pkt.haslayer(DNSQR): q pkt[DNSQR].qname m pat.match(q) if m: idx int(m.group(1)) data m.group(2).decode() chunks[idx].append(data) print(chunk count:, len(chunks)) print(index range:, min(chunks), max(chunks)) s for i in sorted(chunks): s Counter(chunks[i]).most_common(1)[0][0] print(concat length:, len(s)) print(alphabet:, .join(sorted(set(s)))) print(result:, s)输出了很多内容我们在输出内容的同时也整理了alphabet得到字符表为23456ABCDEFGHIJKLMNOPQRSTUVWXYZ这很容易让人联想到Base32编码其字符表如下ABCDEFGHIJKLMNOPQRSTUVWXYZ234567虽然没有出现7但仍然符合Base32的字符范围所以我们尝试解码可以用cyberchef进行解码成功解码出可读内容继续对解码后的这串内容进行研究提取字符表其实也可以用Frequency Distribution进行研究得到新的字符表为123456789ABCDEFGHIJKMNOPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz正好58个不免让人想到题目提示的Base58编码但是这和默认的Base58字符表不同123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz实际上我们默认的Base58编码一般指的都是Bitcoin Base58字母表因为这个编码本身就是比特币的创造者中本聪设计的专门用于生成人类友好的标识符和地址的这Base58和Base64最大的区别就是刻意排除了容易引起混淆的字符如数字0和大写O还有大写的I和小写的l还有和/这六个但是这个其实是没有唯一标准的本题就是利用了这个设计初衷修改了Base58的字母表从我们统计的内容即可发现少了 0 L i o在再这些字符表的基础上尝试一下大小写和数字的排布不难发现本题的字母表应该是123456789abcdefghjklmnpqrstuvwxyzABCDEFGHIJKMNOPQRSTUVWXYZ即“数字 小写字母 大写字母”把这个字符表放到cyberchef即可看文件头就可以确认正确了我们解出了隐藏内容一个压缩包里边是一个Store存储、ZipCrypto加密方法的png图片明显是明文爆破的题目由于PNG文件有用固定的文件头开头一般固定为文件头89504e470d0a1a0a之后大概率紧跟IHDR块也就是可以确定前16字节89504e470d0a1a0a0000000d49484452作为明文爆破的明文绰绰有余了创一个1.bin的文件之后直接bkcrack明文爆破即可bkcrack -C download.zip -c secret.png -p 1.bin -o 0 -j 8成功得到keys直接生成无密码的zip即可bkcrack -C download.zip -k d1c9002e fddfca1e 86c3fee5 -D decrypted.zip成功得到解密后的图片打开来就是本题的flag了flag{Crack_is_to_ezzzzzzzz}