GREP CTF Writeup

この大会は2023/4/1 23:00(JST)~2023/4/3 5:00(JST)に開催されました。
今回もチームで参戦。結果は4250点で237チーム中13位でした。
自分で解けた問題をWriteupとして書いておきます。

Approved ! (MISC)

CTFTIMEでイベントの情報を見ると、以下のように書いてある。

A CTF from team Bits & Pieces, BITS Pilani. It is aimed for beginners but anyone can participate.

Hope you enjoy it.

475245507b57336c63306d655f74305f475233505f4354467d

hexエンコード文字列があるので、hexデコードする。

$ echo 475245507b57336c63306d655f74305f475233505f4354467d | xxd -r -p
GREP{W3lc0me_t0_GR3P_CTF}

Layouts (MISC)

QWERTYキーボードのつもりでWorkmanキーボード配列のキーでタイプした模様。対応する文字に置換する。

#!/usr/bin/env python3
with open('msg.txt', 'r') as f:
    enc = f.read()

P = 'qwertyuiopasdfghjkl;zxcvbnmWYUIFHN'
C = 'qdrwbjfup;ashtgyneoizxmcvklDJFUTYK'

out = ''
for c in enc:
    if c in ' .,\'-_1234567890\n':
        out += c
    elif c in C:
        out += P[C.index(c)]
    else:
        out += '?'
print(out)

実行結果は以下の通り。

Uh, summa-lumma, dooma-lumma, you assumin' I'm a human
What I gotta do to get it through to you I'm superhuman??
Innovative and I'm made of rubber so that anything
You say is ricochetin' off of me, and it'll glue to you and
I'm devastating, more than ever demonstrating
How to give a motherfuckin' audience a feeling like it's levitating
Never fading, and I know the haters are forever waiting
For the day that they can say I fell off, they'll be celebrating
Your flag is r4pg0d_em1n3m_3256gd62 in the usual format
grepCTF{r4pg0d_em1n3m_3256gd62}

Lost Card (MISC)

カード番号が以下のように書いてあるが、2桁が不明。

538XX10365956729

Luhnアルゴリズムによりチェックし、適合するものを探す。

#!/usr/bin/env python3
def check_number(digits):
    _sum = 0
    alt = False
    if digits[0] == "0":
        return False
    for d in reversed(digits):
        d = int(d)
        assert 0 <= d <= 9
        if alt:
            d *= 2
            if d > 9:
                d -= 9
        _sum += d
        alt = not alt
    return (_sum % 10) == 0

for i in range(10):
    for j in range(10):
        number = '538%d%d10365956729' % (i, j)
        if check_number(number):
            print(number)

10パターン出てくる。

5380010365956729
5381910365956729
5382410365956729
5383810365956729
5384310365956729
5385710365956729
5386210365956729
5387610365956729
5388110365956729
5389510365956729

順にフラグとして投入してみる。

GREP{5380010365956729} → NG
GREP{5381910365956729} → NG
GREP{5382410365956729} → NG
GREP{5383810365956729} → NG
GREP{5384310365956729} → NG
GREP{5385710365956729} → NG
GREP{5386210365956729} → NG
GREP{5387610365956729} → NG
GREP{5388110365956729} → OK
GREP{5388110365956729}

GCodeで書かれているようだ。以下のURLのオンラインのNC viewerで開くと、フラグが見える。

https://ncviewer.com/

grepCTF{w0rksh0p_pract1ce_b3st_c0urs3}

Simple rev (REVERSE ENGINEERING)

$ strings outfile | grep grepCTF
grepCTF{4p0g33_h1vem1nd_g3n3s1s}
grepCTF{4p0g33_h1vem1nd_g3n3s1s}

