队伍成绩:1029pt/13kill/50名/1396有解/1938登录/2358报名
个人成绩:522pt/5kill/73名/2234有解/7622登录/10147报名

Misc

谍影重重

拿到题目,一个带密码的压缩包,一个配置文件,一个流量包。

file

压缩包密码说是要拿到api address,大概在流量包里。

file

配置文件里面看起来只给了一部分必要参数?

file

根据关键词"config.json" "settings" "clients" "id"查询可以发现,Google首屏结果与V2Ray高度相关,所以推测流量包是VMess协议,后续放出来的hint也证实了这一点。

file

看下流量包,只有127.0.0.1:37886 - 127.0.0.1:10087的往返流量,发起方是127.0.0.1:37886,所以V2Ray服务器是127.0.0.1:10087

file

那就想办法解析流量呗,先是去看了一下V2Fly组织(因为原组织Project V的主导者失踪了)对VMess协议的一些数据包细节:VMess 协议 | V2Fly.org

尝试写Python脚本去解析流量,在第一天中午的时候马上搞定对开头认证信息的解析(因为真的很标准,没啥难度),爆破出了流量的时间戳是16155289822021-03-12 14:03:02)。

from hashlib import md5
import hmac

uuid = bytes.fromhex("b831381d-6324-4d53-ad4f-8cda48b30811".replace('-', ''))
t = int(time.time())
target = "4dd11f9b04f2b562b9db539d939f1d52b48b35bf592c09b21545392f73f6cef91143786464578c1c361aa72f638cd0135f25343555f509aef6c74cd2a2b86ee0a9eb3b93a81a541def4763cc54f91ba02681add1b815e8c50e028c76bde0ee8a9593db88d901066305a51a9586a9e377ee100e7d4d33fcfc0453c86b1998a95275cd9368a68820c2a6a540b6386c146ea7579cfe87b2e459856772efdcf0e4c6ab0f11d018a15561cf409cbc00491d7f4d22b7c486a76a5f2f25fbef503551a0aeb90ad9dd246a9cc5e0d0c0b751eb7b54b0abbfef198b1c4e5e755077469c318f20f3e418af03540811ab5c1ea780c886ea2c903b458a26"
while True:
    t -= 1
    result = hmac.new(uuid, t.to_bytes(8, "big"), md5).digest().hex()
    if target[0:32] == result:
        print(t, result)
        break

于是开始尝试写指令部分的解析,这里花了一个小时左右搞定了大致的解析,但是版本号 Ver不知道为什么不是文档说的固定值1校验 F也不知道为什么算不对,找了一下午+一晚上的原因都没找到,于是进度卡住了。

半夜突然想到可以调用V2Ray项目直接解析看看,于是找找看看有没有现成的解析流量相关的测试接口(因为这种大型项目发版前肯定要验证的),成功找到了proxy/vmess/encoding/encoding_test.go这个测试流程,里面有通过NewServerSession函数对ServerSession进行实例化,然后调用DecodeRequestHeader函数来解析客户端发过来的认证信息+指令部分,跟过去看了下,还有个DecodeRequestBody函数可以解析数据部分,而且这俩传参都是字节流,还有这种好事?

直接把整个项目拉下来,因为对于Golang不太熟悉(之前只写过一个简单的C/S架构网络小项目,所以勉强算会写的程度),所以也有样学样创建测试流程去解析吧,毕竟万一创建主程序出事了呢?

package encoding_test

import (
    "bytes"
    "encoding/hex"
    "fmt"
    "github.com/v2fly/v2ray-core/v5/common"
    "testing"

    "github.com/v2fly/v2ray-core/v5/common/protocol"
    "github.com/v2fly/v2ray-core/v5/proxy/vmess"
    . "github.com/v2fly/v2ray-core/v5/proxy/vmess/encoding"
)

func TestCtf(t *testing.T) {
    user := &protocol.MemoryUser{
        Level: 0,
        Email: "[email protected]",
    }
    account := &vmess.Account{
        Id:      "b831381d-6324-4d53-ad4f-8cda48b30811",
        AlterId: 0,
    }
    user.Account = toAccount(account)

    userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
    userValidator.Add(user)
    defer common.Close(userValidator)

    sessionHistory := NewSessionHistory()
    defer common.Close(sessionHistory)

    server := NewServerSession(userValidator, sessionHistory)

    requestHex := "4dd11f9b04f2b562b9db539d939f1d52b48b35bf592c09b21545392f73f6cef91143786464578c1c361aa72f638cd0135f25343555f509aef6c74cd2a2b86ee0a9eb3b93a81a541def4763cc54f91ba02681add1b815e8c50e028c76bde0ee8a9593db88d901066305a51a9586a9e377ee100e7d4d33fcfc0453c86b1998a95275cd9368a68820c2a6a540b6386c146ea7579cfe87b2e459856772efdcf0e4c6ab0f11d018a15561cf409cbc00491d7f4d22b7c486a76a5f2f25fbef503551a0aeb90ad9dd246a9cc5e0d0c0b751eb7b54b0abbfef198b1c4e5e755077469c318f20f3e418af03540811ab5c1ea780c886ea2c903b458a26"
    requestBuffer, _ := hex.DecodeString(requestHex)
    requestBufferReader := bytes.NewReader(requestBuffer)

    requestHeader, err := server.DecodeRequestHeader(requestBufferReader)
    if err != nil {
        t.Error(err)
    }
    fmt.Println(requestHeader)
    requestBody, err := server.DecodeRequestBody(requestHeader, requestBufferReader)
    if err != nil {
        t.Error(err)
    }
    fmt.Println(requestBody)
}

