队伍成绩:
1029
pt/13
kill/50
名/1396
有解/1938
登录/2358
报名
个人成绩:522
pt/5
kill/73
名/2234
有解/7622
登录/10147
报名
Misc
谍影重重
拿到题目,一个带密码的压缩包,一个配置文件,一个流量包。
压缩包密码说是要拿到api address
,大概在流量包里。
配置文件里面看起来只给了一部分必要参数?
根据关键词"config.json" "settings" "clients" "id"
查询可以发现,Google首屏结果与V2Ray
高度相关,所以推测流量包是VMess
协议,后续放出来的hint也证实了这一点。
看下流量包,只有127.0.0.1:37886 - 127.0.0.1:10087
的往返流量,发起方是127.0.0.1:37886
,所以V2Ray
服务器是127.0.0.1:10087
那就想办法解析流量呗,先是去看了一下V2Fly
组织(因为原组织Project V
的主导者失踪了)对VMess
协议的一些数据包细节:VMess 协议 | V2Fly.org。
尝试写Python
脚本去解析流量,在第一天中午的时候马上搞定对开头认证信息
的解析(因为真的很标准,没啥难度),爆破出了流量的时间戳是1615528982
(2021-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
左右才行。
那么想办法hook
掉或者硬编码掉取系统当前时间的返回值吧,上面有个vmess.NewTimedUserValidator
的调用,看名字应该就是基于用户id+当前时间的验证器,进去看看怎么取当前时间的,发现用的是语言层的函数time.Now()
。
跟进去看看实现,先下个断点调一下看看值的格式。
看起来应该是改第一个秒钟sec
就行,在第二行加一句sec = 1615528982
试试,成功pass
掉刚才的测试。
不过RequestHeader
和RequestBody
似乎都没有文本化的实现,那就去下个断点看看值吧。
看着似乎没有咱们想要的数据啊,就一个代理目标是127.0.0.1:5000
,走的TCP
,这个之前用Python
去解析的时候就知道了。然后用加密协议怎么变成AES-128-GCM
了,之前Python
解析出来加密方式 Sec
字段是3
啊,按照文档说的应该是ChaCha20-Poly1305
才对。于是去看一下代码里的相关定义,好家伙,文档居然是错的,这文档是得多久没更新也没人看啊...
那看看各自有啥函数能调用吧,应该有读取数据的接口的。
RequestHeader
就一个Destination
函数,看着应该是解析出代理目标用的,应该是个字段的类型转换器,没啥用。
RequestBody
就一个ReadMultiBuffer
函数,不过看着似乎很像咱们要的东西。
看一下项目里对这个函数的用法,因为这个函数是buf.Reader
来的,所以其他各种地方的用法应该也一样。
看着似乎是要for
循环一直判断有没有读完到EOF
,这画风...太流式了哈哈哈。
那就照着写吧,成功搞定http请求的解析。
但是着看着没啥东西啊,就一个http://127.0.0.1:5000/out
的GET
访问,本来刚开始以为这个就是压缩包密码说的那个api address
的,但是后来放hint表示需要的是c2
的地址。
所以那就继续解析http返回吧。
根据刚才的经验,合理猜测ClientSession
类应该会有DecodeResponseHeader
和DecodeResponseBody
两个方法,那就去用NewClientSession
方法实例化咯。代码依旧从项目原本的测试流程里抄。
返回流量的hex
的话,因为tcp
会有粘包、半包的“特性”,所以根据V2Ray
1发1收的流程,下一个发送的非空数据包之前收到数据都是同一个包的半包(实际的网络情况会更复杂,毕竟还有延迟、并发啥的,这题总共就俩非空发送包,所以应该上不用考虑)
第二个数据包(92号数据流):
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)
}
成功报错了...
到这里进度又卡了好久,因为没想明白为什么还会有问题。
后来半夜的时候一想,文档说返回包用的key
和iv
是发送包里的,那咱这解析返回头的时候好像没传requestHeader
啊?找了下也没地方传,毕竟正常情况下,咱作为一个客户端,key
和iv
就是自己生成的,没必要传啊。
所以去看下客户端存储key
和iv
的地方。
再看看怎么赋值的。
这怎么还跟AEAD
有关系啊,文档没说啊,文档就说了非AEAD
的那个情况,所以文档真的是八百年前的了...
算了不管文档,直接抄过来。然后爆红了,说是private
了。
那就public
一下这俩字段。
然后是key
和iv
的来源,和原来一样从客户端自己这边拿肯定不行,因为咱们的客户端是刚建的,不是正常情况,所以就从服务端那边拿咯。
看一下服务器的字段,确实也有存这俩东西。那就也public
一下拿来用呗。
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])
}
但是还是报错了。
这回感觉没啥不对的了,只有那个AEAD
是和Auth
功能有关,所以关掉客户端的isAEAD
看看。
这回报错变了,但是有点不理解是啥意思,去看看报错的代码细节吧。
看了下是在比对返回头的第0
个字节和responseHeader
,那这就是文档的响应认证 V
字段啊,确实忘记这回事了,也和key
/iv
一样初始化一下。
client.ResponseHeader = server.ResponseHeader
成功解析。
那就补上读取过程看看内容。
for {
responseBodyBuffer, err := responseBody.ReadMultiBuffer()
if err != nil {
if err == io.EOF {
break
} else {
t.Error(err)
}
}
fmt.Print(responseBodyBuffer.String())
}
fmt.Println()
成功读取到html
内容。看了下主要内容是js
里面在解一大段Base64
。
但是这里明显没输出完,应该是GoLand
的异步日志问题,如果主进程结束太快但是日志又多的话,日志输出的内容有时候会吞掉最后的那部分。
所以根据经验,在结束之前下个断点再放行就没问题。
看了下js的大致逻辑,就是打开页面自动把Base64
的解析结果保存成一个文件。
所以直接把返回包的html
部分保存一下访问就能拿到一个word
。
那么根据之前的hint
,c2 api address
大概率是在这个word
的宏里了,因为宏病毒带cnc
太常见了。
本来是打算用oletools
去查看宏的,但是尝试了下发现有混淆,而且最后是写了个病毒文件,然后作为Word
主题模板的dll
文件让Word
去调用,最后执行dll
里面的cnc
相关代码。
也就是说还得逆向?感觉不对啊。直接用云沙箱看看网络请求吧:样本报告-微步在线云沙箱
看了下就一个应该是查本机ip的请求,剩下应该还有三个请求因为域名解析失败没被记录。
推测是微步这个样本分析的是时候,c2
服务器已经挂了,所以没解析成,于是又去找了个时间更早的外国沙箱分析报告(因为c2
的域名是俄罗斯的,推测国外样本捕获比国内早):3a5648f7de99c4f87331c36983fc8adcd667743569a19c8dafdd5e8a33de154d | ANY.RUN - Free Malware Sandbox Online
确实有另外几个请求。
看一下详细的请求情况。
推测satursed.com
是主服,主服没反应再看sameastar.ru
次服,如果还不行才是微步那个ludiesibut.ru
备用服。
先看了下api.ipify.org
的请求,确实就是个查ip的api,不是c2
。
然后看了下sameastar.ru
的请求,确实就是经典的数据统计+加密控制流一条龙的c2
请求
所以题目要的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.org
的md5
值08229f4052dde89671134f1784bed2d6
,结果真是压缩包密码...
这他妈的我查个ip就是c2
服务器了?作者不认识c2
可以不出题...
解压出来发现有个描述:This is a Gob File!
。
那就用degob
看看序列化之前的结构体类型吧。
实际上就是个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)
}
给了刚才的gob
提示、一个时间、一个png文件,但是png被加密了。
根据题目hint
,说是数据被随机打乱了,那么和时间放一起,可以联想到Golang
的math/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)
}
}
成功解开得到如下图片。
找半天发现flag在a
通道,但是数据很散。
保存出来去掉0xff
应该就行了。
成功得到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。
得到:
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjpmYWxzZSwiZXhwIjoxNjU5MzIxMzI4NDM4fQ==.n9etXBkmMevswYAF1prJ3245n4o-emhq4JVUq4ocoCOfbPpPjJtVB092o0h1U5SgE8b-pKaCstkhxQYH78yXYEpgI-GqHLaIc7DKZ2cHF5IZU6wdci18TFh_YspAIwpn
第一段根据之前的代码可以知道是固定的东西,不用管,第二段解码之后是{"iss":"qwb","name":"Wankko Ree","admin":false,"exp":1659321328438}
,按照代码的逻辑,需要admin
为true
,然后过期时间戳要比现在还未来(说人话就是别过期),而传过来的是之前那个时刻的时间戳,所以只要增大就完事。比如说改成{"iss":"qwb","name":"Wankko Ree","admin":true,"exp":1759321328438}
,那么其Base64
就是eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjp0cnVlLCJleHAiOjE3NTkzMjEzMjg0Mzh9
,然后最后的签名值,全置0就完事,也就是AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
。
所以发送:
eyJ0eXAiOiJKV1QiLCJhbGciOiJteUVTIn0=.eyJpc3MiOiJxd2IiLCJuYW1lIjoiV2Fua2tvIFJlZSIsImFkbWluIjp0cnVlLCJleHAiOjE3NTkzMjEzMjg0Mzh9.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
就可以拿到flag。
flag{cve-2022-21449_Secur1ty_0f_c0de_1mplementation}
Reverse
GameMaster
拿到题目看一下分析,发现是C#
写的,那就直接dnSpy
一把梭。
先是在goldFunc
找到了奇怪的解码过程。
甚至看情况有AchivePoint1
就有AchivePoint2
、AchivePoint3
,所以继续找。
看过程就是先把Program.memory
每字节异或34
,然后用那个key
(实际上是Brainstorming!!!
的ascii化结果)解AES-ECB
,存到Program.m
,最后反序列化一下。
那先去看看Program.memory
是哪里初始化的。
可以看出是从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()
成功得到序列化的结果。
在文件尾发现一个dll。
提取出来用dnSpy
再看下,大致可以判断出来,Check1
函数用array
生成array2
,然后array2
要等于first
。然后ParseKey
函数用array
生成array4
,再用array4
循环异或array5
,就是最后flag。
而关键的array
来自num
,num
又是从之前那个AchivePoint
来的,但这玩意不确定具体记录的时候是多少,因为游戏是随机的,分数全看缘分。
那么只能用first
和Check1
函数的逻辑倒推array
了。
就一堆位运算,那就直接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())
得到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))
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
,已知a
、c
、r
,求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()
成功得到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
。
然后看着p
、q
、r
、s
也都不大,就128位,感觉可以yafu
直接爆。
跑了十几分钟也确实爆出来了。那按照公式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))
flag{Fear_can_hold_you_prisoner_Hope_can_set_you_free}
Comments NOTHING