ctf
January 14, 2025

Recording of Timing Attack in CTF

想象一下,你正在尝试打开一个密码保护的保险箱。传统的方法是尝试破解密码本身,就像硬核数学家一样研究密码算法的数学基础。但是有时候通过倾听保险箱齿轮转动的声音,或者观察开锁所需的时间,你就能推测出密码。这就是侧信道攻击(Side-Channel Attack)的精髓所在。

就像古代伟大的将军善于利用细微的环境变化来洞察敌情一样,侧信道攻击者也在寻找密码系统运行时泄露的各种”蛛丝马迹”。这些信息可能包括:

本周末笔者在参加两场CTF比赛(SUCTF 2025和UofTCTF 2025)时,遇到了两道关于时间侧信道攻击的题目。题目并不困难但是非常有意思,笔者记录且分享一下相关内容。这种攻击方式利用了密码算法实现中的一个基本事实:不同的输入可能会导致程序执行时间的细微差异。这些差异虽然微小,但在统计意义上是可以被检测和利用的。

接下来,让笔者带领大家深入两个精彩的CTF题目,看看如何在实战中运用时间侧信道攻击:

  1. 🏆 SUCTF 2025: SU_Auth(Crypto, 6 solves, 1st🩸)
    • 针对后量子密码学算法CSIDH的时间侧信道攻击,
    • CSIDH private key collision.
  2. 🔐 UofTCTF 2025: Timed AES(Crypto, 4 solves)
    • 针对对称加密算法AES的时间侧信道攻击,
    • Sbox Inv:当枚举替代查表时会发生什么.

SUCTF 2025: SU_Auth(Crypto, 6 solves)

Overview

