Commit Diff


commit - /dev/null
commit + 9da5762c1d4d33ded214eacd8c1c26a2bdc84531
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 + d1e735b3cb68fdc31dd5e62ae36e2556a1d76ecc (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
+
+<img src="afl-lua.png" alt="AFL + Lua" width="400"/>
+
+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 MIT 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 + 6239ad5f28cbaa4253739664fc2387c5cd297d57 (mode 644)
--- /dev/null
+++ afl-lua/afl-lua.c
@@ -0,0 +1,161 @@
+/*
+ * SPDX-License-Identifier: MIT
+ *
+ * Copyright © 2020, Steven Johnstone
+ *             2022-2023, Sergey Bronnikov
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+#include <sys/shm.h>
+#include <sys/wait.h>
+
+#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