Attack All Around

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

WaniCTF'21-spring Crypto Writeup


大阪大学のCTF サークル Wani Hackaseさんが開催してくださりましたWaniCTFのWriteupです!初心者向けのCTFということもあり、かなり面白かったです。

https://score.wanictf.org/#/

今回はCrypto問について書いていきます。他の分野も少しだけ解けたのですが、もう少し出来るようになりたいですね。

Crypto問のOUCSは解き方に加え証明も書いているので、是非とも読んでいただければと思います。分かりやすく書いたつもりですが、不明瞭な点がありましたら、コメントしていただけますと嬉しいです。

f:id:partender810:20210501004041p:plain
全完達成!


Simple conversion [Begineer]

戻し方を忘れました…
ファイル:cry-simple-conversion.zip

convert.pyを見るとバイト列を整数にしているようですね。とりあえずlong_to_bytesしてみたらflagが出てきました。little endianならto_bytesを使えば良さそうですね。

from Crypto.Util.number import *
x = 709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229
print(long_to_bytes(x))

FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}


Easy [Easy]

手始めに
ファイル:cry-easy.zip

encrypt.pyを見ると、[A-Z]を[0-25]に対応させてそれをxに入れ、a*x+b mod 26の式で暗号化しています。それの結果がoutput.txtに入っています。

outputの先頭4文字は"FLAG"が暗号化された文字列なので、そこからa, bを求めていきます。

  • "H" = a×"F" + b
  • "L" = a×"L" + b
  • "I" = a×"A" + b
  • "M" = a×"G" + b

となるa, bを求めていきますが、複雑なことはしません。mod 26の世界なのでa, b共に0~25でbrute forceすればOKです。

enc = "HLIM{OCLSAQCZASPYFZASRILLCVMC}"
test = "FLAG"
ans = []
for i in range(len(test)):
    temp = []
    for a in range(26):
        for b in range(26):
            t = encrypt(test[i],a,b)
            if t == enc[i]:
                temp.append([a,b])

    ans.append(temp)


for i in range(len(ans[0])):
    #ans[0][i]が他にもあるか探索
    check = True
    for j in range(1,len(ans)):
        ch = False
        for k in range(len(ans[j])):
            if ans[0][i] == ans[j][k]:
                ch = True
                break
        if not ch:
            check = False
            break
    if check:
        print(ans[0][i])

上記の4つの式の内、一番上の式に当てはまるa, bはans[0]に記憶し、その下の式はans[1], ans[2], ans[3]に入っています。ans[0][i]がans[1~3]にも含まれているかをcheckします。もっといいコードありそう。

コードを走らせると、a=5, b=8と一意に決まりました。あとはencrypt関数の逆を作ればOKです。

def decrypt(ct,a,b):
    pt = ""
    for x in ct:
        if ord("A") <= ord(x) <= ord("Z"):
            x = ord(x) - ord("A")
            for i in range(26):
                if x == (a*i+b)%26:
                    x = chr(i + ord("A"))
                    pt += x
                    break
        else: pt += x
    return pt

FLAG{WELCOMETOCRYPTOCHALLENGE}


Extra [Normal]

いつものRSA
ファイル:cry-extra.zip

RSAの公開鍵(e,N)に加えて、M = 2×p+qが公開されています。これは解と係数の関係を使って解けそうです。Mがp+qならx2-N×x+Mの解がp, qになりますが、M = 2×p+qなので2×p, qが解になる二次方程式を立てます。二つの解の積2×p×qはNを2倍すれば手に入るので、解と係数の関係を利用してp, qを求めます。

二次方程式における解と係数の関係 | 高校数学の美しい物語

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

N = (省略)
M = (省略)
e = 65537
c = (省略)
n = 2*N
ro, check = gmpy2.iroot(M*M-4*n,2)
assert check
q = (M-ro)//2
assert math.gcd(N,q) != 1
p = N//q
phi = (p-1)*(q-1)
d = inverse(e,phi)
m = pow(c,d,N)
print(long_to_bytes(m))

FLAG{@n_ex7ra_param3ter_ru1n5_3very7h1n9}


Can't restore the flag? [Hard]

ちりつもですよ
nc crt.cry.wanictf.org 50000
ファイル:cry-cant-restore-the-flag.zip

server.pyを見るとbytes_to_longしたflagに対して、300以下の数値を送るとその数で割った余りを教えてくれます。これはもう中国剰余定理ですね。2~300を全て送ってその余りを記憶してライブラリで解きます。300! > 10103 なので確実にflagを復元できます。

from sympy.ntheory.modular import crt
mod = []
val = []
for i in range(2,301):
    print(i)
    read_until(f,"Mod > ")
    s.send(str(i).encode()+b"\n")
    recv = int(read_until(f).strip())
    mod.append(i)
    val.append(recv)
m,_ = crt(mod,val)
print(long_to_bytes(m))

FLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}


OUCS [Very hard]

OUによるHomomorphicなCryptoSystemです
nc oucs.cry.wanictf.org 50010
ファイル:cry-oucs.zip

この問題解けたの嬉しかったですね。

ではまず暗号化について。n = p×p×qとしgp-1 mod p2 != 1であるgを選び、h=gn mod nとした(n,g,h)が公開鍵となります。plaintextをmとすると暗号文cは、c=(gm mod n × hr mod n) mod nとなります(r : 乱数)。hの条件より上記の式は

c = (gm mod n × (gn)r mod n) = gm+n×r mod n ・・・①

となります。

次に復号について。なぜserver.pyのコードで復号できるのでしょうか。

a = ( (cp-1 mod p2) - 1) // p ・・・②
b = ( (gp-1 mod p2) - 1) // p ・・・③
m = a × b-1 mod p ・・・④

まず、フェルマーの小定理 ( xp-1 mod p = 1) を使います。cp-1 = k'×p+1 (k' : 整数)となるので、k' != pである限り、②より(cp-1 mod p2) = k'×p+1となり、a=k'となります。同様に、gp-1 = k×p+1 (k : 整数)となるので、b=kとなります。ここで④より、k'/k = mとなります。故に、k'=k×mと置くことが出来ます。

k×m = ( (cp-1 mod p2) - 1) // p
k×m×p+1 = cp-1 mod p2 ・・・⑤

⑤を証明していきます。式①と式⑤に代入しています。n = p×p×qであるので、mod nの式をmod p2 の式に落とし込むことが出来ます。

k×m×p+1 = (gm+n×r mod n)p-1 mod p2 = (gm+n×r )p-1 mod p2
= g (m+n×r)×(p-1) mod p2

となります。さらに展開していきます。

k×m×p+1 = g (m+n×r)×(p-1) mod p2 = gm×(p-1) × gn×r×(p-1) mod p2

ここで、オイラーの公式( x φ(n) = 1 mod n )を使います。φ(p2) = p×(p-1)となります(p2と互いに素ではないp2以下の自然数は、p, 2p, 3p, ... , (p-1)p, pp のp個)。

n×r×(p-1) = p×p×q×r×(p-1) = A×p×(p-1)

であるので、gn×r×(p-1) mod p2 = 1です。故に、k×m×p+1 = gm×(p-1) となります、これをgp-1 = k×p+1 (k : 整数) が出てくるようにします。

k×m×p+1 = gm×(p-1) mod p2 = gp-1m mod p2 = (k×p+1)m mod p2

となります。ここで二項係数とパスカルの三角形を考えると、

パスカルの三角形 - Wikipedia

(k×p+1)m = (k×p)m + m×(k×p)m-1 + mC2 × (k×p)m-2 + ... + mC(m-2) × (k×p)2 + m×(k×p) + 1

となります。これをmod p2すると、後ろ2項以外はp2が含まれるので0となるのでm×(k×p) + 1となり、式⑤が証明されました。

では、これを使ってどのようにmを求めればいいのでしょうか。ここまで長い証明を読んで下さった方には申し訳ないですが、答えはあっさりです。
a = ( (cp-1 mod p2) - 1) // p = k×mとなるのに対しa' = 2×k×mとなるa'があれば、
a'×b-1 mod p2= 2×m mod p2となり、復号結果がflagと異なるので復号後その値をサーバが教えてくれるはずです。2×m < p となるのは希望的観測です。このくらいは耐えるだろう。

c = gm+n×r mod n より、c2 mod n = (gm+n×r mod n) × (gm+n×r mod n) = g2×(m+n×r) mod n

c' = c2 mod nとして復号してもらうと、

