HITCON CTF Final 2024
Author: 堇姬Naup
Team: Starburst Beef Stir-Fried BambooFox
Rank: r.k.10 & Taiwan Star
前言
這是是跟台灣大聯隊-Starburst Beef Stir-Fried BambooFox,一起參加的,只有四個人在現場打,我是在飯店remote支援(有在會場附近國聯大飯店、交大、一些線上的人)
賽程在這裡
先說一下這次的結果
我們是第10名及拿到了Taiwan star
接下來開始講講這兩天的過程吧
Day1
第一天一開始主辦方infra貌似直接燒起來,延後了半小時開賽
開賽後先放出了一題KoH一題A&D,之後又放出了一題
KoH-ed25519
A&D-ucontex
A&D-fulu
我這兩天幾乎都花時間在第一題KoH上,另外我也是第一題第一天下半場的PM(caleb那時候跑去看其他題了)
先來介紹一下第一題(我有留提敘)
1 | About |
有一些檔案比較重要的
- proof: 這資料夾底下裡面有負責驗證你的patch是否滿足條件的檔案,下載下來需要先裝golang編譯他。之後要用可以直接調用他提供的check.py來去驗證你的patch
- check.zip: 裡面有一些example、dockerfile、check.py、genkey.py來說說後面兩個
- genkey.py會生成一對
Public key + Private key 或是
Public key + Multiplier
你需要將後面的塞到你的patch裡面,對他做混淆來隱藏key - check.py: 可以在本地去做patch check
python check.py -s yourpatch.py -p pub.key
另外他需要你提供一對鑰匙來去驗證,這樣你在無法破解其他隊伍的狀況下無法直接抄patch,然後我們要嘗試把私鑰塞進去他會去做
裡面也有seccomp掉一些syscall是無法用跟可以用的1
2
3
4
5
6
7
8
9
10
11
12
13SECCOMP = """
from seccomp import *
f = SyscallFilter(defaction=KILL)
f.add_rule(ALLOW, 'access')
f.add_rule(ALLOW, 'arch_prctl')
f.add_rule(ALLOW, 'brk')
f.add_rule(ALLOW, 'close')
f.add_rule(ALLOW, 'dup2')
f.add_rule(ALLOW, 'epoll_create1')
f.add_rule(ALLOW, 'faccessat2')
...
""" - client.py: 可以去上patch、攻擊等等的
回來看到題目介面,他有4個,重要的有三個
- score board: 每round都會計分,並排出名次了加到總積分上,規則如下

- patch: 可以找到每round的patch,並把他載下來分析

- attack: 會看到你在哪輪有打通哪輪對伍的patch