然后不出意外地报错了,具体原因的话,猜测是系统当前时间的锅,因为V2Ray的加密是基于当前秒级时间戳的,具体的话,文档里面有写,说是随机取上下30s的时间戳当关键参数,并且其存在贯穿了整个数据交互。那么咱们系统当前时间戳就应该要在1615528982左右才行。

file

那么想办法hook掉或者硬编码掉取系统当前时间的返回值吧,上面有个vmess.NewTimedUserValidator的调用,看名字应该就是基于用户id+当前时间的验证器,进去看看怎么取当前时间的,发现用的是语言层的函数time.Now()

file

跟进去看看实现,先下个断点调一下看看值的格式。

file

看起来应该是改第一个秒钟sec就行,在第二行加一句sec = 1615528982试试,成功pass掉刚才的测试。

file

不过RequestHeaderRequestBody似乎都没有文本化的实现,那就去下个断点看看值吧。

file

file

看着似乎没有咱们想要的数据啊,就一个代理目标是127.0.0.1:5000,走的TCP,这个之前用Python去解析的时候就知道了。然后用加密协议怎么变成AES-128-GCM了,之前Python解析出来加密方式 Sec字段是3啊,按照文档说的应该是ChaCha20-Poly1305才对。于是去看一下代码里的相关定义,好家伙,文档居然是错的,这文档是得多久没更新也没人看啊...

file

那看看各自有啥函数能调用吧,应该有读取数据的接口的。

RequestHeader就一个Destination函数,看着应该是解析出代理目标用的,应该是个字段的类型转换器,没啥用。

file

RequestBody就一个ReadMultiBuffer函数,不过看着似乎很像咱们要的东西。

file

看一下项目里对这个函数的用法,因为这个函数是buf.Reader来的,所以其他各种地方的用法应该也一样。

file

看着似乎是要for循环一直判断有没有读完到EOF,这画风...太流式了哈哈哈。

那就照着写吧,成功搞定http请求的解析。

file

但是着看着没啥东西啊,就一个http://127.0.0.1:5000/outGET访问,本来刚开始以为这个就是压缩包密码说的那个api address的,但是后来放hint表示需要的是c2的地址。

所以那就继续解析http返回吧。

根据刚才的经验,合理猜测ClientSession类应该会有DecodeResponseHeaderDecodeResponseBody两个方法,那就去用NewClientSession方法实例化咯。代码依旧从项目原本的测试流程里抄。

返回流量的hex的话,因为tcp会有粘包、半包的“特性”,所以根据V2Ray1发1收的流程,下一个发送的非空数据包之前收到数据都是同一个包的半包(实际的网络情况会更复杂,毕竟还有延迟、并发啥的,这题总共就俩非空发送包,所以应该上不用考虑)

第二个数据包(92号数据流):

file

package encoding_test

import (
    "bytes"
    "context"
    "encoding/hex"
    "fmt"
    "github.com/v2fly/v2ray-core/v5/common"
    "io"
    "testing"

    "github.com/v2fly/v2ray-core/v5/common/protocol"
    "github.com/v2fly/v2ray-core/v5/proxy/vmess"
    . "github.com/v2fly/v2ray-core/v5/proxy/vmess/encoding"
)

func TestCtf(t *testing.T) {
    user := &protocol.MemoryUser{
        Level: 0,
        Email: "[email protected]",
    }
    account := &vmess.Account{
        Id:      "b831381d-6324-4d53-ad4f-8cda48b30811",
        AlterId: 0,
    }
    user.Account = toAccount(account)

    userValidator := vmess.NewTimedUserValidator(protocol.DefaultIDHash)
    userValidator.Add(user)
    defer common.Close(userValidator)

    sessionHistory := NewSessionHistory()
    defer common.Close(sessionHistory)

    server := NewServerSession(userValidator, sessionHistory)

    requestHex := "4dd11f9b04f2b562b9db539d939f1d52b48b35bf592c09b21545392f73f6cef91143786464578c1c361aa72f638cd0135f25343555f509aef6c74cd2a2b86ee0a9eb3b93a81a541def4763cc54f91ba02681add1b815e8c50e028c76bde0ee8a9593db88d901066305a51a9586a9e377ee100e7d4d33fcfc0453c86b1998a95275cd9368a68820c2a6a540b6386c146ea7579cfe87b2e459856772efdcf0e4c6ab0f11d018a15561cf409cbc00491d7f4d22b7c486a76a5f2f25fbef503551a0aeb90ad9dd246a9cc5e0d0c0b751eb7b54b0abbfef198b1c4e5e755077469c318f20f3e418af03540811ab5c1ea780c886ea2c903b458a26"
    requestBuffer, _ := hex.DecodeString(requestHex)
    requestBufferReader := bytes.NewReader(requestBuffer)

    requestHeader, err := server.DecodeRequestHeader(requestBufferReader)
    if err != nil {
        t.Error(err)
    }
    fmt.Println(requestHeader)
    requestBody, err := server.DecodeRequestBody(requestHeader, requestBufferReader)
    if err != nil {
        t.Error(err)
    }
    fmt.Println(requestBody)
    for {
        requestBodyBuffer, err := requestBody.ReadMultiBuffer()
        if err != nil {
            if err == io.EOF {
                break
            } else {
                t.Error(err)
            }
        }
        fmt.Print(requestBodyBuffer.String())
    }
    fmt.Println()

    client := NewClientSession(context.TODO(), false, protocol.DefaultIDHash, 0)

    responseHex := "这里贴返回包的hex,特别长,放不下,就不贴了,4a231cf7开头63b871c8结尾的"
    responseBuffer, _ := hex.DecodeString(responseHex)
    responseBufferReader := bytes.NewReader(responseBuffer)

    responseHeader, err := client.DecodeResponseHeader(responseBufferReader)
    if err != nil {
        t.Error(err)
    }
    fmt.Println(responseHeader)
    responseBody, err := client.DecodeResponseBody(requestHeader, responseBufferReader)
    if err != nil {
        t.Error(err)
    }
    fmt.Println(responseBody)
}