a = ( (c'^(p-1) mod p2) - 1) // p = ( (g2×(m+n×r) )p-1 mod p2 - 1) // p = (g2×m×(p-1) mod p2 - 1) // p

b = ( (gp-1 mod p2) - 1) // p

gp-1 = k×p+1 (k : 整数) より、

b = ( ( k×p+1 mod p2) - 1) // p = (k×p) // p = k

a = (g2×m×(p-1) mod p2 - 1) // p = ( (k×p+1)2×m mod p2 - 1) // p = (先程のパスカルの三角形より) ( ( 2×m×k×p+1 ) - 1) // p = 2×m×k

よって、a×b-1 mod p = 2×m×k×k-1 = 2×m となり、a'×b-1 mod p2= 2×m mod p2が成り立つa'をサーバに計算させることが出来ました。

長ったらしい証明をしましたが、公開鍵とcを受け取ってc2 mod nを計算して復号させ、得られた平文の値を2で割ってlong_to_bytes()するだけです。solverは結構短めです。

HOST, PORT = "oucs.cry.wanictf.org", 50010
s, f = sock(HOST, PORT)
for _ in range(10): read_until(f)
for _ in range(7): read_until(f)
read_until(f,"> ")
s.send(b"1\n")
recv_m = read_until(f).split()
c = int(recv_m[-1][2:],16)
for _ in range(8): read_until(f)
read_until(f,"> ")
s.send(b"4\n")
recv_m = read_until(f).split()
n = int(recv_m[-1][2:],16)
recv_m = read_until(f).split()
g = int(recv_m[-1][2:],16)
for _ in range(9): read_until(f)
c2 = pow(c,2,n)
read_until(f,"> ")
s.send(b"3\n")
read_until(f)
read_until(f,"> ")
s.send(str(c2).encode()+b"\n")
recv_m = read_until(f).split()
m2 = int(recv_m[-1][2:],16)
print(long_to_bytes(m2//2))

FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}

Problems & Solver

GitHub - ksbowler/waniCTF_2021spring


おわりに

今回はcrypto問について書いていきました。

OUCSにおいて、証明までしっかりして解いたのは久しぶりでしたしflagが出たときの爽快感が半端なかったです!解いた人の中で証明までした人と、たまたま見つけた人との割合が気になります。この問題ならたまたまの人はそれなりにいるんじゃないかな。

他の分野はまだまだ分からないことだらけで、今年から少しずつ勉強していきたいと思います。もう5月だけど…
m1z0r3として出場するコンテストならともかく、個人戦の初心者向けのCTFならある程度解けるようになりたいですね。

CpawCTF Level1 writeup

初心者向けであるCpawCTF Level 1のwriteupです。今回はかなり噛み砕いて説明していきます。

常設CTFは勉強する上で有難いですね。

ctf.cpaw.site


[Misc] Test Problem

この問題の答え(FLAG)は、cpaw{this_is_Cpaw_CTF} です。
下の入力欄にFLAGを入力してSubmitボタンを押して、答えを送信しましょう!
Enjoy CpawCTF!!!!

そのままです。

cpaw{this_is_Cpaw_CTF}


[Crypto] Classical Cipher

暗号には大きく分けて、古典暗号と現代暗号の2種類があります。特に古典暗号では、古代ローマの軍事的指導者ガイウス・ユリウス・カエサル(英語読みでシーザー)が初めて使ったことから、名称がついたシーザー暗号が有名です。これは3文字分アルファベットをずらすという単一換字式暗号の一つです。次の暗号文は、このシーザー暗号を用いて暗号化しました。暗号文を解読してフラグを手にいれましょう。
暗号文: fsdz{Fdhvdu_flskhu_lv_fodvvlfdo_flskhu}

文字をずらすだけの暗号です。クイズ番組でも日本語をずらした問題とかありますよね。その英語版です。

いつもはrot13と呼ばれる13文字ずらす暗号方式がとても多いのですが、問題文をよく読まず古典暗号といえばrot13だろうとやってしまう早とちり。rot13はアルファベットは26文字あるのでちょうど反対側となる文字にシフトされます。 こちらのサイトではROT0からROT25までやってくれます。ROT23を選び出てきた文字列を提出します。

rot13.com

cpaw{Caesar_cipher_is_classical_cipher}


[Reversing] Can you execute?

拡張子がないファイルを貰ってこのファイルを実行しろと言われたが、どうしたら実行出来るのだろうか。
この場合、UnixLinuxのとあるコマンドを使ってファイルの種類を調べて、適切なOSで実行するのが一般的らしいが…
問題ファイル: exec_me

linuxではfileコマンドでファイルがどういうファイルか教えてくれます。

$ file exec_me
exec_me: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=663a3e0e5a079fddd0de92474688cd6812d3b550, not stripped

どうやらELFファイルのようです。少し難しいですが、ELFファイルについてのサイトです。

[Linux] バイナリファイル(ELFファイル)の調査に使えるコマンドまとめ - Qiita

ELFファイルは"./(ファイル名)"とやると実行できます。C言語の時よく使ったなぁ。

$ ./exec_me
cpaw{Do_you_know_ELF_file?}

ただこのflagの文字列を出力するだけのプログラムですね。

cpaw{Do_you_know_ELF_file?}


[Misc] Can you open this file?

このファイルを開きたいが拡張子がないので、どのような種類のファイルで、どのアプリケーションで開けば良いかわからない。
どうにかして、この拡張子がないこのファイルの種類を特定し、どのアプリケーションで開くか調べてくれ。
問題ファイル: open_me

先程と同様、このファイルの種類が分からないのでfileコマンドを利用します。

$ file open_me
open_me: Composite Document File V2 Document, Little Endian, Os: Windows, Version 10.0, Code page: 932, Author: v, Template: Normal.dotm, Last Saved By: v, Revision Number: 1, Name of Creating Application: Microsoft Office Word, Total Editing Time: 28:00, Create Time/Date: Mon Oct 12 04:27:00 2015, Last Saved Time/Date: Mon Oct 12 04:55:00 2015, Number of Pages: 1, Number of Words: 3, Number of Characters: 23, Security: 0

先頭の文字列をググってみます。CTFでは知らない単語出てきたらすぐググるのが攻略への近道です。

linux - Composite Document File V2ドキュメントとは何ですか? [閉まっている] - ITツールウェブ

.docか.xlsファイルのようです。
他にも、バイナリデータの可読部分を表示してくれるstringsコマンドをよく使います。

$ strings open_me
bjbj
IHDR
sRGB
        pHYs
IDATx^
 @<8
i^v6{
xX*4
wB/B
}{q?
*+KZ
j4?%
=/v>h^r7z]
o+?`
?)fe
<@6Z
:_TOS
okm_(
v.jH
:^678
Y'4z
VdDd
VJC;
|da`D
ovWTV[
D_Su
_yn@
wr6N
h6[.
H^D[
RX00
&S?q
d~~}
>umG
+1bW
.'!#
'}_Ox
4Nb1
_C47{
IEND
nn0h
0j0W0
[Content_Types].xml
_rels/.rels
theme/theme/themeManager.xml
sQ}#
theme/theme/theme1.xml
?99~vr
{a9A
dFTl
KN1a
"AbG,fx
1$y8|
rr||
0j[u
Kh.Pds9D
oyTL
&3IT-$
P+{#,
Vn&
A8>r
pL8<
sDUC
u_m$#
WP;M
u&Ie
"SoV
theme/theme/_rels/themeManager.xml.rels
6?$Q
K(M&$R(.1
[Content_Types].xmlPK
_rels/.relsPK
theme/theme/themeManager.xmlPK
theme/theme/theme1.xmlPK
theme/theme/_rels/themeManager.xml.relsPK
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:clrMap xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
[c:'wc:'
 0 2 3
Normal.dotm
Microsoft Office Word
Microsoft Word 97-2003
MSWordDoc
Word.Document.8

Wordファイルのようなので、cpやmvコマンドでこのファイルに拡張子.docを付けてあげます。

$ cp open_me open_me.doc

開いてみるとワードにflagの書かれた画像を貼り付けています。今後PNG画像ファイルを扱う時によく出てくるIHDRチャンクやIDATチャンクがstringsコマンドを実行した時に出てきていますね。

cpaw{Th1s_f1le_c0uld_be_0p3n3d}


[Web] HTML page

HTML(Hyper Text Markup Language)は、Webサイトを記述するための言語です。
ページに表示されている部分以外にも、ページをより良くみせるためのデータが含まれています。
次のWebサイトからフラグを探して下さい。
http://q9.ctf.cpaw.site

このサイトは一見するとflagがどこにも無いように見えます。しかし、このサイトのコードはどうでしょうか。F12キーを押すとこのページのソースコードが見れます。そこを色々探しているとhead部分にありました。

f:id:partender810:20210429143411p:plain

cpaw{9216ddf84851f15a46662eb04759d2bebacac666}


[Forensics] River

JPEGという画像ファイルのフォーマットでは、撮影時の日時、使われたカメラ、位置情報など様々な情報(Exif情報)が付加されることがあるらしい。
この情報から、写真に写っている川の名前を特定して欲しい。
問題ファイル: river.jpg
FLAGの形式は、"cpaw{river_name}"
例:隅田川 → cpaw{sumidagawa}

この画像が撮られた川の名前を当てる問題です。問題文にある通りJPEG画像ファイルは様々な情報が付与されているのでそれを見ていきましょう。これらはexiftoolというツールで確認できます。こちらからインストールできます。

Installing ExifTool

$ exiftool river.jpg
ExifTool Version Number         : 10.80
File Name                       : river.jpg
(省略)
GPS Latitude                    : 31 deg 35' 2.76" N
GPS Longitude                   : 130 deg 32' 51.73" E
GPS Position                    : 31 deg 35' 2.76" N, 130 deg 32' 51.73" E
(省略)

このようにすることで、この写真が撮られた場所の座標が手に入りました。この座標をgoogle mapを使って検索してみましょう。

場所の座標を確認して使用する - Google Earth ヘルプ

「31°35'2.76"N, 130°32'51.73"E」と検索すればOKです。

f:id:partender810:20210429144237p:plain
甲突川の橋の上で撮ったよう

cpaw{koutsukigawa}


[Network] pcap

ネットワークを流れているデータはパケットというデータの塊です。 それを保存したのがpcapファイルです。
pcapファイルを開いて、ネットワークにふれてみましょう!

pcapファイルはWireSharkというソフトを使って色々調べます。CTFではよく出てくるので是非インストールしてください!

Wireshark · Download

WireSharkでpcapファイルを開くと通信パケットを見ることが出来ます。そのパケットをダブルクリックするとflagがありました。

f:id:partender810:20210429144831p:plain
ごちうさ好きなのかな

他にもTCPストリームなどを使って通信した文字列を見ることなどができます。今回の問題に限って言えばstringsコマンドでもflagを入手できます。

cpaw{gochi_usa_kami}


[Crypto] HashHashHash!

ハッシュ関数とは、値を入れたら絶対にもとに戻せないハッシュ値と呼ばれる値が返ってくる関数です。
ですが、レインボーテーブルなどでいくつかのハッシュ関数は元に戻せてしまう時代になってしまいました。
以下のSHA1というハッシュ関数で作られたハッシュ値を元に戻してみてください!(ヒント:googleで検索)
e4c6bced9edff99746401bd077afa92860f83de3
フラグは
cpaw{ハッシュを戻した値}
です。

ハッシュ関数とは、あるデータをハッシュ値と呼ばれる数値に変換する関数です。どのように変換するかアルゴリズムは公表されていないので、ハッシュ値から元のデータから予測することは出来ません。しかし、誰でも(ツールなどを使うことによって)ハッシュ値を計算することはできるので、よくある文字列のハッシュ値は知られてます。それをまとめたのがレインボーテーブルです。

このサイトでは、よくある文字列や短い文字列のハッシュ値は記憶しているため、ハッシュ値で検索するとその入力データを求めてくれます。

https://hashtoolkit.com/decrypt-hash/?hash=e4c6bced9edff99746401bd077afa92860f83de3

ハッシュ値から元のデータを求めることをハッシュを破るというのですが、破られないためには元のデータに乱数を加えてハッシュ値を取るなどする必要があります。

cpaw{Shal}


[PPC] 並び替えろ!

下にある配列の中身を大きい順に並べ替えて、くっつけてcpaw{並べ替えた後の値}をフラグとして提出してください。
例:もし配列{1,5,3,2}っていう配列があったら、大きい順に並べ替えると{5,3,2,1}となります。
そして、フラグはcpaw{5321}となります。
同じようにやってみましょう(ただし量が多いので、ソートするプログラムを書いたほうがいいですよ!)
[15,1,93,52,66,31,87,0,42,77,46,24,99,10,19,36,27,4,58,76,2,81,50,102,33,94,20,14,80,82,49,41,12,143,121,7,111,100,60,55,108,34,150,103,109,130,25,54,57,159,136,110,3,167,119,72,18,151,105,171,160,144,85,201,193,188,190,146,210,211,63,207]

プログラムを書かない人には厳しい問題かもしれません。ですが、今後pythonというプログラミング言語をよく使うので学んでおきましょう。

本来、ソートにはいくつものアルゴリズムがありその速さなどがよく比較されます。ただ、今回はそんなに難しい話はしません。pythonには勝手にソートしてくれる関数があります。

a = [15,1,93,52,66,31,87,0,42,77,46,24,99,10,19,36,27,4,58,76,2,81,50,102,33,94,20,14,80,82,49,41,12,143,121,7,111,100,60,55,108,34,150,103,109,130,25,54,57,159,136,110,3,167,119,72,18,151,105,171,160,144,85,201,193,188,190,146,210,211,63,207]
b = sorted(a,reverse=True)
flag = "cpaw{"
for i in b:
    flag += str(i)
print(flag+"}")

1行目にはaという変数に問題文にある数列を入れます。[]はpythonのリストを表します。他の言語だと配列などと呼ばれます。

2行目でいきなりソートしています。ソートした後のリストをbという変数に入れています。sortedについて、まずリストaを渡します。もう一つのreverse=Trueというのはリストaを降順に並べるよう指定しています。これが無いとbにはリストaを昇順に並べたものが入ります。

3行目以降は、flagを提出しやすいよう出力しています。

cpaw{2112102072011931901881711671601591511501461441431361301211191111101091081051031021009994938785828180777672666360585755545250494642413634333127252420191815141210743210}


これら全部できたらCTF初心者は卒業と言っても過言ではありません!

HeroCTF v3 writeup

m1z0r3はWPICTFにも参加していたのですが難しすぎて私はすぐ撤退…。
HeroCTFはそこそこの難易度でcryptoは全完できたのでよかったです!

https://www.heroctf.fr

Result

f:id:partender810:20210426143616p:plain
result

もう少しで1000ptと2桁順位が見えたので悔しいですね…


Writeup

Blockchain

Transfer 25pt

We've been tracking drug dealers for a few years. The only thing we got from their network is a hash : bfa8b1c8b7f6acc867d6984968756cafd0da99060aa6d90dfb0db1a9ef0c96a2
Can you tell us where the money is gone?

Blockchainというジャンルは初めてです。いつもはよくEtherscanを使っているのですがそちらでは見つからず、こちらでhash検索で見つかりました。

Transaction: bfa8b1c8b7f6acc867d6984968756cafd0da99060aa6d90dfb0db1a9ef0c96a2 | Blockchain Explorer

Hero{1PQBh6tddet14bYDsSbUiox1YJb5EL185A}


Crypto

h4XOR 75 pt

Can you recover the flag.png image ?
Hint : The xor function is from the pwntools module.
ファイル:flag.png.enc, xor.py

まずxor.pyについて読んでいきます。flag.pngのバイナリデータと、ランダムな8bytesと[0-9]の内どれかをバイト列として繋げた9bytesをxorしてflag.png.encに書き込んでいます。ここでpwntoolsでのxorは特殊で、xorする際に両方の長さを揃えるようにします。長い方に合わせる形で短い方を伸ばします。短い方の長さが長い方の倍数で無い場合、先頭bytesを付け加えます。例えばb"ABCDE"を12bytesにしたい場合、b"ABCDEABCDEAB"という感じです。

ランダムな9bytesの方ですが、後ろ1bytesは10通りしかないのでbrute force可能です。ですが前8bytesをbrute forceするのは2568 = O(1020)とかなりかかるので不可能です。なので別の方法を考えましょう。

PNG画像ファイルのフォーマットはこちらで勉強しました。

PNG ファイルフォーマット

PNG画像ファイルの先頭8bytesは16進数で "89504E470D0A1A0A"と決まっています。なのでこの値とencの先頭8bytesをxorするとランダムな8bytesの値が手に入ります。あとは0~9の10通りを試せばOKです。

from os import urandom
from random import randint
from pwn import xor
from Crypto.Util.number import *


outpout_img = open("flag.png.enc", "rb").read()
head = 0x89504E470D0A1A0A
k = long_to_bytes(bytes_to_long(outpout_img[:8])^head)
img0 =open("flag0.png", "wb")
img1 =open("flag1.png", "wb")
img2 =open("flag2.png", "wb")
img3 =open("flag3.png", "wb")
img4 =open("flag4.png", "wb")
img5 =open("flag5.png", "wb")
img6 =open("flag6.png", "wb")
img7 =open("flag7.png", "wb")
img8 =open("flag8.png", "wb")
img9 =open("flag9.png", "wb")
key0 = k + bytes([0])
key1 = k + bytes([1])
key2 = k + bytes([2])
key3 = k + bytes([3])
key4 = k + bytes([4])
key5 = k + bytes([5])
key6 = k + bytes([6])
key7 = k + bytes([7])
key8 = k + bytes([8])
key9 = k + bytes([9])
img0.write(xor(outpout_img, key0))
img1.write(xor(outpout_img, key1))
img2.write(xor(outpout_img, key2))
img3.write(xor(outpout_img, key3))
img4.write(xor(outpout_img, key4))
img5.write(xor(outpout_img, key5))
img6.write(xor(outpout_img, key6))
img7.write(xor(outpout_img, key7))
img8.write(xor(outpout_img, key8))
img9.write(xor(outpout_img, key9))

f:id:partender810:20210425222308p:plain
9でした

もっと綺麗なsolverはあるはず

Hero{123_xor_321}


ExtractMyBlocks 125 pt

The company PasswordS3cure created a new password reset functionality. Could you crack it and find the flag ?
nc chall0.heroctf.fr 10000
ファイル:challenge.py

challenge.pyを読んでいきます。

文字列を投げるとaccount_idとして処理され、それやflagを含むmsg変数にある文字列がAESのECBモードで暗号化され、それを16進数にした値が渡されます。KEYの値は不明です。

AESのECBモードはもっとも単純な暗号化方式になります。

f:id:partender810:20210425224952p:plain
AES ECB mode wikipediaから引用

KEYが同じ場合、同じ平文なら同じ暗号文が返されます。CBCモードと異なり、同じ16bytesの平文を二つ繋げた文字列を暗号化すると、16bytesの同じ暗号文が二つ繋げた状態で返ってきます。何ブロック目かなんて関係ないです。flagの文字列も一緒に暗号化されるので、同じ文字列を送れば同じ暗号文になり情報が得られそうです。ただし16bytes毎に区切っているので開始位置に注意する必要があります。これらを考慮しながら入力を調整していく方向で考えていきます。

flagをいきなりbrute forceするのは無理があります。長さも分からないので無謀です。しかし、一文字ごと特定するのであれば64くらい×(flag長)なのでそんなに時間がかからないです。では一文字ずつ特定していきましょう。

flagが含まれるmsg変数のflagが始まる10文字を注目していきます。(その10文字)+"Hero{"+(flagの1文字目) (=Fとします )が一つのblockとして扱われるよう(blockを跨ぐことのないよう)入力を調整していきます。(その10文字)の先頭が16*k+1文字目になればOKです。※(その10文字) = "assword : "

ではこちらが送る文字列も(その10文字)+"Hero{"+(flagの1文字目をbrute forceで試している1文字) (=Tとします )が一つのblockとして扱われるようにこちらも入力を調整していきます。

下の図について、\は改行を、*はT,Fが1blockに収まるように調整を、?はbrute forceで試す1文字を表しています。これはflagの{以降の最初の文字を調べる時の図です。

f:id:partender810:20210426155738p:plain
flagの1文字目を求めたい

上から2,4番目の文字列が等しくなる場合、つまり?にflagの1文字目が入った場合、暗号文の2,4 block目(16~32bytesと48~64bytes目)が同じ値になります。これで1文字目が"_"ということが分かりました(1文字目でこの文字は不安になる)。

次に2文字目についてです。

f:id:partender810:20210426155838p:plain
2文字目を求めたい

2block目を1文字シフトさせ3block目のpaddingを一つ減らすことで、上手く2,4block目を対応させます。
このようにして?に"}"が当てはまるまで繰り返していきます。3block目のpaddingが0になったら次は16個増やして2,5 block目が同じならOKという感じにシフトしていきます。flag長が16を超えたら3,6block目にしないと…と考えてたらかなり短かったです。

candi = "ABCDEFGHJIKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890{}_"
FLAG = "Hero{test_flag}" #簡易的なflag。途中msgを出力している際に入力として送る文字列が正しい長さ確認する
flag = "Hero{"
isbreak = False #終了条件
#tempをシフトさせていって1文字ずつbrute forceしていく
temp = "assword : Hero{"
k = len(" !\n\nYour p") #paddingする際の長さ
print(k)
L = 16 #paddingで送る長さ調整。k=16になった時は32にする。
M = 3 #暗号化されたflagが何blockにあたるか
while True:
    printmsg = True
    print("k :",k)
    for i in candi:
        print(i)
        HOST, PORT = "chall0.heroctf.fr", 10000
        s, f = sock(HOST, PORT)
        _ = read_until(f)
        read_until(f,"ID : ")
        account_id = "12" + temp + i + "0"*(L-k)
        _msg = f"""
Welcome back {account_id} !

Your password : {FLAG}

Regards
"""
        #送る入力が正しいものになっているか。特に長さの確認
        if printmsg:
            for j in range(0,len(_msg),16):
                print(j//16,_msg[j:j+16])
        msg = "12" + temp + i + "0"*(L-k)
        s.send(msg.encode()+b"\n")
        ct = read_until(f).strip()
        assert len(ct)%32 == 0
        cip = []
        for j in range(0,len(ct),32):
            cip.append(ct[j:j+32])
        if cip[1] == cip[M]:
            print(i)
            s.close()
            flag += i
            if i == "}":
                print("This is the flag!")
                print(flag)
                isbreak = True
                break
            print(flag)
            temp = temp[1:] + i
            k += 1
            if k == 16:
                L += 16
                M += 1
            break
        s.close()
        printmsg = False
    if isbreak: break

Hero{_BL0CK5_}


Forensics

We need you 1/5 50pt

Interpol and the FBI have been investigating for over a year now. They are trying to get their hands on two hackers very well known for their ransomware and their ultra efficient botnet.
After long months of investigation, they managed to get their hands on one of their servers. But, when they got it back the PC caught fire because of a defense mechanism set up by the two hackers.
The hard drive could not be saved, but they had time to put the RAM in liquid nitrogen and analyze it later.
You know what you have to do!
For this first step, find the name of the PC!
Download, here.
Format: Hero{Name}
ファイル:Challenge.zip

zipファイルを解凍するとcapture.memが得られます。memファイルの解析ってなんだ、とググるとvolatilityが使えることが判明!

ずばり欲しいデータを得られるコマンドを教えてくれるサイトがありました。

Volatility/Retrieve-hostname - aldeid

$ volatility -f capture.mem imageinfo
Volatility Foundation Volatility Framework 2.6
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s) : Win7SP1x86_23418, Win7SP0x86, Win7SP1x86
          (省略)

$ volatility -f capture.mem --profile=Win7SP1x86 hivelist
Volatility Foundation Volatility Framework 2.6
Virtual    Physical   Name
---------- ---------- ----
(省略)
0x823859c8 0x2b5d99c8 \SystemRoot\System32\Config\SAM
0x8940c5e8 0x2d5415e8 [no name]
0x8941a2c0 0x2d58d2c0 \REGISTRY\MACHINE\SYSTEM
(省略)


$ volatility -f capture.mem --profile=Win7SP1x86 printkey -o 0x8941a2c0 -K 'ControlSet001\Control\ComputerName\ComputerName'
Volatility Foundation Volatility Framework 2.6
Legend: (S) = Stable   (V) = Volatile

----------------------------
Registry: \REGISTRY\MACHINE\SYSTEM
Key name: ComputerName (S)
Last updated: 2021-04-19 17:00:09 UTC+0000

Subkeys:

Values:
REG_SZ                        : (S) mnmsrvc
REG_SZ        ComputerName    : (S) KANNIBAL

Hero{KANNIBAL}


We need you 2/5 75pt

It must be their team name.
For this second step, find the user's name and password in clear text.
Format: Hero{Username:Password}

先程使ったサイト、他にも欲しい情報くれるので最高過ぎる

Volatility/Retrieve-password - aldeid

1/5で使ったimageinfoとhivelistを使います

$ volatility -f capture.mem --profile=Win7SP1x86 hashdump -y 0x8941a2c0 -s 0x823859c8
Volatility Foundation Volatility Framework 2.6
Administrateur:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Invit:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Razex:1000:aad3b435b51404eeaad3b435b51404ee:78d9c7e905c695087ee3baa755ce43e4:::

f:id:partender810:20210426001458p:plain
crack成功!

UsernameがRazexであることに気付かず、crackしてから時間かかりました…。

Hero{Razex:liverpoolfc123}


We need you 3/5 80pt

We know for sure that this server allowed to connect to infected machines. Can you check if a connection was instantiated?
Format: Hero{IP:Port}

接続を確認するのはconnectionsコマンドだろうと実行するもサポートしてないと指摘される。うーん、他にないかな。

Volatility, my own cheatsheet (Part 5): Networking | Andrea Fortuna

netscanでもいいのかありがたい。

$ volatility -f capture.mem --profile=Win7SP1x86 netscan
Volatility Foundation Volatility Framework 2.6
Offset(P)          Proto    Local Address                  Foreign Address      State            Pid      Owner          Created
(省略)
0x7ee41538         TCPv4    10.0.2.15:49159                146.59.156.82:4444   ESTABLISHED      3296     nc.exe
(省略)

他を省略していますがStateが唯一ESTABLISHEDとなっているものがあったのでForeign Addressを投げたら通りました。

Hero{146.59.156.82:4444}


Misc

Discord 1pt

Join our Discord server and read the rules !
URL : https://discord.gg/PhNkPrfeJG

discord内に分かりやすく書いてほしい…。

Hero{3njoy_th3_3v3nt}


Cubes 40pt

#257 ; #260 ; #282 ; #12 ; #349:2 ; #344 ; #383:120 ; #368
The flag is in in lowercase, without spaces.

これ解けたの嬉しい!

暗号ぽいので「cube cipher」でググるとrubik's cube cipherがヒットしました。ルービックキューブは9個*6面の54種類のスペース(?)があるのに対し、暗号文の数が大きすぎます。そもそも「383:120」などのコロンが付いているやつが何も分かりません。

この特徴的な値に注目し「383:120 cube」でググると…。

Minecraft ID List - Minecraft Info

めちゃくちゃぽい!!

頭文字を並べてみると"iamscese"となる。"iam"はそれっぽいのに最後がなんか変。提出してもincorrectでした。

方向性は間違っていないだろうとminecraftのIDリストを調べてみるとどうやら見ていたのが古いバージョンだった模様。こちらで調べて頭文字を調べるとcorrectとなりました!

Minecraft ID List (1.16) | Minecraft Item IDs

Hero{iamsteve}


Russian Doll 50pt

Go deeper !
ファイル:archive.zip

問題文と問題名よりzip解凍するとめちゃくちゃディレクトリ階層の深いのだろうと…ビンゴ。ただ予想をはるかに超える深さでした。最下層にflag.txtがあるのが見えたので、なんとか検索できる術はないかと模索したところ、見つかりました!

[Linux]grepでディレクトリ以下の全てのファイルを検索する │ TEAM T3A

$ find ./ -type f | xargs grep "Hero{"
Binary file ./archive.zip matches
./you/are/about/to/get/rick/rolled/rickRollIncoming/no/iMean/really/lastWarning/toldYou/you/should/have/listened/now/stop/looking/in/someoneElse/stuff/cuteCatPictures/goAway/stop/clicking/manually/through/folders/like/that/write/a/script/to/do/it/for/you/come/on/waste/of/time/what/about/a/recipe/now/200/g/de/pois/chiches/500/g/de/feve/seches/1/oignon/moyen/2/gousses/dail/1/bouquet/de/persil/3/cuilleres/a/soupe/de/farine/1/cuillere/a/cafe/de/cumin/en/poudre/1/cuillere/a/cafe/de/coriandre/en/poudre/1/cuillere/à/cafe/de/paprika/3/cuilleres/a/soupe/de/basilic/frais/hache/sel/huile/de/friture/faites/tremper/les/pois/chiches/et/les/feves/dans/leau/12h/les/egoutter/et/les/cuire/45/mn/a/l/auto/cuiseur/peler/oignon/et/ail/les/hacher/ainsi/que/le/persil/passer/les/feves/et/les/pois/chiches/au/mixer/ourobot/melanger/avec/le/persil/loignon/lail/la/farine/les/epices/le/sel/petrissez/le/tout/avec/vos/mains/en/ajoutant/un/peu/deau/si/necessaire/rassemblez/la/pate/et/laisser/reposer/au/refrigerateur/pendant/minimum/30mn/faconner/une/trentaine/de/boulettes/de/la/grosseur/dune/piece/de/2/euros/les/faire/frire/2-3/mn/puis/les/egoutter/sur/du/papier/absorbant/servir/chaud/ou/froid/avec/des/petites/sauces/tomates/aux/herbes/ou/sauces/yaourts/et/voila/cest/pret/bon/appetit/notPorn/stillNotPorn/reallyNot/seriousStuff/work/realWork/porn/did/you/really/think/there/would/be/porn/inA/work/directory/now/stop/looking/in/someoneElseS/stuff/cuteCatPictures/goAway/stop/clicking/manually/through/folders/like/that/write/a/script/to/do/it/for/you/come/on/waste/of/time/what/about/a/recipe/now/200/g/de/pois/chiches/500/g/de/feve/seches/1/oignon/moyen/2/gousses/dail/1/bouquet/de/persil/3/cuilleres/a/soupe/de/farine/1/cuillere/a/cafe/de/cumin/en/poudre/1/cuillere/a/cafe/de/coriandre/en/poudre/1/cuillere/à/cafe/de/paprika/3/cuilleres/a/soupe/de/basilic/frais/hache/sel/huile/de/friture/faites/tremper/les/pois/chiches/et/les/feves/dans/leau/12h/les/egoutter/et/les/cuire/45/mn/a/l/auto/cuiseur/peler/oignon/et/ail/les/hacher/ainsi/que/le/persil/passer/les/feves/et/les/pois/chiches/au/mixer/ourobot/melanger/avec/le/persil/loignon/lail/la/farine/les/epices/le/sel/petrissez/le/tout/avec/vos/mains/en/ajoutant/un/peu/deau/si/necessaire/rassemblez/la/pate/et/laisser/reposer/au/refrigerateur/pendant/minimum/30mn/faconner/une/trentaine/de/boulettes/de/la/grosseur/dune/piece/de/2/euros/les/faire/frire/2-3/mn/puis/les/egoutter/sur/du/papier/absorbant/servir/chaud/ou/froid/avec/des/petites/sauces/tomates/aux/herbes/ou/sauces/yaourts/et/voila/cest/pret/bon/appetit/ok/you/win/hereIsTheFlag/haha/what/about/a/recipe/now/200/g/de/pois/chiches/500/g/de/feve/seches/1/oignon/moyen/2/gousses/dail/1/bouquet/de/persil/3/cuilleres/a/soupe/de/farine/1/cuillere/a/cafe/de/cumin/en/poudre/1/cuillere/a/cafe/de/coriandre/en/poudre/1/cuillere/à/cafe/de/paprika/3/cuilleres/a/soupe/de/basilic/frais/hache/sel/huile/de/friture/faites/tremper/les/pois/chiches/et/les/feves/dans/leau/12h/les/egoutter/et/les/cuire/45/mn/a/l/auto/cuiseur/peler/oignon/et/ail/les/hacher/ainsi/que/le/persil/passer/les/feves/et/les/pois/chiches/au/mixer/ourobot/melanger/avec/le/persil/loignon/lail/la/farine/les/epices/le/sel/petrissez/le/tout/avec/vos/mains/en/ajoutant/un/peu/deau/si/necessaire/rassemblez/la/pate/et/laisser/reposer/au/refrigerateur/pendant/minimum/30mn/faconner/une/trentaine/de/boulettes/de/la/grosseur/dune/piece/de/2/euros/les/faire/frire/2-3/mn/puis/les/egoutter/sur/du/papier/absorbant/servir/chaud/ou/froid/avec/des/petites/sauces/tomates/aux/herbes/ou/sauces/yaourts/et/voila/cest/pret/bon/appetit/bonOk/flag.txt:Hero{if_yOu_gOt_HEre_By_clIcKInG_mANnUaLly_YoU_sHOuLd_REalLy_SeE_SoMeOne}

Hero{if_yOu_gOt_HEre_By_clIcKInG_mANnUaLly_YoU_sHOuLd_REalLy_SeE_SoMeOne}


OSINT

Find me 10pt

Could you retrieve where this photo was taken ?
Format : Hero{place}
ファイル:find_me.jpg

画像検索で一発です。

f:id:partender810:20210426141422p:plain
便利だなぁ

Hero{portes mordelaises}


Social ID #1 15pt

Can you find the twitter ID of @HeroCTF.
Format : Hero{twitter_id}

twitter ID って@HeroCTFじゃないのか?と調べたところある整数値がIDだそうです。スクリーンネーム(@以降)を投げると教えてくれるサイトがありました。

idtwi | Twitter IDチェッカー(変更履歴の追跡)

Hero{815907006708060160}


Pushhhh 35pt

The flag is in one of the two Github repositories of the last HeroCTF edition. Could you find it ?

今回がv3なのでv2のgithubを調べれば良さそうです。色々見たところbranchが複数…。

f:id:partender810:20210426142049p:plain
怪しすぎる

ここを覗いてみるとflagがありました。

HeroCTF_v2/flag.txt at flag · HeroCTF/HeroCTF_v2 · GitHub

Hero{y0u_re_g00d_4t_g1t_250820}


Social ID #2 50pt

Can you find the @username of this Twitter ID 44196397.
Format : Hero{username}

先程のidtwiでOKです。

Hero{@elonmusk}


Prog

Ping Pong 45pt

Could you get the flag ?
ファイル:output.txt

ProgってProgrammingかな?

output.txtを見ると"PING"か"PONG"のどちらかが書いてあります。長さが176と8の倍数であることと、文字列として違うのがIとOなので、"PING" = 1, "PONG" = 0とした2進数を8桁ごとでchrしたらうまく復号されました。

from Crypto.Util.number import *
f = open("output.txt")
a = f.readlines()
ans = ""
for i in a:
    if i.strip() == "PING": ans += "1"
    else: ans += "0"
for i in range(0,len(ans),8):
    print(chr(int(ans[i:i+8],2)),end="")
print()

Hero{p1n6_p0n6_15_fun}


PRo Random Guesser 50pt

Everybody loves a little guessing challenge right ? Guess the number \o/
- Love, Mersenne
/!\ If you are using a script, you have to append '\n' to any data you wish to send back.
Host : chall0.heroctf.fr Port : 7003

数字を予測しろとのことなので、何かの乱数を生成しているのだろう。問題文から恐らくメルセンヌ・ツイスターかな。

メルセンヌ・ツイスタ - Wikipedia

細かい仕組みは全く分からないですが、こちらをコードをパクって解けました。ももテクさんには頭が上がらないです。

Mersenne Twisterの出力を推測してみる - ももいろテクノロジー

def untemper(x):
    x = unBitshiftRightXor(x, 18)
    x = unBitshiftLeftXor(x, 15, 0xefc60000)
    x = unBitshiftLeftXor(x, 7, 0x9d2c5680)
    x = unBitshiftRightXor(x, 11)
    return x

def unBitshiftRightXor(x, shift):
    i = 1
    y = x
    while i * shift < 32:
        z = y >> shift
        y = x ^ z
        i += 1
    return y

def unBitshiftLeftXor(x, shift, mask):
    i = 1
    y = x
    while i * shift < 32:
        z = y << shift
        y = x ^ (z & mask)
        i += 1
    return y

HOST, PORT = "chall0.heroctf.fr", 7003
s, f = sock(HOST, PORT)
print(read_until(f))
print(read_until(f))
print(read_until(f))
x = []
for i in range(624):
    print(i)
    read_until(f,"Guess me : ")
    s.send(b"1337\n")
    recv_m = read_until(f).split()
    x.append(int(recv_m[3]))
    read_until(f)
mt_state = tuple([untemper(z) for z in x] + [624])
random.setstate((3, mt_state, None))
ans = int(random.getrandbits(32))
s.send(str(ans).encode()+b"\n")
while True: print(read_until(f))

Hero{n0t_s0_r4nd0m_4ft3r_4ll}


Reverse

EasyAssembly 40pt

Don't worry, this one is quite easy :) Could be a good introduction to assembly !
Format : Hero{input:modified}
ファイル:EasyAssembly.asm

IDAなど色々ツールを使ったのですが、結局catすれば分かりました。

$ cat EasyAssembly.asm
(省略)
# EasyAssembly.c:19:    modified = input >> 2;
        movl    -8(%rbp), %eax  # input, tmp89
        sarl    $2, %eax        #, tmp88
        movl    %eax, -4(%rbp)  # tmp88, modified
# EasyAssembly.c:21:    if(modified == 1337404)
        cmpl    $1337404, -4(%rbp)      #, modified
        jne     .L5     #,
# EasyAssembly.c:22:            isGood = 0;
        movl    $0, isGood(%rip)        #, isGood
.L5:
# EasyAssembly.c:24:    if(!isGood)
        movl    isGood(%rip), %eax      # isGood, isGood.1_1
# EasyAssembly.c:24:    if(!isGood)
        testl   %eax, %eax      # isGood.1_1
        jne     .L6     #,
# EasyAssembly.c:25:            printf("Well done ! You can validate with the flag Hero{%d:%d}\n", input, modified);
(省略)

"if(modified == 1337404)" とあるのでmodifiedは1337404でしょう。"modified = input >> 2" からinputはmodifiedを2ビット左シフトさせたものでしょう。エントロピーが2bitsなのでbrute forceすればいいかなと思い提出したら一発で通りました。

Hero{5349616:1337404}


Steganography

HolyAbbot 15pt

A certain abbot tried to give us a message...
(the message is in lower case) (No need to speak French)
Format : Hero{messageinlowercase}
ファイル:HolyAbbot.txt

steganoって全部画像ファイルの解析と思ってたらテキストでびっくり。
しかもテキストの先頭行をググったら一発。RITSEC CTFでもやった、Ave Maria cipherのフランス語バージョンでした。Steganoというよりcryptoじゃん。

partender810.hatenablog.com

Ave Maria de Trithème - Déchiffrer, Décoder, Encoder en Ligne

decodeしたものを小文字に変更して終了です。

Hero{substitution}


Nice PDF 20pt

Don't think to much ;)
ファイル:NicePDF.pdf

pdfの解析か…。問題文から察するにそんな難しいことはせずに解けそうです。exiftoolやbinwalkなどのツールやPDFを実際に開いてプロパティを見るとかしましたが分からず…。

藁にも縋る思いでgoogleに頼るとこんなオンラインツールが。

PDF Extract Tool

textで解析すると以下の結果が得られました。

 93,02  760,68 H
  99,86  760,68 ses
 113,90  760,68 e
 119,42  760,68 Histoires,
 161,54  760,68 r
 165,38  760,68 l'historien
 209,93  760,68 o
 215,81  760,68 grec
 234,89  760,68 {
 238,37  760,68 Hérodote
 281,09  760,68 E
 286,49  760,68 rapporte
 325,99  760,68 4
 331,63  760,68 ainsi
 352,03  760,68 S
 357,07  760,68 une
 373,99  760,68 Y
 379,39  760,68 anecdote
 421,27  760,68 _
 426,79  760,68 qui
 440,86  760,68 P
 446,62  760,68 eut
 461,50  760,68 D
 468,34  760,68 lieu
 484,66  760,68 F
 489,70  760,68 au
 500,74  760,68 }

一文字おきに読むとflagになりました。

Hero{E4SY_PDF}


Problems&solver

GitHub - ksbowler/HeroCTF_v3

今回、ファイルサイズが大きくあげられないものがありました。ご了承ください。

  • forensics We need you Challenge.zip
  • OSINT Russian doll archive.zip

Cyber Apocalypse CTF 2021 writeup

4/19 ~ 24にて行われましたCyber Apocalypse CTF 2021のCryptoのwriteupです。
問題数が多くてよかったのですが、解けそうで解けない問題多くてもやもやしました。

問題名の横にある★の数はDifficultyです。

Result

f:id:partender810:20210424110538p:plain
それなりに頑張ったけど順位が伴わない…


Writeup

Warmup & Misc

Welcome 25pt ★

Join our Discord Server and the CA-2021 channels…

Discordに入っても全然flagが見つからない…。始まって1日かかっても分からなかったのですが、HackTheBoxのアカウントを作りdiscordのbotに認証させると色んなチャンネルに入れるようになりflagも見つかりました!
でもこのflagでいいの?とはなるflagでした。

CHTB{CA_CTF_i$_F*ing_EPIC}


Alien Camp 300pt ★

The Ministry of Galactic Defense now accepts human applicants for their specialised warrior unit, in exchange for their debt to be erased. We do not want to subject our people to this training and to be used as pawns in their little games. We need you to answer 500 of their questions to pass their test and take them down from the inside.
This challenge will raise 33 euros for a good cause.

dockerを起動させてncするとこんな感じです。

f:id:partender810:20210422221942p:plain
文字化け~~~

読めないです。うーーん。計算させる問題なんだろうけど、文字化けが分からない…。
slackに貼ってみるとなんと絵文字が!!

f:id:partender810:20210422222253p:plain
読めた!

文字化けしてたのが絵文字と分かればchrすれば文字コードを入手できます。それと矢印先の数字と対応させて四則演算をすればOKです。ただここで足し引き算とかけ算の順序に気を付けて実装する必要があります。

  • リストを作りまず最初の数字をappendします。
  • 演算子が+なら数字をそのままappend
  • 演算子が-なら数字に-1を掛けてappend
  • 演算子が×ならリストの最後尾の数字をその数字と対応する数字の積に置き換える
  • 最後にリストの総和を求める

先にかけ算を行って足し引き算のみの式にしようという感じです。

i=1
while(i<=500):
    print("Now :",i)
    r.recvuntil("Question " + str(i) + ":\n\n")
    question = r.recvline().decode()
    que = question.split()
    for j in range(0,len(que)):
        if que[j] == "?": print(que[j],end="")
        elif j%2 == 0: print(num[hel.index(ord(que[j]))]," ",end="")
        else: print(que[j]+" ",end="")
    print()
    ans = [0 for j in range(len(que)//2)]
    bef = "+"
    way = []
    cnt = 1
    #ansに対応する数字を格納していく。a - bの場合、[a,-b]と格納していき、最後にsum(ans)とすれば和が出る
    ans[0] = num[hel.index(ord(que[0]))]
    for ind in range(1,len(que),2):
        if que[ind] == "=":
            break
        if que[ind] == "*":
            #かけ算の場合、足し引き算より先に行わないといけない
            #先に積を導出し、掛けられる数が入ってたansのところを0に、掛ける数のとこを今求めた積を格納する
            # a + b * c * d の場合、[a,0,0,0] -> [a,b,0,0] -> [a,0,b*c,0] -> [a,0,0,b*c*d] -> sum(ans) となる
            temp = ans[cnt-1]*num[hel.index(ord(que[ind+1]))]
            ans[cnt-1] = 0
            ans[cnt] = temp
            cnt += 1
        else:
            #+ or -
            #足し引き算ならそのまま格納するだけでよい
            if que[ind] == "+":
                ans[cnt] = num[hel.index(ord(que[ind+1]))]
                cnt += 1
            elif que[ind] == "-":
                ans[cnt] = -num[hel.index(ord(que[ind+1]))]
                cnt += 1
            else:
                print("division")
    print(ans)
    ans = sum(ans)

500回は長かったなぁ

CHTB{3v3n_4l13n5_u53_3m0j15_t0_c0mmun1c4t3}


Crypto

Nintendo Base64 300pt ★

Aliens are trying to cause great misery for the human race by using our own cryptographic technology to encrypt all our games. Fortunately, the aliens haven't played CryptoHack so they're making several noob mistakes. Therefore they've given us a chance to recover our games and find their flags.
They've tried to scramble data on an N64 but don't seem to understand that encoding and ASCII art are not valid types of encryption!
This challenge will raise 33 euros for a good cause.
ファイル:output.txt

開いてみると綺麗ですね。

f:id:partender810:20210422232114p:plain
これ作るの大変だろうな

スペースを無くしてbase64 decodeしてみると後ろが==となりました。無限にdecodeしてb"CHTB{"が出るまで無限にdecodeして終わりです。

CHTB{3nc0d1ng_n0t_3qu4l_t0_3ncrypt10n}


PhaseStream1 300pt ★

The aliens are trying to build a secure cipher to encrypt all our games called "PhaseStream". They've heard that stream ciphers are pretty good. The aliens have learned of the XOR operation which is used to encrypt a plaintext with a key. They believe that XOR using a repeated 5-byte key is enough to build a strong stream cipher. Such silly aliens! Here's a flag they encrypted this way earlier. Can you decrypt it (hint: what's the flag format?)
2e313f2702184c5a0b1e321205550e03261b094d5c171f56011904
This challenge will raise 33 euros for a good cause.

これと同じです。

ångstromCTF 2021 writeup - Attack All Around

しかもこの問題と違ってmessageのどこかにflagがあるというわけではなく、flag以外余計な文字列は含まれていないので最初の5bytesは"CHTB{"となることが予想されます。それのxorをとると5bytesのkeyが求まるので後はそれを使って復号します。

CHTB{u51ng_kn0wn_pl41nt3xt}


PhaseStream2 300pt ★

The aliens have learned of a new concept called "security by obscurity". Fortunately for us they think it is a great idea and not a description of a common mistake. We've intercepted some alien comms and think they are XORing flags with a single-byte key and hiding the result inside 9999 lines of random data, Can you find the flag?
This challenge will raise 33 euros for a good cause.
ファイル:output.txt

1byteのkeyで、1つのflagと9999個のランダムデータをencryptした結果がoutput.txtです。
(keyの候補 = 256) × (10000行のデータ) = O(106) なのでまあbrute force可能です。各行をdecryptして"CHTB{"が含まれていたら終了です。

CHTB{n33dl3_1n_4_h4yst4ck}


PhaseStream3 300pt ★

The aliens have learned the stupidity of their misunderstanding of Kerckhoffs's principle. Now they're going to use a well-known stream cipher (AES in CTR mode) with a strong key. And they'll happily give us poor humans the source because they're so confident it's secure!
This challenge will raise 33 euros for a good cause.
ファイル:phasestream3.py, output.txt

AESのCTRモードで暗号化しているみたいです。CTRモードは初耳なのでこちらから勉強しました。

共通鍵暗号の暗号モード CTRについて - Qiita

簡単に言えば、ナンスとカウンターをkeyによって暗号化した値と平文をxorしています。

平文と暗号文のペアがあれば「ナンスとカウンターをkeyによって暗号化した値」が分かります。今回はそれが分かります。

同じkeyでインスタンスを作ると同じナンスとカウンターが使われるので、「」内を求めてflagの暗号文をxorしたら終了です。

from Crypto.Util.number import *

test_enc = "464851522838603926f4422a4ca6d81b02f351b454e6f968a324fcc77da30cf979eec57c8675de3bb92f6c21730607066226780a8d4539fcf67f9f5589d150a6c7867140b5a63de2971dc209f480c270882194f288167ed910b64cf627ea6392456fa1b648afd0b239b59652baedc595d4f87634cf7ec4262f8c9581d7f56dc6f836cfe696518ce434ef4616431d4d1b361c"
flag_enc = "4b6f25623a2d3b3833a8405557e7e83257d360a054c2ea"

test = b"No right of private conversation was enumerated in the Constitution. I don't suppose it occurred to anyone at the time that it could be prevented."
#test=平文と、test_enc=暗号文の組み合わせが分かるのでenc(key) xor test = test_encからenc(key)を求める
enc_key = long_to_bytes(bytes_to_long(test)^int(test_enc,16))
#flag長が46//2 = 23ということが分かる。enc(key)の上位23bytesとxorさせればOK
print(long_to_bytes(bytes_to_long(enc_key[:23])^int(flag_enc,16)))

CHTB{r3u53d_k3Y_4TT4cK}


PhaseStream4 300pt ★★

The aliens saw us break PhaseStream 3 and have proposed a quick fix to protect their new cipher.
This challenge will raise 43 euros for a good cause.
ファイル:phasestream4.py, output.txt

前回と違って、平文と暗号文のペアが分かりません。

testquoteとflagの暗号文が一つずつあり、それぞれ96,85bytesです。「平文と暗号文のペア」が分からんくても解けるのか?とノートに数式いっぱい書いたけど分からず。

先頭5bytesがb"CHTB{"とすると、そこからenc_keyを求めてtestquoteを出すと「I alo」となります。「I alone ... 」なのかなとか思ってたけどそこから有力な情報を得ず…。英文になるようにtestquoteを決めてflagが意味ある文字列になればいいな、とか思ったけど人力じゃ無理でした。

次に前回の問題の平文について考えます。

No right of private conversation was enumerated in the Constitution. I don't suppose it occurred to anyone at the time that it could be prevented.

これはWhitfield Diffieの言葉(?)らしいです。

www.brainyquote.com

Diffieと言えばDiffie-Hellman鍵共有ですよね。3でDiffieなら4はHellmanの言葉を使っているのでは?とHellman quotesで検索するも何も出てこず…。この筋は違うと判断。

次にtestquoteが96bytes, 96文字ということに注目して身の回りに96文字の文章が無いのかと探してみると……

The aliens saw us break PhaseStream 3 and have proposed a quick fix to protect their new cipher.

あった。

早速それがtestquoteとしてenc_keyを求めてflagを出すと、意味ある文字列になりません。
え、これ違うのか。96文字でドンピシャだと思ったのに。問題文をrot13してみたり文字数を変えず色々試してみたのですが、そんな複雑なエスパー問題ではないだろうと考えなおします。
solves数が少ないわけではないので簡単に解けるのかなと思ってたのですが、問題文と暗号文の文字数が一緒だから関係あるのでは?から離れられませんでした。

数日悩み続け、一番最初のflagの先頭5bytesはb"CHTB{"であることからtestquoteの先頭5文字は"I alo"となることから調べていくと上手くいきました。
「quote I alo」と打つと予測で文章らしきものが出てきます。マザーテレサのお言葉でした。

I alone cannot change the world, but I can cast a stone across the water to create many ripples.

最初ヒットしたものは途中の"water"にsが付いており97文字でした。なのでflagが見えているんですが最後ぐちゃぐちゃした感じになりここでも悩みました。残り13文字が上手く復号されなかったので、その13文字を"Mother Teresa"と置けばいいのか?となりましたが違った…。その時に出ていたflagはこちらです。

CHTB{stream_ciphers_with_reused_keystreams_are_vulnerable_to_known_plain'1c;pcptr.>q

上手く復号されなかった部分は"plaintext"と続くのかなと予想します。flagがそうなら追加した"text"の部分からtestquoteはどうなるか確認したところ「to」(※アンダースコアは半角スペース)となり、s以降の文字列と一緒ではと考えsを消せば上手く復号されました。sが無いバージョンのquoteもあったのですが、他に解けた人はこのように解けたのか、はたまた97文字だからおかしいとして別のを探したのか気になります。

CHTB{stream_ciphers_with_reused_keystreams_are_vulnerable_to_known_plaintext_attacks}


SoulCrabber 300pt ★

Aliens heard of this cool newer language called Rust, and hoped the safety it offers could be used to improve their stream cipher.
This challenge will raise 33 euros for a good cause.
ファイル:main.rs, out.txt

Rustは全く触ったことがなく環境整備が難しそうだったので、Rust Playgroundを使いました。

Rust Playground

main.rs を見ると、flagを読み込んで何かとxorして出力しているようです。その何かを求める必要がありそうですね。
get_rng関数では13371337をseedとして何かしらの整数を生成しているようです。結局この仕組みは分からないのですが、このくらいの解釈でも解けました。
ではどんな整数を生成しているのか確かめるために、flagと同じ長さになるように"A"を繋げそれをflagの代わりにして実行してみました。flagの長さはout.txtの長さから分かります。

fn main() -> std::io::Result<()> {
    let flag = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
    let xored = rand_xor(flag.to_string());
    println!("{}", xored);
    Ok(())
}

main.jsのmain関数だけ上記のように変更しました。すると、"19500187e1ba0b5bf3e27914c83f1ffd67123543ffc3ae4a4d22a9d54f" が得られました。
"A"29 xor (13371337をseedとして生成した整数) = 195...54f なので
(13371337をseedとして生成した整数) = "A"
29 xor 195...54f となります。あとはこれと貰ったout.txtとxorして終了です。

CHTB{mem0ry_s4f3_crypt0_f41l}


SoulCrabber2 300pt ★★

Aliens realised that hard-coded values are bad, so added a little bit of entropy.
This challenge will raise 43 euros for a good cause.
ファイル:main.js, out.txt

先程は13371337をseedとしていたのに対し、今回はどうやらUNIX TIMEでseedを取っているようです。ここ2週間で作成したものだろうと高を括って解きました。
問題はRustで関数に引数を渡すやり方がいまいち分からなかったので、main関数で完結させました。

use rand::{Rng,SeedableRng};
use rand::rngs::StdRng;
use std::fs;
use std::io::Write;
use std::time::SystemTime;

fn main() -> std::io::Result<()> {
    let mut counter =0;
    while counter != 86400*14{
        let mut rng = StdRng::seed_from_u64(1617537600+counter);
        let flag = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
        let enc = flag.to_string();
        let xored = enc
            .chars()
            .into_iter()
            .map(|c| format!("{:02x}", (c as u8 ^ rng.gen::<u8>())))
            .collect::<Vec<String>>()
            .join("");
        println!("{}", xored);
        counter += 1;
    }
    Ok(())
}

86400*14のbrute forceなので多少時間はかかりますが、回し切れますね。

CHTB{cl4551c_ch4ll3ng3_r3wr1tt3n_1n_ru5t}


RSA jam 325pt ★★

Even aliens have TLA agencies trying to apply rubber hose cryptanalysis.
This challenge will raise 43 euros for a good cause.
ファイル:rsajam.py

与えられたプログラムを読むと、RSAの公開鍵(e,n)に加え秘密鍵dが与えられます。φ(n)未満の与えられたd以外でme×d2 = m mod n となるd2を送るとflagがもらえるようです。

そもそも0以上φ(n)未満で秘密鍵となり得る自然数が複数存在するのか。フェルマーの小定理から、ap-1 = 1 mod p, aq-1 = 1 mod q (a< min(p-1,q-1) ) です。xがp-1とq-1の倍数とするとオイラーの定理より、ax = 1 mod nとなります。つまり、e×d2 = x + 1 (x = k×(p-1) = l×(q-1), k,lは自然数) となればよいです。

では、上記のの式に当てはまるd2を考えていきます。 e × d = 1 mod φ(n) であるので、e×d-1 = k×φ(n) (k : 自然数)と表せます。e, dは与えられるので、e×d-1を素因数分解して素数を上手く分けるとk × φ(n) という形に分けることが出来ます。
ではその上手く分けるためにはどうするかというと、素因数分解後の素数たちをbrute forceして試すんです。素数の個数の合計が20個以下ならまあ間に合います( O(220) 程度)。ビット全探査です。かなり大きい素数はほとんどの確率でφ(n)の方に含まれるので先に決め打ちしても問題ありません。
e×d-1 = a_1 × a_2 × ... × a_n とした時に、phi = a_x1 × a_x2 × ... × a_xm とおきます。phi = φ(n) であれば n - phi = n - φ(n) = p+q-1 となります。m = n - phi + 1 (= p + q) とおくと、n=p × q という情報もあるので解と係数の関係よりp,qを求めることが出来ます。
二次方程式 x2 - mx + n = 0 の解がp, qとなります。解の公式より m±√(m2 - 4 × 1 × n) / 2×1 が素数となればいいので、ルートの中身が平方数なのか判断すればOKです。求めた解p, qの積がnならnの素因数分解が出来ました。

次に、e×d2 = x + 1 (x = k×(p-1) = l×(q-1), k,lは自然数) となるd2を求めていきます。e×d = 1 + k×φ(n) と表せます。ここで、l = lcm(p-1,q-1)とすると、me×(d+l) = me×d + e×l = m×me×l mod n となります。lはp-1とq-1の倍数であるので、me×l = 1 mod nとなります。故に、d2 = d+l とすれば欲しいd以外の秘密鍵を手に入れられました(d+lがφ(n)を超える場合はd-lでもOKです)。

import gmpy2
import random
import math
e = 65537
pl =[2,2,3,3,5,5,7,11,11,13,191,199,1489]
phi = 2954001204170914691805741860654963857940004074429377692284091070811401369951320958401069220899257193610366982095760499087321176610501426227559528834963391573119792751626794365037601107813476160364027477073371928090485550540475603624680000569693639581200408072914193394481017798545653296930496371873
d = 25279851828946689969935508637891642190361805428614897956944707979466882691402752252273969173866247553416847240691284721506378725185547104028957495800217551084649108905497165090047528206605038035320469604378521860671903204212263641356781239235920533291802966072966655090757438950541684952893977777198807710573
N = 123630001441211791699101815506417771377489862127836323215005247880779127747665261872791516882894729192469212567210262427681780651629370984012072785781572863755222453735206569087396934214192134882937330171191632058382828512367532999211413054935111652810223730835723518262117916443844727685713391668125094593189


for i in range(1<<len(pl)):
    temp = phi
    s = str(bin(i))[2:]
    s = "0"*(len(pl)-len(s)) + s
    #print("s",s)
    for j in range(len(pl)):
        if s[j] == "1": temp *= pl[j]
    M = N-temp+1
    #print(M,N)
    rot, ch = gmpy2.iroot(M*M-4*N,2)
    if ch:
        print("find!")
        p = (M-int(rot))//2
        q = (M+int(rot))//2
        phi = (p-1)*(q-1)
        lc = (p-1) * (q-1) // math.gcd((p-1), (q-1))
        print(d+lc)
        print(d+lc < phi)
        m = random.randrange(N)
        c = pow(m,e,N)
        m1 = pow(c,d+lc,N)
        print(m == m1)

e×d-1を出力した後はfactorDBで素因数分解したのでsolverらしきものは作ってはないです。素因数分解後、上のプログラムでd2を求めて手打ちで送って終了です。

CHTB{lambda_but_n0t_lam3_bda}


Problems

問題ファイルやsolverはこちらからどうぞ!

github.com

Plaid CTF 2021 XORSA writeup


Plaid CTF お疲れ様でした!

 

問題数は多いわけではないのですが、難易度が高い…。XORSAをなんとか解けたのですが他の問題には全く歯が立たず、結局この一問のみのsolveです。

 

plaidctf.com

Result

f:id:partender810:20210419111339p:plain
145位でした

 

XORSA Writeup

XOR + RSA = XORSA
ファイル:xorsa.29fb8e0ef0173eef5953d792373b7db98169018db6747f5e27d26c1c7cb98873.tgz 

linux tgz 解凍」でググります。どれ試しても上手くいきません。出てきたエラーをググるとこんな親切なサイトが!

 

tar.gzファイルの解凍 - ZDNet Japan

 

fileコマンドを叩いてみると

$ file test.tar
test.tar: POSIX tar archive (GNU)

と出るので、このサイトの通りmvコマンドでtest.tarなどにしておきます。そしてtar xvfで解凍できました! いやもう最初からtarファイルで配ってくれ。  

$ tar xvf test.tar
dist/
dist/xorsa.sage
dist/public.pem
dist/flag.enc

 

xorsa.sageが問題プログラムということで覗いてみます。

xはpとqのxorした値です。 keyをRSA.constructに(n,e,d,p,q)を投げて生成し、それを用いてPKCS1_OAEPのインスタンスを作ります。 flagを暗号化したものや公開鍵などをファイルに出力します。

まずは公開鍵を知りたいのですが、public.pemを見るとn = 整数という形ではなくopenSSLを用いて解読する必要がありそうです。 openSSLは良くは知らないのですが、このサイトやpicoGymで出た問題から解読できました。

How to extract public key using OpenSSL? - Stack Overflow

picoCTF

openssl rsa -noout -text -inform PEM -in public.pem -pubin

これで以下の結果を得られました。

$ openssl rsa -noout -text -inform PEM -in public.pem -pubin
RSA Public-Key: (4096 bit)
Modulus:
    00:8c:6d:db:f9:a2:dd:53:a2:46:6f:f6:1b:b6:2c:
    f5:be:3a:f8:9d:e8:21:bb:75:a5:81:ab:c8:51:4a:
    84:39:2a:66:13:ea:a3:26:9c:15:98:43:ee:f7:81:
    df:55:22:a9:2d:2d:5a:dc:3c:95:a5:4c:5f:eb:42:
    7d:08:2c:15:7d:4f:2d:b6:90:14:11:c0:ce:29:f1:
    57:15:73:fb:48:88:85:ee:c8:d3:6f:8c:79:48:2b:
    51:23:a8:20:47:d7:95:88:a9:5f:b2:c1:a1:67:10:
    42:78:5f:5d:0c:14:69:eb:a2:18:58:d6:43:f6:64:
    65:9f:9e:63:03:39:5a:05:da:bb:05:03:f2:e8:80:
    a8:99:a6:7e:78:40:bd:20:fa:c2:83:16:93:62:09:
    28:d0:c9:0c:47:ab:d6:95:d0:0d:27:e4:8e:78:62:
    a3:15:01:59:ac:58:e5:0b:64:d1:c9:d4:6c:c7:c6:
    c1:95:df:61:ec:0f:ef:79:93:7f:55:45:b0:8a:53:
    51:73:82:c0:73:62:fc:8a:24:75:a9:9e:9c:b4:1e:
    4f:c5:44:a7:00:e1:1c:a2:3c:65:8a:df:ce:d9:ee:
    bb:79:5e:3a:a5:a3:32:d1:e3:9b:c7:28:d8:e0:39:
    f0:7d:2b:2b:f9:3c:4e:11:20:a3:d4:cb:1b:7a:e1:
    e0:d7:8c:ea:c3:75:95:f0:bd:99:4e:c7:8e:bf:8d:
    39:c5:fb:12:d8:03:cd:f9:bb:01:9a:6f:da:4d:20:
    bf:fd:e8:fe:f0:52:a6:18:94:33:ed:cf:94:f6:d7:
    bb:6a:8d:9c:c1:91:96:50:e0:13:9b:6f:28:4d:44:
    bf:1d:67:2e:5c:e8:a5:6f:2e:7b:d3:e2:eb:ee:6f:
    b7:f6:31:e0:03:9c:f5:07:9e:cf:74:b1:c7:56:5e:
    60:58:b8:1f:46:b5:b6:6e:af:7f:83:4e:94:2c:ce:
    5a:6c:c3:bd:47:ef:4b:d4:18:b1:79:c1:11:4a:a0:
    fb:68:ab:3f:aa:91:ed:bc:51:04:07:e1:05:24:b3:
    79:0f:04:e3:b2:7e:46:eb:c2:5e:5b:b2:b9:34:d4:
    e4:6b:f0:94:05:a5:67:f3:c7:e7:7c:e3:f0:ae:e3:
    83:bd:0c:8d:72:ec:b2:a3:99:73:7c:3f:d2:79:26:
    1b:f2:7b:7c:cf:ee:8b:b2:14:1e:20:2e:fb:78:68:
    7d:15:85:60:9a:af:7c:2c:6f:35:97:f8:4b:00:3b:
    55:a9:f2:ea:d1:ae:df:e0:00:56:cb:b5:50:99:42:
    c2:88:dd:d2:08:fc:05:da:80:33:35:b5:e0:62:b2:
    70:9e:f5:9f:d0:e3:d4:ea:21:7b:8f:29:5d:4a:c0:
    21:ff:37
Exponent: 65537 (0x10001)

 

この16進数をsplit(":")などで強引に整数型として入れます。

次に、暗号化されたflagについてみていきます。
とりあえずbytes_to_longしてみると、なんとnより大きい値に…。これじゃ復号できないじゃないか!
わざわざPKCS1_OAEPを使っているということはここに復号の方法があるのではないかと手元で動かしてみたら、key = RSA.construct( (n,e,d,p,q) )で暗号化時と同じパラメータを投げて生成したkeyによるインスタンスでdecryptすればOKです。

>>> key = RSA.construct((n,e,d,p,q))
>>> flag = b"m1z0r3{test_flag_yeah}"
>>> cipher = PKCS1_OAEP.new(key)
>>> enc = cipher.encrypt(flag)
>>> cipher.decrypt(enc)
b'm1z0r3{test_flag_yeah}'

 

まあ、普段のRSAと同様(n,e,d,p,q)の全てを求めたらOKですね。頑張って求めていきましょう。

nは当然素因数分解出来ませんし、情報としてもp xor qのみ公開されています。これを使ってp,qを求めるのだろうとなりますね。
とりあえずいつもの「RSAでやってはいけないnのこと」を見て、pの上位ビット(1/2程度)知られるとダメとなるcoppersmithかなぁと目星を付けます(違った)。「xor multiple」でググっても複数入力のxorしか出てきません。
数学的な問題なのでm1z0r3の黄coderに聞いてみました。「bit毎に見ていけばいけるのでは?」なるほど。

p,qを2進数でのかけ算の筆算を頭に浮かべると、例えばp,qの下位3bitsが決まればnの下位3bitsが決まります(p,qの下位3bits以外はnの下位3bitsに影響しません)。 pを01どちらかを最上位に追加するbrute forceを行ってどんどんbits数増やしていき、増やしたpの01とxor結果よりqの最上位に追加する01が決まります。増やす途中のp*qの下位bitsとnの下位bitsが一致しなかったら、そのp,qは違うとなり一致していたbitまで戻り01の違う方を最上位に追加する再帰で考えます。

最初これを考えた時は直感的にO(22048)では?となり無理だなと思っていたのですが、最悪計算量を考えたらO(20482)程度かなと思っています(紙に書いたらそうなりました、頭の良い人証明して)。すぐさま実行に移りました。
pの候補が2048bitsの時にnで割り切れるかどうかでチェックして余りが0なら終わりです。

p,q,xorの値からtp="11", tq="01"(temp_p(q)の略)であることが分かるので、後は再帰で解きます。python再帰回数上限を設定しないと怒られるのでsysライブラリから設定します。 とりあえず3000に設定しておきました

ciphertextについては、最後の改行があるとincorrect lengthと怒られるのでそれを抜いてdecryptすると上手く復号されました。

pを仮に置くとqが一意的に決まってしまうことが素因数分解(?)の計算量削減となる脆弱性出したという感じですね。

今回からmarkdown記法で書いているので、solverを張り付けますね(最初からやれ)。githubには完全版と問題ファイルも添付してます。

GitHub - ksbowler/PlaidCTF_2021

import sys
from Crypto.Util.number import *
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

n = (省略)
x = (省略)

M = 2048

binn = str(bin(n))[2:]
binx = str(bin(x))[2:]
binx = "0" + binx
sys.setrecursionlimit(3000)
def recur(tp,tq):
    #再帰でpを求める。tp,tqはp,qの候補の値
    #求めたらpを返し、現在のtp,tqでは求められないのであればFalseを返す
    bits = len(tp)
    if bits == M:
        #tpが2048bitsになった場合
        p = int(tp,2)
        if n%p == 0:
            print("Find!")
            print(p)
            return p
        #nで割った余りが0でないから、tpはpではない
        return False
    tn = int(tp,2)*int(tq,2)
    if str(bin(tn))[-bits:] != binn[-bits:]:
        #tp,tqの積の下位x bits (xはtpの長さ。変数bitsと同義)がnの下位x bitsと異なるのであれば、tp,tqはp,qの候補値から外れる。
        return False

    #xの値より、tp,tqに追加すべき候補は2通りに絞られる
    if binx[-(bits+1)] == "1":
        p = recur("1"+tp,"0"+tq)
        if p: return p

        p = recur("0"+tp,"1"+tq)
        if p: return p
    else:
        p = recur("1"+tp,"1"+tq)
        if p: return p

        p = recur("0"+tp,"0"+tq)
        if p: return p

    return False

p = recur("11","01")
q = n//p
e = 65537
phi = (p-1)*(q-1)
d = inverse(e,phi)
f = open("./dist/flag.enc","rb")
a = f.readlines()
key = RSA.construct((n,e,d,p,q))
cipher = PKCS1_OAEP.new(key)
ciphertext = a[0] + a[1][:-1] #最後の改行が入ると悪さをするのでそれを抜く
flag = cipher.decrypt(ciphertext)
print(flag)

PCTF{who_needs_xor_when_you_can_add_and_subtract}


おわりに

leaky block cipherの途中経過だけでも供養します。

hashcashの突破について、以下の文字列を投げる必要があります。

  • ":" でsplitされる
  • split後、0番目には"1" とする
  • 1番目は"21"より大きい値にすべきだが、大きくし過ぎると後のPoWがきついので"22"とする。
  • 3番目は最初に貰った16進数
  • 2番目は"100", 4,5番目は"0"とテキトーに決める。
  • 全体のSHA-1ハッシュ値を16進数にした時上位5桁が"0"になるようPoWする。

これで突破できます。

しかし、それから自作のクラスのインスタンスのencryptが予測できず終了です。

関係ない話ですが、Markdownで目次が上手くいかないのなんでだろう。

RITSEC CTF 2021 writeup

picoCTF, angstromCTFと2戦連続調子は良かったのですが、このRITSEC CTFはてんでダメでした…。crypto問題は歯が立たず、OSINT問題もチームメイトに先を越され…。cryptoを2問だけ紹介します。自力で解けてませんが…。

 

調子が良かった頃はこちら!

partender810.hatenablog.com

 

partender810.hatenablog.com

 

 

 

Result

f:id:partender810:20210413111611p:plain

1ptも貢献してません…

 

Writeup

Crypto

What an Enigma? 100pt

Turing with 3 allies was the bomb...

FCIMWWCNRAQQGAEKMKNPJIP

 

Enigmaなんていう暗号あったなぁ。調べてみるか。

 

ja.wikipedia.org

 

ざっくり言うと換字式暗号のようです。詳しいアルゴリズムは分からないので、decoderが無いかググってみたらお世話になっているdcodeを発見!

 

Enigma Machine Cipher - Decoder, Encoder, Solver, Translator

 

Resultsの欄に "RSWTCHIMTTIONGAMEISWRTH" とあり、今回のCTFのflag formatはRS{}とのことなのでビンゴ!と RS{WTCHIMTTIONGAMEISWRTH} で提出したらincorrect。うーーん、こんな簡単じゃない?確かに0 solvesだったからもう一つひねるのかな?もっとアルゴリズムを勉強しないと、と時間をかけてました。

 

そしたら、チームメイトが同じ文字列で通ったとのこと。キレそう。

出来た時にちゃんとcorrectだったら1 solves目だったのに…。

 

RS{WTCHIMTTIONGAMEISWRTH}

 

 

lorem ipsum 150pt

Flag is case sensitive.

f:id:partender810:20210411150524j:plain

hint.jpeg

ファイル:cipher.txt

問題に女性の写真が添付されてます。保存しようとするとデフォルトでhint.jpegになることから、この画像が問題を解くのにかなりのヒントになることが予想されます。

 

cipher.txtは以下の通りです。

Incompraehensibilis Conseruator.
Redemptor optimus
Iudex omnipotens
Sapientissimus omnipotens
Redemptor fabricator
Iudex redemptor
Optimus magnus
Aeternus iudex
Auctor omnipotens.

 

RSが見えるからこの単語の頭文字を入れ替えるとか…?と思いましたが、とりあえず貰った画像を検索してみます。どうやら聖母マリア様の絵のようです。

 

"maria cipher" でググるTrithemius ave maria cipherというのがあるよう。dcodeも対応しているようなのでcipher.txtを投げてみると、Resultsに"RSTHISISTRITHEMIUS"と出てきたので、これはもう出来たと{}を付けて提出。incorrect。

 

https://www.dcode.fr/trithemius-ave-maria

 

大文字小文字は区別すると問題文にあるので単語の頭文字のみ大文字にするもダメ。What an Enigma?からこのスコアサーバに不信感があり、「俺嫌われてるから他の人やってみてw」と今までの経緯をslackに残しチームメイトに託します。

 

そしたらすぐスコアサーバ内でこの問題が解けたと色が変わりました。また俺嫌われてるか…と思っていたらflagの文字列が違う。

 

ここからは、解けたチームメイトによるお話なのですが、Trithemius ave maria cipherとは平文一文字に対して英単語一つ生成するようです。cipher.txtの先頭行だけdcodeに投げるとRSだけ返されます。そこから、case sensitiveから対応する単語の頭文字が大文字なら平文を大文字に、そうでないなら小文字にと変えたら通ったようです。

 

スコアサーバに嫌われてるなど変なこと言ってないでちゃんと調べて考えろ、って話です。悔しい…。

 

RS{ThIsIsTrItHeMiUs}

 

 

今回は自力で解けた問題は一つも無く、何なら自分が通した問題すら無いのですが、戒めを込めてブログに残しておきます。解けたチームメイトに許可を頂けたのですが、おそらくニヤニヤしながら通したことを考えると歯がゆく悔しい…。次回はこうならないようがんばりたいですね!

 

それでは!

 

See you next time!

 

 

picoCTF 2021 writeup

こんにちは!Ken.Sです!

 

つい最近に留学の研究後記ブログを書いたばかりですが(と書いて保存してたらangstromCTFの方を先に公開することになるとは…)、今回もwriteupを書いていきます!今回かなり頑張ったので読んでくださると本当に嬉しいです!

 

picoCTFはチーム上限5人で、m1z0r3からは8人の参加希望があったので2チームに分けて対決してました!私率いるm1z0r3チームはなんとかもう一つのkum0r1に勝てましたドヤァ。AtCoderやってる人ならわかるtourist出しをやりました(笑)

 

また、picoCTFはpicoGymという形で問題が残るのであとで復習が出来るのが素晴らしい!勉強会で扱う問題を探しているときに実際の問題が残ってるの本当に助かるので有難いですね。

 

※問題文にヒントがありましたが、使ってないのは記載してません。

 

※もしこれを読んでくれているm1z0r3メンバーがいたら、一部問題を勉強会に使います!どれを使うかはもう決めているのでその問題の解説の最初に一言書いておきます。読んでもいいけど勉強会つまらなくなっちゃうよ!

 

 

 

 

Result

Ranking

f:id:partender810:20210331131455p:plain

Global 順位

f:id:partender810:20210331131520p:plain

Japanese Students 順位

全体で181位、日本人学生チームで6位でした!!

1位のチーム1万pt超えすごいなぁ…

 

Solves

Rev 9/17問

For 8/13問

Bin 6/16問

Cry 15/17問

Web 9/18問

Gen 7/7問

 

担当したcryptographyをこんなに解けてかなり嬉しいです!他の方に参考になれば幸いです。

 

 

Writeup

General skill

Obedient Cat 5pt

This file has a flag in plain sight (aka "in-the-clear").

ファイル:flag

見たらFlagがありました。

 

picoCTF{s4n1ty_v3r1f13d_1a94e0f9}

 

 

Python Wrangling 10pt

Python scripts are invoked kind of like programs in the Terminal... Can you run this Python script using this password to get the flag?

ファイル:ende.py, flag.txt.en, pw.txt

コード内に書いてあるusageの通りにやってもエラーを吐いてしまい…ende.pyに直接貰ったファイルの文字列を入れてみるとFlagが出ました。

 

picoCTF{4p0110_1n_7h3_h0us3_192ee2db}

 

 

Wave a flag 10pt

Can you invoke help flags for a tool or binary? This program has extraordinarily helpful information...

 ファイル:warm

./warmしてみます。

Hello user! Pass me a -h to learn what I can do! 

 

./warm -hしてみます。

Oh, help? I actually don't do much, but I do have this flag here: picoCTF{b1scu1ts_4nd_gr4vy_616f7182}

 

strings warm | grep "pico" でも見つけられますね

 

picoCTF{b1scu1ts_4nd_gr4vy_616f7182}

 

 

Nice netcat... 15pt

There is a nice program that you can talk to by using this command in a shell: $ nc mercury.picoctf.net 7449, but it doesn't speak English... 

ncしてみると8bit程度の数値がいくつか送られてきます。

最初の数値は112でchrしてみると'p'。これはASCIIですね。

 

picoCTF{g00d_k1tty!_n1c3_k1tty!_f2d7cafa}

 

 

Static ain't always noise 20pt

Can you look at the data in this binary: static? This BASH script might help!

ファイル:static, ltdis.sh

strings static でも一発で出ますがちゃんとやるならば ./ltdis.sh static -> static.ltdis.strings.txtを見るでOKです。

 

picoCTF{d15a5m_t34s3r_f5aeda17}

 

 

Tab, Tab, Attack 20pt

Using tabcomplete in the Terminal will add years to your life, esp. when dealing with long rambling directory structures and filenames: 

ファイル:Addadshashanammu.zip

まずunzipで解凍します。

 

そうするとこんな感じに

f:id:partender810:20210330120105p:plain

unzip結果

Tabキー使って長いディレクトリ名を打つ手間を省きます。fang-なんちゃらのディレクトリまで行きfileコマンドでこのファイルを調べるとELFだということが分かります。./f(Tabキー) で実行するとflagを出してくれました。

 

picoCTF{l3v3l_up!_t4k3_4_r35t!_6f332f10}

 

 

Magikarp Ground Mission 30pt

Do you know how to move between directories and read files in the shell? Start the container, `ssh` to it, and then `ls` once connected to begin. Login via `ssh` as `ctf-player` with the password, `ee388b88`

問題文の横にLaunch Instanceがあるのでクリックしてsshすべきサーバを教えてくれます。 

lsコマンドで何入ってるか見ると「1of3.flag.txt」と「instructions-to-2of3.txt」が入ってます。前者を見るとflagの途中しか見せてくれません。1of3だから仕方ないですね。次のflagを教えてくれそうなtxtファイルがありますね。

 

Next, go to the root of all things, more succinctly `/`

 

cd / ですね。lsとcatで「2of3.flag.txt」と「instructions-to-3of3.txt」を見ます。後者はこう書かれてました。

 

Lastly, ctf-player, go home... more succinctly `~`

 

cd ~ で移動し最後のflagの欠片を貰います。

 

f:id:partender810:20210330121904p:plain

サーバに侵入中…

 

picoCTF{xxsh_0ut_0f_\/\/4t3r_3ca613a1}

 

 

Web Exploitation

Ancient History 10pt

I must have been sleep hacking or something, I don't remember visiting all of these sites... http://mercury.picoctf.net:52731/ (try a couple different browsers if it's not working right) 

このサイトにアクセスしてソースを見ると変な関数が定義されてますね…長すぎる。

 

f:id:partender810:20210327145332p:plain

ソースコード内の関数

よくみたら同じ内容でした。ただ、同じに見えて実は違うところがあるのでは?という発想になるまで時間がかかり、調べてみたら上の画像の中央下部分にある/index.html?pとある箇所が次の行と異なりました。あとはここを抽出したらOK! と思ったのですが…incorrect

 

前の行と違うところだけを見ていたのでflag内で同じ文字が続いた場合、そこを飛ばしてしまってました。そこを注意してcorrectでした。

 

picoCTF{th4ts_k1nd4_n34t_bb660d55}

 

 

Binary Exploitation

What's your input 50pt

We'd like to get your input on a couple things. Think you can answer my questions correctly? 

nc mercury.picoctf.net 56827

ファイル:in.py 

Hint : What version of python am I running?

ヒントを基にコードを見ると2系。print("...") とあったのでよく見ないと分からないですね。

 

31行目のif res == city: がTrueになればFlagが手に入りますが、変数cityに何が入っているか分かりません。

 

これはチームメイトに教えてもらいましたが、2系はinput関数に脆弱性があるとのことで、入力された文字列と同じ名前の変数があるときにその内容がコピーされてしまうとのことです。なので都市の入力の際に"city"と打ち込めばOKです。

 

2系のあからさまな脆弱性を初めて体感したなぁ。

 

picoCTF{v4lua4bl3_1npu7_1599789}

 

 

Forensics

Wireshark doo dooo do doo... 50pt

Can you find the flag?

ファイル:shark1.pcapng

 

pcap系の問題はあまり解いたことがないのですがsolve数が多いので行けるのかなと思ったのが苦戦しました(笑)

 

Wiresharkのfilterを使ってパケットを絞るのだろう、どんなfilterが良いのかは過去のwriteupを見てました。

 

www.slideshare.net

 

「http.request && tcp.port == 80 」とやればパケットが二個に絞られました。あとはそのパケットをTCPストリームすると…

Gur synt vf cvpbPGS{c33xno00_1_f33_h_qrnqorrs}

 

あとはrot13ですね。

 

picoCTF{p33kab00_1_s33_u_deadbeef}

 

 

Cryptography

Mod 26 10pt

Cryptography can be easy, do you know what ROT13 is? cvpbPGS{arkg_gvzr_V'yy_gel_2_ebhaqf_bs_ebg13_jdJBFOXJ}

rot13ですね。さっきやったよ。

 

picoCTF{next_time_I'll_try_2_rounds_of_rot13_wqWOSBKW}

 

 

Mind your Ps and Qs 20pt

In RSA, a small e value can be problematic, but what about N? Can you decrypt this?

ファイル:values

そりゃNが小さいのは問題でしょうよ…。NをfactorDBで殴って終了です。

 

picoCTF{sma11_N_n0_g0od_73918962}

 

 

Easy Peasy 40pt

A one-time pad is unbreakable, but can you manage to recover the flag? (Wrap with picoCTF{}) 

nc mercury.picoctf.net 41934

ファイル:otp.py

Hint : Maybe there's a way to make this a 2x pad.

この問題、配点の割には時間がかかってしまいました。

 

keyの長さが50000で入力された文字列のchrとxorしてくれます。初めにflagをxorしたのをくれますが、keyが分からないので復元できません。10文字を入力に対してkey[i:i+10]を使うとしたら、次の入力に対してはkey[i+10:]が使われます。

 

最初にflagのxorにkeyを使っており、与えられた値とソースよりflagは32文字ということと、key[0:32]を使ってxorしたことが分かります。

 

このkey[0:32]を知りたいのですが、keyの並びに規則性あるのかな…全く見えず…ここでかなり苦戦しました。

 

入力の文字列長の合計が50000越えるようにして、keyを一周させればいいじゃん。"a"*(50000-32)をぶん投げ、次の入力でkeyの0文字目から使えるようにします。また"a"*32でも投げて得られた暗号文よりkey[0:32]を求めます。それを使って最初にもらった暗号化されたflagを復号して完了です。

 

picoCTF{abf2f7d5edf082028076bfd7a4cfe9a9}

 

 

New Caesar 60pt

We found a brand new type of encryption, can you break the secret code? (Wrap with picoCTF{})

ihjghbjgjhfbhbfcfjflfjiifdfgffihfeigidfligigffihfjfhfhfhigfjfffjfeihihfdieieih 

ファイル:new_caesar.py

new_caesar.pyによってこの暗号文が作られたようです。復元頑張りましょう。

 

flagという文字列を引数としたb16_encode関数によってb16文字列を生成し、その文字列を一文字ずつkeyによってshift関数で暗号文を生み出した結果こんなものになりました。

 

b16_encode関数はflagの一文字ずつをASCIIコードにして前4bit, 後4bitに分けます。その値によってa~pを決めます。0000ならaで1111ならpですね。flagの二倍の文字列長になります。

 

shift関数はkeyの文字によってa~pの間でrotateします。

 

復号はやるだけ、って感じでしたね。実装が面倒ですが…。

 

流れとしては、keyの長さが1でa~pのどれかと分かるのでkeyについてはbrute forceしてnew_caesar.py内の変数b16を求めます。

 

shift(c,k)の返り値をalfとすると、(t1+t2)%16 = alfとなるt1を求めます。alfは暗号文の1文字であり、kはbrute forceのkeyでt2はそれに基づく値です。t1に対応する文字列を足していくとb16となります。

 

最後のステップはb16からflagを復元します。

b16を2文字毎に見ていき、(前1文字 - "a")*16+(後1文字 - "a") をchrすれば復元できました。

 

brute forceしているので16種類のflagの候補が出てきます。その中で一番ぽいのを選んで終わりです。

 

gbgcgdgegfggghgi
qQqRY[YSVUT[UYWWWYUYTS
v`@`AHJHwBEDvCurJuuDvHFFFuHDHCvvBssv
et_tu?_0797f143e2da9dd3e7555d7372ee1bbe
TcNcd.N/&(&U #"T!SP(SS"T&$$$S&"&!TT QQT
CR=RS=DCBOBBCBCC@@C
321>112122??2
!01ûóõó"ýðÿ!þ -õ ÿ!óñññ óÿóþ!!ý..!
/
/ ê
ëâäâìïîíäîâàààâîâíì
ùÙùÚÑÓÑÛÞÝÜÓÝÑßßßÑÝÑÜÛ
ÈèÉÀÂÀÿÊÍÌþËýúÂýýÌþÀÎÎÎýÀÌÀËþþÊûûþ
íü×üý·×¸¿±¿î¹¼»íºìé±ìì»í¿½½½ì¿»¿ºíí¹êêí
ÜëÆëì¦Æ§® ®Ý¨«ªÜ©ÛØ Û۪ܮ¬¬¬Û®ª®©ÜܨÙÙÜ
ËÚµÚÛµÌËÊÇÊÊËËËÈÈË
ºÉ¤Éʤ»º¹¶¹¹º¹ºº··º
©¸¸¹st{}{ªuxw©v¨¥}¨¨w©{yyy¨{w{v©©u¦¦©
§§¨bcjljdgfelfjhhhjfjed

 

keyは"d"でした。

et tu? は「(ブルータス、)お前もか?」らしい。

 

picoCTF{et_tu?_0797f143e2da9dd3e7555d7372ee1bbe}

 

 

↓m1z0r3勉強会採用問題

Mini RSA 70pt

What happens if you have a small exponent? There is a twist though, we padded the plaintext so that (M ** e) is just barely larger than N. Let's decrypt this:  

ファイル:ciphertext

ciphertextにはN, e, cが書いてあります。

 

e=3であることとM^eがNよりちょっと大きいことから、c+N*k (k : 自然数)が立方数かを判断して、違うのであればkをインクリメントする、立方数ならばその3乗根を出してlong_to_bytesで終わりです。

 

picoCTF{e_sh0u1d_b3_lArg3r_60ef2420}

 

 

Dachshund Attacks 80pt

What if d is too small? Connect with nc mercury.picoctf.net 58978.

serverに繋げるとn, cととてつもなく大きなeが送られてきます。

 

eが大きいとdが小さくなって成立する攻撃あったな、Wiener's attackか。

 

公開鍵暗号 - RSA - Wiener's Attack - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです

 

実装例を丸パクリで終了。

 

picoCTF{proving_wiener_6907362}

 

 

No Padding, No Problem 90pt

Oracles can be your best friend, they will decrypt anything, except the flag's ciphertext. How will you break it? Connect with nc mercury.picoctf.net 28517.

Hint : What can you do with a different pair of ciphertext and plaintext? What if it is not so different after all...

このserverは、flagの暗号文以外は復号してくれるオラクルとなっています。

 

最初に送られてくる「Padding oracle attack」が本当にそうか怪しい。平文は恐らくb"(パディング)picoCTF{temp}(パディング)"と予想(違った)。cの値を変えて送ったら復号して違うパディングの平文が手に入るのかなと最初はこんな感じでした。

 

上手くいかないので、RSA Oracle Attackでググってみると、以下のサイトが。

 

plain RSAに対するLSB decryption oracle attackをやってみる - ももいろテクノロジー

 

mのbit数だけ問い合わせをするのに対して、このoracleは途中で止まってしまう仕様…。とりあえずこのサイトにある別の方法(リンクがある)をやってみます。

 

RSAに対する適応的選択暗号文攻撃とパディング方式 - ももいろテクノロジー

 

これやってみたら一発。

 

picoCTF{m4yb3_Th0se_m3s54g3s_4r3_difurrent_4005534}

 

 

It's Not My Fault 1 100pt

 What do you mean RSA with CRT has an attack that's not a fault attack? Connect with nc mercury.picoctf.net 47414

ファイル:not_my_fault.py

はじめに、これ解けたの本当に嬉しい!!今回解けた中で一番solves数が少ない問題でした!

 

このserverはまずProof-of-Workをします。これはやるだけで特に問題に関係ないですね。

 

次に512bitの素数p,q (※gcd(p-1,q-1) = 2) を生成し公開鍵n(=p*q)を公開します。d_pは1~2^20の間で、d_qは1~q-1の間でランダムで決めます。秘密鍵dは中国人剰余定理の要領で、(p-1)で割ったらd_pかつ(q-1)で割ったらd_qとなる(p-1)*(q-1)=phi以下の自然数が採用されます。phiを法としたdの逆元eを公開鍵として公開します。

 

最後に15分以内でp+qの値をserverに送り一致すればflagが手に入ります。

 

最初はp-1とq-1でCRTしているところに注目しました。gcd(p-1,q-1) = 2 なのでd_p, d_qが偶数である場合、秘密鍵dは偶数になり(p-1)*(q-1)での逆元は存在しないことになります。この点が何かしらの脆弱性なのかなと調べてみたけど分からず…。

 

次に気になった点としては、p+qを答えさせるのに15分もの猶予があることです。計算させるにもこれだけの時間がかかるとしたらbrute forceだろう。brute forceするとしたらd_pの値は最大100万程度だからいけるはず。けどd_pの値を仮に置いたところで何が分かるんだろう…。

 

RSA

 

このサイトを見て直感的に思ったこととして、自分でmをテキトーに設定し与えられたe, n で暗号化する。d_pをbrute forceして仮に置きd_pを秘密鍵のようにpow(c, d_p, n)を計算しm'を得る。m = m' ならいいなと思ったけどそんなことはありませんでした。

 

貰ったファイルを手元で動かしp, q, d_p, d_q, dなどを出力し色々確かめてみます。本来の秘密鍵を使えばm = m'になるはずですがそうはならないのでこの差分に何があるのかと調べたところなんと(m' - m)%p = 0でした!

 

終わってから考えるとフェルマーの小定理を使えば証明できました!

 

f:id:partender810:20210330231540p:plain

d_p = d mod p-1 がバレると復号出来てしまう証明

 

正しいd_pを選ぶとm' - m = pow(c, d_p, n) - pow(c, d, n) = k*p (kは整数)となります。なのでd_pをbrute forceしてgcd(m' - m, n) != 1になるようなd_pを見つけます。gcdが1出ない時それはpの値と合致するので p+q は簡単に求められます。

 

手元で動かすと分かるのですが、上記の式はe*d = 1 mod φ(n) である時しか成り立ちません。m = c^d mod nとならないためですね。d_p, d_qが偶数の時は逆元は存在しないので、上手くいくためには何回かserverにつなげて運良く奇数になることを願うのみです。

 

秘密鍵dを(p-1) or (q-1)で割った余りが分かると解読される脆弱性があるとは知りませんでした。

 

It's Not My Fault 2 ではd_pの範囲が1 ~ 1<<36 に増えてました。brute force出来ないようになってますね。どうにかしてd_pの候補を1<<36から1/(2^16)ほどカットできる方法があるのかな…?

 

picoCTF{1_c4n'7_b3l13v3_17'5_n07_f4ul7_4774ck!!!}

 

 

↓m1z0r3勉強会採用問題

Play Nice 110pt

Not all ancient ciphers were so bad... The flag is not in standard format. nc mercury.picoctf.net 21003

ファイル:playfair.py

 

Playfair cipherという暗号があるらしい。もらったコード内にもそのwiki URLがあるけどこの問題ではこのwikiを理解する必要は全くなく、コードを読んで暗号アルゴリズムを理解すれば良かったです。言ってみればただの換字式暗号でした。

 

暗号化するためのmatrix(6*6の大きさでアルファベット小文字と半角数字をランダムで散りばめたもののよう):既知 ncすれば教えてくれる&コード内にもある
暗号化されたメッセージ:既知 ncすれば教えてくれる元のplaintext = msgを二文字ずつ暗号化していく。(msgが奇数長なら"0"を加えるが今回はそこまで重要じゃない)
その二文字をs1,s2とすると、まず両者のindexを調べ("0" -> (0,0) "f" -> (5,2) etc)、p1,p2に格納する。
enc_msg = ""
if s1,s2が同じ行にある文字:
enc_msg += s1の一つ右+s2の一つ右の文字
elif s1,s2が同じ列にある文字:
enc_msg += s1の一つ下+s2の一つ下の文字
else:
p1x : s1の行番号, p1x : s1の列番号だとする
enc_msg = matrix[p1x][p2y] + matrix[p2x][p1y]このenc_msgが暗号化されたメッセージである

 

復号はこんな感じです。

enc_msgを2文字毎見る -> e1,e2
if 同じ行: 一つ左の文字を足す
elif 同じ列: 一つ上の文字を足す
else: matrix[e1x][e2y] + matrix[e2x][e1y]

 

cryptoというよりかはプログラミングでしたね。

 

3f4b60ebf36369258d8638d2038c7ad1

 

 

↓m1z0r3勉強会採用問題 

Double DES 120pt

I wanted an encryption service that's more secure than regular DES, but not as slow as 3DES... The flag is not in standard format. 

nc mercury.picoctf.net 31991

ファイル:ddes.py 

このserverは、まずKEY1, KEY2と生成します。0~9から6文字と空白2文字です。そこから入力を受け付けunhexlifyやpaddingなどをしたあと、DES方式でKEY1で暗号化しさらにKEY2で暗号化したものを送ってくれます。

 

DESを2回やる2DESが悪いって何か聞いたことあるな…。これだ!

 

中間一致攻撃 ‐ 通信用語の基礎知識

 

つまりは平文と暗号文が分かっている場合、鍵1と鍵2をbrute forceして求めることが出来ます。暗号文を鍵2でdecodeして鍵1でdecodeするというやり方ですと、鍵の長さをNとした際にO(N^2)とかなり時間がかかってしまいます。

 

そこで、平文を鍵1でencodeしたもの(Aとする)と、暗号文を鍵2でdecodeしたもの(Bとする)を記録しておきます。Aの中からBに含まれているものを探し、一致していればそのencode/decodeしたものが正しい鍵ペアとなり、探索を二分探索で行えばO(N log N)で済みます。birthday attackですね。

 

今回、ddes.pyを見ると0~9の中からランダムに構成された6文字と2文字分のスペースがそれぞれ鍵1,2に採用されています。つまり鍵1,2ともに10^6=100万通りしかありません。

 

サーバは初めに2DESによって暗号化されたflagを公開し、平文を入力すると同じ鍵ペアで暗号化してくれます。この平文と暗号文を頼りに上記の方法でbrute forceします。

 

pythonで「平文-(鍵1)->暗号文1」と「暗号文2-(鍵2)->暗号文1」の生成を行い、テキトーなファイルに"暗号文1 鍵1(もしくは鍵2)"という形で出力します。次に暗号文1が一致しているかの探索はpythonでやると遅いのでc++で記述しました。「暗号文2-(鍵2)->暗号文1」で生成された文字列をソートしておき、「平文-(鍵1)->暗号文1」を二分探索します。O( (KEY1の個数) * log(KEY2の個数) ) = O(10^7)程度ですね。競プロをやってた経験がここで生きました。

 

鍵長が短いためか異なる鍵ペアでも同じ結果となるものが多かったです。

 

あとは見つけた鍵で暗号化されたflagを復号して終わりです。

 

6d4e063d16d250b953d009e2ef07e241

 

 

↓m1z0r3勉強会採用問題

Compress and Attack 130pt

Your goal is to find the flag.  

nc mercury.picoctf.net 29350

ファイル:compress_and_attack.py

Hint : The flag only contains uppercase and lowercase letters, underscores, and braces (curly brackets)

この問題が一番悩みましたね…。

 

このserverは文字列を入力として受け付け、flag文字列の後ろに足してzlib.compress() でバイト列に圧縮します。次にSalsa20方式によって暗号化します。暗号化に必要なkeyは32バイトでos.urandom(32)によって定義されます。最後に暗号文と暗号化に使用したnonce, 暗号文の長さを表示します。

oracleのように入力を何回でも受け付けてくれ全て暗号化してくれます。

 

zlib.compress() を見ていきましょう。

str型をbytes型に圧縮します。どのように圧縮しているかは分からなかったのですが、zlib.decompressで元に戻せるとのことでした。

 

次にSalsa20について考えます。

色々調べたのですが、chacha20と同じように書かれていることが多く理解が難しかったです。wikiなどあるので参考にしてみて下さい。

 

Salsa20 - Wikipedia

 

自分なりの理解とすると長さ16のサイズが8bytesの配列を用意し、複雑な計算で値を決定します。その配列を2bytes毎にリトルエンディアン方式で長さ64の配列に格納します。この配列と平文をxorしたのが暗号文となります。

 

ではこの長さ64の配列を特定できたらflag+inputの圧縮bytesが分かりzlib.decompressで元に戻せるのではないのか、とこの配列のことだけを考えていきましたがどの文献を調べてもCrypto.Cipher.Salsa20と同じ挙動をするプログラムを作れず…。ここに数日使って考えてました。

 

ふと問題名について考えると、攻撃すべきはSalsa20ではなくzlib.compressじゃないのか?len()とれば分かるのにわざわざ暗号文の長さを表示させてるのは意味があるのではないのか?flagは55種類で構成されているというヒントからbrute forceでもするのか?

ここまで想像したらあと一歩頑張るだけで良かったです。

 

データの圧縮ですがzlib.compressがどのような挙動をしているかは不明ですが、辞書を作って圧縮しているそうです。例えばaaabbという文字列ならaが3個並んでbが2個並ぶという意味で圧縮すればa3b2のような形となり元の文字列が5文字なのに対し書き換えると4文字を圧縮することとなりさらに圧縮後の文字列長が短くなります。

 

つまり、flag+inputの形で圧縮するのでflagと同じ文字列をinputとすれば圧縮後の長さは他の文字をinputとした時より短くなり特定できるのでは?という考えに行きつきました。

 

flag+(flagの部分文字列)+(何か一文字) の (何か一文字)をbrute forceして圧縮率の良い入力を探します。有難いことに毎回一文字だけ他の文字と比べて暗号文の長さが短くなる時がありました。それをflagの部分文字列に追加していき、"}"が当てはまる時まで回します。serverに空文字送ると45の長さが返って来るのでそれを使っても良いですね。

 

私の部屋の通信が遅いせいか途中でタイムオーバーしてしまい、途中まで求めたflagの部分文字列をスタートにして求めました。solverには最初から求めるようなものを掲載しています。

 

この問題だけで6日かかった…。

 

picoCTF{sheriff_you_solved_the_crime}

 

 

↓m1z0r3勉強会採用問題

Scrambled: RSA 140pt

Hmmm I wonder if you have learned your lesson... Let's see if you understand RSA and how the encryption works. Connect with nc mercury.picoctf.net 6276.

Hint1 : Look at the ciphertext, anything fishy, maybe a little bit long?

Hint2 : What happens if you encrypt the same input multiple times?

Hint3 : Is RSA deterministic, why would outputs vary?

 

serverにつなげると公開鍵e, nに加えやけに値の大きい暗号文cが送られてきます。その後oracleのように任意の平文を暗号化してくれます。

 

まずcがnより大きいのでただのRSAではないことが分かります。c mod nを取っても何も分かりません…。

 

ヒントを参考に何回か同じ平文をoracleに投げてみます。ここで気付いたことは平文長1の時は何度やっても同じ暗号文が返ってくるのに対し、平文長2の時は異なる暗号文が送られてきて計2種類ありました。平文長3の時はもっと種類がありました。

 

さらに気付いたこととして平文"12"の暗号文の中に平文"1"の暗号文が含まれており"b"は含まれていない点です。

 

I will encrypt whatever you give me: 1
Here you go: 13124115897409148690175503414678311002550072264931973692606617424840875394178924924667165806108832225382916928826588194202873207482242336795957858653898547865981328797617704615680916248306354749186650700653148182638263380446877489886302315539251194333627159551808654634412960860616807864577972598755285353671
I will encrypt whatever you give me: 2
Here you go: 101313714858161761634424992049701340225225901427102268968811128642939843971178700310220868193986270827880834443468869250035948793946978960836348254108084878497428993736369171344812715172293673578722333182457607039902800607183698449846158886607662435088015414390779614690393026603998035663690508448583587232108
I will encrypt whatever you give me: 12
Here you go: 1312411589740914869017550341467831100255007226493197369260661742484087539417892492466716580610883222538291692882658819420287320748224233679595785865389854786598132879761770461568091624830635474918665070065314818263826338044687748988630231553925119433362715955180865463441296086061680786457797259875528535367153230702539676627077713255043960931601547634062672474402191718157569789042884730190065962186209672390181875607152200181525682605899965673289841280220588582699750835635714680926113598794384506889798456369904349419599058763732276368570777636231103073162515084138617288828974174983151895993498712348576936738885
I will encrypt whatever you give me: 123
Here you go: 462421867967343899092247904339366104356633194458564599507361558221364994129967069812583568887596621514840256516950454547665458461766413255685426477319761146914988512715132189205891743864191787311225979337320579909727351783274773056270868986792549936307841261246031472338304220554727128192453579090999110003105323070253967662707771325504396093160154763406267247440219171815756978904288473019006596218620967239018187560715220018152568260589996567328984128022058858269975083563571468092611359879438450688979845636990434941959905876373227636857077763623110307316251508413861728882897417498315189599349871234857693673888513124115897409148690175503414678311002550072264931973692606617424840875394178924924667165806108832225382916928826588194202873207482242336795957858653898547865981328797617704615680916248306354749186650700653148182638263380446877489886302315539251194333627159551808654634412960860616807864577972598755285353671

 

1文字毎に分け暗号化しそれを付け足してるのかと思いきや、"2"の暗号文は"12"に含まれていないので違うよう…。けど平文の長さに比例して暗号文の長さも長くなります。

 

ここで"12"の暗号文をSとTに分け、"1"の暗号文と一致するものをS、そうでないのをTとすると、"123"の暗号文にはSとTがともに含まれており、さらに異なる文字列Uがありました。

 

つまり平文をmとすると、{m[0:1], m[0:2], m[0:3], ... , m[0:n]} をそれぞれRSAで暗号化し暗号文をごちゃ混ぜにして繋げていると予想しました。

 

なので後の実装としては、{ p, pi, pic, pico, picoC, picoCT, picoCTF, picoCTF{ }の暗号文を配列に記憶します。次にstring.printableで回し、貰った暗号文の内記憶した暗号文を除いた文字列S'が最初に渡されるflagの暗号文に含まれていたら、その入力はflagの部分文字列であることが分かります。この際に記憶配列に今の暗号文を追加します。

 

これを繰り返しflagを求めます。

 

Compress and Attackと同様途中でタイムオーバーに...。通信弱いとつらい。

 

picoCTF{bad_1d3a5_2268684}

 

 

It is my Birthday 2 170pt

問題文が見れませんでした…。

内容的には、渡したpdfと後ろ1000バイトは同じである異なるpdfをこの二つ作り、このウェブサイトにアップロードしてください。この二つのpdfのSHA-1ハッシュ値は同じになるようにしてください。

ファイル:invite.pdf

Hint : SHAttered (他にもあったけど覚えてません…)

問題文がもう見れなくなっておりなんとなくこんな感じでしたとだけ…。

 

うーーん、Forensicsぽい。苦手だ。とりあえずSHA-1が良くないということは知ってるけど理由までは知らない…。昔SHA-1の衝突を使ったCTFあったらしいと聞いたことがあるぞ。ググりつつ人に聞いたりしてました。

 

とりあえずSHA-1の理解を。

 

www.slideshare.net

 

バイナリデータが二つ

xxxxABCxxxxxxxx

xxxxDEFxxxxxxxx

このようにある時SHA-1ハッシュ値がABCとDEFで衝突するとき、上の二つのSHA-1ハッシュ値も衝突します。まあそのABCとDEFのペアを見つけるのにかなりな時間がかかるんですけどね。

 

SHA-1 pdf collisionなどでひたすらググってました。githubとかで色んなコードコピペしたのに上手くいかず…。かなり唸ってました。

 

チームメイトとzoomで議論した時にふと「既に出てる衝突する二つのpdfの後ろに1000バイト引っ付ければ良くね?」と。ビンゴでした!!

 

picoCTF{h4ppy_b1rthd4y_2_m3_ed9cc7d0}

 

 

Pixelated 200pt

I have these 2 images, can you make a flag out of them?

ファイル:scrambled1.png, scrambled2.png 

Hint : Visual cryptography - Wikipedia , Think of different ways you can "stack" images

(実はチームメイトが解いたんですけど、自分も惜しかったので紹介させていただきます)

 

pngファイルは前回のutctfで初めて使ったPILを利用してみます。貰ったpngを重ねたらいいのだろう。PIL使ったり、片方のpngの透過度半分にしたりとかやったんですけど灰色の画像しか出てこない…。

 

PILのImage.composite() では上手くいかないようでNumpy arrayで画像を読み込み和を取れば一発で出たようです。チームメイトすごいなぁ。

 

f:id:partender810:20210331232307p:plain

sum.png

 

picoCTF{2a4d45c7}

 

 

↓m1z0r3勉強会採用問題

New Vignere 300pt

Another slight twist on a classic, see if you can recover the flag. (Wrap with picoCTF{})

ioffdcjbfjmcifelcaloifgcjecgpgiebpfeiafhgajafkmlfcbpfbioflgcmacg  

ファイル:new_vignere.py

Hint : Vigenère cipher - Wikipedia

new_vignere.pyで定義されているb16_encode(), shift関数はNew Caesarで与えられたファイルと同じ挙動です。ただ前問と違うのはkeyの長さが15未満とあるのでbrute forceできません。

 

Vigenere暗号のKEYの長さを特定するのにカシスキーテストというのが利用されます。平文内にある単語が複数回登場しその単語を同じKEYの部分で暗号化することで、同じ暗号文になります。その同じ暗号文が登場するindexを調べることでKEYの長さの候補をある程度絞ることが出来ます。

 

今回の場合、"iof"という暗号文が2回出てきています。1~3文字目と55~57文字目が"iof"となっており、差は54です。54を素因数分解すると {1, 2, 3, 6, 9, 18, 27, 54}となり、keyの長さが15未満であるから {1, 2, 3, 6, 9} に絞ることができます。

 

keyの長さが6以下であればbrute forceできそうです。長さ6の場合、keyは16種類の文字で構成されるので16^6 ≒ 16m 程度なのでちょっと待てば行けそうです。今回flagが"abcdef0123456789"のみで構成されているので復号した際にこれと合致すればOKです。

 

(brute force中…)

 

出ない……。keyの長さは9なのか?それは求められないからkeyの長さが6まで全て試したけど出ず…。

 

カシスキーテストを言及していることから恐らくkey長は9なのだろう…。brute force出来ないので別の方法を考えます。

 

今回flagの文字が制限されているのでkeyの選び方によってはその通りに復号されません。ということは、正しく復号できるようなkeyを探すのはそんなに難しくないのでは?となりました。

 

例えば暗号文の2文字2文字"io"が正しく復号される時のkeyの候補は、{(f, i), (f, j), (f, k), (f, l), (f, m), (f, n)}です。keyの1文字目は"f"であることが確定しました。このように暗号文の2文字毎見ていき合計18文字見たところで長さ9のkeyが特定できました。

 

こんな感じ↓  最初の数字をiとすると、i*2, i*2+1文字目を正しく復号出来るのは(s, t)です、という形です。s, tは"a"を0, "p"を15としています。

0
(5, 8)
(5, 9)
(5, 10)
(5, 11)
(5, 12)
(5, 13)
1
(2, 2)
(2, 3)
(2, 4)
2
(0, 12)
(0, 13)
(0, 14)
(0, 15)
3
(6, 11)
(6, 12)
(6, 13)
(6, 14)
(6, 15)
4
(15, 4)
(15, 5)
(15, 6)
5
(9, 2)
(9, 3)
(9, 4)
(9, 5)
6
(2, 0)
(2, 1)
(2, 2)
(2, 14)
(2, 15)
7
(14, 6)
(14, 7)
(14, 8)
(14, 9)
(14, 10)
(14, 11)
(14, 12)
(14, 13)
(14, 14)
(14, 15)
8
(15, 0)
(15, 14)
(15, 15)

keyが[5, 9, 2, 2, 0, 14, 6, 15, 15]となれば上記の候補に全て当てはまるようになります(隣り合う2つのペアは必ず上に含まれています)。つまりkeyは"fjccaogpp"でした。

 

綺麗にkeyが復元されてめっちゃ気持ちよかった!!

 

picoCTF{5342d0ee1ecd51dd9e75b1e929b59da1}

 

 

solverや問題などはこちらから!

github.com

 

 

picoCTFは簡単な問題や難しいけど誘導がそれなりにされている問題、普通に難しい問題と幅広く楽しかったです! 何より解ける問題が多かったのは有難いですね。しかも得点がsolves数によって影響しない方式だったので、せっかく解いたのに得点が下がるなんてこともなくて精神的に嬉しい(笑) でもIt's Not My Fault 1を解けた時はもっと点数欲しかったなぁと…(笑)

 

何より日本人6位で嬉しかった上に解いてて楽しかったです!!ありがとうございました!

 

 

それでは!

 

 

See you next time!