Learning cyber security by playing and enjoying CTFs

Cyber Security関係の雑記帳です。表明されているお気持ちなどは全て個人的なものであり、筆者が所属もしくは関係する組織・団体の意向とは一切関係ありません。

Xbitの値Y個からmod(2^Z)のLCGにおける乱数予測する(分析編)

1 はじめに

チームN30Z30N総帥、Edwow Mathです。

CTF Advent Calendarの昨日(19日目)の記事は、Satoooonさんの「 CTFTime APIで取得したデータを眺める」でした。APIでデータ取れるのは知りませんでしたが、いろいろ面白い結果が出ているようです*1

この記事は、CTF Advent Calendar 202220日目の記事であり、「Xbitの値Y個からmod(2Z)のLCGにおける乱数予測する(実験編)」の続編です。

なお、これらの記事はいずれも、kurenaifさんによるyoutube動画「【17兆通り】4bitの値20個からJavaの乱数予測をする【kurenaif】」の視聴をきっかけとした考察ですので、動画未見の方にはご視聴を強くお勧めします*2

www.youtube.com

2 推測できること

「実験編」での結果から、次のことが推測できます。

  • リークするビット数(X)が多いほど成功する(まぁ、そりゃそうだ)。
  • リークする数(Y)が多いほど成功する(まぁ、そりゃそうだ)。
  • X=2の時の結果はX≧3の時と比べて極めて芳しくなく、Yを増やしても失敗する確率が高い。*3

上の二つはある意味当たり前なので、さほど面白くありません。しかし、最後のはちょっと興味深く感じました。

3 疑問点

さらに、「実験編」の結果から想いを巡らせてみたとき、以下のような疑問を持ちました。

  • X、Y、Zと「成功率」との間にある関係は?
  • aやbの選び方によって、「成功率」は変わるのか?*4
  • 初期値の選び方によって、「成功率」は変わるのか?
  • そもそも「失敗」するのは何故なのか?

これらを細かく見ていくと同人誌が出来そうな気がするので、今回は深入りはせず*5、最後の疑問について少し掘り下げて考えてみることにしました。

4 考察

aとbを適当に*6固定し、元ネタの動画と同じX=4、Y=20、Z=28で考えることにします。

以下のようなSageMathのコードを実行してみます。

#random number prediction
import random
from Crypto.Util.number import *

results = []
ROUND=100

class LCG:
  def __init__(self, a, b, m, seed):
    self.a = a
    self.b = b
    self.m = m
    self.state = seed
  def next_state(self):
    self.state = (self.a * self.state + self.b) % self.m
  def get_bits(self, s, t):
    self.next_state()
    return self.state >> (s - t)

s = 48
t = 4
u = 20
a = 0x5DEECE66D
b = 0xB
m = 2^s
success = 0
hit = 0
bad = 0

for cnt in range(ROUND):
  seed = cnt + m//2
  lcg = LCG(a, b, m, seed)
  leaks = []
  for i in range(u):
    x = lcg.get_bits(s, t)
    leaks += [x]
  Y = vector(ZZ, [leaks[i+1] - leaks[i] for i in range(u-1)])
  A = matrix.identity(ZZ, u-1)
  for i in range(u-1):
    if i == 0:
      A[i,i] = m
    else:
      A[i,0] = -1 * a^i
  L = A.LLL()
  K = vector(ZZ,[round(w / m) for w in 2**(s-t) * L * Y])
  E = ~L * (m * K - 2**(s-t) * L * Y)
  X = E + 2**(s-t) * Y

  var('x')
  cands = solve_mod((a-1)*x == (int(X[0])-b), m)

  _seed = 0
  cand_seed = []
  for cand in cands:
    cand = cand[0]
    lcg = LCG(a, b, m, cand)
    check = [cand>>(s-t)]
    for i in range(u-1):
      check.append(lcg.get_bits(s,t))
    if check == leaks:
      cand_seed += [(cand - b) * inverse(a, m) % m]
      break
  if seed in cand_seed:
    if len(cand_seed) == 1:
      success += 1
      print(f"success:{seed}")
    else:
      bit += 1
      print(f"hit:{seed}")
  else:
    bad += 1
    print(f"bad:{seed}")
  print(f"{cnt+1}/{ROUND}")
print(f"result:s={s}, t={t}, u={u}: {success}/{ROUND}, hit={hit}, bad={bad}")

結果は前回のように概ね70/100くらいで成功となるのですが、失敗時には「候補は出るけど全部ハズレ」ではなく、そもそも「候補が出ない」ものばかりでした。

念のため何回か繰り返してみましたが、結果は変わらず。

つまり、「solve_mod( (a-1)*x == (int(X[0])-b), m ) の解がないときに失敗」ということになります。

a-1 は 4で割り切れ、8で割り切れないので、(X[0])-b が 4で割り切れないときには失敗となります。

それでは、なぜそうなってしまうのか。失敗するケースにおいては、LLLの行列の作り方を再検討する余地があるのかもしれません。

5 おわりに

…と、ここで時間切れとなってしまったので、続きはまたの機会に*7

時間を取って、もう少し調べてみる必要があるようですので。

さて、CTF Advent Calendarの明日(21日目)の記事は、れっくすさんの「CTFプレイヤーにアンケート取ってなんかやりたくない?(結果編)」です。私もアンケートには回答しているので、結果が楽しみです。

*1:開催数が右肩上がりなのは素晴らしいことですし、競技あたりのチーム数が減っているのは人口減少なのか、問題が難化していて得点を獲得できるチームが減っているせいなのか、興味深いところです。

*2:この動画以外にも、面白い動画が盛りだくさんです!

*3:なので、X=1は試行すらしていません。

*4:もちろん、明らかに計算がうまくいかないものは除外して考えます。

*5:あくまで「今回は」です。自由な時間が出来たら深入りしますよ。

*6:kurenaifさんのコードと同じくa=0x5DEECE66D、b=0xBとしました。

*7:ゴメンナサイ。