くれなゐの雑記

例を上げて 自分で手を動かして学習できる入門記事を多めに書いています

InterKosenCTF 2019 Writeup

InterKosenCTF Writeup

interKosenCTFに参加しました! チームR19として、また僕自身がこういう大会にでるのは2回目ですね。

今回もCrypto解くマンとして参加しましたが、1日目で全完できたので、2日目はreversingとWeb問を見ていました。

チームとしては9位を取ることができました。今後は他の人の負担を減らすためにrevとWebも伸ばしたいですね。

f:id:kurenaif:20190813000154p:plain

f:id:kurenaif:20190813000617p:plain

Kurukuru Shuffle

以下のソースコードが与えられて、暗号文も与えられます。

from secret import flag
from random import randrange


def is_prime(N):
    if N % 2 == 0:
        return False
    i = 3
    while i * i < N:
        if N % i == 0:
            return False
        i += 2
    return True


L = len(flag)
assert is_prime(L)

encrypted = list(flag)
k = randrange(1, L)
while True:
    a = randrange(0, L)
    b = randrange(0, L)

    if a != b:
        break

i = k
for _ in range(L):
    s = (i + a) % L
    t = (i + b) % L
    encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
    i = (i + k) % L

encrypted = "".join(encrypted)
print(encrypted)

ソースコードを読解すると、abkが実行時に定数として決まり、

for _ in range(L):
    s = (i + a) % L
    t = (i + b) % L
    encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
    i = (i + k) % L

の部分でflagの文字をひたすら入れ替えています。 このflagの鍵は、 abk の3つの値なのですが、これらの値は高々 L^3 = 148877 程度なので、現実的な時間で復号可能です。 というわけで、abkの取りうる値を全探索して、以下のコードを書くと復号可能です。

flag = "1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm"
L = len(flag)

for k in range(1, L):
    for a in range(L):
        for b in range(L):
            if a == b:
                continue
            encrypted = list(flag)
            i = k
            for _ in range(L):
                s = (i+a)%L
                t = (i+b)%L
                encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
                i = (i+k)%L
            encrypted = "".join(encrypted)
            print(encrypted)

E_S_P

easyだと問題文に出ている気がしたので、2問目にときました。 特にハマることなく解けたので、運が良かったです。(1日目午前中で解けました。)

暗号生成アルゴリズムとしては、普通のRSA暗号ですね。 強いて言うなれば e が小さいのでPlainなRSAだとLow Public Exponent Attackが刺さるかもしれませんが、今回は平文が十分に長いので大丈夫です。

以下のような生成アルゴリズムが与えられます。(kurenaifがデバッグするためにいろいろいじってます)

from Crypto.Util.number import *
### from secret import flag, yukko
import re

yukko = "Yukko the ESPer: My amazing ESP can help you to get the flag! -----> "
flag = "KosenCTF{" + "0"*29 + "}"

assert re.match(r"^KosenCTF{.+}$", flag)

Nbits = 1024
p = getPrime(Nbits)
q = getPrime(Nbits)
n = p * q
e = 5
m = bytes_to_long((yukko + flag).encode())
c = pow(m, e, n)


print("N = {}".format(n))
print("e = {}".format(e))
print("c = {}".format(c))
print("m = {}".format(hex(m)))

print(yukko + "the length of the flag = {}".format(len(flag)))
print(yukko+flag)

yukko = "Yukko the ESPer: My amazing ESP can help you to get the flag! -----> "
flag = "KosenCTF{" + "\x00"*29 + "}"
print(hex(bytes_to_long((yukko+flag).encode())))
flag = "KosenCTF{" + "0"*29 + "}"
print(hex(bytes_to_long((yukko+flag).encode())))

