0x00 前置

来点GoCipher:https://pkg.go.dev/crypto/cipher

再来点Go的XXTEA:https://github.com/xxtea/xxtea-go/blob/master/xxtea/xxtea.go

0x01 解

go语言和rust一样,静态是真看不明白

所以只能调试

然而这题里面有反调

根据调试时出现的字符串可以去搜索然后找到反调的地方

将jz改为jmp就可以过掉了

之后调试需要使用F4(运行至光标处),否则会跳到很多奇怪的地方

由于对go的反编译并不给力,因此大多时候还是以汇编为主

首先来到看上去像是真正执行的地方,可以看到有一个读文件的操作

在文件路径处下断点,往下跟

可以遇到第一个看起来像是加密的函数

进入这个main_tlFyZv,反编译后往下翻翻可以看到一个异或

在汇编看,可以发现基本没有别的操作了,合理怀疑这个函数就是一个异或

查看一下值,发现ptr内是一个表,这个表应该就是异或用的表了

先将其dump下来,然后写个脚本加密一下原flag,看看是否异或成功

1
2
3
4
5
6
7
8
xortable = b"D7BJLsOk9@f&1dWIn53IDlJqUS6$^WhkAk2kk*2GaqmLwiLX^bGGE$&dmqR^g5bL3lCA5^HGK$9qo5T@Bwom9vEXya0HAV3LrWW"
xortable = bytearray(xortable)
plaintxt = b"flag{xxxxxxxxxx}"
plaintxt = bytearray(plaintxt)
for i in range(len(plaintxt)):
plaintxt[i] ^= xortable[i%len(xortable)]

open("flag","wb").write(plaintxt)

将异或后的flag作为输入,可以发现异或成功了

那么就继续往下翻翻吧

搜一下这个bytesToUint32s,不过没什么结果

继续看

下面有个main_zQyveE

进去可以发现很像是XXTEA

搜一下go语言版本XXTEA的可以得到这个:https://github.com/xxtea/xxtea-go/blob/master/xxtea/xxtea.go

对比可发现基本一致,但其中有三处不同:

1.MX被改