成功报错了...

file

到这里进度又卡了好久,因为没想明白为什么还会有问题。

后来半夜的时候一想,文档说返回包用的keyiv是发送包里的,那咱这解析返回头的时候好像没传requestHeader啊?找了下也没地方传,毕竟正常情况下,咱作为一个客户端,keyiv就是自己生成的,没必要传啊。

所以去看下客户端存储keyiv的地方。

file

再看看怎么赋值的。

file

这怎么还跟AEAD有关系啊,文档没说啊,文档就说了非AEAD的那个情况,所以文档真的是八百年前的了...

算了不管文档,直接抄过来。然后爆红了,说是private了。

file

那就public一下这俩字段。

file

然后是keyiv的来源,和原来一样从客户端自己这边拿肯定不行,因为咱们的客户端是刚建的,不是正常情况,所以就从服务端那边拿咯。

看一下服务器的字段,确实也有存这俩东西。那就也public一下拿来用呗。

file

    if !client.IsAEAD {
        client.ResponseBodyKey = md5.Sum(server.RequestBodyKey[:])
        client.ResponseBodyIV = md5.Sum(server.RequestBodyIV[:])
    } else {
        BodyKey := sha256.Sum256(server.RequestBodyKey[:])
        copy(client.ResponseBodyKey[:], BodyKey[:16])
        BodyIV := sha256.Sum256(server.RequestBodyIV[:])
        copy(client.ResponseBodyIV[:], BodyIV[:16])
    }

但是还是报错了。

file

这回感觉没啥不对的了,只有那个AEAD是和Auth功能有关,所以关掉客户端的isAEAD看看。

file

这回报错变了,但是有点不理解是啥意思,去看看报错的代码细节吧。

file

看了下是在比对返回头的第0个字节和responseHeader,那这就是文档的响应认证 V字段啊,确实忘记这回事了,也和key/iv一样初始化一下。

    client.ResponseHeader = server.ResponseHeader

成功解析。

file

那就补上读取过程看看内容。

    for {
        responseBodyBuffer, err := responseBody.ReadMultiBuffer()
        if err != nil {
            if err == io.EOF {
                break
            } else {
                t.Error(err)
            }
        }
        fmt.Print(responseBodyBuffer.String())
    }
    fmt.Println()

file

成功读取到html内容。看了下主要内容是js里面在解一大段Base64

file

但是这里明显没输出完,应该是GoLand的异步日志问题,如果主进程结束太快但是日志又多的话,日志输出的内容有时候会吞掉最后的那部分。

file

所以根据经验,在结束之前下个断点再放行就没问题。

file

看了下js的大致逻辑,就是打开页面自动把Base64的解析结果保存成一个文件。

所以直接把返回包的html部分保存一下访问就能拿到一个word

file

那么根据之前的hintc2 api address大概率是在这个word的宏里了,因为宏病毒带cnc太常见了。

本来是打算用oletools去查看宏的,但是尝试了下发现有混淆,而且最后是写了个病毒文件,然后作为Word主题模板的dll文件让Word去调用,最后执行dll里面的cnc相关代码。

也就是说还得逆向?感觉不对啊。直接用云沙箱看看网络请求吧:样本报告-微步在线云沙箱

看了下就一个应该是查本机ip的请求,剩下应该还有三个请求因为域名解析失败没被记录。

file

推测是微步这个样本分析的是时候,c2服务器已经挂了,所以没解析成,于是又去找了个时间更早的外国沙箱分析报告(因为c2的域名是俄罗斯的,推测国外样本捕获比国内早):3a5648f7de99c4f87331c36983fc8adcd667743569a19c8dafdd5e8a33de154d | ANY.RUN - Free Malware Sandbox Online

确实有另外几个请求。

file

看一下详细的请求情况。

file

推测satursed.com是主服,主服没反应再看sameastar.ru次服,如果还不行才是微步那个ludiesibut.ru备用服。

