📌

# Google CTF writeup

2022/07/04に公開

## ELECTRIC MAYHEM CLS

### 問題概要

• AESで16bytesした平文と暗号文、下図のような電力の組が50個与えられる
• 鍵を求めてください

### 考察/方針

• 図を見てみると、時間軸方向は一致していて、かなりきれいなデータをしている
• AESの10ラウンドっぽい
• まあ電力解析やろな…

### 実装、結果

#ref: https://trmr.hatenablog.com/entry/2018/03/18/220441

import json
import numpy as np
import matplotlib.pyplot as plt
import scipy.io as sio
from Crypto.Cipher import AES
import binascii
import sys

humming = [bin(n).count("1") for n in range(256)]

json_open = open('traces.json', 'r')
json_load = json.load(json_open)

sbox = (
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16)

def addkey_subbytes(pt, guesskey):
return sbox[pt ^ guesskey]

N = len(json_load)

print(N)
# print(json_load[0]['pt'])
# print(json_load[0]['ct'])
# print(json_load[0]['pm'])

bestguess = [0] * 16
data_start = 7
data_end = 200
NUM_POINTS = data_end - data_start
NUM_TRACES = len(json_load)
traces = []

for pt_ct_pm in json_load:
traces.append(pt_ct_pm['pm'][data_start:data_end])

for k_idx in range(16): # determine key index
cpaoutput = [0] * 256

# follow valiables may not be need
maxcpa = [0] * 256
bestcor = 0
bestkey = 0

for kguess in range(256): # determine word key candidate
sumnum = np.zeros(NUM_POINTS)
sumden1 = np.zeros(NUM_POINTS)
sumden2 = np.zeros(NUM_POINTS)

hyp = np.zeros(NUM_TRACES)

for t_idx in range(NUM_TRACES): # hypothesis hamming weight
hyp[t_idx] = humming[addkey_subbytes(json_load[t_idx]['pt'][k_idx], kguess)]

h_mean = np.mean(hyp, dtype=np.float64)
t_mean = np.mean(traces, axis=0, dtype=np.float64)

assert 0 < h_mean and h_mean < 8, "meanh is not between 0 and 8"
assert len(t_mean) == NUM_POINTS, "meant is less than trace points"

cors = []

for t_idx in range(NUM_TRACES):
hdiff = (hyp[t_idx] - h_mean)
tdiff = traces[t_idx] - t_mean

sumnum = sumnum + (hdiff * tdiff)
sumden1 = sumden1 + hdiff * hdiff
sumden2 = sumden2 + tdiff * tdiff

cpaoutput[kguess] = sumnum / np.sqrt(sumden1 * sumden2)

maxcpa[kguess] = max(abs(cpaoutput[kguess]))

bestguess[k_idx] = np.argmax(maxcpa)
print("[+] best guess key [{0}] is {1:02x} (score: {2})".format(k_idx, bestguess[k_idx], maxcpa[bestguess[k_idx]]))

strkey = ''.join(map(chr, bestguess))
print(strkey)



[+] best guess key [0] is 57 (score: 0.7664977105152343)
[+] best guess key [1] is 30 (score: 0.8037369571679035)
[+] best guess key [2] is 63 (score: 0.8157907186646868)
[+] best guess key [3] is 6b (score: 0.8521975480655525)
[+] best guess key [4] is 41 (score: 0.8582517655176524)
[+] best guess key [5] is 77 (score: 0.8911782264396743)
[+] best guess key [6] is 6f (score: 0.8435973736511324)
[+] best guess key [7] is 63 (score: 0.9323855381027755)
[+] best guess key [8] is 4b (score: 0.7750422415424753)
[+] best guess key [9] is 61 (score: 0.7558696238936866)
[+] best guess key [10] is 57 (score: 0.6984881287051522)
[+] best guess key [11] is 6f (score: 0.7681638092660229)
[+] best guess key [12] is 43 (score: 0.8914487915813523)
[+] best guess key [13] is 6b (score: 0.8666624881937892)
[+] best guess key [14] is 61 (score: 0.8998081004528541)
[+] best guess key [15] is 31 (score: 0.8984227108207175)
W0ckAwocKaWoCka1


# Cycling

### 問題概要

#!/usr/bin/python3

# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
It is well known that any RSA encryption can be undone by just encrypting the
ciphertext over and over again. If the RSA modulus has been chosen badly then
the number of encryptions necessary to undo an encryption is small.
If n = 0x112b00148621 then only 209 encryptions are necessary as the following
example demonstrates:

