commit bdad855e9b169749f4dd4423bedf0eeaf0d72fa3 from: Sergey Bronnikov via: Sergey Bronnikov date: Tue Aug 5 19:40:03 2025 UTC luzer: initial AFL custom mutator support A custom mutator binding between Lua and AFL++, https://github.com/stevenjohnstone/lua-mutator Custom Mutators in AFL++, https://aflplus.plus/docs/custom_mutators/ commit - 97e8ac98ec05056115fe60344bce713996dd528e commit + bdad855e9b169749f4dd4423bedf0eeaf0d72fa3 blob - 1deac628552c2b4bf02c42af50aaa97d0d4c3268 blob + bf1cf4f68f03f60b88b1c953551a8cdcb58700c2 --- .luacheckrc +++ .luacheckrc @@ -13,4 +13,5 @@ include_files = { exclude_files = { ".rocks", "build/", + "luzer/afl_mutator.lua", } blob - e3d9160e1ff1ba69cbebbef5b6a716074964708b blob + 64e04fb1826ce220704de8345d8cc9079a2fe90c --- luzer/CMakeLists.txt +++ luzer/CMakeLists.txt @@ -79,6 +79,13 @@ target_compile_options(${AFL_LUA} PRIVATE ${CFLAGS} ) +# TODO: install binary +set(AFL_MUTATOR_NAME luamutator) +add_library(${AFL_MUTATOR_NAME} SHARED afl_mutator.c) +target_include_directories(${AFL_MUTATOR_NAME} PRIVATE ${LUA_INCLUDE_DIR}) +target_link_libraries(${AFL_MUTATOR_NAME} PRIVATE ${LUA_LIBRARIES}) +target_compile_options(${AFL_MUTATOR_NAME} PUBLIC -Wall -Wextra -Wno-unused-parameter) + if(ENABLE_TESTING) add_subdirectory(tests) endif() blob - /dev/null blob + 40c3d65a6eea21e7f922c3438075f3cd1579a036 (mode 644) --- /dev/null +++ luzer/afl_mutator.c @@ -0,0 +1,282 @@ +/****************************************************************************** +* 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(void) { + const char *mutator_script = getenv(mutator_env) ? "XXX" : 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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); + (void)rc; + 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 +++ luzer/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