今回は平文の前半部分がわかっているので、Coppersmith's Attackが有効です。 ももいろテクノロジー様の記事が、ソースコード付きでわかりやすいですね! しかしながら、知っていなければならない上位bitの個数が足りないため、復号ができません。 Yukko the ESPer: My amazing ESP can help you to get the flag! -----> KosenCTF{ までが既知文字列とすると、2,3byteくらい足りないので、その部分は全探索をします。

全探索を高速化する工夫点として、ASCIIの文字だけに絞って探索するようにしているため、ソースコードがやや複雑になっています。

from Crypto.Util.number import *
import os

### yukko = "Yukko the ESPer: My amazing ESP can help you to get the flag! -----> "
### flag = "KosenCTF{" + "0"*29 + "}"
### print(yukko + "the length of the flag = {}".format(len(flag)))
### print(bytes_to_long((yukko+flag).encode()))
### print(yukko+flag)

### 既知文字列を16進数表記にしたもの
base = "59756b6b6f207468652045535065723a204d7920616d617a696e67204553502063616e2068656c7020796f7520746f206765742074686520666c616721202d2d2d2d2d3e204b6f73656e4354467b"

def reverse(num):
    s = ""
    cnt = 0
    while num > 0:
        a = num % 95 + 32
        s += format(a, '02x')
        num = num // 95
        cnt += 1
    s += "0" * (60-cnt*2)
    return s, cnt

s = ""
i = 0
while True:
    s, byte = reverse(i)
    i += 1
    m = int(base + s, 16)

    N = 11854673881335985163635072085250462726008700043680492953159905880499045049107244300920837378010293967634187346804588819510452454716310449345364124188546434429828696164683059829613371961906369413632824692460386596396440796094037982036847106649198539914928384344336740248673132551761630930934635177708846275801812766262866211038764067901005598991645254669383536667044207899696798812651232711727007656913524974796752223388636251060509176811628992340395409667867485276506854748446486284884567941298744325375140225629065871881284670017042580911891049944582878712176067643299536863795670582466013430445062571854275812914317
    e = 5
    c = 4463634440284027456262787412050107955746015405738173339169842084094411947848024686618605435207920428398544523395749856128886621999609050969517923590260498735658605434612437570340238503179473934990935761387562516430309061482070214173153260521746487974982738771243619694317033056927553253615957773428298050465636465111581387005937843088303377810901324355859871291148445415087062981636966504953157489531400811741347386262410364012023870718810153108997879632008454853198551879739602978644245278315624539189505388294856981934616914835545783613517326663771942178964492093094767168721842335827464550361019195804098479315147

    beta = 1
    epsilon = beta^2/7

    nbits = N.nbits()
    kbits = (60*4 - byte*8)
    mbar = m & (2^nbits-2^kbits)
    print "upper %d bits (of %d bits) is given" % (nbits-kbits, nbits)

    PR.<x> = PolynomialRing(Zmod(N))
    f = (mbar + x)^e - c

    print hex(m)
    for x0 in f.small_roots(X=2^kbits, beta=1):
        print long_to_bytes(mbar + x0)
        exit()

flag_ticket

普通のPaddingエラーがあるCBCのブロック暗号なので、パディングオラクル攻撃を使って平文に戻したり、暗号文を改ざんすることが可能です。 Padding Oracle AttackによるCBC modeの暗号文解読と改ざん の知識がある前提の元でこの記事は執筆します。

暗号化する平文は以下のもような形式になります。 is_hit をTrueにして、サーバーに読み込ませたら勝ちです。

{"is_hit": False, "number": number}

ブロックの書き換えは、最後のブロックの変え方しか知らないので、最後のブロックにないis_hitを直接変更することは難しいです。 そこで、以下のように末尾に is_hit を付けて試しに手元のコードで読み込ませると、しっかり誤認してくれることがわかりました。

{"is_hit": False, "number": number,"is_hit":True}

文字を付け足してもブロックのサイズは変わらないようにnumberの文字数を調整して、以下のコードを使用すると、誤認させたい暗号文が生成できます。 あとはこれをcookieに流し込めば、OKです。

