Attack All Around

今やCTFと競プロばかりだが、トビタテ生のアメリカ留学やTOEFL奮闘記

ImaginaryCTF 2021 Writeup

Crypto全完まであと一問…。悔しいですね。
来週のCrypto CTFがあるので精進します。

Result


f:id:partender810:20210728105530p:plain
result


Writeup

Chicken Caesar Salad 50pts


I remember the good old days when Caesar ciphers were easy…
ファイル:chicken-caesar-salad.txt

ROT18でした。


ictf{wHen_dID_cAEseR_cIphERs_gEt_sO_hARd}



Flip Flops 100pts


Yesterday, Roo bought some new flip flops. Let's see how good at flopping you are.
nc chal.imaginaryctf.org 42011
ファイル:flop.py

DawgCTF 2021 writeup - Attack All Around

こちらと同じ、byte flipping attackですね。

100点問題で難易度高くね!?とひたすらビビっていました。案の定、以降の問題のレベルも高かったです。

from Crypto.Util.number import *
from functools import reduce
from operator import mul
from itertools import combinations
import sys
import socket, struct, telnetlib

# --- common funcs ---
def sock(remoteip, remoteport):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((remoteip, remoteport))
    return s, s.makefile('rw')

def read_until(f, delim='\n'):
    data = ''
    while not data.endswith(delim):
        data += f.read(1)
    return data

    
#HOSTはIPアドレスでも可
HOST, PORT = "chal.imaginaryctf.org", 42011
s, f = sock(HOST, PORT)
print(read_until(f))
print(read_until(f,"solution: "))
sol = input("> ")
s.send(sol.encode()+b"\n")
for _ in range(25-8+1): print(read_until(f))

print(read_until(f,"'gimmeflag'."))
for _ in range(2): print(read_until(f))
print(read_until(f,"> "))
s.send(b"1\n")
pt = "31"*48
print(read_until(f,"(in hex): "))
s.send(pt.encode()+b"\n")
enc = read_until(f).strip()
print("got mes:",enc)

print(read_until(f,"'gimmeflag'."))
for _ in range(2): print(read_until(f))
print(read_until(f,"> "))
s.send(b"2\n")

val = bytes_to_long(b"gimmeflag"+b"\x07"*7)
pt0 = int("31"*16,16)
ct1 = int(enc[:32],16)
key = pt0^ct1
ppt0 = val^key
ans0 = hex(ppt0)[2:]
while len(ans0) < 32: ans0 = "0"+ans0
ans0 += enc[32:64]
print(read_until(f,"(in hex): "))
s.send(ans0.encode()+b"\n")
while True: print(read_until(f))


ictf{fl1p_fl0p_b1ts_fl1pped_b6731f96}



Rock Solid Algorithm 100pts


Something was the wrong size here...
ファイル:secure.txt

nが素因数分解できませんが、e=5と小さいです。Low public exponent attackなのだろう。

c+k×nに5乗根が存在するかどうかbrute forceして、flagを出します。

import gmpy2
from Crypto.Util.number import *

n = 18718668654839418101060350414897678724581774081742102287908765212690862231899547405582997157020093499506177632395430572542600019258424947803591395926472246347413986531437177801754324606200243710836609694453888894668656807471052095014376204102474311740080044776201105722801365112971807912406879483156845216746137339614577267869908065296042390812575960639865867729920434603853708907147465162697098688239587320232595412227310236678367
e = 5
c = 4061448515799106340420739691775850071489215699577921072934942485890519294380069123037340174441242842518682390853378784679825023237216051766738593812159344136064529711265570171627670665806072255545198689928996413238102114126558579154343844959868438278433954975590137693439216155482228025380904377837299357044104373966173149290333194304831238889245126840666444234215617022142380016275718234640045049962318290976661640301222078289152
tmpc = c
while True:
    m,ch = gmpy2.iroot(tmpc,e)
    if ch:
        print(long_to_bytes(int(m)))
        break
    tmpc += n


ictf{3_isnt_th3_0nly_sm4ll_3xp0n3nt}



Lines 150pts


Try to crack my unbreakable™ encryption! I based it off of the Diffie-Helman key exchange!
ファイル:lines.py, out.txt