题目的代码量不大,核心如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def SuAuth(A, priv, LIMIT=True):
if any(priv[i] == SUKEY[i] for i in range(len(ells))) and LIMIT: return "🙅SUKEY"
E = EllipticCurve(F, [0, A, 0, 1, 0])
for sgn in [1, -1]:
for e, ell in zip(priv, ells):
for i in range(sgn * e):
while not (P := (p + 1) // ell * E.random_element()) or P.order() != ell:
pass
E = E.isogeny_codomain(P)
E = E.quadratic_twist()
return E.montgomery_model().a2()

cipher = lambda key: AES.new(md5(key.encode()).digest(),AES.MODE_ECB)
SUDOOR = cipher(str(SuAuth(0, SUKEY, 0))).encrypt(b"./OPEN_THE_FLAG!")

print("😜", SuAuth(0, SUKEY, 0))

while True:
AKey = SuAuth(0, literal_eval(input("🔑: ")))
try: subprocess.run(cipher(str(AKey)).decrypt(SUDOOR))
except: continue

Analysis

笔者初见SuAuth函数时,漏洞其实已经非常明显了 🔍 就在函数的第一行:if any(priv[i] == SUKEY[i] for i in range(len(ells))) and LIMIT: return "🙅SUKEY"。这会带来什么后果呢? 🤔

CSIDH的效率并不算高 🐢,如果提交的priv中某一位恰好猜中了SUKEY对应位置的值,函数就会直接返回”🙅SUKEY”,而不会执行后续的CSIDH同源操作。这就导致了一个明显的时间差 ⚡:与执行完整的CSIDH group action(涉及46组同源路径组合)相比,提前返回的执行时间会短得多 ⏱️。 基于这个特点,可以逐位(从第0位到最后一位)尝试-3到3的值,当某次执行时间异常短时,就可以认定这个值就是SUKEY在该位置的值。通过最多7*46次交互 🔄,就能完整还原出SUKEY 🎯。类似SQL盲注。

获得SUKEY后,要成功调用SUDOOR还需要找到一个与SUKEY每个位置都不同的Akey 🔑。一开始我尝试了很多方法去推测群阶 🧮,都没有成功 😅。后来在GPT的提示下得到了6-x for x in SUKEY这个思路 💡,我将其修改为6+x for x in SUKEY后成功找到了等价collision ✨。后来向随缘请教才知道,原来在某篇CSIDH综述中提到[3,3,3,…,3]这个序列可以让它回到起始状态 🔄。有兴趣的读者也可以自行去寻找并且阅读相关文献📖,也给出了对应的证明。

UofTCTF 2025: Timed AES(Crypto, 4 solves)

Overview

题目的代码量也不大,核心如下:

AES.c 其中关键部分为

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
...
u_int8_t doSbox(u_int8_t num){
return sbox[num];
}

u_int8_t doSboxInv(u_int8_t num){
for (size_t i = 0; i < 256; i++)
{
if (sbox[i] == num) return i;
}
}
...

void aesBlockDecrypt(u_int8_t *key, const u_int8_t *enc, u_int8_t *msg){
u_int8_t expandedKey[16 * 11];
keyExpand(key, expandedKey);
memcpy(msg, enc, 16);

addRoundKey(expandedKey + 10*16, msg);

for (size_t i = 9; i > 0; i--)
{
u_int8_t *roundKey = expandedKey + 16 * i;
shiftRowsInv(msg);
subBytesInv(msg);
addRoundKey(roundKey, msg);
mixColumnsInv(msg);
}
shiftRowsInv(msg);
subBytesInv(msg);
addRoundKey(expandedKey, msg);
}

交互即使用flag作为AES的密钥进行加密或者解密操作,其中Encrypt也是查表操作罢了。out如下(共120w行输出):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1. Encrypt
2. Decrypt
3. Exit
> 1
Input the message to encrypt (in hex): 845E4551E164748C339242F4F0D387C0
Result: 4AEDF15DD920135775274A797E8E94A4
Encryption took 5423.000000 ns
> 1
Input the message to encrypt (in hex): 8AEDB8A424E6B54F623335CD0791A202
Result: A486AE81CC81622957840868EAC1D8EE
Encryption took 4026.000000 ns
> 1
Input the message to encrypt (in hex): 395A4A646AF2D2CB6F8D6F52408D4376
Result: 818E6A5071FB8BE8D2EE113E6F9FF4BB
Encryption took 3681.000000 ns
> 2
Input the message to decrypt (in hex): AE3CE1EFE517D36569961249192C84B1
Result: A668A522391B0454AED0FEDAA67283F5
Decryption took 48345.00 ns
> 1
Input the message to encrypt (in hex): 5A0C8C00EC51E36E11C8B538D2BBA952
Result: 4C574A1CB561BAF9758B9BC6BF3849CD
Encryption took 3245.000000 ns
....

Analysis

这道题的漏洞其实一眼就能看出来 - 就在doSboxInv 🔍 函数的实现上。这个函数用了最朴素的线性查找来实现S盒的逆操作,每次都要在256个值里一个一个找,直到找到匹配的值才返回。这种实现方式导致函数的执行时间会和目标值在数组中的位置直接相关。想想看,在AES解密的过程中,每一轮都要调用这个subBytesInv函数,而这个函数内部又会对每个字节调用doSboxInv ⏱️。由于是10轮操作,时间差异会被放大。这就给了我们一个绝佳的时间侧信道攻击机会 💥。

基于这个想法,笔者这样设计了这样的一个区分攻击:如果固定密文的某个字节(比如第一个字节),其他字节随机,那么根据概率论的知识 🎲,其他字节对解密时间的影响在取平均后期望趋近于0。这样观察到的时间差异主要就来自于我们控制的那个字节了。更进一步,如果能够猜测最后一轮的轮密钥 🔑,当猜测正确时,解密过程中的S盒查找会出现一个特殊的模式,这个模式会导致解密时间出现显著的异常⚠️。而当猜测错误时,由于输入是随机的,时间分布会相对均匀。

基于这个思路 💡,对每个可能的轮密钥值都进行测试,记录解密时间并进行统计分析。笔者的做法是给每次查询的时间和查询次数都赋予一定的权重⚖️,然后求和得到一个最终的评分。当某个猜测值的评分明显高于其他值时,这个值很可能就是正确的轮密钥。通过该方法,笔者成功求解出这道题目。一个简单的优化就是预先计算好逆S盒表,这样就能用O(1)的时间完成查找,避免时间侧信道攻击。

About this Post

This post is written by Hao Jiang, licensed under CC BY-NC 4.0.

#ctf#cryptography#TimingAttack#CSIDH#AES