from Crypto.Cipher import AES
from Crypto.Util import Padding
from Crypto import Random
import json
from binascii import hexlify, unhexlify
import sys
from Crypto.Util.number import long_to_bytes, bytes_to_long
import copy
import requests

key =  b'\xa0\xda\xd8\xa3o\xd0\xed:g\x94\xd3\x8d\xe5\xb4\x13\xea'
def decoder(num):
    cipher = unhexlify(num)
    if len(cipher) < AES.block_size * 2:
        resp.text = "ERROR: cookie should be iv(16) + cipher(16*n)"
        exit(1)
    iv, cipher = cipher[: AES.block_size], cipher[AES.block_size :]
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = aes.decrypt(cipher)
    print(data)
    data = Padding.unpad(data, AES.block_size)
    data = json.loads(data.decode())
    ### aes = AES.new(key, AES.MODE_CBC, iv)
    ### data = Padding.unpad(aes.decrypt(cipher), AES.block_size).decode()
    ### data = json.loads(data)
    ### resp.html = api.template("result.html", flag=flag, data=data)
    return data


def encoder(num):
    key =  b'\xa0\xda\xd8\xa3o\xd0\xed:g\x94\xd3\x8d\xe5\xb4\x13\xea'
    iv = Random.get_random_bytes(16)
    data = json.dumps({"is_hit": False, "number": num}).encode()
    data = Padding.pad(data, AES.block_size)
    iv = Random.get_random_bytes(AES.block_size)
    aes = AES.new(key, AES.MODE_CBC, iv)

    res = hexlify(iv + aes.encrypt(data)).decode()
    return res

def block2String(blocks):
    s = ""
    for block in blocks:
        for num in block:
            s += format(num, "02x")
    return s

def request(s):
    cookies = dict(result=s)
    r = requests.get('http://crypto.kosenctf.com:8000/result', cookies=cookies)
    ### print(r.text)
    if r.text == "ERROR: invalid cookie":
        assert False
    if r.text == "ERROR: unicode decode error":
        return 1
    if r.text == "ERROR: json decode error":
        return 1
    if r.text == "ERROR: padding error":
        return 0
    return 2
    ### try:
    ###     res = decoder(s)
    ###     backNum = num
    ###     return 2
    ### except TypeError as e:
    ###     return 0
    ### except UnicodeDecodeError:
    ###     f = True
    ###     return 1
    ### except json.JSONDecodeError as e:
    ###     f = True
    ###     return 1
    ### except ValueError as e:
    ###     "none"
    ###     return 0

C = []
M = []
Corg = []

### cipher = "a0dad8a36fd0ed3a6794d38de5b413ea2777446a3587a32b74432bac38014d9d070bf1451634350c6d685babf3913493c62a1fe616fec8b996d073a27cc17a692c750e22cf67b750dd5424d652aad715" 
cipher = "f8999c402717a871f501786986eebe0a6646dc3abcc2aa2336006eb9f97d23d01026308559de73d87b1d3b1fafcf7e7802484d81845249b9f943588f36f7af623f7b91083d3d97556b589adca33afdf0" ### サーバーが生成した元の暗号文
### cipher = "a0dad8a36fd0ed3a6794d38de5b413ea2777446a3587a32b74432bac38014d9d5bdbdd8f0aa33fd9c4eacdb21fe4fec884ab8f50e317a5cfd969cb1206f8682b" // 誤認させたいやつ
### m = "7b2269735f686974223a2066616c73652c20226e756d626572223a203132337d10101010101010101010101010101010" 
mdash = "7b2269735f686974223a2066616c73652c20226e756d626572223a20223132333435363738393031323334353637222c2269735f686974223a747275657d0202"### 誤認させたいやつ

while len(cipher) > 0:
    nxt, cipher = cipher[: AES.block_size*2], cipher[AES.block_size*2 :]
    block = []
    for i in range(0, len(nxt), 2):
        block.append(int(nxt[i:i+2], 16))
    C.append(copy.copy(block))
    Corg.append(copy.copy(block))

