Commit Diff


commit - 1b0b21689586a0c0832d8ec6d4dcc874e2db881b
commit + 3c9d167b0b78fdbe0b3e20b8242362742ffdec06
blob - 8df5ffa4c55b69e48737c9715a5fe90ce9900955
blob + d7dc37ae5fac41d9c691eb56d26d97d5f2e36880
--- .luacheckrc
+++ .luacheckrc
@@ -1,5 +1,19 @@
 globals = {
+    "deinit",
+    "describe",
     "fuzz",
+    "fuzz_count",
+    "havoc_mutation",
+    "havoc_mutation_probability",
+    "init",
+    "init_trim",
+    "introspection",
+    "package",
+    "post_process",
+    "post_trim",
+    "queue_get",
+    "queue_new_entry",
+    "trim",
 }
 
 include_files = {
blob - 13327d6dc72933b78a2d5bb6f4cb602da0836b7c
blob + 6b646f250f2f41ece6144ee8a5c589d507e88587
--- CMakeLists.txt
+++ CMakeLists.txt
@@ -15,6 +15,7 @@ message(STATUS "Found Lua ${LUA_VERSION_STRING}")
 message(STATUS "Found Lua interpreter ${LUA_EXECUTABLE}")
 
 add_subdirectory(afl-lua)
+add_subdirectory(mutator)
 
 if (NOT CMAKE_LUADIR)
   set(CMAKE_LUADIR "${CMAKE_PREFIX_PATH}")
blob - cdc1b19708c0ced47c2a83d56fe83c6db45db9d5
blob + 7fd95b1f4846921e3d5d2c0c05d99044f4fad0bd
--- README.md
+++ README.md
@@ -20,6 +20,8 @@ programs written in Lua.
 
 ## Usage
 
+### Fuzzing Lua source code
+
 Create a file with Lua program that reads a string from a STDIN:
 
 ```sh
@@ -68,6 +70,25 @@ $ cat out/default/crashes/id\:000000\,sig\:06\,src\:00
 luaiiiii^ii
 ```
 
+### Using custom mutators written in Lua
+
+AFL has an [API][aflplus-mutators-url] for implementing custom mutators. Custom
+mutators can be be written in Lua 5.1 (including LuaJIT), 5.2, 5.3 or 5.4.
+
+The environment variable `AFL_CUSTOM_MUTATOR_LIBRARY` must be set to the
+path to the file with shared library `libluamutator.so`.
+
+The environment variable `AFL_CUSTOM_MUTATOR_LUA_SCRIPT` could be set to the
+path with Lua mutator script. The default path is `./afl_mutator.lua`.
+
+```sh
+$ export AFL_CUSTOM_MUTATOR_LIBRARY=$(pwd)/libluamutator.so
+$ export AFL_CUSTOM_MUTATOR_LUA_SCRIPT=$(pwd)/afl_mutator.lua
+$ afl-fuzz /path/to/program
+```
+
+Lua API described in `example_afl_mutator.lua`.
+
 ## License
 
 - Copyright © 2022-2023 [Sergey Bronnikov](https://bronevichok.ru/)
blob - /dev/null
blob + cbdc4bfa1c580379684e05711b4c2c5146d7e20e (mode 644)
--- /dev/null
+++ mutator/CMakeLists.txt
@@ -0,0 +1,18 @@
+project(afl-lua-mutator)
+
+set(MUTATOR_NAME luamutator)
+add_library(${MUTATOR_NAME} SHARED afl_mutator.c)
+target_include_directories(${MUTATOR_NAME} PRIVATE ${LUA_INCLUDE_DIR})
+target_link_libraries(${MUTATOR_NAME} PRIVATE ${LUA_LIBRARIES})
+target_compile_options(${MUTATOR_NAME} PUBLIC -Wall -Wextra -Wno-unused-parameter)
+
+install(
+  TARGETS ${MUTATOR_NAME}
+  DESTINATION ${LIBDIR}
+)
+
+install(
+  FILES
+    ${CMAKE_CURRENT_SOURCE_DIR}/example_afl_mutator.lua
+  DESTINATION ${LUADIR}/${PROJECT_NAME}/
+)
blob - /dev/null
blob + 82118ee5b345baf74eddf513538b4bd61b463ece (mode 644)
--- /dev/null
+++ mutator/afl_mutator.c
@@ -0,0 +1,271 @@
+/******************************************************************************
+* Copyright (C) 2022 Sergey Bronnikov
+* Copyright (C) 2020 Steven Johnstone
+*
+* Permission is hereby granted, free of charge, to any person obtaining
+* a copy of this software and associated documentation files (the
+* "Software"), to deal in the Software without restriction, including
+* without limitation the rights to use, copy, modify, merge, publish,
+* distribute, sublicense, and/or sell copies of the Software, and to
+* permit persons to whom the Software is furnished to do so, subject to
+* the following conditions:
+*
+* The above copyright notice and this permission notice shall be
+* included in all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+******************************************************************************/
+
+#include <assert.h>
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+static const char *mutator_env = "AFL_CUSTOM_MUTATOR_LUA_SCRIPT";
+static const char *mutator_script_default = "./afl_mutator.lua";
+static const int default_havoc_mutation_probability = 6;
+
+#define METHODS                                                                \
+  X(init)                                                                      \
+  X(fuzz)                                                                      \
+  X(post_process)                                                              \
+  X(init_trim)                                                                 \
+  X(trim)                                                                      \
+  X(post_trim)                                                                 \
+  X(havoc_mutation)                                                            \
+  X(havoc_mutation_probability)                                                \
+  X(queue_get)                                                                 \
+  X(queue_new_entry)
+
+#define xstr(s) str(s)
+#define str(s) #s
+
+#define LUA_OK 0
+
+struct state {
+  lua_State *L;
+  void *trim_buf;
+#define X(name)                                                                \
+  int afl_custom_##name##_enabled;                                             \
+  const char *afl_custom_##name##_method;
+  METHODS
+#undef X
+};
+
+static struct state *new_state() {
+  const char *mutator_script = getenv(mutator_env) ?: mutator_script_default;
+  struct state *s = calloc(1, sizeof(struct state));
+  assert(s);
+  s->L = luaL_newstate();
+  assert(s->L);
+  luaL_openlibs(s->L);
+  int rc = luaL_dofile(s->L, mutator_script);
+  assert(rc == LUA_OK);
+#define X(name)                                                                \
+  {                                                                            \
+    lua_getglobal(s->L, str(name));                                            \
+    if (lua_isfunction(s->L, -1)) {                                            \
+      s->afl_custom_##name##_enabled = 1;                                      \
+      s->afl_custom_##name##_method = str(name);                               \
+    }                                                                          \
+    lua_settop(s->L, 0);                                                       \
+  }
+  METHODS
+#undef X
+  return s;
+}
+
+void *afl_custom_init(void *afl, unsigned int seed) {
+  struct state *s = new_state();
+  if (!s->afl_custom_init_enabled) {
+    return s;
+  }
+  lua_getglobal(s->L, s->afl_custom_init_method);
+  lua_pushinteger(s->L, seed);
+  const int rc = lua_pcall(s->L, 1, 0, 0);
+  assert(rc == LUA_OK);
+  lua_settop(s->L, 0);
+  return (void *)s;
+}
+
+size_t afl_custom_fuzz(void *data, char *buf, size_t buf_size, char **out_buf,
+                       char *add_buf, size_t add_buf_size, size_t max_size) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_fuzz_enabled) {
+    *out_buf = buf;
+    return buf_size;
+  }
+  lua_getglobal(s->L, s->afl_custom_fuzz_method);
+  size_t args = 2;
+  lua_pushlstring(s->L, buf, buf_size);
+  lua_pushinteger(s->L, max_size);
+  if (add_buf) {
+    lua_pushlstring(s->L, add_buf, add_buf_size);
+    args++;
+  }
+  const int rc = lua_pcall(s->L, args, 1, 0);
+  assert(rc == LUA_OK);
+  size_t rstr_len;
+  const char *rstr = lua_tolstring(s->L, -1, &rstr_len);
+  assert(rstr);
+  lua_settop(s->L, 0);
+  rstr_len = rstr_len > max_size ? max_size : rstr_len;
+  *out_buf = malloc(rstr_len);
+  assert(*out_buf);
+  memcpy(*out_buf, rstr, rstr_len);
+  return rstr_len;
+}
+
+size_t afl_custom_post_process(void *data, char *buf, size_t buf_size,
+                               char **out_buf) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_post_process_enabled) {
+    *out_buf = buf;
+    return buf_size;
+  }
+  lua_getglobal(s->L, s->afl_custom_post_process_method);
+  lua_pushlstring(s->L, buf, buf_size);
+  const int rc = lua_pcall(s->L, 1, 1, 0);
+  assert(rc == LUA_OK);
+  size_t rstr_len;
+  const char *rstr = lua_tolstring(s->L, -1, &rstr_len);
+  assert(rstr);
+  lua_settop(s->L, 0);
+  *out_buf = malloc(rstr_len);
+  assert(*out_buf);
+  memcpy(*out_buf, rstr, rstr_len);
+  return rstr_len;
+}
+
+int32_t afl_custom_init_trim(void *data, char *buf, size_t buf_size) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_init_trim_enabled) {
+    return 0;
+  }
+  lua_getglobal(s->L, s->afl_custom_init_trim_method);
+  lua_pushlstring(s->L, buf, buf_size);
+  const int rc = lua_pcall(s->L, 1, 1, 0);
+  assert(rc == LUA_OK);
+  const int rv = lua_tointeger(s->L, -1);
+  lua_settop(s->L, 0);
+  return (uint32_t)(0xffffffff & rv);
+}
+
+size_t afl_custom_trim(void *data, char **out_buf) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_trim_enabled) {
+    return 0;
+  }
+  lua_getglobal(s->L, s->afl_custom_trim_method);
+  const int rc = lua_pcall(s->L, 0, 1, 0);
+  assert(rc == LUA_OK);
+  size_t rstr_len;
+  const char *rstr = lua_tolstring(s->L, -1, &rstr_len);
+  assert(rstr);
+  if (s->trim_buf) {
+    free(s->trim_buf);
+  }
+  s->trim_buf = malloc(rstr_len);
+  assert(s->trim_buf);
+  memcpy(s->trim_buf, rstr, rstr_len);
+  lua_settop(s->L, 0);
+  *out_buf = s->trim_buf;
+  return rstr_len;
+}
+
+int32_t afl_custom_post_trim(void *data, int success) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_post_trim_enabled) {
+    return 0;
+  }
+  lua_getglobal(s->L, s->afl_custom_post_trim_method);
+  lua_pushboolean(s->L, !!success);
+  const int rc = lua_pcall(s->L, 1, 1, 0);
+  assert(rc == LUA_OK);
+  const int rv = lua_tointeger(s->L, -1);
+  lua_settop(s->L, 0);
+  return (uint32_t)(0xffffffff & rv);
+}
+
+size_t afl_custom_havoc_mutation(void *data, char *buf, size_t buf_size,
+                                 char **out_buf, size_t max_size) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_havoc_mutation_enabled) {
+    *out_buf = buf;
+    return buf_size;
+  }
+  lua_getglobal(s->L, s->afl_custom_havoc_mutation_method);
+  lua_pushlstring(s->L, buf, buf_size);
+  lua_pushinteger(s->L, max_size);
+  const int rc = lua_pcall(s->L, 2, 1, 0);
+  assert(rc == LUA_OK);
+  size_t rstr_len;
+  const char *rstr = lua_tolstring(s->L, -1, &rstr_len);
+  assert(rstr);
+  lua_settop(s->L, 0);
+  rstr_len = rstr_len > max_size ? max_size : rstr_len;
+  *out_buf = malloc(rstr_len);
+  assert(*out_buf);
+  memcpy(*out_buf, rstr, rstr_len);
+  return rstr_len;
+}
+
+uint8_t afl_custom_havoc_mutation_probability(void *data) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_havoc_mutation_enabled) {
+    return 0;
+  }
+  if (!s->afl_custom_havoc_mutation_probability_enabled) {
+    return default_havoc_mutation_probability;
+  }
+  lua_getglobal(s->L, s->afl_custom_havoc_mutation_probability_method);
+  const int rc = lua_pcall(s->L, 0, 1, 0);
+  assert(rc == LUA_OK);
+  const int rv = lua_tointeger(s->L, -1);
+  lua_settop(s->L, 0);
+  return (uint8_t)(0xff & rv);
+}
+
+uint8_t afl_custom_queue_get(void *data, const char *filename) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_queue_get_enabled) {
+    return 1;
+  }
+  lua_getglobal(s->L, s->afl_custom_queue_get_method);
+  lua_pushstring(s->L, filename);
+  const int rc = lua_pcall(s->L, 1, 1, 0);
+  assert(rc == LUA_OK);
+  const int rv = lua_toboolean(s->L, -1);
+  lua_settop(s->L, 0);
+  return (uint8_t)(0xff & rv);
+}
+
+void afl_custom_queue_new_entry(void *data, const char *filename_new_queue,
+                                const char *filename_orig_queue) {
+  struct state *s = (struct state *)data;
+  if (!s->afl_custom_queue_new_entry_enabled) {
+    return;
+  }
+  lua_getglobal(s->L, s->afl_custom_queue_new_entry_method);
+  lua_pushstring(s->L, filename_new_queue);
+  lua_pushstring(s->L, filename_orig_queue);
+  const int rc = lua_pcall(s->L, 2, 0, 0);
+  assert(rc == LUA_OK);
+  lua_settop(s->L, 0);
+  return;
+}
+
+void afl_custom_deinit(void *data) {
+  struct state *s = (struct state *)data;
+  lua_close(s->L);
+  free(s);
+}
blob - /dev/null
blob + 6ae13f691d9b126c93b70501f94a9fe6c4fbcdaf (mode 644)
--- /dev/null
+++ mutator/example_afl_mutator.lua
@@ -0,0 +1,205 @@
+--- Initialization.
+--
+-- This method is called when AFL++ starts up and is used to seed RNG and set
+-- up buffers and state.
+-- @function init
+function init()
+end
+
+--- Mutate a data buffer (optional).
+--
+-- This method performs custom mutations on a given input. It also accepts an
+-- additional test case. Note that this function is optional - but it makes
+-- sense to use it. You would only skip this if `post_process` is used to fix
+-- checksums etc. so if you are using it, e.g., as a post processing library.
+-- Note that a length > 0 must be returned!
+--
+-- @string buf
+-- @number max_size
+-- @string add_buf
+-- @return mutated_out, a modified version of buffer.
+--
+-- @function fuzz
+function fuzz(buf, max_size, add_buf)
+    print(buf, max_size, add_buf)
+end
+
+--- Fuzz count (optional).
+--
+-- When a queue entry is selected to be fuzzed, afl-fuzz selects the number of
+-- fuzzing attempts with this input based on a few factors. If, however, the
+-- custom mutator wants to set this number instead on how often it is called
+-- for a specific queue entry, use this function. This function is most useful
+-- if AFL_CUSTOM_MUTATOR_ONLY is not used.
+--
+-- @string buf
+-- @string add_buf
+-- @number max_size
+-- @return cnt
+--
+-- @function fuzz_count
+function fuzz_count(buf, max_size, add_buf)
+    print(buf, max_size, add_buf)
+end
+
+--- Describe (optional).
+--
+-- When this function is called, it shall describe the current test case,
+-- generated by the last mutation. This will be called, for example, to name
+-- the written test case file after a crash occurred. Using it can help to
+-- reproduce crashing mutations.
+--
+-- @number max_description_length
+-- @return desc, a string with description.
+--
+-- @function fuzz_count
+function describe(max_description_length)
+    print(max_description_length)
+end
+
+--- Post-processing (optional).
+--
+-- For some cases, the format of the mutated data returned from the custom
+-- mutator is not suitable to directly execute the target with this input. For
+-- example, when using libprotobuf-mutator, the data returned is in a protobuf
+-- format which corresponds to a given grammar. In order to execute the target,
+-- the protobuf data must be converted to the plain-text format expected by the
+-- target. In such scenarios, the user can define the post_process function.
+-- This function is then transforming the data into the format expected by the
+-- API before executing the target.
+--
+-- @string buf
+-- @return out_buf, a modified version of buffer.
+--
+-- @function post_process
+function post_process(buf)
+     print(buf)
+end
+
+--- Havoc mutation (optional).
+--
+-- havoc_mutation performs a single custom mutation on a given input. This
+-- mutation is stacked with other mutations in havoc. The other method,
+-- havoc_mutation_probability, returns the probability that havoc_mutation is
+-- called in havoc. By default, it is 6%.
+--
+-- @string buf
+-- @number max_size
+-- @return mutated_out, a modified version of buf.
+--
+-- @function havoc_mutation
+function havoc_mutation(buf, max_size)
+     print(buf, max_size)
+end
+
+--- Havoc mutation probability.
+--
+-- havoc_mutation performs a single custom mutation on a given input. This
+-- mutation is stacked with other mutations in havoc. The other method,
+-- havoc_mutation_probability, returns the probability that havoc_mutation is
+-- called in havoc. By default, it is 6%.
+--
+-- @return num, integer in the range [0, 100].
+--
+-- @function havoc_mutation_probability
+function havoc_mutation_probability()
+end
+
+--- Queue get (optional).
+--
+-- This method determines whether the custom fuzzer should fuzz the current
+-- queue entry or not.
+--
+-- @string filename
+-- @return rc, returns true if "filename" should be used.
+--
+-- @function queue_get
+function queue_get(filename)
+    print(filename)
+end
+
+--- Queue new entry (optional).
+--
+-- This method is called after adding a new test case to the queue. If the
+-- contents of the file was changed, return True, False otherwise.
+--
+-- @string filename_new_queue
+-- @string filename_orig_queue
+--
+-- @function queue_new_entry
+function queue_new_entry(filename_new_queue, filename_orig_queue)
+    print(filename_new_queue, filename_orig_queue)
+end
+
+--- Introspection (optional).
+--
+-- This method is called after a new queue entry, crash or timeout is
+-- discovered if compiled with INTROSPECTION. The custom mutator can then
+-- return a string (const char *) that reports the exact mutations used.
+--
+-- @return string
+--
+-- @function introspection
+function introspection()
+end
+
+--- Deinit.
+--
+-- The last method to be called, deinitializing the state.
+--
+-- @function deinit
+function deinit()
+end
+
+--- Initialization of trim (optional).
+--
+-- This method is called at the start of each trimming operation and receives
+-- the initial buffer. It should return the amount of iteration steps possible
+-- on this input (e.g., if your input has n elements and you want to remove
+-- them one by one, return n, if you do a binary search, return log(n), and so
+-- on).
+--
+-- If your trimming algorithm doesn’t allow to determine the amount of
+-- (remaining) steps easily (esp. while running), then you can alternatively
+-- return 1 here and always return 0 in post_trim until you are finished and no
+-- steps remain. In that case, returning 1 in post_trim will end the trimming
+-- routine. The whole current index/max iterations stuff is only used to show
+-- progress.
+--
+-- @string buf
+-- @return cnt, a number of steps needed for trimming process.
+--
+-- @function init_trim
+function init_trim(buf)
+    print(buf)
+end
+
+--- Trim (optional).
+--
+-- This method is called for each trimming operation. It doesn’t have any
+-- arguments because there is already the initial buffer from init_trim and we
+-- can memorize the current state in the data variables. This can also save
+-- reparsing steps for each iteration. It should return the trimmed input
+-- buffer.
+--
+-- @return out_buf, a trimmed buffer.
+--
+-- @function trim
+function trim()
+end
+
+--- Post trim (optional).
+--
+-- This method is called after each trim operation to inform you if your
+-- trimming step was successful or not (in terms of coverage). If you receive a
+-- failure here, you should reset your input to the last known good state. In
+-- any case, this method must return the next trim iteration index (from 0 to
+-- the maximum amount of steps you returned in init_trim).
+--
+-- @param success
+-- @return idx, next trim index.
+--
+-- @function post_trim
+function post_trim(success)
+     print(success)
+end