Skip to content

Instantly share code, notes, and snippets.

@bkerley
Last active August 29, 2024 20:58
Show Gist options
  • Save bkerley/ffb8062c29ce0669a92cddcfc98184bb to your computer and use it in GitHub Desktop.
Save bkerley/ffb8062c29ce0669a92cddcfc98184bb to your computer and use it in GitHub Desktop.

Behavior Sanitizers

Mix.install([
  {:kino_db, "~> 0.2.10"},
  {:postgrex, ">= 0.0.0"}
])

Intro

Hi, I'm Bryce, I work in computer security research at Cromulence.

This past year, I've become very familiar with the "Jazzer" sanitizers in Java.

What is a Sanitizer?

I think they're originally a native thing implemented by C-language (C and C++) compilers. Clang has a few of these that detect memory errors (that can cost the world trillions of dollars in security problems) or undefined behavior (that can allow clang developers to break your program and say it's your fault).

I'm most familiar with the Jazzer sanitizers for Java apps, that can detect SQL injection, LDAP injection, shell injection, and other actual problems in a fuzzing context.

Sanitizers in this sense trigger on unsafe behavior that makes it safer by crashing or killing a process. It's not ideal to lose availability this way! Jazzer sanitizers run in a fuzzing context and not in production, which is fine. I think the best practice for the Clang ones is to run them in prod.

Use sanitizers as a tool to find bugs and fix them and you won't bring down production.

I think it's possible to build these for Elixir in eight hours or so? It's 7:22am on Thursday Aug. 29 2024 at time of writing…

Erlang's Trace Module

%% Create a tracer process that will receive the trace events
Tracer = spawn(fun F() ->
                     receive M -> 
                         io:format("~p~n",[M]), F() end end),
%=> <0.91.0>
%% Create a session using the Tracer
Session = trace:session_create(my_session, Tracer, []),
%=> {#Ref<0.1543805153.1548353537.92331>,{my_session, 0}}
%% Setup call tracing on self()
trace:process(Session, self(), true, [call]),
%=> 1
%% Setup call tracing on lists:seq/2
trace:function(Session, {lists,seq,2}, [], []).
%=> 1
%% Call the traced function
lists:seq(1, 10).
% {trace,<0.89.0>,call,{lists,seq,[1,10]}} % The trace message
%=> [1,2,3,4,5,6,7,8,9,10] % The return value
%% Cleanup the trace session
%=> ok
trace:session_destroy(Session).

Looks like we can run the code of our choice (here, io:format) in a Tracer process on a function call in the main process.

Let's see if we can spike out something that barfs on a query to postgrex with an unquoted __sanitizer__ in the query.

Ideally, I'd use a production-level SQL parser and not just count quotes.

Simple Postgrex Query

opts = [
  hostname: "localhost",
  port: 5432,
  username: "postgres",
  password: System.fetch_env!("LB_QUALS_LIVEBOOK_PASSWORD"),
  database: "gamedb"
]

{:ok, conn} = Kino.start_child({Postgrex, opts})
result = Postgrex.query!(conn, ~S"SELECT * FROM teams", [])

Finding the Target

The goal's to sit on the "actually write query to socket" part of postgrex. It's all entangled with db_connection but reading the source, it looks like it all goes through DBConnection.prepare_execute/4.

defmodule Inspector do
  def receive_loop() do
    receive do
      mesg -> IO.inspect(mesg)
    end

    receive_loop()
  end
end

tracer = spawn(&Inspector.receive_loop/0)
session = :trace.session_create(:sesh, tracer, [])
:trace.process(session, :all, true, [:call])
:trace.function(session, {DBConnection, :prepare_execute, 4}, [], [])
result2 = Postgrex.query!(conn, ~S"SELECT * FROM teams", [])
:trace.session_destroy(session)

Sanitizing instead of Inspecting

To me, the big difference is we want to look for misbehavior and do something high-level and attention-grabbing instead of logging it to stderr.

defmodule Sanitizer do
  def receive_loop() do
    receive do
      {:trace, caller, :call,
        {DBConnection, :prepare_execute, 
        _args = [
          conn,
          %Postgrex.Query{
            statement: stmt
          },
          _params,
          _opts
        ]}
      } ->
        maybe_alert(stmt, caller, conn)
      o -> IO.inspect([other: o])
    end

    # non-local call to allow code changes?
    Sanitizer.receive_loop()
  end

  defp maybe_alert(stmt, caller, conn) do
    stmt_bin = IO.iodata_to_binary(stmt)
    cond do
      not String.contains?(stmt_bin, "__sanitizer__") -> false
      true -> 
        IO.inspect([injectable: stmt, caller: caller, conn: conn])
        # Process.exit(caller, :kill)
        Process.exit(conn, :kill)
        # System.stop("sanitizer triggered")
    end
  end
end

Killing the caller might not be what we want during LivebookConf????

Killing the BEAM might also be bad lmao

Killing the connection is a good choice because if you're fast you might stop an injection before it commits. In a non-LivebookConf context killing the caller is probably good.

:trace.session_destroy(session)
  
tracer = spawn(&Sanitizer.receive_loop/0)
session = :trace.session_create(:sesh, tracer, [])
:trace.process(session, :all, true, [:call])
:trace.function(session, {DBConnection, :prepare_execute, :_}, [], [])
result3 =
  Postgrex.query!(
    conn,
    ~S"select * from posts union select password from users where name = __sanitizer__",
    []
  )

Future Work

  • Is fuzzing in BEAM languages useful?
  • Would this be useful in research?
  • Would this be useful in production?
  • hot dog stand theme for livebook when?

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment