目录
0x01 :一起来拼图
0x02:decryp
出题人现身说法,带来官方解读
团队昵称:混吃等死
一血用时:55分钟
出题人
吴琼
滴滴出行专家工程师
出题人视角
原图被随机打散为6400块,选手通过使用图像识别算法,与原图进行比对,确定拼图碎片的位置,将拼图碎片拼回后,即可看到FLAG。另外一血混吃等死队思路清奇稳中带皮 ,所以再多放出一个猛追湾无人机侦查大队的WP,供同学们参考
-----选手Write Up-----
为了抢一血,和队友手撸出来的。在6400张图片里找含有flag的图片。一人找到17张图同名替换正好得到20张含有flag的图。
用ps拼:
团队昵称:猛追湾无人机侦查大队
对原图片,从左到右从上到下,用 dhash 算法进行相似对匹配,遍历文件夹里面的图片,相似度最高的就拼上去
对拼图排序,大概半个小时可以计算出来
import cv2
import numpy as np
import os
from match import d_score
import pickle
path = 'picture'
dirs = os.listdir(path)
sort_imgs=[]
original_img_len=2160
imgs=[]
match_list=[]
original_img=cv2.imread("demo.jpg")
print(original_img.shape)
for file in dirs:
img=cv2.imread('picture/'+file)
imgs.append(img)
print("ok")
leng = 27
high = 51
current_len=0
current_high=0
imgg=img = np.zeros((27,51,3), np.uint8)
for kkk in range(len(imgs)):
match_list.clear()
for i in range(len(imgs)):
img = imgs[i]
img2=original_img[current_len:current_len+leng,current_high:current_high+high]
rate=d_score(img,img2)
match_list.append(rate)
current_len = current_len + leng
if (current_len == original_img_len):
current_len = 0
current_high = current_high + 51
max=0
max_id=0
for i in range(len(match_list)):
if (match_list[i]>=max):
max=match_list[i]
max_id=i
print(dirs[max_id],max,len(match_list))
sort_imgs.append(dirs[max_id])
del imgs[max_id]
del dirs[max_id]
print(sort_imgs)
obj=open("list","wb")
pickle.dump(sort_imgs,obj)
import cv2
import numpy as np
from PIL import Image
from io import BytesIO
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
def aHash(img):
# 均值哈希算法
# 缩放为8*8
img = cv2.resize(img, (8, 8))
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# s为像素和初值为0,hash_str为hash值初值为''
s = 0
hash_str = ''
# 遍历累加求像素和
for i in range(8):
for j in range(8):
s = s + gray[i, j]
# 求平均灰度
avg = s / 64
# 灰度大于平均值为1相反为0生成图片的hash值
for i in range(8):
for j in range(8):
if gray[i, j] > avg:
hash_str = hash_str + '1'
else:
hash_str = hash_str + '0'
return hash_str
def dHash(img):
# 差值哈希算法
# 缩放8*8
img = cv2.resize(img, (9, 8))
# 转换灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hash_str = ''
# 每行前一个像素大于后一个像素为1,相反为0,生成哈希
for i in range(8):
for j in range(8):
if gray[i, j] > gray[i, j + 1]:
hash_str = hash_str + '1'
else:
hash_str = hash_str + '0'
return hash_str
def pHash(img):
# 感知哈希算法
# 缩放32*32
img = cv2.resize(img, (32, 32)) # , interpolation=cv2.INTER_CUBIC
# 转换为灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 将灰度图转为浮点型,再进行dct变换
dct = cv2.dct(np.float32(gray))
# opencv实现的掩码操作
dct_roi = dct[0:8, 0:8]
hash = []
avreage = np.mean(dct_roi)
for i in range(dct_roi.shape[0]):
for j in range(dct_roi.shape[1]):
if dct_roi[i, j] > avreage:
hash.append(1)
else:
hash.append(0)
return hash
def calculate(image1, image2):
# 灰度直方图算法
# 计算单通道的直方图的相似值
hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
# 计算直方图的重合度
degree = 0
for i in range(len(hist1)):
if hist1[i] != hist2[i]:
degree = degree +
(1 - abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i]))
else:
degree = degree + 1
degree = degree / len(hist1)
return degree
def classify_hist_with_split(image1, image2, size=(256, 256)):
# RGB每个通道的直方图相似度
# 将图像resize后,分离为RGB三个通道,再计算每个通道的相似值
image1 = cv2.resize(image1, size)
image2 = cv2.resize(image2, size)
sub_image1 = cv2.split(image1)
sub_image2 = cv2.split(image2)
sub_data = 0
for im1, im2 in zip(sub_image1, sub_image2):
sub_data += calculate(im1, im2)
sub_data = sub_data / 3
return sub_data
def cmpHash(hash1, hash2):
# Hash值对比
# 算法中1和0顺序组合起来的即是图片的指纹hash。顺序不固定,但是比较的时候必须是相同的顺序。
# 对比两幅图的指纹,计算汉明距离,即两个64位的hash值有多少是不一样的,不同的位数越小,图片越相似
# 汉明距离:一组二进制数据变成另一组数据所需要的步骤,可以衡量两图的差异,汉明距离越小,则相似度越高。汉明距离为0,即两张图片完全一样
n = 0
# hash长度不同则返回-1代表传参出错
if len(hash1) != len(hash2):
return -1
# 遍历判断
for i in range(len(hash1)):
# 不相等则n计数+1,n最终为相似度
if hash1[i] != hash2[i]:
n = n + 1
return n
def getImageByUrl(url):
# 根据图片url 获取图片对象
html = requests.get(url, verify=False)
image = Image.open(BytesIO(html.content))
return image
def PILImageToCV():
# PIL Image转换成OpenCV格式
path = "/Users/waldenz/Documents/Work/doc/TestImages/t3.png"
img = Image.open(path)
plt.subplot(121)
plt.imshow(img)
print(isinstance(img, np.ndarray))
img = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
print(isinstance(img, np.ndarray))
plt.subplot(122)
plt.imshow(img)
plt.show()
def CVImageToPIL():
# OpenCV图片转换为PIL image
path = "/Users/waldenz/Documents/Work/doc/TestImages/t3.png"
img = cv2.imread(path)
# cv2.imshow("OpenCV",img)
plt.subplot(121)
plt.imshow(img)
img2 = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.subplot(122)
plt.imshow(img2)
plt.show()
def bytes_to_cvimage(filebytes):
# 图片字节流转换为cv image
image = Image.open(filebytes)
img = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
return img
def hammingDist(s1, s2):
assert len(s1) == len(s2)
return sum([ch1 != ch2 for ch1, ch2 in zip(s1, s2)])
def d_score(img1,img2):
dhash_str1 = dHash(img1)
dhash_str2 = dHash(img2)
d_scores = 1 - hammingDist(dhash_str1, dhash_str2) * 1. / (32 * 32 / 4)
return d_scores
def runAllImageSimilaryFun(img1, img2):
n4 = classify_hist_with_split(img1, img2)
print('三直方图算法相似度:', n4)
2. 按照排序的结果将图片拼接起来
最后 flag 就出来了
团队昵称:Decrypt
一血用时:2小时30分
出题人视角
本题密码学,本意是考察差分攻击方法。
解题方法一:
本题秘钥有5个参数,key=[k0,k1,k2,k3,k4], 因rol7(b^k4)^k3 = rol7(b)^rol7(k4)^k3=rol7(b)^k34, 那么可以去掉一个参数,key=[k0,k1,k2,k34]
将 y=ror7(sbox0[sbox1[sbox0[x^k0]^k1]^k2]^k34) 转化为5步
A = x ^ k0
B = sbox0(A)^k1
C = sbox1(B)^k2
D = sbox0(C)^k34
E = ror7(D)
可以用题目中提示中的两个结果对进行分析
2684 encrypts to 2568
3599 encrypts to 3185
建立差分矩阵链条,[x1^x2,A1^A2,B1^B2,C1^C2,D1^D2,E1^E2] , 现在我们已知两个输入(x1=2684,x2=3599),和两个输出
(E1=2568,E2=3185), 其原理是找到其中最有可能的差分链,从而求解秘钥
A1^A2 = (k0 ^ x1) ^ (k0 ^ x2) = x1^x2
(注:两个相同的值异或可以抵消)
= 2684 ^ 3599 = 1139
B1^B2 = sbox0(A1)^k1 ^ sbox0(A2)^k1 = sbox0(A1)^sbox0(A2)
建立sbox0的差分表
for A1 in xrange(MAX_VALUE):
for A2 in xrange(x1,MAX_VALUE):
B1^B2 = cipher.sbox0[A1]^cipher.sbox0[A2]
print A1,A2,A1^A2,B1^B2
由于A1^A2 = 1139, 计算B1^B2最大的概率, 结果为2891
awk '{if($3==1139) print $4}' sbox0|sort | uniq -c | sort -nr | more
同理,C1^C2 = sbox1(B1)^sbox1(B2), 当B1^B2为2891时,计算C1^C2的最大概率为1915
D1^D2 = sbox0(C1)^sbox0(C2) , 复用sbox0差分表, D1^D2 最大概率为3251
E1^E2 = ror7(D1)^ror7(D2)=ror7(D1^D2)=ror7(3251)= 1657
计算 E1^E2 = 2568^3185 = 1657 与前推算相同,说明选择的差分链路正确
即 [x1^x2,A1^A2,B1^B2,C1^C2,D1^D2,E1^E2] = [1139,1139,2891,1915,3251,1657]
看sbox0差分表,A1,A2,A1^A2,B1^B2,
当确定A1^A2=1139,B1^B2=2891时,则A1,A2的可能选择的值就不多了,
列举如下
A = [138,1273,183,1220,516,1655,925,2030,943,2012,981,1958,2188,3327]
同理,所有B的可能性为
B = [29,2902,173,3046,239,2980,1285,3662,1397,3646,1571,3432,1621,3358]
C = [2,1913,318,1605,518,1405,918,1261,982,1197,2150,3869,2441,3826]
D = [比较多,有2048个]
A = x^k0, 则k0 = A^x 的可能性为:
k0 = [2806, 3717, 2763, 3768, 2168, 3083, 2529, 3474, 2515, 3488, 2473, 3546, 752, 1667]
k1 = sbox0(A)^B
k2 = sbox1(B)^C
k34 = sbox0(C)^D
然后从给定的明密文对中随便找几个验证下k是否正确,就可以求解出正确的密文 key = [3488,2863,726,1886]
解题方法二:
在同学们提交的writeup中, ddmmhm队的解法较好,get到了本题的思路是差分攻击+暴力破解,通过cipherdiff设计,把算法复杂度降到4096^2。
补充了下推导过程如下:
y = ror7(sbox0[sbox1[sbox0[x^k0]^k1]^k2] ^k3)
y1^y2 = ror7(sbox0[sbox1[sbox0[x1^k0]^k1]^k2] ^k3) ^ ror7(sbox0[sbox1[sbox0[x2^k0]^k1]^k2] ^k3)
= ror7(sbox0[sbox1[sbox0[x1^k0]^k1]^k2] ^ sbox0[sbox1[sbox0[x2^k0]^k1]^k2])
rol7(y1^y2) = sbox0[sbox1[sbox0[x1^k0]^k1]^k2] ^ sbox0[sbox1[sbox0[x2^k0]^k1]^k2]
令 Mx = sbox1[sbox0[x1^k0]^k1] => rol7(y1^y2) = sbox0[Mx1] ^ sbox0[Mx2]
令 My = sbox0(Mx) => Mx = rsbox0(My)
则 My1^ My2 = sbox0(Mx1) ^ sbox0(Mx2) = rol7(y1^y2)
Mx1^Mx2 = rsbox0(My1) ^ rsbox0(My2) = rsbox0(My1) ^ rsbox0(rol7(y1^y2) ^ My1)
脚本如下:
NUM_BITS = 12
BLOCK_SIZE_BITS = 48
BLOCK_SIZE = BLOCK_SIZE_BITS / 8
MAX_VALUE = (2 << (NUM_BITS - 1))
BIT_MASK = MAX_VALUE - 1
def ror7(b):
return ((((b) & BIT_MASK) >> 7) | (((b) << (NUM_BITS - 7)) & BIT_MASK))
def rol7(b):
return ((((b) << 7) & BIT_MASK) | (((b) & BIT_MASK) >> (NUM_BITS - 7)))
k = [(1943, 307), (800, 2489), (3948, 2361), (3941, 2168), (2080, 2160), (1121, 1001), (1380, 73), (1897, 934), (2169, 2845), (1766, 3036), (1396, 415), (104, 2240), (1670, 1214), (0, 326), (364, 2896), (1590, 993), (633, 361), (1655, 990), (1894, 2759), (1799, 1793), (873, 258), (1591, 2209), (1559, 2214), (519, 2269), (1654, 3808), (1394, 1260), (609, 790), (1798, 2503), (1842, 3884), (1079, 567), (1135, 3421), (1862, 300)]
_rand_start = 0
def my_rand():
global _rand_start
if _rand_start == 0:
_rand_start = 123459876
hi = _rand_start / 127773
lo = _rand_start % 127773
x = 16807 * lo - 2836 * hi
if x < 0:
x += 0x7fffffff
_rand_start = (x % (0x7fffffff + 1))
return _rand_start
def generate_boxes(seed):
global _rand_start
_rand_start = seed
sbox = range(MAX_VALUE)
rsbox = range(MAX_VALUE)
for i in range(MAX_VALUE):
r = my_rand() % MAX_VALUE
temp = sbox[i]
sbox[i] = sbox[r]
sbox[r] = temp
for i in range(MAX_VALUE):
rsbox[sbox[i]] = i
return sbox, rsbox
sbox0, rsbox0 = generate_boxes(106)
sbox1, rsbox1 = generate_boxes(81)
SECRET_KEY = [3488, 2863, 0, 0]
# k5 = rol(k4) ^ k3
def extract(n):
global SECRET_KEY
for i in range(4):
SECRET_KEY[i] = n % (2 ** 12)
n /= (2 ** 12)
def encrypt_bits(b, SECRET_KEY):
t = sbox0[(b & BIT_MASK) ^ SECRET_KEY[0]] ^ SECRET_KEY[1]
boxed = sbox0[sbox1[ t ] ^ SECRET_KEY[2]]
return ror7(boxed ^ SECRET_KEY[3] ) & BIT_MASK
def decrypt_bits(b):
unboxed = rol7((b & BIT_MASK)) ^ SECRET_KEY[3]
t = rsbox1[ rsbox0[unboxed] ^ SECRET_KEY[2] ] ^ SECRET_KEY[1]
return (rsbox0[ t ] ^ SECRET_KEY[0])
pair = [(1943, 307), (800, 2489), (3948, 2361), (3941, 2168), (2080, 2160), (1121, 1001), (1380, 73), (1897, 934), (2169, 2845), (1766, 3036), (1396, 415), (104, 2240), (1670, 1214), (0, 326), (364, 2896), (1590, 993), (633, 361), (1655, 990), (1894, 2759), (1799, 1793), (873, 258), (1591, 2209), (1559, 2214), (519, 2269), (1654, 3808), (1394, 1260), (609, 790), (1798, 2503), (1842, 3884), (1079, 567), (2684, 2568), (1135, 3421), (1862, 300), (3599, 3185)]
def fun1(diff, box):
res = {}
for i in range(2 ** 12):
res[box[i] ^ box[i ^ diff]] = i
return res
cipherdiff = []
for i in pair[1:]:
cipherdiff.append(fun1(rol7(pair[0][1] ^ i[1]), rsbox0))
import multiprocessing
def run(start):
print(start)
for key0 in range(start, start+256):
for key1 in range(2 ** 12):
mid = [sbox1[sbox0[(i[0]) ^ key0] ^ key1] for i in pair]
for i in range(1, len(mid)):
if not mid[0] ^ mid[i] in cipherdiff[i - 1]:
break
else:
print (key0, key1)
def run1(start):
print(start)
for key2 in range(start, start+256):
for key3 in range(2 ** 12):
SECRET_KEY = [3488, 2863, key2, key3]
for i in pair:
if encrypt_bits(i[0], SECRET_KEY) != i[1]:
break
else:
print (key2, key3)
return
for i in range(16):
# multiprocessing.Process(target=run, args=(i * 256,)).start()
multiprocessing.Process(target=run1, args=(i * 256,)).start()
-----选手Write Up-----
很容易看出,bits_list_to_string和string_to_bits_list互为逆运算,将每3bytes和非负整数相互转换,核心加密函数为encrypt_bits,是GF(212)上的可逆函数。
具体的加密过程可以表示如下:
对于给定密钥,输入和输出一一对应。而我们有一组明文和密文对照,可以以此爆破密钥,时间复杂度为40965≈1018;考虑中间相遇攻击,时间复杂度为40963≈1011。进一步观察,对于解密第一步操作rol7(b ^ k4) ^ k3 = rol7(b) ^ rol7(k4) ^ k3,而rol7(k4) ^ k3仍属于GF(212),因此可以将时间复杂度降至40962≈107,该时间代价较小,可以利用普通电脑破解。经测试,使用32核cpu服务器不到10分钟可以使用python得出结果。
注:若不进行最后一步化简,将产生多解,虽然也能正确解密,但对算法的理解不够深入。
解题步骤:
1、使用string_to_bits_list将明文和密文分别转换成数组;
2、爆破密钥,分别将前两步加密结果和后两步解密结果存储至不同集合(本代码中使用字典和bits_list_to_string方便存储);
3、求两个集合交集,计算相应密钥并解密。
解题脚本:
NUM_BITS = 12
BLOCK_SIZE_BITS = 48
BLOCK_SIZE = BLOCK_SIZE_BITS // 8
MAX_VALUE = (2 << (NUM_BITS - 1))
BIT_MASK = MAX_VALUE - 1
class Cipher(object):
def __init__(self, k0, k1, k2, k3, k4):
self.k0 = k0
self.k1 = k1
self.k2 = k2
self.k3 = k3
self.k4 = k4
self._rand_start = 0
self.sbox0, self.rsbox0 = self.generate_boxes(106)
self.sbox1, self.rsbox1 = self.generate_boxes(81)
def my_srand(self, seed):
self._rand_start = seed
def my_rand(self):
if self._rand_start == 0:
self._rand_start = 123459876
hi = self._rand_start // 127773
lo = self._rand_start % 127773
x = 16807 * lo - 2836 * hi
if x < 0:
x += 0x7fffffff
self._rand_start = (x % (0x7fffffff + 1))
return self._rand_start
def generate_boxes(self, seed):
self.my_srand(seed)
sbox = list(range(MAX_VALUE))
rsbox = list(range(MAX_VALUE))
for i in range(MAX_VALUE):
r = self.my_rand() % MAX_VALUE
temp = sbox[i]
sbox[i] = sbox[r]
sbox[r] = temp
for i in range(MAX_VALUE):
rsbox[sbox[i]] = i
return sbox, rsbox
def ror7(self, b):
return ((((b) & BIT_MASK) >> 7) | (((b) << (NUM_BITS - 7)) & BIT_MASK))
def rol7(self, b):
return ((((b) << 7) & BIT_MASK) | (((b) & BIT_MASK) >> (NUM_BITS - 7)))
def pad_string(self, s):
num_blocks = len(s) // BLOCK_SIZE
num_remainder = len(s) % BLOCK_SIZE
pad = (BLOCK_SIZE - num_remainder) % BLOCK_SIZE
s += bytes([pad]*(BLOCK_SIZE - num_remainder))
return s
def unpad_string(self, s):
pad = s[-1] & 0xff
if pad == 0 or pad > BLOCK_SIZE:
pad = BLOCK_SIZE
return s[:-pad]
def string_to_bits_list(self, s):
input_chars = s
num_blocks = len(s) // BLOCK_SIZE
bits_list = []
for i in range(num_blocks):
block = 0
for j in range(BLOCK_SIZE):
block = block << 8
block = block | input_chars[i * BLOCK_SIZE + j]
for j in range(BLOCK_SIZE_BITS, 0, -NUM_BITS):
bits_list.append((block >> (j - NUM_BITS)) & BIT_MASK)
return bits_list
def bits_list_to_string(self, input_bits):
num_input_bits_per_block = BLOCK_SIZE_BITS // NUM_BITS;
output_chars = []
for i in range(0, len(input_bits), num_input_bits_per_block):
block = 0
for j in range(num_input_bits_per_block):
block = block << NUM_BITS
block = block | (input_bits[i+j])
for j in range(BLOCK_SIZE, 0, -1):
output_chars.append((block >> ((j-1) * 8)) & 0xff)
return bytes(output_chars)
def encrypt_bits(self, b):
boxed = self.sbox0[self.sbox1[self.sbox0[(b & BIT_MASK) ^ self.k0] ^ self.k1] ^ self.k2] ^ self.k3
return (self.ror7(boxed) ^ self.k4) & BIT_MASK;
def decrypt_bits(self, b):
unboxed = self.rol7((b & BIT_MASK) ^ self.k4) ^ self.k3
return (self.rsbox0[self.rsbox1[self.rsbox0[unboxed] ^ self.k2] ^ self.k1] ^ self.k0);
def encrypt(self, s):
pad_s = self.pad_string(s)
bits = self.string_to_bits_list(pad_s)
return self.bits_list_to_string([(self.encrypt_bits(b)) for b in bits])
def decrypt(self, s):
bits = self.string_to_bits_list(s)
dec = [self.decrypt_bits(b) for b in bits]
return self.unpad_string(self.bits_list_to_string(dec))
#find the right SECRET_KEYS
SECRET_KEYS = [0,0,0,0,0]
cipher = Cipher(*SECRET_KEYS)
test_text = b"Cryptanalysis has coevolved together with cryptography"
test_text=cipher.string_to_bits_list(test_text)
ciphertext = bytes.fromhex("2371697013e9bdcb50133102f2c8c08a69b93e1878ac7939ac70498ddd5dee019f4be4ec8dd3a612c8708a1169701d5d3de3169c7b1d146146146146")
ciphertext=cipher.string_to_bits_list(ciphertext)
import tqdm
"""
rol7(b ^ k4) ^ k3 = rol7(b) ^ rol7(k4) ^ k3 = rol7(b) ^ k3k4
"""
k0_k1_dict={}
i=0
t=tqdm.tqdm(total=MAX_VALUE**2,ascii=True)
for k0 in range(MAX_VALUE):
for k1 in range(MAX_VALUE):
ts=[]
for b in test_text[:12]:
ts.append(cipher.sbox1[cipher.sbox0[b ^ k0] ^ k1])
k0_k1_dict[cipher.bits_list_to_string(ts)]=i
t.update()
i+=1
t.close()
k2_k3k4_dict={}
i=0
t=tqdm.tqdm(total=MAX_VALUE**2,ascii=True)
for k2 in range(MAX_VALUE):
for k3k4 in range(MAX_VALUE):
ts=[]
for b in ciphertext[:12]:
ts.append(cipher.rsbox0[cipher.rol7(b) ^ k3k4] ^ k2)
k2_k3k4_dict[cipher.bits_list_to_string(ts)]=i
t.update()
i+=1
t.close()
for key in k0_k1_dict.keys():
if key in k2_k3k4_dict:
i=k0_k1_dict[key]
j=k2_k3k4_dict[key]
k0=i//MAX_VALUE
k1=i%MAX_VALUE
k2=j//MAX_VALUE
k3k4=j%MAX_VALUE
break
message=[]
enc=bytes.fromhex("8ed251b18692697c0f57513c697f09ae4425eacd92a7982edcf9581fa8019e6dcb5753e96972fdf01dcf")
for c in cipher.string_to_bits_list(enc):
message.append(cipher.rsbox0[cipher.rsbox1[cipher.rsbox0[cipher.rol7(c) ^ k3k4] ^ k2] ^ k1] ^ k0)
flag=cipher.unpad_string(cipher.bits_list_to_string(message)).decode()
print(flag)
FLAG值:
DDCTF{69f15862699b70d7c1084eca5f214a60}
本文始发于微信公众号(滴滴安全应急响应中心):DDCTF2020官方Write Up——MISC 篇
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论