先看了下api.ipify.org的请求,确实就是个查ip的api,不是c2

file

然后看了下sameastar.ru的请求,确实就是经典的数据统计+加密控制流一条龙的c2请求

file

file

所以题目要的api address应该就是下面这几个之一了:

http://satursed.com/8/forum.php
http://sameastar.ru/8/forum.php
http://ludiesibut.ru/8/forum.php
satursed.com/8/forum.php
sameastar.ru/8/forum.php
ludiesibut.ru/8/forum.php
satursed.com
sameastar.ru
ludiesibut.ru
/8/forum.php
8/forum.php
/forum.php
forum.php

但是试了好久都不对,md5的大小写也试了,16位32位也试了,愣是没有一个是对的。

最后不抱希望地试了下api.ipify.orgmd508229f4052dde89671134f1784bed2d6,结果真是压缩包密码...

这他妈的我查个ip就是c2服务器了?作者不认识c2可以不出题...

解压出来发现有个描述:This is a Gob File!

file

那就用degob看看序列化之前的结构体类型吧。

file

实际上就是个map[string][]byte型,写个反序列化脚本看看。

package main

import (
    "encoding/gob"
    "fmt"
    "io/ioutil"
    "math/rand"
    "os"
)

func main() {
    var a map[string][]byte
    File, _ := os.Open("flag")
    D := gob.NewDecoder(File)
    err := D.Decode(&a)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(a)
}

file

给了刚才的gob提示、一个时间、一个png文件,但是png被加密了。

根据题目hint,说是数据被随机打乱了,那么和时间放一起,可以联想到Golangmath/rand随机库有个Shuffle功能,正好适合用来打乱,然后时间可以变成时间戳当作随机数种子去使用。

而其最常见的用法如下(来自Shuffle a slice or array · YourBasic Go):

a := []int{1, 2, 3, 4, 5, 6, 7, 8}
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(a), func(i, j int) { a[i], a[j] = a[j], a[i] })

那就照着这个用法写呗。

package main

import (
    "encoding/gob"
    "fmt"
    "io/ioutil"
    "math/rand"
    "os"
)

func main() {
    var a map[string][]byte
    File, _ := os.Open("flag")
    D := gob.NewDecoder(File)
    err := D.Decode(&a)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(a)

    b := make([]byte, len(a["PNG File"]))
    copy(b, a["PNG File"])
    rand.Seed(1658213396)
    rand.Shuffle(len(b), func(i, j int) {
        b[i], b[j] = b[j], b[i]
    })
    err = ioutil.WriteFile("b.png", b, 0777)
    if err != nil {
        panic(err)
    }
}

结果发现还是没解开。那再打乱下标尝试一下。

package main

import (
    "encoding/gob"
    "fmt"
    "io/ioutil"
    "math/rand"
    "os"
)

func main() {
    var a map[string][]byte
    File, _ := os.Open("flag")
    D := gob.NewDecoder(File)
    err := D.Decode(&a)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(a)

    b := make([]byte, len(a["PNG File"]))
    copy(b, a["PNG File"])
    c := make([]int, len(b))
    for i := 0; i < len(b); i++ {
        c[i] = i
    }
    rand.Seed(1658213396)
    rand.Shuffle(len(b), func(i, j int) {
        c[i], c[j] = c[j], c[i]
    })
    for i := 0; i < len(b); i++ {
        b[c[i]] = a["PNG File"][i]
    }
    err = ioutil.WriteFile("b.png", b, 0777)
    if err != nil {
        panic(err)
    }
}

成功解开得到如下图片。

b.png

找半天发现flag在a通道,但是数据很散。

file

保存出来去掉0xff应该就行了。

file

成功得到flag。

flag{898161df-fabf-4757-82b6-ffe407c69475}

Crypto

myJWT

这题就给了个源码和公共容器。

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.*;
import java.util.Base64;
import java.util.Scanner;

import com.alibaba.fastjson.*;

class ECDSA{
    public KeyPair keyGen() throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
        keyPairGenerator.initialize(384);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        return keyPair;
    }

    public byte[] sign(byte[] str, ECPrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance("SHA384withECDSAinP1363Format");
        signature.initSign(privateKey);
        signature.update(str);
        byte[] sig = signature.sign();
        return sig;
    }

    public boolean verify(byte[] sig, byte[] str ,ECPublicKey publicKey) throws Exception {
        Signature signature = Signature.getInstance("SHA384withECDSAinP1363Format");
        signature.initVerify(publicKey);
        signature.update(str);
        return signature.verify(sig);
    }
}

public class jwt{

    public static int EXPIRE = 60;
    public static ECDSA ecdsa = new ECDSA();
    public static String generateToken(String user, ECPrivateKey ecPrivateKey) throws Exception {
        JSONObject header = new JSONObject();
        JSONObject payload = new JSONObject();
        header.put("alg", "myES");
        header.put("typ", "JWT");
        String headerB64 = Base64.getUrlEncoder().encodeToString(header.toJSONString().getBytes());
        payload.put("iss", "qwb");
        payload.put("exp", System.currentTimeMillis() + EXPIRE * 1000);
        payload.put("name", user);
        payload.put("admin", false);
        String payloadB64 = Base64.getUrlEncoder().encodeToString(payload.toJSONString().getBytes());
        String content = String.format("%s.%s", headerB64, payloadB64);
        byte[] sig = ecdsa.sign(content.getBytes(), ecPrivateKey);
        String sigB64 = Base64.getUrlEncoder().encodeToString(sig);

        return String.format("%s.%s", content, sigB64);
    }