attack
主要要調的就是round數、隊伍id和key
1 | # attack1.py |
1 | # attack2.py |
1 | # attack3.py |
有沒有打通都會告訴你
patch方法
1 | python3 genkey.py -s PRIVATE_KEY -p PUBLICKEY |
auto patch
我們對伍 triangle snake一開始有寫了一個auto patch,裡面有寫簡單的混淆跟自動上patch的工具,第一天就靠這東西一路到底
1 | import os |
1 | from obfu import obfu |
解混淆(看別對patch)
第一天開始的patch都還沒很噁心,所以跟隊友就在每輪都可以拆出一些對伍的patch
其中有幾對我比較印象深刻的
首先是 thehackerscrew 的patch
1 | from base64 import b64decode, b64encode;import pickle, random;pickle.loads(b64decode(b'gASVTAAAAAAAAACMCWltcG9ydGxpYowNaW1wb3J0X21vZHVsZZOUjAZyYW5kb22FUpSMCGJ1aWx0aW5zjAdnZXRhdHRyk5RoAYwEc2VlZIZSTdyshVIu')) |
他塞了一堆怪怪的key,並且用pickle的方式序列化一串random有關的code,實際執行就會發現每次random出來的都一樣,但是我們一開始挖出來的key都不正確。後來我跟隊友猜測是因為環境問題,苦惱了一陣子,因為不知道要怎麼知道主辦方環境,後來才發現主辦方有給docker,所以直接開一個container然後進去抓key就行了。
另外一隊 defunqt 也是一樣的概念
1 | import sys |
中間那段混淆是用sys.version去做混淆的
1 | 3.10.12 (main, Sep 11 2024, 15:47:36) [GCC 11.4.0] |
這裡面有python版本跟python甚麼時候被編譯的還有GCC的版本。
知道有docker後直接進去抓一樣能抓到
接下來開始看到一些對伍用線上的混淆器
https://freecodingtools.org/
混淆出來大概長這樣
1 | _ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));exec((_)(b'=g29Plx/f/+98r1p4LTx5y1EEeOtO24TZL0vOYyGH7jTumL2aA3A5R2ztUxfDQP9C5MRkgkeIACWIASEoDwJa+56gkMHjhKH10HPhr3Dvc6rSZS+IHYCedM7ofOa/OOv5egN1RJeALe6V6swriO6N5H9DHaXmXv8LoJ29Fg0zwlx29gkon9PitnRGT3mn9tQ00a4BAdosRc/ |
解開的方法很簡單,把他的exec(_)改成s=
1 | for i in range(50): |
然後印出來就可以解反混淆了
之後就是用bytecode相關的混淆了
1 | import marshal,zlib;exec(marshal.loads(zlib.decompress(base64.b64decode('eNp1eLnvPVty1/f73gMNA8GTSCBDzkZXfr1vkm3Re9/e903Ij+7by+19u72KYAL4ExAOiZAQIudPICCYsUYy+qVEZINM5ATu78lGM5Y5fbpKpe6qUzpddepT/T8+fmf8vb/mf4m+yb/7yD7+9U...')) |
抓出來就行了
就這樣第一天我們在最後一輪(78 round)時新patch掉了五隊,不過有七隊是可以打通的
這邊來放一些我們沒打出來的隊伍如何做混淆的
先是TokyoWestern
1 | class TWTW(): |
跟YAATN
1 | def sign(data: bytes) -> bytes: |
在晚上的時候有解出來這個
透過在中間插一些print emoji的方法來看每個代表甚麼後,可以發現這裡有一些端倪,順著去去找就可以找到priv是啥了
1 | 'ˆωˆ': <built-in function chr>, 'O_o': ' ', 'QAQ': 'e', 2: 'Ed25519PrivateKey', 3: 'signed', 5: 'Ed25519PrivateKey\n', 7: '\ndel ', 8: 'open("/dev/shm/', 16: "import('", 4: 'data', 32: ' = ', 33: 'priv', 6: "'", 9: ')\n', 10: '.', 11: '(', 13: 'b").', 15: 'from', 17: '", "', 18: 'sk', 19: 'p', 21: 'r', 12: 'te', 20: 'y', 34: 'zm', 35: 't', 38: 'ri', 40: 'b', 41: 'h', 37: '64', 14: 'T', 22: 'de', 26: ').', 27: 'as', 29: 'N', 31: 'w', 39: 'F', 42: 'M', 44: '2', 46: 'm', 45: 'V'} |
關題目後
我們晚上就調整了一下戰略
寫了一些patch跟解了其他隊伍最後一輪的混淆
第一天 Score board
DAY 2
開賽
因為早上要去講議程(10:00~11:00)所以PM換成afan,我在等議程的時候就開著電腦解
開賽的時候第一round,我們打通了11/13隊
之後開始大家上了晚上寫的patch,變得比較難打通,但是看每隊的攻擊狀況發現,幾乎所有隊伍的patch都在不到5 round內被其他隊伍解出(我們的patch全部都沒撐到一round)
放一些我們上的patch
開場的patch
1 | def sign (OO0O0O000OO00OOOO :bytes )->bytes : |
還有Curious的patch
1 | import sys |
改變策略
原本這題我們是有key就送,但是其實看規則就會發現,去搶前三名分數會多很多,所以第二天我們就隔兩三輪在去送key,並且也順手去補了前面round的一些坑,很明顯的狀況就比第一天好很多了(多一些round數這題可以超過RePokemonedCollections)
不過防禦分幾乎都拿不到(不過其他隊伍也拿不到就是了),就全力衝攻擊
最後就持續解混淆跟控制送key方法來顧這題
混淆
基本上第二天patch有幾隊比較特別
Friendly Maltese Citizens 跟 Superflat 都用shellcode的方式來做混淆
1 | m = mmap.mmap(-1, len(data), prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC, flags=mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS) |
1 | SHELLCODE = gzip.decompress(base64.b64decode(SHELLCODE)) |
我們轉成assembly後發現也是被混淆的asm了QQ
最後能打通新patch的隊伍大概7組
第二天score board
這題拿到了1414分
後記
忘記放題單了,丟一下(偷別人的圖)
這題蠻好玩的,看別人怎麼寫垃圾Code,跟寫垃圾code
我非常喜歡實體賽給人的感覺,一起在同一個空間,認真研究同一題,想怎麼拿分怎麼攻防。
打出來一起歡呼的感覺
感謝 @afan、@Curious、@Itiscaleb、@Unicorn、@Triangle_Snake、@Chisheng_Chen 一起在第一題研究。
另外也感謝Startburst beef stir-fried bamboofox的其他人在其他題輸出