Skip to content

Commit e815968

Browse files
committed
speedy unicorn
1 parent dc5422b commit e815968

File tree

7 files changed

+205
-44
lines changed

7 files changed

+205
-44
lines changed

AFLplusplus

DEVELOPMENT.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# UCF Development
2+
3+
In this markdown file, we collect some tips about debugging and developing ucf further.
4+
5+
## Typing
6+
7+
For good measure, try to add type hints wherever you are.
8+
We currently develop with backwards compatible python3 in mind.
9+
Functions may be annotated inline `dev like(this: str) -> None:`.
10+
However to annotate types, we prefer to add type comments (PEP something) `like = this # type: str`.
11+
12+
## Debugging
13+
14+
There are different layers to be debugged.
15+
The python code and afl++/unicorn code.
16+
17+
### Python Debugging
18+
19+
Debugging the python stuff should be easy. Use your favorite pdb interface or pycharm to step thorugh the code, or simply sprinkle printfs around.
20+
21+
### Unicorn Debugging
22+
23+
Debugging bugs in unicorn is a different beast.
24+
Sometimes it's necessary to debug `libunicorn.so` with the real AFL forkserver attached.
25+
A possible best way is via GDB.
26+
27+
#### Building Unicorn With Debug Symbols
28+
29+
This is not super straight forward, but easy enough. We do it by following these steps.
30+
For this, it's adviced to build unicorn with debug symbols.
31+
Change dir to unicorn.
32+
```cd ./AFLplusplus/unicorn_mode/unicorn```
33+
There, edit `config.mk` and change `UNICORN_DEBUG` to `UNICORN_DEBUG ?= yes`.
34+
35+
Afterwards, rebuild Unicorn using something like
36+
```make clean -j8; make -j8```
37+
38+
#### Starting the debugger:
39+
40+
With libunicorn.so built with symbols, let's start ucf in a debugger.
41+
```bash
42+
UCF_DEBUG_SLEEP_BEFORE_FORK=10 UCF_DEBUG_START_GDB=1 ucf fuzz -P -c ./unicorefuzz_cifs/config.py
43+
```
44+
`UCF_DEBUG_START_GDB=1` will load ucf inside afl-fuzz inside gdb.
45+
`UCF_DEBUG_SLEEP_BEFORE_FORK=10` will add a sleep of 10 seconds right before the afl-unicorn forkserver starts. This will be important in the next step
46+
47+
### Debugging unicorn, as child of afl
48+
49+
Inside the gdb shell, make sure you follow the child AFL will spawn for the fork server:
50+
51+
```
52+
set follow-fork-mode child
53+
```
54+
55+
And break whenever afl execs python3/ucf:
56+
```
57+
catch exec
58+
```
59+
60+
Then run afl using `r`.
61+
62+
After the catchpoint triggers, make sure you don't follow random python or avatar threads:
63+
```
64+
set follow-fork-mode parent
65+
```
66+
and continue (`c`) until you see output like
67+
```
68+
[d] Sleeping. Forkserver will start in 10 seconds.
69+
```
70+
Immediately hit `ctrl+c` to break.
71+
72+
Now start setting your desired breakpoints, for example `b uc_emu_start` or `b afl_forkserver` and then continue (`c`).
73+
74+
Using `set follow-fork-mode child` again at the right time (i.e. right before the `fork()` in `afl-unicorn-cpu-inl.h`) allows debugging the actual unicorn execution.
75+
76+
Another, kinda tedious, way, to debug, is to keep the parent process around using (gdb non-stop mode)[https://www-zeuthen.desy.de/unix/unixguide/infohtml/gdb/Non_002dStop-Mode.html] and setting `set detach-on-fork off`, for example:
77+
78+
```bash
79+
gef➤ set target-async 1
80+
gef➤ set pagination off
81+
gef➤ set non-stop on
82+
gef➤ set detach-on-fork off
83+
gef➤ catch ex
84+
gef➤ catch exec
85+
gef➤ r
86+
gef➤ c -a
87+
gef➤ info threads
88+
gef➤ thread xyz
89+
...
90+
```
91+
92+
Happy debugging.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ It's generally a good idea to nop out kprintf or kernel printing functionality i
123123

124124
## Troubleshooting
125125

126-
If you got trouble running unicorefuzz, follow these rulse, worst case feel free to reach out to us, for example to @domenuk on twitter.
126+
If you got trouble running unicorefuzz, follow these rulse, worst case feel free to reach out to us, for example to @domenuk on twitter. For some notes on debugging and developing ucf and afl-unicorn further, read [DEVELOPMENT.md](./DEVELOPMENT.md)
127127

128128
### No instrumentation
129129

ucf

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ from unicorefuzz.probe_wrapper import ProbeWrapper
1616
from unicorefuzz.unicorefuzz import Unicorefuzz
1717

1818

19+
def getenv_default(envname: str, default: str) -> str:
20+
"""
21+
Returns the environment variable if set, else returns the default
22+
:param envname: name of env variable to get
23+
:param default: what to return if envname is not set
24+
:return env variable or default if not set
25+
"""
26+
env = os.getenv(envname)
27+
return env if env is not None else default
28+
29+
1930
def load_conf(args: argparse.Namespace, silent: bool = False) -> Any:
2031
"""
2132
Loads the config from args
@@ -30,6 +41,7 @@ def load_conf(args: argparse.Namespace, silent: bool = False) -> Any:
3041
def print_spec(args: argparse.Namespace) -> None:
3142
"""
3243
Outputs expected config.py spec.
44+
:param args: the arguments
3345
"""
3446
print(serialize_spec(UNICOREFUZZ_SPEC))
3547

@@ -109,28 +121,62 @@ def fuzz(args: argparse.Namespace) -> None:
109121

110122
wait_for_wrapper(args, ucf)
111123

112-
afl = os.path.join(ucf.afl_path, "afl-fuzz")
113-
ucf_main = os.path.join(ucf.config.UNICORE_PATH, "ucf")
124+
config_path = ucf.config.path
114125

126+
# Path to AFL: Should usually(tm) point to the AFLplusplus subrepo
127+
afl_path = getenv_default("AFL_PATH", ucf.afl_path)
128+
# Libunicorn_path: Unicorn allows us to switch out the native lib without reinstalling its python bindings.
129+
libunicorn_path = getenv_default("LIBUNICORN_PATH", ucf.libunicorn_path)
115130
# AFL_COMPCONV_LEVEL=2 is an awesome addition to afl-unicorn, and definitely the one you want :)
116-
# See afl++ repo
117-
env = "PATH={}:$PATH LIBUNICORN_PATH={} AFL_COMPCOV_LEVEL=2".format(
118-
ucf.afl_path, ucf.libunicorn_path
131+
# See afl++ repo for further infos
132+
afl_compcov_level = getenv_default("AFL_COMPCOV_LEVEL", "2")
133+
134+
# TODO: forward all additional parameters to AFL directly, instead.
135+
afl_timeout = getenv_default("AFL_TIMEOUT", "4000+")
136+
137+
emu_params = ""
138+
if args.trace:
139+
if not args.print_outputs:
140+
raise ValueError(
141+
"Won't accept debug option -t without -P. Slowdown without benefit."
142+
)
143+
emu_params += "-t "
144+
145+
afl = os.path.join(afl_path, "afl-fuzz")
146+
ucf_main = os.path.join(ucf.config.UNICORE_PATH, "ucf")
147+
148+
if os.getenv("UCF_DEBUG_START_GDB"):
149+
print("[d] UCF_DEBUG_START_GDB set. Starting GDB, raising AFL timeouts.")
150+
afl_timeout = "99999999+"
151+
afl = "{gdb} {afl} --args {afl}".format(gdb=ucf.config.GDB_PATH, afl=afl)
152+
153+
env = 'PATH="{}:$PATH" LIBUNICORN_PATH="{}" AFL_COMPCOV_LEVEL="{}"'.format(
154+
afl_path, libunicorn_path, afl_compcov_level
119155
)
120156
if args.print_outputs:
121157
env = "{} AFL_DEBUG_CHILD_OUTPUT=1 ".format(env)
122-
run = "{env} {afl} -U -m none -i {afl_in} -o {afl_out} -t 4000+ {mode} -- python3 {ucf_main} emu @@ || exit 1".format(
123-
env=env,
124-
afl=afl,
125-
afl_in=afl_inputs,
126-
afl_out=ucf.config.AFL_OUTPUTS,
127-
mode=mode,
128-
id=id,
129-
ucf_main=ucf_main,
158+
run = (
159+
"{env} {afl} -U -m none -i {afl_in} -o {afl_out} -t {afl_timeout} {mode} "
160+
"-- python3 {ucf_main} emu {emu_params} -c {config_path} @@ || exit 1".format(
161+
env=env,
162+
afl=afl,
163+
afl_in=afl_inputs,
164+
afl_out=ucf.config.AFL_OUTPUTS,
165+
afl_timeout=afl_timeout,
166+
mode=mode,
167+
id=id,
168+
ucf_main=ucf_main,
169+
emu_params=emu_params,
170+
config_path=config_path,
171+
)
130172
)
131173
if args.print_outputs:
132174
print("[*] Starting: ", run)
133-
os.system(run)
175+
if os.getenv("UCF_DEBUG_PRINT_COMMAND_ONLY"):
176+
print("[d] ucf: Would execute:\n")
177+
print(run)
178+
else:
179+
os.system(run)
134180

135181

136182
def kernel_setup(args: argparse.Namespace) -> None:
@@ -238,6 +284,13 @@ if __name__ == "__main__":
238284
action="store_true",
239285
help="When fuzing, print all child output (for debug)",
240286
)
287+
sub_fuzz.add_argument(
288+
"-t",
289+
"--trace",
290+
default=False,
291+
action="store_true",
292+
help="Enables debug tracing for children. Slow. Only useful with -P.",
293+
)
241294

242295
sub_await = create_subparser(subparsers, "await", wait_for_wrapper)
243296
sub_afl_path = create_subparser(subparsers, "afl-path", print_afl_path)

unicorefuzz/configspec.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
) # other types are not supported, sorry...
1717

1818
from avatar2 import Avatar, Target, X86
19-
from sh import which
19+
20+
# from sh import which
2021
from unicorn import unicorn
2122

2223
import unicorefuzz.unicorefuzz
@@ -112,22 +113,29 @@ def init_avatar_target(ucf: Unicorefuzz, avatar: Avatar) -> Target:
112113
Optional(
113114
"WORKDIR",
114115
str,
115-
os.path.join(os.getcwd(), "unicore_workdir"),
116+
lambda config: os.path.join(config.folder, "unicore_workdir"),
116117
"Path to UCF workdir to store state etc.",
117118
),
118-
Optional("GDB_PATH", str, which("gdb"), "The path GDB lives at"),
119+
Optional(
120+
"GDB_PATH", str, "gdb", "The path GDB lives at"
121+
), # which("gdb"), "The path GDB lives at"),
119122
Optional(
120123
"UNICORE_PATH",
121124
str,
122-
os.path.dirname(os.path.abspath(os.path.join(unicorefuzz.__file__, ".."))),
125+
os.path.dirname(os.path.dirname(os.path.abspath(unicorefuzz.__file__))),
123126
"Custom path of Unicore installation",
124127
),
125-
Required("AFL_INPUTS", str, "The seed directory to use for fuzzing"),
128+
Optional(
129+
"AFL_INPUTS",
130+
Union[str, None],
131+
lambda config: os.path.join(config.folder, "afl_inputs"),
132+
"The seed directory to use for fuzzing",
133+
),
126134
Optional(
127135
"AFL_OUTPUTS",
128136
Union[str, None],
129-
lambda config: os.path.join(config.UNICORE_PATH, "afl_output"),
130-
"AFL output directory to use for fuzzing (default will be inside WORKDIR)",
137+
lambda config: os.path.join(config.folder, "afl_outputs"),
138+
"AFL output directory to use for fuzzing (default will be at location of config.py)",
131139
),
132140
Optional("AFL_DICT", Union[str, None], None, "AFL dictionary to use for fuzzing"),
133141
Optional(
@@ -367,6 +375,11 @@ def load_config(path: str, silent: bool = False) -> ModuleType:
367375
"""
368376
path = os.path.abspath(path)
369377
config = import_py("unicoreconfig", path, silent=silent)
378+
# path of the actual config file
379+
config.path = os.path.abspath(path) # type: str
380+
config.filename = os.path.basename(config.path) # type: str
381+
# config.folder is the folder containing the config
382+
config.folder = os.path.dirname(config.path) # type: str
370383
apply_spec(config, UNICOREFUZZ_SPEC, silent=silent)
371384
return config
372385

unicorefuzz/harness.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
X64,
2020
uc_get_pc,
2121
uc_reg_const,
22+
uc_forkserver_init,
2223
)
23-
from unicorefuzz.x64utils import syscall_exit_hook
2424

