commit 95a4d68f1be158baa139a96a9cdf5861487fe37d from: Sergey Bronnikov via: Sergey Bronnikov date: Tue Feb 07 16:05:10 2023 UTC Initial commit commit - /dev/null commit + 95a4d68f1be158baa139a96a9cdf5861487fe37d blob - /dev/null blob + 567609b1234a9b8806c5a05da6c866e480aa148d (mode 644) --- /dev/null +++ .gitignore @@ -0,0 +1 @@ +build/ blob - /dev/null blob + 8df5ffa4c55b69e48737c9715a5fe90ce9900955 (mode 644) --- /dev/null +++ .luacheckrc @@ -0,0 +1,13 @@ +globals = { + "fuzz", +} + +include_files = { + ".luacheckrc", + "*.rockspec", + "**/*.lua", +} + +exclude_files = { + ".rocks", +} blob - /dev/null blob + 13327d6dc72933b78a2d5bb6f4cb602da0836b7c (mode 644) --- /dev/null +++ CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.10.2) + +project(afl-lua + LANGUAGES C CXX + VERSION "1.0.0" +) +find_package(Lua 5.1 REQUIRED) + +set(LUA_NAME "lua${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR}") +find_program(LUA_EXECUTABLE "${LUA_NAME}") +if(NOT EXISTS ${LUA_EXECUTABLE}) + message(FATAL_ERROR "${LUA_NAME} is required") +endif() +message(STATUS "Found Lua ${LUA_VERSION_STRING}") +message(STATUS "Found Lua interpreter ${LUA_EXECUTABLE}") + +add_subdirectory(afl-lua) + +if (NOT CMAKE_LUADIR) + set(CMAKE_LUADIR "${CMAKE_PREFIX_PATH}") +endif() + +if (NOT CMAKE_BINDIR) + set(CMAKE_BINDIR "${CMAKE_PREFIX_PATH}") +endif() + +install( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/README.md + DESTINATION ${CMAKE_LUADIR}/doc +) blob - /dev/null blob + fe00d793faecfc0af35418720cbff536b15b7e86 (mode 644) --- /dev/null +++ CMakePresets.json @@ -0,0 +1,44 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 20, + "patch": 0 + }, + "configurePresets": [ + { + "name": "default", + "displayName": "Default Config (Unix Makefile)", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": { + "type": "BOOL", + "value": "ON" + } + } + } + ], + "buildPresets": [ + { + "name": "default", + "configurePreset": "default", + "jobs": 10 + } + ], + "workflowPresets": [ + { + "name": "default", + "steps": [ + { + "type": "configure", + "name": "default" + }, + { + "type": "build", + "name": "default" + } + ] + } + ] +} blob - /dev/null blob + e076de78a92659fc4405242084e6aa085d1cc35b (mode 644) --- /dev/null +++ CONTRIBUTING.md @@ -0,0 +1,11 @@ +## Hacking + +For developing `afl-lua` you need to install Lua libraries and headers and +CMake. On Debian: `apt install -y liblua5.1-0-dev cmake`. + +```sh +$ cmake -S . -B build +$ cmake --build build --parallel +``` + +You are ready to make patches! blob - /dev/null blob + cdc1b19708c0ced47c2a83d56fe83c6db45db9d5 (mode 644) --- /dev/null +++ README.md @@ -0,0 +1,79 @@ +[![Static analysis](https://github.com/ligurio/afl-lua/actions/workflows/check.yaml/badge.svg)](https://github.com/ligurio/afl-lua/actions/workflows/check.yaml) +[![Testing](https://github.com/ligurio/afl-lua/actions/workflows/test.yaml/badge.svg)](https://github.com/ligurio/afl-lua/actions/workflows/test.yaml) +[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC) +[![Luarocks](https://img.shields.io/luarocks/v/ligurio/afl-lua/scm-1)](https://luarocks.org/modules/ligurio/afl-lua) + +# afl-lua + +AFL + Lua + +is a project that brings integration of [AFL++][aflplus-url] (American Fuzzy +Lop) with Lua programming language. It allows to perform fuzzing testing of +programs written in Lua. + +## Installation + +- Download and setup Lua interpreter and LuaRocks. +- Install AFL++ package: `sudo apt install -y afl++` (on Debian). +- Install module using LuaRocks: `luarocks --local install afl-lua`. +- Update a `PATH`: `export PATH=$PATH:$(luarocks path --lr-bin)`. + +## Usage + +Create a file with Lua program that reads a string from a STDIN: + +```sh +$ cat << EOF > example.lua +function fuzz() + local buf = io.read("*a") + local b = {} + buf:gsub(".", function(c) table.insert(b, c) end) + if b[1] == 'l' then + if b[2] == 'u' then + if b[3] == 'a' then + assert(nil) + end + end + end +end + +fuzz() +EOF +``` + +Make sure Lua script has failed when string "lua" is passed to STDIN: + +```sh +$ echo "lua" | lua example.lua +lua: example.lua:8: assertion failed! +stack traceback: + [C]: in function 'assert' + example.lua:8: in function 'fuzz' + example.lua:14: in main chunk + [C]: in ? +``` + +Execute `afl-lua` against a Lua script: + +```sh +$ mkdir -p {in,out} +$ echo -n "\0" > in/corpus +$ __AFL_SHM_ID=$RANDOM afl-fuzz -D -i in/ -o out/ afl-lua example.lua +``` + +After some time, the fuzzer will find a test case with which the program will crash: + +```sh +$ cat out/default/crashes/id\:000000\,sig\:06\,src\:000008\,time\:197253\,execs\:113636\,op\:havoc\,rep\:4 +luaiiiii^ii +``` + +## License + +- Copyright © 2022-2023 [Sergey Bronnikov](https://bronevichok.ru/) +- Copyright © 2020 Steven Johnstone + +Distributed under the ISC License. + +[aflplus-url]: https://aflplus.plus/ +[aflplus-mutators-url]: https://aflplus.plus/docs/custom_mutators/ blob - /dev/null blob + a6e04bf8991a001054f103f220814e43fc5b656a (mode 644) --- /dev/null +++ afl-lua/CMakeLists.txt @@ -0,0 +1,11 @@ +project(afl-lua) + +add_executable(${PROJECT_NAME} ${PROJECT_NAME}.c) +target_include_directories(${PROJECT_NAME} PRIVATE ${LUA_INCLUDE_DIR}) +target_link_libraries(${PROJECT_NAME} PRIVATE ${LUA_LIBRARIES}) +target_compile_options(${PROJECT_NAME} PUBLIC -Wall -Wextra -Wno-unused-parameter) + +install( + TARGETS ${PROJECT_NAME} + DESTINATION "${CMAKE_BINDIR}/" +) blob - /dev/null blob + 8d936d74d21b131d39a84f0121f652e8dcee5536 (mode 644) --- /dev/null +++ afl-lua/afl-lua.c @@ -0,0 +1,154 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define FUZZ_FUNCTION_NAME "fuzz" + +// The presence of this string is enough to allow AFL fuzz to run without +// using the env variable AFL_SKIP_BIN_CHECK. +const char *SHM_ENV = "__AFL_SHM_ID"; +const char *NOFORK = "AFL_NO_FORKSRV"; + +const int afl_read_fd = 198; +const int afl_write_fd = afl_read_fd + 1; + +static unsigned char *afl_shm; +static size_t afl_shm_size = 1 << 16; + +static int shm_init() { + const char *shm = getenv(SHM_ENV); + if (!shm) { + fprintf(stderr, "Please set %s environment variable.\n", SHM_ENV); + return -1; + } + afl_shm = shmat(atoi(shm), NULL, 0); + if (afl_shm == (void*) -1) { + fprintf(stderr, "shmat() has failed (%s).\n", strerror(errno)); + return -1; + } + return 0; +} + +static int fork_write(int pid) { + const int ok = (4 == write(afl_write_fd, &pid, 4)); + assert(ok); + return 0; +} + +static int fork_read() { + void *buf = NULL; + const int ok = (4 == read(afl_read_fd, &buf, 4)); + assert(ok); + return 0; +} + +static int fork_close() { + close(afl_read_fd); + close(afl_write_fd); + return 0; +} + +static int lua_run_target(lua_State *L) { + if (lua_pcall(L, 0, 0, 0)) { + abort(); + } + return 0; +} + +/** + * From afl-python + * https://github.com/jwilk/python-afl/blob/8df6bfefac5de78761254bf5d7724e0a52d254f5/afl.pyx#L74-L87 + */ +#define LHASH_INIT 0x811C9DC5 +#define LHASH_MAGIC_MULT 0x01000193 +#define LHASH_NEXT(x) h = ((h ^ (unsigned char)(x)) * LHASH_MAGIC_MULT) + +static inline unsigned int lhash(const char *key, size_t offset) { + const char *const last = &key[strlen(key) - 1]; + uint32_t h = LHASH_INIT; + while (key <= last) LHASH_NEXT(*key++); + for (; offset != 0; offset >>= 8) LHASH_NEXT(offset); + return h; +} + +static unsigned int current_location; + +static void debug_hook(lua_State *L, lua_Debug *ar) { + lua_getinfo(L, "Sl", ar); + if (ar && ar->source && ar->currentline) { + const unsigned int new_location = lhash(ar->source, ar->currentline) % afl_shm_size; + afl_shm[current_location ^ new_location] += 1; + current_location = new_location / 2; + } +} + +int main(int argc, const char **argv) { + if (argc == 1) { + fprintf(stderr, "Please pass arguments.\n"); + exit(1); + } + + int rc = shm_init(); + if (rc != 0) { + fprintf(stderr, "shm_init() failed.\n"); + exit(1); + } + + const char *script_path = argv[1]; + if (access(script_path, F_OK) != 0) { + fprintf(stderr, "File (%s) does not exist.\n", script_path); + exit(1); + } + + lua_State *L = luaL_newstate(); + if (L == NULL) { + fprintf(stderr, "Lua initialization failed.\n"); + exit(1); + } + luaL_openlibs(L); + lua_sethook(L, debug_hook, LUA_MASKLINE, 0); + rc = luaL_dofile(L, script_path); + if (rc != 0) { + fprintf(stderr, "luaL_dofile() has failed.\n"); + exit(1); + } + + lua_getglobal(L, FUZZ_FUNCTION_NAME); + if (lua_isfunction(L, -1) != 1) { + fprintf(stderr, "fuzz() is not a Lua function.\n"); + exit(1); + } + + if (getenv(NOFORK)) { + lua_run_target(L); + return 0; + } + + fork_write(0); // Let AFL know we're here. + + while (1) { + fork_read(); + pid_t child = fork(); + if (child == 0) { + fork_close(); + lua_run_target(L); + return 0; + } + fork_write(child); + int status = 0; + rc = wait(&status); + fork_write(status); + } + + return 0; +} blob - /dev/null blob + e7b6d9fa2a38484faed7a219a6dd03c1f654474a (mode 644) Binary files /dev/null and afl-lua.png differ