队伍成绩:
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: "test@v2fly.org", } 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: "test@v2fly.org", } 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