商丘师范学院第四届网络安全及信息对抗大赛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打开(旧的软件还打开不了需要更新)

image-20250410181508710

发现轨道最底部有一个fake flag(提交上去包报错的)SQCTF{yuan_shen_1s_a_good_game!},应该就是原神启动.zip压缩包解压密码了,解压后得到一个原神启动.docx,众所周知docx的文件也是一个压缩包,直接解压然后再vscode里面查看

image-20250410181842131

其中最需要关注的就是word\document.xml,最有可能塞东西,在文件里面找找还真找出来了:

image-20250410182119779

能明显看到SQCTF前缀的flag碎片,拼起来得到:SQCTF{f968566s-3fb6-4bfd-885a-d9e102528784}(提交上去之后也是假的),能明显注意到docx里面也存在加密的压缩包文件:img.zip,但如果用文档里面拿到的fake flag解压的话会发现报错,只能另找密码

可以看到文档里面也是存在图片word\media\image1.png,猜测可能在图片文件里面,用010editor打开

image-20250410183206656

可以发现文件后面存在字符串,直接随波逐流一把梭

image-20250410184755251

发现base58解码之后的最有可能(有一对{}还有-还有SQCTF):S-2Q4av7C77d}T5evF66c{--y6r0db7esbutg28g73-,继续一把梭

image-20250410184943077

发现为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

拿到文件发现是一个彩色色块图片

1

(其实如果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打开,切换到频谱图

image-20250407214334405

拿到flag:SQCTF{Rush_B}

love.host

拿到图片首先010Editor过一下

image-20250407214901807

发现文件末尾存在多余,而且长的还是zip格式的,直接分离出来zip文件

image-20250407215109386

解压后发现存在花花世界.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.wavflag2.bmp,还有一个上传文件接口(根据贝斯64说她想要一张最美的风景图片猜测是flag3接口),以下逐步分析


先对flag1.wav分析,直接audacity打开调到频谱图

image-20250410190717299

image-20250410190733846

可以看到在歌曲中间和结尾存在两段摩斯电码,直接进行解码(看的好糊我说直接拖到对应进度听写出来的):

--...
-...
-....
-....
--...
.....
...--
结果:7B66753

....-
...--
..---
...--
----.
..---
-..
结果:432392D

将两段拼接得到7B66753432392D,直接16进制解码,拿到flag1:{fu429-


接着分析flag2.bmp,010editor打开没发现东西,那就用StegSolve打开

image-20250410191255198

使用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字符串,切段拆分一下

image-20250413124552705

先按照长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;处打上断点)

image-20250410194531091

设置调试器后F9开启调试

image-20250410194651292

双击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 )
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值