    public static boolean verify(String token, ECPublicKey ecPublicKey) throws Exception {
        String[] parts = token.split("\\.");
        if (parts.length != 3) {
            return false;
        }else {
            String headerB64 = parts[0];
            String payloadB64 = parts[1];
            String sigB64 = parts[2];
            String content = String.format("%s.%s", headerB64, payloadB64);
            byte[] sig = Base64.getUrlDecoder().decode(sigB64);
            return ecdsa.verify(sig, content.getBytes(), ecPublicKey);
        }

    }

    public static boolean checkAdmin(String token, ECPublicKey ecPublicKey, String user) throws Exception{
        if(verify(token, ecPublicKey)) {
            String payloadB64 = token.split("\\.")[1];
            String payloadDecodeString = new String(Base64.getUrlDecoder().decode(payloadB64));
            JSONObject payload = JSON.parseObject(payloadDecodeString);

            if(!payload.getString("name").equals(user)) {
                return false;
            }
            if (payload.getLong("exp") < System.currentTimeMillis()) {
                return false;
            }
            return payload.getBoolean("admin");
        } else {
            return false;
        }   
    }

    public static String getFlag(String token, ECPublicKey ecPublicKey, String user) throws Exception{
        String err = "You are not the administrator.";
        if(checkAdmin(token, ecPublicKey, user)) {
            File file = new File("/root/task/flag.txt");
            BufferedReader br = new BufferedReader(new FileReader(file));
            String flag = br.readLine();
            br.close();
            return flag;
        } else {
            return err;
        }
    }

    public static boolean task() throws Exception {
        Scanner input = new Scanner(System.in);
        System.out.print("your name:");
        String user = input.nextLine().strip();
        System.out.print(String.format("hello %s, let's start your challenge.\n", user));
        KeyPair keyPair = ecdsa.keyGen();
        ECPrivateKey ecPrivateKey = (ECPrivateKey) keyPair.getPrivate();
        ECPublicKey ecPublicKey = (ECPublicKey) keyPair.getPublic();
        String menu = "1.generate token\n2.getflag\n>";
        Integer choice = 0;
        Integer count = 0;
        while (count <= 10) {
            count++;
            System.out.print(menu);
            choice = Integer.parseInt(input.nextLine().strip());
            if(choice == 1) {
                String token = generateToken(user, ecPrivateKey);
                System.out.println(token);
            } else if (choice == 2) {
                System.out.print("your token:");
                String token = input.nextLine().strip();
                String flag = getFlag(token, ecPublicKey, user);
                System.out.println(flag);
                input.close();
                break;
            } else {
                input.close();
                break;
            }
        }
        return true;
    }

    public static void main(String[] args) throws Exception {
        task();
    }

}

这题用的验证是ECDSA,而今年这玩意在Java上就有个大洞,具体原理可以看CVE-2022-21449

通俗地讲,用全0的签名值就可以100%通过验证。

所以就连上去先搞个格式看看。随手输个用户名,然后拿一下token。

file

得到:

eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MzIxMzI4NDM4fQ==.n9etXBkmMevswYAF1prJ3245n4o-emhq4JVUq4ocoCOfbPpPjJtVB092o0h1U5SgE8b-pKaCstkhxQYH78yXYEpgI-GqHLaIc7DKZ2cHF5IZU6wdci18TFh_YspAIwpn

第一段根据之前的代码可以知道是固定的东西,不用管,第二段解码之后是{"iss":"qwb","name":"Wankko Ree","admin":false,"exp":1659321328438},按照代码的逻辑,需要admintrue,然后过期时间戳要比现在还未来(说人话就是别过期),而传过来的是之前那个时刻的时间戳,所以只要增大就完事。比如说改成{"iss":"qwb","name":"Wankko Ree","admin":true,"exp":1759321328438},那么其Base64就是eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjp0cnVlLCJleHAiOjE3NTkzMjEzMjg0Mzh9,然后最后的签名值,全置0就完事,也就是AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

所以发送:

eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjp0cnVlLCJleHAiOjE3NTkzMjEzMjg0Mzh9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

file

就可以拿到flag。

flag{cve-2022-21449_Secur1ty_0f_c0de_1mplementation}

Reverse

GameMaster

拿到题目看一下分析,发现是C#写的,那就直接dnSpy一把梭。

file

先是在goldFunc找到了奇怪的解码过程。

file

甚至看情况有AchivePoint1就有AchivePoint2AchivePoint3,所以继续找。

file

file

看过程就是先把Program.memory每字节异或34,然后用那个key(实际上是Brainstorming!!!的ascii化结果)解AES-ECB,存到Program.m,最后反序列化一下。

那先去看看Program.memory是哪里初始化的。

file

可以看出是从gamemessage文件读取的,那就先写个脚本解一下。

from Crypto.Cipher import AES

