You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: plaid-2021/sos/README.md
+53-1Lines changed: 53 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -68,8 +68,60 @@ Blissfully unaware of both unintended solutions, we tried the following approach
68
68
* Abusing `Digest.file`, which wasn't banned and allowed us to compute MD5 of an arbitrary file. Later, it turned out that another team actually came up with an [ingenious solution](http://eternal.red/2021/secure-ocaml-sandbox/) using `Digest.file`, but we failed to extract anything useful out of this primitive.
69
69
* Using the `OO` module, which in particular has a tempting `new_method` function that is marked as `[...] for system use only. Do not call directly.`. In fact, [the implementation](https://github.com/ocaml/ocaml/blob/4.10/stdlib/camlinternalOO.ml#L70) doesn't create any methods and consists of boring string manipulations.
70
70
* Leaking the flag through `Lexing.position`, which describes `a point in a source file` and has a `pos_fname` field, which references a file. This also proved to be a dead-end, since `Lexing` doesn't do anything interesting with `pos_fname`.
71
-
* Exploiting [unsoundness](https://github.com/ocaml/ocaml/issues/9391) in `Ephemeron`. This seemed quite promising, since we were able to reliably segfault the sample program from the issue description. However, we didn't explore it further, because...
71
+
* Exploiting [unsoundness](https://github.com/ocaml/ocaml/issues/9391) in `Ephemeron`. This seemed quite promising, since we were able to reliably segfault the sample program from the issue description. However, we didn't explore it further, because at this moment...
72
72
73
73
74
74
It all comes together
75
75
---
76
+
...we hit [the jackpot](https://github.com/ocaml/ocaml/blob/4.10/stdlib/callback.mli#L23): `Callback.register`. It stores an arbitrary value (typically a function) under a certain `name`. The OCaml C runtime can then retrieve and use the value via `caml_named_value(name)`. Crucially, it's on the programmer to
77
+
ensure that all values and function signatures use the correct types. Type mismatches result in undefined behavior and spectacular segfaults, which is exactly what we need for our exploit.
78
+
79
+
Looking at usages of `caml_named_value()` in the OCaml runtime, we found a perfect match:
80
+
*[`Printexc.handle_uncaught_exception`](https://github.com/ocaml/ocaml/blob/4.10/runtime/printexc.c#L143) allows us to register a handler for an unhandled exception. The handler receives a pointer to the uncaught exception as its first parameter.
81
+
*[`Pervasives.array_bound_error`](https://github.com/ocaml/ocaml/blob/4.10/runtime/fail_nat.c#L192) allows us to override the singleton object the runtime uses to represent the exception that is raised whenever we overstep array bounds.
82
+
83
+
Combining these two, we craft us a type confusion primitive: register an object of type `A` as `Pervasive.array_bound_error`, then use it as an object of type `B ref` in the exception handler for out-of-bounds accesses. Here's a quick demo with `A = float, B = int`:
84
+
```ocaml
85
+
let oob () = "".[1]
86
+
87
+
let y = 1.5E-323;;
88
+
let g (x: int ref) _ = print_endline (string_of_int !x);;
Both `y` and `!x` have the same bit representation, but different types and hence different values:
96
+
97
+
98
+
Notice that even though the bit pattern was `00...011`, `g` prints `1` instead of the more expected `3`. Turns out that OCaml unboxes integers for performance and stores them [`shifted left by 1 bit, with the least significant bit set to 1`](https://dev.realworldocaml.org/runtime-memory-layout.html#table20-1_ocaml) to distinguish them from object references. This is going to be somewhat important for our exploit.
99
+
100
+
With all the necessary machinery in place, the idea of the exploit is straightforward:
101
+
* a function call is essentially dereferencing a pointer;
102
+
* we obtain a pointer to one of the benign, boring functions from the stdlib, e.g. `do_at_exit`;
103
+
* reinterpret the function pointer as an integer and add a statically known offset to make the pointer point at an evil 𝔽𝕆ℝ𝔹𝕀𝔻𝔻𝔼ℕ function, e.g. `open_in`;
104
+
* convert the integer back to a function pointer by registering a second callback;
105
+
* use `open_in` to open and read the flag.
106
+
107
+
The same thing, but in a picture:
108
+
109
+
110
+
And finally, the code, which is not that different from the demo above:
111
+
```ocaml
112
+
let print_flag do_open _ = print_endline (input_line (do_open "/flag"))
The only catch is that the difference between `do_at_exit` and `open_in` is `2832` bytes, but we have to use half of that in the exploit (remember the way integers are stored in OCaml?).
126
+
127
+
All in all, this challenge was surprisingly exciting and elegant (if a little undertested). I'm generally wary of "escape the sandbox" tasks, but this one managed to have just the right amount of `pwn` and the right amount of sandbox. Kudos to the creators!
0 commit comments