eBPF Summit 2023 CTF Writeup
0x00 Introduction
This is the writeup for eBPF Summit 2023 CTF. The challenge details can be found here. The environment is a Ubuntu 22.04 VM.
The goal is to find the process that is reading /etc/passwd
,
kill that process then the flag will be revealed at /ebpf.summit
.
0x01 Information Gathering
All commands are executed as root
.
Check current /ebpf.summit
content :
cat /ebpf.summit
I've been in your kernel for [22.109948 seconds]
Check which process is reading /etc/passwd
:
lsof /etc/passwd
No output, either the process is not holding the fd or it’s invisible.
Check if the process in provision is still running:
ps aux | grep ebpf
root 12474 0.0 0.0 6420 1844 pts/1 S+ 08:10 0:00 grep --color=auto ebpf
Nope, can’t find anything.
To monitor it in real time, we can use traditional tools like inotifywait
or fatrace
.
But since this is an eBPF challenge, let’s use some eBPF tools.
Usually BCC or bpftrace
are my first choice since they are easy to use and have a lot of examples.
In this environment, Tetragon is also pre-installed, another great tool.
And bpftool
to further investigate the eBPF programs.
Install tools first:
apt install bpftrace bpfcc-tools linux-headers-$(uname -r)
Check which process is opening /etc/passwd
using opensnoop
:
opensnoop-bpfcc | grep "/etc/passwd"
2372 ebpf.summit.202 18 0 /etc/passwd
Or a one line bpftrace
script:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat /str(args->filename) == "/etc/passwd"/ { printf("%d %s \n", pid, comm); }'
Attaching 1 probe...
2372 ebpf.summit.202
0x02 Exploit
Kill the process
kill 2372
Check the flag
cat /ebpf.summit
You purged the computers of the malware - and not a second too late. Congratula
tions! The location of the base remains a secret. Maybe not for long though, wh
ile everyone was focusing on the computers, Bajeroff Lake, the traitor, managed
to escape from his cell and stole a shuttle to escape the base. On the radars,
you only see him jump into hyperspace. There's no doubt your paths will cross a
gain one day. Before that, you'll take a day or three off to enjoy a well-deser
ved rest. How about checking in on your giant bees, for a change?
Oh wait, they're just calling all hands on deck: a Rebel squadron fell into an
ambush and is fighting their way out... You'll relax another week!
-------------------------------------------------------------------------------
Thanks for playing the eBPF Summit 2023 Capture the Flag, paste the below code
in the CTF channel on the eBPF Slack!
GJ4xMFOoFRSFES5XFFq7MFO8LKZtnJ9trJ46pvOeMKWhMJjtMz4lVSf0Zl9mZwtmBQLtp7Iwo70xp65X
The same method can be used for both easy and hard mode, I haven’t checked their differences yet.
Update: Revealed in eCHO Episode 107: eBPF Summit CTF Walkthrough. Easy mode can expose pid in system log
$ dmesg
[ 523.951058] ebpf.summit.202[3385] is installing a program with bpf_probe_write_user helper that may corrupt user memory!
While hard mode will clear system log.
I actually solved it before the event started so I can focus on summit talks. However, when approaching to the end of the event, I saw a message from host:
For glory points, has anyone cracked the code?
What? That code has some hidden meanings?
It looks like a base64 string, but decode result doesn’t make any sense.
It’s 5 AM, too tired to do cryptography analysis, so I took a shortcut, reverse engineer the challenge binary.
Load binary into my favorite RE tool Cutter,
use Ghidra
to decompile the main
function.
auVar19 = runtime.stringtoslicebyte
((int64_t)&stack0xfffffffffffffd78, auVar19._0_8_, iVar14, arg_8h, arg_10h,
in_stack_fffffffffffffca8);
auVar19 = encoding/base64.(*Encoding).EncodeToString
(_encoding/base64.StdEncoding, auVar19._0_8_, auVar19._8_8_, iVar14, arg_8h, arg_10h,
in_stack_fffffffffffffca8, arg_20h);
auVar19 = main.rot13rot5(auVar19._0_8_, auVar19._8_8_, arg_8h, arg_10h);
Ok, the flag is encoded by base64
then rot13rot5
(rot18).
To decode it, can use online decoders, or life is short, use python:
import string
import base64
rot18 = str.maketrans(
{
c: string.ascii_lowercase[i - 13] for i, c in enumerate(string.ascii_lowercase)
} | {
c: string.ascii_uppercase[i - 13] for i, c in enumerate(string.ascii_uppercase)
} | {
c: string.digits[i - 5] for i, c in enumerate(string.digits)
}
)
def decode(s):
return base64.b64decode(s.translate(rot18)).decode()
>>> decode('GJ4xMFOoFRSFES5XFFq7MFO8LKZtnJ9trJ46pvOeMKWhMJjtMz4lVSf0Zl9mZwtmBQLtp7Iwo70xp65X')
"Mode [HARD]\nI've was in your kernel for [93.328386 seconds]\n"
Knowing how it’s generated, I can craft an impossible but valid flag:
def encode(s):
return base64.b64encode(s.encode()).decode().translate(rot18)
>>> encode("Mode [HARD]\nI've was in your kernel for [0.000000 seconds]\n")
'GJ4xMFOoFRSFES5XFFq7MFO8LKZtnJ9trJ46pvOeMKWhMJjtMz4lVSfjYwNjZQNjZPOmMJAiozEmKDb='
On second thought, is there a way to achieve the impossible without cheating?
How to kill the process when it’s accessing /etc/passwd
?
It’s doable with eBPF, one handy tool is Tetragon
.
Can use a similar policy like Synchronously stopping the process.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "kill-passwd-access"
spec:
kprobes:
- call: "sys_openat"
syscall: true
args:
- index: 0
type: "int"
- index: 1
type: "string"
selectors:
- matchArgs:
- index: 1
operator: "Equal"
values:
- "/etc/passwd\0"
matchActions:
- action: Signal
argSig: 15
Start tetragon with this policy:
tetragon --tracing-policy tracing_policy.yaml
Then start the challenge:
HARDMODE=true /tmp/ebpf.summit.2023
Check the flag in /ebpf.summit
:
GJ4xMFOoFRSFES5XFFq7MFO8LKZtnJ9trJ46pvOeMKWhMJjtMz4lVSfjYwNjZQN8APOmMJAiozEmKDb=
Decode:
>>> decode('GJ4xMFOoFRSFES5XFFq7MFO8LKZtnJ9trJ46pvOeMKWhMJjtMz4lVSfjYwNjZQN8APOmMJAiozEmKDb=')
"Mode [HARD]\nI've was in your kernel for [0.000074 seconds]\n"
That’s close enough!
0x03 Additional Analysis
One thing about this challenge is that process is not visible in ps
output, otherwise it’s too easy to find and kill.
How is this achieved?
Hide a process is not a new trick, for example libprocesshider. eBPF offensive capabilities are also getting more attention, like bad-bpf or boopkit.
getdents
can be hooked to hide a process, example Pid-Hide.
To see if this is the case, check the eBPF programs:
bpftool prog show
There are some matching ones:
27: tracepoint name handle_getdents tag 5fa666123368aa3b gpl
loaded_at 2023-09-14T13:55:09+0000 uid 0
xlated 296B jited 173B memlock 4096B map_ids 37,38
btf_id 100
28: tracepoint name handle_getdents tag 3da1705dc803ad56 gpl
loaded_at 2023-09-14T13:55:09+0000 uid 0
xlated 1784B jited 1274B memlock 4096B map_ids 38,39,40,37,41,42
btf_id 105
29: tracepoint name handle_getdents tag 97c6fdabf49fbe34 gpl
loaded_at 2023-09-14T13:55:09+0000 uid 0
xlated 520B jited 295B memlock 4096B map_ids 42
btf_id 106
Inspect them to confirm:
bpftool prog dump x id 27 | grep getdents
int handle_getdents_enter(struct trace_event_raw_sys_enter * ctx):
; int handle_getdents_enter(struct trace_event_raw_sys_enter *ctx)
bpftool prog dump x id 28 | grep getdents
int handle_getdents_exit(struct trace_event_raw_sys_exit * ctx):
; int handle_getdents_exit(struct trace_event_raw_sys_exit *ctx)
bpftool prog dump x id 29 | grep getdents
int handle_getdents_patch(struct trace_event_raw_sys_exit * ctx):
It can fool ps
because ps
is using getdents
to get processes info.
But it can’t avoid detection from other eBPF tools. Or can it? Sounds familiar?
You Dare Use My Own Spells Against Me
That’s another story for another day.
0x04 Conclusion
This is a relatively easy challenge compared to previous years. Very friendly for beginners to get started with eBPF. It also contains a lot of fun elements, many thanks to organizers. Looking forward to next year’s challenge!