|
| 1 | +# Fuzz ARM32 Python Native Extensions in Binary-only Mode (LLVM fork-based) |
| 2 | + |
| 3 | +This is an example on how to fuzz Python native extensions in LLVM mode with deferred initialization on ARM32. |
| 4 | + |
| 5 | +We use Ubuntu x86_64 to run AFL++ and an Alpine ARMv7 Chroot to build the fuzzing target. |
| 6 | + |
| 7 | +Check [Resources](#resources) for the code used in this example. |
| 8 | + |
| 9 | +## Setup Alpine ARM Chroot on your x86_64 Linux Host |
| 10 | + |
| 11 | +### Use systemd-nspawn |
| 12 | + |
| 13 | +1. Install `qemu-user-binfmt`, `qemu-user-static` and `systemd-container` dependencies. |
| 14 | +2. Restart the systemd-binfmt service: `systemctl restart systemd-binfmt.service` |
| 15 | +3. Download an Alpine ARM RootFS from https://alpinelinux.org/downloads/ |
| 16 | +4. Create a new `alpine_sysroot` folder and extract: `tar xfz alpine-minirootfs-3.17.1-armv7.tar.gz -C alpine_sysroot/` |
| 17 | +5. Copy `qemu-arm-static` to Alpine's RootFS: `cp $(which qemu-arm-static) ./alpine/usr/bin/` |
| 18 | +6. Chroot into the container: `sudo systemd-nspawn -D alpine/ --bind-ro=/etc/resolv.conf` |
| 19 | +7. Install dependencies: `apk update && apk add build-base musl-dev clang15 python3 python3-dev py3-pip` |
| 20 | +8. Exit the container with `exit` |
| 21 | + |
| 22 | +### Alternatively use Docker |
| 23 | + |
| 24 | +1. Install `qemu-user-binfmt` and `qemu-user-static` |
| 25 | +2. Run Qemu container: ```$ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes``` |
| 26 | +3. Run Alpine container: ```$ docker run -it --rm arm32v7/alpine sh``` |
| 27 | + |
| 28 | +## Build AFL++ Qemu Mode with ARM Support |
| 29 | + |
| 30 | +First, build AFL++ as described [here](https://github.com/AFLplusplus/AFLplusplus/blob/dev/docs/INSTALL.md). Then, run the Qemu build script: |
| 31 | + |
| 32 | +```bash |
| 33 | +cd qemu_mode && CPU_TARGET=arm ./build_qemu_support.sh |
| 34 | +``` |
| 35 | + |
| 36 | +## Compile and Build the Fuzzing Project |
| 37 | +Build the native extension and the fuzzing harness for ARM using the Alpine container (check [Resources](#resources) for the code): |
| 38 | +```bash |
| 39 | +ALPINE_ROOT=<your-alpine-sysroot-directory> |
| 40 | +FUZZ=<your-path-to-the-code> |
| 41 | +sudo systemd-nspawn -D $ALPINE_ROOT --bind=$FUZZ:/fuzz |
| 42 | +CC=$(which clang) CFLAGS="-g" LDSHARED="clang -shared" python3 -m pip install /fuzz |
| 43 | +clang $(python3-config --embed --cflags) $(python3-config --embed --ldflags) -o /fuzz/fuzz_harness.a /fuzz/fuzz_harness.c |
| 44 | +exit |
| 45 | +``` |
| 46 | + |
| 47 | +Manually trigger bug: |
| 48 | +```bash |
| 49 | +echo -n "FUZZ" | qemu-arm-static -L $ALPINE_ROOT $FUZZ/fuzz_harness.a |
| 50 | +``` |
| 51 | + |
| 52 | +## Run AFL++ |
| 53 | +Make sure to start the forkserver *after* loading all the shared objects by setting the `AFL_ENTRYPOINT` environment variable (see [here](https://aflplus.plus/docs/env_variables/#5-settings-for-afl-qemu-trace) for details): |
| 54 | + |
| 55 | +Choose an address just before the `while()` loop, for example: |
| 56 | +```bash |
| 57 | +qemu-arm-static -L $ALPINE_ROOT $ALPINE_ROOT/usr/bin/objdump -d $FUZZ/fuzz_harness.a | grep -A 1 "PyObject_GetAttrString" |
| 58 | + |
| 59 | +00000584 <PyObject_GetAttrString@plt>: |
| 60 | + 584: e28fc600 add ip, pc, #0, 12 |
| 61 | +-- |
| 62 | + 7c8: ebffff6d bl 584 <PyObject_GetAttrString@plt> |
| 63 | + 7cc: e58d0008 str r0, [sp, #8] |
| 64 | +... |
| 65 | +``` |
| 66 | +
|
| 67 | +Check Qemu memory maps using the instructions from [here](https://aflplus.plus/docs/tutorials/libxml2_tutorial/): |
| 68 | +>The binary is position independent and QEMU persistent needs the real addresses, not the offsets. Fortunately, QEMU loads PIE executables at a fixed address, 0x4000000000 for x86_64. |
| 69 | +> |
| 70 | +> We can check it using `AFL_QEMU_DEBUG_MAPS`. You don’t need this step if your binary is not PIE. |
| 71 | +
|
| 72 | +Setup Python environment variables and run `afl-qemu-trace`: |
| 73 | +```bash |
| 74 | +PYTHONPATH=$ALPINE_ROOT/usr/lib/python3.10/ PYTHONHOME=$ALPINE_ROOT/usr/bin/ QEMU_LD_PREFIX=$ALPINE_ROOT AFL_QEMU_DEBUG_MAPS=1 afl-qemu-trace $FUZZ/fuzz_harness.a |
| 75 | +
|
| 76 | +... |
| 77 | +40000000-40001000 r-xp 00000000 103:03 8002276 fuzz_harness.a |
| 78 | +40001000-4001f000 ---p 00000000 00:00 0 |
| 79 | +4001f000-40020000 r--p 0000f000 103:03 8002276 fuzz_harness.a |
| 80 | +40020000-40021000 rw-p 00010000 103:03 8002276 fuzz_harness.a |
| 81 | +40021000-40022000 ---p 00000000 00:00 0 |
| 82 | +40022000-40023000 rw-p 00000000 00:00 0 |
| 83 | +``` |
| 84 | +
|
| 85 | +Finally, setup Qemu environment variables... |
| 86 | +```bash |
| 87 | +export QEMU_SET_ENV=PYTHONPATH=$ALPINE_ROOT/usr/lib/python310.zip:$ALPINE_ROOT/usr/lib/python3.10:$ALPINE_ROOT/usr/lib/python3.10/lib-dynload:$ALPINE_ROOT/usr/lib/python3.10/site-packages,PYTHONHOME=$ALPINE_ROOT/usr/bin/ |
| 88 | +export QEMU_LD_PREFIX=$ALPINE_ROOT |
| 89 | +``` |
| 90 | +
|
| 91 | +... and run AFL++: |
| 92 | +```bash |
| 93 | +mkdir -p $FUZZ/in && echo -n "FU" > $FUZZ/in/seed |
| 94 | +AFL_ENTRYPOINT=0x400007cc afl-fuzz -i $FUZZ/in -o $FUZZ/out -Q -- $FUZZ/fuzz_harness.a |
| 95 | +``` |
| 96 | +
|
| 97 | +## Resources |
| 98 | +
|
| 99 | +### setup.py |
| 100 | +
|
| 101 | +```python |
| 102 | +from distutils.core import setup, Extension |
| 103 | +
|
| 104 | +module = Extension("memory", sources=["fuzz_target.c"]) |
| 105 | +
|
| 106 | +setup( |
| 107 | + name="memory", |
| 108 | + version="1.0", |
| 109 | + description='A simple "BOOM!" extension', |
| 110 | + ext_modules=[module], |
| 111 | +) |
| 112 | +``` |
| 113 | +
|
| 114 | +### fuzz_target.c |
| 115 | +
|
| 116 | +```c |
| 117 | +#define PY_SSIZE_T_CLEAN |
| 118 | +#include <Python.h> |
| 119 | +
|
| 120 | +#pragma clang optimize off |
| 121 | +
|
| 122 | +static PyObject *corruption(PyObject* self, PyObject* args) { |
| 123 | + char arr[3]; |
| 124 | + Py_buffer name; |
| 125 | +
|
| 126 | + if (!PyArg_ParseTuple(args, "y*", &name)) |
| 127 | + return NULL; |
| 128 | +
|
| 129 | + if (name.buf != NULL) { |
| 130 | + if (strcmp(name.buf, "FUZZ") == 0) { |
| 131 | + arr[0] = 'B'; |
| 132 | + arr[1] = 'O'; |
| 133 | + arr[2] = 'O'; |
| 134 | + arr[3] = 'M'; |
| 135 | + } |
| 136 | + } |
| 137 | +
|
| 138 | + PyBuffer_Release(&name); |
| 139 | + Py_RETURN_NONE; |
| 140 | +} |
| 141 | +
|
| 142 | +static PyMethodDef MemoryMethods[] = { |
| 143 | + {"corruption", corruption, METH_VARARGS, "BOOM!"}, |
| 144 | + {NULL, NULL, 0, NULL} |
| 145 | +}; |
| 146 | +
|
| 147 | +static struct PyModuleDef memory_module = { |
| 148 | + PyModuleDef_HEAD_INIT, |
| 149 | + "memory", |
| 150 | + "BOOM!", |
| 151 | + -1, |
| 152 | + MemoryMethods |
| 153 | +}; |
| 154 | +
|
| 155 | +PyMODINIT_FUNC PyInit_memory(void) { |
| 156 | + return PyModule_Create(&memory_module); |
| 157 | +} |
| 158 | +``` |
| 159 | +
|
| 160 | +### fuzz_harness.c |
| 161 | +
|
| 162 | +```c |
| 163 | +#include <Python.h> |
| 164 | +
|
| 165 | +#pragma clang optimize off |
| 166 | +
|
| 167 | +int main(int argc, char **argv) { |
| 168 | + unsigned char buf[1024000]; |
| 169 | + ssize_t size; |
| 170 | +
|
| 171 | + Py_Initialize(); |
| 172 | + PyObject* name = PyUnicode_DecodeFSDefault("memory"); |
| 173 | + PyObject* module = PyImport_Import(name); |
| 174 | + Py_DECREF(name); |
| 175 | +
|
| 176 | + if (module != NULL) { |
| 177 | + PyObject* corruption_func = PyObject_GetAttrString(module, "corruption"); |
| 178 | +
|
| 179 | + while ((size = read(0, buf, sizeof(buf))) > 0 ? 1 : 0) { |
| 180 | + PyObject* arg = PyBytes_FromStringAndSize((char *)buf, size); |
| 181 | +
|
| 182 | + if (arg != NULL) { |
| 183 | + PyObject* res = PyObject_CallFunctionObjArgs(corruption_func, arg, NULL); |
| 184 | +
|
| 185 | + if (res != NULL) { |
| 186 | + Py_XDECREF(res); |
| 187 | + } |
| 188 | +
|
| 189 | + Py_DECREF(arg); |
| 190 | + } |
| 191 | + } |
| 192 | +
|
| 193 | + Py_DECREF(corruption_func); |
| 194 | + Py_DECREF(module); |
| 195 | + } |
| 196 | +
|
| 197 | + // Py_Finalize() leaks memory on certain Python versions (see https://bugs.python.org/issue1635741) |
| 198 | + // Py_Finalize(); |
| 199 | + return 0; |
| 200 | +} |
| 201 | +``` |
0 commit comments