1
2
3
4
//原:
((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (k[p&3^e] ^ z))
//改:
((z>>5 ^ y<<2) + (y>>3 ^ z<<4) ^ (sum ^ y)) + (k[p&3^e] ^ z)

2.执行toBytes时不带长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这个后面可以试出来
//原
func Decrypt(data []byte, key []byte) []byte {
if data == nil || len(data) == 0 {
return data
}
return toBytes(decrypt(toUint32s(data, false), toUint32s(key, false)), true)
}
//改
func Decrypt(data []byte, key []byte) []byte {
if data == nil || len(data) == 0 {
return data
}
return toBytes(decrypt(toUint32s(data, false), toUint32s(key, false)), false)
}

3.delta被改

1
2
3
4
//原
0x9E3779B9
//改
0x7FAB4CAD

然后看魔改XXTEA后的操作

可以在call main_Q05qm6时将XXTEA的结果(存在RAX中)全部改为11,方便后面查看

后面是个sm4,但是有个rand,也就是为什么每次运行结果都不一样的原因

不过在汇编中可以看到更多的东西,有个NewCTR

可以去查看一下gocipher的使用指南看看如何进行调用的:https://pkg.go.dev/crypto/cipher

这里执行到sm4_NewCipher就能在RAX处得到key了

之后执行到rand_read的地方,可以在上面找到存储随机数key的地方,而旁边的rax就是存放生成的随机数的地方

记录rax的地址,在运行到cipher_NewCTR的时候就能在rcx中找到生成的随机数的值

根据go的crypto/rand,他会向指定大小的数组中填满随机数,可以用于验证(本题中指定的数组大小似乎为16字节)

这里将其全改为22,方便后续观察(相当于随机数生成了全22)

之后继续运行到call rdx处,发现它的参数是上一步XXTEA的结果

进去可以发现是一个XORKeyStream

从这里可以知道,这里的操作是使用一串字符作为key,一个随机数作为iv,使用XORKeyStream模式加密

关于go的对称加密可以看这个:https://zhuanlan.zhihu.com/p/58144027

之后继续运行,用sm4同样的方式可以得到aes的密钥(rax中)

然后运行至下面的CBC,可以得到iv

之后继续运行至rdx,可以发现要被加密的字符串中有刚刚生成的随机数

进去这个rsi,可以看到下面有一大串共0x20字节的内容(C2 8E E7 15 F4 2A B5 56 57 …)

这些其实就是上面sm4的结果,这里可以看出来sm4加密后将随机数生成的iv附加到了sm4加密结果的前面

继续执行至retn,可以发现rax的内容就是加密的结果了

将其前面部分改为33

然后出函数,发现又有个函数,进去可以看到是个base32encode

继续执行就输出了,因此整个加密流程就是

1
XOR->魔改XXTEA->sm4(key,iv=rand)(使用XORKeyStream模式加密)->将sm4的iv与sm4加密结果进行拼接->aes(key,iv)(iv和key都是固定的,这里使用CBC模式加密)->base32encode

据此可以写脚本,这里抄一下大哥代码,顺便学学go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package main

import (
"crypto/aes"
"crypto/cipher"
"encoding/base32"
"fmt"

"github.com/tjfoc/gmsm/sm4"
)
const delta = 0x7FAB4CAD//此处有改

func toBytes(v []uint32, includeLength bool) []byte {
length := uint32(len(v))
n := length << 2
if includeLength {
m := v[length-1]
n -= 4
if (m < n-3) || (m > n) {
return nil
}
n = m
}
bytes := make([]byte, n)
for i := uint32(0); i < n; i++ {
bytes[i] = byte(v[i>>2] >> ((i & 3) << 3))
}
return bytes
}

func toUint32s(bytes []byte, includeLength bool) (v []uint32) {
length := uint32(len(bytes))
n := length >> 2
if length&3 != 0 {
n++
}
if includeLength {
v = make([]uint32, n+1)
v[n] = length
} else {
v = make([]uint32, n)
}
for i := uint32(0); i < length; i++ {
v[i>>2] |= uint32(bytes[i]) << ((i & 3) << 3)
}
return v
}

func mx(sum uint32, y uint32, z uint32, p uint32, e uint32, k []uint32) uint32 {
//此处有改
return ((((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ (sum ^ y)) + (k[p&3^e] ^ z))
}

func fixk(k []uint32) []uint32 {
if len(k) < 4 {
key := make([]uint32, 4)
copy(key, k)
return key
}
return k
}

func encrypt(v []uint32, k []uint32) []uint32 {
length := uint32(len(v))
n := length - 1
k = fixk(k)
var y, z, sum, e, p, q uint32
z = v[n]
sum = 0
for q = 6 + 52/length; q > 0; q-- {
sum += delta
e = sum >> 2 & 3
for p = 0; p < n; p++ {
y = v[p+1]
v[p] += mx(sum, y, z, p, e, k)
z = v[p]
}
y = v[0]
v[n] += mx(sum, y, z, p, e, k)
z = v[n]
}
return v
}

func decrypt(v []uint32, k []uint32) []uint32 {
length := uint32(len(v))
n := length - 1
k = fixk(k)
var y, z, sum, e, p, q uint32
y = v[0]
q = 6 + 52/length
for sum = q * delta; sum != 0; sum -= delta {
e = sum >> 2 & 3
for p = n; p > 0; p-- {
z = v[p-1]
v[p] -= mx(sum, y, z, p, e, k)
y = v[p]
}
z = v[n]
v[0] -= mx(sum, y, z, p, e, k)
y = v[0]
}
return v
}

func XXTEAEncrypt(data []byte, key []byte) []byte {
if data == nil || len(data) == 0 {
return data
}
return toBytes(encrypt(toUint32s(data, false), toUint32s(key, false)), false)
}

func XXTEADecrypt(data []byte, key []byte) []byte {
if data == nil || len(data) == 0 {
return data
}
return toBytes(decrypt(toUint32s(data, false), toUint32s(key, false)), false)//此处有改
}

func XOR(key []byte,plaintext []byte) []byte{
ciphertext := make([]byte,len(plaintext))
for i:=0;i<len(plaintext);i++{
ciphertext[i] = plaintext[i] ^ key[i%len(key)]
}
return ciphertext
}

func dec(b32text string){
// 1.对base32解码
// 创建与输入有相同长度的数组
dst := make([]byte, base32.StdEncoding.DecodedLen(len(b32text)))
// base32.StdEncoding.Decode返回解码后的长度和一个err,解码后的字符存在dst中
n,err:=base32.StdEncoding.Decode(dst,[]byte(b32text))
if err != nil{
fmt.Println("decode error:",err)
return
}
// 调整dst的长度
dst = dst[:n]
aes_iv := "dPGWgcLpqmxw3uOX" // 16字节长
aes_key := "dPGWgcLpqmxw3uOXhKpKV009Cql@@XE6" // 32字节长
// 创建aes算法的接口,参数是密匙key,返回Block接口与err,该接口是指定密匙key的加/解密器,对独立数据块提供了加解密能力
cip,_ := aes.NewCipher([]byte(aes_key))
// 创建CBC分组模式下的接口,参数1为Block接口的实例对象,参数2是cbc所需的初始向量(长度等于明文分组长度,即block.BlockSize(),
// 并且加解密iv必须一致),返回BlockMode接口,BlockMode接口代表一个工作在块模式(如CBC、ECB等)的加/解密器
dec := cipher.NewCBCDecrypter(cip,[]byte(aes_iv))
// 使用CryptoBlocks进行密码块的操作,解密dst放入dst中
dec.CryptBlocks(dst,dst)

// 解密的AES的前十六位为sm4的iv,后面为内容
iv := dst[0:16]
dst = dst[16:]
sm4_key := "pg5g#k6Qo3L&1EzT"
// 创建sm4算法的接口,参数是密匙key,返回Block接口与err,该接口是指定密匙key的加/解密器,对独立数据块提供了加解密能力
sm4_cip, _ := sm4.NewCipher([]byte(sm4_key))
// 创建CTR分组模式下的接口,参数1为Block接口的实例对象,参数2是cbc所需的初始向量(长度等于明文分组长度,即block.BlockSize()
// 并且加解密iv必须一致),返回Stream接口,Stream接口代表一个流模式的加/解密器
ctr := cipher.NewCTR(sm4_cip,iv)
// XORKeyStream(dst, src []byte)方法将src的数据与密钥生成的伪随机流取XOR并写入dst。dst和src可指向同一内存地址;但如果指向不同则其底层内存不可重叠。
// 使用XORKeyStream进行密码块的操作,解密dst放入dst中
ctr.XORKeyStream(dst,dst)


key := []byte("Bs^8*wZ4lu8oR&@k")
// flag长度未知,从十六位开始爆破
for i := 16;i<len(dst);i++{
ddd2 := XXTEADecrypt(dst[:i],key)
key2 := "D7BJLsOk9@f&1dWIn53IDlJqUS6$^WhkAk2kk*2GaqmLwiLX^bGGE$&dmqR^g5bL3lCA5^HGK$9qo5T@Bwom9vEXya0HAV3LrWW"
dst2 := XOR([]byte(key2),ddd2)
A:= string(dst2)
fmt.Println(A)
}
}
func main() {
dec("nc result")
}

由于题目是有远程环境的(用这个来生成动态flag)

因此赛后复现只能自己生成一个flag文件,然后用给的程序加密然后验证脚本是否正确了(