Mdash = []
Mdash.append([])
while len(mdash) > 0:
    nxt, mdash = mdash[: AES.block_size*2], mdash[AES.block_size*2 :]
    block = []
    for i in range(0, len(nxt), 2):
        block.append(int(nxt[i:i+2], 16))
    Mdash.append(copy.copy(block))

block_size = len(C)-1
ans = [[0]*16 for i in range(block_size)]
ans_block = []
for block_num in range(block_size-1,-1,-1):
    for i in range(len(C)):
        C[i] = copy.copy(Corg[i])
    print("block_num:", block_num)
    Cdash = [0]*16
    ans_block = [0]*16
    for i in range(15, -1, -1):
        print("i:" , i)
        backNum = -1
        for num in range(256):
            C[block_num][i] = num
            f = request(block2String(C[:block_num+2]))
            if f == 2:
                backNum = num

            if f == 1:
                print("found!")
                print(num)
                Cdash[i] = num
                ### ans[block_num][i] = (16-i) ^ Cdash[i] ^ Corg[block_num][i]
                ### ans_block[i] = (16-i) ^ Cdash[i] ^ Corg[block_num][i]
                ans[block_num][i] = Cdash[i] ^ (16-i) ^ Mdash[block_num+1][i]
                ans_block[i] = Cdash[i] ^ (16-i) ^ Mdash[block_num+1][i]
                print(block2String([ans_block]))
                for j in range(i,16):
                    C[block_num][j] = Cdash[j] ^ (16-j) ^ (17-i)

                print("ans:", block2String(ans))
                break

        if not f and backNum != -1:
            num = backNum
            print("found!")
            print(num)
            Cdash[i] = num
            ### ans[block_num][i] = (16-i) ^ Cdash[i] ^ Corg[block_num][i]
            ### ans_block[i] = (16-i) ^ Cdash[i] ^ Corg[block_num][i]
            ans[block_num][i] = Cdash[i] ^ (16-i) ^ Mdash[block_num+1][i]
            ans_block[i] = Cdash[i] ^ (16-i) ^ Mdash[block_num+1][i]
            print(i)
            print(block2String([ans_block]))
            for j in range(i,16):
                C[block_num][j] = Cdash[j] ^ (16-j) ^ (17-i)

            print("ans:", block2String(ans))
        Corg[block_num] = ans_block

S = block2String(ans) + block2String([C[block_size]])
print(S + block2String([C[-1]]))

pascal homomorphicity

これはRSAに見えますが、 自作暗号ですね paillier暗号というらしいですRSAのeに当たる部分が平文です。 ここで、式を見ていきましょう。

(N+1)^x mod N^2

ここの xflag にしたものが最初に与えられ、その後任意回数 x を好きに変えて暗号文を生成できます。 この時、実行を終了するまで N は共通です。

で暗号文を生成しています。 ここで、 (N+1)^x を展開して mod N2 を取ると、N2以上の項はN2で割り切れるため、

x N + 1 mod N^2

になります!つまり、 x を1変えれば、暗号文は N 変わることになります。 これで容易に N が特定できますね。 Nを特定したら、暗号文をcとして、

(c - 1)/N

でフラグを復元できます。

コード書く必要がなかったので書いてないです。

hugotto

782086ピクセルの画像が与えられ、 r,g,b のうちどれかの最終bitに1bitずつにフラグの情報が順に埋め込まれています。フラグの情報は何ビットかわかりませんが、埋め込んでいる途中に終端まで行くとまだ最初のbitを埋め込み始めます。