def main():
    with open('gamemessage', 'rb') as f:
        data = bytearray(f.read())
    for i in range(len(data)):
        data[i] ^= 34
    cipher = AES.new(b"Brainstorming!!!", AES.MODE_ECB)
    with open('gamemessage.de', 'wb') as f:
        f.write(cipher.decrypt(data))

if __name__ == '__main__':
    main()

成功得到序列化的结果。

file

在文件尾发现一个dll。

file

提取出来用dnSpy再看下,大致可以判断出来,Check1函数用array生成array2,然后array2要等于first。然后ParseKey函数用array生成array4,再用array4循环异或array5,就是最后flag。

file

而关键的array来自numnum又是从之前那个AchivePoint来的,但这玩意不确定具体记录的时候是多少,因为游戏是随机的,分数全看缘分。

file

那么只能用firstCheck1函数的逻辑倒推array了。

file

就一堆位运算,那就直接z3解方程吧。

from z3 import *

x, y, z = BitVecs('x y z', 64)

solver = Solver()
first = [101, 5, 80, 213, 163, 26, 59, 38, 19, 6, 173, 189, 198, 166, 140, 183, 42, 247, 223, 24, 106, 20, 145, 37, 24, 7, 22, 191, 110, 179, 227, 5, 62, 9, 13, 17, 65, 22, 37, 5]
k = [0 for i in range(40)]
num = -1
for i in range(320):
    x = ((x >> 29 ^ x >> 28 ^ x >> 25 ^ x >> 23) & 1) | x << 1
    y = ((y >> 30 ^ y >> 27) & 1) | y << 1
    z = ((z >> 31 ^ z >> 30 ^ z >> 29 ^ z >> 28 ^ z >> 26 ^ z >> 24) & 1) | z << 1
    if i % 8 == 0:
        num += 1
    k[num] = (k[num] << 1) | ((z >> 32 & 1 & (x >> 30 & 1)) ^ (((z >> 32 & 1) ^ 1) & (y >> 31 & 1)))

for i in range(40):
    solver.add(k[i] & 0xff == first[i])

print(solver.check())
print(solver.model())

file

得到y = 868387187, x = 156324965, z = 3131229747

那模拟一下ParseKey然后搞一下最后的循环异或就能出结果。

l = [156324965, 868387187, 3131229747]
key = [0 for i in range(12)]
for i in range(3):
    for j in range(4):
        key[i*4+j] = l[i] >> j * 8 & 0xff
mask = [60, 100, 36, 86, 51, 251, 167, 108, 116, 245, 207, 223, 40, 103, 34, 62, 22, 251, 227]
for i in range(len(mask)):
    mask[i] ^= key[i % len(key)]
print(bytes(mask))

file

flag{Y0u_@re_G3meM3s7er!}

强网先锋

polydiv

题目给了俩文件和一个容器:

class Polynomial2():
    '''
    模二多项式环,定义方式有三种
    一是从高到低给出每一项的系数
        >>> Polynomial2([1,1,0,1])
        x^3 + x^2 + 1

    二是写成01字符串形式
        >>> Polynomial2('1101')
        x^3 + x^2 + 1

    三是直接给出系数为1的项的阶
        >>> Poly([3,1,4])
        x^4 + x^3 + x
        >>> Poly([]) # 加法元
        0
        >>> Poly(0) # 乘法元
        1
        >>> Poly(1,2) * Poly(2,3)
        x^5 + x^3
    '''
    def __init__(self,ll):

        if type(ll) ==  str:
            ll = list(map(int,ll))

        self.param = ll[::-1]
        self.ones = [i for i in range(len(self.param)) if self.param[i] == 1] # 系数为1的项的阶数列表
        self.Latex = self.latex()
        self.b = ''.join([str(i) for i in ll]) # 01串形式打印系数

        self.order = 0 # 最高阶
        try:self.order = max(self.ones)
        except:pass

    def format(self,reverse = True):
        '''
            格式化打印字符串
            默认高位在左
            reverse = False时,低位在左
            但是注意定义多项式时只能高位在右
        '''
        r = ''
        if len(self.ones) == 0:
            return '0'
        if reverse:
            return ((' + '.join(f'x^{i}' for i in self.ones[::-1])+' ').replace('x^0','1').replace('x^1 ','x ')).strip()
        return ((' + '.join(f'x^{i}' for i in self.ones)+' ').replace('x^0','1').replace('x^1 ','x ')).strip()

    def __call__(self,x):
        '''
            懒得写了,用不到
        '''
        print(f'call({x})')

    def __add__(self,other):
        '''
            多项式加法
        '''
        a,b = self.param[::-1],other.param[::-1]
        if len(a) < len(b):a,b = b,a
        for i in range(len(a)):
            try:a[-1-i] = (b[-1-i] + a[-1-i]) % 2
            except:break
        return Polynomial2(a)

    def __mul__(self,other):
        '''
            多项式乘法
        '''

        a,b = self.param[::-1],other.param[::-1]
        r = [0 for i in range(len(a) + len(b) - 1)]
        for i in range(len(b)):
            if b[-i-1] == 1:
                if i != 0:sa = a+[0]*i
                else:sa = a
                sa = [0] * (len(r)-len(sa)) + sa
                #r += np.array(sa)
                #r %= 2
                r = [(r[t] + sa[t])%2 for t in range(len(r))]
        return Polynomial2(r)

    def __sub__(self,oo):
        # 模二多项式环,加减相同
        return self + oo

    def __repr__(self) -> str:
        return self.format()

    def __str__(self) -> str:
        return self.format()

    def __pow__(self,a):
        # 没有大数阶乘的需求,就没写快速幂
        t = Polynomial2([1])
        for i in range(a):
            t *= self
        return t

    def latex(self,reverse=True):
        '''
            Latex格式打印...其实就是给两位及以上的数字加个括号{}
        '''
        def latex_pow(x):
            if len(str(x)) <= 1:
                return str(x)
            return '{'+str(x)+'}'

        r = ''
        if len(self.ones) == 0:
            return '0'
        if reverse:
            return (' + '.join(f'x^{latex_pow(i)}' for i in self.ones[::-1])+' ').replace('x^0','1').replace(' x^1 ',' x ').strip()
        return (' + '.join(f'x^{latex_pow(i)}' for i in self.ones)+' ').replace('x^0','1').replace(' x^1 ',' x ').strip()

    def __eq__(self,other):
        return self.ones == other.ones

    def __lt__(self,other):
        return max(self.ones) < max(other.ones)

    def __le__(self,other):
        return max(self.ones) <= max(other.ones)