複雑なことをしているようですが、sを求めるだけです。a, bは求める必要ありません。

enc_msg = s × msg mod p
s = enc_msg × msg^(-1) mod p

これでsが求まりました。

enc_flag = s × flag mod p
flag = enc_flag × s^(-1) mod p

これでflagも求められました。

p = (省略)
encrypt_flag = (省略)
encrypt_msg = (省略)
msg = bytes_to_long(b":roocursion:")

s = (inverse(msg,p)*encrypt_msg)%p

flag = (encrypt_flag*inverse(s,p))%p
print(long_to_bytes(flag))

これを深夜にやり、翌朝残りをやろうとしていました。


ictf{m0d_4r1th_ftw_1c963241}



New Technology 300pts


If it's not Windows New Technology, what else could NT stand for?
ファイル:new_technology.py

朝起きて、オリンピック見ながらのんびりやるかと思ったらもう既に解かれていました。

まあ、とりあえずやってみるかとコード読んでみたけど分からない…。複雑すぎる。

一日考えて分からなかったので、先に解いた方の解法見てみるとびっくり。

実験してみたらkey = pub * pubであることがわかった
未証明

そんなんあり!?と確かめてみたら、本当でした。とりあえず手元で動かしてみて色々試してみるってのは必要ですね。証明できたら、追記します。


ictf{Would_number_theory_be_new_technology?}



Roll it back 300pts


Once you figure out what this is doing, it could be a straight line to the finish.
ファイル:roll_it_back.py

こちらも先に解かれていた問題です。

itertoolsのislice, cycle, gmpy2のpopcountを調べるのが面倒で放置。けど他の問題も分からんし、と渋々調べてみたらそんなに難しくない。

isliceはリストにするような感じのもの。cycleはリストを無限に長くして、中身はサイクルする感じのもの(a[0], a[1], ... a[n-1], a[0], ...)。popcountは、引数が整数値でいくつbitが立っているか個数を返すものです。

ということで、この部分の逆関数を頑張って考えます。

for _ in range(421337):
    flag = (flag >> 1) | ((popcount(flag & T) & 1) << (l - 1))

flagを1bit右シフトしたものとpopcountの結果を(l-1)bits左シフトのORが次のflagになります。flagは常にl (=420)bits以下です。

なので、flagの420bit目が立っていたら、popcount( (前のflag) & T) が1になります。ORの左側の演算では必ず419bits以下になるからですね。flagの420bit目が立っていない場合、popcount( (前のflag) & T) が0になります。

次に、前のflagを1bit右シフトしているので、前のflagの最下位ビットが分かりません。しかし、popcountの条件がある、且つTは奇数なのでpopcountの結果は前のflagの最下位ビットに依存します。つまり、前のflagの最下位ビットが一意的に決まります。

このfor文の逆関数はこのようになります。

binf = bin(flag)[2:]
for i in range(421337):
    if binf[0] == "1":
        # (popcount(flag & T) & 1) == 1
        tmp_flag = binf[1:]
        if (popcount(int(tmp_flag+"1",2) & T) & 1) == 1:
            binf = binf[1:]+"1"
        else:
            assert (popcount(int(tmp_flag+"0",2) & T) & 1) == 1
            binf = binf[1:]+"0"
    else:
        #assert flag.bit_length() == l-1
        # (popcount(flag & T) & 1) == 0
        tmp_flag = binf[1:]
        if (popcount(int(tmp_flag+"1",2) & T) & 1) == 0:
            binf = binf[1:]+"1"
        else:
            assert (popcount(int(tmp_flag+"0",2) & T) & 1) == 0
            binf = binf[1:]+"0"
        #assert flag.bit_length() == l-1
    #print(i, flag.bit_length())
    assert len(binf) == l

Tの値がコードに書いてあるものをそのまま実行していいのか、それともfake_flagのところもちゃんと本物のflagにしないといけないのか判断が出来ず、先に解いたチームメイトにヒントをもらいました。自力じゃないです(笑)


ictf{I_did_not_have_linear_relations_with_that_LFSR}



Primetime 300pts


