商丘师范学院第四届网络安全及信息对抗大赛WP【万字分享】
解题过程
—— MISC ——
FFT IFFT
一开始拿到题的时候其实是一脸懵的,但是一看有正向脚本:
import os
import cv2
import struct
import numpy as np
def mapping(data, down=0, up=255, tp=np.uint8):
data_max = data.max()
data_min = data.min()
interval = data_max - data_min
new_interval = up - down
new_data = (data - data_min) * new_interval / interval + down
new_data = new_data.astype(tp)
return new_data
def fft(img):
fft = np.fft.fft2(img)
fft = np.fft.fftshift(fft)
m = np.log(np.abs(fft))
p = np.angle(fft)
return m, p
if __name__ == '__main__':
os.mkdir('m')
os.mkdir('p')
os.mkdir('frame')
os.system('ffmpeg -i secret.mp4 frame/%03d.png') #视频拆分多张照片
files = os.listdir('frame')
r_file = open('r', 'wb')
for file in files:
img = cv2.imread(f'frame/{
file}', cv2.IMREAD_GRAYSCALE) #读取所有帧
m, p = fft(img)
r_file.write(struct.pack('!ff', m.min(), m.max()))
new_img1 = mapping(m) #绘制
new_img2 = mapping(p) #绘制
cv2.imwrite(f'm/{
file}', new_img1)
cv2.imwrite(f'p/{
file}', new_img2)
r_file.close()
os.system('ffmpeg -i m/%03d.png -r 25 -vcodec png 1.mkv')
os.system('ffmpeg -i p/%03d.png -r 25 -vcodec png 2.mkv')
根据加密脚本中每帧的fft变换,写出对应的解密脚本:
import os
import cv2
import struct
import numpy as np
from pathlib import Path
# 读取r文件中的幅度范围信息
r_data = open('r', 'rb').read()
num_frames = len(r_data) // 8 # 每个帧占8字节(两个float)
min_max_list = [struct.unpack('!ff', r_data[i * 8:(i + 1) * 8]) for i in range(num_frames)]
# 创建恢复帧的目录
output_dir = Path('rec')
output_dir.mkdir(parents=True, exist_ok=True)
# 处理每个帧
file_names = os.listdir('m') # 确保按文件名顺序处理
for iv, file_name in enumerate(file_names):
# 读取幅度图像并反归一化
min_m, max_m = min_max_list[iv]
img_m = cv2.imread(os.path.join("m", file_name), cv2.IMREAD_GRAYSCALE)
log_m = img_m.astype(np.float32) / 255.0 * (max_m - min_m) + min_m
mag = np.exp(log_m)
# 读取相位图像并反归一化到[-π, π]
img_p = cv2.imread(os.path.join("p", file_name), cv2.IMREAD_GRAYSCALE)
phase = (img_p.astype(np.float32) / 255.0) * (2 * np.pi) - np.pi
# 合成复数FFT并逆变换
fft_shifted = mag * np.exp(1j * phase)
fft_original = np.fft.ifftshift(fft_shifted)
frame = np.fft.ifft2(fft_original).real
frame = np.clip(frame, 0, 255).astype(np.uint8)
# 保存恢复的帧
cv2.imwrite(str(output_dir / file_name), frame)
os.system(f'ffmpeg -framerate 25 -i {
output_dir}/%03d.png -c:v libx264 -pix_fmt yuv420p rec.mp4')
合成后的rec.mp4
中内容就是HELLO
,flag:SQCTF{HELLO}
YuanShen_Start!
拿到压缩包发现两个文件,一个是audacity的工程文件当离别开出花(胡桃版).aup3
,一个被加了密的压缩包原神启动.zip
,先从第一个入手,直接用audacity打开(旧的软件还打开不了需要更新)
发现轨道最底部有一个fake flag(提交上去包报错的)SQCTF{yuan_shen_1s_a_good_game!}
,应该就是原神启动.zip
压缩包解压密码了,解压后得到一个原神启动.docx
,众所周知docx的文件也是一个压缩包,直接解压然后再vscode里面查看
其中最需要关注的就是word\document.xml
,最有可能塞东西,在文件里面找找还真找出来了:
能明显看到SQCTF
前缀的flag碎片,拼起来得到:SQCTF{f968566s-3fb6-4bfd-885a-d9e102528784}
(提交上去之后也是假的),能明显注意到docx里面也存在加密的压缩包文件:img.zip
,但如果用文档里面拿到的fake flag解压的话会发现报错,只能另找密码
可以看到文档里面也是存在图片word\media\image1.png
,猜测可能在图片文件里面,用010editor
打开
可以发现文件后面存在字符串,直接随波逐流一把梭
发现base58解码之后的最有可能(有一对{}
还有-
还有SQCTF
):S-2Q4av7C77d}T5evF66c{--y6r0db7esbutg28g73-
,继续一把梭
发现为W型栅栏密码分为11栏时出来了完整的flag:SQCTF{6bb238u7r-6574-a7e6-0etg-7gsdycvdv27}
(虽然交上去也是假的),使用该fake flag对压缩文件img.zip
解压得到一个也是被加密过的text.zip
,继续使用上面从文档里面拼出来的fake flag作为密码,拿到文件:
亲爱的CTFer你好,当你来到这里的时候证明本题的路途已经结束了,但你的旅途还在继续,前面依然有更多的挑战在等着你,加油 祝你前程似锦,不要忘记旅途的初衷
SQCTF{27fhcwg2h-hv2rv7b-82RFHCbbvw-289fv2}
最终flag:SQCTF{27fhcwg2h-hv2rv7b-82RFHCbbvw-289fv2}
piet
拿到文件发现是一个彩色色块图片
(其实如果ctf打多了,从题目名就知道是piet编程语言编译后的图片)
直接进行解码(工具下载地址:https://www.bertnase.de/npiet/)
D:\ctf\20250407shangqiu\piet .\npiet\npiet.exe -q .\1.png MEM 55% 97% 10:56:07
cygwin warning:
MS-DOS style path detected: .\1.png
Preferred POSIX equivalent is: ./1.png
CYGWIN environment variable option "nodosfilewarning" turns off this warning.
Consult the user's guide for more details about POSIX paths:
http://cygwin.com/cygwin-ug-net/using.html#using-pathnames
Hello world!
拿到flag:SQCTF{Hello world!}
ez_music1
直接用audacity打开,切换到频谱图
拿到flag:SQCTF{Rush_B}
love.host
拿到图片首先010Editor
过一下
发现文件末尾存在多余,而且长的还是zip格式的,直接分离出来zip文件
解压后发现存在花花世界.txt
文件,里面就是flag(SQCTF我记得要大写来着要不然不给过):sqctf{Sun Ensheng is the most handsome.}
花非花,雾非雾_0
写了半天到最后解密部分找密钥,问出题人居然要用在线工具才可以解出来,血压有点高,但最终还是拿了一血
打开链接直接看源码
body>
<div class="upload-container">
<h1>我才不会给你flag呢^..^</h1>
<form id="upload-form" action="upload.php" method="post" enctype="multipart/form-data">
<img src="flag2.bmp" alt="Upload Icon" class="upload-image">
<label for="file">贝斯64说她想要一张最美的风景图片:</label>
<input type="file" id="file" name="file" required><br><br>
<button type="submit">上传文件</button>
</form>
<div class="music-player">
<audio controls>
<source src="flag1.wav" type="audio/mpeg">
</audio>
</div>
<a href="flag1.wav" download class="download-icon" title="">⬇️</a>
</div>
能明显看到存在两个flag文件:flag1.wav
、flag2.bmp
,还有一个上传文件接口(根据贝斯64说她想要一张最美的风景图片
猜测是flag3接口),以下逐步分析
先对flag1.wav
分析,直接audacity打开调到频谱图
可以看到在歌曲中间和结尾存在两段摩斯电码,直接进行解码(看的好糊我说直接拖到对应进度听写出来的):
--...
-...
-....
-....
--...
.....
...--
结果:7B66753
....-
...--
..---
...--
----.
..---
-..
结果:432392D
将两段拼接得到7B66753432392D
,直接16进制解码,拿到flag1:{fu429-
接着分析flag2.bmp
,010editor打开没发现东西,那就用StegSolve
打开
使用Analyse
中的Data Extract
,如图所示选中(别问为什么,问就是写题写习惯了),拿到flag2:wey23-vbd23
最后flag3比较难做,就是卡这上面了
之前猜测的上传接口随便上传一张图片(图片不能大要不然会413,好像是任意文件都可以只要文件不大),拿到数据:
{
"status": "success",
"message": "贝斯64小心翼翼地捧着她精心收集的图片宝库,来到阿尔西四世先生的加密工作室。这位挑剔的图片鉴赏家戴着单片眼镜,仔细审视每一张图片。当他皱起眉头时,图片就会化作一串数字尘埃消失不见;而当他露出满意的笑容说"嘻嘻不嘻嘻"时,就会从西装内袋里取出一串神秘的魔法密文作为奖励——" U2FsdGVkX19hDm7VsaTyR1yuDiGFF8wr7aoQfg== ",这串字符在空气中闪烁着幽蓝色的微光。"
}
从中可以猜测:
加密方式:阿尔西四世
--> RC4
密钥:贝斯64
+嘻嘻不嘻嘻
--> base64后的嘻嘻不嘻嘻
--> 5Zi75Zi75LiN5Zi75Zi7
密文:U2FsdGVkX19hDm7VsaTyR1yuDiGFF8wr7aoQfg==
使用在线工具 https://www.sojson.com/encrypt_rc4.html 进行解码
ps: 就是这一点,试了大半天也没出来结果,最后问出题人原来是需要在线工具才可以,并且必须是指定的网站,好几个网站还都不行
最终得到flag3:-278fgy17dh}
至此将flag1-3进行拼接得到最终flag:SQCTF{fu429-wey23-vbd23-278fgy17dh}
哈哈
拿到文件,发现是个音频,直接audacity打开,听音频一长一短,但是没有摩斯密码的标识符,怀疑是01字符串,切段拆分一下
先按照长0短1来进行解码:
0101001101010001010000110101010001000110011110
110011010101000010010000110110100001101001010
0110001100100010111110100100100110101010111110111
001001100101001101000011000100110001010110010
10111110110001101010101010101000100010101111101
拼在一起:0101001101010001010000110101010001000110011110110011010101000010010000110110100001101001010011000110010001011111010010010011010101011111011100100110010100110100001100010011000101011001010111110110001101010101010101000100010101111101
二进制解码直接出来flag:SQCTF{5BChiLd_I5_re411Y_cUTE}
Welcome_Sign_in
签到题,没什么好说的直接加公众号拿flag:SQCTF{It_is_really_signin}
—— RE ——
天下谁人不识君
拿到一个py文件:
flag = 'SQCTF{xxxxxxxxxxxxxx}'
s = 'wesyvbniazxchjko1973652048@$+-&*<>'
result = ''
for i in range(len(flag)):
s1 = ord(flag[i]) // 17
s2 = ord(flag[i]) % 17
result += s[(s1 + i) % 34] + s[-(s2 + i + 1) % 34]
print(result)
# result = 'v7b3boika$h4h5j0jhkh161h79393i5x010j0y8n$i'
没什么好说的直接逆向就完事了:
s = 'wesyvbniazxchjko1973652048@$+-&*<>'
result = 'v7b3boika$h4h5j0jhkh161h79393i5x010j0y8n$i'
flag = ''
for i in range(len(result) // 2):
ch1 = result[2 * i]
ch2 = result[2 * i + 1]
for c in range(32, 127):
s1 = c // 17
s2 = c % 17
if s[(s1 + i) % 34] == ch1 and s[-(s2 + i + 1) % 34] == ch2:
flag += chr(c)
break
print(flag)
得到flag:SQCTF{libai_jianxian}
遇事不决,可问春风
拿到一个apk文件app-debug.apk
,直接jadx启动打开apk文件进行逆向,拿到Activity入口代码:
package com.example.wakurev;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
private static final String[] ENCRYPTED_PARTS = {
"5", "#", ")", "7", "5", "#", ")", "7"};
private static final String FLAG_PREFIX = "SQCTF{";
private static final String FLAG_SUFFIX = "}";
private static final int XOR_KEY = 66;
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final EditText input = (EditText) findViewById(R.id.etInput);
Button btn = (Button) findViewById(R.id.btnVerify);
final TextView tvResult = (TextView) findViewById(R.id.tvResult);
btn.setOnClickListener(new View.OnClickListener() {
// from class: com.example.wakurev.MainActivity$$ExternalSyntheticLambda0
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MainActivity.this.m49lambda$onCreate$0$comexamplewakurevMainActivity(input, tvResult, view);
}
});
}
/* JADX INFO: Access modifiers changed from: package-private */
/* renamed from: lambda$onCreate$0$com-example-wakurev-MainActivity, reason: not valid java name */
public /* synthetic */ void m49lambda$onCreate$0$comexamplewakurevMainActivity(EditText input, TextView tvResult, View v) {
String userInput = input.getText().toString();
if (checkPassword(userInput)) {
tvResult.setText("验证成功! Flag: " + buildFlag(userInput));
} else {
tvResult.setText("验证失败");
}
}
private boolean checkPassword(String input) {
return input.equals(decryptPassword());
}
private String decryptPassword() {
String encrypted = getEncryptedPassword();
char[] chars = encrypted.toCharArray();
StringBuilder result = new StringBuilder();
for (char c : chars) {
result.append((char) (c ^ 'B'));
}
return result.toString();
}
private String getEncryptedPassword() {
StringBuilder sb = new StringBuilder();
for (String part : ENCRYPTED_PARTS) {
sb.append(part);
}
return sb.toString();
}
private String buildFlag(String password) {
return "SQCTF{i_am_a_" + password + FLAG_SUFFIX;
}
}
发现最重要的解密函数decryptPassword
和加密后的密钥ENCRYPTED_PARTS
和flag头部SQCTF{i_am_a_
,直接写脚本
ENCRYPTED_PARTS = ["5", "#", ")", "7", "5", "#", ")", "7"]
enc = []
for part in ENCRYPTED_PARTS:
enc.append(chr(ord(part) ^ ord("B")))
print("SQCTF{i_am_a_" + "".join(enc) + "}")
得到flag:SQCTF{i_am_a_wakuwaku}
春风也有春风愁
直接ida64打开进行反编译,拿到伪c代码(部分变量名为了阅读性有所更改):
int __cdecl main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rax
char str_input[115]; // [rsp+20h] [rbp-90h] BYREF
_BYTE str_xor_a[15]; // [rsp+93h] [rbp-1Dh]
char str_input_ch_; // [rsp+A2h] [rbp-Eh]
char str_input_ch; // [rsp+A3h] [rbp-Dh]
int str_len_19; // [rsp+A4h] [rbp-Ch]
char v10; // [rsp+AAh] [rbp-6h]
char v11; // [rsp+ABh] [rbp-5h]
int i; // [rsp+ACh] [rbp-4h]
_main(argc, argv, envp);
v11 = -91;
v10 = 55;
*(_QWORD *)str_xor_a = 0xFBF715FA08FD0B0Dui64;
*(_QWORD *)&str_xor_a[7] = 0xF0E011431130DFBi64;
str_len_19 = 15;
printf("Enter flag: ");
v3 = __acrt_iob_func(0);
fgets(str_input, 100, v3);
str_input[strcspn(str_input, "\n")] = 0;
if ( strlen(str_input) == str_len_19 )
{
for ( i = 0; i < str_len_19; ++i )
{
str_input_ch = str_input[i];
str_input_ch_ = (str_input_ch ^ 165) + 55;
if ( str_input_ch_ != str_xor_a[i] )
goto LABEL_2;
}
printf("True\n");
return 0;
}
else
{
LABEL_2:
printf("False\n");
return 0;
}
}
发现最重要的赋值函数与xor运算逻辑:
*(_QWORD *)str_xor_a = 0xFBF715FA08FD0B0Dui64;
*(_QWORD *)&str_xor_a[7] = 0xF0E011431130DFBi64;
str_input_ch = str_input[i];
str_input_ch_ = (str_input_ch ^ 165) + 55;
if ( str_input_ch_ != str_xor_a[i] )
goto LABEL_2;
因为实在是没看懂变量str_xor_a
的运算逻辑,直接进行动态调试从内存里面找(在第18行str_len_19 = 15;
处打上断点)
设置调试器后F9
开启调试
双击str_xor_a
直接跳转到对应地址复制内存
Stack[00007360]:000000C3547FF923 db 0Dh
Stack[00007360]:000000C3547FF924 db 0Bh
Stack[00007360]:000000C3547FF925 db 0FDh
Stack[00007360]:000000C3547FF926 db 8
Stack[00007360]:000000C3547FF927 db 0FAh
Stack[00007360]:000000C3547FF928 db 15h
Stack[00007360]:000000C3547FF929 db 0F7h
Stack[00007360]:000000C3547FF92A db 0FBh
Stack[00007360]:000000C3547FF92B db 0Dh
Stack[00007360]:000000C3547FF92C db 13h
Stack[00007360]:000000C3547FF92D db 31h ; 1
Stack[00007360]:000000C3547FF92E db 14h
Stack[00007360]:000000C3547FF92F db 1
Stack[00007360]:000000C3547FF930 db 0Eh
Stack[00007360]:000000C3547FF931 db 0Fh
编写脚本进行逆向(在py中好像没有BYTE类型,写了个脚本但是变成有符号了,觉得很麻烦懒得转换了,于是用c语言写了脚本)
#include <cstdio>
#include <windows.h>
int main(int argc, char* argv[])
{
BYTE str_xor[15] = {
0x0D, 0x0B, 0xFD, 0x08, 0xFA, 0x15, 0xF7, 0xFB,
0x0D, 0x13, 0x31, 0x14, 0x01, 0x0E, 0x0F
};
// for ( i = 0; i < str_len_19; ++i )
for (int i = 0; i < 19; i++)
{
// str_input_ch_ = (str_input_ch ^ 165) + 55;
BYTE c = (str_xor[i] - 55) ^ 165;
printf("%c", c);
}
return 0;
}
最终得到flag(sqctf需要转为大写):SQCTF{easy_xor}
ezRe
一看exe文件经典pyinstaller生成的,pyinstxtractor.py
+pycdc
组合拳直接解出
python pyinstxtractor.py .\33.exe
pycdc.exe .\33.exe_extracted\33.pyc
拿到源码
# Source Generated with Decompyle++
# File: 33.pyc (Python 3.9)
import base64
encoded_flag = 'NWVkMmJlNDUtMmU4My00OGQyLWI2MzEtYzA4OGU1MWVlOTY0'
flag = base64.b64decode(encoded_flag).decode('utf-8')
print(flag)
慕然回首,那人却在灯火阑珊处
用ida64
加载文件,拿到伪c代码
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char move; // [rsp+2Fh] [rbp-1h] BYREF
_main();
puts("Welcome to the Maze Game!");
puts("Find the path from 'S' to 'E' using w/a/s/d to move.");
puts("Enter your moves (e.g., 'wasd'):");
while ( 1 )
{
scanf(" %c", &move);
if ( move == 'd' )
break;
if ( move > 100 )