まずは文字列の長さを求めます。文字列は KosenCTF{ から始まることは既知なので、最初のある程度のbit数は既知として扱うことができます。 イメージとしては

KosenCTF{?????????????????KosenCTF{?????????????????KosenCTF{?????????????????KosenCTF{?????????????????KosenCTF{?????????????????

のように繰り返しになっているはずなので、このKとKの間を引き算してやると文字列の長さが求まるはずです。 よって、すべてのbitを二重ループ的に操作して、この KosenCTF{ になりうる開始位置を調べます。

開始位置の調べ方としては、0であるはずなのに、 rgbの最終bitが rgb = (1,1,1) であったり、 1であるはずなのに rgb = (0,0,0) であったりするケースを弾いていって、残ったものがそうなります。

誤認識の可能性があるので、最も多いものをフラグ長として採用しました。 以下のようになったのでフラグのbitの長さは544ですね。

{544: 1272, 104: 1, 13: 2, 427: 1, 150: 1, 21: 2, 88: 6, 285: 1, 456: 5, 130: 1, 14: 1, 3: 3, 397: 2, 520: 4, 24: 9, 160: 5, 384: 3, 72: 13, 472: 12, 336: 2, 208: 3, 300: 2, 244: 1, 224: 5, 320: 4, 106: 1, 418: 3, 20: 1, 152: 3, 392: 4, 56: 3, 16: 4, 8: 8, 216: 5, 328: 4, 539: 1, 5: 5, 144: 9, 11: 4, 203: 1, 44: 1, 100: 4, 400: 5, 245: 2, 299: 2, 312: 3, 232: 3, 376: 4, 168: 4, 280: 2, 264: 3, 93: 1, 451: 1, 32: 1, 512: 1, 352: 8, 192: 10, 176: 2, 368: 1, 126: 2, 528: 1, 464: 2, 349: 1, 179: 3, 463: 1, 81: 1, 146: 1, 230: 2, 36: 2, 278: 1, 222: 1, 322: 1, 61: 1, 475: 1, 240: 2, 304: 2, 128: 3, 416: 2, 218: 2, 326: 2, 444: 2, 436: 1, 108: 2, 283: 2, 261: 2, 344: 2, 200: 1, 288: 6, 256: 7, 129: 1, 415: 1, 69: 1, 459: 1, 541: 1, 296: 7, 248: 7, 504: 3, 40: 5, 272: 2, 365: 2, 238: 1, 306: 1, 432: 3, 112: 4, 123: 1, 421: 1, 408: 2, 325: 1, 170: 1, 374: 1, 38: 1, 506: 1, 424: 1, 120: 2, 35: 1, 509: 1, 162: 1, 382: 1, 373: 1, 171: 1, 136: 2, 460: 1, 84: 1, 508: 1, 96: 1, 448: 1, 533: 1, 82: 1, 462: 1, 396: 2, 148: 2, 184: 2, 360: 2, 48: 1, 536: 1, 403: 1, 141: 1, 488: 1, 12: 2, 260: 1, 76: 2, 468: 2, 363: 1, 186: 1, 358: 1, 426: 1, 118: 1, 9: 1, 2: 2, 4: 3, 491: 1, 198: 1, 346: 1, 147: 1, 59: 1, 279: 1, 265: 1, 80: 1, 431: 1, 1: 1, 429: 1, 115: 1, 386: 1, 134: 1, 47: 1, 281: 1, 402: 1, 142: 1, 132: 1, 412: 1, 28: 1, 99: 1, 263: 1, 182: 1}

フラグの長さがわかったので、その分の配列を用意して、rgbの最終bitが (0,0,0) になっているものか (1,1,1) になっているもの見つけて、その値を代入していけばフラグを復元できます。

from PIL import Image
from datetime import datetime
import tarfile
import sys
from Crypto.Util.number import long_to_bytes

import random

random.seed(int(datetime.now().timestamp()))


img = Image.open("./steg_emiru.png")
new_img = Image.new("RGB", img.size)

w, h = img.size

### 既知文字列のbit配列
prefix = []
for c in "KosenCTF{":
    for i in range(8):
        prefix.append((ord(c) >> i) & 1)

print(prefix)

### 配列データへと変換
rgb = []
for x in range(w):
    for y in range(h):
        r, g, b = img.getpixel((x, y))
        flag = [False]*2
        r = (r & 0x01)
        g = (g & 0x01)
        b = (b & 0x01)
        ### 0,1のどちらが立っているか
        flag[r] = True
        flag[g] = True
        flag[b] = True
        rgb.append(flag)

### ピクセル数
print(len(rgb))

### 既知文字列が現れる可能性がある位置
startList = []
for start in range(len(rgb)-len(prefix)):
    f = True
    for i in range(len(prefix)):
        if not rgb[start+i][prefix[i]]:
            f = False
            break
    if f:
        startList.append(start)

### 文字列の開始位置と思われる位置を引き算して、文字列の長さを集計する。
cnt = {} ### {開始位置の差分: 出現回数}
for i in range(len(startList)-1):
    diff = startList[i+1] - startList[i]
    cnt[diff] = cnt.setdefault(diff, 0) + 1
print(cnt)

### 最も多い長さがフラグの長さと仮定する
ma = [-1, -1]
for k, v in cnt.items():
    if ma[1] < v:
        ma = [k, v]

n = ma[0] ### 文字列長さ

### (0,0,0) か (1,1,1) を見つけて、確定したものから代入していく。
res = [-1]*n
for i in range(len(rgb)):
    ### どっちか確定したbitはもうそれで確定
    if rgb[i][0] == True and rgb[i][1] == False:
        res[i%n] = 0
    if rgb[i][0] == False and rgb[i][1] == True:
        res[i%n] = 1

print(res)

### 文字列化
ans = ""
for start in range(0, len(res), 8):
    c = 0
    for i in range(8):
        c += (res[start+i] << i)
    ans += chr(c)
print(ans)

Temple of Time

pcap ファイルが与えられるので、wiresharkでみるとすごい量のリクエストが見えます。 wiresharkで見るときは、httpリクエストで絞るといいことが多いので、とりあえず絞って、ついてにcsv出力をしておきます。

"No.","Time","Source","Destination","Protocol","Length","Info"
"12","5.629457982","150.95.139.51","192.168.1.2","HTTP","92","POST /login.php HTTP/1.1  (application/x-www-form-urlencoded)"
"14","5.631738584","192.168.1.2","150.95.139.51","HTTP","540","HTTP/1.1 302 Found  (text/html)"
"22","5.650384252","150.95.139.51","192.168.1.2","HTTP","428","GET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D48%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1 "
"24","5.652948109","192.168.1.2","150.95.139.51","HTTP","683","HTTP/1.1 200 OK  (text/html)"

どうやらSQLっぽいものがURLにひっついてますね。 デコードして内容を整理してみましょう

OR(
    SELECT(
        IF(
            ORD(
                SUBSTR(
                    (SELECT+password+FROM+Users+WHERE+username='admin'),
                    x,  ### note: 実際にはxではなく整数
                    1
                )
            )=y, ### note: 実際にはyではなく整数
            SLEEP(1),
            ''
        )
    )
)#

どうやら、パスワードの x 文字目が y であれば SLEEP(1) するみたいです。 なので、リクエストが帰ってくるのに時間がかかっていたら、パスワードのx文字目はyであることがわかりますね。

先程書き出したwiresharkcsvから、適当にしきい値を決めて次のリクエストが帰ってくるまでの時間を取り、それが一定以上であれば絞り込むスクリプトを書きました。

import csv
import urllib.parse

f = open("./packet", "r")

reader = csv.reader(f)

next(reader) ### ['No.', 'Time', 'Source', 'Destination', 'Protocol', 'Length', 'Info']
contents = []
for row in reader:
    time_start = float(row[1])
    content = urllib.parse.unquote(row[-1])
    ### print(time_start, content)
    if "SELECT" in content:
        row = next(reader)
        time_end = float(row[1])
        diff = time_end - time_start
        if diff > 0.01: ### しきい値
            contents.append(content)

chars = []
for content in contents:
   chars.append((int(content.split(',')[1]), int(content.split('=')[3].split(',')[0])))

ma = 0
for char in chars:
    ma = max(char[0], ma)

res = [ord('?')]*ma
for char in chars:
    res[char[0]-1] = char[1]

print("".join(map(chr, res)))

passcode

うちのrev担当からwindowsで辛いという声を頂き、やることがなかったので参戦。

.NET系はdnSpyを使うと便利なので、使う。 Correct あたりで探索して、

f:id:kurenaif:20190813000957p:plain

        private void shuffle()
        {
            int num = 0;
            foreach (int num2 in this.state)
            {
                num = num * 10 + num2;
            }
            Random random = new Random(num);
            for (int i = 0; i < 9; i++)
            {
                int index = random.Next(9);
                int value = this.vars[i];
                this.vars[i] = this.vars[index];
                this.vars[index] = value;
            }
        }

        // Token: 0x06000004 RID: 4 RVA: 0x000021CC File Offset: 0x000003CC
        private void push(int index)
        {
            this.indices.Add(index);
            this.state.Add(this.vars[index]);
            this.shuffle();
            if (Enumerable.SequenceEqual<int>(this.state, this.correct_state))
            {
                string text = "";
                for (int i = 0; i < this.indices.Count / 3; i++)
                {
                    text += ((char)(this.indices[i * 3] * 64 + this.indices[i * 3 + 1] * 8 + this.indices[i * 3 + 2])).ToString();
                }
                MessageBox.Show(text, "Correct!");
            }
        }

前回の入力をシード値に、ボタンを押されたときの数字をシャッフルしているらしい。 次に押す数字が、correct_state のものであればよい。 ボタンをポチポチ押す必要もなく、以下のソースコードで再現する。

using System;
using System.Collections.Generic;
using System.Linq;
<200b>
namespace ConsoleApp3
{
    internal class Program
    {
        private List<int> correct_state;
        private List<int> state;
        private List<int> vars;
        private List<int> indices;
<200b>
        private void shuffle()
        {
            int num = 0;
            foreach (int num2 in this.state)
            {
                num = num * 10 + num2;
            }
            Random random = new Random(num);
            for (int i = 0; i < 9; i++)
            {
                int index = random.Next(9);
                int value = this.vars[i];
                this.vars[i] = this.vars[index];
                this.vars[index] = value;
            }
        }
<200b>
        public void f()
        {
            this.vars = new List<int>
            {
                1,
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9
            };
            this.correct_state = Enumerable.ToList<int>(Enumerable.Select<char, int>("231947329526721682516992571486892842339532472728294975864291475665969671246186815549145112147349184871155162521147273481838", (char c) => (int)(c - '0')));
            this.indices = new List<int>();
            this.state = new List<int>();
<200b>
            for (int cnt = 0; cnt < this.correct_state.Count(); cnt++)
            {
                //次押すやつ
                int target = this.correct_state[cnt];
                // index: ボタン番号
                for (int index = 0; index < this.vars.Count(); index++)
                {
                    // ボタンを押す
                    if (this.vars[index] == target)
                    {
                        this.indices.Add(index);
                        this.state.Add(this.vars[index]);
                    }
                }
                this.shuffle();
                if (Enumerable.SequenceEqual<int>(this.state, this.correct_state))
                {
                    string text = "";
                    for (int i = 0; i < this.indices.Count / 3; i++)
                    {
                        text += ((char)(this.indices[i * 3] * 64 + this.indices[i * 3 + 1] * 8 + this.indices[i * 3 + 2])).ToString();
                    }
                    Console.WriteLine(text);
                }
            }
        }
<200b>
        private static void Main(string[] args)
        {
            Program p = new Program();
            p.f();
        }
    }
}