>>> e = 65537
>>> n = 0x112b00148621
>>> pt = 0xdeadbeef
>>> # Encryption
>>> ct = pow(pt, e, n)
>>> # Decryption via cycling:
>>> pt = ct
>>> for _ in range(209):
>>>   pt = pow(pt, e, n)
>>> # Assert decryption worked:
>>> assert ct == pow(pt, e, n)

However, if the modulus is well chosen then a cycle attack can take much longer.
This property can be used for a timed release of a message. We have confirmed
that it takes a whopping 2^1025-3 encryptions to decrypt the flag. Pack out
your quantum computer and perform 2^1025-3 encryptions to solve this
challenge. Good luck doing this in 48h.
"""

e = 65537
n = 0x99efa9177387907eb3f74dc09a4d7a93abf6ceb7ee102c689ecd0998975cede29f3ca951feb5adfb9282879cc666e22dcafc07d7f89d762b9ad5532042c79060cdb022703d790421a7f6a76a50cceb635ad1b5d78510adf8c6ff9645a1b179e965358e10fe3dd5f82744773360270b6fa62d972d196a810e152f1285e0b8b26f5d54991d0539a13e655d752bd71963f822affc7a03e946cea2c4ef65bf94706f20b79d672e64e8faac45172c4130bfeca9bef71ed8c0c9e2aa0a1d6d47239960f90ef25b337255bac9c452cb019a44115b0437726a9adef10a028f1e1263c97c14a1d7cd58a8994832e764ffbfcc05ec8ed3269bb0569278eea0550548b552b1
ct = 0x339be515121dab503106cd190897382149e032a76a1ca0eec74f2c8c74560b00dffc0ad65ee4df4f47b2c9810d93e8579517692268c821c6724946438a9744a2a95510d529f0e0195a2660abd057d3f6a59df3a1c9a116f76d53900e2a715dfe5525228e832c02fd07b8dac0d488cca269e0dbb74047cf7a5e64a06a443f7d580ee28c5d41d5ede3604825eba31985e96575df2bcc2fefd0c77f2033c04008be9746a0935338434c16d5a68d1338eabdcf0170ac19a27ec832bf0a353934570abd48b1fe31bc9a4bb99428d1fbab726b284aec27522efb9527ddce1106ba6a480c65f9332c5b2a3c727a2cca6d6951b09c7c28ed0474fdc6a945076524877680

# Decryption via cycling:
pt = ct
for _ in range(2**1025 - 3):
pt = pow(pt, e, n)
# Assert decryption worked:
assert ct == pow(pt, e, n)

# Print flag:
print(pt.to_bytes((pt.bit_length() + 7)//8, 'big').decode())


### 方針

この問題は数式で書くと、次の式を解く問題といえます。

m = c^{\left(e^{\left(2^{1025}-3\right)}\right)} \mod n

バイナリ法は、 m 乗するときに、 \mathrm{O}(\log m) の計算量になるので、ざっくり 2^{1025}-3 回くらいは計算しないです。これでは大変つらいので、計算量を減らしていきましょう。

この問題のポイントは、e^{2^{1025}-3}乗すると、もとに戻るということを利用するということです。これは何を表しているのでしょうか？

カーマイケル関数を \lambda(n) と書くことにします。\lambda(n)は以下の式を満たす最小の値になります。

x^{\lambda(n)} \equiv 1 \mod n

n = pqのとき、以下の式が成立します。

\lambda(n) = \mathrm{lcm}(p-1, q-1)

この問題は、問題文より、以下の式が成立します。(2^{1025}-3ではなく、2^{1025}-2になっているのは、最初の暗号化の分の一回です)

m = m^{\left(e^{\left(2^{1025}-2\right)}\right)} \mod n

e^{\left(2^{1025}-2\right)} = 1 \mod \lambda(n)

そしてさらにこの式を、カーマイケル関数の定義に照らし合わせると、以下のようになります（「最小」の部分は未証明です。）

\lambda(\lambda(n)) = 2^{1025}-2

ずいぶんとシンプルな式になりました。さて、ここからAを適当な整数として、A \lambda(n) を求めていきます。求めたあとに、以下の式を求めることで、乗数をとても小さくすることができます。適当な整数がかかっていても、m^{A\lambda(n)}=1なことにはかわりないので、問題ありません。

e^{\left(2^{1025}-2\right)} \mod A \lambda(n) \rightarrow \mathrm{small\ value}

では、どのようにして、A\lambda(n)を求めればよいでしょうか？カーマイケル関数は、m = pqr (p,q,rは全部素数)だったとすると、

\lambda(m) = \mathrm{lcm}(p-1,q-1,r-1)

のようになります。ここで、計算結果が 2,3,5,11 だったとすると、もともとの値の候補は、これらをかけ合わせた何かになるはずです。例えば

2 \times 3 = 6 \rightarrow 6 + 1 = 7 2 \times 5 = 10 \rightarrow 10 + 1 = 11 2 \times 11 = 22 \rightarrow 22 + 1 = 23 2 \times 3 \times 5 = 30 \rightarrow 30 + 1 = 31

のようになります。このように組み合わせをすべて列挙し、全部掛け算を求めることで、A\lambda(n) が求まるはずです。（多分指数を考慮に入れる必要はありますが、今回はなんかこれでうまくいきました。指数を考慮に入れる場合はおそらく重なるのは低い次数のものなので、低いものを何度かかければいいのかなと思っております。）

あとは、カーマイケル関数の定義よりこの結果が素数になっていてほしいので、素数であるもののみをフィルタリングして、すべての積を求めました。

1. 2^{1025}-2 を素因数分解する(factorDBを用いれば可能)
2. 素因数分解した値の組み合わせを列挙し、全ての積を求め、A\lambda(n)を求める
3. X = e^{\left(2^{1025}-3\right)} \mod A\lambda(n) を求める
4. c^X \mod n を求める

とすると、求まります。以下がソースコードです。

from Crypto.Util.number import *
import itertools
import math

e = 65537
n = 0x99efa9177387907eb3f74dc09a4d7a93abf6ceb7ee102c689ecd0998975cede29f3ca951feb5adfb9282879cc666e22dcafc07d7f89d762b9ad5532042c79060cdb022703d790421a7f6a76a50cceb635ad1b5d78510adf8c6ff9645a1b179e965358e10fe3dd5f82744773360270b6fa62d972d196a810e152f1285e0b8b26f5d54991d0539a13e655d752bd71963f822affc7a03e946cea2c4ef65bf94706f20b79d672e64e8faac45172c4130bfeca9bef71ed8c0c9e2aa0a1d6d47239960f90ef25b337255bac9c452cb019a44115b0437726a9adef10a028f1e1263c97c14a1d7cd58a8994832e764ffbfcc05ec8ed3269bb0569278eea0550548b552b1
ct = 0x339be515121dab503106cd190897382149e032a76a1ca0eec74f2c8c74560b00dffc0ad65ee4df4f47b2c9810d93e8579517692268c821c6724946438a9744a2a95510d529f0e0195a2660abd057d3f6a59df3a1c9a116f76d53900e2a715dfe5525228e832c02fd07b8dac0d488cca269e0dbb74047cf7a5e64a06a443f7d580ee28c5d41d5ede3604825eba31985e96575df2bcc2fefd0c77f2033c04008be9746a0935338434c16d5a68d1338eabdcf0170ac19a27ec832bf0a353934570abd48b1fe31bc9a4bb99428d1fbab726b284aec27522efb9527ddce1106ba6a480c65f9332c5b2a3c727a2cca6d6951b09c7c28ed0474fdc6a945076524877680

lambda_lambda_divs = [
2,
3,
5,
17,
257,
641,
65537,
274177,
2424833,
6700417,
67280421310721,
1238926361552897,
59649589127497217,
5704689200685129054721,
7455602825647884208337395736200454918783366342657,
93461639715357977769163558199606896584051237541638188580280321,
741640062627530801524787141901937474059940781097519023905821316144415759504705008092818711693940737
]

lambda_lambda = 1
for d in lambda_lambda_divs:
lambda_lambda *= d

assert lambda_lambda == 2**1025-2

lambda_candidate = 1

for d in lambda_lambda_divs:
lambda_candidate *= (d+1)**10

cand = lambda_lambda_divs[:5]

res = 1
cnt = 0
for L in range(0, len(lambda_lambda_divs) + 1):
for subset in itertools.combinations(lambda_lambda_divs, L):
print(f"{cnt}/{131072}")
cnt += 1
a = 2*math.prod(subset)+1
if isPrime(a):
res *= a

print(res.bit_length())
p = pow(e,2**1025-3,res)
pt = pow(ct, p, n)
print(pt.to_bytes((pt.bit_length() + 7)//8, 'big').decode())


...
131068/131072
131069/131072
131070/131072
131071/131072
464441
CTF{Recycling_Is_Great}


### 追記

ところで、このソースコード、 p = pow(e,2**1025-3,res) この部分に時間がかかります。実は、この部分、計算している内容は p = pow(e,-1,res) と同じなので、これに書き換えることでめちゃ早く計算が終わります。終わってから教えられました。どうして気づかなかったのか、悲しみです…

ログインするとコメントできます