def Poly(*args):
    '''
        另一种定义方式
        Poly([3,1,4]) 或 Poly(3,1,4)
    '''
    if len(args) == 1 and type(args[0]) in [list,tuple]:
        args = args[0]

    if len(args) == 0:
        return Polynomial2('0')

    ll = [0 for i in range(max(args)+1)]
    for i in args:
        ll[i] = 1
    return Polynomial2(ll[::-1])

PP = Polynomial2
P = Poly
# 简化名称,按长度区分 P 和 PP
if __name__ == '__main__':
    p = Polynomial2('10011')
    p3 = Polynomial2('11111')
    Q = p*p3
import socketserver
import os, sys, signal
import string, random
from hashlib import sha256

from secret import flag
from poly2 import *

pad = lambda s:s + bytes([(len(s)-1)%16+1]*((len(s)-1)%16+1))
testCases = 40

class Task(socketserver.BaseRequestHandler):
    def _recvall(self):
        BUFF_SIZE = 2048
        data = b''
        while True:
            part = self.request.recv(BUFF_SIZE)
            data += part
            if len(part) < BUFF_SIZE:
                break
        return data.strip()

    def send(self, msg, newline=True):
        try:
            if newline:
                msg += b'\n'
            self.request.sendall(msg)
        except:
            pass

    def recv(self, prompt=b'> '):
        self.send(prompt, newline=False)
        return self._recvall()

    def close(self):
        self.send(b"Bye~")
        self.request.close()

    def proof_of_work(self):
        random.seed(os.urandom(8))
        proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
        _hexdigest = sha256(proof.encode()).hexdigest()
        self.send(f"sha256(XXXX+{proof[4:]}) == {_hexdigest}".encode())
        x = self.recv(prompt=b'Give me XXXX: ')
        if len(x) != 4 or sha256(x+proof[4:].encode()).hexdigest() != _hexdigest:
            return False
        return True

    def guess(self):
        from Crypto.Util.number import getPrime
        a,b,c = [getPrime(i) for i in [256,256,128]]
        pa,pb,pc = [PP(bin(i)[2:]) for i in [a,b,c]]
        r = pa*pb+pc
        self.send(b'r(x) = '+str(r).encode())
        self.send(b'a(x) = '+str(pa).encode())
        self.send(b'c(x) = '+str(pc).encode())
        self.send(b'Please give me the b(x) which satisfy a(x)*b(x)+c(x)=r(x)')
        #self.send(b'b(x) = '+str(pb).encode())

        return self.recv(prompt=b'> b(x) = ').decode() == str(pb)

    def handle(self):
        #signal.alarm(1200)

        if not self.proof_of_work():
            return

        for turn in range(testCases):
            if not self.guess():
                self.send(b"What a pity, work harder.")
                return
            self.send(b"Success!")
        else:
            self.send(b'Congratulations, this is you reward.')
            self.send(flag)

class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

#class ForkedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
class ForkedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

if __name__ == "__main__":

    HOST, PORT = '0.0.0.0', 10000
    server = ForkedServer((HOST, PORT), Task)
    server.allow_reuse_address = True
    server.serve_forever()

题目的意思其实挺简单的,就是过完POW之后来个40轮的模二多项式环求解,具体就是给了a * b + c == r,已知acr,求b,那不是反向运算就完事嘛:b = (r - c) / a嘛。

结果看了下题目给的Polynomial2没实现div运算...那就上网找吧(因为不太熟悉这玩意)。

结果找着照着就找到了出题人这个类的代码出处:模二多项式环 及 BCH码 的纯python实现和一些问题(不过这个站是盗文站,原文章在CSDN上,但是被删了)

