Skip to content

Commit ca2e8a1

Browse files
author
Dawin Schmidt
committed
Add Qemu deferred initialization example
1 parent 53c19a8 commit ca2e8a1

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
```

qemu_mode/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ allows to move the forkserver to a different part, e.g., just before the file is
6666
opened (e.g., way after command line parsing and config file loading, etc.)
6767
which can be a huge speed improvement.
6868

69+
For an example, see [README.deferred_initialization_example.md](README.deferred_initialization_example.md).
70+
6971
## 4) Persistent mode
7072

7173
AFL++'s QEMU mode now supports also persistent mode for x86, x86_64, arm, and

0 commit comments

Comments
 (0)