EXORcist (REVERSE ENGINEERING)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  long in_FS_OFFSET;
  int local_40;
  byte local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Enter the flag: ");
  __isoc99_scanf(&DAT_00102015,local_38);
  for (local_40 = 0; local_40 < 0x17; local_40 = local_40 + 1) {
    local_38[local_40] = (byte)local_40 ^ local_38[local_40];
  }
  iVar1 = strcmp((char *)local_38,"gsgsGQ@|9gn8tRy>c\"Mk$gk");
  if (iVar1 == 0) {
    puts("Correct flag!");
  }
  else {
    puts("Wrong flag!");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

各インデックスとXORして"gsgsGQ@|9gn8tRy>c\"Mk$gk"になるものがフラグ。

#!/usr/bin/env python3
enc = "gsgsGQ@|9gn8tRy>c\"Mk$gk"

flag = ''
for i in range(len(enc)):
    flag += chr(i ^ ord(enc[i]))
print(flag)
grepCTF{1nd3x_w1s3_x0r}

Monke (FORENSICS)

jpgの後ろにbase64文字列らしきものがあるので、デコードする。

$ echo Z3JlcENURntyM2ozY3RfaHVtNG4xdHlfZzBfYjRja190MF9tMG5rM30K | base64 -d
grepCTF{r3j3ct_hum4n1ty_g0_b4ck_t0_m0nk3}
grepCTF{r3j3ct_hum4n1ty_g0_b4ck_t0_m0nk3}

NGGYU (FORENSICS)

Audacityで開き、スペクトログラムを見ると、フラグが現れた。

grepCTF{r1ck_4stl3y_g1v1ng_m3_up}

IronMan (FORENSICS)

StegSolveで開き、[Analyse]-[Data Extact]の画面で、RGBのLSBにチェックを付ける。さらに、Extract Byで[Column]にし、列方向でLSBを読むよう設定し、Previewすると、フラグが表示された。

grepCTF{i_d0n't_f3el_s0_g00d}

R36 (FORENSICS)

sstvでデコードしてみる。

$ sstv -d r36.wav -o flag.png
[sstv] Searching for calibration header... Found!    
[sstv] Detected SSTV mode Robot 36
[sstv] Decoding image...   [#################################################] 100%
[sstv] Drawing image data...
[sstv] ...Done!

出力した画像にフラグが書いてあった。

grepCTF{psych3d3l1c_fr0g}

Royal Steg (FORENSICS)

$ stegseek steg.jpg dict/rockyou.txt
StegSeek 0.6 - https://github.com/RickdeJager/StegSeek

[i] Found passphrase: "cuteessort37"     
[i] Original filename: "orig.zip".
[i] Extracting to "steg.jpg.out".

$ file steg.jpg.out
steg.jpg.out: Zip archive data, at least v1.0 to extract, compression method=store
$ mv steg.jpg.out steg.zip

抽出したファイルはzipファイルでパスワードがかかっている。

$ fcrackzip -u -D -p dict/rockyou.txt steg.zip


PASSWORD FOUND!!!!: pw == jesuslove

このパスワードで解凍・展開する。

$ unzip steg.zip
Archive:  steg.zip
[steg.zip] flag.txt password: 
 extracting: flag.txt
$ cat flag.txt
grepCTF{tw0_l3v3ls_0f_st3g}
grepCTF{tw0_l3v3ls_0f_st3g}

Last Seen Beauty (FORENSICS)

暗号化処理の概要は以下の通り。

・image: mem2.pngのイメージオブジェクト
・width, height: imageの幅、高さ
・img_array: イメージデータのリスト
・channels: 画像モードが"RGBA"の場合は4、そうでない場合は3
・pixels: 画像のピクセルの数
・stop_indicator = "$ckc$"
・stop_length: stop_indicatorの長さ
・messageにstop_indicatorを追加
・byte_message: messageの2進数表記文字列
・bits: byte_messageの長さ
・index = 0
・bitsの長さの間、以下を実行
 ・LSBに埋め込み
・encoded.pngに書き込み

画像を横方向でLSBを取得し、メッセージを取得する。

#!/usr/bin/env python3
from PIL import Image

def bin_to_str(b):
    l = len(b) // 8 * 8
    s = b[:l]
    s = ''.join([chr(int(s[i:i+8], 2)) for i in range(0, len(s), 8)])
    return s

stop_indicator='$ckc$'

img = Image.open('encoded.png').convert('RGB')
w, h = img.size

bin_flag = ''
final = False
for y in range(h):
    for x in range(w):
        r, g, b = img.getpixel((x, y))
        bin_flag += str(r & 1)
        bin_flag += str(g & 1)
        bin_flag += str(b & 1)
        flag = bin_to_str(bin_flag)
        if stop_indicator in flag:
            index = flag.index(stop_indicator)
            flag = flag[:index]
            print(flag)
            final = True
            break
    if final:
        break

取得したメッセージは以下の通り。

GVNJ{B1ur_c4u_f3a_lman}

元の画像のEXIF情報を見てみる。

$ exiftool encoded.png 
ExifTool Version Number         : 12.40
File Name                       : encoded.png
Directory                       : .
File Size                       : 11 MiB
File Modification Date/Time     : 2023:04:02 23:23:28+09:00
File Access Date/Time           : 2023:04:03 00:06:51+09:00
File Inode Change Date/Time     : 2023:04:02 23:23:28+09:00
File Permissions                : -rwxrwxrwx
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 3456
Image Height                    : 5184
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Exif Byte Order                 : Big-endian (Motorola, MM)
X Resolution                    : 72
Y Resolution                    : 72
Resolution Unit                 : inches
Y Cb Cr Positioning             : Centered
Exif Version                    : 0232
Components Configuration        : Y, Cb, Cr, -
User Comment                    : Hint: Port Royal, SC
Flashpix Version                : 0100
Color Space                     : Uncalibrated
Image Size                      : 3456x5184
Megapixels                      : 17.9

User Commentに「Hint: Port Royal, SC」と書いてある。Google mapで「Port Royal, SC」を調べると、以下のようなことが書かれていることがわかる。

Port Royal is a town on Port Royal Island in Beaufort County, South Carolina, United States.

Beaufort暗号が関係ありそう。https://www.boxentriq.com/code-breaking/beaufort-cipherで復号する。復号結果が"GREP"で始まるよう、鍵を操作し、"mmry"のときにフラグになった。

GREP{L1sa_w4s_h3r_name}

Missing Kitty (FORENSICS)

$ stegseek Missing.jpg dict/rockyou.txt
StegSeek 0.6 - https://github.com/RickdeJager/StegSeek

[i] Found passphrase: "kitty123"
[i] Original filename: "secret.txt".
[i] Extracting to "Missing.jpg.out".

$ cat Missing.jpg.out
Dk what's this, some kitten language

memmemmmmeemmememeemeemmmeemeemmmeemeeeemmemmmmmmeemeememeeeemmemmemmmmmmeememeemeememmemeeememmmeeememmmeeeemmemmemeemmmmmmememmmmmememmemmmeemmeememmemeemeeemmeemmmmemeemeemmmeemeemmmeeeemmemmemmmmmmeeeemmemeemeeeemeeemememmemmmmmmeemmeemmeemeeeemeeemememeemeeemmeemmemmmmemmmmmmeememmmmeemmememeeemmemmmemeeemmmemmmmmmemmemmemmemmmmmmeemmmmemeemeememmemmmmmmeemmemmmeemmememeemeemmmeememmemeemmeeemeememmmmeeememmmeemmememeemmemmmmemeeemmmmmememmmmmememmemememmmeemmmmemeememeemeemmememmemmmmmmeememmemeeememmmmemeemmmmemmmmmmeememmmmeemmememeeemmemmeemmememmemmeeemeeemmeemmemmmmmmeeeemmemeemeeeemeeemememeeemmemmmemmmmmmeemmeeemeememmemeemmeemmeeememmmmmmememmeemmeemmeemeemmmeemmmmemeemmeeemmemmmmmmmeeememmmemmmmmmeeeemeemememmeemeeemeeemmeemmeemmeemmeemeeememmmemeeeeemeemeemmmmeemmmemeeememmmeeememmmeemeemmmeemmemememeeeeemeememeemmeemmmemeeememmmeeememmmmeemmeemeemeeemmeeeeeme

mを"0"、eを"1"として、2進数データをデコードする。

#!/usr/bin/env python3
enc = 'memmemmmmeemmememeemeemmmeemeemmmeemeeeemmemmmmmmeemeememeeeemmemmemmmmmmeememeemeememmemeeememmmeeememmmeeeemmemmemeemmmmmmememmmmmememmemmmeemmeememmemeemeeemmeemmmmemeemeemmmeemeemmmeeeemmemmemmmmmmeeeemmemeemeeeemeeemememmemmmmmmeemmeemmeemeeeemeeemememeemeeemmeemmemmmmemmmmmmeememmmmeemmememeeemmemmmemeeemmmemmmmmmemmemmemmemmmmmmeemmmmemeemeememmemmmmmmeemmemmmeemmememeemeemmmeememmemeemmeeemeememmmmeeememmmeemmememeemmemmmmemeeemmmmmememmmmmememmemememmmeemmmmemeememeemeemmememmemmmmmmeememmemeeememmmmemeemmmmemmmmmmeememmmmeemmememeeemmemmeemmememmemmeeemeeemmeemmemmmmmmeeeemmemeemeeeemeeemememeeemmemmmemmmmmmeemmeeemeememmemeemmeemmeeememmmmmmememmeemmeemmeemeemmmeemmmmemeemmeeemmemmmmmmmeeememmmemmmmmmeeeemeemememmeemeeemeeemmeemmeemmeemmeemeeememmmemeeeeemeemeemmmmeemmmemeeememmmeeememmmeemeemmmeemmemememeeeeemeememeemmeemmmemeeememmmeeememmmmeemmeemeemeeemmeeeeeme'

enc = enc.replace('m', '0').replace('e', '1')

msg = ''
for i in range(0, len(enc), 8):
    msg += chr(int(enc[i:i+8], 2))
print(msg)

実行結果は以下の通り。

Hello my kitty,

Finally you found her. I am delighted.

Take it, here's your gift
flag : {Sw33t_l1ttle_k1tt3n}

使用したツールと合わせ、フラグとする。stegseekを使っているが、steghideでブルートフォースしているので、ツール名はsteghide。

GREP{steghide,Sw33t_l1ttle_k1tt3n}

Blind (CRYPTOGRAPHY)

点字を対応表を見ながらデコードする。

the flag is t00_bl1nd_t0_s33
grepCTF{t00_bl1nd_t0_s33}

CaeX0R (CRYPTOGRAPHY)

1~1000のランダムな値1つと、フラグの各文字をXORして暗号化している。このランダムな値をブルートフォースして復号する。

#!/usr/bin/env python3
def is_printable(s):
    for c in s:
        if ord(c) < 32 or ord(c) > 126:
            return False
    return True

c = ['162', '177', '188', '169', '136', '187', '138', '145', '172', '187',
    '138', '145', '172', '190', '152', '156', '187', '195', '177', '142']

for a in range(1, 1001):
    flag = ''
    for code in c:
        flag += chr(int(code) ^ a)
    if is_printable(flag) and flag.endswith('}'):
        print(flag)
        break

復号結果は以下の通り。

QBOZ{Hyb_Hyb_MkoH0B}

さらにシーザー暗号になっている。https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherで復号する。

Rotation 10:
GREP{Xor_Xor_CaeX0R}
GREP{Xor_Xor_CaeX0R}

NOT 13 (CRYPTOGRAPHY)

quipqiupで復号する。

LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. MORBI SCELERISQUE, NULLA VITAE LUCTUS TINCIDUNT, MI TURPIS VESTIBULUM TELLUS, UT CONGUE TURPIS QUAM QUIS AUGUE. PROIN ULTRICIES LUCTUS RISUS, EGET VARIUS RISUS INTERDUM SED. NUNC ID TINCIDUNT IPSUM. THE FLAG IS ITS NOT ALWAYS ROT, IN LOWER CASE, WITH UNDERSCORES INSTEAD OF SPACES. FUSCE DICTUM NULLA ERAT, TINCIDUNT TEMPUS LECTUS ULTRICIES VEL.

この文章に従い、フラグを構成する。

grepCTF{its_not_always_rot}

Birdseed (CRYPTOGRAPHY)

ランダムの値とXORして暗号化しているが、seedの範囲が0~999のため、ブルートフォースで復号する。

#!/usr/bin/env python3
import random

with open('out.txt', 'r') as f:
    encrypted = bytes.fromhex(f.read())

for seed in range(1000):
    random.seed(seed)
    flag = ''
    for c in encrypted:
        flag += chr(c ^ random.randint(0, 255))
    if flag.startswith('grepCTF{'):
        print(flag)
        break
grepCTF{n3v3r_tru1y_r4nd0m}

Derailed (CRYPTOGRAPHY)

user IDは75番目の素数から1引いたもので、passwordのリストで行数のところに該当するパスワードがあると推測できる。

#!/usr/bin/env python3
from Crypto.Util.number import *

with open('password.txt', 'r') as f:
    words = f.read().splitlines()

count = 0
n = 1
while True:
    if isPrime(n):
        count += 1
    if count == 75:
        userID = n - 1
        break
    n += 1

password = words[userID - 1]
print(password)

該当するパスワードは以下であることがわかる。

T_OF}ENI_NNfqR{rlQcjeCe_B

Rail Fence Cipherのようなので、https://www.dcode.fr/rail-fence-cipherでレール数に4を指定して復号する。

TERC{N_Irel_ONQ_cNFfjBeq}

さらにシーザー暗号として、https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherで復号すると、フラグになった。

GREP{A_Very_BAD_pASswOrd}

Uneasy Alliance (CRYPTOGRAPHY)

UNIXTIMEからp, qを計算しているので、UNIXTIMEのブルートフォースで復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *
from random import Random

rand_fn = lambda n: long_to_bytes(rnd.getrandbits(n))
e = 65537
ct = 9898717456951148133749957106576029659879736707349710770560950848503614119828

for seed in range(1677596400, 1680427000):
    rnd = Random(seed)
    p = getPrime(128, randfunc=rand_fn)
    q = getPrime(128, randfunc=rand_fn)
    n = p * q
    phi = (p - 1) * (q - 1)
    d = inverse(e, phi)
    m = pow(ct, d, n)
    flag = long_to_bytes(m)
    if flag.startswith(b'GREP{'):
        flag = flag.decode()
        print(flag)
        break
GREP{Brut3D_M3!_f0r_l1f3}

CaeX0R 2 (CRYPTOGRAPHY)

CaeX0Rと暗号データのみ異なるので、同じ方法で復号する。

#!/usr/bin/env python3
def is_printable(s):
    for c in s:
        if ord(c) < 32 or ord(c) > 126:
            return False
    return True

c = ['313', '296', '295', '304', '274', '280', '263', '280', '263', '310', '315', '310', '316', '345', '268', '263', '310', '302', '345', '296', '276']

for a in range(1, 1001):
    flag = ''
    for code in c:
        flag += chr(int(code) ^ a)
    if is_printable(flag) and flag.endswith('}'):
        print(flag)
        break

復号結果は以下の通り。

PANY{qnqn_R_U0en_G0A}

さらにシーザー暗号となっているので、https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherで復号する。

Rotation 9:
GREP{hehe_I_L0ve_X0R}
GREP{hehe_I_L0ve_X0R}

DOGE DOGE DOGE (CRYPTOGRAPHY)

フラグが"grepCTF{"から始まることを前提にわかっている範囲でXORの鍵を求める。そこから全体の鍵を推測し、復号する。

#!/usr/bin/env python3
enc = b'#="5\x07\x1b\x01>4#s<u! \x1a3~3-\x1b7w7\x1b&4\x1a":)8'

flag_head = b'grepCTF{'

key = b''
for i in range(len(flag_head)):
    key += bytes([flag_head[i] ^ enc[i]])
assert key == b'DOGEDOGE'

key = key[:4]
flag = ''
for i in range(len(enc)):
    flag += chr(enc[i] ^ key[i % len(key)])
print(flag)
grepCTF{pl4y1ng_w1th_x0r_is_fun}