2525

2626
def unicorn_debug_instruction(
@@ -161,24 +161,15 @@ def uc_init(
161161
# On error: map memory, add exits.
162162
uc.hook_add(UC_HOOK_MEM_UNMAPPED, unicorn_debug_mem_invalid_access, self)
163163

164-
if len(exits) > 1:
165-
# unicorn supports a single exit only (using the length param).
166-
# We'll path the binary on load if we have need to support more.
167-
if self.arch == X64:
168-
uc.hook_add(
169-
UC_HOOK_INSN,
170-
syscall_exit_hook,
171-
user_data=(exits, os._exit),
172-
arg1=UC_X86_INS_SYSCALL,
173-
)
174-
else:
175-
# TODO: (Fast) solution for X86, ARM, ...
176-
raise Exception(
177-
"Multiple exits not yet supported for arch {}".format(self.arch)
178-
)
179-
180-
# starts the afl forkserver
181-
self.uc_start_forkserver(uc)
164+
# Last chance to hook before forkserver starts (if running as afl child)
165+
debug_sleep = os.getenv("UCF_DEBUG_SLEEP_BEFORE_FORK")
166+
if debug_sleep:
167+
print(
168+
"[d] Sleeping. Forkserver will start in {} seconds.".format(debug_sleep)
169+
)
170+
time.sleep(float(debug_sleep))
171+
# starts the afl forkserver. Won't fork if afl is not around.
172+
self.uc_start_forkserver(uc, exits)
182173

183174
input_file = open(input_file, "rb") # load afl's input
184175
input = input_file.read()
@@ -268,7 +259,7 @@ def map_known_mem(self, uc: Uc):
268259
except Exception:
269260
pass
270261

271-
def uc_start_forkserver(self, uc: Uc):
262+
def uc_start_forkserver(self, uc: Uc, exits: List[int]):
272263
"""
273264
Starts the forkserver by executing an instruction on some scratch register
274265
:param uc: The unicorn to fork
@@ -298,6 +289,8 @@ def uc_start_forkserver(self, uc: Uc):
298289
uc.mem_write(scratch_addr, arch.insn_nop)
299290
uc.emu_start(scratch_addr, until=0, count=1)
300291

292+
uc_forkserver_init(uc, exits)
293+
301294
def _raise_if_reject(self, base_address: int, dump_file_name: str) -> None:
302295
"""
303296
If dump_file_name + REJECTED_ENDING exists, raises exception

unicorefuzz/unicorefuzz.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@
7878
"armbe": ARMBE,
7979
}
8080

81+
# emulate from @begin, and stop when reaching address @until
82+
def uc_forkserver_init(uc: Uc, exits: List[int]) -> None:
83+
import ctypes
84+
from unicorn import unicorn, UC_ERR_OK, UcError
85+
86+
exit_count = len(exits)
87+
unicorn._uc.uc_afl_forkserver_init(
88+
uc._uch, ctypes.c_size_t(exit_count), (ctypes.c_uint64 * exit_count)(*exits)
89+
)
8190

8291
def regs_from_unicorn(arch: Architecture) -> List[str]:
8392
"""
@@ -157,7 +166,8 @@ def wait_for_probe_wrapper(self) -> None:
157166
Blocks until the request folder gets available
158167
"""
159168
while not os.path.exists(self.requestdir):
160-
print("[.] Waiting for probewrapper to be available...")
169+
print("[*] Waiting for probewrapper to be available...")
170+
print(" ^-> UCF workdir is {}".format(self.config.WORKDIR))
161171
time.sleep(PROBE_WRAPPER_WAIT_SECS)
162172

163173
def calculate_exits(self, entry: int) -> List[int]:

0 commit comments

Comments
 (0)