Alice and Bob are meeting up to watch TV, but they've neglected to invite Elise and Gamal! Elise and Gamal intercepted the network traffic between the two, and managed to extract an encrypted message. Can you help them figure out what TV show Alice and Bob will be watching, and when?
Wrap the decrypted message in the flag format before submitting.
ファイル:primetime.py, output.txt

この問題難しかった分、解けて嬉しかったです!

まず、Elementクラスを見ていきましょう。

class Element:
    def __init__(self, n):
        self.n = Element.reduce(n)

    def __str__(self):
        return str(self.n)

    def __add__(self, other):
        return Element(self.n*other.n)

    def __eq__(self, other):
        return self.n == other.n

    def __mul__(self, n):
        if type(n) == int:
            ret = Element(1)
            for c in "{:0128b}".format(n):
                ret += ret
                if c == '1':
                    ret += self
            return ret
        if type(n) == Element:
            ret = Element(1)
            primelist = list(islice(primes(), ELEMENT_LEN))
            for i, num in enumerate(primelist[::-1]):
                cnt = 0
                temp = n.n
                while gcd(temp, num) > 1:
                    cnt += 1
                    temp //= num
                for c in "{:08b}".format(cnt):
                    ret += ret
                    if c == '1':
                        ret += self
            return ret

    @staticmethod
    def encode(b):
        assert len(b) <= ELEMENT_LEN
        ret = 1
        for i, num in enumerate(primes()):
            if i >= len(b):
                return Element(ret)
            ret *= num**b[i]

    @staticmethod
    def reduce(n):
        if n == 0:
            return 0
        primelist = list(islice(primes(), ELEMENT_LEN))
        for i, num in enumerate(primelist):
            while n % (num**256) == 0:
                n //= num**256
                if num != primelist[-1]:
                    n *= primelist[i+1]
        return n

まず、genを生成したencodeを見ていきます。numには素数が小さいものから入っていきます。[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53] の16個ですね。(素数)bytes列bでのb[i]の積が返されます。

素因数分解するとこうなりました。

gen
53 116
47 112
43 95
41 114
37 48
31 43
29 97
23 114
19 51
17 110
13 51
11 103
7 95
5 51
3 104
2 43

(左側)右側という感じですね。

これを、乱数akey, bkeyでgenとの積を取ります。その積がapub, bpubとなり公開された値となります。

apubをgenで割ったらakeyが出るだろう、とapub%genを見てみると0じゃない。なんで???

Elementクラスでのmulを見てみましょう。

    def __mul__(self, n):
        if type(n) == int:
            ret = Element(1)
            for c in "{:0128b}".format(n):
                ret += ret
                if c == '1':
                    ret += self
            return ret

retにどんどんretを足したり、掛けられる数に該当するselfを足しています。では、addを見てみましょう。

    def __add__(self, other):
        return Element(self.n*other.n)

なんとかけ算しています。足し算がかけ算になるのであったら、かけ算はべき乗になるのでは?と手元でやってみます。Element(23)×11とすると、2311が返ってきました。予想的中!

じゃあなぜ、apub%genが0でないのでしょう。途中でmodとかされているのかなと思ったら、initに罠が。なんと返す時にreduceを通しています。

    @staticmethod
    def reduce(n):
        if n == 0:
            return 0
        primelist = list(islice(primes(), ELEMENT_LEN))
        for i, num in enumerate(primelist):
            while n % (num**256) == 0:
                n //= num**256
                if num != primelist[-1]:
                    n *= primelist[i+1]
        return n

先程の素数の指数が256を越えると、その次の素数の指数が1増えるようです。元の素数の指数は256減るようです。これが原因でした。

つまり、apubはgenのakey乗で、素因数分解して256乗を越えるものはreduce関数でごちゃごちゃさせられます。


途中で繰り上がりのようなものがあって、それを離散対数問題なんて解けるわけ…。ん?繰り上がり?

解法の鍵は繰り上がりでした!

gen, apub, bpubを素因数分解して、その指数を256進数に見立てます。先程のgenで考えると、2の指数が最下桁となりその値は43です。これが256を越えると、その一つ上の位3の指数部分に繰り上がります。また、一番大きい素数53の指数が256を越えた場合は、ただ指数%256になるだけです。つまり、mod 25616 ということです。