所以把里面的除法实现抄到题目的类里就完事:

    def div(self,other):
        r,b = self.param[::-1],other.param[::-1]
        if len(r) < len(b):
            return Polynomial2([0]),self

        q=[0] * (len(r) - len(b) + 1)
        for i in range(len(q)):
            if len(r)>=len(b):
                index = len(r) - len(b) + 1  # 确定所得商是商式的第index位
                q[-index] = int(r[0] / b[0])
                # 更新被除多项式
                b_=b.copy()
                b_.extend([0] * (len(r) - len(b)))
                b_ = [t*q[i] for t in b_] 
                r = [(r[t] - b_[t])%2 for t in range(len(r))]
                for j in range(len(r)):     #除去列表最左端无意义的0
                    if r[0]==0:
                        r.remove(0)
                    else:
                        break
            else:
                break

        return Polynomial2(q),Polynomial2(r)

    def __floordiv__(self,other): # 只重载了整除,即//
        return self.div(other)[0]

然后写个脚本和服务器交互,其中关于表达式解析,我的思路是用题目给的Poly函数去实例化,传入[3,1,4]这种环中1的下标就行,所以就先把结尾的1换成0(因为下标是0),然后去一下两头的换行符啥的,再按 + 去分割一下,再把里面的x^都给去掉,那就只剩下文本格式的数字了,但是也有特殊情况比如说x会变成空字符,所以额外判断一下长度,有字符就转int,没有下标就肯定是1。然后传给Poly函数就可以开始反向运算了。


def pow(end, sha256_result) -> bytes:
    import string
    from hashlib import sha256

    base = string.ascii_letters+string.digits
    for i in base:
        for j in base:
            for k in base:
                for l in base:
                    pow_key = f"{i}{j}{k}{l}{end}".encode()
                    if sha256(pow_key).hexdigest() == sha256_result:
                        print(pow_key)
                        return pow_key[:4]

def main():
    from pwn import connect, context
    context.log_level = 'debug'
    conn = connect("59.110.212.61", 24906)
    conn.recvuntil(b"sha256(XXXX+")
    end = conn.recvn(16).decode()
    conn.recvuntil(b") == ")
    sha256_result = conn.recvn(64).decode()
    conn.sendline(pow(end, sha256_result))
    for _ in range(40):
        conn.recvuntil(b"r(x) = ")
        r = conn.recvline().decode().replace('1\n', '0\n').strip().split(' + ')
        r = Poly([int(i) if len(i) > 0 else 1 for i in [i.strip("x^") for i in r]])
        conn.recvuntil(b"a(x) = ")
        a = conn.recvline().decode().replace('1\n', '0\n').strip().split(' + ')
        a = Poly([int(i) if len(i) > 0 else 1 for i in [i.strip("x^") for i in a]])
        conn.recvuntil(b"c(x) = ")
        c = conn.recvline().decode().replace('1\n', '0\n').strip().split(' + ')
        c = Poly([int(i) if len(i) > 0 else 1 for i in [i.strip("x^") for i in c]])
        conn.recvuntil(b"> b(x) = ")
        b = (r - c) // a
        conn.sendline(str(b).encode())
    print(conn.recvline())
    print(conn.recvline())
    print(conn.recvline())
    print()

if __name__ == '__main__':
    main()

file

成功得到flag。

flag{768782a0-e637-4982-9415-4b8f005e466b}

ASR

题目给的脚本:

from Crypto.Util.number import getPrime
from secret import falg
pad = lambda s:s + bytes([(len(s)-1)%16+1]*((len(s)-1)%16+1))

n = getPrime(128)**2 * getPrime(128)**2 * getPrime(128)**2 * getPrime(128)**2
e = 3

flag = pad(flag)
print(flag)
assert(len(flag) >= 48)
m = int.from_bytes(flag,'big')
c = pow(m,e,n)

print(f'n = {n}')
print(f'e = {e}')
print(f'c = {c}')

'''
n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
e = 3
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149
'''

先是n == (p * q * r * s)^2,那就先开方咯。

from gmpy2 import *

n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
e = 3
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149

nn = iroot(n, 2)[0]
assert nn**2 == n
print(nn)

得到2872432989693854281918578458293603200587306199407874717707522587993136874097838265650829958344702997782980206004276973399784460125581362617464018665640001

然后看着pqrs也都不大,就128位,感觉可以yafu直接爆。

file

跑了十几分钟也确实爆出来了。那按照公式phi = (p^(k-1)) * (p-1) * ...直接求。

结果发现有几个因子的p-1居然和e不互素...

p = 260594583349478633632570848336184053653
q = 218566259296037866647273372633238739089
r = 223213222467584072959434495118689164399
s = 225933944608558304529179430753170813347
assert p*q*r*s == nn

print(gcd((p*(p-1)), e))  # 1
print(gcd((q*(q-1)), e))  # 3
print(gcd((r*(r-1)), e))  # 1
print(gcd((s*(s-1)), e))  # 3

这就给我整不会了啊,于是进度就卡这了。第二天随便乱试的时候,发现用那俩正常的因子重新构造n == (p * r)^2,可以解出flag,具体原理我也不知道...

from Crypto.Util.number import long_to_bytes

phi = (p*(p-1))*(r*(r-1))
d = invert(e, phi)
m = pow(c, d, p**2*r**2)
print(long_to_bytes(m))

file

flag{Fear_can_hold_you_prisoner_Hope_can_set_you_free}


The End