commit 3c9d167b0b78fdbe0b3e20b8242362742ffdec06 from: Sergey Bronnikov via: Sergey Bronnikov date: Wed Feb 08 07:47:53 2023 UTC mutator: add initial version 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 +#include +#include +#include +#include +#include +#include + +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