このように考えると、genを2乗するとこの256進数を2倍するだけです。つまり、(genの256進数方式) × akey = (apubの256進数方式) mod 25616 となり、akeyを簡単に求められます。

akey = (apubの256進数方式) × (genの256進数方式)^(-1) mod 256^16

これと同様にして、bkeyを求めます。


akey, bkeyが求められたので、sを求めます。これはprimetime.pyにある通りに実行すればOKです。

最後に、mを求めます。

ct及びsを先程と同様に256進数方式で計算します。

ct = m × s
m = (ctの256進数方式) × (sの256進数方式)^(-1) mod 256^16

これでmが求まり、flagが出ました。

from Crypto.Util.number import *

from itertools import islice
from math import gcd
from random import randint

ELEMENT_LEN = 16


def primes():
    num = 2
    while True:
        prime = 1
        for i in range(2, num):
            if num%i == 0:
                prime = 0
        if prime:
            yield num
        num += 1


class Element:
    def __init__(self, n):
        self.n = Element.reduce(n)

    def __str__(self):
        return str(self.n)

    def __add__(self, other):
        return Element(self.n*other.n)

    def __eq__(self, other):
        return self.n == other.n

    def __mul__(self, n):
        if type(n) == int:
            ret = Element(1)
            for c in "{:0128b}".format(n):
                ret += ret
                if c == '1':
                    ret += self
            return ret
        if type(n) == Element:
            ret = Element(1)
            primelist = list(islice(primes(), ELEMENT_LEN))
            for i, num in enumerate(primelist[::-1]):
                cnt = 0
                temp = n.n
                while gcd(temp, num) > 1:
                    cnt += 1
                    temp //= num
                for c in "{:08b}".format(cnt):
                    ret += ret
                    if c == '1':
                        ret += self
            return ret

    @staticmethod
    def encode(b):
        assert len(b) <= ELEMENT_LEN
        ret = 1
        for i, num in enumerate(primes()):
            if i >= len(b):
                return Element(ret)
            ret *= num**b[i]

    @staticmethod
    def reduce(n):
        if n == 0:
            return 0
        primelist = list(islice(primes(), ELEMENT_LEN))
        for i, num in enumerate(primelist):
            while n % (num**256) == 0:
                n //= num**256
                if num != primelist[-1]:
                    n *= primelist[i+1]
        return n


gen = (省略)
apub = (省略) 
bpub = (省略)
ct = (省略)
Primes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53]
assert len(Primes) == 16
def frame(x):
    ret = []
    for pri in Primes[::-1]:
        cnt = 0
        while True:
            if x%pri == 0:
                cnt += 1
                x //= pri
            else:
                break
        print(pri,cnt)
        ret.append(cnt)
    assert x == 1
    return ret

#gen, apub, bpubを素因数分解
print("gen")
gg = frame(gen)
print()
print("apub")
aa = frame(apub)
print()
print("bpub")
bb = frame(bpub)

#256進数方式で値にする
Gen = 0
for G in gg:
    Gen *= 256
    Gen += G

print(Gen)
Apub = 0
for A in aa:
    Apub *= 256
    Apub += A
Bpub = 0
for B in bb:
    Bpub *= 256
    Bpub += B
Gen_inv = inverse(Gen,pow(256,16))
assert (Gen_inv*Gen)%pow(256,16) == 1

#akey, bkeyを求める
akey = (Apub*Gen_inv)%pow(256,16)
bkey = (Bpub*Gen_inv)%pow(256,16)
print(akey)
print(bkey)

gEn = Element.encode(b"+h3_g3n3ra+0r_pt")
s = gEn*akey*bkey
print(s)
# sの値
S = (省略)
ss = frame(S)
print(ss)
CT = frame(ct)
print(CT)

Ss = 0

for tmps in ss:
    Ss *= 256
    Ss += tmps

Ct = 0
for tmpct in CT:
    Ct *= 256
    Ct += tmpct

s_inv = inverse(Ss,pow(256,16))
assert (s_inv*Ss)%pow(256,16) == 1
m = Ct*s_inv
print(m%pow(256,16))
print(long_to_bytes(m%pow(256,16))[::-1])


ictf{M1st3rRob0t_2045}



Problems&solver

GitHub - ksbowler/ImaginaryCTF2021